浅曦Vue源码-37-挂载阶段-$mount-(25)渲染watcher(1)

501 阅读7分钟

「这是我参与2022首次更文挑战的第41天,活动详情查看:2022首次更文挑战

一、前情回顾 & 背景

本篇小作文讲述了 Vue 处理自定义组件产生 VNode 的方法 createComponent;这里最需要理解的就是子组件不是由 Vue 直接创建的,而是由 Vue 的子类创建的;

我们日常的开发,以写 .vue 文件为例,在 script 部分,我们导出的其实就是一个选项对象,但是即便写多个组件,他们之间互不影响,这就是因为多个组件是多个实例实例之间是天然隔离的。既然说是实例,那么就是由构造函数创建,这个构造函数就是通过咱们写的这个选项对象扩展得来的子类

还有一个重点就是每个组件都会有自己的 hook 对象,这个对象后四个方法 initprepatchinsertdestroy 四个钩子方法,其中 init 负责子组件实例的初始化,这个过程相当于 new Vue 的全流程,包含子组件的模板编译、渲染函数的创建过程;

今天我们聊下一个主题——渲染 watcher,本篇的重点将放在渲染 watcherVue 中对模板中的数据进行依赖收集。

二、Vue.prototype.$mount

我们讨论的这个版本是包含编译器的版本,所以 Vue.prototype.$mount 是被重写过,在开启挂载前加入了编译获取 render 函数的逻辑,这部分逻辑在不包含编译的版本中是没有的,这一点需要注意一下;

// 缓存原有的 $mount,这个 $mount 不包含编译模板为渲染函数的逻辑
const mount = Vue.prototype.$mount

// 重写 $mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  if (!options.render) {
   
    if (template) {
    
      const { render, staticRenderFns } = compileToFunctions(template, {..}, this)

      // 将两个渲染函数放到 vm.$options 上,
      // 即 this.$options,给 Vue.prototype._render 方法用啊
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 执行挂载,这里将作为今天的重点
  // mount 是被重载之前的 Vue.prototype.$mount
  return mount.call(this, el, hydrating)
}

三、mount 方法

从上面的代码可以看出,mount 方法是被重写之前的 Vue.prototype.$mount

方法位置:src/platforms/web/runtime/index.js -> Vue.prototype.$mount

方法参数:

  1. el,页面中的挂载点元素 div#app
  2. hydrating:忽略他吧

方法作用:调用 mountComponent 创建渲染 watcher,期间会执行前面得到的 render 函数,最终实现元素渲染到页面的能力;

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 -> function mountComponent

方法参数:

  1. vm, Component,
  2. el, 页面中挂载元素 div#app
  3. hydrating, 忽略它

方法作用:

  1. 触发 beforeMount 生命周期钩子;
  2. 声明 updateComponent 方法,updateComponent 是用于传递给渲染 watcher 的方法,在该方法内调用 vm._render 方法得到虚拟 DOM,并传递给 vm._update 方法进入 patch 阶段;当渲染 watcher 依赖的数据发生变化时,这个 updateComponent 同样会被调用,进而更新视图;
  3. 用上一步得到的 updateComponent 方法创建渲染 watcher 实例,渲染 watcher 也是前面数据响应式时用到的 Watcher 一样,都是 Watcher 的实例,只不过他的 expOrFn 是负责渲染的 updateComponent 方法;
  4. 触发手动挂载是的 mounted 生命周期钩子;
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') { // 没有 render 函数抛出警告  }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
 
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // ...
  } else {
    // updateComponent 是核心
    updateComponent = () => {
      // 执行 vm._render() 函数,得到虚拟 VNode,
      // 并将 VNode 传递给 vm._update 方法,接下来就该到 patch 阶段了
      vm._update(vm._render(), hydrating)
    }
  }


  // 这个玩意儿就是渲染 watcher
  // 在初始化渲染 watcher 的时候,Watcher 类的构造函数会判断,
  // 如果是渲染 watcher 就把 watcher 挂载到 vm 实例上:vm._watcher 
  // 之所以要这么处理,是因为渲染 watcher 的初始化可能
  // 会调用时可能会调用 $forceUpdate 方法,
  // 比如在某个子组件的 mounted 生命周期钩子中,
  // 此时就依赖 vm._watcher 已经被定义过
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* true 就是标识这个 watcher 是渲染 watcher */)
  hydrating = false

  // 手动挂载实例调用 vm 上的 mounted 钩子
  // 由渲染函数创建的子组件的 mounted 钩子将会在组件的 inserted hook 中调用
  // inserted hook 就是前面 createComponent 的 installComponentHooks
  // 时给组件的 data 增加的 hook 对象,其中有 init、prepatch、insert、destroy 钩子
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

3.1 updateCompnent

updateComponent 是个中间方法,后面作为创建渲染 watcherexpOrFn,这个参数是 watcher 求值的时候要执行的方法;对于渲染 watcher 来说,求值就是将虚拟 DOM 挂载到页面上的操作了。

let updateComponent
// ....
updateComponent = () => {
  // 执行 vm._render() 函数,得到虚拟 VNode,并将 
  // VNode 传递给 vm._update 方法,接下来就该到 patch 阶段了
  vm._update(vm._render(), hydrating)
}

3.2 创建渲染 watcher

new Watcher(
  vm, 
  updateComponent,
  noop,
  {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  },
  true /* 渲染 watcher 标识符 */
)

3.3 再看 Watcher

export default class Watcher {
  // ...
  constructor (
    vm: Component, // 对应上面传入的 vm
    expOrFn: string | Function, // 上面的 updateComponent
    cb: Function, // noop 忽略,是个空函数
    options?: ?Object, // 对应上面的 { before () {...}  } 对象
    isRenderWatcher?: boolean // 是否渲染watcher,对应上面的 true
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      // 如果是渲染 watcher 就在该渲染 watcher 挂载到 vm 上
      // 前面的 mountComponent 说过是给子组件中调用 vm.$forceUpdate 时准备的
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      // 把传进来的 before 方法挂载到当前 watcher 实例上
      this.before = options.before
    } else {
      // ...
    }
  
    if (typeof expOrFn === 'function') {
      // 注意这个 expOrFn 就是 updateComponent 方法了
      this.getter = expOrFn
    } else {
      // ....
    }
    
    // 对 watcher 进行求值,就会触发上面的 expOrFn,
    // 对于渲染 watcher 来说就是 updateComponent
    // this.lazy 我们只在计算属性中见过 true,一般为false
    this.value = this.lazy 
      ? undefined
      : this.get() // get 方法在下面,就是调用 this.getter 方法
  }


  // 执行 this.getter,也就是 updateComponent 方法
  // this.getter 是实例化 watcher 时传的 updateComponent
  // 收集模板中的依赖
  get () {
    // 打开 Dep.target,Dep.target = this,this 就是 watcher 实例啊
    // 这个地方再下面的收集依赖的时候用
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行回调函数,比如 updateComponent,进入 patch 阶段
      value = this.getter.call(vm, vm)
    } catch (e) {
     
    } finally {
      if (this.deep) {
        traverse(value)
      }
      // 关闭 Dep.target, Dep.target = null
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
  //....
}

3.3.1 watcher.get & 依赖收集

前面我们在说数据响应式的时候也讲过 Watcher 了,但是这里为啥还要重新讲?这是因为两者侧重不同,这里要说的是渲染 watcher

它的求值过程是要收集模板中的依赖,讲的是模板中的依赖的数据,比如用 :xx/v-bind:xx/{{ xx }} 是如何被收集到的一个过程;这一步骤,对于讲清楚数据更新后为啥页面更新的问题至关重要。

首先大致梳理一下调用流程:

new Watcher 
  -> 执行 Watcher 构造函数 
       -> Watcher 构造函数中 this.getter = updateComponent 
             -> this.get 
                 -> this.getter 执行,等价于 updateComponent

  • updateComponent 就做了一件简单的事情,调用 vm._render() 得到 VNode,然后传递给 vm._update() 方法,这个操作就进入到 patch 阶段了,所谓 patch 就是更新(这里是新建)v-dom 树,并且挂载到页面上
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

那么 vm._render() 中就会调用我们前面九牛二虎得到的 render 函数。。。。。

当然距离 render 函数调用还需要时间,我们先说说依赖收集,这个工作算是前置了;

我们找一段渲染函数代码看看:

// 处理 <span 
//       v-for="item in someArr" 
//       :key="index">{{item}}</span>
// 这是个示例
function () {
  return with (this) {
      _l( 
      (someArr), // someArr 是 data 上的数据
      function (item) {
        return _c(
          'span',
          { key:index },
          [
            _v(_s(item))
          ]
         )
      }
    )
  }
}

这个 render 函数调用时绑定的 this 就是 Vue 的实例,所以这个 with this 里面的 someArr 是访问 Vue 实例 this.someArr,而 this.someArr 来自数据响应式的时候将 data 中的 someArr 数组处理成可观测对象后代理到 this 上的。

还记得之前我们把 data 中的数据都变成了可观测对象,即 getter 和 setter,此时访问数据就会触发 someArrgetter 了,大致代码如下;

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // 创建 Dep 实例,这个下面的 Dep 上的 depend 会用到
  const dep = new Dep()
 
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // get 拦截对 obj[key] 的读取操作
    get: function reactiveGetter () {
     
      const value = getter ? getter.call(obj) : val
    
      if (Dep.target) {
        // 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
        dep.depend()
      }
      return value
    },

    // set 拦截对 obj[key] 的设置操作
    set: function reactiveSetter (newVal) {
     
    }
  })
}

3.3.2 dep.depend & 依赖收集

有了上面的 Wather.prototype.get 中的 pushTarget(this) 结合 Object.defineProperty 中的 dep.depend,再看 Dep.prototype.depend 就清晰多了;

export default class Dep {
  // Dep.target 是 pushTarget(watcher) 是赋值的,值就是 watcher
  // 向 watcher 中增加 dep
  // 把观察者放到被观察者里面去
  depend () {
    if (Dep.target) {
      // Dep.target.addDep 就是 
      // Watcher.prototype.addDep 只不过 this 是 Dep 的实例
      // 哪里来的呢?是 defineProperty 里 const dep = new Dep() 创建的
      Dep.target.addDep(this)
    }
  }

  // 在 dep 中增加 watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
}

3.3.3 wathcer.addDep & 依赖收集

通过上面的 defineReactive方法可以看出,每个定义为响应式的数据都有一个 dep实例,然后通过 ep.depend执行,ep.denpend又调用 dep.target.addDep也就是 Wather.prototype.addDep

addDep 方法做的就是:

  1. 把这个数据的 dep 添加到 watcher.newDeps 数组中;
  2. 通过 dep.push 把这个 watcher 添加到 dep.subs 中;
export default class Watcher {
  // ...
 
  // 两件事:
  // 1. 添加 dep 给自己 watcher
  // 2. 添加自己 (watcher) 到 dep
  addDep (dep: Dep) {
    // 判重,如果 dep 已经存在则不重复添加
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // 缓存 dep.id 用于判重
      this.newDepIds.add(id)
      this.newDeps.push(dep)

      // 避免在 dep 中重复添加 watcher,
      // this.depIds 的设置在 cleanupDeps 方法中
      if (!this.depIds.has(id)) {
        // 添加 watcher 自己到 dep 中
        dep.addSub(this)
      }
    }
  }
}

四、总结

本篇小作文的核心是 mount 方法,而 mount 方法的核心是 mountComponent 方法,该方法创建了渲染 watcher,关于渲染 watcher 我们复习了 Watcher、Dep 类,并分析了模板中的依赖收集过程:

  1. 首先数据响应式初始化的时候把 data.someArr 通过 defineReactive 变成了响应式数据结构,即变成了 getter/setter,最后把 someArr 代理到 vm 上 ;
  2. 我们上面举了一个例子,即渲染函数引用了一个 someArr 的数据进行列表渲染。执行 render 函数渲染 watcher 执行了 pushTarget(渲染watcher),Dep.target 就是这个渲染 watcher 了。
  3. 然后要列表渲染就需要访问 someArr 的值,进而触发了 someArrgetter
  4. getter 判断有 Dep.target 的值,然后将 dep 放到 watcher.newDeps 数组中,而 dep 则将 渲染 watcher 放到 dep.subs 数组中。
  5. 如果数据发生变化,dep 就负责告诉渲染 watcher,数据变了,调用 watcher.update 方法重新求值,当然这是后话;