watch 的简单使用
想要知道 watch 的实现方式,先看下 watch函数 的基本使用
// 对 obj 的 a 属性进行监听
watch(() => obj.a, (oldValue, newValue) => {
console.log({
oldValue, newValue
})
})
上面是一个简单的watch使用,通过 对 属性a 进行监听,当a改变时,会执行第二个参数,从而打印出 oldValue, newValue 两个值。
全部代码
既然是围绕着 最少代码来讲,就先把代码展示出来。
这是实现对原始数据代理的,并不算在主要代码当中。
// 原始数据
const data = {
a: 1
}
// 实现代理,对原始数据进行监听
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
})
下面就是主要的几个函数实现,但其实不少的代码只是添加数据和取出数据的简单操作
注: 下面的代码,删除注释与空行,是37行。并没有故意把多行代码强行用一行写。
// 追踪属性读取,执行入桶操作
function track(target, key) {
if (!activeEffect) return;
let objMap = bucket.get(target);
objMap || bucket.set(target, objMap = new Map());
let propSet = objMap.get(key);
propSet || objMap.set(key, propSet = new Set());
// 上面的只是向WeakMap里添加数据,这行是关键(入桶)
propSet.add(activeEffect);
}
// 追踪属性修改,出桶与执行函数
function trigger(target, key) {
let objMap = bucket.get(target);
if (!objMap) return;
let propSet = objMap.get(key);
// 上面的是 从WeakMap里取出数据,下面是关键,执行函数
propSet && propSet.forEach(fn => {
const { scheduler } = fn.options;
scheduler ? scheduler(fn) : fn();
})
}
// 桶
const bucket = new WeakMap();
let activeEffect;
// effect函数,入口函数
function effect(fn, options = {}) {
activeEffect = fn;
activeEffect.options = options;
options.lazy || fn();
return fn;
}
// 基于 effect函数,实现 watch函数
function watch(getter, cb) {
let oldValue, newValue;
// 调用 effect函数,getter中的 ()=>obj.a 将被监听
// 将值改变成,将执行 scheduler 函数,cb为watch第二个参数,
// 并将 旧值与新值传给使用者
const effectFn = effect(getter, {
lazy: true,
scheduler() {
newValue = effectFn();
cb(oldValue, newValue);
oldValue = newValue;
}
})
oldValue = effectFn();
}
上面就可以实现简单的 watch 函数了。
下面我也尽量简短一些,去依次解释其中的几个函数
讲解一些功能
代理
watch 的逻辑是接收两个参数,第一个是要监听的数据,在这里就是一个 getter函数 ,第二个是当数据改变时要执行的函数。而代理则是用来处理 第一个参数的。
const data = {
a: 1
}
// 实现代理,对原始数据进行监听
const obj = new Proxy(data, {
//当读取 obj.a 时,get 方法会被调用
get(target, key) {
track(target, key);
return target[key];
},
//当修改 obj.a 时,set 方法会被调用
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
})
proxy 可以实现对原始数据代理,当对数据做一些操作时,会进入对应的拦截器。在get拦截器调用 track函数 ,实现入桶操作。在set拦截器 中调用 trigger函数 实现执行桶中数据的函数操作。
桶
桶存在的目的就是为了让 之间建立联系,如属性 obj.a 与 。当 a 改变时,调用这个函数,并将 oldValue,newValue 作为参数传入就可以了。
桶格式如下:
{
// 最外层存储不同的对象
obj:{
// 第二层 则是不同的属性
'a':[
// 第三层是 依赖该属性的不同函数
(oldValue, newValue) => {console.log({oldValue, newValue})}
]
}
}
桶只用通过三层就可以让 对象-属性-函数 之间建立关联。因为可能有多个地方对 属性a 进行监听,所以第三层是个集合,而不是只能存储一个函数。
桶是用 WeakMap 存储的
入桶函数 track
桶在初始化时是个空的桶,我们需要向里面添加 属性与函数 关联关系
// 追踪属性读取,执行入桶操作
function track(target, key) {
// activeEffect是要添加的函数
if (!activeEffect) return;
//添加第一层[对象(obj)],没有则初始化
let objMap = bucket.get(target);
objMap || bucket.set(target, objMap = new Map());
//添加第二层[属性(a)],没有则初始化
let propSet = objMap.get(key);
propSet || objMap.set(key, propSet = new Set());
// 添加第三层[函数],这就是入桶操作
propSet.add(activeEffect);
}
其中上面就是三个步骤
- 先将 obj 作为key 添加到桶中
- 再将 a 作为key 添加到 obj 中
- 最后将 (oldValue, newValue) => { console.log({ oldValue, newValue }) } 函数作为值,添加到 a中。
执行函数 trigger
函数 入桶是为了在 属性修改时,取出调用,所以 trigger 就是为了从桶中取出函数再调用就行了
// 追踪属性修改,出桶与执行函数
function trigger(target, key) {
// 取出第一层[对象(obj)]的值
let objMap = bucket.get(target);
if (!objMap) return;
//取出第二层[属性(a)]的值
let propSet = objMap.get(key);
// 如果有值则循环,取出第三层[函数]
propSet && propSet.forEach(fn => {
// scheduler 是在watch 中传入的函数
const { scheduler } = fn.options;
scheduler ? scheduler(fn) : fn();
})
}
上面层层读取,最终获取到 函数 scheduler ,该函数是在 watch函数 中传入的。所以当 属性修改时,就会调用对应的 scheduler函数。因为在某些场景下 是不需要传入 scheduler 的,所以没有就执行fn。
effect函数 —— Vue底层Api
effect函数 不仅是 Vue 用来实现 watch 的,而且还是用来实现 computed 的。下面的只是一个简单版,是为了能更简单的理解。
// 用来临时保存 函数的
let activeEffect;
// effect函数,入口函数
function effect(fn, options = {}) {
//临时保存函数
activeEffect = fn;
//保存配置项,如懒加载
activeEffect.options = options;
//如果是懒加载,则返回函数,由外部决定何时调用
options.lazy || fn();
return fn;
}
上面的 effect函数在这里主要是用来接收 watch函数的参数,和配置信息。如 scheduler函数 就是options 中的一个配置,懒加载也是。
watch的实现
到这里可以实现 watch函数了,因为 watch函数 就是通过调用 effect实现的。
// 接收两个参数,getter 为 ()=>obj.a
// cb 则是属性改变时要执行的函数
function watch(getter, cb) {
// 该属性的 旧值与新值
let oldValue, newValue;
//将 ()=>obj.a 作为函数传入 effect,会被 activeEffect 接收。
const effectFn = effect(getter, {
//目前实现的watch不是立即执行的,所以 lazy为true
lazy: true,
// 当属性改变时,调用 scheduler函数,就是这个函数
scheduler() {
newValue = effectFn();
cb(oldValue, newValue);
oldValue = newValue;
}
})
// 由于 实现的watch不是立即执行
// 所以初始化时要获取 ()=>obj.a 的值,作为旧值
oldValue = effectFn();
}
上面的 effectFn 会有些绕,我要细说一下
const effectFn = effect(getter, {
//目前实现的watch不是立即执行的,所以 lazy为true
lazy: true,
// 当属性改变时,调用 scheduler函数,就是这个函数
scheduler() {
// effectFn函数 就是 effect函数 的返回值
// 而 effect函数中,如果 lazy为true时,则是 return fn;
// 而 fn 就是 getter函数,就是 ()=>obj.a
//所以 newValue 就是 obj.a 的值
newValue = effectFn();
// 调用 watch第二个参数,并将两个值传过去。
cb(oldValue, newValue);
// 更新旧值
oldValue = newValue;
}
})
把流程梳理一下
上面就实现了 简单版的 watch,复杂一些的则是有 更多的配置、解决一些bug、优化性能方面。所以知道是怎么一回事就很重要了。尤其是 effect函数 与 桶bucket。
- 我们首先通过 proxy 对原始数据进行代理,实现对数据读取操作的拦截
- 接着在读取属性时 做入桶操作,将要执行的函数与属性做一个关联
- 然后在修改属性时 从桶中取出要执行的函数,执行
这就是我所总结的 实现 watch的简易版,如果发现有错误的,还请指正。如果有阅读体验不好的,也可以说出,我会尽力去调整自己写的方式,但也会有自己的想法的。