- 作者:老汪软件技巧
- 发表时间:2024-10-04 00:01
- 浏览量:
# Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统
# Vue3 响应式数据设计(二)从4个方面思考完善响应式式数据设计
# Vue3 响应式数据设计(三)computed 实现
在上一篇中我们实现了一个computed ,在本篇我将实现一个精简版的watch。当然如果你对watch 不够了解的话可以先看看这篇文章# Vue3 watch 的六大特点
首先来说说什么是watch,watch的本质其实就是监听响应式数据的变化,变化了之后执行一个回调函数。实际上watch的实现本质就是利用了effect 和options.sheduler选项,如下面代码所示:
effect(() => {
console.log(proxy.name)
}, {
scheduler () {
// 当proxy.name变化时,会执行scheduler 调度执行
}
})
watch 的基本实现:
function watch(source, cb) {
effect(() => source.name, {
scheduler () {
cb()
}
})
}
watch(proxy, () => {
console.log('数据变化了')
})
proxy.name = 'hello'
解决watch 硬编码问题
以上代码执行后,会打印出数据变化了。但是我们的实现中硬编码了source.name的读取操作。也就是说我们实现的watch只能监听source.name的变化。为了让watch 监听具有通用性,我们需要封装一个通用的读取操作:
function watch(source, cb) {
effect(() => traverse(source), {
scheduler () {
// 当数据变化时调用回调函数。
cb()
}
})
function traverse(value, seen = new Set()) {
// 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
seen.add(value)
for (const k in value) {
traverse(value[key], seen)
}
return value
}
}
上面的代码中我们封装了traverse 函数用来递归读取对象上的属性,这样我们就能监听到任意对象类型的数据变化了。
watch 的getter 形式
watch 除了可以监听响应式数据,还可以监听一个getter 函数:
watch(() => proxy.name, () => {
console.log('proxy.name的值变化了')
})
proxy.name = 'hello'
目前我们实现的watch 函数还不能实现监听getter 函数,需要改造下。
function watch(source, cb) {
// 定义getter
let getter
// 如果source 是函数,说明用户传的getter,直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
effect(() => getter(), {
scheduler () {
// 当数据变化时调用回调函数。
cb()
}
})
function traverse(value, seen = new Set()) {
// 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
seen.add(value)
for (const k in value) {
traverse(value[key], seen)
}
return value
}
}
watch(() => proxy.name, () => {
console.log('proxy.name的值变化了')
})
在上面的的代码中,我们实现了getter 函数的监听。
newVal和oldVal 参数补充
我们的watch 实现已经越来越完善,但是如果去看看Vue的watch ,会发现我们的实现回调函数缺少了新值和旧值。接下来我们就来补充这个功能。
function watch(source, cb) {
// 定义getter
let getter
// 如果source 是函数,说明用户传的getter,直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let newVal, oldVal
const effectFn = effect(() => getter(), {
lazy: true,
scheduler () {
newVal = effectFn()
// 当数据变化时调用回调函数。将新值旧值传到回调函数里面
cb(newVal, oldVal)
// 更新旧值,避免下次拿到的旧值是错的。
oldVal = newVal
}
})
// 手动执行副作用函数拿到的就是旧值
oldVal = effectFn()
function traverse(value, seen = new Set()) {
// 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
seen.add(value)
for (const k in value) {
traverse(value[key], seen)
}
return value
}
}
在这段代码中,最核心的是使用lazy选项创建了一个懒执行的effect。注意上面代码中最下面的部分,我们手动调用effectFn函数得到的返回值是旧值,即第一次执行得到的值。当变化发生并触发scheduler时,会重新调用effectFn函数得到新的值,这样我们就拿到了新值与旧值,接着将它们作为参数传递给cb 就可以了。最后一个非常重要的是,不要忘记用新值更新旧值oldVal = newVal,否则下一次变更发生时得到错误的旧值。
watch 执行时机
在Vue中的watch执行时机主要有3种情况,立即执行,组件更新前执行,组件更新后执行。我们先来看看立即执行如何实现。
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source 是函数,说明用户传的getter,直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let newVal, oldVal
const job = () => {
newVal = effectFn()
// 当数据变化时调用回调函数。将新值旧值传到回调函数里面
cb(newVal, oldVal)
// 更新旧值,避免下次拿到的旧值是错的。
oldVal = newVal
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: job
})
// 手动执行副作用函数拿到的就是旧值
if (options.immediate) {
// 当immediate为true时,立即执行job,从而触发回调执行
job()
} else {
oldVal = effectFn()
}
oldVal = effectFn()
function traverse(value, seen = new Set()) {
// 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
seen.add(value)
for (const k in value) {
traverse(value[key], seen)
}
return value
}
}
watch(() => proxy.name, () => {
console.log('proxy.name的值变化了')
}, {
immediate: true
})
在上面的代码中我们主要添加了immediate参数,当immediate 为true 时会立即执行一次。我们运行上面的代码,虽然没有修改proxy属性的值,回调函数也执行了一次,实现了watch的立即执行。
在Vue中watch 除了立即执行,还可以通过flush参数来控是在组件更新前执行还是在更新后执行。值为post 时,在更新后执行 ,为prev在更新前执行。我们还没实现组件,所以这里只能模拟实现一个。
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source 是函数,说明用户传的getter,直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let newVal, oldVal
const job = () => {
newVal = effectFn()
// 当数据变化时调用回调函数。将新值旧值传到回调函数里面
cb(newVal, oldVal)
// 更新旧值,避免下次拿到的旧值是错的。
oldVal = newVal
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: () => {
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
})
// 手动执行副作用函数拿到的就是旧值
if (options.immediate) {
// 当immediate为true时,立即执行job,从而触发回调执行
job()
} else {
oldVal = effectFn()
}
oldVal = effectFn()
function traverse(value, seen = new Set()) {
// 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
seen.add(value)
for (const k in value) {
traverse(value[key], seen)
}
return value
}
}
watch(() => proxy.name, () => {
console.log('proxy.name的值变化了')
}, {
immediate: true,
flush: 'post'
})
proxy.name = 'hello'
在上面的代码中我们添加了flush 参数用来控制是在组件更新前执行回调还是在组件更新后执行回调。这里我们用Promise微任务来模拟组件的更新。
watch 常用的功能我们基本都已经实现,现在来看下我们响应式数据设计的完整代码:
响应式数据设计完整代码
const data = {
name: 'jame',
age: 30
}
const proxy = new Proxy(data, {
get (target, key) {
track(target, key)
return target[key]
},
set (target, key, newVal) {
target[key] = newVal
trigger(target, key)
return true
}
})
let acctiveEffect = null
const effectStack = []
function effect(fn, options = {}) {
function effectFn() {
clearUp(effectFn)
// 当effectFn执行时,将其设置为当前激活的副作用函数
acctiveEffect = effectFn
effectStack.push(effectFn)
const res = fn()
effectStack.pop()
acctiveEffect = effectStack[effectStack.length - 1]
return res
}
effectFn.options = options
// 用来存储所有与该副作用相关的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn()
}
return effectFn
}
let bucket = new WeakMap()
/**
* desc 读取属性值时和副作用函数建立联系
* @param {代理的目标对象} target
* @param {属性,键} key
* @returns
*/
function track(target, key) {
if (!acctiveEffect) 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中
deps.add(acctiveEffect)
// deps 就是一个与当前副作用函数存在联系的集合
// 将其添加到activeEffect.deps数组中
acctiveEffect.deps.push(deps)
}
/**
* desc 当修改属性值时触发副作用函数处理逻辑
* @param {目标对象} target
* @param {键, 属性} key
* @returns
*/
function trigger (target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
// 用一个新变量是为了避免无限循环
const newEffects = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== acctiveEffect) {
newEffects.add(effectFn)
}
})
newEffects && newEffects.forEach(fn => {
if (fn.options && fn.options.scheduler) {
fn.options.scheduler(fn)
} else {
fn()
}
})
}
function clearUp (effectFn) {
for (var i = 0; i < effectFn.deps.length; i++ ) {
// deps 是依赖的集合
const deps = effectFn.deps[i]
// 将effectFn从依赖集合中移除
deps.delete(effectFn)
}
// 最后需要重置effectFn.deps数组
effectFn.deps.length = 0
}
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
function flushJop () {
if (isFlushing) {
return
}
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
function computed (getter) {
// 用来保存上一次的值
let value
// 用来保存是否需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler () {
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
track(obj, 'value')
return value
}
}
return obj
}
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source 是函数,说明用户传的getter,直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let newVal, oldVal
const job = () => {
newVal = effectFn()
// 当数据变化时调用回调函数。将新值旧值传到回调函数里面
cb(newVal, oldVal)
// 更新旧值,避免下次拿到的旧值是错的。
oldVal = newVal
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: () => {
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
})
// 手动执行副作用函数拿到的就是旧值
if (options.immediate) {
// 当immediate为true时,立即执行job,从而触发回调执行
job()
} else {
oldVal = effectFn()
}
oldVal = effectFn()
function traverse(value, seen = new Set()) {
// 如果读取的是原始值,或者已经被读取过了,那么什么的都不做
if(typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到seen 中,代表遍历地读取过了,避免循环引用引起死循环。
seen.add(value)
for (const k in value) {
traverse(value[key], seen)
}
return value
}
}
总结: 本篇基于前面的响应式数据设计,实现了Vue中watch常用的使用场景。
Vue3响应式数据设计(四)watch实现就分享到这里了,感谢收看,一起学习一起进步。