1 初步实现
1) 什么是计算属性
:::info **📝****计算属性**基于现有的状态再次加工得到一个新状态
当现有状态改变时, 新状态会重新计算
:::
使用演示
const fullname = computed(() => {
return state.firstname + state.lastname
})
console.log(fullname.value)
分析上面的示例不难看出
computed()函数的参数是一个副作用函数, 依赖firstname
和lastname
computed()函数跟注册副作用函数effect()类似. 接收一个副作用函数
做为参数
2) 基本实现
因此, 我们可以借助`effect`来实现`computed`示例
function computed(fn) {
// 这里先只考虑fn是函数的情况
const _effect = effect(fn)
return {
get value() {
return _effect.run()
},
}
}
这里, 我们需要给effect
添加返回值
effect用于注册副作用函数
, 返回包装后的实例对象ReactiveEffect
当访问计算属性fullname
的value时, 触发getter
操作. 返回副作用函数的执行结果
function effect(fn) {
if (typeof fn !== 'function') return
const _effect = new RectiveEffect(fn)
_effect.run()
return _effect
}
同时, 也要改进ReactiveEffect
的run
方法, 返回副作用函数的执行结果
class RectiveEffect {
constructor(fn) {
this.fn = fn
this.deps = []
}
run() {
activeEffect = this
cleanup(this)
const res = this.fn() // 修改
activeEffect = null
return res // 新增
}
}
测试用例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./reactive.js"></script>
<script>
const state = reactive({ firstname: 'xiao', lastname: 'ming' })
const fullname = computed(() => {
console.log('computed') // 在注册时, 执行1次
return state.firstname + state.lastname
})
console.log(fullname.value) // 访问时, 执行1次
</script>
</body>
</html>
2 lazy懒执行
1) 什么是懒执行
对计算属性而言, 默认不会执行计算方法(副作用函数). 只有访问其`value`属性时, 才会执行计算方法使用示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./vue.js"></script>
</head>
<body>
<script>
const { reactive, computed } = Vue
const state = reactive({ firstname: 'xiao', lastname: 'ming' })
const fullname = computed(() => {
console.log('默认不执行, 只有当访问fullName.value时执行')
return state.firstname + state.lastname
})
setTimeout(() => {
fullname.value
}, 1000)
</script>
</body>
</html>
2) 具体实现
在我们自己写的源码中, 每次在注册副作用函数时, 默认都会执行一次.解决方案
为了解决上述问题.
可以考虑给注册副作用函数effect
加入配置项, 扩展effect
的功能, 控制是否需要在注册时立该执行副作用函数
function effect(fn, options = {}) {
if (typeof fn !== 'function') return
const _effect = new RectiveEffect(fn)
if (!options.lazy) _effect.run() // 修改
return _effect
}
在计算属性中注册副作用函数时, 加入lazy:true
的配置项, 表明注册时不需要立即执行
function computed(fn) {
// 这里先只考虑fn是函数的情况
const _effect = effect(fn, {
lazy: true, // 修改
})
return {
get value() {
return _effect.run()
},
}
}
3 支持缓存
1) 什么是缓存
在第一次访问`.value`时, 会调用副作用函数计算一次, 并将结果缓存起来在后续访问.value
时, 不会调用副作用函数, 直接返回缓存的结果
使用演示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./vue.js"></script>
<script>
const { reactive, computed } = Vue
const state = reactive({ firstname: 'xiao', lastname: 'ming' })
const fullname = computed(() => {
console.log('computed')
return state.firstname + state.lastname
})
console.log(fullname.value) // 初次访问时, 执行1次, 保存到缓存
console.log(fullname.value) // 再次访问, 直接返回缓存中的数据
</script>
</body>
</html>
2) 具体实现
:::color1 **🤔****思考**如果每次访问计算属性, 都需要重新执行, 效率不高
- 第一次计算时, 将结果缓存起来
- 只有当参与计算的属性改变时, 才重新计算
- 其它情况返回缓存值
:::
思路
- 定义一个变量, 用于缓存
- 定义一个标识, 用于标识是否需要重新计算
// 这里先只考虑fn是函数的情况
function computed(fn) {
// 定义一个变量cache, 存放缓存结果
let cache
// 定义一个标识dirty
// - true: 重新计算
// - false: 直接返回缓存结果
let dirty = true
const _effect = effect(fn, {
lazy: true,
})
return {
get value() {
if (dirty) {
cache = _effect.run()
dirty = false
}
return cache
},
}
}
测试用例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./reactive.js"></script>
<script>
const state = reactive({ firstname: 'xiao', lastname: 'ming' })
const fullname = computed(() => {
console.log('computed') // 在注册时, 有lazy选项, 不会执行
return state.firstname + state.lastname
})
console.log(fullname.value) // 初次访问时, 执行1次, 保存到缓存
console.log(fullname.value) // 再次访问, 直接返回缓存中的数据
</script>
</body>
</html>
4 自定义更新(调度器)
1) 为什么要设计调度器
现在可以支持缓存了, 但是还有新的问题.当改变参与计算的属性后, 副作用函数重新执行, 但是dirty的值并没有改变. 依然还是会返回之前缓存的数据
问题示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./reactive.js"></script>
<script>
const state = reactive({ firstname: 'xiao', lastname: 'ming' })
const fullname = computed(() => {
console.log('computed') // 在注册时, 有lazy选项, 不会执行
return state.firstname + state.lastname
})
console.log(fullname.value) // 初次访问时, 执行1次, 保存到缓存
console.log(fullname.value) // 再次访问, 直接返回缓存中的数据
setTimeout(() => {
state.lastname = 'pang'
console.log(fullname.value) // xiaoming
}, 1000)
</script>
</body>
</html>
为了解决此问题, 我们需要自定义更新过程
2) 什么是调度器
在注册副作用函数, 传入 自定义更新函数(调度器)作用
将注册过程和更新过程分开
这样做不仅仅在实现computed
时可以用. 在后面实现watch
时, 也可以复用~
3) 具体实现
```javascript function computed(fn) { let cachelet dirty = true
const _effect = effect(fn, {
lazy: true,
scheduler() {
dirty = true // 新增
},
})
return {
get value() {
if (dirty) {
cache = _effect.run()
dirty = false
}
return cache
},
}
}
改造effect函数, 在options选项中加入`scheduler`配置项
```javascript
function effect(fn, options = {}) {
if (typeof fn !== 'function') return
let _effect
if (options && options.scheduler) {
_effect = new RectiveEffect(fn, options.scheduler)
} else {
_effect = new RectiveEffect(fn)
}
if (!options.lazy) _effect.run()
return _effect
}
改造ReactiveEffect
类
class RectiveEffect {
constructor(fn, scheduler) {
this.fn = fn
this.deps = []
this.scheduler = scheduler
}
run() {
activeEffect = this
cleanup(this)
const res = this.fn()
activeEffect = null
return res
}
}
修改更新trigger的过程
function trigger(target, key) {
let depMap = bucket.get(target)
if (!depMap) return
let depSet = depMap.get(key)
if (depSet) {
const effects = [...depSet]
effects.forEach(effect => {
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
})
}
}
测试用例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./reactive.js"></script>
<script>
const state = reactive({ firstname: 'xiao', lastname: 'ming' })
const fullname = computed(() => {
console.log('computed') // 在注册时, 有lazy选项, 不会执行
return state.firstname + state.lastname
})
console.log(fullname.value) // 初次访问时, 执行1次, 保存到缓存
console.log(fullname.value) // 再次访问, 直接返回缓存中的数据
setTimeout(() => {
state.lastname = 'pang'
console.log(fullname.value) // xiaoming
}, 1000)
</script>
</body>
</html>
5 渲染计算属性的结果
:::color1 **🤔****思考**如果在一个副作用函数依赖于计算属性的结果
理论上,
当计算属性依赖的状态改变时, 计算属性会重新计算
计算属性改变, 依赖计算属性的副作用函数也需要重新执行
:::
问题示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactive.js"></script>
<script>
const state = reactive({ firstname: 'xiao', lastname: 'ming' })
const fullname = computed(() => {
return state.firstname + state.lastname
})
effect(function effectFn() {
app.innerHTML = fullname.value
})
setTimeout(() => {
state.lastname = 'pang'
}, 1000)
</script>
</body>
</html>
当state.lastname
改变时, 会触发计算属性fullname
重新计算
fullname
的值改变后, effectFn也应该被重新执行. 而实际上并没有
为了解决这个问题. 首先我们要支持嵌套的effect
上述代码可以简化为
effect(function effectFn() {
app.innerHTML = fullname.value
effect(() => {
return state.firstname + state.lastname
})
})
1) 支持嵌套的effect
```javascript class RectiveEffect { constructor(fn, scheduler) { this.fn = fn this.deps = [] this.scheduler = scheduler this.parent = null // 新增 } run() { this.parent = activeEffect // 新增 activeEffect = this cleanup(this)const res = this.fn()
activeEffect = this.parent // 修改
return res
}
}
<h3 id="h5azy">2) 计算属性收集依赖</h3>
```javascript
function computed(fn) {
let cache
let dirty = true
const _effect = effect(fn, {
lazy: true,
scheduler() {
dirty = true
// 触发当前计算属性依赖的副作用函数执行
trigger(obj, 'value')
},
})
let obj = {
get value() {
// 收集当前计算属性依赖的副作用函数
track(obj, 'value')
if (dirty) {
cache = _effect.run()
dirty = false
}
return cache
},
}
return obj
}
6 优化
1) 封装
由于`computed`函数最终会返回一个对象.可以考虑将返回的对象封装成ComputedRefImpl
的实例
class ComputedRefImpl {
constructor(fn) {
this._value = null // 缓存
this._dirty = true // 标识
this.effect = new RectiveEffect(fn, () => {
if (!this._dirty) this._dirty = true
trigger(this, 'value')
})
}
get value() {
// 收集当前计算属性依赖的副作用函数
track(obj, 'value')
if (this._dirty) {
this._value = this.effect.run()
this._dirty = false
}
return this._value
}
}
// 这里先只考虑fn是函数的情况
function computed(fn) {
return new ComputedRefImpl(fn)
}
2) 扩展
考虑到计算属性是可以支持两种配置的. 进一步扩展支持配置(getter/setter)class ComputedRefImpl {
constructor(getter, setter) {
this._value = null // 缓存
this._dirty = true // 标识
this.effect = new RectiveEffect(getter, () => {
if (!this._dirty) this._dirty = true
trigger(this, 'value')
})
this.setter = setter
}
get value() {
track(this, 'value')
if (this._dirty) {
this._value = this.effect.run()
this._dirty = false
}
return this._value
}
set value(newVal) {
return this.setter(newVal)
}
}
function computed(getterOrOptions) {
let getter
let setter
if (typeof getterOrOptions == 'function') {
getter = getterOrOptions
setter = () => {
console.warn('no setter')
}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(getter, setter)
}
3) 解耦复用
在收集依赖时, 不能很好的观察到当前计算属性依赖的副作用函数.可以考虑给ComputedRefImpl
加一个dep
属性, 用来保存哪些副作用函数引用了当前计算属性
而且, 在调用track
和trigger
时, 必须要传入两个参数. 这里我们人为创造了一个value
. 这种方式不够优雅
考虑对track
和trigger
进一步解耦
class ComputedRefImpl {
constructor(getter, setter) {
this._value = null // 缓存
this._dirty = true // 标识
this.dep = new Set()
this.effect = new RectiveEffect(getter, () => {
if (!this._dirty) this._dirty = true
triggerEffects(this.dep)
})
this.setter = setter
}
get value() {
trackEffects(this.dep)
if (this._dirty) {
this._value = this.effect.run()
this._dirty = false
}
return this._value
}
set value(newVal) {
return this.setter(newVal)
}
}
实现trackEffects
function trackEffects(dep) {
// 只有当activeEffect有值时, 才需要收集依赖
if (!activeEffect) return
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
实现triggerEffects
function triggerEffects(dep) {
if (!dep) return
const effects = [...dep]
effects.forEach(effect => {
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
})
}