Vue3 深入响应式原理

1,274 阅读4分钟

一、认识 Proxy

new Proxy(target, handler)
target Proxy 会对 target 对象进行包装。它可以是任何类型的对象,包括内置的数组,函数甚至是另一个代理对象。
handler 它是一个对象,它的属性提供了某些操作发生时所对应的处理函数。

tips: 完整使用教程可移步: 阮一峰ES6入门教程(Proxy 和 Reflect)

二、尝鲜

  • Demo:
    function reactive(obj) {
        return new Proxy(obj, {
            get(target, key, receiver) {
                const res = Reflect.get(target, key, receiver)
                console.log('获取', key, res)
                return res
            },
            set(target, key, value, receiver) {
                const res = Reflect.set(target, key, value, receiver)
                console.log('设置', key, res)
                return res
            },
            deleteProperty(target, key) {
                const res = Reflect.deleteProperty(target, key)
                console.log('删除', key, res)
                return res
            }
        })
    }
    
    const obj = { foo: 'foo', bar: { a: 1 }, arr: [1, 2, 3] }
    const state = reactive(obj)
    
    // 获取
    state.foo
    
    // 设置
    state.foo = 'foooooooo'
    
    // 删除
    delete state.foo
    
    // 添加属性
    state.dong = 'dong'
    state.dong
    
    // 嵌套对象
    state.bar.a = 10
    
    // 数组
    state.arr
    state.arr.push(5)
    
  • 结论:除了嵌套对象无法捕获,其他的操作都能拦截
  • 递归实现深度响应:
    const isObject = val => val !== null && typeof val === 'object'
    function reactive(obj) {
        if (!isObject(obj)) return obj
        return new Proxy(obj, {
        	get(target, key, receiver) {
                const res = Reflect.get(target, key, receiver)
                console.log('获取', key, res)
                return isObject(res) ? reactive(res) : res
           },
           ...
       })
    }
    ...
    

三、缓存代理对象,避免重复代理

  1. 存在重复代理的两种情况:

    const state = reactive(obj)
    const state2 = reactive(obj)

    const state = reactive(reactive(obj))

  2. 淦就完事了

    // 避免重复代理
    const toProxy = new WeakMap() // 形如{ obj: observed }
    const toRaw = new WeakMap() // 形如{ observed: obj }
    
    const observed = new Proxy(obj, {...})
    // 缓存
    toProxy.set(obj, observed)
    toRaw.set(observed, obj)
    return observed
    

四、响应式探究

  • 这里我们实现 Vue watch 的机制,computed 类似。
  • Vue3 使用方式: effect(() => state.xxx, (val, oldVal) => {}),第一个回调函数指明需要 watch 的字段,第二个为需要处理的回调函数。
  • 思考:①要想监听某个对象的变化,是不是只需要在 get() 函数里面建立字段的映射关系就行了呢?②要想回调函数调用什么时机会触发呢?是不是在 set() 函数里面做就行了?有思路了就直接开干!
  • 还得考虑一个问题就是我们用什么样的数据结构存储 代理对象 - 属性 - 回调函数 的关系呢?考虑到这几项都可能多组,这里采用 new WeakMap()、new Map()、new Set() 的结构。
  • 从调用出发实现 effect(getField, callback),上代码:
    function effect(getField, callback) {
        try {
            effectStack.push(callback)
            getField() // 此处会触发 get 操作
        } catch (error) {
          	console.error(error)
        } finally {
          	effectStack.pop()
        }
    }
    
  • observed 函数改造
     const observed = new Proxy(obj, {
       get(target, key, receiver) {
         const res = Reflect.get(target, key, receiver)
         // console.log('获取', key, res)
         track(target, key) // 此处建立数据的绑定关系
         return isObject(res) ? reactive(res) : res
       },
       set(target, key, value, receiver) {
         const oldVal = Reflect.get(target, key, receiver)
         const res = Reflect.set(target, key, value, receiver)
         // console.log('设置', oldVal, res)
         trigger(target, key, value, oldVal) // 此处触发对应的回调函数
         return res
       },
       deleteProperty(target, key) {
         const oldVal = Reflect.get(target, key)
         const res = Reflect.deleteProperty(target, key)
         // console.log('删除', key, res)
         trigger(target, key, undefined, oldVal) // 此处触发对应的回调函数
         return res
       }
    })
    
  • 接下来实现 track(target, key)trigger(target, key, value, oldVal) 两个函数,上代码:
    function track(target, key) {
      const effect = effectStack[effectStack.length - 1]
      if (effect) {
        let depsMap = targetMap.get(target)
        if (!depsMap) {
          depsMap = new Map()
          targetMap.set(target, depsMap)
        }
    
        let deps = depsMap.get(key)
        if (!deps) {
          deps = new Set()
          depsMap.set(key, deps)
        }
    
        deps.add(effect)
      }
    }
    
    function trigger(target, key, val, oldVal) {
      const depsMap = targetMap.get(target)
      if (depsMap) {
        const deps = depsMap.get(key)
        if (deps) {
          deps.forEach(effect => {
            effect(val, oldVal)
          })
        }
      }
    }
    
  • 加上测试代码:
     effect(() => state.foo, (val, oldVal) => {
       console.log(val, oldVal)
     })
    
     effect(() => state.bar.a, (val, oldVal) => {
       console.log(val, oldVal)
     })
    
  • 恭喜你,测试通过了!( ̄▽ ̄)~*

五、完整代码

  const targetMap = new WeakMap() // {target, {key: []}}
  const effectStack = []

  const isObject = val => val !== null && typeof val === 'object'

  // 避免重复代理
  const toProxy = new WeakMap() // 形如{ obj: observed }
  const toRaw = new WeakMap() // 形如{ observed: obj }

  function reactive(obj) {
    if (!isObject(obj)) return obj

    // 避免重复执行代理
    if (toProxy.has(obj)) {
      return toProxy.get(obj)
    }

    // 避免代理代理过的对象
    if (toRaw.has(obj)) {
      return obj
    }

    const observed = new Proxy(obj, {
      get(target, key, receiver) {
        const res = Reflect.get(target, key, receiver)
        // console.log('获取', key, res)
        track(target, key)
        return isObject(res) ? reactive(res) : res
      },
      set(target, key, value, receiver) {
        const oldVal = Reflect.get(target, key, receiver)
        const res = Reflect.set(target, key, value, receiver)
        // console.log('设置', oldVal, res)
        trigger(target, key, value, oldVal)
        return res
      },
      deleteProperty(target, key) {
        const oldVal = Reflect.get(target, key)
        const res = Reflect.deleteProperty(target, key)
        // console.log('删除', key, res)
        trigger(target, key, undefined, oldVal)
        return res
      }
    })

    // 缓存
    toProxy.set(obj, observed)
    toRaw.set(observed, obj)

    return observed
  }

  function effect(getField, callback) {
    try {
      effectStack.push(callback)
      getField()
    } catch (error) {
      console.error(error)
    } finally {
      effectStack.pop()
    }
  }

  // 建立映射关系
  function track(target, key) {
    const effect = effectStack[effectStack.length - 1]
    if (effect) {
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        depsMap = new Map()
        targetMap.set(target, depsMap)
      }

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

      deps.add(effect)
    }
  }

  // 触发
  function trigger(target, key, val, oldVal) {
    const depsMap = targetMap.get(target)
    if (depsMap) {
      const deps = depsMap.get(key)
      if (deps) {
        deps.forEach(effect => {
          effect(val, oldVal)
        })
      }
    }
  }

  const obj = { foo: 'foo', bar: { a: 1 }, arr: [1, 2, 3] }
  const state = reactive(obj)

  effect(() => state.foo, (val, oldVal) => {
    console.log(val, oldVal)
  })

  effect(() => state.bar.a, (val, oldVal) => {
    console.log(val, oldVal)
  })

  // 获取
  state.foo

  // 设置
  state.foo = 'foooooooo'

  // 删除
  delete state.foo

  // 添加属性
  state.dong = 'dong'
  state.dong

  // 嵌套对象
  state.bar.a = 10

  // 数组
  state.arr
  state.arr.push(5)

欢迎提出意见与建议,关注,收藏来一波! skr~