閱讀源碼,個人覺得更多的收獲是你從源碼中提煉到了什么知識點,Vue的很多核心源碼都十分精妙竿痰,讓我們一起來關(guān)注它「依賴收集」的實現(xiàn)。
tip:Vue版本:v2.6.12砌溺,瀏覽器:谷歌影涉,閱讀方式:在靜態(tài)html 引用 Vue 包
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
進(jìn)行斷點閱讀
文章篇幅有點長,泡杯咖啡规伐,慢慢看 ~
我從「依賴收集」中學(xué)習(xí)到了什么蟹倾?
1. 觀察者模式
觀察者模式的基本概念:
觀察目標(biāo)發(fā)生變化 -> notify[通知] -> 觀察者們 -> update[更新]
下面這段代碼是 Vue 源碼中經(jīng)過運算的結(jié)果,可以讓小伙伴們的腦袋瓜先有個簡單的結(jié)構(gòu):
名詞解釋:
dep:depend[依賴]楷力,這里的“依賴”喊式,我們可以理解成 “觀察目標(biāo)” 。
subs:subscribers[訂閱者]萧朝,這里的“訂閱者”等價“觀察者”岔留。
// 基礎(chǔ)數(shù)據(jù)
data: {
a: 1, // 關(guān)聯(lián) dep:id=0 的對象,a如果發(fā)生變化检柬,this.a=3献联,調(diào)用 notify,
b: 2, // 關(guān)聯(lián) dep:id=1 的對象...
// ...
}
dep = {
id: 0,
// 通知觀察者們
notify() {
this.subs.forEach(item => {
item.update();
});
},
// 觀察者們
subs: [
{
id: 1,
update() {
// 被目標(biāo)者通知何址,做點什么事
}
},
{
id: 2,
update() {
// 被目標(biāo)者通知里逆,做點什么事
}
}
]
};
dep = {
id: 1,
//...
2. defineProperty 對一級/多級對象進(jìn)行攔截
對于一級對象的攔截相信小伙伴們都會啦。
這里闡述一下對于多級對象設(shè)置攔截器的封裝用爪,看下這段代碼:
const obj = { message: { str1: 'hello1', str2: 'hello2' } };
function observer(obj) {
if (!(obj !== null && typeof obj === 'object')) {
return;
}
walk(obj);
}
function walk(obj) {
let keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
function defineReactive(obj, key) {
let val = obj[key];
observer(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log('get :>> ', key, val);
return val;
},
set(newVal) {
console.log('set :>> ', key, newVal);
observer(newVal);
val = newVal;
}
});
}
observer(obj);
解釋:observer
這個方法表示如果當(dāng)前是一個對象原押,就會繼續(xù)被遍歷封裝攔截。
我們對 obj 進(jìn)行操作偎血,看控制臺的輸出:
obj.message
// get :>> message { str1: "hello1", str2: "hello2"}
/* 這個例子說明了:不管是在 get/set str1诸衔,都會先觸發(fā) message 的 get*/
obj.message.str1
// get :>> message { str1: "hello1", str2: "hello2" }
// get :>> str1 hello1
obj.message.str1="123"
// get :>> message { str1: "123", str2: "hello2" }
// set :>> str1 123
// 重點:
obj.message={test: "test"}
// set :>> message { test: "test" }
obj.message.test='test2'
// get :>> message { test: "test2" }
// set :>> test test2
/*
有些小伙伴可能會有疑惑,這里進(jìn)行 obj.message={test: "test"} 賦值一個新對象的話颇玷,
不就無法檢測到屬性的變化笨农,為什么執(zhí)行 obj.message.test='test2' 還會觸發(fā)到 set 呢?
返回到上面帖渠,在 defineReactive 方法攔截器 set 中谒亦,我們做了這樣一件事:
set(newVal) {
// 這里調(diào)用 observer 方法重新遍歷,如果當(dāng)前是一個對象空郊,就會繼續(xù)被遍歷封裝攔截
observer(newVal)
// ...
}
*/
延伸到實際業(yè)務(wù)場景:「獲取用戶信息然后進(jìn)行展示」份招。我在 data 設(shè)置了一個 userInfo: {}
,ajax 獲取到結(jié)果進(jìn)行賦值 this.userInfo = { id: 1, name: 'refined' }
渣淳,就可以顯示到模板 {{ userInfo.name }}
脾还,之后再進(jìn)行 this.userInfo.name = "xxx"
,也會進(jìn)行響應(yīng)式渲染了入愧。
3. defineProperty 對數(shù)組的攔截丨Object.create 原型式繼承丨原型鏈丨AOP
我們都知道 defineProperty 只能攔截對象鄙漏,對于數(shù)組的攔截 Vue 有巧妙的擴(kuò)展:
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsToPatch.forEach(function (method) {
var original = arrayProto[method];
Object.defineProperty(arrayMethods, method, {
enumerable: true,
configurable: true,
value: function mutator(...args) {
console.log('set and do something...');
var result = original.apply(this, args);
return result;
}
});
});
function protoAugment(target, src) {
target.__proto__ = src;
}
var arr = [1, 2, 3];
protoAugment(arr, arrayMethods);
arr.push(4)
// set and do something...
解釋:Object.create(arrayProto);
為原型式繼承,即 arrayMethods.__proto__ === Array.prototype === true
棺蛛,所以現(xiàn)在的 arrayMethods
就可以用數(shù)組的所有方法怔蚌。
代碼中的 target.__proto__ = src
,即 arr.__proto__ = arrayMethods
旁赊,我們已經(jīng)對 arrayMethods 自己定義了幾個方法了桦踊,如 push。
現(xiàn)在我們進(jìn)行 arr.push
终畅,就可以調(diào)用到 arrayMethods
自定義的 push 了籍胯,內(nèi)部還是有調(diào)用了 Array.prototype.push
原生方法竟闪。這樣我們就完成了一個攔截,就可以檢測到數(shù)組內(nèi)容的修改杖狼。
原型鏈機(jī)制:Array.prototype
本身是有 push 方法的炼蛤,但原型鏈的機(jī)制就是,arr 通過 __proto__
找到了 arrayMethods.push蝶涩,已經(jīng)找到了理朋,就不會往下進(jìn)行找了。
可以注意到绿聘,封裝的這幾個方法 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
嗽上,都是涉及到數(shù)組內(nèi)容會被改變的,那如果我要調(diào)用 arr.map 方法呢?還是剛剛講的 原型鏈 機(jī)制熄攘,arrayMethods 沒有 map 方法兽愤,就繼續(xù)順著 __proto__
往下找,然后找到 Array.prototype.map
挪圾。
不得不說烹看,這個數(shù)組的擴(kuò)展封裝,可以學(xué)習(xí)到很多洛史,贊贊贊 ~
上面講的例子都是對一個數(shù)組內(nèi)容的改變惯殊。細(xì)節(jié)的小伙伴會發(fā)現(xiàn),如果我對整個數(shù)組進(jìn)行賦值呢也殖,如:arr = [4,5,6]
土思,攔截不到吧,是的忆嗜。其實我只是把這個例子和上面第二點的例子拆分出來了己儒。我們只需要對上面 observer
方法,進(jìn)行這樣一個判斷捆毫,即
function observer(value) {
if (!(value !== null && typeof value === 'object')) {
return;
}
if (Array.isArray(value)) {
protoAugment(value, arrayMethods);
} else {
walk(value);
}
}
多級對象和數(shù)組的攔截概念其實很像闪湾,只是對象只需要逐級遍歷封裝攔截器,而數(shù)組需要用AOP的思想來封裝绩卤。
4. 微任務(wù)(microtask)的妙用丨event loop
直接來一手例子:
var waiting = false;
function queue(val) {
console.log(val);
nextTick();
}
function nextTick() {
if (!waiting) {
waiting = true;
Promise.resolve().then(() => {
console.log('The queue is over途样, do something...');
});
}
}
queue(1);
queue(2);
queue(3);
// 1
// 2
// 3
// The queue is over, do something...
解釋:主程序方法執(zhí)行完畢之后濒憋,才會執(zhí)行 promise 微任務(wù)何暇。這也可以解釋,為什么 Vue 更新動作是異步的【即:我們沒辦法立即操作 dom 】凛驮,因為這樣做可以提高渲染性能裆站,后面會具體講這塊。
5. 閉包的妙用
這里也直接來一手例子,個人認(rèn)為這個閉包用法是成就了依賴收集的關(guān)鍵 ~
var id = 0;
var Dep = function () {
this.id = id++;
};
Dep.prototype.notify = function notify() {
console.log('id :>> ', this.id, '宏胯,通知依賴我的觀察者們');
};
function defineReactive(obj, key) {
var dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {},
set() {
dep.notify();
}
});
}
var obj = { str1: 'hello1', str2: 'hello2' };
defineReactive(obj, 'str1');
defineReactive(obj, 'str2');
obj.str1 = 'hello1-change';
obj.str2 = 'hello2-change';
// id :>> 0 羽嫡,通知依賴我的觀察者們
// id :>> 1 ,通知依賴我的觀察者們
這也是第一點講到的關(guān)聯(lián) dep 對象肩袍,現(xiàn)在每個屬性都可以訪問到詞法作用域的屬于自己的 dep 對象厂僧,這就是閉包。
6. with 改變作用域
這里只是模擬一下 Vue 的渲染函數(shù)
function render() {
with (this) {
return `<div>${message}</div>`;
}
}
var data = { message: 'hello~' };
render.call(data);
// <div>hello~</div>
這就是我們平時在 <template>
中不用寫 {{ this.message }}
的原因了牛,而是如:
<template>
<div> {{ message }} </<div>
</template>
上面這 6 點是個人覺得有學(xué)習(xí)到東西的地方,當(dāng)然要深入理解依賴收集辰妙,我們需要走一遍流程鹰祸。如果你當(dāng)前在電腦前,我會告訴你需要打第幾行的斷點密浑,讓我們一起讀源碼吧蛙婴,go go go ~
深入源碼
tip:為了閱讀質(zhì)量,我會把一些相對與流程無關(guān)的代碼省略掉尔破,代碼中類似「? :123」街图,表示需要打的斷點,谷歌瀏覽器上開啟調(diào)試 ctrl + o
懒构,輸入 :123 即可跳轉(zhuǎn)至 123 行餐济。
本地新建 index.html,引入 Vue 包胆剧,打開瀏覽器瀏覽
<body>
<div id="app">
<div>{{message }}</div>
<button @click="handleClick">change</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
message: 'hello world'
},
methods: {
handleClick() {
this.message = 'hello world 2';
}
}
});
</script>
</body>
斷點 ? :4700 initData方法
顧名思義絮姆,初始化我們寫的 data 數(shù)據(jù)并做一些操作,在這個方法里有兩個方法值得我們關(guān)注秩霍,proxy(vm, "_data", key);
與 observe(data, true);
篙悯。
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
var keys = Object.keys(data);
var i = keys.length;
while (i--) {
var key = keys[i];
? :4734 proxy(vm, "_data", key);
}
? :4738 observe(data, true);
}
tip:在遇到方法的時候,我們用步入的方式可以快速定位到方法铃绒,如圖:
步入到 proxy 方法
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
? :4633 }
分析:這個方法是在 while 里的鸽照,這里循環(huán)遍歷了我們寫在 data 上的對象。當(dāng)前 target = vm 颠悬,key = message矮燎,走到這個 4633 斷點,控制臺打印 target赔癌,如圖:
上面我們提到了一個 with
的例子:Vue會進(jìn)行 render.call(vm)
漏峰。這樣子我們就會觸發(fā)到 message 的 get 方法,這是一個入口届榄,后續(xù)會做一系列的操作浅乔。
步入到 observe 方法
function observe (value, asRootData) {
if (!isObject(value)) {
return
}
var ob;
? :4633 ob = new Observer(value);
}
分析:可以理解成這個方法開始正在對 data 上的 可觀察數(shù)據(jù) 進(jìn)行觀察的一些提前準(zhǔn)備,如:往屬性上附加 get/set 攔截器,然后分別在 get/set 里做點什么...
步入到 new Observer [可觀測類]
這是第一個核心類靖苇,接下來我們還會分別講到其他的兩個類席噩,每個類都是核心
var Observer = function Observer (value) {
this.dep = new Dep();
if (Array.isArray(value)) {
protoAugment(value, arrayMethods);
} else {
? :935 this.walk(value);
}
};
分析:這里有個數(shù)組 or 對象的判斷,對于數(shù)組攔截我們在上面已經(jīng)有講過了贤壁,我們現(xiàn)在關(guān)注 walk 方法悼枢。
步入到 walk 方法
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
? :947 defineReactive$$1(obj, keys[i]);
}
};
繼續(xù)步入到 defineReactive$$1 方法
function defineReactive$$1 (
obj, // obj -> data
key, // key -> 'message'
val
) {
? :1021 var dep = new Dep();
val = obj[key];
var childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = val;
? :1041 if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal;
childOb = observe(newVal);
? :1070 dep.notify();
}
});
}
分析:這個方法可以說是依賴收集中的核心,通過 get 方法添加依賴脾拆,通過 set 方法通知觀察者馒索。我們上面講到的 proxy 方法,可以把它當(dāng)做第一層攔截器名船,當(dāng)我們觸發(fā)一級攔截器之后绰上,就會到二級攔截器 defineReactive$$1 里定義的 get/set 方法。
new Dep() [觀察目標(biāo)類] 這個是第二個核心類渠驼。
還記得我們在上面說過這個方法是一個 “閉包” 嗎蜈块?是的,在當(dāng)前方法內(nèi)部 Object.defineProperty(obj, key, {
以上的所有變量/方法迷扇,是各個屬性各自獨立擁有的百揭。
至此,我們對于 data 上屬性的 get/set 封裝 就講完了 蜓席。
如何對數(shù)據(jù)進(jìn)行依賴收集器一?
斷點 ? :4074
updateComponent = function () {
? :4067 vm._update(vm._render(), hydrating);
};
? :4074 new Watcher(vm, updateComponent);
分析:Watcher類,這個是第三個核心類厨内,觀察者類盹舞。和上面說的 Observer[可觀擦類]、Dep[觀察目標(biāo)類]隘庄,總共三個踢步。這個代碼片段是在 mounted 鉤子之前調(diào)用的,也就是我們之前對 data 數(shù)據(jù)先進(jìn)行了 get/set 封裝之后丑掺,就要開始進(jìn)行 render 了获印,在 render 之前,需要創(chuàng)建 render 觀察者街州,為了方便我們這里叫它 renderWatcher
兼丰。除了 renderWatcher
,我們還有 computedWatcher
和 watchWatcher
唆缴,這兩個分別是 計算屬性 和 偵聽器 觀察者鳍征,在 Vue 中主要是這三個類型的觀察者。
步入到 new Watcher [觀察者類]
var Watcher = function Watcher (
vm,
expOrFn
) {
this.getter = expOrFn;
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
? :4467 this.get();
};
分析:
- deps:緩存每次執(zhí)行觀察者函數(shù)時所用到的dep所有實例面徽。
- depIds:緩存每次執(zhí)行觀察者函數(shù)時所用到的dep所有實例 id艳丛,用于判斷匣掸。
- newDeps:存儲本次執(zhí)行觀察者函數(shù)時所用到的dep所有實例。
- newDepIds:存儲本次執(zhí)行觀察者函數(shù)時所用到的dep所有實例 id氮双,用于判斷碰酝。
步入到 get 方法
Watcher.prototype.get = function get () {
? :4474 pushTarget(this);
var vm = this.vm;
? :4478 this.getter.call(vm, vm);
? :4491 popTarget();
? :4492 this.cleanupDeps();
};
分析【這段分析比較詳細(xì)】:pushTarget 和 popTarget 是一對方法,分別用來記錄當(dāng)前的觀察者戴差,和剔除當(dāng)前觀察者
Dep.target = null;
var targetStack = [];
function pushTarget (target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget () {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
Dep.target 為全局唯一的送爸,因為在一個時刻內(nèi),就只會有一個觀察者函數(shù)在執(zhí)行暖释,把當(dāng)前的 觀察者實例 賦值給 Dep.target袭厂, 后續(xù)只要訪問 Dep.target 就能知道當(dāng)前的觀察者是誰了。
我們繼續(xù)步入 this.getter.call(vm, vm)
球匕,【以下這幾個步入我們就簡單過一下】
updateComponent = function () {
? :4067 vm._update(vm._render(), hydrating);
};
步入 vm._update(vm._render(), hydrating)
Vue.prototype._render = function () {
? :3551 vnode = render.call(vm._renderProxy, vm.$createElement);
};
步入 render.call(vm._renderProxy, vm.$createElement)
纹磺,在谷歌會新打開一個 tab 用來執(zhí)行下面這個函數(shù)
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_v(_s(message))]),_v(" "),_c('button',{on:{"click":handleClick}},[_v("change")])])}
})
關(guān)鍵部分來了,這個是 Vue 的渲染函數(shù)谐丢。我們現(xiàn)在只要關(guān)注,它這里是會讀取到 this.message 的蚓让,所以會觸發(fā) message 的 get 方法乾忱,也就是說當(dāng)前觀察者 renderWatcher
依賴了 message ,所以就會開始對它進(jìn)行 “收集”历极。
谷歌瀏覽器器窄瘟,直接點擊下一步「 ||> 」,
我們就可以看到光標(biāo)跳到了 defineReactive$$1
方法內(nèi)部我們的 get 方法趟卸,開始進(jìn)行“依賴收集” 了
get: function reactiveGetter () {
var value = val;
? :1041 if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
當(dāng)前的 Dep.target 是有值的蹄葱,所以執(zhí)行 dep.depend 開始進(jìn)行依賴,
步入 dep.depend
Dep.protJavaScriptotype.depend = function depend () {
if (Dep.target) {
? :731 Dep.target.addDep(this);
}
};
步入 Dep.target.addDep(this)
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
? :4059 };
dep.addSub(this)
把當(dāng)前的 watcher 實例 push 到 subs 數(shù)組锄列,并且判斷如果當(dāng)前 觀察者 被 觀察目標(biāo) 添加到 subs 數(shù)組里图云,就不會繼續(xù)添加,過濾重復(fù)數(shù)據(jù)邻邮。
走到這個 4059 斷點竣况,控制臺打印 dep,如:
dep = {
id:3,
subs:[
renderWatcher 實例
]
}
跳出繼續(xù)往下走會調(diào)用 4491 popTarget()
筒严,剔除當(dāng)前 觀察者丹泉。
接著步入 this.cleanupDeps()
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var i = this.deps.length;
while (i--) {
var dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
var tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
};
這里把 this.deps = this.newDeps
,緩存到 deps 里鸭蛙,然后清空newDeps摹恨,來做下一次的收集。
至此娶视,我們就完成了一個 依賴收集 ~
更新依賴數(shù)據(jù)如何 notify 觀察者做出 update 晒哄?
官網(wǎng):只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更揩晴。如果同一個 watcher 被多次觸發(fā)勋陪,只會被推入到隊列中一次。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的硫兰。然后诅愚,在下一個的事件循環(huán)“tick”中,Vue 刷新隊列并執(zhí)行實際 (已去重的) 工作劫映。
當(dāng)用戶點擊change按鈕
this.message = 'hello world 2';
光標(biāo)自動跳轉(zhuǎn)至 message 對應(yīng)的 set 方法违孝,執(zhí)行 dep.notify() 進(jìn)行通知觀察者進(jìn)行 update 動作
Dep.prototype.notify = function notify () {
for (var i = 0, l = subs.length; i < l; i++) {
? :745 subs[i].update();
}
};
步入 subs[i].update()
Watcher.prototype.update = function update () {
? :4543 queueWatcher(this);
};
步入 queueWatcher()
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
if (!waiting) {
waiting = true;
? :4403 nextTick(flushSchedulerQueue);
}
}
}
flushSchedulerQueue 方法
function flushSchedulerQueue () {
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
? :4311 watcher.run();
}
}
分析[結(jié)合上面 queueWatcher
和 flushSchedulerQueue
兩個方法]:
flushSchedulerQueue
方法:
queue.sort
需要排序是原因:
確保 watcher 的更新順序與它們被創(chuàng)建的順序一致。
- 對于父子組件來說泳赋,組件的創(chuàng)建順序是父組件先被創(chuàng)建雌桑,然后子組件再被創(chuàng)建,所以父組件的renderWatcher的id是小于子組件的祖今。
- 對于用戶自定義watcher【watchWatcher】和 renderWatcher校坑,用戶自定義watcher是先于組件的renderWatcher被創(chuàng)建的。
- 如果子組件在父組件的監(jiān)視程序運行期間被銷毀千诬,則會跳過子組件的watcher耍目。
queueWatcher
方法:
- 這里進(jìn)行了 watcher id 的重復(fù)判斷,因為在一個
renderWatch
中可能會依賴多個觀察目標(biāo)徐绑,當(dāng)我們同時改變多個依賴的值 邪驮,經(jīng)過判斷 watcher.id 一樣就不用把兩次更新 push 到 隊列,避免渲染性能消耗傲茄,如:
this.message1 = 'hello world 1';
this.message2 = 'hello world 2';
// 更多...
或 循環(huán)改變同一個依賴
for (let i = 0; i < 10; i++) {
this.message++;
}
- flushing 表示 queue 隊列的更新狀態(tài)毅访,
flushing=true
代表隊列正在更新中。
這里的 else 分支盘榨,主要是判斷一種邊界情況喻粹,
i--
,從后往前遍歷草巡,其實目的是看剛進(jìn)入的這個 watcher 在不在當(dāng)前更新隊列中磷斧。注意這里的index
是來自flushSchedulerQueue
方法內(nèi)部定義的,是全局的捷犹。
我們可以看到跳出 while 的條件為:
queue[i].id === watcher.id
我們可以這樣理解弛饭,當(dāng)前在更新一個 id 為 3 的 watcher,然后又進(jìn)來了一個 watcher萍歉,id 也為3侣颂。相當(dāng)于需要重新更新一次 id 為 3 的 watcher,這樣才能獲取到最新值保證視圖渲染正確枪孩。用代碼解釋如:
// ...
<div>{{ message }}</div>
// ...
new Vue({
el: '#app',
data: {
message: 'hello world'
},
watch: {
message() {
this.message = 'hello world 3';
}
},
methods: {
handleClick() {
this.message = 'hello world 2';
}
}
});
點擊按鈕更新 message 之后憔晒,又用 watch 監(jiān)聽其變化藻肄,然后在內(nèi)部再對 message 進(jìn)行更新,我們試著讀一下這段代碼的更新流程拒担。首先嘹屯,用戶自定義watcher【watchWatcher】是先于 renderWatcher 被創(chuàng)建的,所以我們在更新 message 的時候从撼,會先執(zhí)行 watch 州弟,觸發(fā)到內(nèi)部方法又更新了一次 message,為了保證視圖渲染正確低零,我們需要在執(zhí)行一次這個 watcher 的 update婆翔。
-
queue[i].id < watcher.id
分析:更新隊列中有 id 為1,2,5 三個 watcher,當(dāng)前正在更新id為 2 的watcher掏婶,當(dāng) queueWatcher 被調(diào)用并傳進(jìn)來一個 id 為 3 的watcher啃奴,于是就將這個 watcher 放到 2 的后面,確保 watcher 的更新順序與它們被創(chuàng)建的順序一致雄妥。
我們都知道最蕾,flushSchedulerQueue
方法是一個微任務(wù)。在對queue操作之后老厌,主程序方法執(zhí)行完畢之后瘟则,開始執(zhí)行微任務(wù),進(jìn)行 queue 的調(diào)度更新梅桩,watcher.run()
至此壹粟,我們就完成了當(dāng)觀察目標(biāo)改變時通知觀察者更新的動作拜隧。
總結(jié)
以上舉的例子是一個簡單 renderWatcher
的一個流程閉環(huán)宿百,依賴收集 到 通知更新。Vue 有renderWatcher
【視圖觀察者】洪添,computedWatcher
【計算屬性觀察者】 和 watchWatcher
【偵聽器觀察者】垦页,主要這三個類型的觀察者洛退。
主要的三個類 Dep【觀察目標(biāo)類】照藻,Observe【可觀測類】,Watcher【觀察者類】凭语。
我們可以理解忿峻,在依賴被改變的時候通知觀察者的一過程薄啥,一切都是為了視圖渲染,在這過程中會進(jìn)行一些性能優(yōu)化 / 處理一些邊界情況逛尚,最終保證視圖渲染的完整性垄惧。
個人覺得源碼有點晦澀難懂,但還是得自己多過幾遍才能熟悉绰寞。這邊還是建議親自閱讀幾遍源碼到逊,看一些他人的總結(jié)還是會有點模糊铣口,所以本篇文章提供了 斷點 參考。幫助小伙伴快速定位源碼比較精髓的位置觉壶。
了解源碼的運作脑题,也可以讓我們更加的知道,我們需要怎么去調(diào)用框架提供的 api 會更加優(yōu)化铜靶。
后話
Vue3 已經(jīng)出來了叔遂,我們看完 Vue2 就可以對比看看 Vue3 的更強(qiáng)大之處了,
這邊就不再舉例 computedWatcher
和 watchWatcher
了旷坦,小伙伴們可以動手 debug 看看 ~ 掏熬。
可以從頁面的 initState 方法作為入口:
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
? :4645 if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
? :4647 initWatch(vm, opts.watch);
}
}
感興趣的小伙伴也可以 debug 看 computed 這種場景
computed: {
c1() {
return this.c2 + 'xxx';
},
c2() {
return this.message + 'xxx';
}
}
computed 是 “l(fā)azy” 的,它不參與 queue 的更新秒梅,而是如果在模板上有用到 computed 屬性旗芬,才會去進(jìn)行獲取計算后的值。