第二次写文章,写得不对的地方望各位大神指正~javascript
以前研究Vue的响应式原理有提到, 当数据发生变化时, Watcher会调用 vm._update(vm._render(), hydrating)
来进行DOM更新, 接下来咱们看看这个具体的更新过程是如何实现的。vue
//摘自core\instance\lifecycle.js
Vue.prototype._update = function(vnode: VNode, hydrating ? : boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */ ,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}复制代码
( 这里咱们就将一些不过重要的代码忽略掉不讲了, 好比callHook调用钩子函数之类的, 咱们只关注实现组件渲染相关代码。)java
这里面最重要的代码就是经过 vm.__patch__
进行DOM更新。 若是以前没有渲染过, 就直接调用 vm.__patch__
生成真正的DOM并将生成的DOM挂载到vm.$el上, 不然会调用 vm.__patch__(prevVnode, vnode)
将当前vnode与以前的vnode进行diff比较, 最小化更新。node
接下来咱们就看一下这个最重要的 vm.__patch__
到底作了些什么。web
//摘自platforms\web\runtime\patch.js
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({ nodeOps, modules })复制代码
能够看到patch方法主要就是调用了createPatchFunction这个函数。 一步步看看它主要干了些什么。数组
顾名思义, 这个函数的做用是建立并返回一个patch函数。app
//摘自core\vdom\patch.js
//......
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
//......
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
//......
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}复制代码
在这个返回的patch函数里, 会进行许多的判断:dom
createElm(vnode, insertedVnodeQueue, parentElm, refElm)
建立一个新的DOM。patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
来更新oldVnode并生成新的DOM。( 这里判断nodeType是否认义是由于vnode是没有nodeType的, 当进行服务端渲染时会有nodeType, 这样能够排除掉服务端渲染的状况。 )咱们分别看一下上面的两种状况:函数
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */ ,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}复制代码
若是没有prevVnode(也就是第一次渲染), 这时vm.$el若是为undefined则知足 isUndef(oldVnode)
,会调用createElm函数;若是vm.$el存在,但其不知足 sameVnode(oldVnode, vnode)
,一样会调用createElm函数。也就是说若是是首次渲染,就会调用createElm函数建立新的DOM。post
若是有prevVnode(也就是进行视图的更新),这时若是知足 sameVnode(oldVnode, vnode)
(即vnode相同),则会调用patchVnode对vnode进行更新;若是vnode不相同,则会调用createElm函数建立新的DOM节点替换掉原来的DOM节点。
那么接下来分别看看这两个函数。
//摘自\core\vdom\patch.js
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested // for transition enter check
//......
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
//......
createChildren(vnode, children, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
//......
}复制代码
能够看到, createElm中主要会根据vnode.ns(vnode的命名空间)是否存在调用createElementNS函数或createElmement函数生成真正的DOM节点并赋给vnode.elm保存。而后经过createChildren函数建立vnode的子节点,而且经过insert函数将vnode.elm插入到父节点中。
//摘自\core\vdom\patch.js
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true)
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(vnode.text))
}
}复制代码
createChildren函数会判断vnode的children是不是数组,若是是,则代表vnode有子节点,循环调用createElm函数为子节点建立DOM;若是是text节点,则会调用createTextNode为其建立文本节点。
//摘自\core\vdom\patch.js
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
//......
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)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}复制代码
patchVnode主要是对oldVnode和vnode进行必定的对比:
咱们先来看下比较简单的当vnode和oldVnode只有其中一个有children时调用的addVnodes和removeVnodes函数。
//摘自\core\vdom\patch.js
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm)
}
}复制代码
addVnodes函数经过循环调用createElm分别对vnode的children中的每一个子vnode建立子节点并挂载到DOM上。
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch)
invokeDestroyHook(ch)
} else { // Text node
removeNode(ch.elm)
}
}
}
}复制代码
removeVnodes函数经过调用removeNode函数(removeAndInvokeRemoveHook函数最终也是调用removeNode函数)将oldVnode的children节点所有移除。
接下来就看一下当vnode和oldVnode都有children时调用的updateChildren函数。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
//......
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} 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)) { // Vnode moved right
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)) { // Vnode moved left
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)) { // New element
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, elmToMove.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
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)
}
}复制代码
在这里咱们主要须要关注三个数组:oldCh、newCh和parentElm.children。oldCh就是oldVnode.children,newCh就是vnode.children,parentElm就是oldVnode.elm。
而oldStartIdx、oldEndIdx、newStartIdx和newEndIdx这四个是用于标志当前关注的vnode的头指针和尾指针。
简单来讲,咱们会将oldCh和newCh进行比较,将oldCh跟newCh差别的部分patch到parentElm中,最终获得一个根据newCh所对应的elm.children。接下来咱们一步步分析这个函数究竟是如何进行diff的。
oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
时继续进行循环。sameVnode(oldStartVnode, newStartVnode)
,则递归调用patchVnode对二者进行比较,同时头指针往右走。由于咱们最终想要获得的是newCh所对应的elm,而这个elm是oldVnode.elm,它的children一开始是根据oldCh生成的。那么当oldStartVnode跟newStartVnode相同时,意味着elm.children中这个位置的子节点已是跟newCh所对应的。sameVnode(oldEndVnode, newEndVnode)
,同理,递归调用patchVnode对二者进行比较,同时尾指针往左走。sameVnode(oldStartVnode, newEndVnode)
,意味着newEndVnode跟oldStartVnode相同,这个时候递归调用patchVnode对二者进行比较后咱们须要经过nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
,将oldStartVnode.elm移动到parentElm.children中newEndVnode所对应的位置,也就是oldEndVnode.elm后面。sameVnode(oldEndVnode, newStartVnode)
,同理,经过递归调用patchVnode对二者进行比较后经过nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
将oldEndVnode.elm移动到parentElm.children中newStartVnode所对应的位置,也就是oldStartVnode.elm前面。createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
建立一个新的DOM节点并插入到oldStartVnode.elm前面。nodeOps.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
将elmToMove.elm移动到oldStartVnode.elm前面。能够看到,咱们将这个节点设为了undefined,这样当指针移动到这里的时候发现是undefined就会继续移动,由于这个节点已经被复用了,这个就是上面第2步判断的做用。oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
时,循环结束。这时候咱们就要判断究竟是oldStartIdx > oldEndIdx
仍是newStartIdx > newEndIdx
。
oldStartIdx > oldEndIdx
,由于只有当oldCh中的节点被复用时,oldCh的指针才会移动,当oldCh的头指针大于尾指针时,意味着oldCh已经没有节点能够被复用了,这样咱们就须要直接将newCh中还未添加到parentElm.children的节点经过addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
添加到parentElm.children中。newStartIdx > newEndIdx
,意味着newCh中的全部节点都已经在parentElm.children中了,也就意味着OldCh中若是oldStartIdx到oldEndIdx之间(包括oldStartIdx和oldEndIdx)指针所指向的节点在newCh中没有对应的节点,也就是说剩下的都是多余的节点,因此咱们须要经过removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
将多余的节点都移除。通过这样的一个过程以后,parentElm.children就变成了与newCh相对应了。
总的来讲,updateChildren的做用是根据newCh生成相应的parentElm.children,同时尽可能复用其中的节点。因此对于每个newCh的节点,会先在oldCh中找相应的节点,找到了就将其移动到parentElm.children中与newCh对应的位置,没找到就建立一个新的节点插入到对应的位置。最后将parentElm.children中多余的节点移除或者将newCh中还未添加到parentElm.children中的节点添加上去。
文字描述仍是有点比较难理解,用图例来进一步解释。
首先,假设咱们的oldCh有四个节点,用数字表示,分别为一、二、三、4,newCh五个节点,分别为五、二、六、三、1。因为parentElm.children是根据oldCh生成的,因此也有四个节点一、二、三、4。oldCh的头尾指针分别指向1和4,newCh的头尾指针分别指向五、1。
parentElm.children | 1 | 2 | 3 | 4 | - |
---|---|---|---|---|---|
oldCh指针 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指针 | ↑ | ↑ |
根据上面咱们说到的updateChildren的判断过程,判断到oldCh的头节点和newCh的尾节点相同,因而就将parentElm.children中的oldCh头节点移动到oldCh尾节点后面。而后oldCh跟newCh的指针分别移动,因而就变成了下面这样。
parentElm.children | 2 | 3 | 4 | 1 | - |
---|---|---|---|---|---|
oldCh指针 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指针 | ↑ | ↑ |
继续进行循环判断,发现头尾的节点都没有相同的,这个时候咱们就要去oldCh中根据key找与newCh头节点相同的节点。可是没有找到,因此咱们会建立一个新的节点插入到parentElm.children中头节点前面,而后指针移动。结果以下。
parentElm.children | 5 | 2 | 3 | 4 | 1 |
---|---|---|---|---|---|
oldCh指针 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指针 | ↑ | ↑ |
继续进行循环。发现头节点相同,无需移动,直接对头节点进行patch,指针移动。结果以下。
parentElm.children | 5 | 2 | 3 | 4 | 1 |
---|---|---|---|---|---|
oldCh指针 | ↓ | ↓ | |||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指针 | ↑ | ↑ |
继续进行循环。发现newCh尾节点和oldCh头节点相同,将parentElm.children中的3节点移动到parentElm.children的尾指针后面,指针移动。结果以下。
parentElm.children | 5 | 2 | 4 | 3 | 1 |
---|---|---|---|---|---|
oldCh指针 | ↓↓ | ||||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指针 | ↑↑ |
如今两个头尾指针都相等了,但仍是符合循环的条件,因而继续进行循环。因为两个节点不相同,因而会建立一个新的节点插入到parentElm.children的头指针前面,指针移动。结果以下。
parentElm.children | 5 | 2 | 6 | 4 | 3 | 1 |
---|---|---|---|---|---|---|
oldCh指针 | ↓↓ | |||||
oldCh | 1 | 2 | 3 | 4 | ||
newCh | 5 | 2 | 6 | 3 | 1 | |
newCh指针 | ↑ | ↑ |
这样以后newStartIdx > newEndIdx
,循环结束。由于newStartIdx > newEndIdx
,意味着parentElm.children中可能还有多余的节点,咱们再调用removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
将多余的节点移除。结果以下。
parentElm.children | 5 | 2 | 6 | 3 | 1 |
---|---|---|---|---|---|
oldCh指针 | ↓↓ | ||||
oldCh | 1 | 2 | 3 | 4 | |
newCh | 5 | 2 | 6 | 3 | 1 |
newCh指针 | ↑ | ↑ |
这样,咱们就完成了整一个updateChildren的过程,parentElm.children已经变成了与newCh相对应了。整一个patch的递归完成后,vnode.elm就变成全新的elm了,视图也就更新完毕啦。