Vue.js设计与实现—响应式的设计与实现学习笔记(二)
调度执行
调度执行就是让用户可以控制副作用函数的执行时机、次数以及方式
调度器控制副作用函数的执行顺序
const data = { foo: 1 }
effect(() => {
console.log(obj.foo)
})
obj.foo++
console.log('gg')
打印出来的顺序是
// 1
// 2
// gg
// 我们希望的打印顺序是
// 1
// gg
// 2
这时我们需要一个调度器(让用户可以控制副作用函数的执行时机)
// 新增options选项参数
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将options挂载到effectFn上
effectFn.options = options
effectFn.deps = []
effectFn()
}
function trigger(target, key) {
let depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects)
effectsToRun &&
effectsToRun.forEach(effectFn => {
if (effectFn !== activeEffect) {
// 如果用户指定了调度器,则调用该调度器,并传入副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
// 否则直接执行副作用函数
effectFn()
}
}
})
}
// effect中传入调度器
effect(
() => {
console.log(obj.foo)
},
{
scheduler(fn) {
setTimeout(() => {
fn()
}, 100)
},
}
)
调度器控制副作用函数的执行次数
effect(() => {
console.log(obj.foo)
})
obj.foo++
obj.foo++
打印出来的是
// 1
// 2
// 3
// 我们期望的结果是不包含过渡状态,只要头和尾
// 1
// 3
我们用一个调度器来控制副作用函数的执行次数次数
const jobQueue = new Set()
const job = Promise.resolve()
let isFlushing = false
function flushJob() {
if (isFlushing) return
isFlushing = true
// 书中用一个微任务来实现
job
.then(() => {
jobQueue.forEach(job => job())
})
.finally(() => {
isFlushing = false
})
// 宏任务也可以实现
// TODO 不知道宏任务实现跟微任务实现的区别在哪里,作者为什么要用微任务来实现
// setTimeout(() => {
// jobQueue.forEach(job => job())
// isFlushing = false
// }, 0)
}
effect(
() => {
console.log(obj.foo)
},
{
scheduler(fn) {
jobQueue.add(fn)
flushJob()
},
}
)
obj.foo++
obj.foo++
这里利用 js 中任务执行顺序是(同步任务——> 微任务——>宏任务),所以会等 foo 自增完成,才会去执行微任务/宏任务。
所以实现了控制副作用函数的执行次数,对同一个属性连续变更,无论变更 N 次,都只会在最后一次更新时执行副作用函数。
计算属性 computed 与 lazy
懒执行的 effect 实现 computed
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将options挂载到effectFn上
effectFn.options = options
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
const effectFn = effect(
() => {
console.log('执行副作用函数', obj.foo)
},
{
lazy: true,
}
)
// 手动执行effectFn()
effectFn()
const data = { foo: 1, bar: 1 }
// 修改副作用函数为一个getter
const effectFn = effect(() => obj.foo + obj.bar, {
lazy: true,
})
console.log(effectFn())
// 我们希望打印出来的是2,但实际是undefined
// 还需要修改effect函数
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 返回副作用函数的执行结果
return res
}
// 将options挂载到effectFn上
effectFn.options = options
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
实现一个简单的计算属性
function computed(getter) {
const effectFn = effect(getter, {
lazy: true,
})
const obj = {
get value() {
return effectFn()
},
}
return obj
}
const sum = computed(() => obj.foo + obj.bar)
console.log('sum', sum.value) // sum 2
console.log('sum1', sum.value) // sum 2
console.log('sum2', sum.value) // sum 2
我们多次连续读取sum.value,值是一样的,但是每次都会去执行副作用函数,没有缓存。
我们希望计算属性绑定的依赖没有变更时,返回缓存值;变更时,才重新计算。
function computed(getter) {
// 用于缓存上一次计算的值 --避免每次调用都重新计算
let value,
// 是否需要重新计算值,true则需要
dirty = true
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,重置dirty
scheduler() {
if (!dirty) {
dirty = true
}
},
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
return value
},
}
return obj
}
const sum = computed(() => obj.foo + obj.bar)
console.log('sum', sum.value)
console.log('sum1读取缓存值', sum.value)
// 设置响应式的值,触发trigger函数进而触发scheduler重置dirty
obj.foo = 3
console.log('sum4重新计算', sum.value)
嵌套计算属性还存在缺陷
// 嵌套计算属性
effect(() => {
console.log('sum111111111', sum.value)
})
obj.foo++
响应式数据变更时,没有执行外层(嵌套计算属性)的副作用函数。外层的effect不会被内层effect中的响应式数据收集。修改如下:
function computed(getter) {
// 用于缓存上一次计算的值 --避免每次调用都重新计算
let value,
// 是否需要重新计算值,true则需要
dirty = true
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,重置dirty
scheduler() {
if (!dirty) {
dirty = true
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发响应
trigger(obj, 'value')
}
},
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
// 读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
},
}
return obj
}
watch的实现原理--watch的本质是观测一个响应式数据,并传递一个回调函数
实现一个简单的watch
function watch(source, cb) {
effect(
// 调用traverse递归的读取,触发读取操作,从而建立联系
() => traverse(source),
{
scheduler() {
// 当响应式数据发生变化时,调用cb
cb()
},
}
)
}
function traverse(value, seen = new Set()) {
// 如果要读取的数据是原始值,或者已经被读取过了,那什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
for (const key in value) {
traverse(value[key], seen)
}
return value
}
watch(obj, () => {
console.log('obj响应式数据发生变化了')
})
obj.foo++
watch函数的第一个参数是一个getter函数,在getter函数内部,用户可以制定该watch依赖哪些响应式数据
function watch(source, cb) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
const effectFn = effect(
// 调用traverse递归的读取,触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
scheduler() {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
},
}
)
// 这里如果是值是对象,会有浅拷贝的问题
oldValue = effectFn()
}
watch(obj, (newV, oldV) => {
console.log(newV, oldV)
})
obj.foo++
watch(() => obj.bar, (newV, oldV) => {
console.log(newV, oldV)
})
obj.bar++
接受第三个可选参数options是一个对象
立即执行的watch
function watch(source, cb, options) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
const effectFn = effect(
// 调用traverse递归的读取,触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
scheduler: job,
}
)
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
指定回调函数的执行时机options.flush:
- flush是post,则将job函数放入微任务中实现一步延迟执行,故会在DOM节点更新结束后再执行
function watch(source, cb, options) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
const effectFn = effect(
// 调用traverse递归的读取,触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
scheduler: () => {
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
},
}
)
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
watch(
obj,
(newV, oldV) => {
console.log(newV, oldV)
},
{
flush: 'pre', // 'pre'/'post'/'sync'
}
)
过期的副作用函数--竞态问题
let finalData
function watch(source, cb, options) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 用来存储用户注册过的过期回调
let cleanup
function onInvalidata(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
newValue = effectFn()
if (cleanup) {
cleanup()
}
cb(newValue, oldValue, onInvalidata)
oldValue = newValue
}
const effectFn = effect(
// 调用traverse递归的读取,触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
scheduler: () => {
if (options?.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
},
}
)
if (options?.immediate) {
job()
} else {
oldValue = effectFn()
}
}
watch(obj, async (newValue, oldValue, onInvalidata) => {
let expired = false
onInvalidata(() => {
expired = true
})
const res = await fetch()
if (!expired) {
finalData = res
}
console.log('finalData', finalData)
})
obj.foo++
setTimeout(() => {
obj.foo++
}, 200)