0
点赞
收藏
分享

微信扫一扫

响应式系统的设计与实现(二)

冶炼厂小练 2022-04-03 阅读 90

上一篇讲了响应式数据的基本实现 ,现在对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函数得到的就是新值。

举报

相关推荐

0 条评论