Vue.js设计与实现—响应式的设计与实现学习笔记(二)

146 阅读6分钟

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)