vue2從數(shù)據(jù)變化到視圖變化之diff算法圖文詳解
引言
vue
數(shù)據(jù)的渲染會引入視圖的重新渲染。
從數(shù)據(jù)到視圖的渲染流程可以移步http://chabaoo.cn/article/261839.htm,那么從數(shù)據(jù)的變化到視圖的變化是怎樣的?
vue
在數(shù)據(jù)的初始化階段會進行響應式的處理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() } }) }
數(shù)據(jù)的變化會觸發(fā)set
方法,會讓發(fā)布者dep
執(zhí)行 dep.notify
,當vue
所有的同步執(zhí)行完后,在異步隊列中按次序執(zhí)行到vm
的渲染流程,訂閱者接收到發(fā)布者的通知后會執(zhí)行到this.get()
,指的是
updateComponent = () => { vm._update(vm._render(), hydrating) }
vm._render()
獲取到vNode
后,會執(zhí)行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) } // ... }
主要區(qū)別在于數(shù)據(jù)變化引起的視圖變化有prevVnode
,vm.__patch__(prevVnode, vnode)
之后會執(zhí)行到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 }
在數(shù)據(jù)變化引起的patch
過程中isRealElement
顯然為false
,新舊節(jié)點是否相同的另一個判斷條件是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
,則執(zhí)行createElm
以及后續(xù)流程,該流程可以參考模板渲染的流程(請移步http://chabaoo.cn/article/261850.htm )。
sameVnode(oldVnode, vnode)
為true
的時候,執(zhí)行到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
都存在時會執(zhí)行到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
循環(huán),進行四個指針的移動:
1、isUndef(oldStartVnode)
如果oldStartVnode
不存在,執(zhí)行oldStartVnode = oldCh[++oldStartIdx]
,將oldStartIdx
指針向右移動一位,進行下次循環(huán)。
2、isUndef(oldEndVnode)
如果oldEndVnode
不存在,執(zhí)行oldEndVnode = oldCh[--oldEndIdx]
,將oldEndIdx
指針向左移動一位,進行下次循環(huán)。
3、sameVnode(oldStartVnode, newStartVnode)
如果滿足sameVnode(oldStartVnode, newStartVnode)
,執(zhí)行patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
開始遞歸執(zhí)行,結束后oldStartIdx
和newStartIdx
分別向右移動一位。
4、sameVnode(oldEndVnode, newEndVnode)
如果滿足sameVnode(oldEndVnode, newEndVnode)
,執(zhí)行patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newStartIdx)
開始遞歸執(zhí)行,結束后oldEndIdx
和newEndIdx
分別向左移動一位。
5、sameVnode(oldStartVnode, newEndVnode)
如果滿足sameVnode(oldStartVnode, newEndVnode)
,執(zhí)行patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newStartIdx)
開始遞歸執(zhí)行,結束后oldStartVnode
向右移動一位,newEndIdx
向左移動一位。
并且通過nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
的方式將oldStartVnode.elm
插入到oldEndVnode.elm
節(jié)點之后。
6、sameVnode(oldEndVnode, newStartVnode)
如果滿足sameVnode(oldEndVnode, newStartVnode)
,執(zhí)行patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
開始遞歸執(zhí)行,結束后newStartIdx
向右移動一位,oldEndIdx
向左移動一位。
并且通過nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
的方式將 oldEndVnode.elm
插入到oldStartVnode.elm
節(jié)點之前。
7、如果以上都不滿足
如果新舊vNode
首首、首尾、尾首和尾尾對比都沒找到相同的,則在舊vNode
的oldStartIdx
和oldEndIdx
之間去找。 oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
創(chuàng)建以舊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
去進行節(jié)點的創(chuàng)建。
2、找到了如果找到了,通過vnodeToMove = oldCh[idxInOld]
獲取到介于oldStartIdx
和oldEndIdx
之間的可以比對的vnode
, 執(zhí)行完patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
后將當前位置的oldCh[idxInOld] = undefined
。
通過nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
將vnodeToMove.elm
移動到oldStartVnode.elm
之前。
小結
diff算法從兩端進行比對,找不到再從中間尋找,是一種 “滑動窗口” 算法的使用,以達到通過節(jié)點移動來實現(xiàn)原地復用的目的。
以上就是vue2從數(shù)據(jù)變化到視圖變化之diff算法圖文詳解的詳細內容,更多關于vue2數(shù)據(jù)視圖變化diff算法的資料請關注腳本之家其它相關文章!
相關文章
把vue-router和express項目部署到服務器的方法
下面小編就為大家分享一篇把vue-router和express項目部署到服務器的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02vue-router動態(tài)設置頁面title的實例講解
今天小編就為大家分享一篇vue-router動態(tài)設置頁面title的實例講解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08Vue-cli3項目引入Typescript的實現(xiàn)方法
這篇文章主要介紹了Vue-cli3項目引入Typescript的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10Vue向后臺傳數(shù)組數(shù)據(jù),springboot接收vue傳的數(shù)組數(shù)據(jù)實例
這篇文章主要介紹了Vue向后臺傳數(shù)組數(shù)據(jù),springboot接收vue傳的數(shù)組數(shù)據(jù)實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11vue-image-crop基于Vue的移動端圖片裁剪組件示例
這篇文章主要介紹了vue-image-crop基于Vue的移動端圖片裁剪組件示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08