浅曦Vue源码-38-挂载阶段-$mount-(26)渲染watcher(2)

580 阅读3分钟

一、前情回顾 & 背景

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

那么本篇接着说渲染 watcher,本篇的侧重点在于如何将 VNode 通过 patch 挂载到页面上,其中涉及到的重点方法就是 vm._rendervm._update 这两个 Vue 原型对象上的方法;

Vue 里面所谓 patch 是指将 VNode 变成真实 DOM 渲染到页面的过程,这个过程既包括首次的初始化渲染,也包括初次渲染后由于响应式数据更新需要更新视图的过程;

二、回顾创建渲染 watcher 过程

2.1 mountComponent

mountComponent 创建渲染 watcher,创建渲染 watcher;

export function mountComponent (): Component {


  let updateComponent
  
  if (....) {

  } else {
    // 传递给 Watcher 构造函数的第二个参数
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
   
  // 渲染 watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
 
  return vm
}

2.2 Watcher 类

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function, // 上面 mountComponent 方法传入的 updateComponent
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // expOrFn 是 
    // updateComponent = () => vm._update(vm._render(), hy...)
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }

    this.value = this.lazy
      ? undefined
      : this.get()
  }
  
  get () {
    try {
      // 执行回调函数,比如 updateComponent,进入 patch 阶段
      value = this.getter.call(vm, vm)
    } catch (e) {
    }
    return value
  } 
}

通过这里发现 new Watcher 会调用 this.get() 进而调用 updateComponent 方法,其方法调用 vm._render() 得到 VNode,并传递给 vm._update 进入到 patch 阶段,接下来我们看看 vm._rendervm._update

let updateComponent = () => vm._update(vm._render(), hydrating)

2.3 Vue.prototype._render 方法

方法位置:src/core/instance/render.js -> function renderMix -> Vue.prototype._render

方法参数:无

方法作用:通过执行 render 函数生成 VNode,并添加错误处理逻辑,下面的代码总忽略掉了错误处理逻辑;生成的 VNode 将会传递给 vm._update 方法;

export function renderMixin (Vue: Class<Component>) {
  // 渲染函数帮助函数的注册,_l/_t/_v/_s...
  installRenderHelpers(Vue.prototype)


  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    
    // 设置父 vnode,这使得渲染函数可以访问占位符节点上的数据
    vm.$vnode = _parentVnode
   
    // 开始执行 render 函数了 
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      // 执行 render 函数,生成 vnode;这里没必要维护一个栈,
      // 这是因为每个 render 函数都会被单独调用,
      // 嵌套组件的 render 函数在父组件被 patch 后调用
      
      currentRenderingInstance = vm
      // render 是个 with (this) {} 语句,
      // vm._renderProxy 就是 this 了,
      // 是渲染函数中引用的数据源头,生产环境下就是 vm 自己
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      // ...
    } finally {
      currentRenderingInstance = null
    }
   
    // 如果返回的 vnode 是数组且只有一项,直接返回这一项 vnode
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

Vue.prototype._update 方法

方法位置:src/core/instance/lifecycle.js -> function linfecycleMixin -> Vue.prototype._update

方法参数:vnodeVNode 实例,也是大家常说的虚拟 DOM 树;

方法作用:负责更新页面,包括页面的首次渲染和数据发生变更后更新页面的入口,也是 patch 的入口位置

export function lifecycleMixin (Vue: Class<Component>) {

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this

    // 页面的挂载点,真实元素
    const prevEl = vm.$el

    // 旧的 VNode
    // 什么时候有旧的 VNode 呢?
    // 当然是数据发生变化需要更新时,初次渲染的 VNode 对于更新时得到的 VNode 旧的 VNode
    // 通过 prevVnode 暂存旧的 VNode,这有啥用?DOM diff 就是比较这新旧两颗数据
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)

    // 新的 VNode
    vm._vnode = vnode


    // Vue.prototype.__patch__ 是在 Vue 入口模块注入的
    if (!prevVnode) {
      // 旧的 VNode 不存在,说明是首次渲染即始化渲染

      // 首次渲染,即初始化页面时走这里
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 响应式数据更新带来的更新视图的过程就是个 else
      // Vue.prototype.__patch__ 是在入口模块注入的
      vm.$el = vm.__patch__(prevVnode, vnode) 
    }

    restoreActiveInstance()
    
    // 更新 __vue__ 的引用
    if (prevEl) {
      // 解除旧的 VNode 对 vm 的引用
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    
    // if parent is an HOC, update its $el as well
    // 如果父节点是一个高阶组件,还要更新它的 $el
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    
    // updated 生命周期钩子将会在 scheduler 调用,
    // 以保证子节点的 updated 在父节点的 updated 调用过程中被调用
  }
}

三、Vue.prototype.patch

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

import { patch } from './patch'
// 在 Vue 原型上安装 web 平台的 patch 函数,
// Vue 有跨平台的能力 weex,所以这里强调了 web 平台
Vue.prototype.__patch__ = inBrowser ? patch : noop

3.1 patch

上面被赋值给 Vue.prototype.__patch__patch 方法是下面的 createPatchFunction 工厂函数的返回结果;

// patch 工厂函数,为其传入平台特有的一些操作,然后返回一个 patch 函数
export const patch: Function = createPatchFunction({ nodeOps, modules })

3.2 createPatchFunction

方法位置:src/core/vdom/patch.js -> function createPatchFunction

方法参数:

  1. backend,这是个配置对象,在 web 平台有两个属性: { nodeOps, modules }
    • 1.1 nodeOps, 来自 src/platforms/web/runtime/node-ops.js,其中包含了 createElement、appendChild、insertBefore 等封装过的浏览器 DOM api,这些都是操作真实 DOM 的,后面用到的时候再说;
    • 1.2 modules, 平台特有能力的封装,modules 是个数组,数组的每一项都是一个包含 create/update/destroy 方法(或其中的某几个)的对象;这些方法在 patch 时被调用,从而实现对应的能力,这些能力包含创建、更新、移除。这其中包含了 attrs、ref、directive、klass(class)、style、events 的创建、更新和移除等;

方法作用:

export function createPatchFunction (backend) {
  // .... 这里面很多的内部工具方法
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
     // 这个作为返回值的函数就是 patch 方法,即 vm.__patch__ 
  }
}

四、总结

本篇小作文深入讨论了渲染 watcher 求值调用 updateComponent 方法中对 vm._rendervm._update 的调用:

  1. vm._renderVue.prototype._render 是用于调用前面 parse & generate 后得到的渲染函数,即 vm.$options.render 得到 VNode,所谓 VNode 就是传说中的虚拟 DOM 树,描述节点间的关系;

  2. vm._updateVue.prototype._update 接收上一步得到的虚拟 DOM,将其渲染到页面,变成真实 DOM,也就是 vm.__patch__ 方法的工作;

  3. 紧接着我们溯源了 vm.__patch__Vue.prototype.__patch__ 方法的过程,它是由 createPatchFunction 这个工厂返回的方法;vm.__patch__ 负责初次渲染和响应式数据更新后的更新渲染工作;