vue2从数据变化到视图变化之diff算法图文详解
目录
- 引言
- 1、isUndef(oldStartVnode)
- 2、isUndef(oldEndVnode)
- 3、sameVnode(oldStartVnode, newStartVnode)
- 4、sameVnode(oldEndVnode, newEndVnode)
- 5、sameVnode(oldStartVnode, newEndVnode)
- 6、sameVnode(oldEndVnode, newStartVnode)
- 7、如果以上都不满足
- 小结
引言
vue
数据的渲染会引入视图的重新渲染。
从数据到视图的渲染流程可以移步https://www.jb51.net/article/261839.htm,那么从数据的变化到视图的变化是怎样的?
vue
在数据的初始化阶段会进行响应式的处理defineReactive
:
/** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
数据的变化会触发set
方法,会让发布者dep
执行 dep.notify
,当vue
所有的同步执行完后,在异步队列中按次序执行到vm
的渲染流程,订阅者接收到发布者的通知后会执行到this.get()
,指的是
updateComponent = () => { vm._update(vm._render(), hydrating) }
vm._render()
获取到vNode
后,会执行vm._update
视图的渲染:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { // ... const prevVnode = vm._vnode // ... if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } // ... }
主要区别在于数据变化引起的视图变化有prevVnode
,vm.__patch__(prevVnode, vnode)
之后会执行到patch
方法:
function patch (oldVnode, vnode, hydrating, removeOnly) { // ... if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // ... // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // ... // destroy old node if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
在数据变化引起的patch
过程中isRealElement
显然为false
,新旧节点是否相同的另一个判断条件是sameVnode
:
function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }
如果sameVnode(oldVnode, vnode)
为false
,则执行createElm
以及后续流程,该流程可以参考模板渲染的流程(请移步https://www.jb51.net/article/261850.htm )。
sameVnode(oldVnode, vnode)
为true
的时候,执行到patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
:
function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // ... const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(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) } }
ch = vnode.children
和oldCh = oldVnode.children
分别获取到新旧vnode
的子元素,ch
和oldCh
都存在时会执行到updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { 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, vnodeToMove, refElm // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } 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, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) 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, newCh, newStartIdx) 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] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } 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(oldCh, oldStartIdx, oldEndIdx) } }
这里定义了四个索引oldStartIdx
、newStartIdx
、oldEndIdx
和newEndIdx
,也可以称之为指针,通过while
循环,进行四个指针的移动:
1、isUndef(oldStartVnode)
如果oldStartVnode
不存在,执行oldStartVnode = oldCh[++oldStartIdx]
,将oldStartIdx
指针向右移动一位,进行下次循环。
2、isUndef(oldEndVnode)
如果oldEndVnode
不存在,执行oldEndVnode = oldCh[--oldEndIdx]
,将oldEndIdx
指针向左移动一位,进行下次循环。
3、sameVnode(oldStartVnode, newStartVnode)
如果满足sameVnode(oldStartVnode, newStartVnode)
,执行patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
开始递归执行,结束后oldStartIdx
和newStartIdx
分别向右移动一位。
4、sameVnode(oldEndVnode, newEndVnode)
如果满足sameVnode(oldEndVnode, newEndVnode)
,执行patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newStartIdx)
开始递归执行,结束后oldEndIdx
和newEndIdx
分别向左移动一位。
5、sameVnode(oldStartVnode, newEndVnode)
如果满足sameVnode(oldStartVnode, newEndVnode)
,执行patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newStartIdx)
开始递归执行,结束后oldStartVnode
向右移动一位,newEndIdx
向左移动一位。
并且通过nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
的方式将oldStartVnode.elm
插入到oldEndVnode.elm
节点之后。
6、sameVnode(oldEndVnode, newStartVnode)
如果满足sameVnode(oldEndVnode, newStartVnode)
,执行patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
开始递归执行,结束后newStartIdx
向右移动一位,oldEndIdx
向左移动一位。
并且通过nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
的方式将 oldEndVnode.elm
插入到oldStartVnode.elm
节点之前。
7、如果以上都不满足
如果新旧vNode
首首、首尾、尾首和尾尾对比都没找到相同的,则在旧vNode
的oldStartIdx
和oldEndIdx
之间去找。 oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
创建以旧vNode
的key为key
值,位置索引为value
的map映射:
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 }
如果通过createKeyToOldIdx
找不到,则通过findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
和旧vNode
的方式去进行比对,并返回位置索引:
function findIdxInOld (node, oldCh, start, end) { for (let i = start; i < end; i++) { const c = oldCh[i] if (isDef(c) && sameVnode(node, c)) return i } }
通过oldKeyToIdx[newStartVnode.key]
和findIdxInOld (node, oldCh, start, end)
的查询会有两种结果:
1、没找到如果没有找到,则以newStartVnode
为渲染vNode
通过createElm
去进行节点的创建。
2、找到了如果找到了,通过vnodeToMove = oldCh[idxInOld]
获取到介于oldStartIdx
和oldEndIdx
之间的可以比对的vnode
, 执行完patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
后将当前位置的oldCh[idxInOld] = undefined
。
通过nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
将vnodeToMove.elm
移动到oldStartVnode.elm
之前。
小结
diff算法从两端进行比对,找不到再从中间寻找,是一种 “滑动窗口” 算法的使用,以达到通过节点移动来实现原地复用的目的。
以上就是vue2从数据变化到视图变化之diff算法图文详解的详细内容,更多关于vue2数据视图变化diff算法的资料请关注我们其它相关文章!