在这篇文章深入源码学习Vue响应式原理讲解了当数据更改时,Vue是如何通知订阅者进行更新的,这篇文章讲得就是:视图知道了依赖的数据的更改,如何将新的数据反映在视图上。
Vnode Tree
在真实的HTML中有DOM树与之对应,在Vue中也有类似的Vnode Tree与之对应。
抽象DOM树
在jquery时代,实现一个功能,往往是直接对DOM进行操作来达到改变视图的目的。但是我们知道直接操作DOM往往会影响重绘和重排,这两个是最影响性能的两个元素。
进入Virtual DOM时代以后,将真实的DOM树抽象成了由js对象构成的抽象树。virtual DOM就是对真实DOM的抽象,用属性来描述真实DOM的各种特性。当virtual DOM发生改变时,就去修改视图。在Vue中就是Vnode Tree的概念
VNode
当修改某条数据的时候,这时候js会将整个DOM Tree进行替换,这种操作是相当消耗性能的。所以在Vue中引入了Vnode的概念:Vnode是对真实DOM节点的模拟,可以对Vnode Tree进行增加节点、删除节点和修改节点操作。这些过程都只需要操作VNode Tree,不需要操作真实的DOM,大大的提升了性能。修改之后使用diff算法计算出修改的最小单位,在将这些小单位的视图进行更新。
// core/vdom/vnode.js
class Vnode {
constructor(tag, data, children, text, elm, context, componentOptions) {
// ...
}
}

生成vnode
生成vnode有两种情况:
- 创建非组件节点的
vnodetag不存在,创建空节点、注释、文本节点- 使用
vue内部列出的元素类型的vnode - 没有列出的创建元素类型的
vnode
以<p>123</p>为例,会被生成两个vnode:
tag为p,但是没有text值的节点- 另一个是没有
tag类型,但是有text值的节点
- 创建组件节点的
VNode组件节点生成的Vnode,不会和DOM Tree的节点一一对应,只存在VNode Tree中这里创建一个组件占位// core/vdom/create-component function createComponent() { // ... const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children } ) }vnode,也就不会有真实的DOM节点与之对应
组件vnode的建立,结合下面例子进行讲解:
<!--parent.vue-->
<div classs="parent">
<child></child>
</div>
<!--child.vue-->
<template>
<div class="child"></div>
</template>
真实渲染出来的DOM Tree是不会存在child这个标签的。child.vue是一个子组件,在Vue中会给这个组件创建一个占位的vnode,这个vnode在最终的DOM Tree不会与DOM节点一一对应,即只会出现vnode Tree中。
/* core/vdom/create-component.js */
export function createComponent () {
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children }
)
}
那最后生成的Vnode Tree就大概如下:
vue-component-${cid}-parent
vue-component-${cid}-child
div.child
最后生成的DOM结构为:
<div class="parent">
<div class="child"></div>
</div>
在两个组件文件中打印自身,可以看出两者之间的关系
chlid实例对象

parent实例对象

- 父
vnode通过children指向子vnode - 子
vnode通过$parent指向父vnode - 占位
vnode为实例的$vnode - 渲染的
vnode为对象的_vnode
patch
在上一篇文章提到当创建Vue实例的时候,会执行以下代码:
updateComponent = () => {
const vnode = vm._render();
vm._update(vnode)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
例如当data中定义了一个变量a,并且模板中也使用了它,那么这里生成的Watcher就会加入到a的订阅者列表中。当a发生改变时,对应的订阅者收到变动信息,这时候就会触发Watcher的update方法,实际update最后调用的就是在这里声明的updateComponent。
当数据发生改变时会触发回调函数updateComponent,updateComponent是对patch过程的封装。patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例。
// core/vdom/patch.js
function createPatchFunction(backend) {
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]])
}
}
}
return function patch(oldVnode, vnode) {
if (isUndef(oldVnode)) {
let isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
oldVnode = emptyNodeAt(oldVnode)
}
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveC ? null : parentELm,
nodeOps.nextSibling(oldElm)
)
if (isDef(vnode.parent)) {
let ancestor = vnode.parent;
while(ancestor) {
ancestor.elm = vnode.elm;
ancestor = ancestor.parent
}
if (isPatchable(vnode)) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode.parent)
}
}
}
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue)
return vode.elm
}
}
- 如果是首次
patch,就创建一个新的节点 - 老节点存在
- 老节点不是真实
DOM并且和新节点相似- 调用
patchVnode修改现有节点
- 调用
- 新老节点不相同
- 如果老节点是真实
DOM,创建对应的vnode节点 - 为新的
Vnode创建元素/组件实例,若parentElm存在,则插入到父元素上 - 如果组件根节点被替换,遍历更新父节点
elm - 然后移除老节点
- 如果老节点是真实
- 老节点不是真实
- 调用
insert钩子- 是首次
patch并且vnode.parent存在,设置vnode.parent.data.pendingInsert = queue - 如果不满足上面条件则对每个
vnode调用insert钩子
- 是首次
- 返回
vnode.elm真实DOM内容nodeOps上封装了针对各种平台对于DOM的操作,modules表示各种模块,这些模块都提供了create和update钩子,用于创建完成和更新完成后处理对应的模块;有些模块还提供了activate、remove、destory等钩子。经过处理后cbs的最终结构为:
cbs = {
create: [
attrs.create,
events.create
// ...
]
}
可以看到的是只有当oldVnode和vnode满足sameVnode的时候,并且新vnode都是vnode节点,不是真实的DOM节点。 其他情况要么创建,要么进行删除。
当下面情况时出现时就会出现根节点被替换的情况:
<!-- parent.vue -->
<template>
<child></child>
</template>
<!-- child.vue -->
<template>
<div class="child">
child
</div>
</template>
这个时候parent生成的vnode.elm就是div.child的内容。
patch函数最后返回了经过一系列处理的vnode.elm也就是真实的DOM内容。
createElm
createElm的目的创建VNode节点的vnode.elm。不同类型的VNode,其vnode.elm创建过程也不一样。对于组件占位VNode,会调用createComponent来创建组件占位VNode的组件实例;对于非组件占位VNode会创建对应的DOM节点。
现在有三种节点:
- 元素类型的
VNode:- 创建
vnode对应的DOM元素节点vnode.elm - 设置
vnode的scope - 调用
createChildren遍历子节点创建对应的DOM节点 - 执行
create钩子函数 - 将
DOM元素插入到父元素中
- 创建
- 注释和本文节点
- 创建注释/文本节点
vnode.elm,并插入到父元素中
- 创建注释/文本节点
- 组件节点:调用
createComponent
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
// 创建一个组件节点
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data;
const childre = vnode.children;
const tag = vnode.tag;
// ...
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
createChildren(vnode, children, insertedVnodeQueue)
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
} else {
vnode.elm = nodeOps.createTextNode(vnode.te)
}
insert(parentElm, vnode.elm, refElm)
}
createComponent的主要作用是在于创建组件占位Vnode的组件实例, 初始化组件,并且重新激活组件。在重新激活组件中使用insert方法操作DOM。createChildren用于创建子节点,如果子节点是数组,则遍历执行createElm方法,如果子节点的text属性有数据,则使用nodeOps.appendChild()在真实DOM中插入文本内容。insert用将元素插入到真实DOM中。
// core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
// ...
let i = vnode.data.hook.init
i(vnode, false, parentElm, refElm)
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
return true;
}
}
function initComponent(vnode, insertedVnodeQueue) {
/* 把之前的已经存在的Vnode队列合并进去 */
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
}
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
// 调用create钩子
invokeCreateHooks(vnode, insertedVnodeQueue);
// 为scoped css设置scoped id
setScope(vnode)
} else {
// 注册ref
registerRef(vnode);
insertedVnodeQueue.push(vnode)
}
}
- 执行
init钩子生成componentInstance组件实例 - 调用
initComponent初始化组件- 把之前已经存在的
vnode队列进行合并 - 获取到组件实例的
DOM根元素节点,赋给vnode.elm - 如果
vnode是可patch- 调用
create函数,设置scope
- 调用
- 如果不可
patch- 注册组件的
ref,把组件占位vnode加入insertedVnodeQueue
- 注册组件的
- 把之前已经存在的
- 将
vnode.elm插入到DOM Tree中 在createComponent中,首先获取 在组件创建过程中会调用core/vdom/create-component中的createComponent,这个函数会创建一个组件VNode,然后会再vnode上创建声明各个声明周期函数,init就是其中的一个周期,他会为vnode创建componentInstance属性,这里componentInstance表示继承Vue的一个实例。在进行new vnodeComponentOptions.Ctor(options)的时候就会重新创建一个vue实例,也就会重新把各个生命周期执行一遍如created-->mounted。
init (vnode) {
// 创建子组件实例
const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
chid.$mount(undefined)
}
function createComponentInstanceForVnode(vn) {
// ... options的定义
return new vnodeComponentOptions.Ctor(options)
}
这样child就表示一个Vue实例,在实例创建的过程中,会执行各种初始化操作, 例如调用各个生命周期。然后调用$mount,实际上会调用mountComponent函数。
// core/instance/lifecycle
function mountComponent(vm, el) {
// ...
updateComponent = () => {
vm._update(vm._render())
}
vm._watcher = new Watcher(vm, updateComponent, noop)
}
在这里就会执行vm._render
// core/instance/render.js
Vue.propotype._render = function () {
// ...
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
可以看到的时候调用_render函数,最后生成了一个vnode。然后调用vm._update进而调用vm.__patch__生成组件的DOM Tree,但是不会把DOM Tree插入到父元素上,如果子组件中还有子组件,就会创建子孙组件的实例,创建子孙组件的DOM Tree。当调用insert(parentElm, vnode.elm, refElm)才会将当前的DOM Tree插入到父元素中。
在回到patch函数,当不是第一次渲染的时候,就会执行到另外的逻辑,然后oldVnode是否为真实的DOM,如果不是,并且新老VNode不相同,就执行patchVnode。
// core/vdom/patch.js
function sameVnode(a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType
)
}
sameVnode就是用于判断两个vnode是否是同一个节点。
insertedVnodeQueue的作用
在当前patch过程中,有一个数组insertedVnodeQueue,这是干嘛的,从单词上来看就是对这个队列中的vnode调用inserted钩子。在patch函数中最后调用了invokeInserthook
function invokeInsertHook(vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
当不是首次patch并且vnode.parent不存在的时候,就会对insertedVnodeQueue中vnode进行遍历,依次调用inserted钩子。
那什么时候对insertedVnodeQueue进行修改的呢。
function createElm() {
// ...
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
if (isDef(tag)) {
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
}
}
function initComponent(vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
}
if (isPatchable) {
invokeCreateHooks(vnode, insertedVnodeQueue)
} else {
insertedVnodeQueue.push(vnode)
}
}
function invokeCreateHooks(vnode, insertedVnodeQueue) {
// ...
insertedVnodeQueue.push(vnode);
}
在源码中可以看到在createElm中对组件节点和非组件节点都对insertedVnodeQueue进行了操作,每创建一个组件节点或非组件节点的时候就会往insertedVnodeQueue中push当前的vnode,最后对insertedVnodeQueue中所有的vnode调用inserted钩子。
但是当子组件首次渲染完成以后,invokeInsertHook中不会立即调用insertedVnodeQueue中各个Vnode的insert方法,而是将insertedVnodeQueue转存至父组件占位vnode的vnode.data.pendingInert上,当父组件执行initComponent的时候,将子组件传递过来的insertedVnodeQueue和自身的insertedVnodeQueue进行连接,最后调用父组件的insertedVnodeQueue中各个vnode的insert方法。
Vnode的生命周期
在createPatchFunction中会传入参数backend
function createPatchFunction (backend) {
const { modules, nodeOps } = backend;
}
nodeOps是各种平台对DOM节点操作的适配,例如web或者weex
modules是各种平台的模块,以web为例:
Web平台相关模块:
- attrs模块: 处理节点上的特性attribute
- klass模块:处理节点上的类class
- events模块: 处理节点上的原生事件
- domProps模块: 处理节点上的属性property
- style模块: 处理节点上的内联样式style特性
- trasition模块
核心模块:
- ref模块:处理节点上的引用ref
- directives模块: 处理节点上的指令directives
每个功能模块都包含了各种钩子,用于DOM节点创建、更新和销毁。
在Vnode中存在各种生命周期如:
- create:DOM元素节点创建时/初始化组件时调用
- activate: 组件激活时调用
- update: DOM节点更新时调用
- remove: DOM节点移除时调用
- destory: 组件销毁时调用
那这些生命周期是如何加入的,回到最开始的地方:
vnode = vm.render();
Vue.prototype._render = function () {
const vm = this;
const {
render,
} = vm.$options;
vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode;
}
vnode是由render.call(vm._renderProxy, vm.$createElement)生成的。
这里的render有两种情况:
- 基于
HTML的模板形式,即template选项 - 用于手写的
render函数形式 使用template形式的模板最终转换为render函数的形式。vm.$createElement返回的就是vnode,createElement在vdom/create-element中,对于真实的DOM还是组件类型用不同的方式创建相应的vnode。 - 真实节点调用
vnode = new VNode(tag, data, children, undefined, undefined, context) - 组件节点调用
createComponent(Ctor, data, context, children, tag)createComponent定义在vdom/create-component中
function createComponent(Ctor, data, context, children, tag) {
mergeHooks();
}
const componentVnodeHooks = {
init(){},
prepatch(){},
insert(){},
destory(){}
}
function mergeHooks(data) {
if (!data.hook) {
data.hook = {}
}
const hooksToMerge = Object.keys(componentVNodeHooks)
for (let i = 0; i < hooksToMerge.length; i++) {
const key = hooksToMerge[i];
const fromParent = data.hook[key]
const ours = componentVNodeHooks[key];
data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours;
}
}
在这里就给vnode.data.hook上绑定了各种钩子init、prepatch、insert、destroy。在patch过程中,就会调用对应的钩子。
patchVnode
如果符合sameVnode,就不会渲染vnode重新创建DOM节点,而是在原有的DOM节点上进行修补,尽可能复用原有的DOM节点。
- 如果两个节点相同则直接返回
- 处理静态节点的情况
vnode是可patch的- 调用组件占位
vnode的prepatch钩子 update钩子存在,调用update钩子
- 调用组件占位
vnode不存在text文本- 新老节点都有
children子节点,且children不相同,则调用updateChildren递归更新children(这个函数的内容放到diff中进行讲解) - 只有新节点有子节点:先清空文本内容,然后为当前节点添加子节点
- 只有老节点存在子节点: 移除所有子节点
- 都没有子节点的时候,就直接移除节点的文本
- 新老节点都有
- 新老节点文本不一样: 替换节点文本
- 调用
vnode的postpatch钩子
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) return
// 静态节点的处理程序
const data = vnode.data;
i = data.hook.prepatch
i(oldVnode, vnode);
if (isPatchable(vnode)) {
for(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
i = data.hook.update
i(oldVnode, vnode)
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
i = data.hook.postpatch
i(oldVnode, vnode)
}
diff算法
在patchVnode中提到,如果新老节点都有子节点,但是不相同的时候就会调用updateChildren,这个函数通过diff算法尽可能的复用先前的DOM节点。
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
算了这个图没画明白,借用网上的图

oldStartIdx、newStartIdx、oldEndIdx以及newEndIdx分别是新老两个VNode两边的索引,同时oldStartVnode、newStartVnode、oldEndVnode和new EndVnode分别指向这几个索引对应的vnode。整个遍历需要在oldStartIdx小于oldEndIdx并且newStartIdx小于newEndIdx(这里为了简便,称sameVnode为相似)
- 当
oldStartVnode不存在的时候,oldStartVnode向右移动,oldStartIdx加1 - 当
oldEndVnode不存在的时候,oldEndVnode向右移动,oldEndIdx减1 oldStartVnode和newStartVnode相似,oldStartVnode和newStartVnode都向右移动,oldStartIdx和newStartIdx都增加1
oldEndVnode和newEndVnode相似,oldEndVnode和newEndVnode都向左移动,oldEndIdx和newEndIdx都减1
oldStartVnode和newEndVnode相似,则把oldStartVnode.elm移动到oldEndVnode.elm的节点后面。然后oldStartIdx向后移动一位,newEndIdx向前移动一位

oldEndVnode和newStartVnode相似时,把oldEndVnode.elm插入到oldStartVnode.elm前面。同样的,oldEndIdx向前移动一位,newStartIdx向后移动一位。

key与旧vnode对应的哈希表
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
最后生成的对象就是以children的key为属性,递增的数字为属性值的对象例如
children = [{
key: 'key1'
}, {
key: 'key2'
}]
// 最后生成的map
map = {
key1: 0,
key2: 1,
}
所以oldKeyToIdx就是key和旧vnode的key对应的哈希表
根据newStartVnode的key看能否找到对应的oldVnode
- 如果
oldVnode不存在,就创建一个新节点,newStartVnode向右移动 - 如果找到节点:
- 并且和
newStartVnode相似。将map表中该位置的赋值undefined(用于保证key是唯一的)。同时将newStartVnode.elm插入啊到oldStartVnode.elm的前面,然后index向后移动一位 - 如果不符合
sameVnode,只能创建一个新节点插入到parentElm的子节点中,newStartIdx向后移动一位
- 并且和
-
结束循环后
oldStartIdx又大于oldEndIdx,就将新节点中没有对比的节点加到队尾中

- 如果
newStartIdx > newEndIdx,就说明还存在新节点,就将这些节点进行删除

总结
本篇文章对数据发生改变时,视图是如何更新进行了讲解。对一些细节地方进行了省略,如果需要了解更加深入,结合源码更加合适。我的github请多多关注,谢谢
Log
12-19: 更新patch的具体过程1-15: 更新keep-alive的实现原理