在这篇文章深入源码学习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
有两种情况:
- 创建非组件节点的
vnode
tag
不存在,创建空节点、注释、文本节点- 使用
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
向后移动一位。
7. 当以上情况都不符合的时候
生成一个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
的实现原理