Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue实例挂载的实现 #12

Open
daodaolee opened this issue Feb 24, 2021 · 0 comments
Open

Vue实例挂载的实现 #12

daodaolee opened this issue Feb 24, 2021 · 0 comments

Comments

@daodaolee
Copy link
Owner

本篇说一下 Vue 的实例挂载,也就是 vm.$mount 都做了什么事情。

打开 src/platforms/web/entry-runtime-with-compiler.js 可以看到有一个 Vue.prototype.$mount 方法:

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

可以看到 Vue.prototype.$mount 赋值给了 mount 变量进行缓存,然后又重新定义了 Vue.prototype.$mount 这个方法,最开始的 Vue.prototype.$mount 是已经定义之后的,可以在 src/platforms/web/runtime/index.js 中看到它的定义:

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

那为什么又重新定义了一遍呢,是因为 Vue 有 Runtime-Complier 版本和 Runtime-Only 版本:

Runtime-Only是编译阶段运行,也就是使用 webpack 的 vue-loader,把 .vue 文件编译成JavaScript 使用。

Runtime-Complier是通过页面内的 template 编译成 render 函数,最终渲染到页面上。

最开始的 Vue.prototype.$mount 是给 Runtime-Only 版本使用的,所以在使用 Runtime-Complier 版本的时候,需要把它给重写。

还记得 Vue在初始化的时候有一个 vm.$mount(vm.$options.el) 么:

// src/core/instance/init.js
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

这个里面的 vm.$mount(vm.$options.el) 实际上就是调用的重写之后的 $mount 函数。来看下这个函数都做了事情:

首先对传入的 el 参数进行处理,它可以是一个 String,也可以是一个 Element ,之后调用了 query 方法,看下这个 query 方法做了什么事情:

export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

它调用了原生方法 document.querySelecto 来获取传入的 el,如果 el 是一个字符串,就调用这个原生方法获取 dom,如果找不到就返回一个空的 div,如果 el 是个 dom 对象,就直接返回这个 dom 对象。此时返回的 el 一定是一个 dom 对象。

接着,拿到这个 el 以后,判断 el 是不是 body 或者文档标签,如果是,就报一个错,说不可以把 Vue 挂载到 <html><body> 上。

因为它是会覆盖的,如果可以挂在到 <html> 或者 <body>上的话,就会把整个 body 给替换掉! 所以我们一般使用一个 id 为 app 的方式去使用它

然后拿到 options,紧接着有一句 if (!options.render),意思是判断有没有定义 render 方法,接着判断有没有 template,以下写法定义一个 template 是可以的:

new Vue({
  el: "#app",
  template: ``,
  data(){
    return{
      name: "abc"
    }
  }
})

继续看它的源码逻辑,如果 template 是一个字符串,就对它做一点处理,如果是 template.nodeType 也就是一个dom对象的话,就 innerHTML, 否则就会走一个 getOuterHTML 方法:

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

getOuterHTML 判断传入的 el 有没有 outerHTML 方法,没有就把 el 外面包一层 div,然后 innerHTML此时的 template 最终是一个字符串。

接着开始进行编译阶段,判断有没有 template,大致就是拿到一个 complieToFunctionsrender 函数,和一个 staticRenderFns 函数,并且赋值。

整体过一遍这个 $mount 做了事情:

首先对 el 进行一个解析,然后看看有没有 render 方法,没有的话就转化成一个 template,然后这个 template 最终通过编译成一个 render 方法。

即 Vue 只认 render 函数,如果有 render 函数,就直接 return mount.call(this, el, hyrating),return出去,如果没有 render 函数,就通过一系列操作,把 template 转化编译成 render 函数。

此刻, render 函数一定存在,然后 return 的 mount.call(this, el, hyrating) 中的 mount 就是之前缓存的 mount,也就是:

const mount = Vue.prototype.$mount

中的 mount,然后进行最开始的 Vue.prototype.$mount 方法:

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

接着进行 mountComponent 方法,定义是在:src/core/instance/lifecycle.js 中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

现在开始分析这个 mountComponent 做了什么事情:

首先把 el 缓存给 vm.$el,然后判断有没有 render 函数,如果没有或者没有将 template 正常转为 render,就定义一个 createEmptyVNode,一个虚拟dom。接着判断在开发环境下的 template 的第一个不是 #,就报一个错。

简单说就是开发过程使用 Runtime-Only 版本的 Vue,然后使用了 template,但是没有使用 render 函数,就会报一个错:

You are using the runtime-only build of Vue where the templatecompiler is not available. Either pre-compile the templates intorender functions, or use the compiler-included build.

或者使用了 Runtime-Complier 版本的Vue, 没有写 template ,或者没有写 render 函数,就会报一个错:

Failed to mount component: template or render function not defined.

这个错应该很熟悉吧。就是没有正确的 render 函数,所以报这个错,Vue 最终只认 render 函数。

接着,定义了一个 updateComponent,有关 markperformance 的判定先忽略,它是一些性能埋点的校验,一般情况下直接走最后:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

调用了 vm._update 方法,第一个参数是通过 render 渲染出来一个 VNode,第二个参数是一个服务端渲染的参数,先忽略,默认为false。

紧接着后面,调用了一个 new Watcher 函数,它是一个 渲染watcher,记住这个点,一般在写代码的时候,watch被用来监听一些东西,所以这个 new Watcher 是一个和监听有关的强相关的一个类,也就是一个 观察者模式。代码中可以有很多自定义watcher,内部逻辑会有一个 渲染watcher。来看下这个 渲染watcher是干嘛的,在 src/core/observer/watcher.js 里,一个特别大的 watcher 定义:

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }

  /**
   * Evaluate and return the value of the watcher.
   * This only gets called for computed property watchers.
   */
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

  /**
   * Depend on this watcher. Only for computed property watchers.
   */
  depend () {
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

看下传进去的参数都有哪些:vmexpOrFncboptionisRenderWatcher

在上面,传入的参数有:

	vm,  // vm实例
    
  updateComponent,  // vm._update方法
    
  noop,  // 一个空函数
    
  { //一个生命周期函数
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, 
    
  true // 是不是一个渲染watcher

接着,判断 isRenderWatcher 是不是 true,也就是说,传进来的是不是一个 渲染watcher,如果是,就在 vm 下添加一个 _watcher,然后把所有东西都 push 这个 _watcher 里面。options 的判定先忽略,后面,定义了一个 expression,如果是在开发环境就 expOrFn.toString()

后面,判断 expOrFn 是不是一个函数,如果是,就赋值给 getter,否则调用 parsePath 然后赋值给 getter。后面的 this.computed 是有关计算属性的设置,先忽略。到 value = this.getter.call(vm, vm) 这一步,这句会把刚才赋值的 this.getter 调用,也就是刚才传入的 updateComponent 被调用执行,也就是 vm._update(vm._render(), hydrating) 会执行。vm._updatevm._render 就是 最终挂载到真实dom 的函数。

首先执行 vm._render,还记得么,上面的 render 最终生成了一个 VNode,然后调用 _update,把 VNode 传进去。

至此,Vue实例就挂载好了。

总体来捋一遍:

Vue 实例挂载是通过 vm.prototype.$mount 实现的,先获取 templatetemplate 的情况大致分为三种:

  1. 直接写 template

    if (typeof template === 'string') {
      if (template.charAt(0) === '#') {
        template = idToTemplate(template)
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && !template) {
          warn(
            `Template element not found or is empty: ${options.template}`,
            this
          )
        }
      }
    } 
  2. template 是一个dom

    if (template.nodeType) {
      template = template.innerHTML
    }
  3. 以及不写 template,通过 el 去获取 template

    if (el) {
      template = getOuterHTML(el)
    }
    
    function getOuterHTML (el: Element): string {
      if (el.outerHTML) {
        return el.outerHTML
      } else {
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
      }
    }

接着把 template 通过一堆操作转化成 render 函数,然后调用 mountComponent 方法,里面定义了 updateComponent 方法:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

然后将 updateComponent 扔到 渲染watcher(new Watcher) 里面,从而挂载成功!

updateComponent 函数其实是执行了一次真实的渲染,渲染过程除了首次的 _render_update,在之后更新数据的时候,还是会触发这个 渲染watcher(new Watcher),再次执行 updateComponent,它是一个监听到执行的过程,当数据发生变化,在修改的时候,入口也是 updateComponent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant