4.vue3源码-computed

177 阅读3分钟

1.介绍

本内容是为了方便自己以后查阅同时让基础比较薄弱的人也能看懂vue的源码,所以采用的方式比较啰嗦,勿喷。不喜欢直接划走即可!!!

这里的代码和vue的源码不是完全一样的。但是核心的代码以及思路是一样的。当你学会当前的代码再去阅读源码会容易很多

这里为了编写文档时方便 且方便理解 我将所有的内容全部写到一个文件中了,后续会在main分支将所有代码抽离

computed 会创建一个ComputedRefImpl实例 该实例的核心也是依赖响应式的effect 当我们调用实例的value会触发get进而收集依赖

不过在此之前你应该学会使用computed

2.开始编写

2.1.核心流程

首先我们要清楚computed只接收一个函数或者带有set/get的对象,computed 会创建一个ComputedRefImpl实例,当我们调用实例的value会触发get进而收集依赖 ,基于这段话 我们来编写核心的流程

// computed接收函数或者带set/get的obj
export function computed(getterOrOptions) {
  let setter
  let getter
  
  // 判断传入的是否是函数
  const isGetter = isFunction(getterOrOptions)
​
  if (isGetter) { // 如果是函数 那么当前的函数就是getter 则没有setter
    getter = getterOrOptions
    setter = function () {
      console.warn('you maybe need a setter')
    }
  } else { // 如果是options那么就分别赋值setter和getter
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  // 创建ComputedRefImpl实例 然后return
  return new ComputedRefImpl(getter, setter)
}
​
// ComputedRefImpl核心就是创建一个响应式的effect
// 当调用value 就会调用effect.run 进而收集依赖
class ComputedRefImpl {
  public effect
​
  constructor(getter, public setter) {
    this.effect = new ReactiveEffect(getter)
  }
​
  get value() {
    return this.effect.run()
  }
​
  set value(value) {
    this.setter(value)
  }
}

2.2.缓存

上面我们已经完成了computed的核心流程,但是并没有实现缓存,现在调用多少次computed返回实例的value就会执行多少次,那么我们可以将第一次获取的值存起来,然后用一个标识符dirty来表示是否已经缓存过了 当设置新值的时候再次修改这个标识dirty的状态进而重新获取新值,重新收集依赖,这里不用担心如果修改的值相同也会触发的问题,因为在set方法的时候我们做过判断

import { isFunction } from '@vue/shared'
import { ReactiveEffect } from './effect'class ComputedRefImpl {
  public effect
  public _value
  public dirty = true
​
  constructor(getter, public setter) {
    // 当修改值 调用调度函数 修改dirty的状态 
    this.effect = new ReactiveEffect(getter, () => {
      if (!this.dirty) this.dirty = true
    })
  }
​
  get value() {
    // 只有dirty是true 表示没有缓存
    // 需要缓存新值
    if (this.dirty) {
      this.dirty = false
      this._value = this.effect.run()
    }
    return this._value
  }
​
  set value(value) {
    this.setter(value)
  }
}
​
​

2.3.依赖数据发生变化无法更新

看下面的情况

// 情况一
const name = computed({
  get() {
    const r = obj.firstName + obj.lastName
    console.log(r)
    return r
  },
  set(value) {
    console.log(value)
  }
})
​
effect(() => {
  console.log(name)
  app.innerHTML = name.value
​
})
​
setTimeout(() => {
  obj.lastName = '四'
}, 2000)
​
/*
上述代码  当更改了响应式对象obj的lastName 并不会触发effect函数 也就没法重新渲染
原因是我们没有去收集effect,那么当数据变化就不会重新执行effect
解决:当获取value的时候我们去收集这个effect  等到依赖的数据变化了那么我们就触发effect*/

解决

这里为了书写文档方便 将代码的抽离都放到了一起 仓库代码中将抽离的代码放到了effect.ts中

// 当触发 get时我们只需要收集依赖。等到依赖的响应式数据跟新时会触发调度函数 然后我们执行依赖即可
import activeEffect from './active/effect.ts'class ComputedRefImpl {
  public effect
  public _value
  public dirty = true
  // 解决情况一
  public dep = new Set()
​
  constructor(getter, public setter) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this.dirty) this.dirty = true
      // 解决情况一
      triggerEffects(this.dep)
    })
  }
​
  get value() {
    // 解决情况一
    trackEffects(this.dep)
    if (this.dirty) {
      this.dirty = false
      this._value = this.effect.run()
    }
    return this._value
  }
​
  set value(value) {
    this.setter(value)
  }
}
​
​
export function trackEffects(dep) {
  // 虽然set是不会重复的 但是内部肯定会做逻辑的 那么如果我们判断一下在加入会提升性能
  let shouldTrack = !dep.has(activeEffect)
  if (shouldTrack) {
    dep.add(activeEffect)
    // 解决情况四
    activeEffect.deps.push(dep)
  }
}
​
​
export function triggerEffects(dep) {
  const effects = [...dep]
​
  effects &&
    effects.forEach((effect) => {
      // 做个判断 为了防止情况三 的发生
      if (effect !== activeEffect) {
        // 解决情况六
        if (effect.scheduler) {
          effect.scheduler()
        } else {
          effect.run()
        }
      }
    })
}

3.仓库地址

github.com/wscymdb/vue…