手摸手從0實現簡版Vue --- (依賴收集)

接:javascript

手摸手從0實現簡版Vue --- (對象劫持)html

手摸手從0實現簡版Vue --- (數組劫持)vue

手摸手從0實現簡版Vue --- (模板編譯)java

前面咱們實現了:git

  • 數據的劫持
  • 模板解析

可是目前咱們去更新數據,視圖不能正常去更新,如何知道視圖是否須要更新,是否是任意一組data數據修改都須要從新渲染更新視圖?其實並非,只有那些在頁面被引用的數據變動後纔會須要視圖的更新,因此須要記錄哪些數據是否被引用,被誰引用,從而決定是否更新,更新誰,這也就是依賴收集的目的。github

1. 發佈-訂閱

這裏須要使用發佈-訂閱模式來收集咱們的依賴,咱們先簡單實現一個簡單的發佈訂閱,新建一個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

2. 依賴收集

因此基於發佈-訂閱模式,咱們就要考慮咱們須要在哪去對咱們的數據進行發佈訂閱,能夠想到咱們以前都對咱們的數據都添加了gettersetter,能夠在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方法,添加addSubdep.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',會發現視圖已經更新了。

咱們梳理一下視圖更新的執行流程:

  1. new Vue()初始化數據後,從新定義了數據的gettersetter
  2. 而後調用$mount,初始化了一個渲染watcher, new Watcher(vm, updateComponent)
  3. Watcher實例化時調用get方法,把當前的渲染watcher掛在Dep.target上,而後執行updateComponent方法渲染模版。
  4. complier解析頁面的時候取值vm.msg,觸發了該屬性的getter,往vm.msg的dep中添加Dep.target,也就是渲染watcher。
  5. setTimeout2秒後,修改vm.msg,該屬性的dep進行廣播,觸發渲染watcherupdate方法,頁面也就從新渲染了。

代碼點擊=> 傳送門

3. 依賴收集優化--過濾相同的watcher

若是在頁面上,出現兩個引用相同的變量,那麼dep 便會存入兩個相同的渲染watcher,這樣就會致使在msg發生變化的時候觸發兩次更新。

<div id="app">
  {{msg}}
  {{msg}}
</div>
複製代碼

下面進行一些優化,讓depwatcher相互記憶,在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.addSubwatcher存到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()
    }
  });
}
複製代碼

此時去修改引用兩次的變量,會發現只會更新一次了。

代碼點擊=> 傳送門

4. 數組的依賴收集

上面處理了非數組的依賴收集,可是數組的依賴收集並不在defineReactivegettersetter中。

首先咱們給每一個觀察過的對象和數組添加一個__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;
    },
    ...
  });
}
複製代碼

此時給arrpush一個數據的話,會走到childOb.dep.depend();而後這個Dep收集的Watcher將會去調用數組中notify更新視圖。

5.嵌套數組依賴收集

上面處理了數組的依賴收集,可是若是一個數組爲[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的基本框架和響應式核心原理也就實現了,後面的話咱們再去看下

computedwatch,核心原理也和前面相似。都是利用Watcher去監聽變化,後面咱們一塊兒去實現一下!

代碼點擊=> 傳送門

但願各位老闆點個star,小弟跪謝~

相關文章
相關標籤/搜索