vue事件总线_[面试] 聊聊你对 Vue.js 框架的理解
作者:yacan8
https://github.com/yacan8/blog/issues/26
本文為一次前端技術(shù)分享的演講稿,所以盡力不貼 Vue.js 的源碼,因?yàn)橘N代碼在實(shí)際分享中,比較枯燥,效果不佳,而更多的是以圖片和文字的形式進(jìn)行表達(dá)。
分享目標(biāo):
- 了解 Vue.js 的組件化機(jī)制
- 了解 Vue.js 的響應(yīng)式系統(tǒng)原理
- 了解 Vue.js 中的 Virtual DOM 及 Diff 原理
分享keynote:Vue.js框架原理剖析.key
Vue.js概述
Vue 是一套用于構(gòu)建用戶界面的漸進(jìn)式MVVM框架。那怎么理解漸進(jìn)式呢?漸進(jìn)式含義:強(qiáng)制主張最少。
漸進(jìn)式概念Vue.js包含了聲明式渲染、組件化系統(tǒng)、客戶端路由、大規(guī)模狀態(tài)管理、構(gòu)建工具、數(shù)據(jù)持久化、跨平臺(tái)支持等,但在實(shí)際開(kāi)發(fā)中,并沒(méi)有強(qiáng)制要求開(kāi)發(fā)者之后某一特定功能,而是根據(jù)需求逐漸擴(kuò)展。
Vue.js的核心庫(kù)只關(guān)心視圖渲染,且由于漸進(jìn)式的特性,Vue.js便于與第三方庫(kù)或既有項(xiàng)目整合。
組件機(jī)制
定義:組件就是對(duì)一個(gè)功能和樣式進(jìn)行獨(dú)立的封裝,讓HTML元素得到擴(kuò)展,從而使得代碼得到復(fù)用,使得開(kāi)發(fā)靈活,更加高效。
與HTML元素一樣,Vue.js的組件擁有外部傳入的屬性(prop)和事件,除此之外,組件還擁有自己的狀態(tài)(data)和通過(guò)數(shù)據(jù)和狀態(tài)計(jì)算出來(lái)的計(jì)算屬性(computed),各個(gè)維度組合起來(lái)決定組件最終呈現(xiàn)的樣子與交互的邏輯。
數(shù)據(jù)傳遞
每一個(gè)組件之間的作用域是孤立的,這個(gè)意味著組件之間的數(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字段,如下聲明一個(gè)child組件:
{{msg}}
父組件向該組件傳遞數(shù)據(jù):
"parentMsg">
事件傳遞
Vue內(nèi)部實(shí)現(xiàn)了一個(gè)事件總線系統(tǒng),即EventBus。在Vue中可以使用 EventBus 來(lái)作為溝通橋梁的概念,每一個(gè)Vue的組件實(shí)例都繼承了?EventBus,都可以接受事件$on和發(fā)送事件$emit。
如上面一個(gè)例子,child.vue 組件想修改 parent.vue 組件的 parentMsg 數(shù)據(jù),怎么辦呢?為了保證數(shù)據(jù)流的可追溯性,直接修改組件內(nèi) prop 的 msg 字段是不提倡的,且例子中為非引用類型 String,直接修改也修改不了,這個(gè)時(shí)候需要將修改 parentMsg 的事件傳遞給 child.vue,讓 child.vue 來(lái)觸發(fā)修改 parentMsg 的事件。如:
{{msg}}
父組件:
"parentMsg"?@updateMsg="changeParentMsg">
父組件 parent.vue 向子組件 child.vue 傳遞了 updateMsg 事件,在子組件實(shí)例化的時(shí)候,子組件將 updateMsg 事件使用$on函數(shù)注冊(cè)到組件內(nèi)部,需要觸發(fā)事件的時(shí)候,調(diào)用函數(shù)this.$emit來(lái)觸發(fā)事件。
除了父子組件之間的事件傳遞,還可以使用一個(gè) Vue 實(shí)例為多層級(jí)的父子組件建立數(shù)據(jù)通信的橋梁,如:
const?eventBus?=?new?Vue();//?父組件中使用$on監(jiān)聽(tīng)事件
eventBus.$on('eventName',?val?=>?{
????//??...do?something
})
//?子組件使用$emit觸發(fā)事件
eventBus.$emit('eventName',?'this?is?a?message.');
除了$on和$emit以外,事件總線系統(tǒng)還提供了另外兩個(gè)方法,$once和$off,所有事件如下:
- $on:監(jiān)聽(tīng)、注冊(cè)事件。
- $emit:觸發(fā)事件。
- $once:注冊(cè)事件,僅允許該事件觸發(fā)一次,觸發(fā)結(jié)束后立即移除事件。
- $off:移除事件。
內(nèi)容分發(fā)
Vue實(shí)現(xiàn)了一套遵循?Web Components 規(guī)范草案?的內(nèi)容分發(fā)系統(tǒng),即將元素作為承載分發(fā)內(nèi)容的出口。
插槽slot,也是組件的一塊HTML模板,這一塊模板顯示不顯示、以及怎樣顯示由父組件來(lái)決定。實(shí)際上,一個(gè)slot最核心的兩個(gè)問(wèn)題在這里就點(diǎn)出來(lái)了,是顯示不顯示和怎樣顯示。
插槽又分默認(rèn)插槽、具名插槽。
默認(rèn)插槽
又名單個(gè)插槽、匿名插槽,與具名插槽相對(duì),這類插槽沒(méi)有具體名字,一個(gè)組件只能有一個(gè)該類插槽。
如:
"parent">父容器
"tmpl">菜單1"child">
子組件
如上,渲染時(shí)子組件的slot標(biāo)簽會(huì)被父組件傳入的div.tmpl替換。
具名插槽
匿名插槽沒(méi)有name屬性,所以叫匿名插槽。那么,插槽加了name屬性,就變成了具名插槽。具名插槽可以在一個(gè)組件中出現(xiàn)N次,出現(xiàn)在不同的位置,只需要使用不同的name屬性區(qū)分即可。
如:
"parent">父容器
"tmpl"?slot="up">菜單up-1"tmpl"?slot="down">菜單down-1"tmpl">菜單->1"child">"up">
這里是子組件
"down">如上,slot 標(biāo)簽會(huì)根據(jù)父容器給 child 標(biāo)簽內(nèi)傳入的內(nèi)容的 slot 屬性值,替換對(duì)應(yīng)的內(nèi)容。
其實(shí),默認(rèn)插槽也有 name 屬性值,為default,同樣指定 slot 的 name 值為 default,一樣可以顯示父組件中傳入的沒(méi)有指定slot的內(nèi)容。
作用域插槽
作用域插槽可以是默認(rèn)插槽,也可以是具名插槽,不一樣的地方是,作用域插槽可以為 slot 標(biāo)簽綁定數(shù)據(jù),讓其父組件可以獲取到子組件的數(shù)據(jù)。
如:
"parent">這是父組件
"default"?slot-scope="slotProps">????????????????{{?slotProps.user.name?}}
"child">
這是子組件
"user">如上例子,子組件 child 在渲染默認(rèn)插槽 slot 的時(shí)候,將數(shù)據(jù) user 傳遞給了 slot 標(biāo)簽,在渲染過(guò)程中,父組件可以通過(guò)slot-scope屬性獲取到 user 數(shù)據(jù)并渲染視圖。
slot 實(shí)現(xiàn)原理:當(dāng)子組件vm實(shí)例化時(shí),獲取到父組件傳入的 slot 標(biāo)簽的內(nèi)容,存放在vm.$slot中,默認(rèn)插槽為vm.$slot.default,具名插槽為vm.$slot.xxx,xxx 為 插槽名,當(dāng)組件執(zhí)行渲染函數(shù)時(shí)候,遇到標(biāo)簽,使用$slot中的內(nèi)容進(jìn)行替換,此時(shí)可以為插槽傳遞數(shù)據(jù),若存在數(shù)據(jù),則可曾該插槽為作用域插槽。
至此,父子組件的關(guān)系如下圖:
父子組件關(guān)系圖模板渲染
Vue.js 的核心是聲明式渲染,與命令式渲染不同,聲明式渲染只需要告訴程序,我們想要的什么效果,其他的事情讓程序自己去做。而命令式渲染,需要命令程序一步一步根據(jù)命令執(zhí)行渲染。如下例子區(qū)分:
var?arr?=?[1,?2,?3,?4,?5];//?命令式渲染,關(guān)心每一步、關(guān)心流程。用命令去實(shí)現(xiàn)
var?newArr?=?[];
for?(var?i?=?0;?i?????newArr.push(arr[i]?*?2);
}
//?聲明式渲染,不用關(guān)心中間流程,只需要關(guān)心結(jié)果和實(shí)現(xiàn)的條件
var?newArr1?=?arr.map(function?(item)?{
????return?item?*?2;
});
Vue.js 實(shí)現(xiàn)了if、for、事件、數(shù)據(jù)綁定等指令,允許采用簡(jiǎn)潔的模板語(yǔ)法來(lái)聲明式地將數(shù)據(jù)渲染出視圖。
模板編譯
為什么要進(jìn)行模板編譯?實(shí)際上,我們組件中的 template 語(yǔ)法是無(wú)法被瀏覽器解析的,因?yàn)樗皇钦_的 HTML 語(yǔ)法,而模板編譯,就是將組件的 template 編譯成可執(zhí)行的 JavaScript 代碼,即將 template 轉(zhuǎn)化為真正的渲染函數(shù)。
模板編譯分三個(gè)階段,parse、optimize、generate,最終生成render函數(shù)。
模板編譯parse階段:使用正在表達(dá)式將template進(jìn)行字符串解析,得到指令、class、style等數(shù)據(jù),生成抽象語(yǔ)法樹(shù) AST。
optimize階段:尋找 AST 中的靜態(tài)節(jié)點(diǎn)進(jìn)行標(biāo)記,為后面 VNode 的 patch 過(guò)程中對(duì)比做優(yōu)化。被標(biāo)記為 static 的節(jié)點(diǎn)在后面的 diff 算法中會(huì)被直接忽略,不做詳細(xì)的比較。
generate階段:根據(jù) AST 結(jié)構(gòu)拼接生成 render 函數(shù)的字符串。
預(yù)編譯
對(duì)于 Vue 組件來(lái)說(shuō),模板編譯只會(huì)在組件實(shí)例化的時(shí)候編譯一次,生成渲染函數(shù)之后在也不會(huì)進(jìn)行編譯。因此,編譯對(duì)組件的 runtime 是一種性能損耗。而模板編譯的目的僅僅是將template轉(zhuǎn)化為render function,而這個(gè)過(guò)程,正好可以在項(xiàng)目構(gòu)建的過(guò)程中完成。
比如webpack的vue-loader依賴了vue-template-compiler模塊,在 webpack 構(gòu)建過(guò)程中,將template預(yù)編譯成 render 函數(shù),在 runtime 可直接跳過(guò)模板編譯過(guò)程。
回過(guò)頭看,runtime 需要是僅僅是 render 函數(shù),而我們有了預(yù)編譯之后,我們只需要保證構(gòu)建過(guò)程中生成 render 函數(shù)就可以。與 React 類似,在添加JSX的語(yǔ)法糖編譯器babel-plugin-transform-vue-jsx之后,我們可以在 Vue 組件中使用JSX語(yǔ)法直接書寫 render 函數(shù)。
如上面組件,使用 JSX 之后,可以在 JS 代碼中直接使用 html 標(biāo)簽,而且聲明了 render 函數(shù)以后,我們不再需要聲明 template。當(dāng)然,假如我們同時(shí)聲明了 template 標(biāo)簽和 render 函數(shù),構(gòu)建過(guò)程中,template 編譯的結(jié)果將覆蓋原有的 render 函數(shù),即 template 的優(yōu)先級(jí)高于直接書寫的 render 函數(shù)。
相對(duì)于 template 而言,JSX 具有更高的靈活性,面對(duì)與一些復(fù)雜的組件來(lái)說(shuō),JSX 有著天然的優(yōu)勢(shì),而 template 雖然顯得有些呆滯,但是代碼結(jié)構(gòu)上更符合視圖與邏輯分離的習(xí)慣,更簡(jiǎn)單、更直觀、更好維護(hù)。
需要注意的是,最后生成的 render 函數(shù)是被包裹在with語(yǔ)法中運(yùn)行的。
小結(jié)
Vue 組件通過(guò) prop 進(jìn)行數(shù)據(jù)傳遞,并實(shí)現(xiàn)了數(shù)據(jù)總線系統(tǒng)EventBus,組件集成了EventBus進(jìn)行事件注冊(cè)監(jiān)聽(tīng)、事件觸發(fā),使用slot進(jìn)行內(nèi)容分發(fā)。
除此以外,實(shí)現(xiàn)了一套聲明式模板系統(tǒng),在runtime或者預(yù)編譯是對(duì)模板進(jìn)行編譯,生成渲染函數(shù),供組件渲染視圖使用。
響應(yīng)式系統(tǒng)
Vue.js 是一款 MVVM 的JS框架,當(dāng)對(duì)數(shù)據(jù)模型data進(jìn)行修改時(shí),視圖會(huì)自動(dòng)得到更新,即框架幫我們完成了更新DOM的操作,而不需要我們手動(dòng)的操作DOM。可以這么理解,當(dāng)我們對(duì)數(shù)據(jù)進(jìn)行賦值的時(shí)候,Vue 告訴了所有依賴該數(shù)據(jù)模型的組件,你依賴的數(shù)據(jù)有更新,你需要進(jìn)行重渲染了,這個(gè)時(shí)候,組件就會(huì)重渲染,完成了視圖的更新。
數(shù)據(jù)模型 && 計(jì)算屬性 && 監(jiān)聽(tīng)器
在組件中,可以為每個(gè)組件定義數(shù)據(jù)模型data、計(jì)算屬性computed、監(jiān)聽(tīng)器watch。
數(shù)據(jù)模型:Vue 實(shí)例在創(chuàng)建過(guò)程中,對(duì)數(shù)據(jù)模型data的每一個(gè)屬性加入到響應(yīng)式系統(tǒng)中,當(dāng)數(shù)據(jù)被更改時(shí),視圖將得到響應(yīng),同步更新。data必須采用函數(shù)的方式 return,不使用 return 包裹的數(shù)據(jù)會(huì)在項(xiàng)目的全局可見(jiàn),會(huì)造成變量污染;使用return包裹后數(shù)據(jù)中變量只在當(dāng)前組件中生效,不會(huì)影響其他組件。
計(jì)算屬性:computed基于組件響應(yīng)式依賴進(jìn)行計(jì)算得到結(jié)果并緩存起來(lái)。只在相關(guān)響應(yīng)式依賴發(fā)生改變時(shí)它們才會(huì)重新求值,也就是說(shuō),只有它依賴的響應(yīng)式數(shù)據(jù)(data、prop、computed本身)發(fā)生變化了才會(huì)重新計(jì)算。那什么時(shí)候應(yīng)該使用計(jì)算屬性呢?模板內(nèi)的表達(dá)式非常便利,但是設(shè)計(jì)它們的初衷是用于簡(jiǎn)單運(yùn)算的。在模板中放入太多的邏輯會(huì)讓模板過(guò)重且難以維護(hù)。對(duì)于任何復(fù)雜邏輯,你都應(yīng)當(dāng)使用計(jì)算屬性。
監(jiān)聽(tīng)器:監(jiān)聽(tīng)器watch作用如其名,它可以監(jiān)聽(tīng)響應(yīng)式數(shù)據(jù)的變化,響應(yīng)式數(shù)據(jù)包括 data、prop、computed,當(dāng)響應(yīng)式數(shù)據(jù)發(fā)生變化時(shí),可以做出相應(yīng)的處理。當(dāng)需要在數(shù)據(jù)變化時(shí)執(zhí)行異步或開(kāi)銷較大的操作時(shí),這個(gè)方式是最有用的。
響應(yīng)式原理
在 Vue 中,數(shù)據(jù)模型下的所有屬性,會(huì)被 Vue 使用Object.defineProperty(Vue3.0 使用 Proxy)進(jìn)行數(shù)據(jù)劫持代理。響應(yīng)式的核心機(jī)制是觀察者模式,數(shù)據(jù)是被觀察的一方,一旦發(fā)生變化,通知所有觀察者,這樣觀察者可以做出響應(yīng),比如當(dāng)觀察者為視圖時(shí),視圖可以做出視圖的更新。
Vue.js 的響應(yīng)式系統(tǒng)以來(lái)三個(gè)重要的概念,Observer、Dep、Watcher。
發(fā)布者-Observer
Observe 扮演的角色是發(fā)布者,他的主要作用是在組件vm初始化的時(shí),調(diào)用defineReactive函數(shù),使用Object.defineProperty方法對(duì)對(duì)象的每一個(gè)子屬性進(jìn)行數(shù)據(jù)劫持/監(jiān)聽(tīng),即為每個(gè)屬性添加getter和setter,將對(duì)應(yīng)的屬性值變成響應(yīng)式。
在組件初始化時(shí),調(diào)用initState函數(shù),內(nèi)部執(zhí)行initState、initProps、initComputed方法,分別對(duì)data、prop、computed進(jìn)行初始化,讓其變成響應(yīng)式。
初始化props時(shí),對(duì)所有props進(jìn)行遍歷,調(diào)用defineReactive函數(shù),將每個(gè) prop 屬性值變成響應(yīng)式,然后將其掛載到_props中,然后通過(guò)代理,把vm.xxx代理到vm._props.xxx中。
同理,初始化data時(shí),與prop相同,對(duì)所有data進(jìn)行遍歷,調(diào)用defineReactive函數(shù),將每個(gè) data 屬性值變成響應(yīng)式,然后將其掛載到_data中,然后通過(guò)代理,把vm.xxx代理到vm._data.xxx中。
初始化computed,首先創(chuàng)建一個(gè)觀察者對(duì)象computed-watcher,然后遍歷computed的每一個(gè)屬性,對(duì)每一個(gè)屬性值調(diào)用defineComputed方法,使用Object.defineProperty將其變成響應(yīng)式的同時(shí),將其代理到組件實(shí)例上,即可通過(guò)vm.xxx訪問(wèn)到xxx計(jì)算屬性。
調(diào)度中心/訂閱器-Dep
Dep 扮演的角色是調(diào)度中心/訂閱器,在調(diào)用defineReactive將屬性值變成響應(yīng)式的過(guò)程中,也為每個(gè)屬性值實(shí)例化了一個(gè)Dep,主要作用是對(duì)觀察者(Watcher)進(jìn)行管理,收集觀察者和通知觀察者目標(biāo)更新,即當(dāng)屬性值數(shù)據(jù)發(fā)生改變時(shí),會(huì)遍歷觀察者列表(dep.subs),通知所有的 watcher,讓訂閱者執(zhí)行自己的update邏輯。
其dep的任務(wù)是,在屬性的getter方法中,調(diào)用dep.depend()方法,將觀察者(即 Watcher,可能是組件的render function,可能是 computed,也可能是屬性監(jiān)聽(tīng) watch)保存在內(nèi)部,完成其依賴收集。在屬性的setter方法中,調(diào)用dep.notify()方法,通知所有觀察者執(zhí)行更新,完成派發(fā)更新。
觀察者-Watcher
Watcher 扮演的角色是訂閱者/觀察者,他的主要作用是為觀察屬性提供回調(diào)函數(shù)以及收集依賴,當(dāng)被觀察的值發(fā)生變化時(shí),會(huì)接收到來(lái)自調(diào)度中心Dep的通知,從而觸發(fā)回調(diào)函數(shù)。
而Watcher又分為三類,normal-watcher、?computed-watcher、?render-watcher。
normal-watcher:在組件鉤子函數(shù)watch中定義,即監(jiān)聽(tīng)的屬性改變了,都會(huì)觸發(fā)定義好的回調(diào)函數(shù)。
computed-watcher:在組件鉤子函數(shù)computed中定義的,每一個(gè)computed屬性,最后都會(huì)生成一個(gè)對(duì)應(yīng)的Watcher對(duì)象,但是這類Watcher有個(gè)特點(diǎn):當(dāng)計(jì)算屬性依賴于其他數(shù)據(jù)時(shí),屬性并不會(huì)立即重新計(jì)算,只有之后其他地方需要讀取屬性的時(shí)候,它才會(huì)真正計(jì)算,即具備lazy(懶計(jì)算)特性。
render-watcher:每一個(gè)組件都會(huì)有一個(gè)render-watcher, 當(dāng)data/computed中的屬性改變的時(shí)候,會(huì)調(diào)用該Watcher來(lái)更新組件的視圖。
這三種Watcher也有固定的執(zhí)行順序,分別是:computed-render -> normal-watcher -> render-watcher。這樣就能盡可能的保證,在更新組件視圖的時(shí)候,computed 屬性已經(jīng)是最新值了,如果 render-watcher 排在 computed-render 前面,就會(huì)導(dǎo)致頁(yè)面更新的時(shí)候 computed 值為舊數(shù)據(jù)。
小結(jié)
響應(yīng)式系統(tǒng)Observer 負(fù)責(zé)將數(shù)據(jù)進(jìn)行攔截,Watcher 負(fù)責(zé)訂閱,觀察數(shù)據(jù)變化, Dep 負(fù)責(zé)接收訂閱并通知 Observer 和接收發(fā)布并通知所有 Watcher。
Virtual DOM
在 Vue 中,template被編譯成瀏覽器可執(zhí)行的render function,然后配合響應(yīng)式系統(tǒng),將render function掛載在render-watcher中,當(dāng)有數(shù)據(jù)更改的時(shí)候,調(diào)度中心Dep通知該render-watcher執(zhí)行render function,完成視圖的渲染與更新。
DOM更新整個(gè)流程看似通順,但是當(dāng)執(zhí)行render function時(shí),如果每次都全量刪除并重建 DOM,這對(duì)執(zhí)行性能來(lái)說(shuō),無(wú)疑是一種巨大的損耗,因?yàn)槲覀冎?#xff0c;瀏覽器的DOM很“昂貴”的,當(dāng)我們頻繁的更新 DOM,會(huì)產(chǎn)生一定的性能問(wèn)題。
為了解決這個(gè)問(wèn)題,Vue 使用 JS 對(duì)象將瀏覽器的 DOM 進(jìn)行的抽象,這個(gè)抽象被稱為 Virtual DOM。Virtual DOM 的每個(gè)節(jié)點(diǎn)被定義為VNode,當(dāng)每次執(zhí)行render function時(shí),Vue 對(duì)更新前后的VNode進(jìn)行Diff對(duì)比,找出盡可能少的我們需要更新的真實(shí) DOM 節(jié)點(diǎn),然后只更新需要更新的節(jié)點(diǎn),從而解決頻繁更新 DOM 產(chǎn)生的性能問(wèn)題。
VNode
VNode,全稱virtual node,即虛擬節(jié)點(diǎn),對(duì)真實(shí) DOM 節(jié)點(diǎn)的虛擬描述,在 Vue 的每一個(gè)組件實(shí)例中,會(huì)掛載一個(gè)$createElement函數(shù),所有的VNode都是由這個(gè)函數(shù)創(chuàng)建的。
比如創(chuàng)建一個(gè) div:
//?聲明?render?functionrender:?function?(createElement)?{
????//?也可以使用?this.$createElement?創(chuàng)建?VNode
????return?createElement('div',?'hellow?world');
}
//?以上?render?方法返回html片段?hellow?world
render 函數(shù)執(zhí)行后,會(huì)根據(jù)VNode Tree將 VNode 映射生成真實(shí) DOM,從而完成視圖的渲染。
Diff
Diff 將新老 VNode 節(jié)點(diǎn)進(jìn)行比對(duì),然后將根據(jù)兩者的比較結(jié)果進(jìn)行最小單位地修改視圖,而不是將整個(gè)視圖根據(jù)新的 VNode 重繪,進(jìn)而達(dá)到提升性能的目的。
patch
Vue.js 內(nèi)部的 diff 被稱為patch。其 diff 算法的是通過(guò)同層的樹(shù)節(jié)點(diǎn)進(jìn)行比較,而非對(duì)樹(shù)進(jìn)行逐層搜索遍歷的方式,所以時(shí)間復(fù)雜度只有O(n),是一種相當(dāng)高效的算法。
DIFF首先定義新老節(jié)點(diǎn)是否相同判定函數(shù)sameVnode:滿足鍵值key和標(biāo)簽名tag必須一致等條件,返回true,否則false。
在進(jìn)行patch之前,新老 VNode 是否滿足條件sameVnode(oldVnode, newVnode),滿足條件之后,進(jìn)入流程patchVnode,否則被判定為不相同節(jié)點(diǎn),此時(shí)會(huì)移除老節(jié)點(diǎn),創(chuàng)建新節(jié)點(diǎn)。
patchVnode
patchVnode 的主要作用是判定如何對(duì)子節(jié)點(diǎn)進(jìn)行更新,
如果新舊VNode都是靜態(tài)的,同時(shí)它們的key相同(代表同一節(jié)點(diǎn)),并且新的 VNode 是 clone 或者是標(biāo)記了 once(標(biāo)記v-once屬性,只渲染一次),那么只需要替換 DOM 以及 VNode 即可。
新老節(jié)點(diǎn)均有子節(jié)點(diǎn),則對(duì)子節(jié)點(diǎn)進(jìn)行 diff 操作,進(jìn)行updateChildren,這個(gè) updateChildren 也是 diff 的核心。
如果老節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)而新節(jié)點(diǎn)存在子節(jié)點(diǎn),先清空老節(jié)點(diǎn) DOM 的文本內(nèi)容,然后為當(dāng)前 DOM 節(jié)點(diǎn)加入子節(jié)點(diǎn)。
當(dāng)新節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn)而老節(jié)點(diǎn)有子節(jié)點(diǎn)的時(shí)候,則移除該 DOM 節(jié)點(diǎn)的所有子節(jié)點(diǎn)。
當(dāng)新老節(jié)點(diǎn)都無(wú)子節(jié)點(diǎn)的時(shí)候,只是文本的替換。
updateChildren
Diff 的核心,對(duì)比新老子節(jié)點(diǎn)數(shù)據(jù),判定如何對(duì)子節(jié)點(diǎn)進(jìn)行操作,在對(duì)比過(guò)程中,由于老的子節(jié)點(diǎn)存在對(duì)當(dāng)前真實(shí) DOM 的引用,新的子節(jié)點(diǎn)只是一個(gè) VNode 數(shù)組,所以在進(jìn)行遍歷的過(guò)程中,若發(fā)現(xiàn)需要更新真實(shí) DOM 的地方,則會(huì)直接在老的子節(jié)點(diǎn)上進(jìn)行真實(shí) DOM 的操作,等到遍歷結(jié)束,新老子節(jié)點(diǎn)則已同步結(jié)束。
updateChildren內(nèi)部定義了4個(gè)變量,分別是oldStartIdx、oldEndIdx、newStartIdx、newEndIdx,分別表示正在 Diff 對(duì)比的新老子節(jié)點(diǎn)的左右邊界點(diǎn)索引,在老子節(jié)點(diǎn)數(shù)組中,索引在oldStartIdx與oldEndIdx中間的節(jié)點(diǎn),表示老子節(jié)點(diǎn)中為被遍歷處理的節(jié)點(diǎn),所以小于oldStartIdx或大于oldEndIdx的表示未被遍歷處理的節(jié)點(diǎn)。同理,在新的子節(jié)點(diǎn)數(shù)組中,索引在newStartIdx與newEndIdx中間的節(jié)點(diǎn),表示老子節(jié)點(diǎn)中為被遍歷處理的節(jié)點(diǎn),所以小于newStartIdx或大于newEndIdx的表示未被遍歷處理的節(jié)點(diǎn)。
每一次遍歷,oldStartIdx和oldEndIdx與newStartIdx和newEndIdx之間的距離會(huì)向中間靠攏。當(dāng) oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 時(shí)結(jié)束循環(huán)。
img在遍歷中,取出4索引對(duì)應(yīng)的 Vnode節(jié)點(diǎn):
- oldStartIdx:oldStartVnode
- oldEndIdx:oldEndVnode
- newStartIdx:newStartVnode
- newEndIdx:newEndVnode
diff 過(guò)程中,如果存在key,并且滿足sameVnode,會(huì)將該 DOM 節(jié)點(diǎn)進(jìn)行復(fù)用,否則則會(huì)創(chuàng)建一個(gè)新的 DOM 節(jié)點(diǎn)。
首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較,一共有 2*2=4 種比較方法。
情況一:當(dāng)oldStartVnode與newStartVnode滿足 sameVnode,則oldStartVnode與newStartVnode進(jìn)行 patchVnode,并且oldStartIdx與newStartIdx右移動(dòng)。
情況二:與情況一類似,當(dāng)oldEndVnode與newEndVnode滿足 sameVnode,則oldEndVnode與newEndVnode進(jìn)行 patchVnode,并且oldEndIdx與newEndIdx左移動(dòng)。
情況三:當(dāng)oldStartVnode與newEndVnode滿足 sameVnode,則說(shuō)明oldStartVnode已經(jīng)跑到了oldEndVnode后面去了,此時(shí)oldStartVnode與newEndVnode進(jìn)行 patchVnode 的同時(shí),還需要將oldStartVnode的真實(shí) DOM 節(jié)點(diǎn)移動(dòng)到oldEndVnode的后面,并且oldStartIdx右移,newEndIdx左移。
情況四:與情況三類似,當(dāng)oldEndVnode與newStartVnode滿足 sameVnode,則說(shuō)明oldEndVnode已經(jīng)跑到了oldStartVnode前面去了,此時(shí)oldEndVnode與newStartVnode進(jìn)行 patchVnode 的同時(shí),還需要將oldEndVnode的真實(shí) DOM 節(jié)點(diǎn)移動(dòng)到oldStartVnode的前面,并且oldStartIdx右移,newEndIdx左移。
當(dāng)這四種情況都不滿足,則在oldStartIdx與oldEndIdx之間查找與newStartVnode滿足sameVnode的節(jié)點(diǎn),若存在,則將匹配的節(jié)點(diǎn)真實(shí) DOM 移動(dòng)到oldStartVnode的前面。
若不存在,說(shuō)明newStartVnode為新節(jié)點(diǎn),創(chuàng)建新節(jié)點(diǎn)放在oldStartVnode前面即可。
當(dāng) oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx,循環(huán)結(jié)束,這個(gè)時(shí)候我們需要處理那些未被遍歷到的 VNode。
當(dāng) oldStartIdx > oldEndIdx 時(shí),說(shuō)明老的節(jié)點(diǎn)已經(jīng)遍歷完,而新的節(jié)點(diǎn)沒(méi)遍歷完,這個(gè)時(shí)候需要將新的節(jié)點(diǎn)創(chuàng)建之后放在oldEndVnode后面。
當(dāng) newStartIdx > newEndIdx 時(shí),說(shuō)明新的節(jié)點(diǎn)已經(jīng)遍歷完,而老的節(jié)點(diǎn)沒(méi)遍歷完,這個(gè)時(shí)候要將沒(méi)遍歷的老的節(jié)點(diǎn)全都刪除。
總結(jié)
借用官方的一幅圖:
Vue.js 實(shí)現(xiàn)了一套聲明式渲染引擎,并在runtime或者預(yù)編譯時(shí)將聲明式的模板編譯成渲染函數(shù),掛載在觀察者 Watcher 中,在渲染函數(shù)中(touch),響應(yīng)式系統(tǒng)使用響應(yīng)式數(shù)據(jù)的getter方法對(duì)觀察者進(jìn)行依賴收集(Collect as Dependency),使用響應(yīng)式數(shù)據(jù)的setter方法通知(notify)所有觀察者進(jìn)行更新,此時(shí)觀察者 Watcher 會(huì)觸發(fā)組件的渲染函數(shù)(Trigger re-render),組件執(zhí)行的 render 函數(shù),生成一個(gè)新的 Virtual DOM Tree,此時(shí) Vue 會(huì)對(duì)新老 Virtual DOM Tree 進(jìn)行 Diff,查找出需要操作的真實(shí) DOM 并對(duì)其進(jìn)行更新。
總結(jié)
以上是生活随笔為你收集整理的vue事件总线_[面试] 聊聊你对 Vue.js 框架的理解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 什么是牙齿贴面
- 下一篇: pc 图片预览放大 端vue_移动端Vu