今天主要来学习watch和ref的具体实现。下面代码示例部分只做参考,具体完整代码可以在vue3源码解读: 手写vue3核心源码,内含详细解读 (gitee.com)内查看。
1. watch 和 computed 的区别
- computed 目的在于计算新值,有缓存
- watch 目的在于监控属性的变化做某一件事
2. watch监听简单类型为什么要包装成方法?
因为如果直接写 state.firstName 的话,取的是字符串,无法监听,所以需要包装成方法,利用effect触发依赖收集。
3. watch 核心实现
代码
js/reactivity/watch.js
scss
function traverse(source, seen = new Set()) {
if (!isObject(source)) {
return source
}
// 对象嵌套互相引用,如果已经有了,直接返回
if (seen.has(source)) {
return source
}
seen.add(source)
for (let k in source) {
// 这里访问了对象中得1所有属性,触发依赖收集
traverse(source[k], seen)
}
return source
}
export function watch(source, cb, options) {
doWatch(source, cb, options)
}
function doWatch(source, cb, options) {
let getter
if (isReactive(source)) {
// 要进行依赖收集
getter = () => traverse(source)
} else if (isFunction(source)) {
getter = source
}
let oldValue
let clean
const onCleanup = (fn) => {
clean = fn
}
const job = () => {
if (cb) {
if (clean) clean()
const newValue = effect.run()
cb(newValue, oldValue, onCleanup)
oldValue = newValue
} else {
effect.run()
}
}
const effect = new ReactiveEffect(getter, job)
if (options && options.immediate) {
job()
}
oldValue = effect.run()
}
4. 竟态问题
问题场景
考虑这么一个场景,有一个搜索框,如百度搜索框,实时监听输入内容请求接口。我们发送了3次请求,第一个请求接口耗时3s返回, 第二个请求耗时2s返回,第三个请求耗时1s返回,如果我们不作处理,那么必然返回第一个请求接口的内容。而我们实际需要的是第三个请求内容。
watch 的解决方案就是如果有新请求进来就清空上次的操作。这个原理类似于防抖。
自行实现
ini
let timer = 4000;
function getData(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, (timer -= 1000));
});
}
let arr = []; // 用于存储上一次的清理操作
// 什么是闭包?
// 我定义函数的作用域与执行函数的作用域不是同一个
watch(
() => state.age,
async function (newVal, oldVal, onCleanup) {
// 自行实现
// while (arr.length > 0) {
// let fn = arr.shift();
// fn();
// }
let flag = true;
arr.push(function () {
flag = false
})
let r = await getData(newVal);
flag && (app.innerHTML = r);
},
{
flush: "sync",
}
);
state.age = 100; // 请求3s返回 100
state.age = 200; // 请求2s返回 200
state.age = 300; // 请求1s返回 300
首先第一次进来, arr.length 为 0, 不会执行 while, 接着定义 flag 为 true。添加闭包函数放入数组,函数内执行 flag 为 false。 接着等待 getData 异步返回。接着下次请求进来,执行了上次闭包函数将第一次的 flag 变为 false。 等到同步代码执行完成, 异步开始执行 第一次的 flag 已经变为 false, 就不会执行接下来的操作了。
简单来说, 就是每个回调有自己的 flag,当执行下一个回调 改变上一个 flag 状态。
源码实现
js/reactivity/watch.js
ini
let clean
const onCleanup = (fn) => {
clean = fn
}
const job = () => {
if (cb) {
if (clean) clean()
const newValue = effect.run()
cb(newValue, oldValue, onCleanup)
oldValue = newValue
} else {
effect.run()
}
}
步骤图
如果 getData 这一步为同步就不行了, 如果实际开发中,请把它包装成异步代码。
5. watchEffect
其实watchEffect 就是 effect。基于 watch 进行实现。
js/reactivity/watch.js
javascript
export function watchEffect(effect, options) {
doWatch(effect, null, options)
}
6. ref 的实现
核心源码
js/reactivity/ref.js
kotlin
import {toReactive} from "./reactive.js";
import {activeEffect} from "./effect.js";
import {trackEffects, triggerEffects} from "./handler.js";
export function ref(value) {
return new RefImpl(value)
}
class RefImpl {
// 内部采用类的属性访问器 -》 Object.defineProperty
constructor(rawValue) {
this.rawValue = rawValue
this._value = toReactive(rawValue)
this.dep = new Set()
}
get value() {
if (activeEffect) {
trackEffects(this.dep)
}
return this._value
}
set value(newValue) {
if (newValue !== this.rawValue) {
this.rawValue = newValue
this._value = toReactive(newValue)
triggerEffects(this.dep)
}
}
}
// ref 代理的实现
class ObjectRefImpl {
constructor(object, key) {
this.object = object
this.key = key
}
get value() {
return this.object[this.key]
}
set value(newValue) {
this.object[this.key] = newValue
}
}
export function toRef(object, key) {
return new ObjectRefImpl(object, key)
}
export function toRefs(object) {
const ret = {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}