响应系统的作用与实现 (下)

简介: 响应系统的作用与实现

响应系统的作用与实现  (上)https://developer.aliyun.com/article/1392226

4.6 避免无限递归循环

既会读取 obj.foo 的值,又会设置 obj.foo 的值。会导致无限递归。

effect(() => {
  // 语句
  obj.foo = obj.foo + 1
})

首先读取 obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执

行。这样会导致无限递归地调用自己,于是就产生了栈溢出。

解决方案:

可以在 trigger 动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。

// trigger函数
function trigger(target, key) {
    const depsMap = bucket.get(target) // target Map
    if (!depsMap) return;
    const effects = depsMap.get(key) // effectFn Set
    const effectToRun = new Set()
    effects && effects.forEach(effectFn => { // 增加守卫条件
        if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行
            effectToRun.add(effectFn)
        }
    })
    effectToRun && effectToRun.forEach(fn => {
        if (typeof fn === 'function') fn()
    })
}

4.7 调度执行

可调度性是响应系统非常重要的特性。首先我们需要明确什么是可调度性。所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

利用 Set 数据结构的自动去重能力


const data = {
    foo: 1
}
let activeEffect,// 当前被激活的副作用函数
    effectStack = [], // 副作用函数栈
    jobQueue = new Set() // 任务队列,通过Set自动去重相同的副作用函数
const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap
const p = Promise.resolve() // 使用promise实例将任务添加到微任务队列
let isFlushing = false // 是否正在刷新队列
function flushJob() {
    if (isFlushing) return // 如果正在刷新,则什么也不做
    isFlushing = true // 正在刷新
    p.then(() => { // 将副作用函数的执行放到微任务队列中
        jobQueue.forEach(effectFn => effectFn()) // 取出任务队列中的所有副作用函数执行
    }).finally(() => {
        isFlushing = false // 重置刷新标志
    })
}
function effect(fn, options = {}) {
    const effectFn = () => {
        // 副作用函数执行之前,将该函数从其所在的依赖集合中删除
        cleanup(effectFn)
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn
        effectStack.push(activeEffect) // 将当前副作用函数推进栈
        fn()
        // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数
        // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn.options = options // 将用户传进来的options挂载到副作用函数effectFn上
    effectFn()
}
function cleanup(effectFn) {
    for (let i = 0, len = effectFn.deps.length; i < len; i++) {
        let deps = effectFn.deps[i] // 依赖集合
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0 // 重置effectFn的deps数组
}
const obj = new Proxy(data, {
    get(target, p, receiver) {
        track(target, p)
        return target[p]
    },
    set(target, p, value, receiver) {
        target[p] = value
        trigger(target, p) // 把副作用函数取出并执行
        return true
    }
})
// track函数
function track(target, key) {
    if (!activeEffect) return // 没有正在执行的副作用函数 直接返回
    let depsMap = bucket.get(target)
    if (!depsMap) { // 不存在,则创建一个Map
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
    if (!deps) { // 不存在,则创建一个Set
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect) // 将副作用函数加进去
    // deps就是当前副作用函数存在联系的依赖集合
    // 将其添加到activeEffect.deps数组中
    activeEffect.deps.push(deps)
}
// trigger函数
function trigger(target, key) {
    const depsMap = bucket.get(target) // target Map
    if (!depsMap) return;
    const effects = depsMap.get(key) // effectFn Set
    const effectToRun = new Set()
    effects && effects.forEach(effectFn => { // 增加守卫条件
        if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行
            effectToRun.add(effectFn)
        }
    })
    effectToRun && effectToRun.forEach(fn => {
        if (fn.options.scheduler) { // 该副作用函数选项options中的调度器函数存在
            fn.options.scheduler(fn)
        } else { // 如果不存在scheduler调度函数,则直接调用副作用函数
            fn()
        }
    })
}
effect(
    () => {
        console.log(obj.foo)
    },
    {
        scheduler(fn) {
            // 每次调度时, 将副作用函数添加到任务队列中。注意:同一个副作用函数加进去会由于jobQueue是Set而去重
            // 当宏任务完成后,值已经是最终状态,中间状态的值不会通过副作用函数体现出来
            jobQueue.add(fn)
            // 调用flushJob刷新队列
            flushJob()
        },
    })
obj.foo++
obj.foo++
console.log(`over`)

4.8 计算属性 computed 与 lazy

实现懒执行的副作用函数,并且能够拿到副作用函数的执行结果

const data = {
    foo: 1,
    bar: 2
}
let activeEffect,// 当前被激活的副作用函数
    effectStack = [], // 副作用函数栈
    jobQueue = new Set() // 任务队列,通过Set自动去重相同的副作用函数
const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap
const p = Promise.resolve() // 使用promise实例将任务添加到微任务队列
let isFlushing = false // 是否正在刷新队列
function flushJob() {
    if (isFlushing) return // 如果正在刷新,则什么也不做
    isFlushing = true // 正在刷新
    p.then(() => { // 将副作用函数的执行放到微任务队列中
        jobQueue.forEach(effectFn => effectFn()) // 取出任务队列中的所有副作用函数执行
    }).finally(() => {
        isFlushing = false // 重置刷新标志
    })
}
function effect(fn, options = {}) {
    const effectFn = () => {
        // 副作用函数执行之前,将该函数从其所在的依赖集合中删除
        cleanup(effectFn)
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn
        effectStack.push(activeEffect) // 将当前副作用函数推进栈
        const res = fn() // lazy选项,getter函数,执行的结果res
        // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数
        // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res;// 将函数的结果传递出去,配合lazy选项
    }
    effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn.options = options // 将用户传进来的options挂载到副作用函数effectFn上
    if (options.lazy) { // lazy的话就把副作用函数返回出去
        return effectFn
    }else { // 否则就立即执行该副作用函数
        effectFn()
    }
}
function cleanup(effectFn) {
    for (let i = 0, len = effectFn.deps.length; i < len; i++) {
        let deps = effectFn.deps[i] // 依赖集合
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0 // 重置effectFn的deps数组
}
const obj = new Proxy(data, {
    get(target, p, receiver) {
        track(target, p)
        return target[p]
    },
    set(target, p, value, receiver) {
        target[p] = value
        trigger(target, p) // 把副作用函数取出并执行
        return true
    }
})
// track函数
function track(target, key) {
    if (!activeEffect) return // 没有正在执行的副作用函数 直接返回
    let depsMap = bucket.get(target)
    if (!depsMap) { // 不存在,则创建一个Map
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
    if (!deps) { // 不存在,则创建一个Set
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect) // 将副作用函数加进去
    // deps就是当前副作用函数存在联系的依赖集合
    // 将其添加到activeEffect.deps数组中
    activeEffect.deps.push(deps)
}
// trigger函数
function trigger(target, key) {
    const depsMap = bucket.get(target) // target Map
    if (!depsMap) return;
    const effects = depsMap.get(key) // effectFn Set
    const effectToRun = new Set()
    effects && effects.forEach(effectFn => { // 增加守卫条件
        if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行
            effectToRun.add(effectFn)
        }
    })
    effectToRun && effectToRun.forEach(fn => {
        if (fn.options.scheduler) { // 该副作用函数选项options中的调度器函数存在
            fn.options.scheduler(fn)
        } else { // 如果不存在scheduler调度函数,则直接调用副作用函数
            fn()
        }
    })
}
// 传递给effect函数注册的才是真正的副作用函数(getter),effectFn是包装过后的函数
// 通过执行包装后的effectFn函数可以得到副作用函数的结果,下面为obj.foo+obj.bar的结果
// const effectFn = effect(
//     () => obj.foo + obj.bar, // 将传递给effect的函数当做getter函数,该getter函数可以返回任何值
//     {
//         lazy: true
//     }
// )
// const value = effectFn()
// console.log(value)
function computed(getter) {
    // 缓存设置
    let value,
        dirty = true // true意味着脏,则需要重新调用effectFn进行计算得到结果
    const effectFn = effect(getter, {
        lazy: true,
        scheduler(fn) {
            // fn() // 此处看控制台
            // const res = fn() // 此处要不要fn()都无所谓,因为不会产生影响,computed是一个计算属性,副作用函数是个getter
            // console.log('res', res)
            dirty = true // 通过调度器,将dirty设为脏
            // computed依赖的响应式数据变化时,手动调用trigger函数触发响应
            trigger(obj, 'value')
        }
    })
    const obj = {
        get value() { // value属性是一个getter,当被obj.value时就会执行包装的副作用函数effectFn得到getter副作用的结果
            if (dirty) {
                value = effectFn()
                dirty = false
            }
            if(activeEffect) {
                // 当读取value时,手动调用track函数进行追踪
                track(obj, 'value')
            }
            return value
        }
    }
    return obj
}
const o = computed(() => {
    console.log('effect Fn')
    return obj.foo + obj.bar
})
console.log(o.value)
obj.foo++
console.log(o.value)
console.log('-----------------------------------')
effect(() => {
    console.log('另一个effect调用computed计算属性')
    console.log(o.value)
})
obj.foo++

实现计算属性并且添加缓存功能

const data = {
    foo: 1,
    bar: 2
}
let activeEffect,// 当前被激活的副作用函数
    effectStack = [], // 副作用函数栈
    jobQueue = new Set() // 任务队列,通过Set自动去重相同的副作用函数
const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap
const p = Promise.resolve() // 使用promise实例将任务添加到微任务队列
let isFlushing = false // 是否正在刷新队列
function flushJob() {
    if (isFlushing) return // 如果正在刷新,则什么也不做
    isFlushing = true // 正在刷新
    p.then(() => { // 将副作用函数的执行放到微任务队列中
        jobQueue.forEach(effectFn => effectFn()) // 取出任务队列中的所有副作用函数执行
    }).finally(() => {
        isFlushing = false // 重置刷新标志
    })
}
function effect(fn, options = {}) {
    const effectFn = () => {
        // 副作用函数执行之前,将该函数从其所在的依赖集合中删除
        cleanup(effectFn)
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn
        effectStack.push(activeEffect) // 将当前副作用函数推进栈
        const res = fn() // lazy选项,getter函数,执行的结果res
        // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数
        // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res;// 将函数的结果传递出去,配合lazy选项
    }
    effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn.options = options // 将用户传进来的options挂载到副作用函数effectFn上
    if (options.lazy) { // lazy的话就把副作用函数返回出去
        return effectFn
    }else { // 否则就立即执行该副作用函数
        effectFn()
    }
}
function cleanup(effectFn) {
    for (let i = 0, len = effectFn.deps.length; i < len; i++) {
        let deps = effectFn.deps[i] // 依赖集合
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0 // 重置effectFn的deps数组
}
const obj = new Proxy(data, {
    get(target, p, receiver) {
        track(target, p)
        return target[p]
    },
    set(target, p, value, receiver) {
        target[p] = value
        trigger(target, p) // 把副作用函数取出并执行
        return true
    }
})
// track函数
function track(target, key) {
    if (!activeEffect) return // 没有正在执行的副作用函数 直接返回
    let depsMap = bucket.get(target)
    if (!depsMap) { // 不存在,则创建一个Map
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
    if (!deps) { // 不存在,则创建一个Set
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect) // 将副作用函数加进去
    // deps就是当前副作用函数存在联系的依赖集合
    // 将其添加到activeEffect.deps数组中
    activeEffect.deps.push(deps)
}
// trigger函数
function trigger(target, key) {
    const depsMap = bucket.get(target) // target Map
    if (!depsMap) return;
    const effects = depsMap.get(key) // effectFn Set
    const effectToRun = new Set()
    effects && effects.forEach(effectFn => { // 增加守卫条件
        if (effectFn !== activeEffect) { // trigger触发执行的副作用函数如果和当前正在执行的副作用函数一样,就不触发执行
            effectToRun.add(effectFn)
        }
    })
    effectToRun && effectToRun.forEach(fn => {
        if (fn.options.scheduler) { // 该副作用函数选项options中的调度器函数存在
            fn.options.scheduler(fn)
        } else { // 如果不存在scheduler调度函数,则直接调用副作用函数
            fn()
        }
    })
}
// 传递给effect函数注册的才是真正的副作用函数(getter),effectFn是包装过后的函数
// 通过执行包装后的effectFn函数可以得到副作用函数的结果,下面为obj.foo+obj.bar的结果
// const effectFn = effect(
//     () => obj.foo + obj.bar, // 将传递给effect的函数当做getter函数,该getter函数可以返回任何值
//     {
//         lazy: true
//     }
// )
// const value = effectFn()
// console.log(value)
function computed(getter) {
    // 缓存设置
    let value,
        dirty = true // true意味着脏,则需要重新调用effectFn进行计算得到结果
    const effectFn = effect(getter, {
        lazy: true,
        scheduler(fn) {
            // fn() // 此处看控制台
            // const res = fn() // 此处要不要fn()都无所谓,因为不会产生影响,computed是一个计算属性,副作用函数是个getter
            // console.log('res', res)
            dirty = true // 通过调度器,将dirty设为脏
            // computed依赖的响应式数据变化时,手动调用trigger函数触发响应
            trigger(obj, 'value')
        }
    })
    const obj = {
        get value() { // value属性是一个getter,当被obj.value时就会执行包装的副作用函数effectFn得到getter副作用的结果
            if (dirty) {
                value = effectFn()
                dirty = false
            }
            if(activeEffect) {
                // 当读取value时,手动调用track函数进行追踪
                track(obj, 'value')
            }
            return value
        }
    }
    return obj
}
const o = computed(() => {
    console.log('effect Fn')
    return obj.foo + obj.bar
})
console.log(o.value)
obj.foo++
console.log(o.value)
console.log('-----------------------------------')
effect(() => {
    console.log('另一个effect调用computed计算属性')
    console.log(o.value)
})
obj.foo++

image.png

4.9 watch 的实现原理

所谓 watch,其本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。

watch 的实现本质上就是利用了 effect 以及 options.scheduler 选项。

目录
相关文章
|
6月前
接口请求内容改变的问题.
接口请求内容改变的问题.
25 0
|
2月前
|
缓存 网络协议 CDN
在网页请求到显示的过程中,如何优化网络通信速度?
在网页请求到显示的过程中,如何优化网络通信速度?
186 59
|
12天前
|
缓存 安全 数据安全/隐私保护
在实际应用中,如何根据具体场景选择合适的请求方法?
【10月更文挑战第29天】在实际应用中,需要综合考虑各种因素,如数据的性质、操作的语义、安全性要求、性能优化等,来选择最合适的 HTTP 请求方法。同时,还需要根据具体的业务逻辑和系统架构,对请求方法的使用进行合理的设计和规范,以确保系统的安全、稳定和高效运行。
|
3月前
|
Web App开发 监控 安全
[译] 用 sendBeacon 发送分析信息的优点
[译] 用 sendBeacon 发送分析信息的优点
|
4月前
|
前端开发 Java Spring
设置响应内容类型的几种方法比较
设置响应内容类型的几种方法比较
|
5月前
|
前端开发 开发工具 git
大事件项目15----axios响应拦截器,统一判断401做被动退出
大事件项目15----axios响应拦截器,统一判断401做被动退出
|
6月前
|
Python
【需求响应】一种新的需求响应机制DR-VCG研究
【需求响应】一种新的需求响应机制DR-VCG研究
【需求响应】一种新的需求响应机制DR-VCG研究
|
6月前
|
存储 JavaScript Java
响应系统的作用与实现(上)
响应系统的作用与实现
83 0
|
11月前
全局响应返回处理
全局响应返回处理
42 0
|
JSON PHP 数据格式
响应 方式
响应 方式