首頁 > 軟體

Vue2 響應式系統之分支切換

2022-04-13 04:00:28

場景

我們考慮一下下邊的程式碼會輸出什麼。

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    text: "hello, world",
    ok: true,
};
observe(data);

const updateComponent = () => {
    console.log("收到", data.ok ? data.text : "not");
};

new Watcher(updateComponent); // updateComponent 執行一次函數,輸出 hello, world

data.ok = false; // updateComponent 執行一次函數,輸出 not

data.text = "hello, liang"; // updateComponent 會執行嗎?

我們來一步一步理清:

observer(data)

攔截了 中 和 的 ,並且各自初始化了一個 範例,用來儲存依賴它們的 物件。datatextokget、setDepWatcher

new Watcher(updateComponent)

這一步會執行 函數,執行過程中用到的所有物件屬性,會將 收集到相應物件屬性中的 中。updateComponentWatcherDep

當然這裡的 其實是同一個,所以用了指向的箭頭。Watcher

data.ok = false

這一步會觸發 ,從而執行 中所有的 ,此時就會執行一次 。setDepWatcherupdateComponent

執行 就會重新讀取 中的屬性,觸發 ,然後繼續收集 。updateComponentdatagetWatcher

重新執行 函數 的時候:updateComponent

const updateComponent = () => {
    console.log("收到", data.ok ? data.text : "not");
};

因為 的值變為 ,所以就不會觸發 的 , 的 就不會變化了。data.okfalsedata.textgettextDep

而 會繼續執行,觸發 收集 ,但由於我們 中使用的是陣列,此時收集到的兩個 其實是同一個,這裡是有問題,會導致 重複執行,一會兒我們來解決下。data.okgetWatcherDepWacherupdateComponent

data.text = "hello, liang"

執行這句的時候,會觸發 的 ,所以會執行一次 。但從程式碼來看 函數中由於 為 , 對輸出沒有任何影響,這次執行其實是沒有必要的。textsetupdateComponentupdateComponentdata.okfalsedata.text

之所以執行了,是因為第一次執行 讀取了 從而收集了 ,第二次執行 的時候, 雖然沒有讀到,但之前的 也沒有清除掉,所以這一次改變 的時候 依舊會執行。updateComponentdata.textWatcherupdateComponentdata.textWatcherdata.textupdateComponent

所以我們需要的就是當重新執行 的時候,如果 已經不依賴於某個 了,我們需要將當前 從該 中移除掉。updateComponentWatcherDepWatcherDep

問題

總結下來我們需要做兩件事情。

  • 去重, 中不要重複收集 。DepWatcher
  • 重置,如果該屬性對 中的 已經沒有影響了(換句話就是, 中的 已經不會讀取到該屬性了 ),就將該 從該屬性的 中刪除。DepWacherWatcherupdateComponentWatcherDep

去重

去重的話有兩種方案:

  • Dep 中的 陣列換為 。subsSet
  • 每個 物件引入 , 物件中記錄所有的 的 ,下次重新收集依賴的時候,如果 的 已經存在,就不再收集該 了。DepidWatcherDepidDepidWatcher

Vue2 原始碼中採用的是方案 這裡我們實現下:2

Dep 類的話只需要引入 即可。id

/*************改動***************************/
let uid = 0;
/****************************************/
export default class Dep {
    static target; //當前在執行的函數
    subs; // 依賴的函數
  	id; // Dep 物件標識
    constructor() {
      /**************改動**************************/
        this.id = uid++;
      /****************************************/
        this.subs = []; // 儲存所有需要執行的函數
    }

    addSub(sub) {
        this.subs.push(sub);
    }
    depend() {
        if (Dep.target) {
            // 委託給 Dep.target 去呼叫 addSub
            Dep.target.addDep(this);
        }
    }

    notify() {
        for (let i = 0, l = this.subs.length; i < l; i++) {
            this.subs[i].update();
        }
    }
}

Dep.target = null; // 靜態變數,全域性唯一

在 中,我們引入 來記錄所有的 。Watcherthis.depIdsid

import Dep from "./dep";
export default class Watcher {
  constructor(Fn) {
    this.getter = Fn;
    /*************改動***************************/
    this.depIds = new Set(); // 擁有 has 函數可以判斷是否存在某個 id
    /****************************************/
    this.get();
  }

  /**
     * Evaluate the getter, and re-collect dependencies.
     */
  get() {
    Dep.target = this; // 儲存包裝了當前正在執行的函數的 Watcher
    let value;
    try {
      value = this.getter.call();
    } catch (e) {
      throw e;
    } finally {
      this.cleanupDeps();
    }
    return value;
  }

  /**
     * Add a dependency to this directive.
     */
  addDep(dep) {
    /*************改動***************************/
    const id = dep.id;
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
    /****************************************/

  }

  /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
  update() {
    this.run();
  }

  /**
     * Scheduler job interface.
     * Will be called by the scheduler.
     */
  run() {
    this.get();
  }
}

重置

同樣是兩個方案:

  • 全量式移除,儲存 所影響的所有 物件,當重新收集 的前,把當前 從記錄中的所有 物件中移除。WatcherDepWatcherWatcherDep
  • 增量式移除,重新收集依賴時,用一個新的變數記錄所有的 物件,之後再和舊的 物件列表比對,如果新的中沒有,舊的中有,就將當前 從該 物件中移除。DepDepWatcherDep

Vue2 中採用的是方案 ,這裡也實現下。2

首先是 類,我們需要提供一個 方法。DepremoveSub

import { remove } from "./util";
/*
export function remove(arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1);
        }
    }
}
*/
let uid = 0;

export default class Dep {
    static target; //當前在執行的函數
    subs; // 依賴的函數
    id; // Dep 物件標識
    constructor() {
        this.id = uid++;
        this.subs = []; // 儲存所有需要執行的函數
    }
		
    addSub(sub) {
        this.subs.push(sub);
    }
  /*************新增************************/
    removeSub(sub) {
        remove(this.subs, sub);
    }
  /****************************************/
    depend() {
        if (Dep.target) {
            // 委託給 Dep.target 去呼叫 addSub
            Dep.target.addDep(this);
        }
    }

    notify() {
        for (let i = 0, l = this.subs.length; i < l; i++) {
            this.subs[i].update();
        }
    }
}

Dep.target = null; // 靜態變數,全域性唯一

然後是 類,我們引入 來儲存所有的舊 物件,引入 來儲存所有的新 物件。Watcherthis.depsDepthis.newDepsDep

import Dep from "./dep";
export default class Watcher {
    constructor(Fn) {
        this.getter = Fn;
        this.depIds = new Set(); // 擁有 has 函數可以判斷是否存在某個 id
      	/*************新增************************/
        this.deps = [];
        this.newDeps = []; // 記錄新一次的依賴
        this.newDepIds = new Set();
      	/****************************************/
        this.get();
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        Dep.target = this; // 儲存包裝了當前正在執行的函數的 Watcher
        let value;
        try {
            value = this.getter.call();
        } catch (e) {
            throw e;
        } finally {
          	/*************新增************************/
            this.cleanupDeps();
          	/****************************************/
        }
        return value;
    }

    /**
     * Add a dependency to this directive.
     */
    addDep(dep) {
        const id = dep.id;
      /*************新增************************/
        // 新的依賴已經存在的話,同樣不需要繼續儲存
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id);
            this.newDeps.push(dep);
            if (!this.depIds.has(id)) {
                dep.addSub(this);
            }
        }
      /****************************************/
    }

    /**
     * Clean up for dependency collection.
     */
  	/*************新增************************/
    cleanupDeps() {
        let i = this.deps.length;
        // 比對新舊列表,找到舊列表裡有,但新列表裡沒有,來移除相應 Watcher
        while (i--) {
            const dep = this.deps[i];
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this);
            }
        }

        // 新的列表賦值給舊的,新的列表清空
        let 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;
    }
  	/****************************************/
    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    update() {
        this.run();
    }

    /**
     * Scheduler job interface.
     * Will be called by the scheduler.
     */
    run() {
        this.get();
    }
}

測試

回到開頭的程式碼

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    text: "hello, world",
    ok: true,
};
observe(data);

const updateComponent = () => {
    console.log("收到", data.ok ? data.text : "not");
};

new Watcher(updateComponent); // updateComponent 執行一次函數,輸出 hello, world

data.ok = false; // updateComponent 執行一次函數,輸出 not

data.text = "hello, liang"; // updateComponent 會執行嗎?

此時 修改的話就不會再執行 了,因為第二次執行的時候,我們把 中 裡的 清除了。data.textupdateComponentdata.textDepWatcher

總結

今天這個主要就是對響應式系統的一點優化,避免不必要的重新執行。所做的事情就是重新呼叫函數的時候,把已經沒有關聯的 去除。Watcher

不知道看到這裡大家有沒有一個疑問,我是一直沒想到說服我的點,歡迎一起交流:

在解決去重問題上,我們是引入了 ,但如果直接用 其實就可以。在 類中是用 來存 ,用陣列來存 物件,為什麼不直接用 來存 物件呢?idsetWatcherSetidDepSetDep

到此這篇關於Vue2 響應式系統之分支切換的文章就介紹到這了,更多相關Vue2分支切換內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com