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

一、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);
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理