Vue2原理淺談

本文重點講述Vue2渲染的總體流程,包括數據響應的實現(雙向綁定)、模板編譯、virtual dom原理等,但願讀者看完有所收穫。html

前言

此部份內容初步介紹前端主流框架部分特色,來提升你們對框架的認識,從而最後導出對vue2原理的總體介紹
參考尤雨溪的live 不吹不黑聊聊前端框架
有興趣的同窗能夠聽聽前端

現代主流框架均使用一種數據=>視圖的方式,隱藏了繁瑣的dom操做,採用了聲明式編程(Declarative Programming)替代了過去的類jquery的命令式編程(Imperative Programming)vue

$( "#xxx" ).text( "xxx" );
// 變爲下者
view = render(state);

前者咱們詳細地寫了如何去操做dom節點的過程,咱們命令什麼,它就操做什麼;
後者則是咱們輸入了數據狀態,輸出視圖(咱們不關心中間的過程,它們均由框架幫助咱們實現);
前者當然直接,可是當應用變得複雜則代碼將難以維護,然後者框架幫咱們實現了一系列的操做,無需管理過程,優點顯然可見。node

爲了實現這一點,就是實現如何輸入數據,輸出視圖,咱們就會注意到上面的render函數,render函數的實現,主要在對dom性能的優化上,固然實現方式也多種多樣,直接的innerHTML、使用documentFragment、還有virtual dom,在不一樣場景下性能上有所不一樣,可是框架追求的是在大部分場景中框架已經知足你的優化需求,這裏咱們也不加以贅述,後文會提到。react

固然還有數據變化偵測,從而re-render視圖,數據變化偵測中,值得一提的是數據生產者(Producer)和數據消費者(Consumer)之間的聯繫,這裏,咱們能夠暫且將系統(視圖)做爲一個數據的消費者,咱們的代碼設置數據的變化,做爲數據的生產者
咱們這裏能夠分爲系統不可感知數據變化和系統可感知數據變化jquery

Rx.js中是將二者通訊分紅拉取(Pull)和推送(Push),比較很差理解,這裏我本身就分了個類git

  • 系統不可感知數據變化

像React/Angular這類框架並不知道數據何時變了,可是它視圖何時更新呢,好比React就是經過setState發信號告訴系統有可能數據變了,而後經過virtual dom diff去渲染視圖,angular則是有一個髒值檢查流程,遍歷比對github

  • 系統可感知數據變化
從一個簡單的應用看起
<div id= "app" >
   {{ message }}
</div>
 
var app = new Vue({
   el: '#app' ,
   data: {
     message: 'Hello Vue!'
   }
})
app.message = `xxx`; // 發現視圖發生了變化

從這裏咱們也能夠提出幾個問題,讓後面原理的解析更有針對性。算法

  • 數據響應?如何得知數據變化?

    還有一個小細節,app.message如何拿到vue data中的message?編程

  • 數據變更如何和視圖聯繫在一塊兒?
  • virtual dom是什麼?virtual dom diff又是什麼?

固然同時咱們也會講解一些收集依賴等相關的概念。

數據響應原理

Object.defineProperty

Vue數據響應核心是使用了Object.defineProperty方法(IE9+)在對象中定義屬性或者修改屬性,其中存取描述符很關鍵的就是get和set,提供給屬性getter和setter方法

能夠看下面例子,咱們攔截到了數據獲取以及設置

var obj = {};
Object.defineProperty(obj, 'msg' , {
   get () {
     console.log( 'get' )
   },
   set (newValue) {
     console.log( 'set' , newValue)
   }
});
obj.msg // get
obj.msg = 'hello world' // set hello world

順便提到那個小細節的問題

app.message如何拿到vue data中的message?

其實也是跟Object.defineProperty有關
Vue在初始化數據的時候會遍歷data代理這些數據

function initData (vm) {
     let data = vm.$options.data
     vm._data = data
     const keys = Object.keys(data)
     let i = keys.length
     while (i--) {
         const key = keys[i]
         proxy(vm, `_data`, key)
     }
     observe(data)
}

proxy作了哪些操做呢?

function proxy (target, sourceKey, key) {
     Object.defineProperty(target, key, {
       enumerable: true ,
       configurable: true ,
       get () {
         return this [sourceKey][key]
       }
       set () {
         this [sourceKey][key] = val
       }
     })
}

其實就是用Object.defineProperty多加了一層的訪問
所以咱們就能夠用app.message訪問到app.data.message
也算個Object.defineProperty小應用吧

講完這語法的核心層面得知了如何知道數據發生變化,可是響應,是還有迴應的,接下來來談下Vue是如何實現數據響應的?
其實就是解決下面的問題,如何實現$watch?

const vm = new Vue({
   data:{
     msg: 1,
   }
})
vm.$watch( "msg" , () => console.log( "msg變了" ));
vm.msg = 2; //輸出「msg變了」

觀察者模式(Observer, Watcher, Dep)

Vue實現響應式有三個很重要的類,Observer類,Watcher類,Dep類
我這裏先籠統介紹一下(詳細可見源碼英文註解)

  • Observer類主要用於給Vue的數據defineProperty增長getter/setter方法,而且在getter/setter中收集依賴或者通知更新
  • Watcher類來用於觀察數據(或者表達式)變化而後執行回調函數(其中也有收集依賴的過程),主要用於$watch API和指令上
  • Dep類就是一個可觀察對象,能夠有不一樣指令訂閱它(它是多播的)

觀察者模式,跟發佈/訂閱模式有點像
可是其實略有不一樣,發佈/訂閱模式是由統一的事件分發調度中心,on則往中心中數組加事件(訂閱),emit則從中心中數組取出事件(發佈),發佈和訂閱以及發佈後調度訂閱者的操做都是由中心統一完成

可是觀察者模式則沒有這樣的中心,觀察者訂閱了可觀察對象,當可觀察對象發佈事件,則就直接調度觀察者的行爲,因此這裏觀察者和可觀察對象其實就產生了一個依賴的關係,這個是發佈/訂閱模式上沒有體現的。

其實Dep就是dependence依賴的縮寫

如何實現觀察者模式呢?

咱們先看下面代碼,下面代碼實現了Watcher去訂閱Dep的過程,Dep因爲是能夠被多個Watcher所訂閱的,因此它擁有着訂閱者數組,訂閱了它,就把Watcher放入數組便可。

class Dep {
   constructor () {
     this .subs = []
   }
   notify () {
     const subs = this .subs.slice()
     for (let i = 0; i < subs.length; i++) {
         subs[i].update()
     }
   }
   addSub (sub) {
     this .subs.push(sub)
   }
}
 
class Watcher {
   constructor () {
   }
   update () {
   }
}
 
let dep = new Dep()
dep.addSub( new Watcher()) // Watcher訂閱了依賴

咱們實現了訂閱,那通知發佈呢,也就是上面的notify在哪裏實現呢?

咱們到這裏就能夠聯繫到數據響應,咱們須要的是數據變化去通知更新,那顯然是會在defineProperty中的setter中去實現了,聰明的你應該想到了,咱們能夠把每個數據當成一個Dep實例,而後setter的時候去notify就好了,因此咱們能夠在defineProperty中new Dep(),經過閉包setter就能夠取到Dep實例了

就像下面這樣

function defineReactive (obj, key, val) {
     const dep = new Dep()
     Object.defineProperty(obj, key, {
         enumerable: true ,
         configurable: true ,
         get: function reactiveGetter () {
             //...
         },
         set: function reactiveSetter (newVal) {
             //...
             dep.notify()
         }
     })
}

而後這裏就又產生了一個問題
你都把Dep實例放裏面了,我怎麼讓個人Watcher實例訂閱到這個Dep實例呢,Vue在這裏實現了精妙的一筆,從get裏面作手腳,在get中是能夠取到這個Dep實例的,因此能夠在執行watch操做的時候,執行獲取數值,觸發getter去收集依賴

function defineReactive (obj, key, val) {
     const dep = new Dep()
     const property = Object.getOwnPropertyDescriptor(obj, key)
 
     const getter = property && property.get
     const setter = property && property.set
 
     let childOb = 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() // 等價執行dep.addSub(Dep.target),在這裏收集
             }
             return value
         },
         set: function reactiveSetter (newVal) {
             const value = getter ? getter.call(obj) : val
             if (newVal === value) {
                 return
             }
             if (setter) {
                 setter.call(obj, newVal)
             } else {
                 val = newVal
             }
             dep.notify()
         }
     })

這裏咱們也要結合Watcher的實現來看

class Watcher () {
   constructor (vm, expOrFn, cb, options) {
     this .cb = cb
     this .value = this .get()
   }
   get () {
     pushTarget( this ) // 標記全局變量Dep.target
     let value = this .getter.call(vm, vm) // 觸發getter
     if ( this .deep) {
       traverse(value)
     }
     popTarget() // 標記全局變量Dep.target
     return value
   }
   update () {
     this .run()
   }
   run () {
       const value = this .get() // new Value
       // re-collect dep
       if (value !== this .value ||
           isObject(value)) {
           const oldValue = this .value
           this .value = value
           this .cb.call( this .vm, value, oldValue)
       }
   }
}

因此咱們在new Watcher的時候會執行一個求值的操做,而後由於標記了這個Watcher觸發的,因此收集了依賴,也就是觀察者訂閱了依賴(這個求值有可能不止觸發了一個getter,有可能觸發了不少個getter,那就收集了多個依賴),咱們能夠再注意一下上面的run操做,也就是dep.notify()後watcher會執行的操做,還會出現一個get操做,咱們能夠注意到這裏從新收集了一波依賴!(固然裏面有相關的去重操做)

咱們再回來回顧上面咱們要解決的小例子

const vm = new Vue({
   data: {
     msg: 1,
   }
})
vm.$watch( "msg" , () => console.log( "msg變了" ));
vm.msg = 2; //輸出「變了」

$watcher其實就是一個new Watcher的封裝
即new Watcher(vm, ‘msg’, () => console.log(「msg變了」))

  • 首先是new Vue遍歷了數據,給數據defineProperty加上了getter/setter方法
  • 咱們new Watcher(vm, 'msg', () => console.log("msg變了")),首先標記了全局變量Dep.target = 該Watcher實例,而後執行msg的get操做,觸發到了它的getter,而後dep成功獲取到它的訂閱者,放入它的訂閱者數組,最後咱們將Dep.target = null
  • 最後設置vm.msg = 2,觸發到了setter,閉包中的dep.notify,遍歷訂閱者數組,執行相應的回調操做。

其實講到這裏,核心的響應式原理就講得差很少了。

可是其實Object.defineProperty並非萬能的,

  • 數組的push/pop等操做
  • 不能監測數組length長度的變化
  • 數組的arr[xxx] = yyy沒法感知
  • 一樣的,對象屬性的添加和刪除沒法感知

爲了解決這些自己js限制的問題

  • Vue首先是對數組方法進行變異,用__proto__繼承那些方法(若是不行則直接一個個defineProperty到數組上),具體的變異方法就是在後面加上dep.notify的操做
  • 至於屬性的添加和刪除,咱們能夠想象到,增長屬性,那咱們根本沒有defineProperty,刪除屬性則連咱們以前的defineProperty都給刪了,因此這裏Vue增長了一個$set/$delete的API去實現這些操做,一樣也是在最後加上了dep.notify的操做
  • 固然以上就不是單純靠defineProperty中每個數據所對應的dep來實現了,在Observer類也有一個dep實例,同時會給數據掛載一個__ob__屬性去獲取它的Observer實例,像數組和對象的上面特殊操做,在watch收集依賴的時候都會把這個依賴收集到,而後最後使用的是這個dep去notify更新

這部分就不詳細介紹了,有興趣的讀者能夠閱讀源碼

這裏咱們能夠稍微提一下一個ES6的新特性Proxy,頗有多是下一代響應機制的主角,由於它能夠解決咱們上面的缺陷,可是因爲兼容問題還不能很好地使用,可讓咱們期待一下~

如今咱們再來看看Vue官網的這張圖

至少目前咱們對右半部分很清晰了,Data如何和Watcher聯繫已經很清楚,可是Render Function,Watcher怎麼Trigger Render Function這個還須要去解答,固然還有左下角的Virtual DOM Tree

數據與視圖如何聯繫

我這裏摘出一段關鍵的Vue代碼

class Watcher () {
   constructor (vm, expOrFn, cb, options) {
   }
}
updateComponent = () => {
    // hydrating有關ssr本文不涉及
     vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
// noop是回調函數,它是空函數

這個其實就是Watcher和Render的核心關係

還記得咱們上面所說的,在執行new Watcher會有一個求值的操做,這裏的求值是一個函數表達式,也就是執行updateComponent,執行updateComponent後,會再執行vm._render(),傳參數給vm._update(vm._render(), hydrating),收集完依賴之後才結束,這裏有兩個關鍵的點,vm._render在作什麼?vm._update在作什麼?

vm._render

咱們看下Vue.prototype._render是何方神聖(如下爲刪減代碼)

Vue.prototype._render = function (): VNode {
   const vm: Component = this
   const {
     render,
     staticRenderFns,
     _parentVnode
   } = vm.$options
   // ...
   let vnode
   try {
     // vm._renderProxy咱們直接當成vm,其實就是爲了開發環境報warning用的
     vnode = render.call(vm._renderProxy, vm.$createElement)
   } catch (e) {
 
   }
 
   // set parent
   vnode.parent = _parentVnode
   return vnode
}

因此它這裏咱們能夠看到裏面是執行了render函數,render函數來自options,而後返回了vnode

因此到這裏咱們能夠把咱們的目光移到這個render函數從哪裏來的

若是熟悉Vue2的朋友可能知道,Vue提供了一個選項是render就是做爲這個函數的,假如沒有提供這個選項呢
咱們不妨看看生命週期

咱們能夠看到Compile template into render function(沒有template會將el的outerHTML當成template),因此這裏就有一個模板編譯的過程

模板編譯

再摘一段核心代碼

const ast = parse(template.trim(), options) // 構建抽象語法樹
optimize(ast, options) // 優化
const code = generate(ast, options) // 生成代碼
return {
     ast,
     render: code.render,
     staticRenderFns: code.staticRenderFns
}

咱們能夠看到上面分紅三部分

  • 將模板轉化爲抽象語法樹
  • 優化抽象語法樹
  • 根據抽象語法樹生成代碼

那裏面具體作了什麼呢?這裏我簡略講一下

  • 第一部分其實就是各類正則了,對左右開閉標籤的匹配以及屬性的收集,經過棧的形式,不斷出棧入棧去匹配以及更換父節點,最後生成一個對象,包含children,children又包含children的對象
  • 第二部分則是以第一部分爲基礎,根據節點類型找出一些靜態的節點並標記
  • 第三部分就是生成render函數代碼了

因此最後會產生這樣的效果

模板

<div id= "container" >
   <p>Message is: {{ message }}</p>
</div>

生成render函數

( function () {
     with ( this ) {
         return _c( 'div' , {
             attrs: {
                 "id" : "container"
             }
         }, [_c( 'p' , [_v( "Message is: " + _s(message))])])
     }
}
)

這裏咱們又能夠結合上面的代碼了

vnode = render.call(vm._renderProxy, vm.$createElement)

其中_c就是vm.$createElement

咱們將virtual dom具體實現移到下一節,以防影響咱們Vue2主線

vm.$createElement其實就是一個建立vnode的一個API

知道了vm._render()建立了vnode返回,接下來就是vm._update了

vm._update

vm._update部分也是跟virtual dom有關,下一節具體介紹,咱們能夠先透露下函數的功能,顧名思義,就是更新視圖,根據傳入的vnode更新到視圖中。

數據到視圖的總體流程

因此到這裏咱們就能夠得出一個數據到視圖的總體流程的結論了

  • 在組件級別,vue會執行一個new Watcher
  • new Watcher首先會有一個求值的操做,它的求值就是執行一個函數,這個函數會執行render,其中可能會有編譯模板成render函數的操做,而後生成vnode(virtual dom),再將virtual dom應用到視圖中
  • 其中將virtual dom應用到視圖中(這裏涉及到diff後文會講),必定會對其中的表達式求值(好比{{message}},咱們確定會取到它的值再去渲染的),這裏會觸發到相應的getter操做完成依賴的收集
  • 當數據變化的時候,就會notify到這個組件級別的Watcher,而後它還會去求值,從而從新收集依賴,而且從新渲染視圖

咱們再一次來看看Vue官網的這張圖

Virtual DOM

咱們上一節隱藏了不少Virtual DOM的細節,是由於Virtual DOM大篇幅有可能讓咱們忘記咱們所要探究的問題,這裏咱們來揭開Virtual DOM的謎團,它其實並無那麼神祕。

爲何會有Virtual DOM?

作過前端性能優化的朋友應該都知道,DOM操做都是很慢的,咱們要減小對它的操做
爲啥慢呢?
咱們能夠嘗試打出一層DOM的key

咱們能夠看出它的屬性是龐大,更況且這只是一層

同時直接對DOM的操做,就必須很注意一些有可能觸發重排的操做。

那Virtual DOM是什麼角色呢?它其實就是咱們代碼到操做DOM的一層緩衝,既然操做DOM慢,那我操做js對象快吧,我就操做js對象,而後最後把這個對象再一塊兒轉換成真正的DOM就好了

因此就變成 代碼 => Virtual DOM( 一個特殊的js對象) => DOM

什麼是Virtual DOM

上文其實咱們就解答了什麼是虛擬DOM,它就是一個特殊的js對象
咱們能夠看看Vue中的Vnode是怎麼定義的?

export class VNode {
   constructor (
     tag?: string,
     data?: VNodeData,
     children?: ?Array<VNode>,
     text?: string,
     elm?: Node,
     context?: Component,
     componentOptions?: VNodeComponentOptions,
     asyncFactory?: Function
   ) {
     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
     this .asyncFactory = asyncFactory
     this .asyncMeta = undefined
     this .isAsyncPlaceholder = false
   }
}

用以上這些屬性就能來表示一個DOM節點

Virtual DOM算法

這裏咱們講的就是涉及上面vm.update的操做

  • 首先是js對象(Virtual DOM)描述樹(vm._render),轉換dom插入(第一次渲染)
  • 狀態變化,生成新的js對象(Virtual DOM),比對新舊對象
  • 將變動應用到DOM上,並保存新的js對象(Virtual DOM),重複第二步操做

用js對象描述樹(生成Virtual DOM),Vue中就是先轉成AST生成code,而後經過$creatElement經過Vnode的那種形式生成Virtual DOM (vm._render的操做)

這裏咱們能夠具體看下vm._update(其實就是Virtual DOM算法的後兩步)

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
   // ...
   if (!prevVnode) {
     // initial render
     // 第一次渲染
     vm.$el = vm.__patch__(
       vm.$el, vnode, hydrating, false /* removeOnly */ ,
       vm.$options._parentElm,
       vm.$options._refElm
     )
   } else {
     // updates
     // 更新視圖
     vm.$el = vm.__patch__(prevVnode, vnode)
   }
   // ...
}

能夠看到一個關鍵點vm.__patch__,其實它就是Virtual DOM Diff的核心,也是它最後把真實DOM插入的

Virtual DOM Diff

完整Virtual DOM Diff算法,根據有一篇論文(我忘記在哪裏了),是須要O(n^3)的,由於它涉及跨層級的複用,這種時間複雜度是不可接受的,同時考慮到DOM較少涉及跨層級的複用,因此就減小至當前層級的複用,這個算法的複雜度就降到O(n)了,Perfect~

引用一張React經典的圖來幫助你們理解吧,左右同一顏色圈起來的就是比較/複用的範圍

步入正題,咱們看看Vue的patch函數

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)) {
       // empty mount (likely as component), create new root element
       // 老節點不存在,直接建立元素
       isInitialPatch = true
       createElm(vnode, insertedVnodeQueue, parentElm, refElm)
     } else {
       const isRealElement = isDef(oldVnode.nodeType)
       if (!isRealElement && sameVnode(oldVnode, vnode)) {
         // patch existing root node
         // 新節點和老節點相同,則給老節點打補丁
         patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
       } else {
         // ... 省略ssr代碼
         // replacing existing element
         // 新節點和老節點相同,直接替換老節點
         const oldElm = oldVnode.elm
         const parentElm = nodeOps.parentNode(oldElm)
         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)
         )
       }
     }
     // ...省略代碼
     return vnode.elm
   }

因此patch大概作下面幾件事

  • 判斷老節點存不存在
      不存在則爲首次渲染,直接建立元素
      存在的話則sameVnode使用判斷根節點是否相同
        相同則使用patchVnode給老節點打補丁
        不相同則使用新節點直接替換老節點
    對於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)
       )
     )
   )
}

對於patchVnode
其實就是比較節點的子節點,分別對新老節點的擁有的子節點作判斷,假如二者都沒有或者一者有一者沒有,就比較容易,直接刪除或者增長便可,可是假如二者都有子節點,這裏就涉及到列表對比以及一些複用操做了,實現的方法是updateChildren

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
     if (oldVnode === vnode) {
       // 新老節點相同
       return
     }
     // ... 省略代碼
     if (isUndef(vnode.text)) {
       // 假如新節點沒有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) {
       // 假如新節點和老節點text不相等
       nodeOps.setTextContent(elm, vnode.text)
     }
     if (isDef(data)) {
       if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
     }
   }

咱們最後再來看看這個updateChildren
這部分其實就是https://leetcode.com/problems/edit-distance/ 最小編輯距離問題,這裏也並無用複雜的動態規劃算法(複雜度爲O(m * n))去實現最小的移動操做,而是選擇可犧牲必定的dom操做去優化部分場景,複雜度能夠下降到O(max(m, n),比較分別首尾節點,若是沒有匹配到,則使用第一個節點key(這裏就是咱們常在v-for用的)去找相同的key去patch比較,假如沒有key的話,則是直接遍歷找類似的節點,有則patch移動,沒有則建立新節點

這裏告訴咱們
列表假若有可能有複用的節點,可使用惟一的key去標識,提高patch效率,可是也不能亂設置key,假如根本不同,可是你設置同樣的話,會致使框架沒找到真正類似的節點去複用,反而下降效率,會增長一個建立dom的消耗

這裏代碼較多,有興趣的讀者能夠深刻閱讀,這裏我就不畫圖了,讀者也能夠找網上的相應updateChildren的圖,有助於理解patch的過程

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
 
     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)) {
         // 假如新節點的第一個和老節點的第一個相同
         // patch該節點而且新老節點頭指針分別往下一個移動
         patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
         oldStartVnode = oldCh[++oldStartIdx]
         newStartVnode = newCh[++newStartIdx]
       } else if (sameVnode(oldEndVnode, newEndVnode)) {
         // 假如新節點的最後一個和老節點的最後一個相同
         // patch該節點而且新老節點尾指針分別往上一個移動
         patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
         oldEndVnode = oldCh[--oldEndIdx]
         newEndVnode = newCh[--newEndIdx]
       } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
         // 假如新節點的最後一個和老節點的第一個相同
         // patch該節點而且新節點尾指針往上一個移動,老節點頭指針往下一個移動
         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
         // 假如新節點的第一個和老節點的最後一個相同
         // patch該節點而且老節點尾指針往上一個移動,新節點頭指針往下一個移動
         patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
         canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
         oldEndVnode = oldCh[--oldEndIdx]
         newStartVnode = newCh[++newStartIdx]
       } else {
         // 建立老節點key to index的映射
         if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
         idxInOld = isDef(newStartVnode.key)
           ? oldKeyToIdx[newStartVnode.key] // 假如新節點第一個有key,找該key下老節點的index
           : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 假如新節點沒有key,直接遍歷找相同的index
         if (isUndef(idxInOld)) { // New element
           // 假如沒有找到index,則建立節點
           createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
         } else {
           // 假若有index,則找出這個須要move的老節點
           vnodeToMove = oldCh[idxInOld]
           /* istanbul ignore if */
           if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
             warn(
               'It seems there are duplicate keys that is causing an update error. ' +
               'Make sure each v-for item has a unique key.'
             )
           }
           if (sameVnode(vnodeToMove, newStartVnode)) {
             // move老節點和新節點的第一個基本相同則開始patch
             patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
             // 設置老節點空
             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)
           }
         }
         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)
     }
   }

總結

到這裏總體Vue2原理也就講解結束了,還有不少細節沒有深刻,讀者能夠閱讀源碼去深刻研究。
咱們能夠再回顧下開頭的問題(其實文中也是不斷的在提出問題解決問題),做爲看到這裏的你,但願你能有所收穫~

  • 數據響應?如何得知數據變化?(提示:defineProperty)

    還有一個小細節,app.message如何拿到vue data中的message?

  • 數據變更如何和視圖聯繫在一塊兒?(提示:Watcher、Dep、Observer)
  • virtual dom是什麼?virtual dom diff又是什麼?(提示:特殊的js對象)

參考連接/推薦閱讀

深度剖析:如何實現一個 Virtual DOM 算法
Vue源碼詳解:compile,link,依賴,批處理…一網打盡,全解析!
深刻響應式原理

最後

謝謝閱讀~
歡迎follow我哈哈https://github.com/BUPT-HJM
歡迎繼續觀光個人新博客~

相關文章
相關標籤/搜索