在这篇文章深入源码学习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
存在,则插入到父元素上 - 如果组件根节点被替换,遍历更新父节点
element
。然后移除老节点
- 如果老节点是真实
-
-
调用
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
,就将新节点中没有对比的节点加到队尾中
-
![](https://user-gold-cdn.xitu.io/2019/11/19/16e83a83366194d3?w=784&h=373&f=png&s=73559)
- 如果`newStartIdx > newEndIdx`,就说明还存在新节点,就将这些节点进行删除
![](https://user-gold-cdn.xitu.io/2019/11/19/16e83a871c34ea5f?w=836&h=367&f=png&s=77933)
总结
本篇文章对数据发生改变时,视图是如何更新进行了讲解。对一些细节地方进行了省略,如果需要了解更加深入,结合源码更加合适。我的github请多多关注,谢谢
Log
-
12-19
: 更新patch
的具体过程
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。