跳转至

2.1.响应系统 响应系统的作用与实现

响应系统的作用与实现

微型响应式系统

  • 副作用函数:执行会直接或间接的影响其他函数的执行。
  • 响应式数据:当目标数据改变之后,和这个数据绑定的副作用函数自动执行。通过拦截对象属性的读写,可以实现响应式数据。Vue3 使用 Proxy 来实现对象属性的拦截。

响应式数据最简单的例子:

HTML
<div id="app"></div>  
<script>  
  const app = document.getElementById('app');  

  // 定义包含函数的“桶”,Set 数据结构保证了不能添加重复的  
  const bucket = new Set()  
  // 定义原始数据  
  const data = {  
    text: 'Hello world',  
  }  
  // 设置数据读写代理  
  const obj = new Proxy(data, {  
    get(target, key) {  
      // 将函数放入桶中  
      bucket.add(effect)  
      return target[key];  
    },  
    set(target, key, value) {  
      target[key] = value;  
      // 执行函数  
      bucket.forEach(fn => fn())  
      return true  
    }  
  })  

  // 数据变化后应该执行的函数  
  function effect() {  
    app.innerText = obj.text  
  }  

  effect()  
  setTimeout(() => {  
    obj.text = 'Hello world 2'  
  }, 1000)  
</script>

实现匿名函数

上面的代码有一个主要问题:我们硬编码了副作用函数名字(effect),我们可以改进上述代码,通过一个匿名函数的形式来实现这个功能:

JavaScript
// 创建一个变量存储被注册的副作用函数
let activeEffect

// effect 用于注册副作用函数
function effect(fn) {
    // 注册副作用函数
    activeEffect = fn
    // 执行副作用函数
    fn()
}

const bucket = new Set()
const data = {
    text: 'Hello world',
}
const obj = new Proxy(data,{
    get(target, key) {
        // 将当前副作用函数放入桶中
        if (activeEffect) {
            bucket.add(activeEffect)
        }
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal
        bucket.forEach(fn=>fn())
        return true
    }
})

// 定义匿名的副作用函数
effect(()=>{
    console.log('effect run')
    document.body.innerText = obj.text
})


setTimeout(() => {
    // 这里有个问题,副作用函数中并没有读取 notExist 属性值,但副作用和函数仍然执行了
    obj.notExist = 'hello, vue3'
}, 1000)

副作用函数与被操作的字段之间的联接

我们没有在副作用函数与被操作的目标字段间建立明确的联系,所以当更新其他字段时,副作用函数仍被执行了。要解决这个问题, 就不能使用 Set 数据结构,而要改为 Map,由于 WeakMap 可以更好的回收不用的内存(弱引用),我们使用 WeakMap。

  • WeakMap
    • Map target(对象:data
      • Set key(该对象的键:text
        • Function activeEffect(副作用函数:匿名函数)
        • Function
        • ...
      • Set
        • ...
    • Map
    • ...
JavaScript
let activeEffect
function effect(fn) {
    activeEffect = fn
    fn()
}

// 使用 WeakMap 构建树状数据结构
const bucket = new WeakMap()

const data = {
    text: 'Hello world',
}
const obj = new Proxy(data,{
    get(target, key) {
        if (!activeEffect) {
            return target[key]
        }

        // 取得键为目标对象的 Map,如果不存在就创建
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }

        // 取得当前对象操作的key的副作用函数 Set 桶,不存在就创建
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }

        // 向桶中添加副作用函数
        deps.add(activeEffect)

        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal

        // 尝试取出副作用函数并执行
        const depsMap = bucket.get(target)
        if (!depsMap) {
            return
        }
        const effects = depsMap.get(key)
        effects && effects.forEach(fn=>fn())

    }
})

effect(()=>{
    console.log('effect run')
    document.body.innerText = obj.text
})

// 现在就不会有重复执行问题了
setTimeout(()=>{
    obj.notExist = 'hello, vue3'
}
, 1000)

分支切换与cleanup

什么是分支切换:

JavaScript
const data = {
    ok: false,
    text: 'Hello world',
}

effect(()=>{
    console.log('effect run')
    // 这个三元表达式会造成分支切换,即 obj.ok 的值发生变化时,代码执行的分支会跟着变化。
    // 分支切换可能会造成遗留的副作用函数。
    // 当 obj.ok 由 true 设置为 false 后,再次更新 obj.text 仍然会造成不必要的更新,
    // 我们期望的效果是:当 obj.ok 为 false 时,不对 obj.text 的变化进行更新
    document.body.innerText = obj.ok ? obj.text : 'not'
})
要解决这个问题, 我们可以在每次副作用函数执行之前,将其从相关联的依赖集合中移除。

  • WeakMap
    • Map data
      • Set ok = true
        • Function activeEffect
          • Array deps = [Set, Set]
      • Set text = "hello world"
        • Function activeEffect
          • Array deps = [Set, Set] (这两个 activeEffect 是相同的)

分支切换与 cleanup:

JavaScript
let activeEffect
function effect(fn) {
    const effectFn = ()=>{
        cleanup(effectFn)
        // 当 effectFn 执行时,将其设置为激活的副作用和函数
        activeEffect = effectFn
        fn()
    }
    // 在函数上挂载一个数组,用来存储所有与该副作用函数相关联的依赖 Set
    effectFn.deps = []
    effectFn()
}

// 清除副作用函数避免不必要的更新
function cleanup(effectFn) {
    // console.warn('cleanup', [...effectFn.deps])
    // 遍历依赖数组
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        // 从依赖集合中删除该函数
        deps.delete(effectFn)
    }
    // 重置数组
    effectFn.deps.length = 0
}

function track(target, key) {
    if (!activeEffect) {
        return
    }

    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, (depsMap = new Map()))
    }

    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, (deps = new Set()))
    }

    deps.add(activeEffect)
    // console.log('getter track', key, deps)

    // deps 是一个与当前副作用函数关联的 Set
    // 添加到函数的 deps 数组中
    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    const effects = depsMap.get(key)

    // 创建 Set 的副本,防止无限循环
    const effectToRun = new Set(effects)
    effectToRun.forEach(effectFn=>effectFn())
}

const bucket = new WeakMap()

const data = {
    ok: true,
    text: 'Hello world',
}
const obj = new Proxy(data,{
    get(target, key) {
        track(target, key)
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal
        trigger(target, key)
    }
})

effect(()=>{
    console.log('effect run')
    document.body.innerText = obj.ok ? obj.text : 'not'
}
)

// 更新值
setTimeout(()=>{
    console.log('set text 1')
    obj.text = 1
}
, 1000)
setTimeout(()=>{
    console.log('set ok false')
    obj.ok = false
}
, 2000)
setTimeout(()=>{
    console.log('set text 2')
    // 此处不触发更新,很好。
    obj.text = 2
}
, 3000)

副作用函数的嵌套

副作用函数是可以嵌套的,执行顺序是 effectFn1, effectFn2

JavaScript
const data = {
    foo: true,
    bar: true
}

// const obj = new Proxy(data,{...

let temp1, temp2

effect(function effectFn1() {
    console.log('effectFn1 executed')

    effect(function effectFn2() {
        console.log('effectFn2 executed')
        temp2 = obj.bar
    })

    temp1 = obj.foo
})
但是,当我们修改 obj.foo 的值,期望执行的是 effectFn1,实际上却执行的是 effectFn2
JavaScript
1
2
3
4
5
setTimeout(()=>{
    obj.foo = false
}
, 1000)
// effectFn2 executed
原因是:当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。 只需要将函数保存在栈中就可以控制执行顺序:
JavaScript
let activeEffect
const effectStack = [] // 副作用函数栈
function effect(fn) {
    const effectFn = ()=>{
        cleanup(effectFn)

        activeEffect = effectFn
        // 将当前副作用函数压入栈中
        effectStack.push(effectFn)

        fn()

        // 在副作用函数执行完后将其弹出
        effectStack.pop()
        // 还原为之前的值
        activeEffect = effectStack[effectStack.length - 1]
    }

    effectFn.deps = []
    effectFn()
}

避免无限递归

JavaScript
1
2
3
effect(function effectFn1() {
    obj.foo++
})

这个语句会造成无限递归,原因是 foo++ 既进行了读又进行了写操作。读的时候副作用函数正在执行中,还没执行完毕就开始下一次执行,导致无限递归调用。 通过简单的修改 trigger 方法可以解决无限递归:

JavaScript
function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    const effects = depsMap.get(key)

    const effectToRun = new Set()
    effects && effects.forEach(effectFn => {
        // 只有在当前副作用函数与要执行的函数不同的情况下,才将其加入要执行的函数桶中
        if (effectFn !== activeEffect) {
            effectToRun.add(effectFn)
        }
    })
    effectToRun.forEach(effectFn => effectFn())
}

调度器

通过给开放调度器配置, 可以指定函数的执行时机或次数

JavaScript
function effect(fn, options={}) {
    const effectFn = ()=>{
        cleanup(effectFn)

        activeEffect = effectFn
        effectStack.push(effectFn)

        fn()

        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    // 挂载配置
    effectFn.options = options
    effectFn.deps = []
    effectFn()
}

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    const effects = depsMap.get(key)

    const effectToRun = new Set()
    effects && effects.forEach(effectFn=>{
        if (effectFn !== activeEffect) {
            effectToRun.add(effectFn)
        }
    })
    effectToRun.forEach(effectFn=>{
        // 如果有调度器,则执行调度器
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

执行以下代码,成功改变了 effectFn 的执行顺序,让其在全部代码执行结束之后执行。也可以在调度器中自行实现队列、节流、防抖等功能,以优化大量数据变化时的性能。

JavaScript
const data = {
    foo: 1,
}

// const obj = new Proxy(data,{ ...

effect(()=>{
    console.log(obj.foo)
}
, {
    scheduler(fn) {
        setTimeout(fn)
    }
})

obj.foo++

console.log('end.')
// 1
// end.
// 2
使用防抖函数优化执行性能:JS 的节流与防抖函数
JavaScript
// 包装防抖函数
const debouncedFn = debounce((fn) => {
    fn()
}, 100)

effect(()=>{
    console.log(obj.foo)
}
, {
    // 使用防抖函数作为调度器
    scheduler: fn => debouncedFn(fn)
})

// 这里执行了3次,但调度器执行的防抖函数的回调只执行1次
obj.foo++
obj.foo++
obj.foo++

console.log('end.')

// 1
// end.
// 4
任务队列
JavaScript
// 定义一个任务队列
const jobQueue = new Set()
// 创建一个Promise实例,用它将任务添加到微任务队列
const p = Promise.resolve()

// 是否正在刷新队列
let isFlushing = false
function flushJob() {
    // 如果正在刷新,不进行下一步操作
    if (isFlushing) {
        return
    }
    isFlushing = true
    // 执行微任务队列
    p.then(() => {
        jobQueue.forEach(job => job())
    }).finally(() => {
        isFlushing = false
    })
}

function print() {
    console.log('boom')
}
function scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
}

// 无论调度执行多少次,回调函数只执行一次
for (let index = 0; index < 10; index++) {
    scheduler(print)
}

计算属性computed与lazy

2.1.x 计算属性computed与lazy

watch的实现原理

#计算属性computed与lazy的基础上增加watch函数

JavaScript
let activeEffect
const effectStack = []
// 副作用函数栈
function effect(fn, options={}) {
    const effectFn = ()=>{
        cleanup(effectFn)

        activeEffect = effectFn
        effectStack.push(effectFn)

        // 保存副作用函数执行的返回值
        const res = fn()

        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]

        return res
    }
    // 挂载配置
    effectFn.options = options
    effectFn.deps = []

    // 只有在不为lazy的时候立即执行
    if (!options.lazy) {
        effectFn()
    }
    return effectFn
}

// 清除副作用函数避免不必要的更新
function cleanup(effectFn) {
    // console.warn('cleanup', [...effectFn.deps])
    // 遍历依赖数组
    for (let i = 0; i < effectFn.deps.length; i++) {
        const deps = effectFn.deps[i]
        // 从依赖集合中删除该函数
        deps.delete(effectFn)
    }
    // 重置数组
    effectFn.deps.length = 0
}

function track(target, key) {
    if (!activeEffect) {
        return
    }

    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, (depsMap = new Map()))
    }

    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, (deps = new Set()))
    }

    deps.add(activeEffect)
    // console.log('getter track', key, deps)

    // deps 是一个与当前副作用函数关联的 Set
    // 添加到函数的 deps 数组中
    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    const effects = depsMap.get(key)

    const effectToRun = new Set()
    effects && effects.forEach(effectFn=>{
        if (effectFn !== activeEffect) {
            effectToRun.add(effectFn)
        }
    }
    )
    effectToRun.forEach(effectFn=>{
        // 如果有调度器,则执行调度器
        if (effectFn.options.scheduler) {
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    }
    )
}

const bucket = new WeakMap()

const data = {
    foo: 1,
    bar: 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)
    }
})

function watch(source, cb, options={}) {
    // 递归遍历读取对象的所有属性
    function traverse(value, seen=new Set()) {
        // 如果是原始值或null或已经读取过了,什么都不做
        if (typeof value !== 'object' || value === null || seen.has(value)) {
            return
        }
        // 添加到已读取的集合中,避免死循环
        seen.add(value)
        // 暂时不考虑数组的情况
        for (const k in value) {
            // 读取
            traverse(value[k], seen)
        }
    }
    let getter
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = ()=>traverse(source)
    }

    let oldValue, newValue

    // 用来存储用户注册的过期回调,解决竞态问题
    let cleanup
    function onInvalidate(fn) {
        cleanup = fn
    }

    const job = ()=>{
        // 得到新值
        newValue = effectFn()

        // 在调用回调函数cb之前,先调用过期回调
        if (cleanup) {
            cleanup()
        }

        // onInvalidate设置用户的过期回调函数
        cb(newValue, oldValue, onInvalidate)
        // 更新旧值
        oldValue = newValue
    }

    const effectFn = effect(()=>getter(), {
        lazy: true,
        scheduler: ()=>{
            // 判断flush是否为post,如果是,将job放到微任务中执行
            if (options.flush === 'post') {
                const p = Promise.resolve()
                p.then(job)
            } else {
                job()
            }
        }
    })
    if (options.immediate) {
        job()
    } else {
        oldValue = effectFn()

    }
}

watch(()=>obj.foo, (nv,ov)=>{
    console.log('obj 更新了', nv, ov)
}
, {
    immediate: true,
    // flush: 'post'
})

obj.foo++

console.log('end')