看過vue官方文件的同學,對這張圖應該已然相當熟悉了。
vue的響應式是如何實現的?
聽過太多回答,通過Object.defineProperty
,可是再詳細的問時,對方渾然不知。
先擼為敬
const Observer = function(data) {
// 迴圈修改為每個屬性新增get set
for (let key in data) {
defineReactive(data, key);
}
}
const defineReactive = function(obj, key) {
// 區域性變數dep,用於get set內部呼叫
const dep = new Dep();
// 獲取當前值
let val = obj[key];
Object.defineProperty(obj, key, {
// 設定當前描述屬性為可被迴圈
enumerable: true,
// 設定當前描述屬性可被修改
configurable: true,
get() {
console.log('in get');
// 呼叫依賴收集器中的addSub,用於收集當前屬性與Watcher中的依賴關係
dep.depend();
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 當值發生變更時,通知依賴收集器,更新每個需要更新的Watcher,
// 這裡每個需要更新通過什麼斷定?dep.subs
dep.notify();
}
});
}
const observe = function(data) {
return new Observer(data);
}
const Vue = function(options) {
const self = this;
// 將data賦值給this._data,原始碼這部分用的Proxy所以我們用最簡單的方式臨時實現
if (options && typeof options.data === 'function') {
this._data = options.data.apply(this);
}
// 掛載函式
this.mount = function() {
new Watcher(self, self.render);
}
// 渲染函式
this.render = function() {
with(self) {
_data.text;
}
}
// 監聽this._data
observe(this._data);
}
const Watcher = function(vm, fn) {
const self = this;
this.vm = vm;
// 將當前Dep.target指向自己
Dep.target = this;
// 向Dep方法新增當前Wathcer
this.addDep = function(dep) {
dep.addSub(self);
}
// 更新方法,用於觸發vm._render
this.update = function() {
console.log('in watcher update');
fn();
}
// 這裡會首次呼叫vm._render,從而觸發text的get
// 從而將當前的Wathcer與Dep關聯起來
this.value = fn();
// 這裡清空了Dep.target,為了防止notify觸發時,不停的繫結Watcher與Dep,
// 造成程式碼死迴圈
Dep.target = null;
}
const Dep = function() {
const self = this;
// 收集目標
this.target = null;
// 儲存收集器中需要通知的Watcher
this.subs = [];
// 當有目標時,繫結Dep與Wathcer的關係
this.depend = function() {
if (Dep.target) {
// 這裡其實可以直接寫self.addSub(Dep.target),
// 沒有這麼寫因為想還原原始碼的過程。
Dep.target.addDep(self);
}
}
// 為當前收集器新增Watcher
this.addSub = function(watcher) {
self.subs.push(watcher);
}
// 通知收集器中所的所有Wathcer,呼叫其update方法
this.notify = function() {
for (let i = 0; i < self.subs.length; i += 1) {
self.subs[i].update();
}
}
}
const vue = new Vue({
data() {
return {
text: 'hello world'
};
}
})
vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get
複製程式碼
這裡我們用不到100行的程式碼,實現了一個簡易的vue響應式。當然,這裡如果不考慮期間的過程,我相信,40行程式碼之內可以搞定。但是我這裡不想省略,為什麼呢?我怕你把其中的過程自動忽略掉,怕別人問你相關東西的時候,明明自己看過了,卻被懟的啞口無言。總之,我是為了你好,多喝熱水。
Dep的作用是什麼?
依賴收集器,這不是官方的名字蛤,我自己起的,為了好記。
用兩個例子來看看依賴收集器的作用吧。
-
例子1,毫無意義的渲染是不是沒必要?
const vm = new Vue({ data() { return { text: 'hello world', text2: 'hey', } } }) 複製程式碼
當
vm.text2
的值發生變化時,會再次呼叫render
,而template
中卻沒有使用text2
,所以這裡處理render
是不是毫無意義?針對這個例子還記得我們上面模擬實現的沒,在
Vue
的render
函式中,我們呼叫了本次渲染相關的值,所以,與渲染無關的值,並不會觸發get
,也就不會在依賴收集器中新增到監聽(addSub
方法不會觸發),即使呼叫set
賦值,notify
中的subs
也是空的。OK,繼續迴歸demo,來一小波測試去印證下我說的吧。const vue = new Vue({ data() { return { text: 'hello world', text2: 'hey' }; } }) vue.mount(); // in get vue._data.text = '456'; // in watcher update /n in get vue._data.text2 = '123'; // nothing 複製程式碼
-
例子2,多個Vue例項引用同一個data時,通知誰?是不是應該倆都通知?
let commonData = { text: 'hello world' }; const vm1 = new Vue({ data() { return commonData; } }) const vm2 = new Vue({ data() { return commonData; } }) vm1.mount(); // in get vm2.mount(); // in get commonData.text = 'hey' // 輸出了兩次 in watcher update /n in get 複製程式碼
希望通過這兩個例子,你已經大概清楚了Dep
的作用,有沒有原來就那麼回事的感覺?有就對了。總結一下吧(以下依賴收集器實為Dep
):
vue
將data
初始化為一個Observer
並對物件中的每個值,重寫了其中的get
、set
,data
中的每個key
,都有一個獨立的依賴收集器。- 在
get
中,向依賴收集器新增了監聽 - 在mount時,例項了一個
Watcher
,將收集器的目標指向了當前Watcher
- 在
data
值發生變更時,觸發set
,觸發了依賴收集器中的所有監聽的更新,來觸發Watcher.update
如果看完還覺得不夠過癮,可以看看筆者的其他文章