关于mountComponent函数

93 阅读5分钟

mountComponent代码:src/core/instance/lifecycle.js

第一部分的工作

// 核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,🚀
// 在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    // 判断实例上是否存在渲染函数,如果不存在,则设置一个默认的渲染函数createEmptyVNode,该渲染函数会创建一个注释类型的VNode节点。
    vm.$options.render = createEmptyVNode
    /* istanbul ignore if */
  }
  // 生命周期函数🚀该钩子函数触发后标志着正式开始执行挂载操作
  callHook(vm, 'beforeMount')

  let updateComponent // 先_render生成虚拟DOM,再_update生成真实DOM
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // 如果调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中,这样就完成了挂载操作的一半工作
    updateComponent = () => {
      console.log('这部分删掉了');
    }
  } else {
    updateComponent = () => {
      //核心🚀
      /**
       * 首先执行渲染函数vm._render()得到一份最新的VNode节点树,返回一个VNode树
       * 然后执行vm._update()方法对最新的VNode节点树与上一次渲染的旧VNode节点树进行对比并更新DOM节点(即patch操作),完成一次渲染
       * 目的: 将最新的模板内容渲染到视图页面中,这样就完成了挂载操作的一半工作,即图中的上半部分:
       */
      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// 设置 vm._isMounted 为 true🚀, 表示这个实例已经挂载了,同时执行 mounted 钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}

调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中,这样就完成了挂载操作的一半工作,即图中的上半部分:

image.png

第二部分的工作

这是因为在挂载阶段不但要将模板渲染到视图中,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。

image.png

接下来创建了一个Watcher实例,并将定义好的updateComponent函数传入。要想开启对模板中数据(状态)的监控,这一段代码是关键

new Watcher(
    vm,                    // 第一个参数
    updateComponent,       // 第二个参数
    noop,                  // 第三个参数
    {                      // 第四个参数
        before () {
          if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')
          }
        }
	},
    true                    // 第五个参数
)

可以看到,在创建Watcher实例的时候,传入的第二个参数是updateComponent函数。

回顾一下我们在数据侦测篇文章中介绍Watcher类的时候,Watcher类构造函数的第二个参数支持两种类型:函数和数据路径(如a.b.c)。如果是数据路径,会根据路径去读取这个数据;如果是函数,会执行这个函数。一旦读取了数据或者执行了函数,就会触发数据或者函数内数据的getter方法,而在getter方法中会将watcher实例添加到该数据的依赖列表中,当该数据发生变化时就会通知依赖列表中所有的依赖,依赖接收到通知后就会调用第四个参数回调函数去更新视图。

换句话说,上面代码中把updateComponent函数作为第二个参数传给Watcher类从而创建了watcher实例,那么updateComponent函数中读取的所有数据都将被watcher所监控,这些数据中只要有任何一个发生了变化,那么watcher都将会得到通知,从而会去调用第四个参数回调函数cb去更新视图,如此反复,直到实例被销毁。

这样就完成了挂载阶段的另一半工作。

如此之后,挂载阶段才算是全部完成了,接下来调用挂载完成的生命周期钩子函数mounted


Object数据的变化侦测篇的联系

Watcher类的具体实现如下:

export default class Watcher {
  constructor (vm,expOrFn,cb) { // expOrFn可以传入updateComponent函数
    this.vm = vm; //实例
    this.cb = cb;// 更新的时候执行的回调函数
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get () { //该函数创建的时候就会执行
    window.target = this;//将当前实例置为全局的一个target的点
    const vm = this.vm
    let value = this.getter.call(vm, vm) //执行getter函数,触发updateComponent函数
    window.target = undefined; //将target置为undefined
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue) // 更新的时候执行传入的cb回调函数
  }
}

下面我们分析Watcher类的代码实现逻辑:

  1. 当实例化Watcher类时,会先执行其构造函数;
  2. 在构造函数中调用了this.get()实例方法;
  3. get()方法中,首先通过window.target = this把实例vm(也就是this)自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据(updateComponent函数)
  4. 🌟获取被依赖数据的目的是触发该数据上面的getter,上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
  5. 🌟而当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖(即watcher实例)的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数(cb参数),从而更新视图。

简单总结一下就是:Watcher先把自己设置到全局唯一的指定位置(window.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。为了便于理解,我们画出了其关系流程图,如下图:

image.png