• 【源码系列#03】Vue3计算属性原理(Computed)


    专栏分享:vue2源码专栏vue3源码专栏vue router源码专栏玩具项目专栏,硬核💪推荐🙌

    欢迎各位ITer关注点赞收藏🌸🌸🌸

    语法

    传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象

    const count = ref(1)
    const plusOne = computed(() => count.value + 1)
    
    console.log(plusOne.value) // 2
    
    plusOne.value++ // 错误!
    

    或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态

    const count = ref(1)
    const plusOne = computed({
      get: () => count.value + 1,
      set: (val) => {
        count.value = val - 1
      },
    })
    
    plusOne.value = 1
    console.log(count.value) // 0
    

    源码实现

    • @issue1 computed参数兼容只传getter方法和handler对象的情况

    • @issue2 缓存特性,只要依赖的变量值没有发生变化,就取缓存中的值

      _dirty作为缓存标识,如果依赖的变量值有变化,则将 _dirty 值置为 true,后续读取计算属性时,重新执行getter;否则直接取_value

    • @issue3 嵌套effect,firstname -> 计算属性fullName -> effect,下一章节详细介绍

    import { isFunction } from '@vue/shared'
    import { ReactiveEffect, trackEffects, triggerEffects } from './effect'
    
    /**
     * @issue1 computed参数兼容只传getter方法和handler对象
     * @issue2 缓存,只要依赖的变量值没有发生变化,就取缓存中的值
     * @issue3 嵌套effect,firname -> fullName -> effect
     */
    class ComputedRefImpl {
      public effect
      public _dirty = true // 默认应该取值的时候进行计算
      public _value
      public dep = new Set()
      public __v_isReadonly = true
      public __v_isRef = true
      constructor(public getter, public setter) {
        // 我们将用户的getter放到effect中,这里面firstname和lastname就会被这个effect收集起来
        this.effect = new ReactiveEffect(getter, () => {
          // 稍后依赖的属性firstname、lastname变化了,会执行此调度函数
          if (!this._dirty) {
            this._dirty = true
            // 实现一个触发更新 @issue3
            triggerEffects(this.dep)
          }
        })
      }
      
      // 类中的访问器属性 底层就是Object.defineProperty
      // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/get
      get value() {
        // 做依赖收集 @issue3
        trackEffects(this.dep)
        // @issue2
        if (this._dirty) {
          // 说明这个值是脏的
          this._dirty = false
          this._value = this.effect.run()
        }
        return this._value
      }
      
      set value(newValue) {
        this.setter(newValue)
      }
    }
    
    export const computed = getterOrOptions => {
      let onlyGetter = isFunction(getterOrOptions)
    
      let getter
      let setter
      // @issue1 
      if (onlyGetter) {
        getter = getterOrOptions
        setter = () => {
          console.warn('no set')
        }
      } else {
        getter = getterOrOptions.get
        setter = getterOrOptions.set
      }
      return new ComputedRefImpl(getter, setter)
    }
    
    

    trackEffects 和 triggerEffects 方法如下

    export function trackEffects(dep) { // 收集dep 对应的effect
      if (activeEffect) {
        let shouldTrack = !dep.has(activeEffect) // 去重了
        if (shouldTrack) {
          dep.add(activeEffect)
          // 存放的是属性对应的set
          activeEffect.deps.push(dep) // 让effect记录住对应的dep, 稍后清理的时候会用到
        }
      }
    }
    
    export function triggerEffects(effects) { 
      effects = new Set(effects);
      for (const effect of effects) {
        if (effect !== activeEffect) { // 如果effect不是当前正在运行的effect
          if (effect.scheduler) {
            effect.scheduler()
          } else {
            effect.run(); // 重新执行一遍
          }
        }
      }
    }
    

    嵌套 effect

    让我们分析一下这个测试用例

    const { effect, reactive, computed } = VueReactivity
    const state = reactive({ firname: '李', lastname: '柏成' })
    
    const fullName = computed(() => {
      // defineProperty中的getter
      return state.firstname + state.lastname
    })
    
    effect(() => {
      app.innerHTML = fullName.value
    })
    
    setTimeout(() => {
      state.firstname = '王'
    }, 1000)
    
    // 1. firstname要依赖于计算属性的effect
    // 2. 计算属性收集了外层effect
    // 3. 依赖的值变化了会触发计算属性effect重新执行, 计算属性重新执行的时候会触发外层effect来执行
    
    // computed 特点:缓存
    console.log('fullName.value', fullName.value)
    console.log('fullName.value', fullName.value)
    
    1. 当执行到 renderEffect 时,默认先执行一次 effect.run(),activeEffect --> renderEffect,并运行 this.fn() --> app.innerHTML = fullName.value
    effect(() => {
      app.innerHTML = fullName.value
    })
    
    1. 当访问 fullName.value 时,在 getter 方法中执行 trackEffects(this.dep),计算属性fullName 依赖收集 当前的 activeEffect(renderEffect)
    2. 当运行 this._value = this.effect.run() 时,activeEffect --> computedEffect,并运行 this.fn() ---> return state.firstname + state.lastname
    3. 访问了state.firstname,属性 firstname 依赖收集当前的 activeEffect(computedEffect)
    4. 访问了state.lastname,属性 lastname 依赖收集当前的 activeEffect(computedEffect)
    5. 一秒钟后,firstname 发生了变化。。。firstname变化触发更新 triggerEffects --> computedEffect.scheduler()
    6. 在计算属性 scheduler 中,触发更新 triggerEffects(this.dep) --> renderEffect.run() ,最终重新渲染页面 app.innerHTML = fullName.value

  • 相关阅读:
    java计算机毕业设计鞋店销售管理源程序+mysql+系统+lw文档+远程调试
    SA实战 ·《SpringCloud Alibaba实战》第03章-微服务介绍
    javascript字符串转对象
    C++11线程池
    小成代码路的错误
    记一次服务宕机、优化全流程(以后也可以装X了)
    关于ZooKeeper的一些面试题
    猿创征文|openGauss数据库从3.0.0升级到3.1.0操作实践
    【PyTorch】数据封装和模型保存
    cdh3.6.2集成flink1.12.0
  • 原文地址:https://www.cnblogs.com/burc/p/17881045.html