关于vue.js:Vue3响应式原理与reactiveeffectcomputed实现

4次阅读

共计 12492 个字符,预计需要花费 32 分钟才能阅读完成。

一、Vue3 响应式零碎简介

Vue 响应式零碎的外围仍然是 对数据进行劫持,只不过 Vue3 采样点是Proxy 类,而 Vue2 采纳的是Object.defineProperty()。Vue3 之所以采纳 Proxy 类次要有两个起因:

  • 能够晋升性能,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; // 可对象数组的索引操作进行劫持

二、Vue3 响应式零碎初体验

Vue3 的响应式零碎被放到了一个独自的 @vue/reactivity 模块中,其提供了 reactiveeffectcomputed 等办法,其中 reactive 用于 定义响应式的数据 ,effect 相当于是 Vue2 中的watcher,computed 用于定义 计算属性。咱们先来看一下这几个函数的简略示例,如:

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}`);
});

三、实现 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,在取到的值是对象的时候再去代理这个对象,如:

+ 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),能够看到输入后果如下:

拦挡到了 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 的值更新了。

四、实现 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 的回调函数执行。

五、实现 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);
}
正文完
 0