vue3.0响应式原理

258 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、什么是响应式

1. 你所理解的响应式是什么?

比如有一个update()函数,函数体是 A2 = A0 + A1,当A0或者A1变化的时候,结果A2也能随着改变。

let A2

function update() {
  A2 = A0 + A1
}
  • 这个 update() 函数会产生一个副作用,或者就简称为作用 (effect),因为它会更改程序里的状态。
  • A0 和 A1 被视为这个作用的依赖 (dependency),因为它们的值被用来执行这个作用。因此这次作用也可以被称作它的依赖的一个订阅者 (subscriber)。

我们需要一个魔法函数,能够在 A0 或 A1 (这两个依赖) 变化时调用 update() (产生作用)。

whenDepsChange(update)

这个 whenDepsChange() 函数有如下的任务:

  1. 当一个变量被读取时进行追踪。例如我们执行了表达式 A0 + A1 的计算,则 A0 和 A1 都被读取到了。
  2. 如果一个变量在当前运行的副作用中被读取了,就将该副作用设为此变量的一个订阅者。例如由于 A0 和 A1 在 update() 执行时被访问到了,则 update() 需要在第一次调用之后成为 A0 和 A1 的订阅者。
  3. 探测一个变量的变化。例如当我们给 A0 赋了一个新的值后,应该通知其所有订阅了的副作用重新执行。

二、 vue响应式原理

1. vue3与vue2相比,响应式原理不同在哪?

  1. vue2中响应式核心是Object.defineProperty
  • 初始化的时候就执行,如果没有使用这个属性也进行了响应式处理
  • 初始化的时候遍历所有的成员,通过defineProperty把对象的属性转换成getter和setter,如果子元素也是对象需要递归处理
  • vue2中动态新增属性只能通过vue.set()
  • vue2中监听不到删除属性,删除的话只能通过vue.delete()
  • vue2中监听不到数组的索引和length的属性

vue2 响应式源码解读可查看:vue2 响应式源码解析

  1. vue3使用proxy对象重写响应式
  • proxy的性能本来就比defineproperty好
  • 代理属性可以拦截对象的访问,赋值,删除等操作
  • 不需要初始化的时候遍历属性,如果有属性嵌套,只有访问的时候才会进行递归处理
  • 可以监听到动态新增属性
  • 可以监听到删除属性
  • 可以监听数组的索引和length的属性
  • 可以作为单独的模块使用

2. proxy

proxy,代理,可以理解为,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

  • Reflect反射的意思,es6中新增成员,将Object对象的一些明显属于语言内部的方法(比如Object.getPrototypeOf),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
const target = {
  foo: 'xxx',
  bar: 'yyy'
}

const proxy = new Proxy(target, {
  // receiver代表当前的proxy对象,或者继承proxy的对象
  get(target, key, receiver) {
    // return target[key]
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    // target[key] = value
    return Reflect.set(target, key, value, receiver)
  },
  deleteProperty(target, key) {
    // delete target[key]
    return Reflect.deleteProperty(target, key)
  }
})

proxy.foo = 'zzz';
delete proxy.foo;
  • 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)

Reflect中没有receiver时,this指向obj,此时结果:

image.png

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)
  }
})
console.log(proxy.foo)

Reflect中加上receiver后,this指向代理对象,此时结果:

image.png

3. 主要实现:

  • reactive/toRefs/ref/computed
  • watch的底层:effect
  • 收集依赖:tract
  • 触发更新:trigger reactive等composition API的使用:vue.js3.0 Composition API

4. reactive

  • 接受一个参数,判断参数是否是对象
  • 创建拦截器对象handler,设置get/set/deleteProperty
  • 返回proxy对象
// 判断是否是对象
const isObject = (obj) => {
  return typeof obj === 'object' && obj !== null
}

// 判断target是否是对象,是对象进行递归
const convert = (target) => {
  return isObject(target) ? reactive(target) : target
}

// 判断target中是否有key属性
const hasOwn = (target, key) => {
  return Object.prototype.hasOwnProperty.call(target, key)
}

export function reactive (target) {
  // 判断是否为对象,不是对象返回
  if (!isObject(target)) {
    return target
  }

  // handler对象,包含get,set,deleteProperty
  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key);
      const result = Reflect.get(target, key, receiver);
      return convert(result);
    },
    set(target, key, value, receiver) {
      // 获取旧值,与新值进行比较
      const oldValue = Reflect.get(target, key, receiver);
      let result = true;
      if(oldValue !== value) {
        result = Reflect.set(target, key, value, receiver);
        // 触发更新
        trigger(target, key)

      }
      return result;
    },
    deleteProperty(target, key) {
      // 判断当前target中是否有自身属性key,把key删除后再触发更新
      const hadkey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key);
      // 如果有自身属性,并且删除成功true
      if (hadkey && result) {
        // 触发更新
        trigger(target, key)
      }
      return result;
    }
  }
  
  // 返回一个proxy对象
  return new Proxy(target, handler)
}

5. 收集依赖track过程

1. 检查当前是否有正在运行的副作用。如果有,会查找到一个存储了所有追踪了该属性的订阅者的 Set,然后将当前这个副作用作为新订阅者添加到该 Set 中。

2. 副作用订阅将被存储在一个全局的 WeakMap<target, Map<key, Set<effect>>> 数据结构中

  • targetMap-new WeakMap():记录目标对象和字典(也就是中间的map()),使用的是new WeakMap(),也就是弱引用map,key是目标对象,当目标对象失去引用后可以销毁;value值是depsMap;
    • WeakMaps 保持了对键名所引用的对象的弱引用,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
  • depsMap-new Map():是一个字典,类型是Map(),key是目标对象的属性名称,value是集合
  • dep-new Set():Set元素不会重复,存储的是effect函数

image.png

  • 收集依赖tract,先根据targetMap对象找到depsMap,如果没有找到,给当前对象创建depsMap添加到targetMap中;
  • 如果找到了,根据当前使用的属性找到dep,dep存储的是effect,函数如果没有找到,为当前属性创建dep存储到depsMap中;
  • 如果找到,将当前的effect函数存储到dep集合中
  • 在reactive的get中收集依赖
// activeEffect要收集的依赖
let activeEffect = null;
export function effect (callback) {
  activeEffect = callback;
  callback(); // 访问响应式对象属性,去收集依赖
  activeEffect = null;
}

let targetMap = new WeakMap();
// track传入两个值,目标对象,属性
export function track (target, key) {
  // 没有可收集的依赖,返回
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  // 不存在depsMap,创建,存入到targetMap中
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  // 不存在dep,创建,存入到depsMap中
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(activeEffect);
}

6. 触发更新trigger过程

查找这个属性的所有订阅副作用,然后去循环执行他们

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

7. 验证reactive,effect

<script type="module">
  import {reactive, effect} from './reactive.js'
  // import {reactive, effect} from './node_modules/vue/dist/vue.esm-browser.js'
  const product = reactive({
    name: 'xiaomi',
    price: 3000,
    count: 3
  })
  let total = 0;
  effect(() => {
    total = product.price * product.count
  })
  console.log(total) // 9000

  product.price = 2000
  console.log(total) // 6000

  product.count = 1;
  console.log(total) // 2000

</script>
  • reactive传入的是一个对象,不能是基本数据类型;返回了一个proxy实例;reactive响应式进行深层递归,如果展开属性还是引用类型,还会进行深层递归,否则不再递归
  • effect函数用于定义副作用,参数是副作用函数;当响应式数据变化后,会导致副作用函数重新执行

8. ref

ref与reactive的区别:

  • ref可以把基本类型数据转为响应式对象(处理基本数据类型,变为引用类型)
  • ref返回的对象,重新赋值成对象也是响应式的
  • reactive返回的对象,重新赋值丢失响应式
  • reactive返回的对象不能解构,解构的话需要使用toRefs
export function ref (raw) {
  // 判断raw,是否是ref创建的对象,如果是的话直接返回
  if (isObject(raw) && r.__v_isRef) {
    return;
  }
  // 判断是否为对象,是对象使用reactive创建
  let value = convert(raw)
  const r = {
    // 标识是否是ref
    __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;
}

验证ref:

<script type="module">
  import {reactive, effect, ref} from './reactive.js'
  // import {reactive, effect} from './node_modules/vue/dist/vue.esm-browser.js'
  const price = ref(3000);
  const count = ref(3);
  let total = 0;
  effect(() => {
    total = price.value * count.value
  })
  console.log(total) // 9000

  price.value = 2000
  console.log(total) // 6000

  count.value = 1;
  console.log(total) // 2000

</script>

9. toRefs

toRefs对传入的reactive对象的每个属性变为响应式,进行解构

  • toRefs需要传入一个代理对象,否则会报错;
  • 然后toRefs内部创建一个新的对象来,遍历传入对象的所有属性,将属性值转为响应式对象,然后挂载到新创建的对象,最后将对象返回;
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];
      // 不用收集依赖,因为proxy是响应式的,会进行收集依赖
    },
    set value (newValue) {
      proxy[key] = newValue;
    }
  }
  return r;
}

验证toRefs:

<script type="module">
  import {reactive, effect, toRefs} from './reactive.js'
  // import {reactive, effect} from './node_modules/vue/dist/vue.esm-browser.js'

  function useProduct() {
    const product = reactive({
      name: 'xiaomi',
      price: 3000,
      count: 3
    })
    return toRefs(product);
  }
  const { price, count } = useProduct();

  let total = 0;
  effect(() => {
    total = price.value * count.value
  })
  console.log(total)

  price.value = 2000
  console.log(total)

  count.value = 1;
  console.log(total)

</script>

10. computed

computed简化模板中的代码,缓存计算结果,当数据发生变化时重新计算。

  • 传入带有返回值的函数
  • 返回具有ref属性的对象
export function computed (getter) {
  // 返回具有ref属性的对象
  const result = ref();
  effect(() => {
    result.value = getter()
  })
  return result;
}

验证computed:

<script type="module">
  import {reactive, computed} from './reactive.js'
  // import {reactive, effect} from './node_modules/vue/dist/vue.esm-browser.js'
  const product = reactive({
    name: 'xiaomi',
    price: 3000,
    count: 3
  })
  let total = computed(() => {
    return product.price * product.count
  })
  console.log(total.value)

  product.price = 2000
  console.log(total.value)

  product.count = 1;
  console.log(total.value)

</script>

三、vue响应式系统是基于运行时

Vue 的响应式系统基本是基于运行时的。追踪和触发都是在浏览器中运行时进行的。运行时响应性的优点是,它可以在没有构建步骤的情况下工作,而且边界情况较少。另一方面,这使得它受到了 JavaScript 语法的制约,导致需要使用一些例如 Vue ref 这样的值的容器。

四、响应式调试

1. 组件内调试

在一个组件渲染时使用 onRenderTracked 生命周期钩子来调试查看哪些依赖正在被使用,或是用 onRenderTriggered 来确定哪个依赖正在触发更新

<script setup>
import { onRenderTracked, onRenderTriggered } from 'vue'

onRenderTracked((event) => {
  debugger
})

onRenderTriggered((event) => {
  debugger
})
</script>

2. 通过计算属性调试

可以向 computed() 传入第二个参数,是一个包含了 onTrack 和 onTrigger 两个回调函数的对象:计算属性和侦听器的 onTrack 和 onTrigger 选项仅会在开发模式下工作

  • onTrack 将在响应属性或引用作为依赖项被跟踪时被调用。
  • onTrigger 将在侦听器回调被依赖项的变更触发时被调用。
const plusOne = computed(() => count.value + 1, {
  onTrack(e) {
    // 当 count.value 被追踪为依赖时触发
    debugger
  },
  onTrigger(e) {
    // 当 count.value 被更改时触发
    debugger
  }
})

// 访问 plusOne,会触发 onTrack
console.log(plusOne.value)

// 更改 count.value,应该会触发 onTrigger
count.value++

3. 通过侦听器调试

watch(source, callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})

watchEffect(callback, {
  onTrack(e) {
    debugger
  },
  onTrigger(e) {
    debugger
  }
})