模板tamplate
经过parse
,optimize
,generate
等一些列操作之后,把AST
转为render function code
进而生成虚拟VNode
,模板编译阶段基本已经完成了,那么这一章,我们来探讨一下Vue
中的一个算法策略–dom diff
首先来介绍下什么叫dom diff
我们经过前面的章节学习已经知道,要知道渲染真实DOM
的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染
到真实dom上会引起整个dom树
的重绘
和重排
,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?
为了解决这个问题,我们的解决方案是–根据真实DOM
生成一颗virtual DOM
,当virtual DOM
某个节点的数据改变后会生成一个新的Vnode
,然后Vnode和oldVnode
作对比,发现有不一样的地方就直接修改在真实的DOM
上,然后使oldVnode
的值为Vnode
。这也就是我们所说的一个虚拟dom diff
的过程
传统的Diff算法所耗费的时间复杂度为O(n^3)
,那么这个O(n^3)
是怎么算出来的?
O(n)
O(n^2)
O(n^3)
到这里那么n个节点与n个节点暴力对比就对比完了,那么就开启第三轮可编辑树节点遍历,更改之后的树由vdom(old)
到vdom(new)
故而传统diff算法O(n^3)
是这么算出来的,但是这不是我们今天研究的重点。
现代diff算法
策略说的是,同层级比较,广度优先
那么这里的话我们要深入源码了,在深入源码之前我们在心中应该形成这样一个概念,整个diff的流程是什么?我们再对比着源码解读
我们在Vue初始化的时候调用lifecycleMixin
函数的时候,会给Vue
的原型上挂载_update
方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
//会调用声明周期中的beforeUpdate回调函数
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
//若组件本身的vnode未生成,直接用传入的vnode生成dom
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null
} else {
//对新旧vnode进行diff
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
我们在这里可以看到vm.$el = vm.__patch__
方法,追根溯源_patch_
的定义:
Vue.prototype.__patch__ = inBrowser ? patch : noop
可见这里是一个浏览器环境的鉴别,如果在浏览器环境中,我们会执行patch,不在的话会执行noop,这是一个util里面的一个方法,用来跨平台的,我们这里暂时不考虑,接着我们去看patch的具体实现./patch
文件,参考vue实战视频讲解:进入学习
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'
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({
nodeOps, modules })
/** * 创建patch方法 */
export function createPatchFunction (backend) {
let i, j
const cbs = {
}
const {
modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {
}, [], undefined, elm)
}
/** * 创建一个回调方法, 用于删除节点 * * */
function createRmCb (childElm, listeners) {
function remove () {
if (--remove.listeners === 0) {
removeNode(childElm)
}
}
remove.listeners = listeners
return remove
}
function removeNode (el) {
const parent = nodeOps.parentNode(el)
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
/** * 通过vnode的tag判断是否是原生dom标签或者组件标签 * 用于创建真实DOM节点时, 预先判断tag的合法性 */
function isUnknownElement (vnode, inVPre) {
return (
!inVPre &&
!vnode.ns &&
!(
config.ignoredElements.length &&
config.ignoredElements.some(ignore => {
return isRegExp(ignore)
? ignore.test(vnode.tag)
: ignore === vnode.tag
})
) &&
config.isUnknownElement(vnode.tag)
)
}
let creatingElmInVPre = 0
// 创建一个节点
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 节点已经被渲染, 需要使用一个克隆节点
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 创建组件节点 详见本文件中的createComponent方法
vnode.isRootInsert = !nested // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
/** * 如果要创建的节点有tag属性, 这里做一下校验 * 如果该节点上面有v-pre指令, 直接给flag加1 * 如果没有v-pre需要调用isUnknownElement判断标签是否合法, 然后给出警告 */
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++
}
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// in Weex, the default insertion order is parent-first.
// List items can be optimized to use children-first insertion
// with append="tree".
const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
if (!appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
createChildren(vnode, children, insertedVnodeQueue)
if (appendAsTree) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
/** * 创建组件 * 如果组件实例已经存在, 只需要初始化组件并重新激活组件即可 */
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */, parentElm, refElm)
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
/** * 初始化组件 * 主要的操作是已插入的vnode队列, 触发create钩子, 设置style的scope, 注册ref */
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
vnode.data.pendingInsert = null
}
vnode.elm = vnode.componentInstance.$el
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
setScope(vnode)
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode)
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode)
}
}
/** * 激活组件 */
function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i
// hack for #4339: a reactivated component with inner transition
// does not trigger because the inner node's created hooks are not called
// again. It's not ideal to involve module-specific logic in here but
// there doesn't seem to be a better way to do it.
let innerNode = vnode
while (innerNode.componentInstance) {
innerNode = innerNode.componentInstance._vnode
if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
for (i = 0; i < cbs.activate.length; ++i) {
cbs.activate[i](emptyNode, innerNode)
}
insertedVnodeQueue.push(innerNode)
break
}
}
// unlike a newly created component,
// a reactivated keep-alive component doesn't insert itself
insert(parentElm, vnode.elm, refElm)
}
/** * 插入节点, 有父节点的插入到前面, 没有的插入到后面 */
function insert (parent, elm, ref) {
if (isDef(parent)) {
if