一文帶你看懂vue3響應式系統原理

前言

vue3 beta版本已經發布快兩個月了,相信你們或多或少都有去了解一些vue3的新特性,也有一部分人調侃學不動了,在我看來,技術確定是不斷更迭的,新的技術出現可以提升生產力,落後的技術確定是要被淘汰的,五年前會JQ一把梭就能找到一份還行的工做,如今只會JQ應該不多公司會要了吧。恰好前兩天尤大也發了一篇文章講述了vue3的製做歷程,有興趣的同窗能夠點擊連接前往查看,文章是全英文的,英文不是很好的同窗能夠藉助翻譯插件閱讀。好了,廢話很少說,本篇的主題是手寫vue3的響應式功能。html

vue3的代碼實例

在寫代碼前,不妨來看看如何使用vue3吧,咱們能夠先去 github.com/vuejs/vue-n… clone一份代碼,使用npm install && npm run dev後,會生成一個packages -> vue -> dist -> vue.global.js文件,這樣咱們就可使用vue3了,在vue文件夾新建一個index.html文件。vue

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue3示例</title>
</head>
<body>
    <div id="app"></div>
    <button id="btn">按鈕</button> 
    <script src="./dist/vue.global.js"></script>    
    <script> const { reactive, computed, watchEffect } = Vue; const app = document.querySelector('#app'); const btn = document.querySelector('#btn'); const year = new Date().getFullYear(); let person = reactive({ name: '煙花渲染離別', age: 23 }); let birthYear = computed(() => year - person.age); watchEffect(() => { app.innerHTML = `<div>我叫${person.name},今年${person.age}歲,出生年是${birthYear.value}</div>`; }); btn.addEventListener('click', () => { person.age += 1; }); </script>
</body>
</html>
複製代碼

能夠看到,咱們每次點擊一次按鈕,觸發person.age += 1;,而後watchEffect自動執行,計算屬性也相應更新,如今咱們的目標就很明確了,就是實現reactivewatchEffectcomputed方法。react

reactive方法

咱們知道vue3是基於proxy來實現響應式的,對proxy不熟悉的能夠去看看阮一峯老師的es6教程:es6.ruanyifeng.com/#docs/proxy reflect 也是es6新提供的API,具體做用也能夠參考阮一峯老師的es6教程:es6.ruanyifeng.com/#docs/refle… ,簡單來講他提供了一個操做對象的新API,將Object對象屬於語言內部的方法放到Reflect對象上,將老Object方法報錯的狀況改爲返回false值。 下面咱們來看看具體的代碼吧,它對對象的getsetdel操做進行了代理。git

function isObject(target) {
    return typeof target === 'object' && target !== null;
}

function reactive() {
    // 判斷是否對象,proxy只對對象進行代理
    if (!isObject(target)) {
        return target;
    }
    const baseHandler = {
        set(target, key, value, receiver) { // receiver:它老是指向原始的讀操做所在的那個對象,通常狀況下就是 Proxy 實例
            trigger(); // 觸發視圖更新
            return Reflect.set(target, key, value, receiver);
        },
        get(target, key, receiver) {
            return Reflect.get(target, key, value, receiver);
        },
        del(target, key) {
            return Reflect.deleteProperty(target, key);
        }
    };
    let observed = new Proxy(target, baseHandler);
    return observed;
}
複製代碼

添加更新限制

上面的代碼看上去好像沒啥問題,可是在代理數組的時候,添加、刪除數組的元素,除了能監聽到數組自己要設置的元素變化,還會監聽到數組長度length屬性修改的變化,以下圖:es6

因此咱們應該只在新增屬性的時候去觸發更新,咱們添加hasOwnProperty判斷與老值和新值比較判斷,只有修改自身對象的屬性或者修改了自身屬性而且值不一樣的時候纔去更新視圖。github

set(target, key, value, receiver) {
    const oldValue = target[key];
    if (!target.hasOwnProperty(key) || oldValue !== value) { // 新增屬性或者設置屬性老值不等於新值
        trigger(target, key); // 觸發視圖更新函數
    } 
    return Reflect.set(target, key, value, receiver);
}
複製代碼

深層級對象監聽

上面咱們只對對象進行了一層代理,若是對象的屬性對應的值仍是對象的話,它並無被代理過,此時咱們去操做該對象的時候,就不會觸發set,也就不會更新視圖了。以下圖:npm

那麼咱們應該怎麼進行深層次的代理呢?設計模式

咱們觀察一下person.hair.push(4)這個操做,當咱們去取person.hair的時候,會去調用personget方法,拿到屬性hair的值,那麼咱們就能夠再它拿到值以後判斷是不是對象,再去進行深層次的監聽。數組

get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    return isObject(res) ? reactive(res) : res;
},
複製代碼

緩存已代理對象

代理過的對象再去執行reactive方法的時候,會去從新設置代理,咱們應該避免這種狀況,經過hashmap緩存代理過的對象,這樣在再次代理的時候,判斷對象存在hashmap中,直接返回該結果便可。緩存

  • 進行屢次代理示例
let obj = {
    name: '煙花渲染離別',
    age: 23,
    hair: [1,2,3]
}
let person = reactive(obj);
person = reactive(obj);
person = reactive(obj);
複製代碼
  • 定義hashmap緩存代理對象

咱們使用WeakMap緩存代理對象,它是一個弱引用對象,不會致使內存泄露。 es6.ruanyifeng.com/#docs/set-m…

const toProxy = new WeakMap(); // 代理後的對象
const toRaw = new WeakMap(); // 代理前的對象

function reactive(target) {
    // 判斷是否對象,proxy只對對象進行代理
    if (!isObject(target)) {
        return target;
    }
    let proxy = toProxy.get(target); // 當前對象在代理表中,直接返回該對象
    if (proxy) { 
        return proxy;
    }
    if (toRaw.has(target)) { // 當前對象是代理過的對象
        return target;
    }
    let observed = new Proxy(target, baseHandler);

    toProxy.set(target, observed);
    toRaw.set(observed, target);
    return observed;
}
let obj = {
    name: '煙花渲染離別',
    age: 23,
    hair: [1,2,3]
}
let person = reactive(obj);
person = reactive(obj); // 再去代理的時候返回的就是從緩存中取到的數據了
複製代碼

這樣reactive方法就基本已經實現完了。

收集依賴,自動更新

咱們先來瞅瞅以前是怎麼渲染DOM的。

watchEffect(() => {
    app.innerHTML = `<div>我叫${person.name},今年${person.age}歲,出生年是${birthYear.value}</div>`;
});
複製代碼

在初始化默認執行一次watchEffect函數後,渲染DOM數據,以後依賴的數據發生變化,會自動再次執行,也就會自動更新咱們的DOM內容了,這就是咱們常說的收集依賴,響應式更新。

那麼咱們在哪裏進行依賴收集,何時通知依賴更新呢?

  • 咱們在用到數據進行展現的時候,它就會觸發咱們建立好的proxy對象的get方法,這個時候咱們就能夠收集依賴了。
  • 在數據發生變化的時候一樣會觸發咱們的set方法,咱們在set中通知依賴更新。 這實際上是一種設計模式叫作發佈訂閱。

咱們在get中收集依賴:

get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    track(target, key); // 收集依賴,若是目標上的key變化,執行棧中的effect
    return isObject(res) ? reactive(res) : res;
}
複製代碼

set中通知依賴更新:

set(target, key, value, receiver) { 
    if (target.hasOwnProperty(key)) {
        trigger(target, key); // 觸發更新
    }
    return Reflect.set(target, key, value, receiver);
}
複製代碼

能夠看到咱們在get中執行了一個track方法進行收集依賴,在set中執行trigger觸發更新,這樣咱們知道了它的過程後,再來看看怎麼實現watchEffect方法。

watchEffect方法

咱們傳入到watchEffect方法裏的函數就是咱們要收集的依賴,咱們將收集到的依賴用棧保存起來,棧是一種先進後出的數據結構,具體咱們看看下面代碼實現:

let effectStack = []; // 存儲依賴數據effect

function watchEffect(fn, options = {}) {
    // 建立一個響應式的影響函數,往effectsStack push一個effect函數,執行fn
    const effect = createReactiveEffect(fn, options);
    return effect;
}

function createReactiveEffect(fn) {
    const effect = function() {
        if (!effectsStack.includes(effect)) { // 判斷棧中是否已經有過該effecy,防止重複添加
            try {
                effectsStack.push(effect); // 將當前的effect推入棧中
                return fn(); // 執行fn
            } finally {
                effectsStack.pop(effect); // 避免fn執行報錯,在finally裏執行,將當前effect出棧
            }
        }
    }
    effect(); // 默認執行一次
}
複製代碼

關聯effect和對應對象屬性

上面咱們只是收集了fn存到effectsStack中,可是咱們還沒將fn和對應的對象屬性關聯,下面步咱們要實現track方法,將effect和對應的屬性關聯。

let targetsMap = new WeakMap();

function track(target, key) { // 若是taeget中的key發生改變,執行棧中的effect方法
    const effect = effectsStack[effectsStack.length - 1];
    // 最新的effect,有才建立關聯
    if (effect) {
        let depsMap = targetsMap.get(target);
        if (!depsMap) { // 第一次渲染沒有,設置對應的匹配值
            targetsMap.set(target, depsMap = new Map());
        }
        let deps = depsMap.get(key);
        if (!deps) { // 第一次渲染沒有,設置對應的匹配值
            depsMap.set(key, deps = new Set());
        }
        if (!deps.has(effect)) {
            deps.add(effect); // 將effect添加到當前的targetsMap對應的target的存放的depsMap裏key對應的deps
        }
    }
}

function trigger(target, key, type) {
    // 觸發更新,找到依賴effect
    let depsMap = targetsMap.get(target);
    if (depsMap) {
        let deps = depsMap.get(key);
        if (deps) {
            deps.forEach(effect => {
                effect();
            });
        }
    }
}
複製代碼

targetsMap的數據結構較爲複雜,它是一個WeakMap對象,targetsMapkey就是咱們target對象,在targetsMap中該target對應的值是一個Map對象,該Map對象的keytarget對象的屬性,Map對象對應的key的值是一個Set數據結構,存放了當前該target.key對應的effect依賴。看下面的代碼可能會比較清晰點:

let person = reactive({
    name: '煙花渲染離別',
});
targetsMap = {
    person: {
        'name': [effect]
    }
}
// {
// target: {
// key: [dep1, dep2]
// }
// }
複製代碼

執行流程

  • 收集流程:執行watchEffect方法,將fn也就是effectpush到effectStack棧中,執行fn,若是fn中有用到reactive代理過的對象,此時會觸發該代理對象的get方法,而咱們在get方法中使用了track方法收集依賴,track方法首先從effectStack中取出最後一個effect,也就是咱們剛剛push到棧中的effect,而後判斷它是否存在,若是存在的話,咱們從targetMap取出對應的targetdepsMap,若是depsMap不存在,咱們手動將當前的target做爲keydepsMap = new Map()做爲值設置到targetMap中,而後咱們再從depsMap中取出當前代理對象key對應的依賴deps,若是不存在則存放一個新Set進去,而後將對應的effect添加到該deps中。
  • 更新流程:修改代理後的對象,觸發set方法,執行trigger方法,經過傳入的targettargetsMap中找到depsMap,經過keydepsMap中找到對應的deps,循環執行裏面保存的effect

computed方法

computed以前咱們也來回顧下它的用法:

let person = reactive({
    name: '煙花渲染離別',
    age: 23
});
let birthYear = computed(() => 2020 - person.age);
person.age += 1;
複製代碼

能夠看到computed接受一個函數,而後返回一個通過處理後的值,在依賴的數據發生了修改後,computed也會從新計算一次。

實際computed它也是一個watchEffect函數,不過它比較特殊,這裏在調用watchEffect時候傳入了兩個參數,一個是computedfn,還有一個就是咱們要給watchEffect的參數{ lazy: true, computed: true },咱們以前寫watchEffect的時候並無對這些參數進行處理,因此如今咱們還得進行處理。

function computed(fn) {
    let computedValue;
    const computedEffect = watchEffect(fn, { 
        lazy: true, 
        computed: true
    });
    return {
        effect: computedEffect,
        get value() {
            computedValue = computedEffect();
            trackChildRun(computedEffect);
            return computedValue;
        }
    }
}
function trackChildRun(childEffect) {
    if (!effectsStack.length) return;
    const effect = effectsStack[effectsStack.length - 1];
    for (let i = 0; i < childEffect.deps.length; i++) {
        const dep = childEffect.deps[i];

        if (!dep.has(effect)) {
            dep.add(effect);
            effect.deps.push(dep);
        }
    }
}
複製代碼

修改watchEffect方法,接收一個opstion參數而且添加lazy屬性判斷,當lazytrue時不當即執行傳入的函數,由於computed方法是不會當即執行的。

function watchEffect(fn, options = {}) {
    // 建立一個響應式的影響函數,往effectsStack push一個effect函數,執行fn
    const effect = createReactiveEffect(fn, options);
    // start: 添加的代碼
    if (!options.lazy) {
        effect()
    }
    // end: 添加的代碼
    return effect;
}
複製代碼

修改createReactiveEffect方法,添加options參數,而且給當前的effect添加deps用於收集被計算的屬性的依賴,在本文的實例中就是age屬性的依賴集合,保存computedlazy屬性。

function createReactiveEffect(fn, options) {
    const effect = function() {
        // 判斷棧中是否已經有過該effect,避免遞歸循環重複添加,好比在監聽函數中修改依賴數據
        if (!effectsStack.includes(effect)) { 
            try {
                effectsStack.push(effect); // 將當前的effect推入棧中
                return fn(); // 執行fn
            } finally {
                effectsStack.pop(effect); // 避免fn執行報錯,在finally裏執行,將當前effect出棧
            }
        }
    }
    // start: 添加的代碼
    effect.deps = [];
    effect.computed = options.computed;
    effect.lazy = options.lazy;
    // end: 添加的代碼
    return effect;
}
複製代碼

track方法將收集到的屬性依賴集合添加到effectdeps

function track(target, key) { // 若是taeget中的key發生改變,執行棧中的effect方法
    const effect = effectsStack[effectsStack.length - 1];
    // 最新的effect,有才建立關聯
    if (effect) {
        let depsMap = targetsMap.get(target);
        if (!depsMap) { // 第一次渲染沒有,設置對應的匹配值
            targetsMap.set(target, depsMap = new Map());
        }
        let deps = depsMap.get(key);
        if (!deps) { // 第一次渲染沒有,設置對應的匹配值
            depsMap.set(key, deps = new Set());
        }
        if (!deps.has(effect)) {
            deps.add(effect);
            // start: 添加的代碼
            effect.deps.push(deps); // 將屬性的依賴集合掛載到effect
            // end: 添加的代碼
        }
    }
}
複製代碼

trigger方法經過以前保存在effectcomputed屬性區分是computed函數仍是普通的函數,而後分別保存起來,而後先執行普通的effect函數,在執行computed函數。

function trigger(target, key, type) {
    // 觸發更新,找到依賴effect
    let depsMap = targetsMap.get(target);

    if (depsMap) {
        let effects = new Set();
        let computedRunners = new Set();
        let deps = depsMap.get(key);

        if (deps) {
            deps.forEach(effect => {
                if (effect.computed) {
                    computedRunners.add(effect);
                } else {
                    effects.add(effect);
                }
            });
        }

        if ((type === 'ADD' || type === 'DELETE') && Array.isArray(target)) {
            const iterationKey = 'length';
            const deps = depsMap.get(iterationKey);
            if (deps) {
                deps.forEach(effect => {
                    effects.add(effect);
                });
            }
        }

        computedRunners.forEach(computed => computed());
        effects.forEach(effect => effect());
    }
}
複製代碼

總結computed執行流程

咱們來根據下面的代碼來分析執行流程。

const value = reactive({ count: 0 });
const cValue = computed(() => value.count + 1);
let dummy;

watchEffect(() => { 
    dummy = cValue.value;
    console.log(dummy)
});

value.count = 1;
複製代碼

第一步:先將count對象轉換成響應式的對象。

第二步:執行computed方法,computed內部會執行watchEffect,而且傳入lazycomputed屬性,因爲傳入了lazytrue,因此並不會當即執行生成的effect,爲了區分,下面統稱這個effect爲計算effect,將傳入的fn稱爲計算fn,也就是不會往棧中添加數據,此時cValue保存的是一個包含計算effectget方法的對象。

第三步:執行watchEffect方法:這也是最關鍵的一步,執行watchEffect方法,因爲沒有帶lazy屬性,因此此時會馬上執行effect方法,往effectsStack中添加當前的effect,而後執行fn

第四步:執行fn,執行fn中會去獲取cValue的值,此時觸發了computedget方法,而後執行第二步保存的計算effect

第五步:執行計算effect,將計算effect添加到effectsStack中(此時的effectsStack[普通effect, 計算effect]),而後執行計算fn

第五步:執行計算fn,計算fn依賴了響應式對象value,此時讀取valuecount屬性,觸發value對象的get方法,get方法中執行track方法收集依賴。

第六步:執行track方法,拿到棧中最後一個元素也就是計算effect,初始化targetsMapdepsMap,而後將計算effect保存到count對應的deps中,同時也將deps保存到計算effectdeps中,下一步要用,這樣就造成了一個雙向收集的關係,計算effect保存了count的全部依賴,count也存了計算effect的依賴,track方法執行完執行下一步,返回獲取到的value.count的值,存到computedValue中,而後咱們繼續往下執行。

第六步:執行trackChildRun,計算fn執行完則將計算effect從棧中推出,此時effectsStack的棧頂爲普通effect,首先咱們在trackChildRun中拿到棧尾元素也就是剩下的普通effect,而後循環傳入的計算effectdeps數據,咱們在上一步執行track的時候,在計算effectdeps中保存了count屬性對應的依賴集合,此時的deps中只有一個元素[計算effect],如今將普通effect也添加到dep中,因此此時depsMap{ count: [計算effect, 普通effect] }

第七步:執行value.count = 1,觸發set方法,執行trigger方法,獲取到count對應的deps也就是[計算effect, 普通effect],循環deps分別存儲普通的effect和計算effect,而後前後執行計算effect和普通effect

致謝

感謝小夥伴們看到了這裏,以爲本文寫的不錯的點個贊再走唄。(^o^)/~

相關文章
相關標籤/搜索