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

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

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

让我们一起看下vue3中计算属性是如何实现的?重点分析其缓存原理和嵌套effect模型

语法

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

javascript 复制代码
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误!

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

javascript 复制代码
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,下一章节详细介绍

javascript 复制代码
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 方法如下

javascript 复制代码
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

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

javascript 复制代码
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
javascript 复制代码
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
相关推荐
孤水寒月1 小时前
基于HTML的悬窗可拖动记事本
前端·css·html
祝余呀2 小时前
html初学者第一天
前端·html
脑袋大大的3 小时前
JavaScript 性能优化实战:减少 DOM 操作引发的重排与重绘
开发语言·javascript·性能优化
速易达网络4 小时前
RuoYi、Vue CLI 和 uni-app 结合构建跨端全家桶方案
javascript·vue.js·低代码
耶啵奶膘4 小时前
uniapp+firstUI——上传视频组件fui-upload-video
前端·javascript·uni-app
JoJo_Way4 小时前
LeetCode三数之和-js题解
javascript·算法·leetcode
视频砖家5 小时前
移动端Html5播放器按钮变小的问题解决方法
前端·javascript·viewport功能
lyj1689975 小时前
vue-i18n+vscode+vue 多语言使用
前端·vue.js·vscode
小白变怪兽6 小时前
一、react18+项目初始化(vite)
前端·react.js
ai小鬼头6 小时前
AIStarter如何快速部署Stable Diffusion?**新手也能轻松上手的AI绘图
前端·后端·github