vue.js设计与实现——vue3响应式原理

217 阅读5分钟

1 响应式实现的原理

提起vue3的实现原理,你可能会说:Proxy的拦截,在get的时候拦截进行双向绑定,在set的时候更新界面实现。然而再深入询问后可能就说不出什么来了,因为也没有具体看过。

下面根据对vue.js设计和实现的学习,结合自己的理解记录下响应式的实现。

1.1 响应式实现讲解

  1. 创建原始data数据,添加proxy拦截

  2. 创建effect副作用函数,里面会有dom的更新,数据的get操作

  3. effect副作用函数执行时会触发get的拦截,此时进行依赖收集track,建立数据和副作用函数直接的联系

  4. 等待数据发生改变,此时会触发set拦截,取出相关的副作用函数,清除相关的依赖,执行副作用函数。

  5. 副作用函数的执行又会触发get进行依赖收集,相当于回到第二步继续向下执行,形成循环。

1.2 响应式实现流程图

Dingtalk_20220817142229.jpg

2 代码模拟实现

2.1 先上完整代码,及效果

let activeEffect,
    effectStack = []

const bucket = new WeakMap()    // 收集副作用函数的桶

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)
    activeEffect.deps.push(deps)    // key和effect之间建立双向联系,方便在取出副作用函数的时候清除相关依赖
}

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)    // effects就是要执行的副作用函数集合,是个Set
    const effectToRun = new Set()
    effects && effects.forEach(effectFn => {   // 执行前过滤一下,防止get的同时set(例如++操作)导致的死循环
        if (activeEffect !== effectFn) {
            effectToRun.add(effectFn)
        }
    })
    effectToRun && effectToRun.forEach(effectFn => {
        effectFn()
    })
}

function effect(fn) {
    const effectFn = () => {
        cleanUp(effectFn)       // 执行副作用函数前要清除相关依赖,依赖在deps中,后面触发get又会收集的
        activeEffect = effectFn     // 将副作用函数用activeEffect记录下来,后面收集和执行的时候用
        effectStack.push(effectFn)  // 用effectStack套一层是为了防止effect嵌套导致外层数据丢失
        fn()        // 执行一次函数,这里会触发get拦截
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    effectFn()
}

function cleanUp(effectFn) {
    for (let i = 0, len = effectFn.deps.length; i < len; i++) {  // 依赖在deps中
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0    // 清除完依赖后将他置空
}

const data = {
    foo: 1,
    bar: 2
}

const obj = new Proxy(data, {
    get(target, key, receiver) {
        track(target, key)  // 收集依赖
        return target[key]
    },
    set(target, key, newValue, receiver) {
        target[key] = newValue
        trigger(target, key)    // 取出相关依赖执行
        return true
    }
})

effect(() => {
    console.log('efffect run')
    console.log(obj.foo)    // 这里会触发依赖收集
})

obj.foo++   // 期望这里执行后会触发上面收集的依赖,打印出对应的值

Dingtalk_20220817151308.jpg

2.2 分布讲解

2.2.1 创建data并添加proxy拦截
const data = {
    foo: 1,
    bar: 2
}

const obj = new Proxy(data, {
    get(target, key, receiver) {
        track(target, key)  // 收集依赖
        return target[key]
    },
    set(target, key, newValue, receiver) {
        target[key] = newValue
        trigger(target, key)    // 取出相关依赖执行
        return true
    }
})

2.2.2 创建effect副作用函数
function effect(fn) {
    const effectFn = () => {
        cleanUp(effectFn)       // 执行副作用函数前要清除相关依赖,依赖在deps中,后面触发get又会收集的
        activeEffect = effectFn     // 将副作用函数用activeEffect记录下来,后面收集和执行的时候用
        effectStack.push(effectFn)  // 用effectStack套一层是为了防止effect嵌套导致外层数据丢失
        fn()        // 执行一次函数,这里会触发get拦截
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    effectFn()
}

effect(() => {
    console.log('efffect run')
    console.log(obj.foo)    // 这里会触发get拦截依赖收集
})

obj.foo++   // 期望这里执行后会触发上面收集的依赖
2.2.3 依赖收集track

建立数据和副作用函数之间的联系

image.png

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)
    activeEffect.deps.push(deps)    // key和effect之间建立双向联系,方便在取出副作用函数的时候清除相关依赖
}
2.2.4 数据改变取出依赖执行trigger
function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)    // effects就是要执行的副作用函数集合,是个Set
    const effectToRun = new Set()
    effects && effects.forEach(effectFn => {   // 执行前过滤一下,防止get的同时set(例如++操作)导致的死循环
        if (activeEffect !== effectFn) {
            effectToRun.add(effectFn)
        }
    })
    effectToRun && effectToRun.forEach(effectFn => {
        effectFn()
    })
}
2.2.5 清除依赖函数
function cleanUp(effectFn) {
    for (let i = 0, len = effectFn.deps.length; i < len; i++) {  // 依赖在deps中
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0    // 清除完依赖后将他置空
}

到这里就可以运行了,可看到先打印出obj.foo的值为1,obj.foo改变之后又触发了effect执行,打印出obj.foo的值为2 Dingtalk_20220817151308.jpg

2.2.6 副作用函数添加options,实现computed

现在的effect函数写的比较死,不够灵活,副作用函数应该是能够自定义执行方式的,因此先给,effect添加第二个参数options自定义配置。在执行的时候使用自定义配置

function effect(fn, options = {}) {
    const effectFn = () => {
        cleanUp(effectFn)       // 执行副作用函数前要清除相关依赖,依赖在deps中,后面触发get又会收集的
        activeEffect = effectFn     // 将副作用函数用activeEffect记录下来,后面收集和执行的时候用
        effectStack.push(effectFn)  // 用effectStack套一层是为了防止effect嵌套导致外层数据丢失
        const res = fn()        // 执行一次函数,这里会触发get拦截
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res  // 新增,注意这里要把值返回,不然computed拉拿不到值
        
    }
    effectFn.deps = []
    effectFn.options = options  // 将自定义的配置保存起来
    if(options.lazy) {  // 新增,如果有lazy就返回函数后面执行,增强可控性,可利用他实现watch
        return effectFn
    }
    effectFn()
}
function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)    // effects就是要执行的副作用函数集合,是个Set
    const effectToRun = new Set()
    effects && effects.forEach(effectFn => {   // 执行前过滤一下,防止get的同时set(例如++操作)导致的死循环
        if (activeEffect !== effectFn) {
            effectToRun.add(effectFn)
        }
    })
    effectToRun && effectToRun.forEach(effectFn => {
        if (effectFn.options.scheduler) {   // 有自定义调度器就用自定义的调度器
            effectFn.options.scheduler(effectFn)
        } else {
            effectFn()
        }
    })
}

期望watch实现这样效果

const o = computed(() => {
    return obj.foo + obj.bar
})

console.log(o.value)    // 期望输出3

effect(() => { 
    console.log(o.value) 
})

obj.foo++   // 自动执行副作用函数,期望输出4

下面实现watch。

  • watch里面显示利用effect的options的lazy配置,获取到未执行的副作用函数,后面利用他来获取副作用函数的值,也就是计算属性的值;
  • 利用options的调度器,在数据获取的时候为其添加响应式,保证watch的返回的值也是响应式的
function computed(getter) {
    let value,  // 记录值
        dirty = true    // 是否需要重新计算
    const effectFn = effect(getter, {
        lazy: true,
        scheduler: () => {
            dirty = true   // 副作用函数重新触发了,证明值改变了,需要重新计算
            trigger(obj, 'value')   // 计算属性也是响应式的
        }
    })
    const obj = {
        get value() {
            if (dirty) {
                value = effectFn()
                dirty = false
            }
            track(obj, 'value') // 要保证计算属性也是响应式的,因此需要执行track, 第二个参数为避免重复最好为Symbol类型
            return value
        }
    }
    value = effectFn()  // 初始化值
    return obj
}

最终结果,可以看到计算属性生效并且是响应式的

image.png

2.2.7 实现watch

期望像下面这样实现watch

watch(()=>{
  return obj.foo
}, (oldValue, newValue) => {
  console.log('oldValue', oldValue, 'newValue', newValue)
})

obj.foo++
obj.foo++
obj.foo++
obj.foo++   // 期望在obj.foo修改后能触发watch的回调

实现方式和computed差不多,但是会增加回调函数

function watch(source, cb) {
  let getter,
    oldValue,
    newValue // 增加了newValue,用来给回调
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = traverse(source)
  }

  const effectFn = effect(() => {
    return getter()
  }, {
    lazy: true,
    scheduler: () => {
      newValue = effectFn()
      cb(oldValue, newValue)
      oldValue = newValue
    }
  })

  oldValue = effectFn()
}

image.png

后续计划

1. 副作用函数添加options配置,利用他实现computed。

2. 进一步实现watch