引言
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算法的资料请关注其它相关文章!
查看更多关于vue2从数据变化到视图变化之diff算法图文详解的详细内容...