1乾胶,vue的運(yùn)行機(jī)制
在 new Vue() 之后。 Vue 會(huì)調(diào)用 init 函數(shù)進(jìn)行初始化飒泻,其中最重要的是通過 Object.defineProperty 設(shè)置 setter 與 getter 函數(shù)崭捍,用來實(shí)現(xiàn)「響應(yīng)式」以及「依賴收集」
Object.defineProperty()
方法會(huì)直接在一個(gè)對象上定義一個(gè)新屬性,或者修改一個(gè)已經(jīng)存在的屬性侯嘀, 并返回這個(gè)對象。
Object.defineProperty(obj,prop,descriptor)
參數(shù)
obj:需要定義屬性的對象谱轨。
prop:需定義或修改的屬性的名字戒幔。
descriptor:將被定義或修改的屬性的描述符。
對象里目前存在的屬性描述符有兩種主要形式:數(shù)據(jù)描述符和存取描述符土童。數(shù)據(jù)描述符是一個(gè)擁有可寫或不可寫值的屬性诗茎。存取描述符是由一對 getter-setter 函數(shù)功能來描述的屬性。
數(shù)據(jù)描述符和存取描述符均具有以下可選鍵值:
configurable
當(dāng)且僅當(dāng)該屬性的 configurable 為 true 時(shí)献汗,該屬性描述符才能夠被改變错沃,也能夠被刪除。默認(rèn)為 false雀瓢。
enumerable
當(dāng)且僅當(dāng)該屬性的 enumerable 為 true 時(shí),該屬性才能夠出現(xiàn)在對象的枚舉屬性中玉掸。默認(rèn)為 false刃麸。
數(shù)據(jù)描述符同時(shí)具有以下可選鍵值:
value
該屬性對應(yīng)的值∷纠耍可以是任何有效的 JavaScript 值(數(shù)值泊业,對象把沼,函數(shù)等)。默認(rèn)為 undefined吁伺。
writable
當(dāng)且僅當(dāng)該屬性的 writable 為 true 時(shí)饮睬,該屬性才能被賦值運(yùn)算符改變。默認(rèn)為 false篮奄。
存取描述符(第三個(gè)參數(shù)對象)同時(shí)具有以下可選鍵值:
get
一個(gè)給屬性提供 getter 的方法捆愁,如果沒有 getter 則為undefined。當(dāng)我們讀取某個(gè)屬性的時(shí)候窟却,其實(shí)是在對象內(nèi)部調(diào)用了該方法昼丑,此方法必須要有return語句。該方法返回值被用作屬性值夸赫。默認(rèn)為 undefined菩帝。
set
一個(gè)給屬性提供 setter 的方法,如果沒有 setter 則為 undefined茬腿。該方法將接受唯一參數(shù)呼奢,并將該參數(shù)的新值分配給該屬性。默認(rèn)為 undefined切平。也就是說握础,當(dāng)我們設(shè)置某個(gè)屬性的時(shí)候,實(shí)際上是在對象的內(nèi)部調(diào)用了該方法揭绑。
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, /* 屬性可枚舉 */
configurable: true, /* 屬性可被修改或刪除 */
get: function reactiveGetter () {
return val; /* 實(shí)際上會(huì)依賴收集弓候,下一小節(jié)會(huì)講 */
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
cb(newVal);
}
});
}
這個(gè)方法通過 Object.defineProperty 來實(shí)現(xiàn)對對象的「響應(yīng)式」化,入?yún)⑹且粋€(gè) obj(需要綁定的對象)他匪、key(obj的某一個(gè)屬性)菇存,val(具體的值)。經(jīng)過 defineReactive 處理以后邦蜜,我們的 obj 的 key 屬性在「讀」的時(shí)候會(huì)觸發(fā) reactiveGetter 方法依鸥,而在該屬性被「寫」的時(shí)候則會(huì)觸發(fā) reactiveSetter 方法。
初始化之后調(diào)用 $mount 會(huì)掛載組件悼沈,進(jìn)行「編譯」步驟贱迟。
compile編譯可以分成 parse、optimize 與 generate 三個(gè)階段絮供,最終需要得到 render function
parse
parse 會(huì)用正則等方式解析 template 模板中的指令衣吠、class、style等數(shù)據(jù)壤靶,形成AST缚俏。
optimize
optimize 的主要作用是標(biāo)記 static 靜態(tài)節(jié)點(diǎn),這是 Vue 在編譯過程中的一處優(yōu)化,后面當(dāng) update 更新界面時(shí)忧换,會(huì)有一個(gè) patch 的過程恬惯, diff 算法會(huì)直接跳過靜態(tài)節(jié)點(diǎn),從而減少了比較的過程亚茬,優(yōu)化了 patch 的性能酪耳。
generate
generate 是將 AST 轉(zhuǎn)化成 render function 字符串的過程,得到結(jié)果是 render 的字符串以及 staticRenderFns 字符串刹缝。
在經(jīng)歷過 parse碗暗、optimize 與 generate 這三個(gè)階段以后,組件中就會(huì)存在渲染 VNode 所需的 render function 了赞草。
接下來我們來介紹一下「依賴收集」是如何實(shí)現(xiàn)的讹堤。
class Dep {
constructor () {
/* 用來存放Watcher對象的數(shù)組 */
this.subs = [];
}
/* 在subs中添加一個(gè)Watcher對象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher對象更新視圖 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
用 addSub 方法可以在目前的 Dep 對象中增加一個(gè) Watcher 的訂閱操作;
用 notify 方法通知目前 Dep 對象的 subs 中的所有 Watcher 對象觸發(fā)更新操作厨疙。
首先在 observer 的過程中會(huì)注冊 get 方法洲守,該方法用來進(jìn)行「依賴收集」。在它的閉包中會(huì)有一個(gè) Dep 對象沾凄,這個(gè)對象用來存放 Watcher 對象的實(shí)例知残。其實(shí)「依賴收集」的過程就是把 Watcher 實(shí)例存放到對應(yīng)的 Dep 對象中去屡穗。get 方法可以讓當(dāng)前的 Watcher 對象(Dep.target)存放到它的 subs 中(addSub)方法习劫,在數(shù)據(jù)變化時(shí)许溅,set 會(huì)調(diào)用 Dep 對象的 notify 方法通知它內(nèi)部所有的 Watcher 對象進(jìn)行視圖更新。
在修改對象的值的時(shí)候保屯,會(huì)觸發(fā)對應(yīng)的 setter手负, setter 通知之前「依賴收集」得到的 Dep 中的每一個(gè) Watcher,告訴它們自己的值改變了姑尺,需要重新渲染視圖竟终。這時(shí)候這些 Watcher 就會(huì)開始調(diào)用 update 來更新視圖,最終是將新產(chǎn)生的 VNode 節(jié)點(diǎn)與老 VNode 進(jìn)行一個(gè) patch 的過程切蟋,比對得出「差異」统捶,最終將這些「差異」更新到視圖上。
我們知道柄粹,render function 會(huì)被轉(zhuǎn)化成 VNode 節(jié)點(diǎn)喘鸟。Virtual DOM 其實(shí)就是一棵以 JavaScript 對象( VNode 節(jié)點(diǎn))作為基礎(chǔ)的樹,用對象屬性來描述節(jié)點(diǎn)驻右,實(shí)際上它只是一層對真實(shí) DOM 的抽象什黑。最終可以通過一系列操作使這棵樹映射到真實(shí)環(huán)境上
首先說一下 patch 的核心 diff 算法,我們用 diff 算法可以比對出兩顆樹的「差異」堪夭,我們來看一下兑凿,假設(shè)我們現(xiàn)在有如下兩顆樹凯力,它們分別是新老 VNode 節(jié)點(diǎn),這時(shí)候到了 patch 的過程礼华,我們需要將他們進(jìn)行比對
diff 算法是通過同層的樹節(jié)點(diǎn)進(jìn)行比較而非對樹進(jìn)行逐層搜索遍歷的方式,所以時(shí)間復(fù)雜度只有 O(n)拗秘,是一種相當(dāng)高效的算法圣絮,如下圖。
這張圖中的相同顏色的方塊中的節(jié)點(diǎn)會(huì)進(jìn)行比對雕旨,比對得到「差異」后將這些「差異」更新到視圖上扮匠。因?yàn)橹贿M(jìn)行同層級的比對,所以十分高效凡涩。
function patch (oldVnode, vnode, parentElm) {
if (!oldVnode) {
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
} else if (!vnode) {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
} else {
if (sameVnode(oldVNode, vnode)) {
patchVnode(oldVNode, vnode);
} else {
removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);
addVnodes(parentElm, null, vnode, 0, vnode.length - 1);
}
}
}
首先在 oldVnode(老 VNode 節(jié)點(diǎn))不存在的時(shí)候棒搜,相當(dāng)于新的 VNode 替代原本沒有的節(jié)點(diǎn),所以直接用 addVnodes 將這些節(jié)點(diǎn)批量添加到 parentElm 上活箕。
然后同理力麸,在 vnode(新 VNode 節(jié)點(diǎn))不存在的時(shí)候,相當(dāng)于要把老的節(jié)點(diǎn)刪除育韩,所以直接使用 removeVnodes 進(jìn)行批量的節(jié)點(diǎn)刪除即可克蚂。
最后一種情況,當(dāng) oldVNode 與 vnode 都存在的時(shí)候筋讨,需要判斷它們是否屬于 sameVnode(相同的節(jié)點(diǎn))埃叭。如果是則進(jìn)行patchVnode(比對 VNode )操作,否則刪除老節(jié)點(diǎn)悉罕,增加新節(jié)點(diǎn)赤屋。
Vue.js 是在我們修改 data 中的數(shù)據(jù)后修改視圖其實(shí)就是一個(gè)“setter -> Dep -> Watcher -> patch -> 視圖”的過程。
假設(shè)我們有如下這么一種情況壁袄。
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 1000; i++) {
this.number++;
}
}
}
}
當(dāng)我們按下 click 按鈕的時(shí)候类早,number 會(huì)被循環(huán)增加1000次。
那么按照之前的理解然想,每次 number 被 +1 的時(shí)候莺奔,都會(huì)觸發(fā) number 的 setter 方法,從而根據(jù)上面的流程一直跑下來最后修改真實(shí) DOM变泄。那么在這個(gè)過程中令哟,DOM 會(huì)被更新 1000 次!太可怕了妨蛹。
Vue.js 肯定不會(huì)以如此低效的方法來處理屏富。Vue.js在默認(rèn)情況下,每次觸發(fā)某個(gè)數(shù)據(jù)的 setter 方法后蛙卤,對應(yīng)的 Watcher 對象其實(shí)會(huì)被 push 進(jìn)一個(gè)隊(duì)列 queue 中狠半,在下一個(gè) tick 的時(shí)候?qū)⑦@個(gè)隊(duì)列 queue 全部拿出來 run( Watcher 對象的一個(gè)方法噩死,用來觸發(fā) patch 操作) 一遍。
因?yàn)?number 執(zhí)行 ++ 操作以后對應(yīng)的 Watcher 對象都是同一個(gè)神年,我們并不需要在下一個(gè) tick 的時(shí)候執(zhí)行 1000 個(gè)同樣的 Watcher 對象去修改界面已维,而是只需要執(zhí)行一個(gè) Watcher 對象,使其將界面上的 0 變成 1000 即可已日。
那么垛耳,我們就需要執(zhí)行一個(gè)過濾的操作,同一個(gè)的 Watcher 在同一個(gè) tick 的時(shí)候應(yīng)該只被執(zhí)行一次飘千,也就是說隊(duì)列 queue 中不應(yīng)該出現(xiàn)重復(fù)的 Watcher 對象堂鲜。...
let has = {};
let queue = [];
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
number 會(huì)被不停地進(jìn)行 ++ 操作,不斷地觸發(fā)它對應(yīng)的 Dep 中的 Watcher 對象的 update 方法护奈。然后最終 queue 中因?yàn)閷ο嗤?id 的 Watcher 對象進(jìn)行了篩選缔莲,從而 queue 中實(shí)際上只會(huì)存在一個(gè) number 對應(yīng)的 Watcher 對象。在下一個(gè) tick 的時(shí)候(此時(shí) number 已經(jīng)變成了 1000)霉旗,觸發(fā) Watcher 對象的 run 方法來更新視圖痴奏,將視圖上的 number 從 0 直接變成 1000。
參考于https://juejin.im/book/5a36661851882538e2259c0f