Vue2.6源码(2):$mount方法干了啥

2022-09-27 14:13:25 浏览数 (1)

笔者在上一篇文章中提到过如下内容:

mount方法执行过程中,会想办法把vue实例所控制的组件等内容转化成DOM并挂载到mount方法的参数所指向的DOM节点上 杨艺韬,公众号:杨艺韬的网络日志浅析Vue初始化过程(基于Vue2.6)

当时受限于篇幅,并未分析$mount方法内的执行流程,需要告诉大家的是。$mount方法内部执行的过程依然非常复杂,难以在一篇文章中详述,所以本文依然只会分析$mount方法的主体流程,至于内部的各个分支逻辑,笔者将在后续的文章中一一进行解析。从宏观到微观,从抽象到具体,是Vue源码分析系列文章的分析方式。

我们先来看看源码中,$mount方法都干了些什么:

代码语言:javascript复制
//src/platform/web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

让我们来看一看 mountComponent(this, el, hydrating)内部如何运行的:

代码语言:javascript复制
//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 && !vm._isDestroyed) {
        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方法一共做了几件事情:

  1. 校验this.$options上面的render函数是否存在,如果不存在则赋默认值,同时在开发环境下进行检测报警告。
  2. 调用方法callHook(vm, 'beforeMount')
  3. 初始化方法updateComponent,updateComponent方法内部调用了vm._update(vm._render(), hydrating)
  4. 初始化Watcher,并把updateComponent方法传入
  5. 调用callHook(vm,'mounted')

本文先忽略第2和第5件事情,后续会有专门的分析Vue生命周期的文章。同时我们把第4件事情简化成“调用updateComponent”即可,具体Watcher的内容,后续会有专门的关于Vue的响应式原理的文章进行分析。那好,我们整理下思路,现在我们可以简单的认为,mountComponent方法只做了一件事:执行方法vm._update(vm._render(), hydrating)。

大家会发现,vm又有了个_update方法,vm什么时候有的这个方法呢?大家还记不记得我在上一篇文章中为某个代码片段写了如下注释:

代码语言:javascript复制
//src/core/instance/index.js
/**
 * Vue.prototype._update
 * Vue.prototype.$forceUpdate
 * Vue.prototype.$destroy
 */
lifecycleMixin(Vue)

上面这里的lifecycleMixin(Vue)执行后,Vue.prototype上就多了个_update方法

那我们来看看_update方法里面做了些上面事情:

代码语言:javascript复制
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

本文的前提是基于Vue的首次初始化,所以我们可以认为上面这段代码只做了一件事情:

代码语言:javascript复制
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

上面这个__patch__方法中的vnode参数是什么,是上文中_update方法传入的一个VNode,那这个VNode是从哪里来的呢?是vm.render方法执行后返回的Node。这里的__patch__方法,实际上就是对把vm所控制的Vnode转化成DOM并替换vm.$el所指向的DOM。至于render方法的执行过程,以及__patch__方法内部是如果将VNode转化成DOM,如何进行替换,我将会在后续的文章中逐步进行分析,敬请期待。

欢迎关注github,内有笔者不断完善的源码注释:

https://github.com/creator-yangyitao/vue2.6-source-code-analyse

0 人点赞