Vue3響應式原理與reactive、effect、computed實現

1、Vue3響應式系統簡介

Vue響應式系統的核心依然是對數據進行劫持,只不過Vue3採樣點是Proxy類,而Vue2採用的是Object.defineProperty()。Vue3之因此採用Proxy類主要有兩個緣由:vue

  • 能夠提高性能,Vue2是經過層層遞歸的方式對數據進行劫持,而且數據劫持一開始就要進行層層遞歸(一次性遞歸),若是對象的路徑很是深將會很是影響性能。而Proxy能夠在用到數據的時候再進行對下一層屬性的劫持
  • Proxy能夠實現對整個對象的劫持,而Object.defineProperty()只能實現對對象的屬性進行劫持。因此對於對象上的方法或者新增刪除的屬性則無能爲力。
// 展現使用Object.defineProperty()存在的缺點
const obj = {name: "vue", arr: [1, 2, 3]};
Object.keys(obj).forEach((key) => {
    let value = obj[key];
    Object.defineProperty(obj, key, {
        get() {
            console.log(`get key is ${key}`);
            return value;
        },
        set(newVal) {
            console.log(`set key is ${key}, newVal is ${newVal}`);
            value = newVal;
        }
    });
});
// 此時給對象新增一個age屬性
obj.age = 18; // 由於對象劫持的時候,沒有對age進行劫持,因此新增屬性沒法劫持
delete obj.name; // 刪除對象上已經進行劫持的name屬性,發現刪除屬性操做也沒法劫持
obj.arr.push(4); // 沒法劫持數組的push等方法
obj.arr[3] = 4; // 沒法劫持數組的索引操做,由於沒有對數組的每一個索引進行劫持,而且因爲性能緣由,Vue2並無對數組的每一個索引進行劫持
// 使用Proxy實現完美劫持
const obj = {name: "vue", arr: [1, 2, 3]};
function proxyData(value) {
    const proxy = new Proxy(value, {
        get(target, key) {
            console.log(`get key is ${key}`);
            const val = target[key];
            if (typeof val === "object") {
                return proxyData(val);
            }
            return val;
        },
        set(target, key, value) {
            console.log(`set key is ${key}, value is ${value}`);
            return target[key] = value;
        },
        deleteProperty(target, key) {
            console.log(`delete key is ${key}`);
        }
    });
    return proxy;
}
const proxy = proxyData(obj);
proxy.age = 18; // 可對新增屬性進行劫持
delete proxy.name; // 可對刪除屬性進行劫持
proxy.arr.push(4); // 可對數組的push等方法進行劫持
proxy.arr[3] = 4; // 可對象數組的索引操做進行劫持

2、Vue3響應式系統初體驗

Vue3的響應式系統被放到了一個單獨的@vue/reactivity模塊中,其提供了reactiveeffectcomputed等方法,其中reactive用於定義響應式的數據,effect至關因而Vue2中的watcher,computed用於定義計算屬性。咱們先來看一下這幾個函數的簡單示例,如:react

import {reactive, effect, computed} from "@vue/reactivity";
const state = reactive({
    name: "lihb",
    age: 18,
    arr: [1, 2, 3]
});
console.log(state); // 這裏返回的是Proxy代理後的對象
effect(() => {
    console.log("effect run");
    console.log(state.name); // 每當name數據變化將會致使effect從新執行
});
state.name = "vue"; // 數據發生變化後會觸發使用了該數據的effect從新執行

const info = computed(() => { // 建立一個計算屬性,依賴name和age
    return `name: ${state.name}, age: ${state.age}`;
});
effect(() => { // name和age變化會致使計算屬性的value發生變化,從而致使當前effect從新執行
    console.log(`info is ${info.value}`);
});

3、實現reactive方法

reactive() 方法本質是傳入一個要定義成響應式的target目標對象,而後經過Proxy類去代理這個target對象,最後返回代理以後的對象,如:數組

export function reactive(target) {
    return new Proxy(target, {
        get() {

        },
        set() {
            
        }
    });
}

若是咱們代理的僅僅是普通對象或者數組,那麼咱們能夠直接採用上面的形式,可是咱們還須要代理SetMapWeakMapWeakSet等集合類。因此爲了程序的擴展性,咱們須要根據target的類型動態的返回Proxy類的handler。咱們能夠改寫成以下形式:數據結構

// shared/index.js
export const isObject = (val) => val !== null && typeof val === 'object';
import {isObject} from "./shared";
import {mutableHandlers, mutableCollectionHandlers} from "./handlers";
const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]);

export function reactive(target) {
    // 給函數傳入不一樣的handlers而後經過target類型進判斷
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
}

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
    if (!isObject(target)) { // 若是傳入的target不是對象,那麼直接返回該對象便可
        return target;
    }
    // 根據傳入的target的類型判斷該使用哪一種handler,若是是Set或Map則採用collectionHandlers,若是是普通對象或數組則採用baseHandlers
    const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers);
    return observed;
}

接下來咱們就須要實現Proxy的handlers,對於普通對象和數組,咱們須要使用baseHandlers即mutableHandlers,Proxy的handler能夠代理不少方法,好比getsetdeletePropertyhasownKeys,若是將這些方法直接都寫在handlers上,那麼handlers就會變得很是多代碼,因此能夠將這些方法分開,以下:函數

// handlers.js
const get = createGetter();
const set = createSetter();
function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver); // 等價於target[key]
        console.log(`攔截到了get取值操做`, target, key);
        return res;
    }
}

function createSetter(shallow = false) {
    return function set(target, key, value, receiver) {
        const result = Reflect.set(target, key, value, receiver); // 等價於target[key] = value
        console.log(`攔截到了set設置值操做`, target, key, value);
        return result; // set方法必須返回一個值
    }
}

export const mutableHandlers = {
    get,
    set,
    // deleteProperty,
    // has,
    // ownKeys
}

export const mutableCollectionHandlers = {

}

Proxy的handlers對象中的get和set方法均可以拿到被代理的對象target、獲取或修改了對象的哪一個key,設置了新的值value,以及被代理後的對象receiver,目前咱們攔截到用戶的get操做後僅僅是從target中取出對應的值並返回回去,攔截到用戶的set操做後僅僅是修改了target中對應key的值並返回回去性能

此時會存在一個問題,若是咱們執行state.arr.push(4)這樣的一個操做,會發現僅僅觸發了arr的取值操做,並沒有收到arr新增了一個值的通知。由於Proxy代理只是淺層的代理,只代理了一層,因此咱們拿到的arr是一個普通數組,此時對普通數組進行操做是不會收到通知的。正是因爲Proxy是淺層代理,因此避免了一上來就遞歸,咱們須要修改get,在取到的值是對象的時候再去代理這個對象,如:ui

+ import { isObject } from "./shared";
+ import { reactive } from "./reactive";
function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver); // 等價於target[key]
        console.log(`攔截到了get取值操做`, target, key);
      + if (isObject(res)) { // 若是取到的值是一個對象,則代理這個值
      +    return reactive(res);
      + }
        return res;
    }
}

此時咱們再次執行state.arr.push(4),能夠看到輸出結果以下:prototype

攔截到了get取值操做 {name: "lihb", age: 18, arr: Array(3)} arr
攔截到了get取值操做 (3) [1, 2, 3] push
攔截到了get取值操做 (3) [1, 2, 3] length
攔截到了set設置值操做 (4) [1, 2, 3, 4] 3 4
攔截到了set設置值操做 (4) [1, 2, 3, 4] length 4

同時也觸發了length的修改,其實咱們將4 push進入數組後,數組的length會自動修改,也就是說不須要再去設置一遍length的值了,一樣的咱們執行state.arr[0] = 1也會觸發set操做,設置的是一樣的值也會觸發set操做,因此咱們須要判斷一下設置的新值和舊值是否相同,不一樣才須要觸發set操做代理

// shared/index.js
+ export const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key);
+ export const hasChanged = (newValue, oldValue) => newValue !== oldValue;
import { isObject, hasOwn, hasChanged } from "./shared";
function createSetter(shallow = false) {
    return function set(target, key, value, receiver) {
        const hadKey = hasOwn(target, key);
        const oldValue = target[key]; // 修改前獲取到舊的值
        const result = Reflect.set(target, key, value, receiver); // 等價於target[key] = value
        if (!hadKey) {  // 若是當前target對象中沒有該key,則表示是新增屬性
            console.log(`用戶新增了一個屬性,key is ${key}, value is ${value}`);
        } else if (hasChanged(value, oldValue)) { // 判斷一下新設置的值和以前的值是否相同,不一樣則屬於更新操做
            console.log(`用戶修改了一個屬性,key is ${key}, value is ${value}`);
        }
        return result;
    }
}

此時再次執行state.arr.push(4)就不會觸發length的更新了,執行state.arr[0] = 1也不會觸發索引爲0的值更新了。code

4、實現effect()方法

通過前面reactive()方法的實現,咱們已經可以拿到一個響應式的數據對象了,咱們進行get和set操做都可以被攔截。接下來就是實現effect()方法,當咱們修改數據的時候可以觸發傳入effect的回調函數執行
effect()方法的回調函數要想在數據發生變化後可以執行,必須返回一個響應式的effect()函數,因此effect()內部會返回一個響應式的effect。

所謂響應式的effect,就是該effect在執行的時候會在取值以前將本身放入到effectStack收到棧頂同時將本身標記爲activeEffect,以便進行依賴收集與reactive進行關聯

export function effect(fn, options = {}) {
    const effect = createReactiveEffect(fn, options); // 返回一個響應式的effect函數
    if (!options.lazy) { // 若是不是計算屬性的effect,那麼會當即執行該effect
        effect();
    }
    return effect;
}

let uid = 0;
let activeEffect; // 存放當前執行的effect
const effectStack = []; // 若是存在多個effect,則依次放入棧中
function createReactiveEffect(fn, options) {
    /** 
     * 所謂響應式的effect,就是該effect在執行的時候會將本身放入到effectStack收到棧頂,
     * 同時將本身標記爲activeEffect,以便進行依賴收集與reactive進行關聯
     * 
    */
    const effect = function reactiveEffect() {
        if (!effectStack.includes(effect)) { // 防止不停的更改屬性致使死循環
            try {
                // 在取值以前將當前effect放到棧頂並標記爲activeEffect
                effectStack.push(effect); // 將本身放到effectStack的棧頂
                activeEffect = effect; // 同時將本身標記爲activeEffect
                return fn(); // 執行effect的回調就是一個取值的過程
            } finally {
                effectStack.pop(); // 從effectStack棧頂將本身移除
                activeEffect = effectStack[effectStack.length - 1]; // 將effectStack的棧頂元素標記爲activeEffect
            }
        }
    }
    effect.options = options;
    effect.id = uid++;
    effect.deps = []; // 依賴了哪些屬性,哪些屬性變化了須要執行當前effect
    return effect;
}

這裏的取值操做就是傳入effect(fn)函數的fn的執行fn中會使用到響應式數據

此時數據發生變化還沒法通知effect的回調函數執行,由於reactive和effect還未關聯起來,也就是說尚未進行依賴收集,因此接下來須要進行依賴收集。

① 何時收集依賴?
咱們須要在取值的時候開始收集依賴,因此須要在取值以前將依賴的effect放到棧頂並標識爲activeEffect,而前面響應式effect執行的時候已經實現,而執行effect回調取值的時候會在Proxy的handlers的get中進行取值,因此咱們須要在這裏進行依賴收集。

+ import { track, trigger } from "./effect";
function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver); // 等價於target[key]
        console.log(`攔截到了get取值操做`, target, key);
      + track(target, "get", key); // 取值的時候開始收集依賴
        if (isObject(res)) {
            return reactive(res);
        }
        return res;
    }
}

一樣的,須要在Proxy類的handlers的set中觸發依賴的執行

function createSetter(shallow = false) {
    return function set(target, key, value, receiver) {
        const hadKey = hasOwn(target, key);
        const oldValue = target[key]; // 修改前獲取到舊的值
        const result = Reflect.set(target, key, value, receiver); // 等價於target[key] = value
        if (!hadKey) {  // 若是當前target對象中沒有該key,則表示是新增屬性
            console.log(`用戶新增了一個屬性,key is ${key}, value is ${value}`);
          + trigger(target, "add", key, value); // 新增了一個屬性,觸發依賴的effect執行
        } else if (hasChanged(value, oldValue)) { // 判斷一下新設置的值和以前的值是否相同,不一樣則屬於更新操做
            console.log(`用戶修改了一個屬性,key is ${key}, value is ${value}`);
          + trigger(target, "set", key, value); // 修改了屬性值,觸發依賴的effect執行
        }
        return result;
    }
}

② 如何收集依賴,如何保存依賴?
首先依賴是一個一個的effect函數,咱們能夠經過Set集合進行存儲,而這個Set集合確定是要和對象的某個key進行對應,即哪些effect依賴了對象中某個key對應的值,這個對應關係能夠經過一個Map對象進行保存,即:

// depMap
{
    someKey: [effect1, effect2,..., effectn] // 用集合存儲依賴的effect,並放入Map對象中與對象的key相對應
}

若是隻有一個響應式對象,那麼咱們直接用一個全局的Map對象根據不一樣的key進行保存便可,即用上面的Map結構就能夠了。
可是咱們的響應式對象是能夠建立多個的,而且每一個響應式對象的key也可能相同,因此僅僅經過一個Map結構以key的方式保存是沒法實現的。
既然響應式對象有多個,那麼就能夠以整個響應式對象做爲key進行區分,而可以用一個對象做爲key的數據結構就是WeakMap,因此咱們能夠用一個全局的WeakMap結構進行存儲,以下:

// 全局的WeakMap
{
    targetObj1: {
        someKey: [effect1, effect2,..., effectn]
    },
    targetObj2: {
        someKey: [effect1, effect2,..., effectn]
    }
    ...
}

當咱們取值的時候,首先經過該target對象從全局的WeakMap對象中取出對應的depsMap對象,而後根據修改的key獲取到對應的dep依賴集合對象,而後將當前effect放入到dep依賴集合中,完成依賴的收集。

// 用一個全局的WeakMap結構以target做爲key保存該target對象下的key對應的依賴
const targetMap = new WeakMap(); 
/** 
 * 取值的時候開始收集依賴,即收集effect
*/
export function track(target, type, key) {
    if (activeEffect == undefined) { // 收集依賴的時候必需要存在activeEffect
        return;
    }
    let depsMap = targetMap.get(target); // 根據target對象取出當前target對應的depsMap結構
    if (!depsMap) { // 第一次收集依賴可能不存在
        targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key); // 根據key取出對應的用於存儲依賴的Set集合
    if (!dep) { // 第一次可能不存在
        depsMap.set(key, (dep = new Set()));
    }
    if (!dep.has(activeEffect)) { // 若是依賴集合中不存在activeEffect
        dep.add(activeEffect); // 將當前effect放到依賴集合中
        // 一個effect可能使用到了多個key,因此會有多個dep依賴集合
        activeEffect.deps.push(dep); // 讓當前effect也保存一份dep依賴集合
    }
}

觸發依賴更新,當修改值的時候,也是經過target對象從全局的WeakMap對象中取出對應的depMap對象,而後根據修改的key取出對應的dep依賴集合,並遍歷該集合中的全部effect,並執行effect。
每次effect執行,都會從新將當前effect放到棧頂,而後執行effect回調再次取值的時候,再一次執行track收集依賴,不過第二次track的時候,對應的依賴集合中已經存在當前effect了,因此不會再次將當前effect添加進去了。

/** 
 * 數據發生變化的時候,觸發依賴的effect執行
*/
export function trigger(target, type, key, value) {
    const depsMap = targetMap.get(target); // 獲取當前target對應的Map
    if (!depsMap) { // 若是該對象沒有收集依賴
        console.log("該對象還未收集依賴"); // 好比修改值的時候,沒有調用過effect
        return;
    }
    const effects = new Set(); // 存儲依賴的effect
    const add = (effectsToAdd) => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => {
                effects.add(effect);
            });
        }
    };
    const run = (effect) => {
        effect(); // 當即執行effect
    }
    /** 
     *  對於effect中使用到的數據,那確定是響應式對象中已經存在的key,當數據變化後確定能經過該key拿到對應的依賴,
     * 對於新增的key,咱們也不須要通知effect執行。
     * 可是對於數組而言,若是給數組新增了一項,咱們是須要通知的,若是咱們仍然以key的方式去獲取依賴那確定是沒法獲取到的,
     * 由於也是屬於新增的一個索引,以前沒有對其收集依賴,可是咱們使用數組的時候會使用JSON.stringify(arr),此時會取length屬性,
     * 索引會收集length的依賴,數組新增元素後,其length會發生變化,咱們能夠經過length屬性去獲取依賴
    */
    if (key !== null) {
        add(depsMap.get(key)); // 對象新增一個屬性,因爲沒有依賴故不會執行
    }
    if (type === "add") { // 處理數組元素的新增
        add(depsMap.get(Array.isArray(target)? "length": ""));
    }
    // 遍歷effects並執行
    effects.forEach(run);
}

此時已經完成了effect和active的關聯了,當數據發生變化的時候,就會遍歷以前收集的依賴,從而從新執行effect,effect的執行必然會致使effect的回調函數執行。

5、實現computed()方法

計算屬性本質也是一個effect,也就是說,計算屬性內部會建立一個effect對象,只不過這個effect不是當即執行,而是等到取值的時候再執行,從以前computed的用法中,能夠看到,computed()函數返回一個對象,而且這個對象中有一個value屬性能夠進行get和set操做

import {isFunction} from './shared/index';
import { effect, track, trigger } from './effect';
export function computed(getterOrOptions) {
    let getter;
    let setter;
    if (isFunction(getterOrOptions)) {
        getter = getterOrOptions;
        setter = () => {};
    } else {
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
    }
    let dirty = true; // 默認是髒的數據
    let computed;
    // 計算屬性本質也是一個effect,其回調函數就是計算屬性的getter
    let runner = effect(getter, {
        lazy: true, // 默認是非當即執行,等到取值的時候再執行
        computed: true, // 標識這個effect是計算屬性的effect
        scheduler: () => { // 數據發生變化的時候不是直接執行當前effect,而是執行這個scheduler弄髒數據
            if (!dirty) { // 若是數據是乾淨的
                dirty = true; // 弄髒數據
                trigger(computed, "set", "value"); // 數據變化後,觸發value依賴
            }
        }
    });
    let value;
    computed = {
        get value() {
            if (dirty) {
                value = runner(); // 等到取值的時候再執行計算屬性內部建立的effect
                dirty = false; // 取完值後數據就不是髒的了
                track(computed, "get", "value"); // 對計算屬性對象收集value屬性
            }
            return value;
        },
        set value(newVal) {
            setter(newVal);
        }
    }
    return computed;
}

因爲計算屬性的effect比較特殊,不是當即執行,因此不能像以前同樣,數據發生變化後,都遍歷並當即執行effect,須要將計算屬性的effect和普通的effect分開處理,若是是計算屬性的effect,則執行其scheduler()方法將數據弄髒便可。僅僅修改run()方法便可,如:

/** 
 * 數據發生變化的時候,觸發依賴的effect執行
*/
export function trigger(target, type, key, value) {
    const depsMap = targetMap.get(target); // 獲取當前target對應的Map
    if (!depsMap) { // 若是該對象沒有收集依賴
        console.log("該對象還未收集依賴"); // 好比修改值的時候,沒有調用過effect
        return;
    }
    const effects = new Set(); // 存儲依賴的effect
    const add = (effectsToAdd) => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => {
                effects.add(effect);
            });
        }
    };
    // const run = (effect) => {
    //     effect(); // 當即執行effect
    // }
    // 修改run方法,若是是計算屬性的effect則執行其scheduler方法
   + const run = (effect) => {
   +     if (effect.options.scheduler) { // 若是是計算屬性的effect則執行其scheduler()方法
   +        effect.options.scheduler();
   +     } else { // 若是是普通的effect則當即執行effect方法
   +         effect();
   +     }
   + }
    /** 
     *  對於effect中使用到的數據,那確定是響應式對象中已經存在的key,當數據變化後確定能經過該key拿到對應的依賴,
     * 對於新增的key,咱們也不須要通知effect執行。
     * 可是對於數組而言,若是給數組新增了一項,咱們是須要通知的,若是咱們仍然以key的方式去獲取依賴那確定是沒法獲取到的,
     * 由於也是屬於新增的一個索引,以前沒有對其收集依賴,可是咱們使用數組的時候會使用JSON.stringify(arr),此時會取length屬性,
     * 索引會收集length的依賴,數組新增元素後,其length會發生變化,咱們能夠經過length屬性去獲取依賴
    */
    if (key !== null) {
        add(depsMap.get(key)); // 對象新增一個屬性,因爲沒有依賴故不會執行
    }
    if (type === "add") { // 處理數組元素的新增
        add(depsMap.get(Array.isArray(target)? "length": ""));
    }
    // 遍歷effects並執行
    effects.forEach(run);
}
相關文章
相關標籤/搜索