面試官:聊聊對Vue.js框架的理解
作者:yacan8
本文為一次前端技術(shù)分享的演講稿,所以盡力不貼 Vue.js 的源碼,因為貼代碼在實際分享中,比較枯燥,效果不佳,而更多的是以圖片和文字的形式進行表達。
分享目標:
了解 Vue.js 的組件化機制 了解 Vue.js 的響應(yīng)式系統(tǒng)原理 了解 Vue.js 中的 Virtual DOM 及 Diff 原理
分享keynote:Vue.js框架原理剖析.key
原文地址
Vue.js概述
Vue 是一套用于構(gòu)建用戶界面的漸進式MVVM框架。那怎么理解漸進式呢?漸進式含義:強制主張最少。

Vue.js包含了聲明式渲染、組件化系統(tǒng)、客戶端路由、大規(guī)模狀態(tài)管理、構(gòu)建工具、數(shù)據(jù)持久化、跨平臺支持等,但在實際開發(fā)中,并沒有強制要求開發(fā)者之后某一特定功能,而是根據(jù)需求逐漸擴展。
Vue.js的核心庫只關(guān)心視圖渲染,且由于漸進式的特性,Vue.js便于與第三方庫或既有項目整合。
組件機制
定義:組件就是對一個功能和樣式進行獨立的封裝,讓HTML元素得到擴展,從而使得代碼得到復(fù)用,使得開發(fā)靈活,更加高效。
與HTML元素一樣,Vue.js的組件擁有外部傳入的屬性(prop)和事件,除此之外,組件還擁有自己的狀態(tài)(data)和通過數(shù)據(jù)和狀態(tài)計算出來的計算屬性(computed),各個維度組合起來決定組件最終呈現(xiàn)的樣子與交互的邏輯。
數(shù)據(jù)傳遞
每一個組件之間的作用域是孤立的,這個意味著組件之間的數(shù)據(jù)不應(yīng)該出現(xiàn)引用關(guān)系,即使出現(xiàn)了引用關(guān)系,也不允許組件操作組件內(nèi)部以外的其他數(shù)據(jù)。Vue中,允許向組件內(nèi)部傳遞prop數(shù)據(jù),組件內(nèi)部需要顯性地聲明該prop字段,如下聲明一個child組件:
????{{msg}}
父組件向該組件傳遞數(shù)據(jù):
????"parentMsg">
事件傳遞
Vue內(nèi)部實現(xiàn)了一個事件總線系統(tǒng),即EventBus。在Vue中可以使用 EventBus 來作為溝通橋梁的概念,每一個Vue的組件實例都繼承了?EventBus,都可以接受事件$on和發(fā)送事件$emit。
如上面一個例子,child.vue 組件想修改 parent.vue 組件的 parentMsg 數(shù)據(jù),怎么辦呢?為了保證數(shù)據(jù)流的可追溯性,直接修改組件內(nèi) prop 的 msg 字段是不提倡的,且例子中為非引用類型 String,直接修改也修改不了,這個時候需要將修改 parentMsg 的事件傳遞給 child.vue,讓 child.vue 來觸發(fā)修改 parentMsg 的事件。如:
????{{msg}}
父組件:
????"parentMsg"?@updateMsg="changeParentMsg">
父組件 parent.vue 向子組件 child.vue 傳遞了 updateMsg 事件,在子組件實例化的時候,子組件將 updateMsg 事件使用$on函數(shù)注冊到組件內(nèi)部,需要觸發(fā)事件的時候,調(diào)用函數(shù)this.$emit來觸發(fā)事件。
除了父子組件之間的事件傳遞,還可以使用一個 Vue 實例為多層級的父子組件建立數(shù)據(jù)通信的橋梁,如:
const?eventBus?=?new?Vue();
//?父組件中使用$on監(jiān)聽事件
eventBus.$on('eventName',?val?=>?{
????//??...do?something
})
//?子組件使用$emit觸發(fā)事件
eventBus.$emit('eventName',?'this?is?a?message.');
除了$on和$emit以外,事件總線系統(tǒng)還提供了另外兩個方法,$once和$off,所有事件如下:
$on:監(jiān)聽、注冊事件。 $emit:觸發(fā)事件。 $once:注冊事件,僅允許該事件觸發(fā)一次,觸發(fā)結(jié)束后立即移除事件。 $off:移除事件。
內(nèi)容分發(fā)
Vue實現(xiàn)了一套遵循?Web Components 規(guī)范草案?的內(nèi)容分發(fā)系統(tǒng),即將元素作為承載分發(fā)內(nèi)容的出口。
插槽slot,也是組件的一塊HTML模板,這一塊模板顯示不顯示、以及怎樣顯示由父組件來決定。實際上,一個slot最核心的兩個問題在這里就點出來了,是顯示不顯示和怎樣顯示。
插槽又分默認插槽、具名插槽。
默認插槽
又名單個插槽、匿名插槽,與具名插槽相對,這類插槽沒有具體名字,一個組件只能有一個該類插槽。
如:
"parent">
????父容器
????
????????"tmpl">
????????????菜單1
????????
????
"child">
????子組件
????
如上,渲染時子組件的slot標簽會被父組件傳入的div.tmpl替換。
具名插槽
匿名插槽沒有name屬性,所以叫匿名插槽。那么,插槽加了name屬性,就變成了具名插槽。具名插槽可以在一個組件中出現(xiàn)N次,出現(xiàn)在不同的位置,只需要使用不同的name屬性區(qū)分即可。
如:
"parent">
????父容器
????
????????"tmpl"?slot="up">
????????????菜單up-1
????????
????????"tmpl"?slot="down">
????????????菜單down-1
????????
????????"tmpl">
????????????菜單->1
????????
????
????"child">
????????
????????"up">
????????這里是子組件
????????
????????"down">
????????
????????
????
如上,slot 標簽會根據(jù)父容器給 child 標簽內(nèi)傳入的內(nèi)容的 slot 屬性值,替換對應(yīng)的內(nèi)容。
其實,默認插槽也有 name 屬性值,為default,同樣指定 slot 的 name 值為 default,一樣可以顯示父組件中傳入的沒有指定slot的內(nèi)容。
作用域插槽
作用域插槽可以是默認插槽,也可以是具名插槽,不一樣的地方是,作用域插槽可以為 slot 標簽綁定數(shù)據(jù),讓其父組件可以獲取到子組件的數(shù)據(jù)。
如:
????
????"parent">
????????這是父組件
????????
????????????"default"?slot-scope="slotProps">
????????????????{{?slotProps.user.name?}}
????????????
????????
????
????
????"child">
????????這是子組件
????????"user">
????
如上例子,子組件 child 在渲染默認插槽 slot 的時候,將數(shù)據(jù) user 傳遞給了 slot 標簽,在渲染過程中,父組件可以通過slot-scope屬性獲取到 user 數(shù)據(jù)并渲染視圖。
slot 實現(xiàn)原理:當(dāng)子組件vm實例化時,獲取到父組件傳入的 slot 標簽的內(nèi)容,存放在vm.$slot中,默認插槽為vm.$slot.default,具名插槽為vm.$slot.xxx,xxx 為 插槽名,當(dāng)組件執(zhí)行渲染函數(shù)時候,遇到標簽,使用$slot中的內(nèi)容進行替換,此時可以為插槽傳遞數(shù)據(jù),若存在數(shù)據(jù),則可曾該插槽為作用域插槽。
至此,父子組件的關(guān)系如下圖:

模板渲染
Vue.js 的核心是聲明式渲染,與命令式渲染不同,聲明式渲染只需要告訴程序,我們想要的什么效果,其他的事情讓程序自己去做。而命令式渲染,需要命令程序一步一步根據(jù)命令執(zhí)行渲染。如下例子區(qū)分:
var?arr?=?[1,?2,?3,?4,?5];
//?命令式渲染,關(guān)心每一步、關(guān)心流程。用命令去實現(xiàn)
var?newArr?=?[];
for?(var?i?=?0;?i?????newArr.push(arr[i]?*?2);
}
//?聲明式渲染,不用關(guān)心中間流程,只需要關(guān)心結(jié)果和實現(xiàn)的條件
var?newArr1?=?arr.map(function?(item)?{
????return?item?*?2;
});
Vue.js 實現(xiàn)了if、for、事件、數(shù)據(jù)綁定等指令,允許采用簡潔的模板語法來聲明式地將數(shù)據(jù)渲染出視圖。
模板編譯
為什么要進行模板編譯?實際上,我們組件中的 template 語法是無法被瀏覽器解析的,因為它不是正確的 HTML 語法,而模板編譯,就是將組件的 template 編譯成可執(zhí)行的 JavaScript 代碼,即將 template 轉(zhuǎn)化為真正的渲染函數(shù)。
模板編譯分三個階段,parse、optimize、generate,最終生成render函數(shù)。

parse階段:使用正在表達式將template進行字符串解析,得到指令、class、style等數(shù)據(jù),生成抽象語法樹 AST。
optimize階段:尋找 AST 中的靜態(tài)節(jié)點進行標記,為后面 VNode 的 patch 過程中對比做優(yōu)化。被標記為 static 的節(jié)點在后面的 diff 算法中會被直接忽略,不做詳細的比較。
generate階段:根據(jù) AST 結(jié)構(gòu)拼接生成 render 函數(shù)的字符串。
預(yù)編譯
對于 Vue 組件來說,模板編譯只會在組件實例化的時候編譯一次,生成渲染函數(shù)之后在也不會進行編譯。因此,編譯對組件的 runtime 是一種性能損耗。而模板編譯的目的僅僅是將template轉(zhuǎn)化為render function,而這個過程,正好可以在項目構(gòu)建的過程中完成。
比如webpack的vue-loader依賴了vue-template-compiler模塊,在 webpack 構(gòu)建過程中,將template預(yù)編譯成 render 函數(shù),在 runtime 可直接跳過模板編譯過程。
回過頭看,runtime 需要是僅僅是 render 函數(shù),而我們有了預(yù)編譯之后,我們只需要保證構(gòu)建過程中生成 render 函數(shù)就可以。與 React 類似,在添加JSX的語法糖編譯器babel-plugin-transform-vue-jsx之后,我們可以在 Vue 組件中使用JSX語法直接書寫 render 函數(shù)。
如上面組件,使用 JSX 之后,可以在 JS 代碼中直接使用 html 標簽,而且聲明了 render 函數(shù)以后,我們不再需要聲明 template。當(dāng)然,假如我們同時聲明了 template 標簽和 render 函數(shù),構(gòu)建過程中,template 編譯的結(jié)果將覆蓋原有的 render 函數(shù),即 template 的優(yōu)先級高于直接書寫的 render 函數(shù)。
相對于 template 而言,JSX 具有更高的靈活性,面對與一些復(fù)雜的組件來說,JSX 有著天然的優(yōu)勢,而 template 雖然顯得有些呆滯,但是代碼結(jié)構(gòu)上更符合視圖與邏輯分離的習(xí)慣,更簡單、更直觀、更好維護。
需要注意的是,最后生成的 render 函數(shù)是被包裹在with語法中運行的。
小結(jié)
Vue 組件通過 prop 進行數(shù)據(jù)傳遞,并實現(xiàn)了數(shù)據(jù)總線系統(tǒng)EventBus,組件集成了EventBus進行事件注冊監(jiān)聽、事件觸發(fā),使用slot進行內(nèi)容分發(fā)。
除此以外,實現(xiàn)了一套聲明式模板系統(tǒng),在runtime或者預(yù)編譯是對模板進行編譯,生成渲染函數(shù),供組件渲染視圖使用。
響應(yīng)式系統(tǒng)
Vue.js 是一款 MVVM 的JS框架,當(dāng)對數(shù)據(jù)模型data進行修改時,視圖會自動得到更新,即框架幫我們完成了更新DOM的操作,而不需要我們手動的操作DOM。可以這么理解,當(dāng)我們對數(shù)據(jù)進行賦值的時候,Vue 告訴了所有依賴該數(shù)據(jù)模型的組件,你依賴的數(shù)據(jù)有更新,你需要進行重渲染了,這個時候,組件就會重渲染,完成了視圖的更新。
數(shù)據(jù)模型 && 計算屬性 && 監(jiān)聽器
在組件中,可以為每個組件定義數(shù)據(jù)模型data、計算屬性computed、監(jiān)聽器watch。
數(shù)據(jù)模型:Vue 實例在創(chuàng)建過程中,對數(shù)據(jù)模型data的每一個屬性加入到響應(yīng)式系統(tǒng)中,當(dāng)數(shù)據(jù)被更改時,視圖將得到響應(yīng),同步更新。data必須采用函數(shù)的方式 return,不使用 return 包裹的數(shù)據(jù)會在項目的全局可見,會造成變量污染;使用return包裹后數(shù)據(jù)中變量只在當(dāng)前組件中生效,不會影響其他組件。
計算屬性:computed基于組件響應(yīng)式依賴進行計算得到結(jié)果并緩存起來。只在相關(guān)響應(yīng)式依賴發(fā)生改變時它們才會重新求值,也就是說,只有它依賴的響應(yīng)式數(shù)據(jù)(data、prop、computed本身)發(fā)生變化了才會重新計算。那什么時候應(yīng)該使用計算屬性呢?模板內(nèi)的表達式非常便利,但是設(shè)計它們的初衷是用于簡單運算的。在模板中放入太多的邏輯會讓模板過重且難以維護。對于任何復(fù)雜邏輯,你都應(yīng)當(dāng)使用計算屬性。
監(jiān)聽器:監(jiān)聽器watch作用如其名,它可以監(jiān)聽響應(yīng)式數(shù)據(jù)的變化,響應(yīng)式數(shù)據(jù)包括 data、prop、computed,當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時,可以做出相應(yīng)的處理。當(dāng)需要在數(shù)據(jù)變化時執(zhí)行異步或開銷較大的操作時,這個方式是最有用的。
響應(yīng)式原理
在 Vue 中,數(shù)據(jù)模型下的所有屬性,會被 Vue 使用Object.defineProperty(Vue3.0 使用 Proxy)進行數(shù)據(jù)劫持代理。響應(yīng)式的核心機制是觀察者模式,數(shù)據(jù)是被觀察的一方,一旦發(fā)生變化,通知所有觀察者,這樣觀察者可以做出響應(yīng),比如當(dāng)觀察者為視圖時,視圖可以做出視圖的更新。
Vue.js 的響應(yīng)式系統(tǒng)以來三個重要的概念,Observer、Dep、Watcher。
發(fā)布者-Observer
Observe 扮演的角色是發(fā)布者,他的主要作用是在組件vm初始化的時,調(diào)用defineReactive函數(shù),使用Object.defineProperty方法對對象的每一個子屬性進行數(shù)據(jù)劫持/監(jiān)聽,即為每個屬性添加getter和setter,將對應(yīng)的屬性值變成響應(yīng)式。
在組件初始化時,調(diào)用initState函數(shù),內(nèi)部執(zhí)行initState、initProps、initComputed方法,分別對data、prop、computed進行初始化,讓其變成響應(yīng)式。
初始化props時,對所有props進行遍歷,調(diào)用defineReactive函數(shù),將每個 prop 屬性值變成響應(yīng)式,然后將其掛載到_props中,然后通過代理,把vm.xxx代理到vm._props.xxx中。
同理,初始化data時,與prop相同,對所有data進行遍歷,調(diào)用defineReactive函數(shù),將每個 data 屬性值變成響應(yīng)式,然后將其掛載到_data中,然后通過代理,把vm.xxx代理到vm._data.xxx中。
初始化computed,首先創(chuàng)建一個觀察者對象computed-watcher,然后遍歷computed的每一個屬性,對每一個屬性值調(diào)用defineComputed方法,使用Object.defineProperty將其變成響應(yīng)式的同時,將其代理到組件實例上,即可通過vm.xxx訪問到xxx計算屬性。
調(diào)度中心/訂閱器-Dep
Dep 扮演的角色是調(diào)度中心/訂閱器,在調(diào)用defineReactive將屬性值變成響應(yīng)式的過程中,也為每個屬性值實例化了一個Dep,主要作用是對觀察者(Watcher)進行管理,收集觀察者和通知觀察者目標更新,即當(dāng)屬性值數(shù)據(jù)發(fā)生改變時,會遍歷觀察者列表(dep.subs),通知所有的 watcher,讓訂閱者執(zhí)行自己的update邏輯。
其dep的任務(wù)是,在屬性的getter方法中,調(diào)用dep.depend()方法,將觀察者(即 Watcher,可能是組件的render function,可能是 computed,也可能是屬性監(jiān)聽 watch)保存在內(nèi)部,完成其依賴收集。在屬性的setter方法中,調(diào)用dep.notify()方法,通知所有觀察者執(zhí)行更新,完成派發(fā)更新。
觀察者-Watcher
Watcher 扮演的角色是訂閱者/觀察者,他的主要作用是為觀察屬性提供回調(diào)函數(shù)以及收集依賴,當(dāng)被觀察的值發(fā)生變化時,會接收到來自調(diào)度中心Dep的通知,從而觸發(fā)回調(diào)函數(shù)。
而Watcher又分為三類,normal-watcher、?computed-watcher、?render-watcher。
normal-watcher:在組件鉤子函數(shù)
watch中定義,即監(jiān)聽的屬性改變了,都會觸發(fā)定義好的回調(diào)函數(shù)。computed-watcher:在組件鉤子函數(shù)
computed中定義的,每一個computed屬性,最后都會生成一個對應(yīng)的Watcher對象,但是這類Watcher有個特點:當(dāng)計算屬性依賴于其他數(shù)據(jù)時,屬性并不會立即重新計算,只有之后其他地方需要讀取屬性的時候,它才會真正計算,即具備lazy(懶計算)特性。render-watcher:每一個組件都會有一個
render-watcher, 當(dāng)data/computed中的屬性改變的時候,會調(diào)用該Watcher來更新組件的視圖。
這三種Watcher也有固定的執(zhí)行順序,分別是:computed-render -> normal-watcher -> render-watcher。這樣就能盡可能的保證,在更新組件視圖的時候,computed 屬性已經(jīng)是最新值了,如果 render-watcher 排在 computed-render 前面,就會導(dǎo)致頁面更新的時候 computed 值為舊數(shù)據(jù)。
小結(jié)

Observer 負責(zé)將數(shù)據(jù)進行攔截,Watcher 負責(zé)訂閱,觀察數(shù)據(jù)變化, Dep 負責(zé)接收訂閱并通知 Observer 和接收發(fā)布并通知所有 Watcher。
Virtual DOM
在 Vue 中,template被編譯成瀏覽器可執(zhí)行的render function,然后配合響應(yīng)式系統(tǒng),將render function掛載在render-watcher中,當(dāng)有數(shù)據(jù)更改的時候,調(diào)度中心Dep通知該render-watcher執(zhí)行render function,完成視圖的渲染與更新。

整個流程看似通順,但是當(dāng)執(zhí)行render function時,如果每次都全量刪除并重建 DOM,這對執(zhí)行性能來說,無疑是一種巨大的損耗,因為我們知道,瀏覽器的DOM很“昂貴”的,當(dāng)我們頻繁的更新 DOM,會產(chǎn)生一定的性能問題。
為了解決這個問題,Vue 使用 JS 對象將瀏覽器的 DOM 進行的抽象,這個抽象被稱為 Virtual DOM。Virtual DOM 的每個節(jié)點被定義為VNode,當(dāng)每次執(zhí)行render function時,Vue 對更新前后的VNode進行Diff對比,找出盡可能少的我們需要更新的真實 DOM 節(jié)點,然后只更新需要更新的節(jié)點,從而解決頻繁更新 DOM 產(chǎn)生的性能問題。
VNode
VNode,全稱virtual node,即虛擬節(jié)點,對真實 DOM 節(jié)點的虛擬描述,在 Vue 的每一個組件實例中,會掛載一個$createElement函數(shù),所有的VNode都是由這個函數(shù)創(chuàng)建的。
比如創(chuàng)建一個 div:
//?聲明?render?function
render:?function?(createElement)?{
????//?也可以使用?this.$createElement?創(chuàng)建?VNode
????return?createElement('div',?'hellow?world');
}
//?以上?render?方法返回html片段?hellow?world
render 函數(shù)執(zhí)行后,會根據(jù)VNode Tree將 VNode 映射生成真實 DOM,從而完成視圖的渲染。
Diff
Diff 將新老 VNode 節(jié)點進行比對,然后將根據(jù)兩者的比較結(jié)果進行最小單位地修改視圖,而不是將整個視圖根據(jù)新的 VNode 重繪,進而達到提升性能的目的。
patch
Vue.js 內(nèi)部的 diff 被稱為patch。其 diff 算法的是通過同層的樹節(jié)點進行比較,而非對樹進行逐層搜索遍歷的方式,所以時間復(fù)雜度只有O(n),是一種相當(dāng)高效的算法。

首先定義新老節(jié)點是否相同判定函數(shù)sameVnode:滿足鍵值key和標簽名tag必須一致等條件,返回true,否則false。
在進行patch之前,新老 VNode 是否滿足條件sameVnode(oldVnode, newVnode),滿足條件之后,進入流程patchVnode,否則被判定為不相同節(jié)點,此時會移除老節(jié)點,創(chuàng)建新節(jié)點。
patchVnode
patchVnode 的主要作用是判定如何對子節(jié)點進行更新,
如果新舊VNode都是靜態(tài)的,同時它們的key相同(代表同一節(jié)點),并且新的 VNode 是 clone 或者是標記了 once(標記v-once屬性,只渲染一次),那么只需要替換 DOM 以及 VNode 即可。
新老節(jié)點均有子節(jié)點,則對子節(jié)點進行 diff 操作,進行
updateChildren,這個 updateChildren 也是 diff 的核心。如果老節(jié)點沒有子節(jié)點而新節(jié)點存在子節(jié)點,先清空老節(jié)點 DOM 的文本內(nèi)容,然后為當(dāng)前 DOM 節(jié)點加入子節(jié)點。
當(dāng)新節(jié)點沒有子節(jié)點而老節(jié)點有子節(jié)點的時候,則移除該 DOM 節(jié)點的所有子節(jié)點。
當(dāng)新老節(jié)點都無子節(jié)點的時候,只是文本的替換。
updateChildren
Diff 的核心,對比新老子節(jié)點數(shù)據(jù),判定如何對子節(jié)點進行操作,在對比過程中,由于老的子節(jié)點存在對當(dāng)前真實 DOM 的引用,新的子節(jié)點只是一個 VNode 數(shù)組,所以在進行遍歷的過程中,若發(fā)現(xiàn)需要更新真實 DOM 的地方,則會直接在老的子節(jié)點上進行真實 DOM 的操作,等到遍歷結(jié)束,新老子節(jié)點則已同步結(jié)束。
updateChildren內(nèi)部定義了4個變量,分別是oldStartIdx、oldEndIdx、newStartIdx、newEndIdx,分別表示正在 Diff 對比的新老子節(jié)點的左右邊界點索引,在老子節(jié)點數(shù)組中,索引在oldStartIdx與oldEndIdx中間的節(jié)點,表示老子節(jié)點中為被遍歷處理的節(jié)點,所以小于oldStartIdx或大于oldEndIdx的表示未被遍歷處理的節(jié)點。同理,在新的子節(jié)點數(shù)組中,索引在newStartIdx與newEndIdx中間的節(jié)點,表示老子節(jié)點中為被遍歷處理的節(jié)點,所以小于newStartIdx或大于newEndIdx的表示未被遍歷處理的節(jié)點。
每一次遍歷,oldStartIdx和oldEndIdx與newStartIdx和newEndIdx之間的距離會向中間靠攏。當(dāng) oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 時結(jié)束循環(huán)。

在遍歷中,取出4索引對應(yīng)的 Vnode節(jié)點:
oldStartIdx:oldStartVnode oldEndIdx:oldEndVnode newStartIdx:newStartVnode newEndIdx:newEndVnode
diff 過程中,如果存在key,并且滿足sameVnode,會將該 DOM 節(jié)點進行復(fù)用,否則則會創(chuàng)建一個新的 DOM 節(jié)點。
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較,一共有 2*2=4 種比較方法。
情況一:當(dāng)oldStartVnode與newStartVnode滿足 sameVnode,則oldStartVnode與newStartVnode進行 patchVnode,并且oldStartIdx與newStartIdx右移動。

情況二:與情況一類似,當(dāng)oldEndVnode與newEndVnode滿足 sameVnode,則oldEndVnode與newEndVnode進行 patchVnode,并且oldEndIdx與newEndIdx左移動。

情況三:當(dāng)oldStartVnode與newEndVnode滿足 sameVnode,則說明oldStartVnode已經(jīng)跑到了oldEndVnode后面去了,此時oldStartVnode與newEndVnode進行 patchVnode 的同時,還需要將oldStartVnode的真實 DOM 節(jié)點移動到oldEndVnode的后面,并且oldStartIdx右移,newEndIdx左移。

情況四:與情況三類似,當(dāng)oldEndVnode與newStartVnode滿足 sameVnode,則說明oldEndVnode已經(jīng)跑到了oldStartVnode前面去了,此時oldEndVnode與newStartVnode進行 patchVnode 的同時,還需要將oldEndVnode的真實 DOM 節(jié)點移動到oldStartVnode的前面,并且oldStartIdx右移,newEndIdx左移。

當(dāng)這四種情況都不滿足,則在oldStartIdx與oldEndIdx之間查找與newStartVnode滿足sameVnode的節(jié)點,若存在,則將匹配的節(jié)點真實 DOM 移動到oldStartVnode的前面。

若不存在,說明newStartVnode為新節(jié)點,創(chuàng)建新節(jié)點放在oldStartVnode前面即可。

當(dāng) oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx,循環(huán)結(jié)束,這個時候我們需要處理那些未被遍歷到的 VNode。
當(dāng) oldStartIdx > oldEndIdx 時,說明老的節(jié)點已經(jīng)遍歷完,而新的節(jié)點沒遍歷完,這個時候需要將新的節(jié)點創(chuàng)建之后放在oldEndVnode后面。

當(dāng) newStartIdx > newEndIdx 時,說明新的節(jié)點已經(jīng)遍歷完,而老的節(jié)點沒遍歷完,這個時候要將沒遍歷的老的節(jié)點全都刪除。

總結(jié)
借用官方的一幅圖:

Vue.js 實現(xiàn)了一套聲明式渲染引擎,并在runtime或者預(yù)編譯時將聲明式的模板編譯成渲染函數(shù),掛載在觀察者 Watcher 中,在渲染函數(shù)中(touch),響應(yīng)式系統(tǒng)使用響應(yīng)式數(shù)據(jù)的getter方法對觀察者進行依賴收集(Collect as Dependency),使用響應(yīng)式數(shù)據(jù)的setter方法通知(notify)所有觀察者進行更新,此時觀察者 Watcher 會觸發(fā)組件的渲染函數(shù)(Trigger re-render),組件執(zhí)行的 render 函數(shù),生成一個新的 Virtual DOM Tree,此時 Vue 會對新老 Virtual DOM Tree 進行 Diff,查找出需要操作的真實 DOM 并對其進行更新。
原文地址
參考
深入理解vue中的slot與slot-scope
vue響應(yīng)式系統(tǒng)--observe、watcher、dep
Vue.js技術(shù)揭秘
VirtualDOM與diff(Vue實現(xiàn))
掃碼關(guān)注公眾號,訂閱更多精彩內(nèi)容。

