Vue3 → 响应式 effect 拓展 - runner、scheduler、stop/onStop、readonly/isReadonly/...

111 阅读19分钟

effect -> runner「返回逻辑加强」

功能概述

  • 调用effect之后会返回一个函数 - runner;
  • 后续手动调用返回的runner之后会有两个作用
    • 作用一:再次调用 effect中的fn
    • 拿到调用fn之后的返回值并返回给外界 - 返回fn内的return

测试用例

it('return runner when call effect', () => {
    let foo = 10.1;
    const runner = effect(()=>{
        foo++;
        return +(foo.toFixed(2))
    })
    expect(foo).toBe(11.1)
    let r= runner()
    expect(foo).toBe(12.1)
    r= runner()
    expect(foo).toBe(13.1)
    expect(r).toBe(13.10)
});

内部实现

effect 初版参考
Vue3 响应式 reactive、ref、effect 实现浅析

  • 更改effect中的run方法 使其返回缓存的函数实例
class ReactiveEffect {
  private _fn: any;
  constructor(fn) {
    this._fn = fn;
  }
  run() {
    const result = this._fn();
    activeEffect = this;

    return result;
  }
}
  • 更改effect的实现 使其返回ReactiveEffect中的run函数 - 更改this指向到当前实例
export const extend = Object.assign

export function effect(fn,options:any = {}) {
    const { scheduler } = options
    const _effect = new ReactiveEffect(fn)
    // Object.assign(_effect,options) //全局挂载
    extend(_effect,options) //全局挂载
    _effect.run()
    
    const runner:any = _effect.run.bind(_effect)
    // call和apply都会改变this指向 并且立即执行对应的函数
    // bind也是改变this指向  但是不会立即执行  而是返回一个永久改变this指向的函数
    
    runner.effect = _effect
    
    // 返回被改变过this到_effect的一个函数  供外部调用effect后手动再次调用
    return runner
}

effect -> scheduler「调度器」

可调度性是响应式系统中非常重要的特性,所谓可调度性是指当trigger动作触发副作用函数重新执行时,有能力决定副作用函数的执行时机、次数和方式等

scheduler 概述

  • scheduler就是调度器的意思
    • 调度对象:一些任务
    • 什么样的任务需要scheduler进行调度:通过常规的API无法马上给出结果的任务,这些任务会分发到scheduler中,内部根据时间的carryer或者interval来定时发送或者延时触发的任务(异步任务)
  • 具体应用场景分析
    • 网站延时删除视频的原理就是scheduler调度器的作用
      • 当删除视频的时候,有时会考虑到数据恢复或审核的需求,删除数据后仅仅在观看端是看不到的,需要做一个source delay操作,后台是真实存在的,后台会将这个数据下放,然后再scheduler过指定时间后进行真正的删除操作,这个scheduler会定时进行运行操作;
  • scheduler 通用场景
    • 做一些异步的、周期性的任务,一些瞬时不会给出结果的任务

scheduler 执行过程阶段分析

  • 三个阶段
    • 前置刷新阶段
    • 刷新阶段
    • 后置刷新阶段
  • 每个阶段两种状态
    • 等待刷新中
    • 正在刷新 「每个阶段的状态发生变化后,对应阶段的两种状态都会进行重置,执行顺序是:前置 → 当前 → 后置 → 前置 ...的顺序」 「nextTick是等所有阶段的刷新任务都结束后返回一个Promise.resolve」
具体分析

在刷新的三个阶段中,都会维护对应的回调任务池,首先会启动一个微任务,依次执行对应的阶段任务

  • 前置刷新阶段
    • 先进行前置刷新阶段,当前置刷新阶段的回调任务池为空时,执行刷新阶段逻辑
  • 刷新阶段
    • 当前置刷新阶段的回调队列为空时执行该阶段,直到当前阶段的回调任务池为空,执行后续阶段逻辑
  • 后置刷新
    • 当刷新阶段的回调队列为空时执行该阶段,直到当前阶段的回调任务池为空,执行完后置刷新阶段后会循环上述步骤,直到三个阶段的任务池中的逻辑都完成

测试用例

it('scheduler', () => {
    // 1、通过effect的第二个参数给定一个 scheduler 的 fn
    // 2、effect 首次执行时 会执行 fn
    // 3、当响应式对象 set 时  不会执行 fn 而是去执行 schedule
    // 4、当执行 runner 的时候会再次执行 fn  此时不会执行 schedule
    let dummy,
        run,
        tempData;
    const scheduler = jest.fn(() => {
        run = runner;
        tempData = dummy
    })

    const obj = reactive({ foo: 1 })

    const runner = effect(()=>{
        dummy = obj.foo
    },{scheduler})

    expect(scheduler).not.toHaveBeenCalled();
    expect(dummy).toBe(1) //首次effect执行fn  run = runner
    obj.foo++ // 执行scheduler 不执行fn
    expect(scheduler).toHaveBeenCalledTimes(1) // 响应式对象 set 执行了 schedule
    expect(dummy).toBe(1) //响应式对象 set 没有执行fn

    run() // run即runner 执行fn
    expect(dummy).toBe(2)
    obj.foo++ // 执行scheduler 不执行fn
    expect(dummy).toBe(2)
    // expect(scheduler).toHaveBeenCalledTimes(2)  // ❎ 执行了一次 调用run时不执行scheduler
    expect(tempData).toBe(2) // 只在set的时候执行 scheduler
});

内部实现

  • 更改effect中的_effect实例化逻辑,解构出schedulerReactiveEffect类中,供后续set时触发trigger后进行拦截使用,保证在set的时候只走scheduler操作,不走run方法
export function effect(fn,options:any = {}) {
    const { scheduler } = options
    const _effect = new ReactiveEffect(fn,scheduler)
    // Object.assign(_effect,options) //全局挂载
    extend(_effect,options) //全局挂载
    // _effect.onStop = options.onStop
    _effect.run()
    const runner:any = _effect.run.bind(_effect)
    // call和apply都会改变this指向 并且立即执行对应的函数
    // bind也是改变this指向  但是不会立即执行  而是返回一个永久改变this指向的函数
    runner.effect = _effect
    return runner
}
  • 更改ReactiveEffect中的constructor,将scheduler挂载到public上或者挂载到this上,进而在activeEffect全局变量上加scheduler配置进而打到dep中供trigger获取使用
class ReactiveEffect {
  private _fn: any;
  constructor(fn, public scheduler?) {
    // 公共属性 scheduler 才会被允许在类外部执行  方便在track中进行调用
    this._fn = fn;
  }
  run() {
    const result = this._fn();
    activeEffect = this;
    // 此时`scheduler`已经挂载到this上,通过将this赋值到全局变量上后供后续`trck`依赖收集时收集到对应的`scheduler`配置
    return result;
  }
}
  • 更改trigger逻辑,根据scheduler配置进行set触发依赖的拦截操作
export function trigger(target,key) {
    let depsMap = targetMap.get(target),
        dep = depsMap.get(key);
    for (const effect of dep) {
        if(effect.scheduler){
            effect.scheduler()
        } else {
            effect.run()
        }
        
    }
}

参考文献

Vue3 任务调度器 scheduler 源码分析
趣谈scheduler任务调度器

拓展实现 - 调度器的过度状态

在某些场景下,副作用函数的执行是不需要执行中间的过度状态的,比如两次更改依赖的数据只需要副作用函数执行最后一次即可,这时就可以用调度器的过度拓展来实现了;

实现原理
  • 内部依赖于Set数据结构进行自动去重能力,在Effect中的scheduler配置里进行依赖数据添加到队列中
  • 添加过度状态内部依赖于变量标志进行控制,当第一次触发时就将其更改为false,阻止后续再次出发,保证一个周期内只会执行一次
    • 注意:内部队列的遍历执行副作用函数的逻辑需要写在Promise.then中,在微任务队列里完成副作用函数的执行
  • 外部的实现还是正常的,当依赖变化时,Effect中的scheduler还是执行两遍,只是内部的Set数据结构会进行去重,scheduler内部的过度拓展控制执行次数
内部实现
// 定义一个任务队列
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()

let isFlushing = false //保证一个周期内副作用函数只会执行一次
function flushJob() {
  if (isFlushing) return
  
  isFlushing = true //首次执行 将后续再次执行的情况拦截
 
  p.then(() => {
    jobQueue.forEach(fn => fn()) //在微任务队列里进行副作用函数触发
  }).finally(() => {
    
    isFlushing = false //微任务队列执行结束后关闭 isFlushing 拦截
  })
}

effect(() => {
  console.log(obj.foo)
}, {
  scheduler(fn) {
    // 在调度器中将副作用函数添加到队列中
    jobQueue.add(fn)
    flushJob()
  }
})

obj.foo++
obj.foo++

// code by 《《Vue设计与实现》》

Vue中的多次修改响应式数据但只会触发一次更新就是通过更加完善的调度器逻辑实现的

effect -> stop「清除依赖收集」 -> onstop「调用stop时执行onstop」

功能概述

作用就是拿到effect返回的runner,将当前返回runner的effect实例从依赖收集器中全部清除掉,从而避免响应式数据改变的时候自动执行runner函数,不需要通知update进行依赖触发更新,而onstop可以在stop执行的时候做出相应的逻辑;

测试用例

it('stop effect', () => {
    // 通过该API之后将停止进行响应式数据的runner调用 - 不需要通知update进行依赖触发更新
    let dummy;
    const obj = reactive({prop:1});
    const runner = effect(() => {
        dummy = obj.prop
    })
    obj.prop = 2
    expect(dummy).toBe(2)

    stop(runner)

    obj.prop = 3
    // obj.prop++
    // obj.prop++ => obj.prop = obj.prop+1 会先触发get操作 再进行set操作 在get操作中会再次收集依赖(effect的回调函数) 从而使得stop的清除依赖失效,需要单独进行兼容处理

    expect(dummy).toBe(2)

    runner()
    expect(dummy).toBe(3)
});

// 调用stop后的回调函数,允许调用stop后可进行其它操作
it('onStop', () => {
    const obj = reactive({
        foo: 1
    })

    const onStop = jest.fn()
    // 当调用stop时会自动执行onStop  允许用户的自定义逻辑正常编译
    let dummy;

    const runner = effect(
        ()=>{
            dummy = obj.foo
        },
        {
            onStop
        }
    )

    stop(runner)

    expect(onStop).toBeCalledTimes(1)
});

内部实现

  • stop自身依赖于effect返回的runner,而stop内部是通过effect缓存的实例上进行调用stop进而删除缓存数据的,因此第一步就需要将effect返回的runner进行反向收集effect依赖
export function effect(fn,options:any = {}) {
    const { scheduler } = options
    // 创建 ReactiveEffect 类,生成effect工厂,每次生成唯一的effect,每次调用effect的时候,执行fn方法。
    const _effect = new ReactiveEffect(fn,scheduler)
    // Object.assign(_effect,options) //全局挂载
    extend(_effect,options) //全局挂载
    // _effect.onStop = options.onStop
    _effect.run()
    const runner:any = _effect.run.bind(_effect)
    // call和apply都会改变this指向 并且立即执行对应的函数
    // bind也是改变this指向  但是不会立即执行  而是返回一个永久改变this指向的函数
    
    runner.effect = _effect
    // _effect上会通过`new`继承到ReactiveEffect类上的stop属性方法
    // 反向收集依赖 供外部runner实例进行effect缓存逻辑调用
    
    // 返回一个runner函数
    return runner
}
  • stop 内部依赖于返回的runner实例上反向收集的effect属性
export function stop(runner){
    runner.effect.stop()
} 
  • 在track收集依赖的时候应该反向收集依赖dep,用于外部如stop的等逻辑对收集的依赖进行处理
export function trck(target,key){

    if(!shouldTrack) return
    // 依赖收集
    let depsMap = targetMap.get(target)

    if(!depsMap){
        depsMap = new Map()

        targetMap.set(target,depsMap)
    }

    let dep = depsMap.get(key)
    if(!dep){
        dep = new Set()
        depsMap.set(key,dep)
    }

    if(dep.has(activeEffect)) return 
    dep.add(activeEffect)

    activeEffect.deps.push(dep) // 反向收集依赖dep 用于stop等外部方法操作收集的dep
}
  • effect内部需要进行stop处理,即对缓存的依赖进行清除,避免响应式数据变化时自动触发对应的runner函数逻辑,但需要做避免多次清除的逻辑处理
class ReactiveEffect {
    private _fn
    active = true //avtive 属性控制stop方式执行的状态,stop执行其变为false
    onStop?:()=>void // 可选方法用?: 声明  供外部extend(_effect,options)时进行内部挂载
    constructor(fn, public scheduler?){
        // public scheduler 外部能够获取到scheduler方法
        this._fn = fn
    }
    
    // 每次effect被调用时候,执行run方法,进而执行fn()方法
    run(){
        // 如果调用了 stop,就直接调用fn函数
        // 在单测中单纯调用 runner 后会走到这 避免外部单独调用runner时再次收集依赖
        if(!this.active) this._fn(); 

        shouldTrack = true
        activeEffect = this;
        const result = this._fn()

        // reset
        shouldTrack = false
        return result
    }

    stop(){
        // 避免外部多次调用stop后重复清除已清除依赖的逻辑操作 节约性能
        // 当this.active为true时,代表stop第一次执行
        if(this.active){
            cleanupEffect(this)
            // 如果外部有onStop配置 则在调用stop的过程中调用传入的onStop配置
            if(this.onStop){
                this.onStop()
            }
            this.active = false
        }
    }
}

function cleanupEffect(effect) {
  // 取出effect中反向收集到的deps,循环删除dep中的effect
  effect.deps.forEach((dep: any) => {
    // dep是一个Map
    dep.delete(effect);
  });
  // 内部的dep上收集的effect已经都清空了 所以effect上的deps也没必要存在了  可以清空 节约空间
  effect.deps.length = 0;
}

难点分析

stop的难点在于什么时候收集依赖什么时候清除依赖和什么时候不应该收集依赖,通过设置全局的shouldTrack配置来控制依赖的相应操作,当调用effect中的run时,就会触发依赖收集,shouldTrack默认为true,当收集完依赖(即执行完fn并将返回值返回给外部runner后)将shouldTrack设置为false;当调用stop后,依赖缓存已清空,且onStop配置也已执行,run方法中依赖的active配置以变为false,后续的更新等操作不会再执行fn;此时已经清空完对应的依赖缓存,后续的更新操作不会引起fn缓存的执行,进而失去响应式,当再次调用runner时会进行相应fn的执行,返回实际的新值

effect -> readonly「代理拦截」 -> isReadonly「是否是readonly」 -> shallowReadonly「表层readonly」

功能概述

回顾reactive逻辑:reactive是返回了一个代理对象,通过proxy进行代理,通过proxy的第二个参数进行get和set配置,进而触发依赖收集和依赖触发操作;

readonly 概述

readonly的作用是将转入的对象变成不能set的只读对象,并且readonly返回的对象和传入的原对象是不相等的,该对象只能get不能set - 即不用再get的时候进行track了,同样的既然不用收集依赖了,也就没必要用到set中的trigger了;
但是如果传入的是一个对象的引用,当原始对象数据变化时,由于引用关系存在,所以readonly产生的对象也会随之变化,readonly只限制于新产生的对象是只读的代理; 为了进行readonly的set正确拦截,需要在进行set的时候进行相应的警告处理,暂时用console.warn()进行处理;

isReadonly 概述

isReadonly 用来检查对象是否由readonly创建的只读代理

shallowReadonly 概述

shallowReadonly 的作用是: 第一层是代理对象,内部不需要是代理对象,这个是不是和上面的嵌套代理对象相反;
表层Readonly 即最外层是readonly的 内层嵌套不是 -- 程序优化 防止全部都转化为响应式对象; readonly 内部还是通过proxy进行代理 - createActiveObject,所以数据还是响应式的

测试用例

import { isReadonly, readonly, isProxy } from "./reactive";

describe('readonly', () => {
    it('happy path readonly', () => {
        const origin = {foo:1, bar:{baz:[1]}}
        // 内部深层对象也会进行深度代理,遇到普通变量时不进行代理
        const wrapped = readonly(origin)
        expect(wrapped.foo).toBe(1)
        
        // 常规对象与readonly代理的对象不相等  但是会有引用关系
        expect(wrapped).not.toBe(origin)
        origin.foo = 2
        // 引用关系存在 wrapped的foo也会变化
        expect(wrapped.foo).toBe(2)
        expect(isReadonly(wrapped)).toBe(true)
        expect(isReadonly(origin)).toBe(false)  // 常规不是响应式的数据不进行代理
        expect(isReadonly(wrapped.bar)).toBe(true) // 深层对象也会进行深度代理
        expect(isReadonly(wrapped.bar.baz)).toBe(true) // 深层对象也会进行深度代理
        expect(isReadonly(wrapped.bar.baz[0])).toBe(false) // 基本类型不会代理
        expect(isReadonly(origin.bar.baz)).toBe(false) // 常规不是响应式的数据不进行代理
        expect(wrapped.foo).toBe(2)
        expect(isProxy(wrapped)).toBe(true)
        expect(wrapped.foo).toBe(2)
    });

    it('warn then call set', () => {
        // jest.fn() 会创建一个特殊的function 上面有一些属性 这些属性可以方便后续做断言来使用 - expect
        console.warn = jest.fn()
        const user = readonly({
            age: 12
        })

        user.age = 13
        expect(isReadonly(user)).toBe(true)
        expect(user.age).toBe(12) // 只读代理 自身不会进行改变

        expect(console.warn).toBeCalled()
    });
});

内部实现

readonly 实现

获取一个对象(响应式或纯对象)或ref并返回原始代理的只读代理,只读代理是深层次的,访问操作任何嵌套属性都是只读的;

// 简易版
export function readonly(raw) {
  return new Proxy(raw, {
      get(key,value) {
          let res = Reflect.get(target,key)
          //track()
          return res
      }
      set(target,key,value) {
          //trigger()
          console.warn(`key:${key} set 失败,因为 target是readonly`,target)
          return true
      }
  })
}
    
// 优化版
export function readonly(from){
    return createActiveObject(from,readonlyHandlers)
}
function createActiveObject(raw,baseHandlers){
    return new Proxy(raw,baseHandlers)
}
export const readonlyHandlers = {
    // get:createGetter(true),
    get:readonlyGet,
    // set时只进行warn展示 不做依赖处理
    set(target, key, value) {
        console.warn(`${key} set 失败, ${target} is readonly`)
        return true
    }
}

const readonlyGet = createGetter(true)

function createGetter(isReadonly = false){
    return function get(target,key){
        const result = Reflect.get(target,key)
        
        //兼顾 isReadonly 逻辑
        if(key === ReactiveFlags.IS_REACTIVE){
            return !isReadonly;
        } else if(key === ReactiveFlags.IS_READONLY){
            return isReadonly
        }
        // 当传入的目标值是一个对象的时候 需要进行递归处理内部的值 进行reactive转化
        if(isObject(result)){
            return isReadonly ? readonly(result) : reactive(result)
        }


        if(!isReadonly){
            // 需要进行依赖收集
            trck(target,key)
        }
        
    
        return result
    }
}
isReadonly 实现 - 只读

isReadonly 需要依赖get逻辑,通过调用value[ReactiveFlags.IS_READONLY]来实现触发get操作 进而判断是否是readonly的

export function isReadonly(value){
    // 触发get操作 在get逻辑中进行处理
    // !! 是为了解决value不是reactive的数据(普通对象) 不会触发get操作 返回undefined的问题
    // 当调用value[ReactiveFlags.IS_READONLY]的时候都会走到value的get逻辑中,当value不是一个响应式数据时,就没有get操作,进而没有返回值,返回了undefined 所以需要二次!!转化一下
    return !!value[ReactiveFlags.IS_READONLY]
}
shallowReadonly 实现 - 浅只读

shallowReadonly 创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值),也不会对ref传入的值进行拦截,因为操作ref的值时需要多一层.value的操作,而shallowReadonly只做第一层代理,所以拦截不到,如const user = shallowReadonly(ref({name:'Lbxin'}));obj.value.name = 'Lbxin11'
表层Readonly 即最外层是readonly的 内层嵌套不是 -- 程序优化 防止全部都转化为响应式对象;

测试用例
describe('shallowReadonly', () => {
    // 表层Readonly 即最外层是readonly的 内层嵌套不是  -- 程序优化 防止全部都转化为响应式对象
    test('should not make non-reactive properties reactive', () => {
        const props = shallowReadonly({n:{foo:1}})
        expect(isReadonly(props)).toBe(true) // 最外层是readonly
        expect(isReadonly(props.n)).toBe(false) // 非最外层不进行readonly代理
    });

    it('should call console.warn warn set', () => {
        console.warn = jest.fn();
        const user = shallowReadonly({
            age: 12,
            info: {
                name:'Lbxin'
            }
        })

        user.age = 13;
        user.info.name = 'Lbxin-11' // ✅ 非最外层不进行readonly代理
        user.info = {} // ❎ 最外层是readonly
        expect(user.info.name).toBe('Lbxin-11')
        expect(console.warn).toHaveBeenCalled()
    });
});
内部实现

//外部响应式  内部正常
export function shallowReadonly(from){
    return createActiveObject(from,shallowReadonlyHandlers)
}

// shallowReadonlyHandlers 和 readonlyHandlers 的内部实现是类似的  set的时候进行一致的相应的警告处理  get的时候调用单独的shallowReadonlyGet进行处理 所以利用extend进行处理
export const shallowReadonlyHandlers = extend({},readonlyHandlers,{
    get: shallowReadonlyGet
})

const shallowReadonlyGet = createGetter(true,true)

function createGetter(isReadonly = false, shallow = false){
    return function get(target,key){
        const result = Reflect.get(target,key)
        if(key === ReactiveFlags.IS_REACTIVE){
            return !isReadonly;
        } else if(key === ReactiveFlags.IS_READONLY){
            return isReadonly
        }
        
        if(shallow){
            return result
        }
        // 当传入的目标值是一个对象的时候 需要进行递归处理内部的值 进行reactive或readonly转化
        // reactive 和 readonly 嵌套对象转化功能
        if(isObject(result)){
            return isReadonly ? readonly(result) : reactive(result)
        }


        if(!isReadonly){
            // 需要进行依赖收集
            trck(target,key)
        }
        return result
    }
}

拓展

reactive -> isReactive

功能概述

isReactive 用来检查对象是否由reactive创建的响应式代理

测试用例
import { isReactive, reactive } from './reactive'
describe('reactive', () => {
    test('nested reactive', () => {
        const original = {
            nested: {
                foo: 1
            },
            array: [
                {
                    bar: 2
                }
            ]
        }
        const observed = reactive(original)
        expect(isReactive(observed.nested)).toBe(true)
        expect(isReactive(observed.nested.foo)).toBe(false)
        expect(isReactive(original)).toBe(false)
        expect(isReactive(original.nested)).toBe(false)
        expect(isReactive(original.nested.foo)).toBe(false)
        expect(isReactive(observed.array)).toBe(true)
        expect(isReactive(observed.array[0])).toBe(true)
    });
});
内部实现
export function isReactive(value){
    // 触发get操作 在get逻辑中进行处理
    // !! 是为了解决value不是reactive的数据(普通对象) 不会触发get操作 返回undefined的问题
    // 当调用value[ReactiveFlags.IS_REACTIVE]的时候都会走到value的get逻辑中,当value不是一个响应式数据时,就没有get操作,进而没有返回值,返回了undefined 所以需要二次!!转化一下
    return !!value[ReactiveFlags.IS_REACTIVE]
}
  • isReactive中调用!!value[ReactiveFlags.IS_REACTIVE]时会走到reactiveget属性中,所以需要在createGetter中进行ReactiveFlags.IS_REACTIVEReactiveFlags.IS_READONLY的判断拦截
function createGetter(isReadonly = false, shallow = false){
    return function get(target,key){
        const result = Reflect.get(target,key)
        if(key === ReactiveFlags.IS_REACTIVE){
            return !isReadonly;
        } else if(key === ReactiveFlags.IS_READONLY){
            return isReadonly
        }
        // 其他实现省略...
    }
}

isProxy

功能概述

检查一个对象是否是由reactivereadonlyshallowReactiveshallowReadonly创建的代理。

测试用例
  • reactive/shallowReactive创建
describe('reactive', () => {
    it('happy path', () => {
        const original = { foo: 1}
        const observed = reactive(original)
        expect(observed).not.toBe(original)
        expect(observed.foo).toBe(1)
        // 实现isReactive和isReadonly
        expect(isReactive(observed)).toBe(true)
        expect(isReactive(original)).toBe(false)
        
        
        expect(isProxy(observed)).toBe(true)
        expect(isProxy(observed.bar)).toBe(true)
        expect(isProxy(original)).toBe(false)
        expect(isProxy(original.bar)).toBe(false)
    });
}
  • readonly/shallowReadonly创建
describe('readonly', () => {
    it('happy path readonly', () => {
        const origin = {foo:1, bar:{baz:[1]}}
        // 内部深层对象也会进行深度代理,遇到普通变量时不进行代理
        const wrapped = readonly(origin)
        expect(wrapped.foo).toBe(1)
        expect(wrapped).not.toBe(origin)
        origin.foo = 2
        // 引用关系存在 wrapped的foo也会变化
        expect(wrapped.foo).toBe(2)
        expect(isReadonly(wrapped)).toBe(true)
        expect(isReadonly(origin)).toBe(false)
        expect(isReadonly(wrapped.bar)).toBe(true)
        expect(isReadonly(wrapped.bar.baz)).toBe(true)
        expect(isReadonly(wrapped.bar.baz[0])).toBe(false)
        expect(isReadonly(origin.bar.baz)).toBe(false)
        expect(wrapped.foo).toBe(2)
        
        expect(isProxy(wrapped)).toBe(true)
        expect(isProxy(wrapped.foo)).toBe(false)
        expect(isProxy(wrapped.bar)).toBe(true)
        expect(isProxy(origin)).toBe(false)
        
        expect(wrapped.foo).toBe(2)
    });
}
内部实现
export function isProxy(value){
    return isReactive(value) || isReadonly(value)
}

shallowReactive -> 取消深度监听 - 浅相应

适用场景

如果有一个对象数据, 结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。

功能概述

在Vue3中,无论是ref还是reactive创建的数据,默认都是深度监听代理的,在某些情况下不需要进行深度监听代理,提升性能,因此就需要用shallowReactive进行非深度监听的代理shallowReactive默认会监听代理数据的第一层,此时需要更改深层时就需要通过更改第一层来间接的更改其他层数据;

内部实现

基础版本

const reactiveHandler = {
  // 获取属性值
  get(target, prop) {
    const result = Reflect.get(target, prop);
    console.log("拦截了读取数据");
    return result;
  },
  // 修改属性值或者添加属性值
  set(target, prop, value) {
    const result = Reflect.set(target, prop, value);
    console.log("拦截了修改属性");
    return result;
  }
};
// shallowReactive 实现 只判断第一层  不会回调判断
// 定义一个shallowReactive函数,传入一个目标对象
function shallowReactive(target) {
  // 判断当前target是不是对象类型,如果传入的是基本数据类型则直接return出去
  if (target && typeof target === "object") {
    return new Proxy(target, reactiveHandler);
  }
  return target;
}

// reactive 实现 有回调判断
function reactive(target) {
  // 判断当前target是不是对象类型,如果传入的是基本数据类型则直接return出去
  if (target && typeof target === "object") {
    //运用递归实现深度的响应,即使用递归来判断子元素是否还是对象类型
    //先判断是不是数组
    if (Array.isArray(target)) {
      target.forEach((item, index) => {
        target[index] = reactive(item);
      });
    } else {
      // 在判断是不是对象(数组也属于对象,但是需要不同的处理方式)
      // 对象数据便利
      Object.keys(target).forEach((key) => {
        target[key] = reactive(target[key]);
      });
    }
    return new Proxy(target, reactiveHandler);
  }
  return target;
}

封装版本

function createGetter(isReadonly = false, shallow = false){
    return function get(target,key){
        const result = Reflect.get(target,key)
        if(key === ReactiveFlags.IS_REACTIVE){
            return !isReadonly;
        } else if(key === ReactiveFlags.IS_READONLY){
            return isReadonly
        }
        
        if(shallow){
            // 浅相应时不需要进行后续的深层次递归响应式处理 直接返回原始值 shallowReactive 
            return result
        }
        // 当传入的目标值是一个对象的时候 需要进行递归处理内部的值 进行reactive或readonly转化
        if(isObject(result)){
            return isReadonly ? readonly(result) : reactive(result)
        }


        if(!isReadonly){
            // 需要进行依赖收集
            trck(target,key)
        }
        
    
        return result
    }
}

const get = createGetter() //避免调用 mutableHandlers 和 readonlyHandlers 时重复调用 createGetter ,实现多次调用 只进行缓存读取
const set = createSetter() //避免调用 mutableHandlers 和 readonlyHandlers 时重复调用 createGetter ,实现多次调用 只进行缓存读取
const mutableHandlers = {
    // get: createGetter(),
    // set: createSetter(),
    get,
    set,
}
const shallowHandlers = {
    get: createGetter(false,true),
    set: createSetter(),
}
function reactive(obj){
    return new Proxy(raw,mutableHandlers)
}

function shallowReactive(obj){
    return new Proxy(raw,shallowHandlers)
}

推荐文献

Vue3的其他组和API - 实例解析