前言
本文比较长,主要篇幅都是在介绍 diff 算法,在了解 diff 算法之前,有必要了解一下 Vue 为什么需要 diff 算法,以及 Vue 在什么时刻会使用到 diff 算法,我们先来看看这两个问题。
Vue 为什么需要 diff 算法?
diff 算法是 虚拟 DOM 的必然产物,通过对新旧虚拟 DOM 的比对,将变化的地方更新到真实 DOM 上,可以尽可能减少不必要的 DOM 操作,提升性能。
前文 介绍响应式原理说到,一个组件对应一个 Watcher
实例,当状态被修改时,相关的 Watcher
实例会被通知更新,此时对应的组件都会重新 render 并生成新的 vnode
,一个组件有大有小,总会有不需要更新的地方,若整个组件都进行 DOM 更新,对性能影响将是极大的,因此需要使用 diff 算法,通过新旧 vnode
比对,从而找到实际需要更新的结点,再进行更新,这个过程称为 patch。
在 Vue 内部,组件的挂载、更新、移除都是经过这个 patch
的阶段,介绍 patch 之前先来了解一下 vue 会在什么时刻执行 patch。
执行 patch 的时刻
Vue.prototype.$mount
执行 new Vue
时传入 el 参数,内部会调用 $mount
,或直接通过 new Vue().$mount('#app')
挂载组件,Vue.prototype.$mount()
实际上是调用了 mountComponent
// core/stance/init.js
Vue.prototype._init = function (options) {
// ...
const vm = this;
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
// platforms/web/runtime/index.js
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
复制代码
mountComponent
从函数名字就知道这个函数是用于挂载组件的,执行这个函数时做了以下几件事
- 调用生命周期函数
beforeMount
- 定义一个函数
updateComponent
,这个函数内部会执行vm._update
,而vm._update
的参数是vm._render()
执行后的结果,即生成新的 vnode - 创建一个
Watcher
实例,并将updateComponent
作为参数,在组件挂载和更新时会调用这个函数
下面是 mountComponent
和 Watcher
涉及到的主要代码
function mountComponent (vm, el) {
callHook(vm, 'beforeMount')
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
}
// src/core/observer/watcher.js
export default class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.getter = expOrFn // updateComponent 作为 expOrFn 参数保存在 getter 属性
this.value = this.lazy ? undefined : this.get() // 挂载时会调用一次
}
get () {
// ...
value = this.getter.call(vm, vm)
// ...
}
// 通知更新,会调用 getter
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this) // 异步队列更新的方式,最后也会调用到 run,此处不再赘述
}
}
run () {
// ...
const value = this.get()
}
}
复制代码
vm._update
现在我们知道组件挂载和更新都会调用 _update
方法,它接收重新 render 生成的 vnode
作为参数,在内部可以获取到当前的 vnode (preVnode),然后分下面两种情况进行处理
- 没有 preVnode:当前没有旧 vnode,是挂载阶段,调用
__patch__
传入 DOM 元素$el
和新的vnode
- 有 preVnode:代表当前是更新阶段,调用
__patch__
传入preVnode
和新的vnode
两种情况都调用同一个函数,只是传入的参数不一样,因此在 __patch__
内部还需要判断分情况处理
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
}
复制代码
vm.__patch__
vm.__patch__
也是原型对象上的方法,定义如下
在浏览器才需要 patch,服务端渲染没有 DOM 操作和挂载的概念,不需要 patch
// src/platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
复制代码
继续查找,patch 函数是调用 createPatchFunction
返回的
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码
createPatchFunction
接收两个参数,这里大概了解一下分别是什么
nodeOps
是web平台下一些 DOM API 的封装,在 patch 阶段,自然少不了要操作DOMmodules
是一些核心的模块,如 attr, class, style, event, directive, ref 等,这些模块对外暴露 create, update, remove, destory 等API,在 patch 阶段会被调用
createPatchFunction
代码很多,声明了很多函数,在 patch 的各个阶段方便调用,可见 patch 逻辑是很复杂的,但是这里我们只关注 diff 算法的实现,其他就不过多介绍了,下面来看 patch
函数的定义
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) { }
}
复制代码
patch
接收 oldVnode
和 vnode
,然后根据两个参数的值分不同的情况进行处理,终于要探究 patch 的实现原理了,接下来就来逐步分析代码,了解 vue 如何实现 diff 算法。
patch 阶段工作原理
patch
方法的代码很多,但基本上只是条件判断,再执行不同的操作,diff 算法的核心代码主要是当新旧 vnode 是对应相同元素时,继续比较它们的子结点及如何实现高效的对比,因此这里只需了解一下大概的流程即可,下面是 patch
方法执行的流程图