美文网首页
Vue.js3.0 响应式系统原理

Vue.js3.0 响应式系统原理

作者: 翔子丶 | 来源:发表于2021-04-26 19:53 被阅读0次
    Vue.js响应式原理回顾
    • Proxy对象实现属性监听
    • 多层属性嵌套,在访问属性过程中处理下一级属性
    • 默认监听动态添加的属性
    • 默认监听属性的删除操作
    • 默认监听数组索引和 length属性
    • 可以作为单独的模块使用
    核心方法
    • reactive/ref/toRefs/computed
    • effect watch/watchEffect是vue3 runtime.core中实现的,内部使用effect底层函数
    • track 收集依赖
    • trigger 触发更新
    响应式系统原理——Proxy

    ProxyReflect是ES6 为了操作对象而提供的新 API

    proxy中有两个需要注意的地方:

    • set 和 deleteProperty 中需要返回布尔类型的值

      <script>
            'use strict'
            // set 和 deleteProperty 中需要返回布尔类型的值
            // 在严格模式下,如果返回 false 的话会出现 Type Error 的异常
            const target = {
              foo: 'xxx',
              bar: 'yyy'
            }
            // Reflect.getPrototypeOf()相当于Object.getPrototypeOf()
            const proxy = new Proxy(target, {
              // receiver代表当前的的Proxy对象或者继承Proxy的对象
              get (target, key, receiver) {
                // return target[key]
                // Reflect反射,代码运行期间获取对象中的成员
                return Reflect.get(target, key, receiver)
              },
              set (target, key, value, receiver) {
                // target[key] = value
                // Reflect.set设置成功返回true 设置失败返回false
                return Reflect.set(target, key, value, receiver)
              },
              deleteProperty (target, key) {
                // delete target[key]
                return Reflect.deleteProperty(target, key)
              }
            })
      
            proxy.foo = 'zzz'
            // delete proxy.foo
      </script>
      

      如果set和deleteProperty返回false时,页面会报错

      image-20210414080553080.png
    • Proxy 和 Reflect 中使用的 receiver指向

      // Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
      // Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
      
      const obj = {
          get foo() {
              console.log(this)
              return this.bar
          },
      }
      
      const proxy = new Proxy(obj, {
          get(target, key, receiver) {
              if (key === 'bar') {
                  return 'value - bar'
              }
              return Reflect.get(target, key, receiver)
          },
      })
      console.log(proxy.foo)
      

      不传递receiver时,可以看到this返回的是obj对象,proxy.foo返回undefined

      image-20210414080743227.png

      当传递了receiver时,this指向Proxy对象

      image-20210414080825068.png
    响应式系统原理——reactive
    • 接收一个参数,判断这参数是否是对象,不是直接返回,只能转换对象为响应式对象

    • 创建拦截器对象handler,设置get/set/deleteProperty

    • 返回Proxy 对象

      // reactivily/index.js
      const isObject = (val) => val !== null && typeof val === 'object'
      export function reactive(target) {
        if (!isObject(target)) return
      
        const handler = {
          get(target, key, receiver) {
            console.log('get', key, target)
          },
          set(target, key, value, receiver) {
            console.log('set', key, value)
            return value
          },
          deleteProperty(target, key) {
            console.log('delete', key)
            return target
          },
        }
      
        return new Proxy(target, handler)
      }
      

      测试set和delete,结果如下

      image-20210414082410979.png

    reactive实现思路:

    1. 定义handler对象,用于Proxy的第二个参数(拦截器对象)
    2. get方法实现
      • 收集依赖
      • 返回target中对于key的value
      • 如果value为对象,需要再次转为响应式对象
    3. set方法中实现
      • 获取key属性的值,判断新旧值是否相同,相同时返回true
      • 不同时,先将target中的key对应的value修改为新值
      • 最后触发更新
    4. deleteProperty方法实现
      • 首先判断target本身是否存在key
      • 删除target中的key,并返回成功或失败
      • 删除成功,触发更新

    代码示例:

    const isObject = (val) => val !== null && typeof val === 'object'
    const convert = (val) => (isObject(val) ? reactive(val) : val)
    const hasOwnProperty = Object.prototype.hasOwnProperty
    const hasOwn = (target, key) => hasOwnProperty.call(target, key)
    
    export function reactive(target) {
      if (!isObject(target)) return
    
      const handler = {
        get(target, key, receiver) {
          // 收集依赖
          const value = Reflect.get(target, key, receiver)
          return convert(value)
        },
        set(target, key, value, receiver) {
          const oldValue = Reflect.get(target, key, receiver)
          let result = true
          if (oldValue !== value) {
            let result = Reflect.set(target, key, value, receiver)
            // 触发更新
          }
          return result
        },
        deleteProperty(target, key) {
          const hasKey = hasOwn(target, key)
          const result = Reflect.deleteProperty(target, key)
          if (hasKey && result) {
            // 触发更新
          }
          return result
        },
      }
    
      return new Proxy(target, handler)
    }
    

    测试,创建html文件进行测试:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <script type="module">
        import { reactive } from './reactivity/index.js'
        const obj = reactive({
          name: 'zs',
          age: 18
        })
        obj.name = 'lisi'
        delete obj.age
        console.log(obj)
      </script>
    </body>
    </html>
    
    响应式系统原理——收集依赖
    image-20210415080133182.png
    image-20210414082624298.png
    • 依赖收集过程中会创建3个集合,分别是targetMap、depsMap和dep
    • targetMap作用是记录目标对象和一个字典(depsMap),使用WeakMap弱引用,当目标对象失去引用之后,可以销毁
    • targetMap的值是depsMap,depsMap的key是目标对象的属性名称,值是一个set集合dep
    • dep中存储的是effect函数,因为可以多次调用一个effect,在effect中访问同一个属性,这时该属性会收集多次依赖,对应多个effect函数
    • 通过这种结构,可以存储目标对象,目标对象属性,以及属性对应的effect函数
    • 一个属性可能对应多个函数,当触发更新时,在这个结构中根据目标对象属性找到effect函数然后执行
    • 收集依赖的track函数内部,首先根据当前targetMap对象找到depsMap,如果没找到要给当前对象创建一个depsMap,并添加到targetMap中,如果找到了再根据当前使用的属性在depsMap找到对应的dep,dep中存储的是effect函数,如果没有找到时,为当前属性创建对应的dep集合,并且存储到depsMap中,如果找到当前属性对应的dep集合,就把当前的effect函数存储到集合中

    effect方法实现

    实现思路:

    1. effect接收函数作为参数
    2. 执行函数并返回响应式对象去收集依赖,收集依赖过程中将callback存储起来,需要在后面的track函数中能够访问到这里的callback
    3. 依赖收集完毕设置activeEffect为null

    代码实现:

    let activeEffect = null
    export function effect (callback) {
      activeEffect = callback
      callback() // 访问响应式对象属性,去收集依赖
      activeEffect = null
    }
    

    track方法实现

    实现思路:

    1. track接收两个参数,目标对象target和需要跟踪的属性key
    2. 内部需要将target存储到targetMap中,targetMap定义在外面,除了track使用外,trigger函数也要使用
    3. activeEffect不存在直接返回,否则需要在targetMap中根据当前target找depsMap
    4. 判断是否找到depsMap,因为target可能还没有收集依赖
    5. 未找到,为当前target创建depsMap去存储对应的键和dep对象,并添加到targetMap中
    6. 根据属性查找对应的dep对象,dep是个集合,存储effect函数
    7. 判断是否存在,未找到时创建新的dep集合并添加到depsMap中
    8. 将effect函数添加到dep集合中
    9. 在收集依赖的get中调用这个函数

    代码实现:

    let targetMap = new WeakMap()
    export function track(target, key) {
      if (!activeEffect) return
      let depsMap = targetMap.get(target)
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
      }
      let dep = depsMap.get(key)
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      dep.add(activeEffect)
    }
    

    此时,整个依赖收集过程已经完成

    trigger方法实现

    依赖收集完成后需要触发更新

    实现思路:

    1. 参数target和key
    2. 根据target在targetMap中找到depsMap
    3. 未找到时,直接返回
    4. 再根据key找对应的dep集合,effect函数
    5. 如果dep有值,遍历dep集合执行每一个effect函数
    6. 在set和deleteProperty中触发更新

    代码实现:

    export function trigger(target, key) {
      const depsMap = targetMap.get(target)
      if (!depsMap) return
      const dep = depsMap.get(key)
      if (dep) {
        dep.forEach((effect) => {
          effect()
        })
      }
    }
    

    依赖收集和触发更新代码完成,创建html文件进行测试

    <body>
      <script type="module">
        import { reactive, effect } from './reactivity/index.js'
    
        const product = reactive({
          name: 'iPhone',
          price: 5000,
          count: 3
        })
        let total = 0 
        effect(() => {
          total = product.price * product.count
        })
        console.log(total)
    
        product.price = 4000
        console.log(total)
    
        product.count = 1
        console.log(total)
    
      </script>
    </body>
    

    打开浏览器控制台,可以看到输出结果如下

    image-20210416084313137.png
    响应式系统原理——ref

    ref vs reactive

    • ref可以把基本数据类型数据,转成响应式对象
    • ref返回的对象,重新赋值成对象也是响应式的
    • reactive返回的对象,重新赋值丢失响应式
    • reactive返回的对象不可以解构

    实现原理:

    1. 判断 raw 是否是ref 创建的对象,如果是的话直接返回
    2. 判断 raw是否是对象,如果是对象调用reactive创建响应式对象,否则返回原始值
    3. 创建ref对象并返回,标识是否是ref对象,这个对象只有value属性,并且这个value属性具有set和get
    4. get中调用track收集依赖,收集依赖的对象是刚创建的r对象,属性是value,也就是当访问对象中的值,返回的是内部的变量value
    5. set中判断新旧值是否相等,不相等时将新值存储到raw中,并调用convert处理raw,最终把结果存储到value中,如果给value重新赋值为一个对象依然是响应式的,当raw是对象时,convert里调用reactive转换为响应式对象
    6. 最后触发更新

    代码实现:

    export function ref(raw) {
      // 判断 raw 是否是ref 创建的对象,如果是的话直接返回
      if (isObject(raw) && raw.__v_isRef) {
        return
      }
      let value = convert(raw)
      const r = {
        __v_isRef: true,
        get value() {
          track(r, 'value')
          return value
        },
        set value(newValue) {
          if (newValue !== value) {
            raw = newValue
            value = convert(raw)
            trigger(r, 'value')
          }
        },
      }
      return r
    }
    

    创建html文件进行测试:

    <body>
      <script type="module">
        import { reactive, effect, ref } from './reactivity/index.js'
    
        const price = ref(5000)
        const count = ref(3)
       
        let total = 0 
        effect(() => {
          total = price.value * count.value
        })
        console.log(total)
    
        price.value = 4000
        console.log(total)
    
        count.value = 1
        console.log(total)
    
      </script>
    </body>
    

    打开控制台可以看到输出结果和上面的相同

    响应式系统原理——toRefs

    实现思路:

    1. 接收参数proxy,判断参数是否为reactive创建的对象,如果不是发出警告
    2. 判断传入参数,如果是数组创建长度是length的数组,否则返回空对象,因为传入的proxy可能是响应式数组或响应式对象
    3. 接着遍历proxy对象的所有属性,如果是数组遍历索引,将每一个属性都转换为类似ref返回的对象
    4. 创建toProxyRef函数,接收proxy和key,创建对象并最终返回对象(类似ref返回的对象)
    5. 创建标识属性__v_isRef,这里的get中不需要收集依赖,因为这里访问的是响应式对象,当访问属性时,内部的getter回去收集依赖,set不需要触发更新,调用代理对象内部的set触发更新
    6. 调用toProxyRef,将所有属性转换并存储到ret中
    7. toRefs将reactive返回的对象的所有属性都转换成一个对象,所以当对响应式对象进行解构的时候,解构出的每一个属性都是对象,而对象是引用传递,所以解构的属性依然是响应式的

    代码实现:

    export function toRefs(proxy) {
      const ret = proxy instanceof Array ? new Array(proxy.length) : {}
    
      for (const key in proxy) {
        ret[key] = toProxyRef(proxy, key)
      }
    
      return ret
    }
    
    function toProxyRef(proxy, key) {
      const r = {
        __v_isRef: true,
        get value() {
          return proxy[key]
        },
        set value(newValue) {
          proxy[key] = newValue
        },
      }
      return r
    }
    

    创建html进行测试:

    <body>
      <script type="module">
        import { reactive, effect, toRefs } from './reactivity/index.js'
    
        function useProduct () {
          const product = reactive({
            name: 'iPhone',
            price: 5000,
            count: 3
          })
          
          return toRefs(product)
        }
    
        const { price, count } = useProduct()
    
    
        let total = 0 
        effect(() => {
          total = price.value * count.value
        })
        console.log(total)
    
        price.value = 4000
        console.log(total)
    
        count.value = 1
        console.log(total)
    
      </script>
    </body>
    

    打开控制台可以看到输出结果和上面的相同

    响应式系统原理——computed

    实现原理:

    1. 接收一个有返回值的函数作为参数,函数的返回值就是计算属性的值
    2. 监听这个函数内部的响应式数据变化,最后将函数执行结果返回
    3. computed内部会通过effect监听getter内部的响应式数据变化,因为在effect中执行getter访问响应式数据的getter会去收集依赖,当数据变化后,回去重新执行effect函数将getter结果在存储到result中

    代码实现:

    export function computed(getter) {
      const result = ref()
    
      effect(() => (result.value = getter()))
    
      return result
    }
    

    创建html文件进行测试:

    <body>
      <script type="module">
        import { reactive, effect, computed } from './reactivity/index.js'
    
        const product = reactive({
          name: 'iPhone',
          price: 5000,
          count: 3
        })
        let total = computed(() => {
          return product.price * product.count
        })
       
        console.log(total.value)
    
        product.price = 4000
        console.log(total.value)
    
        product.count = 1
        console.log(total.value)
    
      </script>
    </body>
    

    打开控制台可以看到输出结果和上面的相同

    github地址

    相关文章

      网友评论

          本文标题:Vue.js3.0 响应式系统原理

          本文链接:https://www.haomeiwen.com/subject/wboelltx.html