本文重點講述Vue2渲染的總體流程,包括數據響應的實現(雙向綁定)、模板編譯、virtual dom原理等,但願讀者看完有所收穫。javascript
博客同步原文連接:imhjm.com/article/59b…html
此部份內容初步介紹前端主流框架部分特色,來提升你們對框架的認識,從而最後導出對vue2原理的總體介紹
參考尤雨溪的live 不吹不黑聊聊前端框架
有興趣的同窗能夠聽聽前端
現代主流框架均使用一種數據=>視圖
的方式,隱藏了繁瑣的dom操做,採用了聲明式編程(Declarative Programming
)替代了過去的類jquery的命令式編程(Imperative Programming
)vue
$("#xxx").text("xxx");
// 變爲下者
view = render(state);複製代碼
前者咱們詳細地寫了如何去操做dom節點的過程,咱們命令什麼,它就操做什麼;
後者則是咱們輸入了數據狀態,輸出視圖(咱們不關心中間的過程,它們均由框架幫助咱們實現);
前者當然直接,可是當應用變得複雜則代碼將難以維護,然後者框架幫咱們實現了一系列的操做,無需管理過程,優點顯然可見。java
爲了實現這一點,就是實現如何輸入數據,輸出視圖,咱們就會注意到上面的render函數,render函數的實現,主要在對dom性能的優化上,固然實現方式也多種多樣,直接的innerHTML、使用documentFragment、還有virtual dom,在不一樣場景下性能上有所不一樣,可是框架追求的是在大部分場景中框架已經知足你的優化需求,這裏咱們也不加以贅述,後文會提到。node
固然還有數據變化偵測,從而re-render視圖,數據變化偵測中,值得一提的是數據生產者(Producer
)和數據消費者(Consumer
)之間的聯繫,這裏,咱們能夠暫且將系統(視圖)做爲一個數據的消費者,咱們的代碼設置數據的變化,做爲數據的生產者
咱們這裏能夠分爲系統不可感知數據變化
和系統可感知數據變化
react
Rx.js中是將二者通訊分紅拉取(
Pull
)和推送(Push
),比較很差理解,這裏我本身就分了個類jquery
像React/Angular這類框架並不知道數據何時變了,可是它視圖何時更新呢,好比React就是經過setState發信號告訴系統有可能數據變了,而後經過virtual dom diff去渲染視圖,angular則是有一個髒值檢查流程,遍歷比對git
Rx.js / vue這一類響應式的,經過觀察者模式,使用Observable (可觀察對象),Observer (觀察者)(或者是watcher)去訂閱(好比視圖渲染這一類,其實也能夠當成一個觀察者去訂閱數據了,後面會提到),系統是能夠很準確知道哪裏數據變了的,從而也就能實現視圖更新渲染。github
上者系統不可感知數據變化
,粒度粗,有時候還得手動優化(好比pureComponet和shouldComponentUpdate)去跳過一些數據不會更新的視圖從而提高性能
下者系統可感知數據變化
,粒度細,可是綁定大量觀察者,有大量的依賴追蹤的內存開銷
因此
這裏也就終於提到本文的主角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官網的這張圖
我這裏摘出一段關鍵的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?
參考連接/推薦閱讀
謝謝閱讀~
歡迎follow我哈哈github.com/BUPT-HJM
歡迎繼續觀光個人新博客~
歡迎關注