一、前情回顾 & 背景
上一篇小作文的核心是 mount 方法,而 mount 方法的核心是 mountComponent 方法,该方法创建了渲染 watcher,关于渲染 watcher 我们复习了 Watcher、Dep 类,并分析了模板中的依赖收集过程;
那么本篇接着说渲染 watcher,本篇的侧重点在于如何将 VNode 通过 patch 挂载到页面上,其中涉及到的重点方法就是 vm._render 、vm._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._render 和 vm._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
方法参数:vnode,VNode 实例,也是大家常说的虚拟 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
方法参数:
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._render 和 vm._update 的调用:
-
vm._render即Vue.prototype._render是用于调用前面parse & generate后得到的渲染函数,即vm.$options.render得到VNode,所谓VNode就是传说中的虚拟 DOM树,描述节点间的关系; -
vm._update即Vue.prototype._update接收上一步得到的虚拟 DOM,将其渲染到页面,变成真实 DOM,也就是vm.__patch__方法的工作; -
紧接着我们溯源了
vm.__patch__即Vue.prototype.__patch__方法的过程,它是由createPatchFunction这个工厂返回的方法;vm.__patch__负责初次渲染和响应式数据更新后的更新渲染工作;