Skip to the content.

从一个最简单的响应式处理开始

(代码地址)

先定义一个对象和一个函数:

const obj = {
  name: '杰克',
}
function printName() {
  console.log(obj.name)
}
  1. 假如说要实现一个功能:当修改objname属性时,就打印一下修改后的属性值。 这个功能应该是非常好实现的,只需要再定义一个函数: js function printNameWhenUpdate(name) { obj.name = name printName() } 每次修改objname属性时调用这个函数就可以了,简单明了。
  2. 然后我们再来考虑一下稍微复杂一点点的需求:对于obj的每一个属性,当它们修改时,都打印一下修改后的新值。 对于这个需求在靠定义新的函数来达到目的就有点捉襟见肘了,因为: a. 对于obj存在的每一个属性都需要定义一个函数,需要定义太多函数了 b. 有新增属性时,无法对新增属性进行打印 所以这时可以用Proxy来拦截objset操作来达到目的
    const objProxy = new Proxy(obj, {
      set(target, key, value) {
        target[key] = value
        console.log(target[key]) // 每次修改或新增完属性打印值
        return true
      },
    })
    objProxy.newProp = 'new prop'
    
  3. 把功能再考虑得复杂一点:对于obj,只有他被用到的属性修改时,才打印修改后的新值 这个要实现起来也很容易,我们可以继续在 Proxy 中把 get 也拦截了

    const keysUsed = new Set() // 定义一个set来储存被使用过的key
    const objProxy = new Proxy(obj, {
      get(target, key) {
        keysUsed.add(key) // 使用过的key存起来
        return target[key]
      },
      set(target, key, value) {
        target[key] = value
        if (keysUsed.has(key)) {
          console.log(target[key]) // key被使用过就打印一下
        }
        return true
      },
    })
    
    // 定义一个函数,在这个函数中使用obj做一些事情
    function excute() {
      const fullName = objProxy.firstName + ' ' + objProxy.lastName
      console.log(fullName)
    }
    
    excute() // 执行excute就会触发objProxy的get,保存下来firstName和lastName两个key
    objProxy.lastName = 'Tom' // 会打印Tom
    objProxy.firstName = 'Jack' // 会打印Jack
    

    但是看上面的代码感觉有点怪怪的,修改属性后打印新的值这个操作好像并没有什么用,反倒是excute这个函数值得再执行一遍 => 当firstNamelastName改变时再打印一遍fullName

    我们暂且把 excute 这样的函数称之为副作用函数(虽然这个函数不满足副作用函数的定义)

    副作用函数的定义是:如果一个函数的执行会直接或间接影响到其他函数的执行,那么这个函数就叫做副作用函数。


要满足的上面的要求其实也很简单,只需要在 get 的时候把 excute 这样的函数收集一下,保存起来,然后在 set 成功的时候执行一遍就行了。 简单实现一下就是这个样子:

   function excute() {
     const fullName = objProxy.firstName + ' ' + objProxy.lastName
     console.log(fullName)
   }

   const effectSet = new Set() // 保存副作用函数的Set
   const objProxy = new Proxy(obj, {
     get(target, key) {
       if (key === 'firstName' || key === 'lastName') {
         effectSet.add(excute)
       }
       return target[key]
     },
     set(target, key, value) {
       target[key] = value
       effectSet.forEach((fn) => fn()) // 把副作用函数取出来执行一遍
       return true
     },
   })

不过这样的实现很不优雅:不够灵活,没有自动的收集不同keyeffecteffect函数的收集也是写死的,这里需要做到可以自动的收集任何key的任何effect。 为了达到这个目的,先来设计一下存储 effect 的数据结构:

  1. 存储 effect 的肯定还得是一个 Set,保证 effect 没有重复添加
  2. 每个 key 都应该保存这个 key 对应的所有 effect,所以 key 和所有 effectSet 需要用一个 Map 来存 结构就是
     depsMap
         key:   obj的每个属性
         value: obj每个属性的副作用函数Set

然后在来考虑怎么在 get 的时候拿到副作用函数并且存起来和怎么在修改属性值的时候执行副作用函数:

  1. 定义一个全局变量,在副作用函数执行之前,把副作用函数本身保存在这个全局变量
  2. 执行副作用函数,在 get 拦截中把全局的副作用函数存起来
  3. 有属性值修改时,触发了 set 拦截,这个时候把副作用函数取出来执行
   // 用来存储effect
   const depsMap = new Map()

   let activeEffect
   function effect(fn) {
     activeEffect = fn
     fn()
   }

   const objProxy = new Proxy(obj, {
     get(target, key) {
       if (activeEffect) {
         // 取出key对应的effectSet
         let effectSet = depsMap.get(key)
         // 不存在的话新建一个
         if (!effectSet) {
           effectSet = new Set()
           depsMap.set(key, effectSet)
         }
         // 加到set里面
         effectSet.add(activeEffect)
       }
       return target[key]
     },
     set(target, key, value) {
       target[key] = value
       // 把副作用函数取出来执行一遍
       const effectSet = depsMap.get(key)
       effectSet && effectSet.forEach((fn) => fn())
       return true
     },
   })

这样就比较完整的让 obj 变成响应式的了,不过还可以在更进一步:定义一个函数,接收一个对象作为参数,这个函数执行完成后对象变为响应式对象

   // 用来储存不同的响应式对象对应的depsMap
   // key为一个对象
   const targetEffectMap = new WeakMap()

   let activeEffect

   function reactive(obj) {
     return new Proxy(obj, {
       get(target, key) {
         if (activeEffect) {
           /** 新增的代码 --- start */
           // 取出target对应的depsMap
           let depsMap = targetEffectMap.get(target)
           if (!depsMap) {
             depsMap = new Map()
             targetEffectMap.set(target, depsMap)
           }
           /** 新增的代码 --- end */

           // 取出key对应的effectSet
           let effectSet = depsMap.get(key)
           // 不存在的话新建一个
           if (!effectSet) {
             effectSet = new Set()
             depsMap.set(key, effectSet)
           }
           // 加到set里面
           effectSet.add(activeEffect)
         }
         return target[key]
       },
       set(target, key, value) {
         target[key] = value
         /** 新增的代码 --- start */
         const depsMap = targetEffectMap.get(target)
         // 没有就不执行了
         if (!depsMap) return
         /** 新增的代码 --- end */

         // 把副作用函数取出来执行一遍
         const effectSet = depsMap.get(key)
         effectSet && effectSet.forEach((fn) => fn())
       },
     })
   }

再更进一步封装一下,把收集依赖和触发副作用函数再抽离出来

   function track(target, key) {
     if (activeEffect) {
       /** 新增的代码 --- start */
       // 取出target对应的depsMap
       let depsMap = targetEffectMap.get(target)
       if (!depsMap) {
         depsMap = new Map()
         targetEffectMap.set(target, depsMap)
       }
       /** 新增的代码 --- end */
       // 取出key对应的effectSet
       let effectSet = depsMap.get(key)
       // 不存在的话新建一个
       if (!effectSet) {
         effectSet = new Set()
         depsMap.set(key, effectSet)
       }
       // 加到set里面
       effectSet.add(activeEffect)
     }
   }
   function trigger(target, key) {
     /** 新增的代码 --- start */
     const depsMap = targetEffectMap.get(target)
     // 没有就不执行了
     if (!depsMap) return
     /** 新增的代码 --- end */

     // 把副作用函数取出来执行一遍
     const effectSet = depsMap.get(key)
     effectSet && effectSet.forEach((fn) => fn())
   }
   function reactive(obj) {
     return new Proxy(obj, {
       get(target, key) {
         track(target, key)
         return target[key]
       },
       set(target, key, value) {
         target[key] = value
         trigger(target, key)
       },
     })
   }