現代主流框架均使用一種數據=>視圖
的方式,隱藏了繁瑣的dom操做,採用了聲明式編程(Declarative Programming
)替代了過去的類jquery的命令式編程(Imperative Programming
)javascript
$("#xxx").text("xxx"); // 變爲下者 view = render(state);
前者咱們詳細地寫了如何去操做dom節點的過程,咱們命令什麼,它就操做什麼;
後者則是咱們輸入了數據狀態,輸出視圖(咱們不關心中間的過程,它們均由框架幫助咱們實現);
前者當然直接,可是當應用變得複雜則代碼將難以維護,然後者框架幫咱們實現了一系列的操做,無需管理過程,優點顯然可見。html
爲了實現這一點,就是實現如何輸入數據,輸出視圖,咱們就會注意到上面的render函數,render函數的實現,主要在對dom性能的優化上,固然實現方式也多種多樣,直接的innerHTML、使用documentFragment、還有virtual dom,在不一樣場景下性能上有所不一樣,可是框架追求的是在大部分場景中框架已經知足你的優化需求,這裏咱們也不加以贅述,後文會提到。前端
固然還有數據變化偵測,從而re-render視圖,數據變化偵測中,值得一提的是數據生產者(Producer
)和數據消費者(Consumer
)之間的聯繫,這裏,咱們能夠暫且將系統(視圖)做爲一個數據的消費者,咱們的代碼設置數據的變化,做爲數據的生產者
咱們這裏能夠分爲系統不可感知數據變化
和系統可感知數據變化
vue
Rx.js中是將二者通訊分紅拉取(
Pull
)和推送(Push
),比較很差理解,這裏我本身就分了個類java
像React/Angular這類框架並不知道數據何時變了,可是它視圖何時更新呢,好比React就是經過setState發信號告訴系統有可能數據變了,而後經過virtual dom diff去渲染視圖,angular則是有一個髒值檢查流程,遍歷比對node
Rx.js / vue這一類響應式的,經過觀察者模式,使用Observable (可觀察對象),Observer (觀察者)(或者是watcher)去訂閱(好比視圖渲染這一類,其實也能夠當成一個觀察者去訂閱數據了,後面會提到),系統是能夠很準確知道哪裏數據變了的,從而也就能實現視圖更新渲染。react
上者系統不可感知數據變化
,粒度粗,有時候還得手動優化(好比pureComponet和shouldComponentUpdate)去跳過一些數據不會更新的視圖從而提高性能
下者系統可感知數據變化
,粒度細,可是綁定大量觀察者,有大量的依賴追蹤的內存開銷jquery
因此算法
這裏也就終於提到本文的主角Vue2,它採用了折中粒度的方式,粒度到組件級別上,由watcher訂閱數據,當數據變化咱們能夠得知哪一個組件數據變了,而後採用virtual dom diff的方式去更新相應組件。編程
後文咱們也將展開它是如何實現這些過程的,咱們能夠先從一個簡單的應用開始。
<div id="app"> {{ message }} </div> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) app.message = `xxx`; // 發現視圖發生了變化
從這裏咱們也能夠提出幾個問題,讓後面原理的解析更有針對性。
還有一個小細節,app.message如何拿到vue data中的message?
固然同時咱們也會講解一些收集依賴等相關的概念。
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變了」
Vue實現響應式有三個很重要的類,Observer類,Watcher類,Dep類
我這裏先籠統介紹一下(詳細可見源碼英文註解)
defineProperty
增長getter/setter方法,而且在getter/setter中收集依賴或者通知更新觀察者模式,跟發佈/訂閱模式有點像
可是其實略有不一樣,發佈/訂閱模式是由統一的事件分發調度中心,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 Watcher(vm, 'msg', () => console.log("msg變了"))
,首先標記了全局變量Dep.target = 該Watcher實例,而後執行msg的get操做,觸發到了它的getter,而後dep成功獲取到它的訂閱者,放入它的訂閱者數組,最後咱們將Dep.target = null其實講到這裏,核心的響應式原理就講得差很少了。
可是其實Object.defineProperty並非萬能的,
爲了解決這些自己js限制的問題
__proto__
繼承那些方法(若是不行則直接一個個defineProperty到數組上),具體的變異方法就是在後面加上dep.notify的操做__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 }
咱們能夠看到上面分紅三部分
那裏面具體作了什麼呢?這裏我簡略講一下
因此最後會產生這樣的效果
模板
<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官網的這張圖
一切瓜熟蒂落!
咱們上一節隱藏了不少Virtual DOM的細節,是由於Virtual DOM大篇幅有可能讓咱們忘記咱們所要探究的問題,這裏咱們來揭開Virtual DOM的謎團,它其實並無那麼神祕。
作過前端性能優化的朋友應該都知道,DOM操做都是很慢的,咱們要減小對它的操做
爲啥慢呢?
咱們能夠嘗試打出一層DOM的key
咱們能夠看出它的屬性是龐大,更況且這只是一層
同時直接對DOM的操做,就必須很注意一些有可能觸發重排的操做。
那Virtual DOM是什麼角色呢?它其實就是咱們代碼到操做DOM的一層緩衝,既然操做DOM慢,那我操做js對象快吧,我就操做js對象,而後最後把這個對象再一塊兒轉換成真正的DOM就好了
因此就變成 代碼 => Virtual DOM( 一個特殊的js對象) => 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節點
這裏咱們講的就是涉及上面vm.update
的操做
vm._render
),轉換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算法,根據有一篇論文(我忘記在哪裏了),是須要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判斷,其實就是簡單比較了幾個屬性判斷
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
這部分其實就是leetcode.com/problems/ed… 最小編輯距離問題,這裏也並無用複雜的動態規劃算法(複雜度爲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原理也就講解結束了,還有不少細節沒有深刻,讀者能夠閱讀源碼去深刻研究。
咱們能夠再回顧下開頭的問題(其實文中也是不斷的在提出問題解決問題),做爲看到這裏的你,但願你能有所收穫~
還有一個小細節,app.message如何拿到vue data中的message?