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

2021-04-26  本文已影响0人  翔子丶
Vue.js响应式原理回顾
核心方法
响应式系统原理——Proxy

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

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

响应式系统原理——reactive

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

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

实现原理:

  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地址

上一篇下一篇

猜你喜欢

热点阅读