目前的副作用函数effect
是立即执行的:
effect(() => {
console.log(obj.foo)
})
在某些场景下并不希望effect
立即执行, 因此就可以添加options
添加属性:
effect(() => {
console.log(obj.foo)
},
// options
{
lazy: true
})
这里的lazy
就是前面文章介绍的调度, 当options.lazy
为true
时不立即执行副作用函数:
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.options = options
options.deps = []
// 只有非懒加载时才执行副作用函数
if (!options.lazy) effectFn()
// 将副作用函数作为返回值返回
return effectFn
}
由于最后一行将副作用函数暴露在了函数外部因此可以手动执行改副作用函数:
const effectFn = effect(() => {
console.log(obj.foo)
},
// options
{
lazy: true
})
// 手动执行
effectFn()
仅仅是这样的意义其实不太大, 但手动执行后如果可以拿到传入effect
函数(fn
)的返回值要好的多:
const effectFn = effect(() => obj.foo + obj.bar,
// options
{
lazy: true
})
// val 是 传函数的返回值
const val = 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
}
其实传递给effect
的fn
才是真正的副作用函数, 而effectFn
是对fn
的再包装, 也正因此effectFn
执行后也应该返回fn
得出来的值也就新增了const res = fn()
与return res
说句题外话, 根据前一段时间发的文章<闭包浅谈>, 在这里面新增的res
变量是闭包哦,fn
也正好是回调函数, 当然effectFn
也是闭包
现在实现了懒执行的副作用函数并且可以拿到副作用函数的执行结果, 可实现计算属性了:
function computed (getter) {
// 将 getter 作为副作用函数
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)
到目前完整的代码是:
// 储存副作用函数的桶
const bucket = new WeakMap()
// 用于储存被注册的副作用的函数
let activeEffect = undefined
// 副作用函数栈
const effectStack = []
function cleanup (effectFn) {
for (let itme of effectFn.deps) {
itme.delete(effectFn)
}
effectFn.deps.length = []
}
function effect (fn, options = {}) {
const effectFn = () => {
console.log('effect');
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
}
// const data = {
// text: 'hello world',
// ok: true
// }
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {
// 拦截读取操作
get (target, key) {
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set (target, key, newVal) {
// 设置属性值
target[key] = newVal
trigger(target, key)
}
})
function track (target, key) {
// 没有 activeEffect, 直接 return
if (!activeEffect) return target[key]
// 根据 target 从'桶'中回去 depsMap, 它也是一个 Map 类型: key ---> effects
let depsMap = bucket.get(target)
// 如果 depsMap 不存在, 则新建一个 Map 并与 target 关联
if (!depsMap) bucket.set(target, (depsMap = new Map()))
// 再根据 key 从depsMap 中去的 deps, 它是一个 Set 类型
// 里面存贮所有与当前 key 相关的副作用函数: effects
let deps = depsMap.get(key)
// 如果 deps 不存在, 同样新建一个 Set 并与 key 关联0
if (!deps) depsMap.set(key, (deps = new Set()))
// 最后将当前激活的副作用函数添加到'桶'里
deps.add(activeEffect)
}
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的函数相同则不触发执行
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果一个副作用函数有调度器则调用改调度器, 并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否则直接执行副作用函数
effectFn()
}
})
}
function computed (getter) {
// 将 getter 作为副作用函数
const effectFn = effect(getter, { lazy: true })
const obj = {
// 当读取 value 时才执行 effectFn
get value () {
return effectFn()
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
console.log(sumRes.value) // 3
在函数effectFn
中打印了字符串'effect'会发现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 () {
if (dirty) {
value = effectFn()
// 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
dirty = false
}
return value
}
}
return obj
}
此时确实只会计算一次, 且每次访问不会重新执行副作用函数, 但是相信聪明如你已经发现问题了, 如果我们改变obj
中的值后在访问sumRes.value
会发现访问的值没有变化, 这里就不做演示了. 解决方法就是当其中的某一个值放生改变时将dirty
重新设为true
就可以了, 这时我们可以添加调度器(请参看: 执行调度):
function computed(getter) {
// value 用来缓存上一次计算的值
let value
// dirty 标志, 用来标识是否需要重新计算, 为 true 标识需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 添加调度器, 将 dirty 重置
scheduler () {
dirty = true
}
})
const obj = {
get value () {
if (dirty) {
value = effectFn()
// 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
dirty = false
}
return value
}
}
return obj
}
现在基本完美了, 只是在某些情况下出现一个缺陷:
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
// 在副作用函数中读取计算属性
console.log(sumRes.value) // 3
})
obj.bar++
在obj.bar++
时期望是触发计算属性, 重新渲染, 但实际上并没有, 原因是因为计算属性是有自己的effect
并且是懒执行的, 只有真正在读取计算属性的值才会执行. 对于计算属性的getter
函数, 它里面访问的响应数据只会把计算属性函数内部的effect
收集为依赖, 而在上面的例子中把计算属性用于另一个effect
时就发生了effect
嵌套, 且外层的effect
不会被内层的effect
中的响应式数据收集
从computed
函数中我们也可以看到里面重新顶一个了一个对象obj
并手动赋予它get
函数, 并没有像这样:
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {
// 拦截读取操作
get (target, key) {
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set (target, key, newVal) {
// 设置属性值
target[key] = newVal
trigger(target, key)
}
})
使用代理, 两个其实是完全分开的, 只是使用了同一个副作用函数
解决方法很简单既然计算属性没有绑定跟踪那就手动调用:
function computed(getter) {
// value 用来缓存上一次计算的值
let value
// dirty 标志, 用来标识是否需要重新计算, 为 true 标识需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 添加调度器, 将 dirty 重置
scheduler () {
dirty = true
// 当计算属性依赖的响应式数据发生变化时, 手动触发响应
trigger(obj, 'value')
}
})
const obj = {
get value () {
if (dirty) {
value = effectFn()
// 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
dirty = false
}
// 当读取 value 时, 手动调用 track 函数进行跟踪
track(obj, 'value')
return value
}
}
return obj
}
到此就完成了!
里面有许多的闭包...., 及其常见的闭包
目前的完整代码为:
// 储存副作用函数的桶
const bucket = new WeakMap()
// 用于储存被注册的副作用的函数
let activeEffect = undefined
// 副作用函数栈
const effectStack = []
function cleanup (effectFn) {
for (let itme of effectFn.deps) {
itme.delete(effectFn)
}
effectFn.deps.length = []
}
function effect (fn, options = {}) {
const effectFn = () => {
console.log('effect');
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
}
// const data = {
// text: 'hello world',
// ok: true
// }
const data = { foo: 1, bar: 2 }
const obj = new Proxy(data, {
// 拦截读取操作
get (target, key) {
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set (target, key, newVal) {
// 设置属性值
target[key] = newVal
trigger(target, key)
}
})
function track (target, key) {
// 没有 activeEffect, 直接 return
if (!activeEffect) return target[key]
// 根据 target 从'桶'中回去 depsMap, 它也是一个 Map 类型: key ---> effects
let depsMap = bucket.get(target)
// 如果 depsMap 不存在, 则新建一个 Map 并与 target 关联
if (!depsMap) bucket.set(target, (depsMap = new Map()))
// 再根据 key 从depsMap 中去的 deps, 它是一个 Set 类型
// 里面存贮所有与当前 key 相关的副作用函数: effects
let deps = depsMap.get(key)
// 如果 deps 不存在, 同样新建一个 Set 并与 key 关联0
if (!deps) depsMap.set(key, (deps = new Set()))
// 最后将当前激活的副作用函数添加到'桶'里
deps.add(activeEffect)
}
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的函数相同则不触发执行
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果一个副作用函数有调度器则调用改调度器, 并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否则直接执行副作用函数
effectFn()
}
})
}
function computed(getter) {
// value 用来缓存上一次计算的值
let value
// dirty 标志, 用来标识是否需要重新计算, 为 true 标识需要计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 添加调度器, 将 dirty 重置
scheduler () {
dirty = true
// 当计算属性依赖的响应式数据发生变化时, 手动触发响应
trigger(obj, 'value')
}
})
const obj = {
get value () {
if (dirty) {
value = effectFn()
// 将 dirty 设置为 false, 下一次直接访问 value 中存储的值
dirty = false
}
// 当读取 value 时, 手动调用 track 函数进行跟踪
track(obj, 'value')
return value
}
}
return obj
}
const sumRes = computed(() => obj.foo + obj.bar)
effect(() => {
// 在副作用函数中读取计算属性
console.log(sumRes.value) // 3
})
obj.bar++
// effect(() => {
// console.log('effect run');
// document.body.innerText = obj.ok ? obj.text : 'not'
// })
// setTimeout(() => {
// obj.ok = false
// }, 2000)