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()
}
}
})
}