深入vue3响应式

1,103 阅读6分钟

vue3响应式简而言之就是:
1.effect中的所有属性,都会收集 effect。 2.当这个属性值发生变化,会重新执行 effect。 下面通过手写简易vue3响应式,深入理解吧。

1.reactiveApi 实现

package/reactivity/src/index 导出响应式的各种方法

export {
    reactive,
    shallowReactive,
    shallowReadonly,
    readonly
} from './reactive'

export {
    effect
} from './effect'


export {
    ref,
    shallowRef,
    toRef,
    toRefs
} from './ref'

package/reactivity/src/index/reactivity 函数柯里化

Vue3中采用proxy实现数据代理, 核心就是拦截get方法和set方法,当获取值时收集effect函数,当修改值时触发对应的effect重新执行

import { isObject } from "@vue/shared"
import {
    mutableHandlers,
    shallowReactiveHandlers,
    readonlyHandlers,
    shallowReadonlyHandlers
} from './baseHandlers'
export function reactive(target){
    return createReactiveObject(target,false,mutableHandlers)
}
export function shallowReactive(target){
    return createReactiveObject(target,false,shallowReactiveHandlers)
}

export function readonly(target){
    return createReactiveObject(target,true,readonlyHandlers)
}

export function shallowReadonly(target){
    return createReactiveObject(target,true,shallowReadonlyHandlers)
}

const reactiveMap = new WeakMap();
const readonlyMap = new WeakMap();
//核心方法 createReactiveObject
//reactive 这个 api 只能拦截对象类型  
//WeakMap将要代理的对象 和对应代理结果缓存起来,如果已经被代理了,直接返回即可。  
//reactiveMap/readonlyMap 对应响应/只读映射表
export function createReactiveObject(target,isReadonly,baseHandlers){
    if( !isObject(target)){
        return target;
    }
    const proxyMap = isReadonly? readonlyMap:reactiveMap
    const existProxy = proxyMap.get(target);
    if(existProxy){
        return existProxy;
    }
    const proxy = new Proxy(target,baseHandlers);

    proxyMap.set(target,proxy); 

    return proxy;
}

package/reactivity/src/index/baseHandlers 实现 reactive、shallowReactive、readonly、shallowReadonly 对应的handler: setget-> createGetter/createSetter
get 收集依赖
set 触发更新,区分是新增、修改。

// 实现 new Proxy(target, handler)

import { extend, hasChanged, hasOwn, isArray, isIntegerKey, isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOrTypes } from "./operators";
import { reactive, readonly } from "./reactive";


const get = createGetter();
const shallowGet = createGetter(false, true);
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);
const set = createSetter();
const shallowSet = createSetter(true);

export const mutableHandlers = {
    get,
    set
}
export const shallowReactiveHandlers = {
    get: shallowGet,
    set: shallowSet
}

let readonlyObj = {
    set: (target, key) => {
        console.warn(`set on key ${key} falied`)
    }
}
export const readonlyHandlers = extend({
    get: readonlyGet,
}, readonlyObj)
export const shallowReadonlyHandlers = extend({
    get: shallowReadonlyGet,
}, readonlyObj)

function createGetter(isReadonly = false, shallow = false) { // 拦截获取功能
    return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver); // 等价于target[key];
        if(!isReadonly){
            // 收集依赖,等会数据变化后更新对应的视图
            track(target,TrackOpTypes.GET,key)
        }
        if(shallow){
            return res;
        }
        if(isObject(res)){
            // vue2 是一上来就递归,vue3 是当取值时会进行代理 。 vue3的代理模式是懒代理
            return isReadonly ? readonly(res) : reactive(res)
        }
        return res;
    }
}
function createSetter(shallow = false) { // 拦截设置功能
    return function set(target, key, value, receiver) {

        const oldValue = target[key]; // 获取老的值
        
       //target中是否有属性key
        let hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target,key);
        //修改的是数组,key是索引,索引在数组长度内
        //修改的是对象,看一下当前target中有没有这个属性key

        const result = Reflect.set(target, key, value, receiver); //等价于 target[key] = value


        if(!hadKey){
            // 新增
            trigger(target,TriggerOrTypes.ADD,key,value);
        }else if(hasChanged(oldValue,value)){
            // 修改 并老值!==新值
            trigger(target,TriggerOrTypes.SET,key,value,oldValue)
        }
        // 当数据更新时 通知对应属性的effect重新执行


        return result;
    }
}


2.effect实现

package/reactivity/src/index/effect

响应的 effect,可以做到数据变化重新执行,默认会先执行一次 全局变量 activeEffect 保存当前的 effect 使用 effectStack 栈来记录当前的 activeEffect

export function effect(fn, options: any = {}) {
    const effect = createReactiveEffect(fn, options);
    if (!options.lazy) {
        effect();
    }
    return effect;
}
let uid = 0;
let activeEffect; // 存储当前的effect,当前正在运行的effect
const effectStack = []
function createReactiveEffect(fn, options) {
    const effect = function reactiveEffect() {
        if (!effectStack.includes(effect)) { // 保证effect没有加入到effectStack中
            try {
                effectStack.push(effect);
                activeEffect = effect;
                return fn(); // 函数执行时会取值  会执行get方法
            } finally {
                effectStack.pop();
                activeEffect = effectStack[effectStack.length - 1];
            }
        }
    }
    effect.id = uid++; // 制作一个effect标识 用于区分effect
    effect._isEffect = true; // 用于标识这个是响应式effect
    effect.raw = fn; // 保留effect对应的原函数
    effect.options = options; // 在effect上保存用户的属性
    return effect;
}

3.track依赖收集实现

package/reactivity/src/index/effect

区别在于 vue2 任何属性变化 watcher.update 都会执行 effect 没被用到的数据变化 effect 不会重新执行 effect 等价于 vue2 中的 watcher

track 方法 收集依赖 将 key 和对应的 effect 关联起来
weakMap key: {age:12} value:(map) =>{age => set(effect)}



const targetMap = new WeakMap();
// 让某个对象中的属性 收集当前他对应的effect函数
export function track(target, type, key) {
    if (activeEffect === undefined) {
        // 此属性不用收集依赖,因为没在effect中使用
        return;
    }
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        //如果没有targetMap.get(target)
        targetMap.set(target, (depsMap = new Map));
    }
    let dep = depsMap.get(key);
    if (!dep) {
        //如果没有depsMap.get(key)
        depsMap.set(key, (dep = new Set))
    }
    if (!dep.has(activeEffect)) {
        //如果没有dep.has(activeEffect)
        dep.add(activeEffect);
    }
}

4.trigger触发更新

触发set方法,trigger方法触发更新,让key对应的effect执行

  • 看修改的是不是数组的长度 因为改长度影响比较大 如果更改的长度小于收集的索引,那么这个索引也需要触发effect重新执行

  • 可能是对象

  • 如果修改数组中的某一个索引怎么办?
    如果添加了一个索引就触发长度的更新

package/reactivity/src/index/effect

import { isArray, isIntegerKey } from "@vue/shared";
import { TriggerOrTypes } from "./operators";

// 找属性对应的effect 让其执行 (数组、对象)
export function trigger(target, type, key?, newValue?, oldValue?) {

    // 如果这个属性没有 收集过effect,那不需要做任何操作
    const depsMap = targetMap.get(target);
    if (!depsMap) return;

    const effects = new Set(); // 这里对effect去重了
    const add = (effectsToAdd) => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => effects.add(effect));
        }
    }
    // 我要将所有的 要执行的effect 全部存到一个新的集合中,最终一起执行

    // 1. 看修改的是不是数组的长度 因为改长度影响比较大
    if (key === 'length' && isArray(target)) {
        // 如果对应的长度 有依赖收集需要更新
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key > newValue) { // 如果更改的长度 小于收集的索引,那么这个索引也需要触发effect重新执行
                add(dep)
            }
        })
    } else {
        // 可能是对象
        if (key !== undefined) { // 这里肯定是修改, 不能是新增
            add(depsMap.get(key)); // 如果是新增
        }
        // 如果修改数组中的 某一个索引 怎么办?
        switch (type) {  // 如果添加了一个索引就触发长度的更新
            case TriggerOrTypes.ADD:
                if (isArray(target) && isIntegerKey(key)) {
                    add(depsMap.get('length'));
                }
        }
    }
    effects.forEach((effect: any) => effect())
}


5.ref实现

ref本质就是通过类的属性访问器来实现的,可以将一个普通值类型进行包装。ref将普通的类型转化成一个对象,这个对象中有value属性,指向原来的值。

import { hasChanged, isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operations";
import { reactive } from "./reactive";

export function ref(value) { // ref Api
    return createRef(value);
}

export function shallowRef(value) { // shallowRef Api
    return createRef(value, true);
}
function createRef(rawValue, shallow = false) {
    return new RefImpl(rawValue, shallow)
}

const convert = (val) => isObject(val) ? reactive(val) : val; // 递归响应式

class RefImpl {
    public _value; //表示 声明了一个_value属性 但是没有赋值
    public __v_isRef = true; // 产生的实例会被添加 __v_isRef 表示是一个ref属性
    constructor(public rawValue, public shallow) { // 参数中前面增加修饰符 标识此属性放到了实例上
        this._value = shallow ? rawValue : convert(rawValue)// 如果是深度 需要把里面的都变成响应式的
    }
    // 类的属性访问器
    get value() { // 代理 取值取value 会帮我们代理到 _value上
        track(this, TrackOpTypes.GET, 'value');
        return this._value
    }
    set value(newValue) {
        if (hasChanged(newValue, this.rawValue)) { // 判断老值和新值是否有变化
            this.rawValue = newValue; // 新值会作为老值
            this._value = this.shallow ? newValue : convert(newValue);
            trigger(this, TriggerOrTypes.SET, 'value', newValue);
        }
    }
}

6.toRefs实现

将对象中的属性转换成ref属性,toRefs基于ref,遍历对象加上ref

class ObjectRefImpl {
    public __v_isRef = true;
    constructor(public target, public key) {}
    get value(){ // 代理  
        return this.target[this.key] // 如果原对象是响应式的就会依赖收集
    }
    set value(newValue){
        this.target[this.key] = newValue; // 如果原来对象是响应式的 那么就会触发更新
    }
}

// 将某一个key对应的值 转化成ref
export function toRef(target, key) { // 可以把一个对象的值转化成 ref类型
    return new ObjectRefImpl(target, key)
}


export function toRefs(object){ // object 可能传递的是一个数组 或者对象
    const ret = isArray(object) ? new Array(object.length) :{}
    for(let key in object){
        ret[key] = toRef(object,key);
    }
    return ret;
}

7.vue2、vue3区别

vue2 是一上来就对data中的数据进行递归,vue3 是当取值时会进行代理。 vue3 的代理模式是懒代理。让某个对象中的属性,收集当前他对应的 effect 函数,相当于 vue2 中的 watcher。 ref内部使用的是defineProperty,reactive内部采用的proxy

对于数组的处理,vue2采用了重写数组方法。vue3则是trigger方法触发更新时,进行判断,如果添加了一个索引就触发长度的更新,如果更改的数组长度小于收集的索引,那么这个索引也需要触发effect重新执行。