麻豆小视频在线观看_中文黄色一级片_久久久成人精品_成片免费观看视频大全_午夜精品久久久久久久99热浪潮_成人一区二区三区四区

首頁 > 編程 > JavaScript > 正文

Vue實現(xiàn)virtual-dom的原理簡析

2019-11-19 16:07:22
字體:
供稿:網(wǎng)友

virtual-dom(后文簡稱vdom)的概念大規(guī)模的推廣還是得益于react出現(xiàn),virtual-dom也是react這個框架的非常重要的特性之一。相比于頻繁的手動去操作dom而帶來性能問題,vdom很好的將dom做了一層映射關(guān)系,進而將在我們本需要直接進行dom的一系列操作,映射到了操作vdom,而vdom上定義了關(guān)于真實dom的一些關(guān)鍵的信息,vdom完全是用js去實現(xiàn),和宿主瀏覽器沒有任何聯(lián)系,此外得益于js的執(zhí)行速度,將原本需要在真實dom進行的創(chuàng)建節(jié)點,刪除節(jié)點,添加節(jié)點等一系列復(fù)雜的dom操作全部放到vdom中進行,這樣就通過操作vdom來提高直接操作的dom的效率和性能。

Vue在2.0版本也引入了vdom。其vdom算法是基于snabbdom算法所做的修改。

在Vue的整個應(yīng)用生命周期當中,每次需要更新視圖的時候便會使用vdom。那么在Vue當中,vdom是如何和Vue這個框架融合在一起工作的呢?以及大家常常提到的vdom的diff算法又是怎樣的呢?接下來就通過這篇文章簡單的向大家介紹下Vue當中的vdom是如何去工作的。

首先,我們還是來看下Vue生命周期當中初始化的最后階段:將vm實例掛載到dom上,源碼在src/core/instance

  Vue.prototype._init = function () {    ...    vm.$mount(vm.$options.el) // 實際上是調(diào)用了mountComponent方法    ...  }  

mountComponent函數(shù)的定義是:

export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component { // vm.$el為真實的node vm.$el = el // 如果vm上沒有掛載render函數(shù) if (!vm.$options.render) {  // 空節(jié)點  vm.$options.render = createEmptyVNode } // 鉤子函數(shù) callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) {  ... } else {  // updateComponent為監(jiān)聽函數(shù), new Watcher(vm, updateComponent, noop)  updateComponent = () => {   // Vue.prototype._render 渲染函數(shù)   // vm._render() 返回一個VNode   // 更新dom   // vm._render()調(diào)用render函數(shù),會返回一個VNode,在生成VNode的過程中,會動態(tài)計算getter,同時推入到dep里面   vm._update(vm._render(), hydrating)  } } // 新建一個_watcher對象 // vm實例上掛載的_watcher主要是為了更新DOM // vm/expression/cb vm._watcher = new Watcher(vm, updateComponent, noop) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) {  vm._isMounted = true  callHook(vm, 'mounted') } return vm}

注意上面的代碼中定義了一個updateComponent函數(shù),這個函數(shù)執(zhí)行的時候內(nèi)部會調(diào)用vm._update(vm._render(), hyddrating)方法,其中vm._render方法會返回一個新的vnode,(關(guān)于vm_render是如何生成vnode的建議大家看看vue的關(guān)于compile階段的代碼),然后傳入vm._update方法后,就用這個新的vnode和老的vnode進行diff,最后完成dom的更新工作。那么updateComponent都是在什么時候去進行調(diào)用呢?

vm._watcher = new Watcher(vm, updateComponent, noop)

實例化一個watcher,在求值的過程中this.value = this.lazy ? undefined : this.get(),會調(diào)用this.get()方法,因此在實例化的過程當中Dep.target會被設(shè)為這個watcher,通過調(diào)用vm._render()方法生成新的Vnode并進行diff的過程中完成了模板當中變量依賴收集工作。即這個watcher被添加到了在模板當中所綁定變量的依賴當中。一旦model中的響應(yīng)式的數(shù)據(jù)發(fā)生了變化,這些響應(yīng)式的數(shù)據(jù)所維護的dep數(shù)組便會調(diào)用dep.notify()方法完成所有依賴遍歷執(zhí)行的工作,這里面就包括了視圖的更新即updateComponent方法的調(diào)用。

updateComponent方法的定義是:

updateComponent = () => { vm._update(vm._render(), hydrating)}

完成視圖的更新工作事實上就是調(diào)用了vm._update方法,這個方法接收的第一個參數(shù)是剛生成的Vnode,調(diào)用的vm._update方法的定義是

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  // 新的vnode  vm._vnode = vnode  // Vue.prototype.__patch__ is injected in entry points  // based on the rendering backend used.  // 如果需要diff的prevVnode不存在,那么就用新的vnode創(chuàng)建一個真實dom節(jié)點  if (!prevVnode) {   // initial render   // 第一個參數(shù)為真實的node節(jié)點   vm.$el = vm.__patch__(    vm.$el, vnode, hydrating, false /* removeOnly */,    vm.$options._parentElm,    vm.$options._refElm   )  } else {   // updates   // 如果需要diff的prevVnode存在,那么首先對prevVnode和vnode進行diff,并將需要的更新的dom操作已patch的形式打到prevVnode上,并完成真實dom的更新工作   vm.$el = vm.__patch__(prevVnode, vnode)  }  activeInstance = prevActiveInstance  // update __vue__ reference  if (prevEl) {   prevEl.__vue__ = null  }  if (vm.$el) {   vm.$el.__vue__ = vm  }  // if parent is an HOC, update its $el as well  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {   vm.$parent.$el = vm.$el  }}

在這個方法當中最為關(guān)鍵的就是vm.__patch__方法,這也是整個virtaul-dom當中最為核心的方法,主要完成了prevVnode和vnode的diff過程并根據(jù)需要操作的vdom節(jié)點打patch,最后生成新的真實dom節(jié)點并完成視圖的更新工作。

接下來就讓我們看下vm.__patch__里面到底發(fā)生了什么:

  function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {    // 當oldVnode不存在時    if (isUndef(oldVnode)) {      // 創(chuàng)建新的節(jié)點      createElm(vnode, insertedVnodeQueue, parentElm, refElm)    } else {      const isRealElement = isDef(oldVnode.nodeType)      if (!isRealElement && sameVnode(oldVnode, vnode)) {      // patch existing root node      // 對oldVnode和vnode進行diff,并對oldVnode打patch      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)   }     }  }

在對oldVnode和vnode類型判斷中有個sameVnode方法,這個方法決定了是否需要對oldVnode和vnode進行diff及patch的過程。

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) )}

sameVnode會對傳入的2個vnode進行基本屬性的比較,只有當基本屬性相同的情況下才認為這個2個vnode只是局部發(fā)生了更新,然后才會對這2個vnode進行diff,如果2個vnode的基本屬性存在不一致的情況,那么就會直接跳過diff的過程,進而依據(jù)vnode新建一個真實的dom,同時刪除老的dom節(jié)點。

vnode基本屬性的定義可以參見源碼:src/vdom/vnode.js里面對于vnode的定義。

constructor (  tag?: string,  data?: VNodeData,     // 關(guān)于這個節(jié)點的data值,包括attrs,style,hook等  children?: ?Array<VNode>, // 子vdom節(jié)點  text?: string,    // 文本內(nèi)容  elm?: Node,      // 真實的dom節(jié)點  context?: Component, // 創(chuàng)建這個vdom的上下文  componentOptions?: VNodeComponentOptions ) {  this.tag = tag  this.data = data  this.children = children  this.text = text  this.elm = elm  this.ns = undefined  this.context = context  this.functionalContext = undefined  this.key = data && data.key  this.componentOptions = componentOptions  this.componentInstance = undefined  this.parent = undefined  this.raw = false  this.isStatic = false  this.isRootInsert = true  this.isComment = false  this.isCloned = false  this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void {  return this.componentInstance }}

每一個vnode都映射到一個真實的dom節(jié)點上。其中幾個比較重要的屬性:

  1. tag 屬性即這個vnode的標簽屬性
  2. data 屬性包含了最后渲染成真實dom節(jié)點后,節(jié)點上的class,attribute,style以及綁定的事件
  3. children 屬性是vnode的子節(jié)點
  4. text 屬性是文本屬性
  5. elm 屬性為這個vnode對應(yīng)的真實dom節(jié)點
  6. key 屬性是vnode的標記,在diff過程中可以提高diff的效率,后文有講解

比如,我定義了一個vnode,它的數(shù)據(jù)結(jié)構(gòu)是:

  {    tag: 'div'    data: {      id: 'app',      class: 'page-box'    },    children: [      {        tag: 'p',        text: 'this is demo'      }    ]  }

最后渲染出的實際的dom結(jié)構(gòu)就是:

  <div id="app" class="page-box">    <p>this is demo</p>  </div>

讓我們再回到patch函數(shù)當中,在當oldVnode不存在的時候,這個時候是root節(jié)點初始化的過程,因此調(diào)用了createElm(vnode, insertedVnodeQueue, parentElm, refElm)方法去創(chuàng)建一個新的節(jié)點。而當oldVnode是vnode且sameVnode(oldVnode, vnode)2個節(jié)點的基本屬性相同,那么就進入了2個節(jié)點的diff過程。

diff的過程主要是通過調(diào)用patchVnode方法進行的:

function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {  ...}
if (isDef(data) && isPatchable(vnode)) {   // cbs保存了hooks鉤子函數(shù): 'create', 'activate', 'update', 'remove', 'destroy'   // 取出cbs保存的update鉤子函數(shù),依次調(diào)用,更新attrs/style/class/events/directives/refs等屬性   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)  }

更新真實dom節(jié)點的data屬性,相當于對dom節(jié)點進行了預(yù)處理的操作

接下來:

  ...  const elm = vnode.elm = oldVnode.elm  const oldCh = oldVnode.children  const ch = vnode.children  // 如果vnode沒有文本節(jié)點  if (isUndef(vnode.text)) {   // 如果oldVnode的children屬性存在且vnode的屬性也存在   if (isDef(oldCh) && isDef(ch)) {    // updateChildren,對子節(jié)點進行diff    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)   } else if (isDef(ch)) {    // 如果oldVnode的text存在,那么首先清空text的內(nèi)容    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')    // 然后將vnode的children添加進去    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)   } else if (isDef(oldCh)) {    // 刪除elm下的oldchildren    removeVnodes(elm, oldCh, 0, oldCh.length - 1)   } else if (isDef(oldVnode.text)) {    // oldVnode有子節(jié)點,而vnode沒有,那么就清空這個節(jié)點    nodeOps.setTextContent(elm, '')   }  } else if (oldVnode.text !== vnode.text) {   // 如果oldVnode和vnode文本屬性不同,那么直接更新真是dom節(jié)點的文本元素   nodeOps.setTextContent(elm, vnode.text)  }

這其中的diff過程中又分了好幾種情況,oldCh為oldVnode的子節(jié)點,ch為Vnode的子節(jié)點:

  1. 首先進行文本節(jié)點的判斷,若oldVnode.text !== vnode.text,那么就會直接進行文本節(jié)點的替換;
  2. 在vnode沒有文本節(jié)點的情況下,進入子節(jié)點的diff;
  3. 當oldCh和ch都存在且不相同的情況下,調(diào)用updateChildren對子節(jié)點進行diff;
  4. 若oldCh不存在,ch存在,首先清空oldVnode的文本節(jié)點,同時調(diào)用addVnodes方法將ch添加到elm真實dom節(jié)點當中;
  5. 若oldCh存在,ch不存在,則刪除elm真實節(jié)點下的oldCh子節(jié)點;
  6. 若oldVnode有文本節(jié)點,而vnode沒有,那么就清空這個文本節(jié)點。

這里著重分析下updateChildren方法,它也是整個diff過程中最重要的環(huán)節(jié):

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {  // 為oldCh和newCh分別建立索引,為之后遍歷的依據(jù)  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    // 直到oldCh或者newCh被遍歷完后跳出循環(huán)  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)    // 插入到老的開始節(jié)點的前面    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)    oldEndVnode = oldCh[--oldEndIdx]    newStartVnode = newCh[++newStartIdx]   } else {    // 如果以上條件都不滿足,那么這個時候開始比較key值,首先建立key和index索引的對應(yīng)關(guān)系    if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)    idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null    // 如果idxInOld不存在    // 1. newStartVnode上存在這個key,但是oldKeyToIdx中不存在    // 2. newStartVnode上并沒有設(shè)置key屬性    if (isUndef(idxInOld)) { // New element     // 創(chuàng)建新的dom節(jié)點     // 插入到oldStartVnode.elm前面     // 參見createElm方法     createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)     newStartVnode = newCh[++newStartIdx]    } else {     elmToMove = oldCh[idxInOld]     /* istanbul ignore if */     if (process.env.NODE_ENV !== 'production' && !elmToMove) {      warn(       'It seems there are duplicate keys that is causing an update error. ' +       'Make sure each v-for item has a unique key.'      )          // 將找到的key一致的oldVnode再和newStartVnode進行diff     if (sameVnode(elmToMove, newStartVnode)) {      patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)      oldCh[idxInOld] = undefined      // 移動node節(jié)點      canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)      newStartVnode = newCh[++newStartIdx]     } else {      // same key but different element. treat as new element      // 創(chuàng)建新的dom節(jié)點      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)      newStartVnode = newCh[++newStartIdx]     }    }   }  }  // 如果最后遍歷的oldStartIdx大于oldEndIdx的話  if (oldStartIdx > oldEndIdx) {    // 如果是老的vdom先被遍歷完   refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm   // 添加newVnode中剩余的節(jié)點到parentElm中   addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)  } else if (newStartIdx > newEndIdx) { // 如果是新的vdom先被遍歷完,則刪除oldVnode里面所有的節(jié)點   // 刪除剩余的節(jié)點   removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)  }}

在開始遍歷diff前,首先給oldCh和newCh分別分配一個startIndex和endIndex來作為遍歷的索引,當oldCh或者newCh遍歷完后(遍歷完的條件就是oldCh或者newCh的startIndex >= endIndex),就停止oldCh和newCh的diff過程。接下來通過實例來看下整個diff的過程(節(jié)點屬性中不帶key的情況):

首先從第一個節(jié)點開始比較,不管是oldCh還是newCh的起始或者終止節(jié)點都不存在sameVnode,同時節(jié)點屬性中是不帶key標記的,因此第一輪的diff完后,newCh的startVnode被添加到oldStartVnode的前面,同時newStartIndex前移一位;

第二輪的diff中,滿足sameVnode(oldStartVnode, newStartVnode),因此對這2個vnode進行diff,最后將patch打到oldStartVnode上,同時oldStartVnode和newStartIndex都向前移動一位

第三輪的diff中,滿足sameVnode(oldEndVnode, newStartVnode),那么首先對oldEndVnode和newStartVnode進行diff,并對oldEndVnode進行patch,并完成oldEndVnode移位的操作,最后newStartIndex前移一位,oldStartVnode后移一位;

第四輪的diff中,過程同步驟3;

第五輪的diff中,同過程1;

遍歷的過程結(jié)束后,newStartIdx > newEndIdx,說明此時oldCh存在多余的節(jié)點,那么最后就需要將這些多余的節(jié)點刪除。

在vnode不帶key的情況下,每一輪的diff過程當中都是起始和結(jié)束節(jié)點進行比較,直到oldCh或者newCh被遍歷完。而當為vnode引入key屬性后,在每一輪的diff過程中,當起始和結(jié)束節(jié)點都沒有找到sameVnode時,首先對oldCh中進行key值與索引的映射:

if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null

createKeyToOldIdx方法,用以將oldCh中的key屬性作為鍵,而對應(yīng)的節(jié)點的索引作為值。然后再判斷在newStartVnode的屬性中是否有key,且是否在oldKeyToIndx中找到對應(yīng)的節(jié)點。

如果不存在這個key,那么就將這個newStartVnode作為新的節(jié)點創(chuàng)建且插入到原有的root的子節(jié)點中:

if (isUndef(idxInOld)) { // New element  // 創(chuàng)建新的dom節(jié)點  // 插入到oldStartVnode.elm前面  // 參見createElm方法  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)     newStartVnode = newCh[++newStartIdx]    } 

如果存在這個key,那么就取出oldCh中的存在這個key的vnode,然后再進行diff的過程:

elmToMove = oldCh[idxInOld]     /* istanbul ignore if */     if (process.env.NODE_ENV !== 'production' && !elmToMove) {          // 將找到的key一致的oldVnode再和newStartVnode進行diff     if (sameVnode(elmToMove, newStartVnode)) {      patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)      // 清空這個節(jié)點      oldCh[idxInOld] = undefined      // 移動node節(jié)點      canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)      newStartVnode = newCh[++newStartIdx]     } else {      // same key but different element. treat as new element      // 創(chuàng)建新的dom節(jié)點      createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)      newStartVnode = newCh[++newStartIdx]     }

通過以上分析,給vdom上添加key屬性后,遍歷diff的過程中,當起始點, 結(jié)束點的搜尋及diff出現(xiàn)還是無法匹配的情況下時,就會用key來作為唯一標識,來進行diff,這樣就可以提高diff效率。

帶有Key屬性的vnode的diff過程可見下圖:

注意在第一輪的diff過后oldCh上的B節(jié)點被刪除了,但是newCh上的B節(jié)點上elm屬性保持對oldCh上B節(jié)點的elm引用。





以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持武林網(wǎng)。

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 久久久久久99 | 深夜视频在线 | 欧美日本91精品久久久久 | 92看片淫黄大片欧美看国产片 | 久色成人网 | 免费试看av | 亚洲一级片在线观看 | 一道本不卡一区 | 成品片a免人视频 | 免费看黄色一级大片 | 爱性久久久久久久 | 国产中出视频 | 色网免费观看 | 国产精品成人一区二区三区吃奶 | 精品亚洲福利一区二区 | 精品国产一区二区三区四区在线 | 在线播放免费人成毛片乱码 | 亚洲天堂成人在线观看 | 国产九色91 | av在线一区二区三区 | 国产精品视频yy9299一区 | 午夜视频国产 | 亚洲日本韩国在线观看 | 欧美亚洲一级 | 久久国产精品久久久久久电车 | 999精品国产 | www.91视频com| 国产乱淫av片免费 | 精品久久久久久久久久久aⅴ | 欧美18—19sex性护士中国 | 欧美成人一级 | 欧美大片一级毛片 | 性生活视频网站 | 久久这| 国产免费一区二区三区视频 | 午夜视频在线观看免费视频 | 欧美一级黑人 | 一级α片免费看刺激高潮视频 | 欧美一级免费视频 | 亚州精品天堂中文字幕 | 91黄瓜视频 |