• 从effect理解Vue3的响应式原理


    effect 会立即执行传入的函数,并在函数的依赖发生变化时重新运行该函数(后文统一称为依赖函数)

    列表查询是业务中最常见的需求,通常是默认查询一次,当页码、分页大小、关键词发生变化时重新查询,一般我们是在onMounted中触发一次查询,分页,关键词变化时手动触发下查询,如果用efftect,代码会精简很多。

    import { effect, reactive } from 'vue'
    const state = reactive({
      pageIndex: 1,
      pageSize: 20
    })
    effect(() => {
      let params = {
        pageNum: state.pageIndex,
        pageSize: state.pageSize
      }
       request({
        method: 'post',
        url: 'xxx',
        params
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    众所周知vue3是用了Proxy把对象进行了代理,在get中进来依赖搜集,在set中触发依赖函数的执行,具体是怎么操作的呢,我们来看下源码。

    后文的源码中会去掉处理边界条件的部分,这部分不影响对源码的理解。

    先从reactive来看vue是怎么把普通对象变成响应式对象的

    • 代码在 packages/reactivity/src/reactive.ts
    // 用createReactiveObject 函数创建响应式,对象类型和集合类型分开处理,收集的依赖放在reactiveMap中
    export function reactive(target: object) {
      return createReactiveObject(
        target, // 代理的对象
        false, // 是否是只读
        mutableHandlers, // 为对象创建响应式
        mutableCollectionHandlers, // 为集合类型创建响应式
        reactiveMap // 保存对象的代理结果
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    先看下reactiveMap

    reactiveMap 是一个WeakMap,WeakMap 可以把对象作为key,value可以是任何类型,用来判断当前对象是否代理过。

    export const reactiveMap = new WeakMap<Target, any>()
    
    • 1

    看下mutableHandlers

    进入createReactiveObject函数中

    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>,
      proxyMap: WeakMap<Target, any>
    ) {
      // 如果已经代理过就直接返回
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      // 省去边界条件处理后,这个函数就是 new Proxy, 然后再把 proxy和target放在reactiveMap中
      // reactiveMap 就是 这个函数里的proxyMap
      const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers // 判断类型
      )
      proxyMap.set(target, proxy)
      return proxy
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    关键还是baseHandlers

    • packages/reactivity/src/baseHandlers.ts

    进入baseHandlers文件中,Proxy的 get 和 set 是通过createGetter 和 createSetter 这两个函数生成的

    先看下createGetter
    // 依赖收集完成后 返回值
    function createGetter(isReadonly = false, shallow = false) {
      return function get(target: Target, key: string | symbol, receiver: object) {
    
        const targetIsArray = isArray(target)
    		// 在用了Proxy后,数组和对象可以一起处理了
        if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
          return Reflect.get(arrayInstrumentations, key, receiver)
        }
    
        const res = Reflect.get(target, key, receiver)
        if (!isReadonly) {
          // 这个track就是进行依赖收集的函数
          track(target, TrackOpTypes.GET, key)
        }
        return res
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    继续分析track
    • track代码在:packages/reactivity/src/effect.ts
    依赖是放在哪里的

    targetMap 是依赖存放的对象,大致是 对象——对象key——key对应的依赖这样的结构

    const targetMap = new WeakMap<any, KeyToDepMap>()
    
    // eg
    const state = reactive({
      age: 18,
      name: '小明'
    })
    // 对state这个响应式对象来说,大致的依赖结构如下
    // 在后文中会继续分析每一层具体的类型
    {
      state: {
        age: [依赖1, 依赖2,依赖3],
        name: [依赖1, 依赖2,依赖3]
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    知道依赖怎么存放后,再看具体的存的过程
    export let activeEffect: ReactiveEffect | undefined
    export let shouldTrack = true
    
    export function track(target: object, type: TrackOpTypes, key: unknown) {
      // 先忽略shouldTrack,activeEffect这两个变量
      // 直接看if里面的代码,就是存放依赖的过程
      if (shouldTrack && activeEffect) {
        // 1、先判断对象有没有进行过依赖收集
        let depsMap = targetMap.get(target)
        if (!depsMap) {
          // targetMap的key是对象,值是Map类型的
          targetMap.set(target, (depsMap = new Map()))
        }
        // 2、再判断对象的key有没有进行过依赖收集
        // dep 就是依赖最终存放的地方
        let dep = depsMap.get(key)
        if (!dep) {
          // createDep() 返回的是一个 Set 可以看到依赖最终是放在一个Set中
          depsMap.set(key, (dep = createDep()))
        }
    		// 到这里用于存放依赖的depsMap创建好了,其结构是
        // WeakMap - Map - Set
        
        // trackEffects 就是将依赖放在dep这个set中
        trackEffects(dep)
      }
    }
    
    // 可以看出activeEffect最终是add到dep中,可以猜测activeEffect就是依赖
    export function trackEffects(dep: Dep) {
      let shouldTrack = false
      // 判断是否存过依赖
      shouldTrack = !dep.has(activeEffect)
    
      if (shouldTrack) {
        dep.add(activeEffect)
        activeEffect!.deps.push(dep)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    activeEffect是怎么变成依赖的shouldTrack什么时候会变成true,通过看effect的源码来理解

    • packages/reactivity/src/effect.ts

    可以先回到文章开头看下effect的用法

    export function effect<T = any>(
      fn: () => T, // fn就是文章开头那段执行接口查询的代码
      options?: ReactiveEffectOptions // effect可以传入第二个参数,第二个参数中有lazy时可以手动控制依赖函数调用的时机
    ): ReactiveEffectRunner {
      // ReactiveEffect 就是跟activeEffect, shouldTrack密切相关的类
      const _effect = new ReactiveEffect(fn)
      
      // 这里调用了run方法,往下看ReactiveEffect就可以理解run是什么了
      if (!options || !options.lazy) {
        _effect.run()
      }
      const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
      runner.effect = _effect
      return runner
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    export class ReactiveEffect<T = any> {
      deps: Dep[] = []
    	constructor(
        public fn: () => T, // fn是依赖函数
        public scheduler: EffectScheduler | null = null,
        scope?: EffectScope
      ) {
        // 可以先忽略
        recordEffectScope(this, scope)
      }
      // 下面是关键, shouldTrack和activeEffect都登场了
      run() {
        let lastShouldTrack = shouldTrack
        try {
          // 原来activeEffect就是ReactiveEffect
          activeEffect = this
          shouldTrack = true
          // 在effect中会默认执行run函数,当执行run函数时,就会执行fn依赖函数
          // 执行依赖函数时,就会触发对象的get,get触发后就会执行track
          // track 里面的 activeEffect 就是这里的 this
          // ReactiveEffect 就是依赖,执行依赖的run就是执行依赖函数
          // 可以想像出set过程就是执行dep里面所有ReactiveEffect的run方法的过程
          return this.fn()
        } finally {
          shouldTrack = lastShouldTrack
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    至此依赖搜集的过程就分析完了,从effect函数为入口总结下:

    • effect中new ReactiveEffect() 得到run方法,effect中默认执行run方法,触发响应式对象的get,进行依赖收集
    • effect中默认执行的过程就是依赖收集的过程,依赖就是ReactiveEffect实例

    在ReactiveEffect中有段很精彩的位运算代码,可以单独写一篇文章分析。

    再看set

    function createSetter(shallow = false) {
      return function set(
        target: object,
        key: string | symbol,
        value: unknown,
        receiver: object
      ): boolean {
        let oldValue = (target as any)[key]
        const result = Reflect.set(target, key, value, receiver)
        // trigger 值改变时触发依赖函数
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
        return result
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown,
      oldTarget?: Map<unknown, unknown> | Set<unknown>
    ) {
      // 从depsMap中获取依赖
      let deps: (Dep | undefined)[]
    	deps.push(depsMap.get(key))
      // 执行依赖
      triggerEffects(deps[0])
    }
    
    export function triggerEffects(
      dep: Dep | ReactiveEffect[],
    ) {
      const effects = isArray(dep) ? dep : [...dep]
      // computed 计算属性也是利用effect实现的,ReactiveEffect中还有个computed
      
      // 下面两个for循环,先触发计算属性的依赖,再触发非计算属性的依赖
      // 可以看到计算属性的依赖函数是优先触发的
      for (const effect of effects) {
        if (effect.computed) {
          triggerEffect(effect)
        }
      }
      for (const effect of effects) {
        if (!effect.computed) {
          triggerEffect(effect)
        }
      }
    }
    
    function triggerEffect( effect: ReactiveEffect) {
      // 在get中就猜测,触发依赖就是执行ReactiveEffect的run方法
      effect.run()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    至此vue3的响应式原理分析完毕,从API使用的作为入口看vue3的源码是一个不错的方式。

  • 相关阅读:
    【精通内核】计算机程序的本质、内存组成与ELF格式深度解析
    传奇列表获取失败与登录器太老怎么解决
    p5.js map映射
    Gin 文件上传操作(单/多文件操作)
    W5500模块PHY连接问题
    阿里云服务器u1和e实例有什么区别?哪个比较好?
    TeeChart Pro for .NET 2022.10.24 Crack
    JWT开发详解
    RocketMQ研究
    神级开源库收藏
  • 原文地址:https://blog.csdn.net/qq_24134853/article/details/126102395