研究 runtime
一邊 Vue
一邊源碼html
初看 Vue 是 Vue
源碼是源碼vue
再看 Vue 不是 Vue
源碼不是源碼node
再再看
Vue 是調用棧
源碼也是調用棧git
—— By DOM哥github
Vue 運行時這一塊是很是有意思的,不像 Vue 編譯器那麼枯燥,這裏面有大量的實用技巧和設計思想能夠學習。使用過 Vue 的小夥伴應該對 Vue 【響應的數據綁定】(也叫雙向綁定)的印象很是深入,在修改了數據以後,視圖就會實時獲得相應更新,這無疑極大地減輕了開發者的負擔,使得開發人員能夠專一於處理業務邏輯和操做數據,也就是聞名遐邇的【數據驅動開發】。至於操做 DOM 更新視圖這件苦髒累的活,Vue 已經幫你妥善處理完畢而且對你徹底透明(意思是它就像空氣同樣你徹底注意不到它,卻又深度依賴它,離不開它)。算法
Vue 運行時模塊主要是圍繞 Vue 實例的生命週期展開的,它涵蓋了 Vue 實例生命週期內所須要的所有設施,包括實例建立,響應的數據綁定,掛載到 DOM 節點以及數據變化時自動更新視圖等關鍵部分。本篇也將沿着 Vue 實例的生命週期路線,結合運行時關鍵實現僞代碼,一步步清晰地描繪出 Vue 運行時的空中鳥瞰圖。瀏覽器
本段的部份內容參考自 Vue 官網的生命週期描述。框架
就像每一個人的生命週期有 幼年、童年、少年、青年、中年、老年,每一個 Vue 實例的生命週期也有 beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated、deactivated、beforeDestroy、destroyed 等多個階段。dom
Vue 實例生命週期代碼示例:ide
<div id='index'>{{msg}}</div>
複製代碼
new Vue({
el: '#index',
data: {
msg: 'lifecycle',
},
beforeCreate(){ console.log('beforeCreate')},
created(){ console.log('created')},
beforeMount(){ console.log('beforeMount')},
mounted(){ console.log('mounted')},
})
// Console output:
// beforeCreate
// created
// beforeMount
// mounted
複製代碼
每一個 Vue 實例在被建立時都要通過一系列的初始化過程,例如設置數據監聽,編譯 HTML 模板,將實例掛載到 DOM 等。在這個初始化的過程當中會在特定的地方運行一些叫作【生命週期鉤子】的函數,這些鉤子其實就是開發者能夠自定義的回調函數,如上面傳入的 created
函數就會在 Vue 實例 created 時被調用。
下面一張圖能夠很是清晰地說明 Vue 各個生命週期鉤子的調用時機(圖片來自 Vue 官網生命週期圖示):
Vue 的生命週期圖示
你不須要立馬弄明白圖上全部的東西,不過隨着你的不斷學習和使用,它的參考價值會愈來愈高。
衆所周知 Vue 是經過 new Vue()
的方式進行使用的,也就是說 Vue 內部將本身封裝成了一個類。然而 Vue 並無使用 ES6 最新的 class
方式進行實現,而是用了原來 prototype 那一套,這是讓寶寶有些傷心的。閒話待會再敘,先看一下源碼:
// vue/src/core/instance/index.js
function Vue (options) {
this._init(options)
}
複製代碼
Vue 將初始化工做所有放在了 Vue.prototype._init()
方法裏。去僞存真,_init
方法主代碼以下:
// vue/src/core/instance/init.js
Vue.prototype._init = function (options) {
const vm = this
vm.$options = mergeOptions(options || {})
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製代碼
initEvents
和 initRender
函數主要用來初始化 Vue 實例的一些容器字段,如今可暫時忽略它們。接下來重點來了,在 initState
函數中封裝了實現【響應的數據綁定】的關鍵代碼,雖然這不是 Vue 最流弊的部分,但倒是咱對 Vue 最好奇的地方,也是咱開始本源碼系列的最初動力。在 initState
以前和以後分別調用了 Vue 的生命週期鉤子函數 beforeCreate
和 created
,接下來看看 Vue 是如何實現響應的數據綁定的。
響應的數據綁定並非 Vue 首創的,而是 MVVVM 模式理論的一部分,它是 View 層和 ViewModel 層的鏈接方式。以下圖所示:
MVVM 分層示意圖
Vue 經過【觀察者模式】實現了一套響應式系統。觀察者模式(也叫發佈/訂閱模式)會將觀察者和被觀察的對象嚴格分離開,當被觀察對象的狀態發生變化時,全部依賴於它的觀察者都將獲得通知並自動刷新。舉個栗子,用戶界面能夠做爲一個觀察者,業務數據是被觀察者,用戶界面觀察業務數據的變化,當數據發生變化時,用戶界面就會自動更新。
該模式必須包含兩個角色:觀察者和被觀察對象。Vue 定義了一個 Watcher
類來建立觀察者,定義了一個 Dep
類來建立被觀察對象。 Dep 是 Dependent 的縮寫,意思是做爲觀察者的依賴存在,也就是被觀察對象。
首先看一下【觀察者】 Watcher
的定義:
// vue/src/core/observer/watcher.js
import Dep from './dep'
export default class Watcher {
constructor(vm) {
this.vm = vm
this.newDeps = []
Dep.target = this
}
// 添加一個觀察者,或者說註冊一個依賴
addDep(dep) {
this.newDeps.push(dep)
// 在【觀察者】收集【被觀察者】的同時,【被觀察者】也會收集【觀察者】
// 這比如王八看綠豆對眼兒了,遂互存了電話號碼,就有了後來的相識相知
dep.addSub(this)
}
// 在被觀察對象狀態發生變化時調用此方法
update() {
let {vm} = this
// 更新視圖
vm._update(vm._render())
}
}
複製代碼
每個【觀察者】都會收集本身要觀察的數據對象(Dep),當【被觀察對象】發生變化時,【被觀察對象】會通知【觀察者】,【觀察者】收到通知後執行 update
方法更新視圖。
接下來看一下【被觀察者】 Dep
:
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知全部對本身有依賴的觀察者
notify () {
const subs = this.subs
for (let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}
Dep.target = null
複製代碼
每一個【被觀察對象】一樣會收集依賴本身的【觀察者】,當本身發生變化時,就會通知(notify
)這些觀察者 update
。
那麼問題來了,這兩個角色是如何收集對方的呢?又如何得知【被觀察者】發生變化了呢? 這就用到了並不經常使用的 Object.defineProperty() 方法,經過在 JavaScript 對象每一個屬性描述符的 setter
和 getter
裏作文章,就能實時捕捉 JavaScript 對象的變化。
須要注意的是,Object.defineProperty()
是 JS 語言自己的一個 API 而不是 Vue 實現的,Object.defineProperty 是 ES5 中一個沒法 shim 的特性,這也是爲何 Vue 不支持 IE8 以及更低版本瀏覽器的緣由。若是想支持 IE8 以及更低版本瀏覽器怎麼辦呢?那就只有放棄 Vue,選擇 Knockout。更好的解決方案就是直接讓 IE8 以及更 low 的傢伙見鬼去吧。不過基本上不用擔憂這個問題了,由於據最新瀏覽器使用調查報告,IE8 以及更低版本瀏覽器的市場份額已經微不足道,直接忽略不計就好了。
既然 JS 已經支持在對象屬性變化時添加自定義處理,Vue 須要作的事就是遍歷傳入的 data
選項,爲 data
的每一個屬性設置 setter
和 getter
。這就解決了如何得知【被觀察者】發生了變化這個問題。
接下來講說這二者是如何收集對方的。【觀察者】和【被觀察者】就比如單身男和單身女,得有人安排相親才能創建起聯繫呵,Vue 就是這個牽線搭橋的媒婆。下面是相親源碼:
// vue/src/core/observer/index.js
import Dep from './dep'
export function observe (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
let key = keys[i], value = obj[key];
// 深度優先遍歷
observe(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 【觀察者】收集【被觀察者】
// 同時【被觀察者】也會收集【觀察者】
if (Dep.target) {
Dep.target.addDep(dep)
}
return value
},
set(newVal) {
value = newVal
// 【被觀察者】通知【觀察者】
dep.notify()
}
})
}
}
複製代碼
能夠看到,Vue 在遍歷 data
對象時完成了【觀察者】和【被觀察對象】彼此之間的收集工做。而且在 data
的某字段發生變化時,相應的依賴就會通知【觀察者】本身發生了變化,【觀察者】就能夠作出反應。
Vue 接下來就會在 initState()
中調用 observe(vm.$options.data)
,執行以後實例化 Vue 時傳入的 data
對象就會成爲響應式的,當你修改 data
對象的數據時(一般是根據用戶操做執行對應的業務邏輯),【被觀察者】就會通知已收集的全部【觀察者】,觀察者就會調用本身的 update
方法,從而更新視圖。這基本上就是 Vue 所實現的響應的數據綁定的工做原理。
在構建完響應式系統以後,Vue 接下來會檢查用戶是否傳入了 el
選項,由於 Vue 在將包含指令的 HTML 模板編譯成最終的樸素的 HTML 以後會執行 DOM 替換操做,最終展現在頁面上,若是沒有 el
選項,Vue 就不知道要把產出的 HTML 放到哪裏去展現。
掛載到 DOM 節點並不是替換一下 DOM 那麼簡單,它包括將模板編譯成 render
函數,執行 render
函數生成虛擬DOM,計算出新舊虛擬DOM之間的最小變動,打補丁式地更新頁面視圖等幾大步。
這個編譯過程在前幾篇的 Vue 編譯器模塊裏已經講得很清楚了,主要分爲根據模板生成 AST,對 AST 進行優化,根據 AST 生成 render 函數這三步,這裏再也不贅述,感興趣的可前往查看。
【虛擬DOM】並不是 Vue 提出的概念,而是老早就被髮掘出來的新型DOM操做方式,MVVM 框架在引入虛擬DOM以後如虎添翼。之因此叫作虛擬DOM,是相對於真實DOM而言的。直接操做DOM很慢,由於真實的DOM對象很重,操做真實DOM對象(HTMLElement)花銷很大,並且操做完以後每每會引發瀏覽器對頁面的重繪和重排。若是頻繁的進行DOM操做,頁面性能會急劇降低。因而聰明的 Jser 決定使用簡單的 JS 對象格式來表示真實 DOM,也就是虛擬DOM。先執行對虛擬DOM的操做(這會執行的很快,由於是純 JS 操做),最後對比操做先後的新舊虛擬DOM樹,找出最小變動,一次性地應用到真實DOM上。雖然仍是要對真實DOM操做,但次數卻大大減小,從而在更新視圖的同時可有效保證頁面性能。
Vue 的虛擬DOM系統是在開源虛擬DOM庫 Snabbdom 的基礎上作了適當的改進。
下面是 Vue 的 VNode 定義(正是一個個這樣的 VNode 組成了一棵虛擬DOM樹):
// vue/src/core/vdom/vnode.js
export default class VNode {
constructor (tag, data, children, text, elm) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm // 此字段存放真實DOM
}
}
複製代碼
在上一步執行 render
函數生成虛擬DOM後,接下來就須要對比新舊虛擬DOM之間的差別,從而得到DOM的最小變動。比較兩棵DOM樹的差別是虛擬DOM庫最核心的部分,這也是所謂的 Virtual DOM 的 diff 算法。就像版本控制系統 Git 的 diff 能夠計算出兩次提交之間的變動,虛擬DOM的 diff 也能夠計算出新舊虛擬DOM之間的差別。計算出來的差別稱爲一個 patch,也就是補丁。
若是是首次渲染,也就是頁面剛加載進來第一次渲染,Vue 會用模板編譯後的DOM替換掉傳入的 el
元素。請注意這一點,對模板內DOM的操做(綁定事件,引用DOM等)應該始終放在 Vue 的 mounted
以後,不然全部處理都將丟失,由於模板會被替換掉。
若是是後續數據發生變化,Vue 就會用打補丁的方式更新視圖,儘量重用現有DOM,將真實的DOM操做減到最少。
在上面【觀察者】 Watcher
的定義中 update
方法裏執行視圖更新。所以 Vue 運行時的整個工做流程基本上是這樣的:
用戶調用 new Vue(options)
實例化 Vue,Vue 在 _init
方法中初始化相關字段和事件,最重要的,創建起響應式系統,Vue 實例的後續運行重度依賴於此響應式系統。Vue 會新建一個【觀察者】,該觀察者在建立時會執行 update
方法首次渲染視圖,包含 Vue 指令的模板會被替換成編譯後的樸素 HTML。Vue 會遍歷傳入的 data
選項,經過 Object.defineProperty
設置 setter
和 getter
將其變成【被觀察對象】。當 data
的數據發生變化時,被觀察對象就會通知觀察者,觀察者就會再次調用 update
方法打補丁式地更新視圖。
本篇完,將在下一篇中開始深究運行時實現細節。
本系列會以每週一篇的速度持續更新,喜歡的小夥伴記得點關注哦