本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、什么是响应式
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() 函数有如下的任务:
- 当一个变量被读取时进行追踪。例如我们执行了表达式
A0 + A1的计算,则A0和A1都被读取到了。 - 如果一个变量在当前运行的副作用中被读取了,就将该副作用设为此变量的一个订阅者。例如由于
A0和A1在update()执行时被访问到了,则update()需要在第一次调用之后成为A0和A1的订阅者。 - 探测一个变量的变化。例如当我们给
A0赋了一个新的值后,应该通知其所有订阅了的副作用重新执行。
二、 vue响应式原理
1. vue3与vue2相比,响应式原理不同在哪?
- vue2中响应式核心是Object.defineProperty
- 初始化的时候就执行,如果没有使用这个属性也进行了响应式处理
- 初始化的时候遍历所有的成员,通过defineProperty把对象的属性转换成getter和setter,如果子元素也是对象需要递归处理
- vue2中动态新增属性只能通过vue.set()
- vue2中监听不到删除属性,删除的话只能通过vue.delete()
- vue2中监听不到数组的索引和length的属性
vue2 响应式源码解读可查看:vue2 响应式源码解析
- vue3使用proxy对象重写响应式
- proxy的性能本来就比defineproperty好
- 代理属性可以拦截对象的访问,赋值,删除等操作
- 不需要初始化的时候遍历属性,如果有属性嵌套,只有访问的时候才会进行递归处理
- 可以监听到动态新增属性
- 可以监听到删除属性
- 可以监听数组的索引和length的属性
- 可以作为单独的模块使用
2. proxy
proxy,代理,可以理解为,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
- Reflect反射的意思,es6中新增成员,将
Object对象的一些明显属于语言内部的方法(比如Object.getPrototypeOf),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在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,此时结果:
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指向代理对象,此时结果:
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函数
- 收集依赖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
}
})