接:javascript
前面咱們實現了:git
可是目前咱們去更新數據,視圖不能正常去更新,如何知道視圖是否須要更新,是否是任意一組data數據修改都須要從新渲染更新視圖?其實並非,只有那些在頁面被引用的數據變動後纔會須要視圖的更新,因此須要記錄哪些數據是否被引用,被誰引用,從而決定是否更新,更新誰,這也就是依賴收集的目的。github
這裏須要使用發佈-訂閱模式來收集咱們的依賴,咱們先簡單實現一個簡單的發佈訂閱,新建一個dep.js
:數組
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
const dep = new Dep()
dep.addSub({
update() {
console.log('1')
}
})
dep.addSub({
update() {
console.log('2')
}
})
dep.notify()
複製代碼
此時咱們去調用notify
的話,會依次輸出1, 2
,這裏的dep
就至關於發佈者,watcher
就屬於訂閱者,當執行notify
時,全部的watcher
都會收到通知,而且執行本身的update
方法。app
因此基於發佈-訂閱模式,咱們就要考慮咱們須要在哪去對咱們的數據進行發佈訂閱,能夠想到咱們以前都對咱們的數據都添加了getter
和setter
,能夠在getter
的時候調用dep.addSub()
,在setter
的時候去調用dep.notify()
,可是以什麼樣的方式去添加訂閱。咱們以前在$mount
的時候實現了一個渲染watcher
,如今咱們去修改一下這個watcher
。首先給Dep
添加兩個方法,用來操做subs
:框架
let stack = [];
export function pushTarget(watcher) {
Dep.target = watcher;
stack.push(watcher);
}
export function popTarget() {
stack.pop();
Dep.target = stack[stack.length - 1];
}
複製代碼
而後去修改一下watcher
:函數
class Watcher { // 每次產生一個watch 都會有一個惟一的標識
...
get() {
+ pushTarget(this); // 讓 Dep.target = 這個渲染Watcher,若是數據變化,讓watcher從新執行
this.getter && this.getter(); // 讓傳入的函數執行
+ popTarget();
}
+ update() {
+ console.log('數據更新');
+ this.get();
+ }
}
複製代碼
而後去修改defineReactive
方法,添加addSub
和dep.notify()
。
export function defineReactive(data, key, value) {
observe(value); // 若是value依舊是一個對象,須要深度遞歸劫持
+ const dep = new Dep()
Object.defineProperty(data, key, {
get() {
// 取數據的時候進行依賴收集
+ if (Dep.target) {
+ dep.addSub(Dep.target)
+ }
return value;
},
set(newValue) {
if (newValue === value) return;
observe(newValue); // 若是新設置的值是一個對象, 應該添加監測
value = newValue;
// 數據更新 去通知更新視圖
+ dep.notify()
}
});
}
複製代碼
此時咱們2s後去更新一下vm.msg = 'hello world'
,會發現視圖已經更新了。
咱們梳理一下視圖更新的執行流程:
new Vue()
初始化數據後,從新定義了數據的getter
,setter
。new Watcher(vm, updateComponent)
。complier
解析頁面的時候取值vm.msg
,觸發了該屬性的getter
,往vm.msg
的dep中添加Dep.target,也就是渲染watcher。setTimeout
2秒後,修改vm.msg
,該屬性的dep進行廣播,觸發渲染watcher
的update
方法,頁面也就從新渲染了。代碼點擊=> 傳送門
若是在頁面上,出現兩個引用相同的變量,那麼dep
便會存入兩個相同的渲染watcher
,這樣就會致使在msg發生變化的時候觸發兩次更新。
<div id="app">
{{msg}}
{{msg}}
</div>
複製代碼
下面進行一些優化,讓dep
和watcher
相互記憶,在dep
收集watcher
的同時,讓watcher
記錄自身訂閱了哪些dep
。
首先給Dep
添加一個depend
方法,讓watcher
也就是Dep.target
將該dep
記錄。
class Dep {
...
+ depend() {
+ if (Dep.target) { // Dep.target = 渲染 watcher
+ Dep.target.addDep(this);
+ }
+ }
}
複製代碼
而後在watcher
中添加addDep
方法,用來記錄Dep
和調用dep.addSub
將watcher
存到Dep
中,互相記錄。
class Watcher {
constructor(vm, exprOrFn, cb = () => {}, opts = {}) {
...
+ this.deps = [];
+ this.depsId = new Set();
this.get();
}
+ addDep(dep) {
+ // 同一個watcher 不該該重複記錄 dep
+ let id = dep.id;
+ if (!this.depsId.has(id)) {
+ this.depsId.add(id);
+ this.deps.push(dep); // 讓watcher記錄dep
+ dep.addSub(this);
+ }
}
複製代碼
因此此時的defineReactive
不該該去直接調用dep.addSub
,應該改成:
export function defineReactive(data, key, value) {
observe(value); // 若是value依舊是一個對象,須要深度遞歸劫持
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
// 取數據的時候進行依賴收集
if (Dep.target) {
// 實現dep存watcher, watcher也能夠存入dep
+ dep.depend();
- dep.addSub(Dep.target)
}
return value;
},
set(newValue) {
if (newValue === value) return;
observe(newValue); // 若是新設置的值是一個對象, 應該添加監測
value = newValue;
// 數據更新 去通知更新視圖
dep.notify()
}
});
}
複製代碼
此時去修改引用兩次的變量,會發現只會更新一次了。
代碼點擊=> 傳送門
上面處理了非數組的依賴收集,可是數組的依賴收集並不在defineReactive
的getter
和setter
中。
首先咱們給每一個觀察過的對象和數組添加一個__ob__
屬性,返回observer
實例自己,而且給每一個observer
實例添加一個dep
,用來數組的依賴收集.
class Observe {
constructor(data) {
// 這個dep屬性專門爲數組設置
+ this.dep = new Dep()
+ // 給每一個觀察過的對象添加一個__ob__屬性, 返回當前實例
+ Object.defineProperty(data, '__ob__', {
+ get: () => this
+ })
// ...
}
}
複製代碼
添加事後,咱們就能夠在array
的方法中,獲取到這個dep
,並在更新時調用dep.notify
。
methods.forEach(method => {
arrayMethods[method] = function(...args) { // 函數劫持
...
if(inserted) observerArray(inserted);
+ this.__ob__.dep.notify();
return result;
}
});
複製代碼
可是還有重要的一點,咱們如今能夠通知到了,可是數組的依賴沒有收集到,下面去處理下數組的依賴收集:
export function defineReactive(data, key, value) {
+ let childOb = observe(value);
- observe(value);
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
// 取數據的時候進行依賴收集
if (Dep.target) {
// 實現dep存watcher, watcher也能夠存入dep
dep.depend();
+ if (childOb) {
+ childOb.dep.depend(); // 收集數組的依賴收集
+ }
}
return value;
},
...
});
}
複製代碼
此時給arr
去push
一個數據的話,會走到childOb.dep.depend();
而後這個Dep
收集的Watcher
將會去調用數組中notify
更新視圖。
上面處理了數組的依賴收集,可是若是一個數組爲[1, 2, [3, 4]]
,那麼arr[2].push('xx')
將不能正常更新,下面咱們去處理嵌套數組的依賴收集,
處理的方法就是,在外層arr收集依賴的同時也幫子數組收集,這裏新增一個dependArray
方法。
咱們給每一個觀察過的對象都添加過一個__ob__
,裏面嵌套的數組一樣有這個屬性,這時候只須要取到裏面的dep,depend收集一下就能夠,若是裏面還有數組嵌套則須要繼續調用dependArray
。
export function defineReactive(data, key, value) {
let childOb = observe(value); // 若是value依舊是一個對象,須要深度遞歸劫持
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
// 取數據的時候進行依賴收集
if (Dep.target) {
// 實現dep存watcher, watcher也能夠存入dep
dep.depend();
if (childOb) {
childOb.dep.depend(); // 收集數組的依賴收集
+ dependArray(value); // 收集數組嵌套的數組
}
}
return value;
},
...
});
}
複製代碼
咱們實現一下dependArray
:
export function dependArray(value) {
for(let i = 0; i < value.length; i++) {
let currentItem = value[i];
currentItem.__ob__ && currentItem.__ob__.dep.depend();
if (Array.isArray(currentItem)) {
dependArray(currentItem);
}
}
}
複製代碼
這樣,數組爲[1, 2, [3, 4]]
,那麼arr[2].push('xx')
,能夠正常去更新了。
到這裏,依賴收集就結束了,整個Vue
的基本框架和響應式核心原理也就實現了,後面的話咱們再去看下
computed
和watch
,核心原理也和前面相似。都是利用Watcher
去監聽變化,後面咱們一塊兒去實現一下!
代碼點擊=> 傳送門
但願各位老闆點個star,小弟跪謝~