上一篇讲了响应式数据的基本实现 ,现在对get/set拦截器代码进行封装优化,并看一下计算属性computed和watch的实现原理。
const obj = new Proxy(data, {
get(target, key) {
// 收集副作用函数
track(target, key);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal;
// 取出副作用并执行
trigger(target, key);
}
})
// 封装track函数,追踪变化
function track(target, key) {
if (activeEffect) {
// 如果在第一次追踪时没有找到对相应属性订阅的副作用集合,它将会在这里新建
const effects = getSubscribersForProperty(target, key);
effects.add(activeEffect)
}
}
// 封装trigger函数,触发副总用函数重新执行
function trigger(target, key) {
const effects = getSubscribersForProperty(target, key);
effects.forEach(effect => effect());
}
可调度性
回顾一下之前的注册副作用函数
function effect(fn) {
const effectFn = () => {
// 从依赖集合中删除副作用函数
cleanup(effectFn);
activeEffect = effectFn;
fn();
};
// 存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
effectFn();
}
在trigger中触发副作用函数执行时,立即调用了effectFn。为了提供决定副作用函数执行的时机、次数以及方式的能力,响应系统增加了可调度性特性。通过给effect多传入一个options的方式实现。
effect(
() => {
console.log(obj.foo)
},
// options
{
// 调度器
scheduler(fn) {
// ...
}
}
)
// 修改effect注册函数
function effect(fn, options = {}){
const effectFn = () => {}
effectFn.options = options; // options挂载到effectFn上
effectFn.deps = [];
effectFn();
}
有了调度函数,在trigger触发effect重新执行时,就可以调用用户传的调度器函数,将控制权交给用户。
function trigger(target, key) {
// ...
effectsToRun.forEach(effectFn => {
// 如果有调度函数,则执行调度器函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
}
else {
effectFn();
}
})
}
举个列子:
effect(
() => {
console.log(obj.foo)
},
// options
{
scheduler(fn){
// 将副作用函数放到宏任务队列中执行
setTimeout(fn)
}
}
)
computed
懒执行的effect (lazy)
某些场景下,不希望副作用函数立即执行,而是需要的时候在执行,例如计算属性。可以类似调度函数一样,在options中添加lazy属性实现
effect(
() => {},
// options
{
lazy: true // 当lazy为true时不立即执行副作用函数
}
)
// 修改effect注册函数
function effect(fn, options = {}) {
const effectFn = () => {}
if (!options.lazy) {
effectFn()
}
return effectFn;
}
将传递给effect的函数看作是一个getter,那么这个getter函数可以返回任何值:
const effectFn = effect(
// getter返回obj.foo 与 obj.bar 的和
() => obj.foo + obj.bar,
{lazy: true}
)
const calue = effectFn() // value时getter的返回值
为实现这个目标,需要对副作用注册函数做些修改
function effect(fn ,options = {}) {
const effectFn = () => {
// ...
const res = fn();
// ...
return res;
}
// ...
return effectFn
}
通过以上代码可以看到,fn是真正的副作用函数,而effectFn时包装后的副作用函数,将fn的执行结果作为effecFn函数的返回值。
开始实现计算属性
计算属性的特性一:只有读取计算属性的时候,才会执行副作用函数。实现方法:
function computed(getter) {
const effectFn = effect(getter, {
lazy: true
})
const obj = {
get value() {
return effectFn()
}
}
return obj;
}
computed执行会返回一个对象,对象的value是一个访问器属性,只有当读取value时,才会执行effectFn并将结果返回。
计算属性的特性二: 值缓存
function computed(getter) {
// 缓存上一次计算的值
let value
// dirty标志用来标识是否需要重新计算值,true需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 在调度器中将lazy设置为true
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
}
wacth
watch: 本质是观察一个响应式的数据,当数据发生变化时通知并执行响应的回调函数。
effect的调度函数,当响应式数据变化的时候,会执行scheduler调度函数,即,wacth的本质是利用了effect及options.scheduler。
function wacth(source, cb) {
effect(
// 触发读取操作,建立联系
() => traverse(source),
{
scheduler(){
// 当数据变化时调用回调函数
cb();
}
}
)
}
function traverse(value, seen = new Set()) {
// 原始值或者已经被读取的值,什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
// 暂不考虑数组等其他结构,假设value是一个对象
for(const key in value) {
traverse(value[key], seen)
}
return value
}
获取更新前后的值需要用到lazy选项
function wacth(source, cb) {
let getter
getter = () => traverse(source)
let oldValue, newValue
const effectFn = effect(
// 触发读取操作,建立联系
() => traverse(source),
{
lazy: true,
scheduler(){
newValue = effectFn();
// 当数据变化时调用回调函数
cb(newValue, oldValue)
oldValue = newValue
}
}
)
oldVlaue = effectFn();
}
核心改动是使用lazy选项创建了一个懒执行的effect。倒数第二行执行的effectFn,返回的就是旧值,即第一次执行得到的值。当发生变化后,调度函数中执行的effectFn函数得到的就是新值。