在注册副作用函数时,可以指定一些选项参数 options 来扩展响应式数据的功能,如计算属性,即懒执行的effect。
现在我们所实现的 effect 函数会立即执行传递给它的副作用函数
// 这个函数会立即执行
effect(()=>{
console.log(obj.foo)
})
// 这个函数会立即执行
effect(()=>{
console.log(obj.foo)
})
懒执行effect
但在有些场景下,我们不希望它立即执行,而是希望在需要的时候才执行,例如计算属性。这时我们可以通过在 options 中添加 lazy 属性来达到目的。
effect(()=>{
console.log(obj.foo)
}, { lazy: true })
effect(()=>{
console.log(obj.foo)
}, { lazy: true })
有了它,我们就可以修改 effect 函数的实现逻辑了,当 options.lazy 为 true 时,则不立即执行副作用函数:
function effect(fn, options: {}){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(activeEffect)
fn()
effectStack.pop()
activeEffect = effectStack.at(-1)
}
effectFn.options = options
effectFn.deps = []
if(!options.lazy){
effectFn()
}
return effectFn
}
function effect(fn, options: {}){
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(activeEffect)
fn()
effectStack.pop()
activeEffect = effectStack.at(-1)
}
effectFn.options = options
effectFn.deps = []
if(!options.lazy){
effectFn()
}
return effectFn
}
通过这个判断,我们就实现了让副作用函数不立即执行的功能。但问题是,副作用函数应该什么时候执行呢?通过上面的代码可以看到,我们将副作用函数 effectFn 作为 effect 函数的返回值,这就意味着当调用 effect 函数时,通过其返回值能够拿到对应的副作用函数,这样我们就能手动执行该副作用函数了:
const effectFn = effect(() => {
console.log(obj.foo)
}, { lazy: true })
// 手动执行副作用函数
effectFn()
const effectFn = effect(() => {
console.log(obj.foo)
}, { lazy: true })
// 手动执行副作用函数
effectFn()
初期计算属性
如果仅仅能够手动执行副作用函数,其意义并不大。但如果我们把传递给 effect 的函数看作一个 getter,那么这个 getter 函数可以返回任何值,例如:
const effectFn = effect(
// getter 返回 obj.foo 与 obj.bar 的和
() => obj.foo + obj.bar,
{ lazy: true }
)
const effectFn = effect(
// getter 返回 obj.foo 与 obj.bar 的和
() => obj.foo + obj.bar,
{ lazy: true }
)
这样我们在手动执行副作用函数时,就能够拿到其返回值:
const effectFn = effect(
// getter 返回 obj.foo 与 obj.bar 的和
() => obj.foo + obj.bar,
{ lazy: true }
)
// value 是 getter 的返回值
const value = effectFn()
const effectFn = effect(
// getter 返回 obj.foo 与 obj.bar 的和
() => obj.foo + obj.bar,
{ lazy: true }
)
// value 是 getter 的返回值
const value = effectFn()
为了实现这个目标,我们需要再对 effect 函数做一些修改,如以下代码所示:
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
// 将 fn 的执行结果存储到 res 中
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将 res 作为 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
// 将 fn 的执行结果存储到 res 中
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将 res 作为 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
通过新增的代码可以看到,传递给 effect 函数的参数 fn 才是真正的副作用函数,而 effectFn 是我们包装后的副作用函数。为了通过 effectFn 得到真正的副作用函数 fn 的执行结果,我们需要将其保存到 res 变量中,然后将其作为 effectFn 函数的返回值。现在我们已经能够实现懒执行的副作用函数,并且能够拿到副作用函数的执行结果了,接下来就可以实现计算属性了,如下所示:
computed 基本实现
function computed(getter){
// 把 getter 作为副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, { lazy: true })
const obj = {
// 当读取 value 时才执行 effectFn
get value(){
return effectFn()
}
}
return obj
}
function computed(getter){
// 把 getter 作为副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, { lazy: true })
const obj = {
// 当读取 value 时才执行 effectFn
get value(){
return effectFn()
}
}
return obj
}
我们可以使用 computed 函数来创建一个计算属性:
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, { /* ... */ })
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
可以看到它能够正确地工作。不过现在我们实现的计算属性只做到了懒计算,也就是说,只有当你真正读取 sumRes.value 的值时,它才会进行计算并得到值。但是还做不到对值进行缓存,即假如我们多次访问 sumRes.value 的值,会导致 effectFn 进行多次计算,即使 obj.foo 和 obj.bar 的值本身并没有变化:
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
computed 缓存
上面的代码多次访问 sumRes.value 的值,每次访问都会调用 effectFn 重新计算。为了解决这个问题,就需要我们在实现 computed 函数时,添加对值进行缓存的功能,如以下代码所示:
function computed(getter){
// value 用来缓存上一次计算的值
let value
// dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true
})
const obj = {
get value(){
// 只有“脏”时才计算值,并将得到的值缓存到 value 中
if(dirty){
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
function computed(getter){
// value 用来缓存上一次计算的值
let value
// dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true
})
const obj = {
get value(){
// 只有“脏”时才计算值,并将得到的值缓存到 value 中
if(dirty){
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
<script setup>
const data = { bar: 1, foo: 2 }
const bucket = new WeakMap()
let activeEffect
const effectStack = []
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
return true
}
})
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target, key) {
let depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(fn => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
effectsToRun.forEach(fn => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (fn.options.scheduler) { // 新增
fn.options.scheduler(fn) // 新增
} else {
// 否则直接执行副作用函数(之前的默认行为)
fn() // 新增
}
})
}
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(activeEffect)
// 将 fn 的执行结果存储到 res 中
const res = fn()
effectStack.pop()
activeEffect = effectStack.at(-1)
// 将 res 作为 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
function computed(getter) {
// value 用来缓存上一次计算的值
let value
// dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true
})
const obj = {
get value() {
// 只有“脏”时才计算值,并将得到的值缓存到 value 中
if (dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)
</script>
<template></template>
<style scoped lang='scss'></style>
<script setup>
const data = { bar: 1, foo: 2 }
const bucket = new WeakMap()
let activeEffect
const effectStack = []
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
return true
}
})
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target, key) {
let depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(fn => {
if (fn !== activeEffect) {
effectsToRun.add(fn)
}
})
effectsToRun.forEach(fn => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (fn.options.scheduler) { // 新增
fn.options.scheduler(fn) // 新增
} else {
// 否则直接执行副作用函数(之前的默认行为)
fn() // 新增
}
})
}
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(activeEffect)
// 将 fn 的执行结果存储到 res 中
const res = fn()
effectStack.pop()
activeEffect = effectStack.at(-1)
// 将 res 作为 effectFn 的返回值
return res
}
effectFn.options = options
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
function computed(getter) {
// value 用来缓存上一次计算的值
let value
// dirty 标志,用来标识是否需要重新计算值,为 true 表示“脏”,需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true
})
const obj = {
get value() {
// 只有“脏”时才计算值,并将得到的值缓存到 value 中
if (dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value)
</script>
<template></template>
<style scoped lang='scss'></style>