Vue2.x 响应式原理
Vue2.x 响应式:
- 实现原理
- 对象类型:通过 Object.defineProperty() 对属性的读取、修改进行拦截( 数据劫持 )
- 数组类型:通过重写数组方法,并作为拦截器挂载到数组对象与数组原型之间,来实现拦截。
- 存在问题
- 对于对象类型,直接操作对象新增属性或删除属性,界面不会更新,因为Vue无法监听到
- 对于数组类型,直接通过下标修改数组元素,界面不会更新。直接修改数组长度,也不会生效。Vue 同样无法监听到
- 解决办法:
- 通过 this.$set() 或 Vue.set() 来实现对于对象类型的属性新增。通过 this.$delete 来删除对象类型中的属性。
- 通过可以通过 this.$set() 或 Vue.set() 来实现对于数组类型的数组元素修改。也可以通过 调用拦截器中的数组方法来改变数组元素,例如:splice
Vue3.x的响应式
因为Vue3重新改写了底层响应式原理,那我们大胆猜测一下,Vue3 应该修复了 Vue2.x 版本的问题,也就是说 Vue3 能够直接监听到对于对象的新增属性或删除属性,同时还能直接通过数组下标来操作数组,或通过 length 属性直接改变数组长度,而不用依赖于 $set 方法、$delete 方法或 splice 方法。
新增或删除对象中的属性:
<template><button @click="addSex">点击新增 sex 属性</button><button @click="deleteName">点击删除 name 属性</button>
</template>let person = reactive({name: "al",
});function addSex() {person.sex = '男' // Proxy(Object){name:'al',sex:'男'}
}function deleteName() {delete person.name // Proxy(Object){sex:'男'}
}
新增或删除数组中的元素:由此可以证明,只要是能改变原数组的方法,在操作数组之后,都会被 Vue3 所监听到
let person = reactive({hobby:['吃饭','睡觉']
});// 新增第三个元素
function updateHobby() {person.hobby[2] = '打豆豆' // Proxy(Object) {hobby: ['吃饭', '睡觉']}
}//删除第一个元素
function deleteArr() {// 通过shift 删除第一个元素person.hobby.shift() // Proxy(Object) {hobby: ['睡觉', '打豆豆']}// 通过splice 删除第一个元素person.hobby.splice(0,1) // Proxy(Object) {hobby: ['睡觉', '打豆豆']}// 通过pop删除最后一个元素person.hobby.pop() // Proxy(Object) {hobby: ['吃饭','睡觉']}}
通过数组下标改变数组元素
let person = reactive({hobby:['吃饭','睡觉']
});//通过数组下标改变数组元素
function changeHobby (){person.hobby[0] = '学习' // Proxy(Object) {hobby: ['学习', '睡觉']}
}
通过length直接改变数组长度
function changeLength() {person.hobby.length = 1 //["吃饭"] 数组 length 变为1
}function changeLength() {person.hobby.length = 4 //[ "吃饭", "睡觉", null, null ] 数组 length 变为4
}
Vue3响应式方式区分
上面说了这么多Vue3响应式的使用方式,我们发现 Vue3 针对于 Vue2 的痛点,Vue3给出了解决办法,那我们现在来看一下,Vue3 是怎么解决这些问题的。
首先,Vue3 存在两种响应式转化的方式,分别是 ref()函数 和 reactive()函数。
ref() 函数能接收基本数据类型,也能接收引用类型的数据,所以Vue3推荐使用 ref() 函数来实现响应式。
reactive() 函数只能接受引用类型数据,不能转化基本类型数据。
所以 ref() 函数和 reactive()函数进行响应式转化的底层原理其实是不一样的。
ref()函数实现响应式
- 在调用 ref() 函数时,会先调用 createRef() 函数。
export function ref(value) {return createRef(value, false); }
- 然后在 createRef() 函数中 判断当前接收的参数是否为 ref 对象,如果是,则直接返回该响应式数据,避免重复转化,如果不是,则调用 new RefImpl() 构造函数生成 RefImpl 引用对象
// 接收两个参数, // 第一个参数 rawValue 是需要转化为 ref 的原始值, // 第二个参数 shallow 是一个布尔值,标识是否是浅层响应,如果为 true,则只处理表面层次的响应式,而不会递归处理嵌套对象function createRef(rawValue, shallow) {// 判断需要转化的数据是否已经是 ref 对象,如果是,则直接返回该数据,避免重复转化// 但是 isRef 函数并不会进行深度响应式判断,如果对一个深度响应式对象再次使用 ref 或 reactive可能会导致嵌套的代理对象,if (isRef(rawValue)) {return rawValue;}// 如果不是 ref 对象,则调用 RefImpl 构造函数生成新的 RefImpl 引用对象// 同时传递 rawValue 和 shallow 来初始化响应式数据以及确定相应深度return new RefImpl(rawValue, shallow); }
-
RefImpl 类:是 ref 对象的实际实现。主要包括
-
存储原始值以及响应值:_rawValue 存储原始值,_value存储响应值
-
响应式处理:通过 shallow 来决定是否进行深层响应式数据处理
-
依赖收集与分发:在 get 函数中通过
trackRefValue
函数来收集依赖。在set函数中通过triggerRefValue
函数通知依赖更新。 -
RefImpl 类解析
class RefImpl {private _value: any; // 用来存储响应值private _rawValue: any; // 用来存储原始值public dep?: Dep = undefined; // 用来收集分发依赖public readonly __v_isRef = true; //是否只读,暂不考虑// 接收 new RefImpl() 传递过来的 rawValue 和 shallow constructor(value, public readonly __v_isShallow: boolean) {// 判断是否需要深层响应,如果不用,直接返回 Value 值,如果需要深层响应,则调用 toRaw 函数解除 value 的响应式,将其转化为原始值,以保证后续的深层响应this._rawValue = __v_isShallow ? value : toRaw(value);// 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应this._value = __v_isShallow ? value : reactive(value);}get value() {// 收集依赖trackRefValue(this);// 返回响应式数据return this._value;}set value(newVal) {// 将 newVal 转化为原始值,并于初始原始值比较,若不同,则准备更新数据,渲染页面,分发依赖if (hasChanged(toRaw(newVal), this._rawValue)) {//判断是否需要深层响应,如果不用,直接返回 newVal 值,如果需要深层响应,则调用 toRaw 函数解除 newVal 的响应式,将其转化为原始值,以保证后续的深层响应this._rawValue = this.__v_isShallow ? newVal : toRaw(newVal);// 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应this._value = this.__v_isShallow ? newVal : reactive(newVal);// 分发依赖,通知更新triggerRefValue(this);}} }
-
trackRefValue() 函数:用来收集依赖
// 接收参数 ref ,也就是当前 refImpl 引用实例对象 function trackRefValue(ref) {// 判断当前是否处于依赖收集状态,在 Vue2.x 中,相当于 window.target 。一般用来判断当前是否有活跃的响应式副作用正在运行if (isTracking()) {// ref.dep 是 RefImpl 实例对象上的一个属性,相当于 Vue2.x中的 Dep 类,用来收集或分发依赖// 判断 ref.dep 是否存在。若存在则直接使用,若不存在,则通过 createDep 函数创建一个新的依赖集合并赋值给 ref.dep ,然后使用// 将当前活跃的副作用(effect)添加到 ref.dep 中,以便在将来 ref 值变化时能够触发这些副作用。trackEffects(ref.dep || (ref.dep = createDep()));} }
-
triggerRefValue()函数:用来分发依赖
function triggerRefValue(ref) {// ref.dep:依赖集合。如果存在依赖集合,则继续进行触发操作。if (ref.dep) {// 遍历并执行 ref.dep 中的所有副作用(effect),以响应 ref 值的变化。这个函数会通知所有依赖于 ref 值的副作用重新运行。类似于 Vue2.x中的 nofiny() triggerEffects(ref.dep);} }
-
如果需要深层响应转化,则需要用到 reactive() 函数,这里需要重点说明一下,
reactive()
函数的设计是单例模式,也就是说:对同一个对象多次调用reactive()
函数,返回的都是同一个代理对象。对一个代理对象调用reactive()
函数,总会返回代理对象自身。所以如果 ref 函数接收的是一个 Proxy代理对象的话,调用 reactive 函数之后,返回的还是本身的 Proxy 代理对象,并不会重复转化一次。这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理对象。
-
reactive 函数的响应式原理
上面说到了 在 ref函数中如果接收了一个对象,且需要深层响应的话,就会调用 reactive 函数来进行响应式转化,那我们现在来看看 reactive 函数转化响应式数据的原理
首先,在了解 reactive 函数的原理时,我们需要了解reactive 函数的基本概念,可以参考我的上一篇博文--reactive()函数。在这篇博文中,我大概讲了一下 reactive 函数是怎么通过 Proxy 代理对象以及 Reflect 对象来实现响应式操作的。现在,让我们完善一下 reactive函数的响应式原理吧。
import { isObject, toRawType } from '@vue/shared';
import { mutableHandlers } from './baseHandlers';
import { ReactiveFlags, reactiveMap } from './reactive';export function reactive(target) {// 判断 target 是否是一个对象if (!isObject(target)) {return target;}// 如果 target 已经是一个响应式对象,直接返回它if (target[ReactiveFlags.IS_REACTIVE]) {return target;}// 如果已经存在对应的 Proxy 对象,直接返回缓存的 Proxy 对象const existingProxy = reactiveMap.get(target);if (existingProxy) {return existingProxy;}// 否则创建一个新的 Proxy 对象const proxy = new Proxy(target, mutableHandlers);// 缓存创建的 Proxy 对象,避免重复创建,这就是单例模式--reactive函数对于Proxy代理对象返回的是其本身reactiveMap.set(target, proxy);return proxy;
}
到了这一步,已经完成了数据代理,通过对Proxy代理对象的操作,可以同步影响源对象。这时我们就需要进行数据监测了,而这一步其实就是在 mutableHandlers 对象之中。
const proxy = new Proxy(target, mutableHandlers);
mutableHandlers
是 Proxy
代理的核心,它定义了各种操作的拦截器,如 get
、set
、has
、deleteProperty
等。在 Vue 3 中,get
和 set
是最重要的两个拦截器:为了方便解释,我把 mutableHandlers
中的所有属性方法全部抽离出来了,真正源码不是这样的。
import { track, trigger } from './effect';
import { toRaw, reactive, readonly } from './reactive';
import { isObject, hasOwn, isSymbol, hasChanged } from '@vue/shared';
import { ReactiveFlags, toReactive, toReadonly } from './reactive';const get = createGetter();
const set = createSetter();
const deleteProperty = createDeleteProperty();
const has = createHas();
const ownKeys = createOwnKeys();export const mutableHandlers = {get,set,deleteProperty,has,ownKeys
};
到这里我们能了解 mutableHandlers
对象中基本都有些什么,然后就需要对每个方法进行深入解析了。
在介绍方法之前,我们了解一些概念
- 副作用函数(Effect):使用了响应式数据的函数,例如组件的渲染函数。当数据发生变化时,这些函数需要重新执行以更新视图或计算新的值。
- 依赖关系:在 Vue 3 中,每个响应式属性都可能有多个副作用函数依赖于它。依赖关系在
track
函数中被收集,而在trigger
函数中被使用。 - 全局状态(activeEffect):Vue 3 的响应式系统中有一个全局状态(activeEffect),用于保存当前正在执行的副作用函数。当我们读取响应式数据时,Vue 会检查是否存在当前副作用函数,并将其与数据的依赖关联起来。在组件渲染过程中,activeEffect 会指向当前的渲染函数,从而实现对所有使用到的响应式数据的依赖收集。
get方法:get拦截器负责处理对对象属性的读取操作。这是 Vue 响应式系统中最重要的部分之一,因为它涉及到依赖追踪。
function createGetter() {return function get(target, key, receiver) {// Reflect.get(target, key, receiver): 使用 Reflect.get 来获取源对象属性的值。// 这是现代 JavaScript 中获取属性值的标准方式,可以避免一些特殊情况下的错误。// 可以参考上一篇博文--reactive函数const res = Reflect.get(target, key, receiver);// 依赖收集 track(target, 'get', key): 调用 track 函数来进行依赖收集。// 这使得 Vue 能够追踪哪些组件依赖于这个属性,以便在属性变化时触发重新渲染。track(target, 'get', key);// 如果属性值是对象类型,则递归地将其转化为响应式对象,实现深度响应式。if (isObject(res)) {return reactive(res); // 深度响应式}// 如果属性值是基本类型,则直接返回return res;};
}
set方法:set
拦截器负责处理对象属性的修改操作,这是触发依赖更新的关键部分。
function createSetter() {return function set(target, key, value, receiver) {// 获取上一次的属性值const oldValue = target[key];// Reflect.set(): 使用 Reflect.set 来设置对象属性的值,与get一致,返回Boolean值来判断是否设置成功const result = Reflect.set(target, key, value, receiver);// 检查新值与旧值是否不同,只有在值确实发生变化时才触发更新。if (hasChanged(value, oldValue)) {// 调用 trigger 函数触发响应式更新,通知所有依赖该属性的副作用函数(如渲染函数)重新运行。trigger(target, 'set', key, value);}// 返回设置的结果状态,true or falsereturn result;};
}
deleteProperty
:拦截 delete
操作,在删除对象属性时触发响应式更新。
function deleteProperty(target, key) {// 检查 target 是否具有 key 属性const hadKey = hasOwn(target, key); // 删除 target 的 key 属性,与get 和 set 方法类似const result = Reflect.deleteProperty(target, key); // 如果删除成功,并且 key 属性确实存在if (result && hadKey) { // 触发响应式系统的更新通知trigger(target, 'delete', key); }// 返回删除操作的结果(true 或 false)return result;
},
has:用于检查一个对象是否拥有某个属性,相当于in 操作符 。
has(target, key) {// 判断 target 对象中是否存在属性key,返回 Boolean 值const result = Reflect.has(target, key);// 收集依赖track(target, 'has', key);// 返回结果return result;},
ownKeys
:Reflect.ownKeys
方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
。
function ownKeys(target) {// 依赖收集,标记为数组类型track(target, 'iterate', 'array');// 返回所有键--是一个数组return Reflect.ownKeys(target);}
上面是操作对象的方法,其中 track 则是收集依赖的方法,trigger则是分发依赖通知更新的方法,下面简单介绍一下这两个方法是如何工作的。
track:收集依赖,track
函数 会将 target
、key
和当前的副作用函数关联起来,这样在 key
对应的属性值发生变化时,Vue 就能找到所有依赖这个属性的副作用函数,并触发它们重新执行。
function track(target, type, key) {// 首先检查当前是否有正在执行的副作用函数。如果没有,则不需要进行依赖收集。// 副作用函数一般在 Vue 的响应式系统运行时注册,比如在组件的渲染过程中,Vue 会把当前的渲染函数注册为全局的副作用函数。if (!isTracking()) return;// targetMap 是一个全局的 WeakMap,用于存储所有的响应式对象及其依赖关系。// 从 targetMap 中获取当前 target 对象的依赖图(即 depsMap),如果不存在,则创建一个新的 Map。//depsMap 是一个 Map,用于存储 target 对象中每个属性(即 key)的依赖集合(dep)let depsMap = targetMap.get(target);if (!depsMap) {targetMap.set(target, (depsMap = new Map()));}// dep 是一个 Set,用于存储依赖于某个特定属性 key 的所有副作用函数。// 从 depsMap 中获取 key 的依赖集合 dep,如果不存在,则创建一个新的 Set。let dep = depsMap.get(key);if (!dep) {depsMap.set(key, (dep = createDep()));}// 将当前的副作用函数添加到 dep 中// 这一步是将当前正在执行的副作用函数(如渲染函数)添加到依赖集合中,以便在这个 key 发生变化时,能够重新执行这些副作用函数。trackEffects(dep);
}
trigger:
当响应式对象的属性值发生变化时,Vue 会通过调用 trigger
函数来通知依赖该属性的所有副作用函数(如组件渲染函数、计算属性等)重新执行,从而实现视图的自动更新。
function trigger(target, type, key, newValue, oldValue) {// targetMap 是全局存储所有响应式对象及其依赖关系的 WeakMap。targetMap 的键是响应式对象 target,值是一个 Map,这个 Map 存储了该对象每个属性 key 的依赖集合。// depsMap 是当前 target 对象的依赖图,里面存储了 key 与其依赖的副作用函数的映射关系。// 如果 depsMap 不存在,说明这个 target 对象没有被任何副作用函数依赖,直接返回。const depsMap = targetMap.get(target);if (!depsMap) {return;}// effects 是一个 Set,用于去重并存储所有需要触发的副作用函数。这样可以避免重复触发相同的副作用函数。const effects = new Set();// 如果是 SET 操作,收集相关副作用函数, 从 depsMap 中获取与 key 相关的副作用函数集合,并将其添加到 effects 中。if (key !== undefined) {addEffects(depsMap.get(key));}// addEffects 函数检查是否存在与当前 key 相关的副作用函数,如果存在,它们将被添加到 effects 集合中。const addEffects = (effectsToAdd) => {if (effectsToAdd) {effectsToAdd.forEach(effect => effects.add(effect));}};// 如果操作类型是 'add'、'delete' 或 'set',而且对象是数组或其他特殊数据结构,还需要处理特殊的依赖关系。例如,操作数组的 length 属性时,需要触发依赖于 length 的副作用函数。if (type === 'add' || type === 'delete' || type === 'set') {addEffects(depsMap.get(Array.isArray(target) ? 'length' : ''));}// 遍历 effects 集合,并执行其中的每个副作用函数// 如果副作用函数有一个 scheduler 调度器(通常是用来调度执行顺序的),则调用调度器,否则直接执行副作用函数。effects.forEach(effect => {if (effect.options.scheduler) {effect.options.scheduler(effect);} else {effect();}});
}
trackEffects 收集依赖
通过源码我们可以知道,不论是ref还是reactive,在收集依赖时,最终都使用了 trackEffects 函数,其作用是将当前正在执行的副作用函数(例如渲染函数、计算属性等)添加到某个响应式数据的依赖集合中。这是依赖收集的一部分,目的是在数据变化时触发相关的副作用函数进行更新。
function trackEffects(dep) {// 获取当前正在执行的副作用函数let shouldTrack = shouldTrackEffect(dep);if (shouldTrack) {// 将当前的副作用函数 activeEffect 添加到 dep 中。这一步建立了属性与副作用函数之间的单向关联,即这个属性知道有哪些副作用函数依赖于它dep.add(activeEffect);// 同时将 dep 添加到 activeEffect 的 deps 数组中。这一步建立了副作用函数与属性之间的双向关联,即这个副作用函数知道它依赖于哪些属性。activeEffect.deps.push(dep);}
}// 检查当前的副作用函数是否已经被添加到 dep 中,以避免重复添加。
function shouldTrackEffect(dep) {// 如果 dep 中已经存在 activeEffect,则返回 false,表示不需要重复添加;否则返回 true。return !dep.has(activeEffect);
}
在 if 判断中,Vue3实现了 双向依赖管理,这样做的好处是
- 避免重复添加:由于
dep
是一个Set
,它会自动去重,确保每个副作用函数只会被添加一次。 - 双向清理:在副作用函数停止依赖某个属性时,可以通过清理
activeEffect.deps
中的dep
,从而移除双向关联,避免内存泄漏。
同样的,在这里我们也需要理解一些基本概念,那就是:dep
、 effect、activeEffect
、activeEffect.deps
- dep:是一个
Set
集合,用来存储依赖某个响应式属性的所有副作用函数。 effect:
是一个包装了副作用逻辑的函数(例如渲染函数、计算属性等)。当effect
函数被执行时,它会触发响应式数据的读取,从而进行依赖收集。activeEffect:
是一个全局变量,用于指向当前正在运行的副作用函数。每当副作用函数被执行时,activeEffect
就会被设置为当前的副作用函数,从而在依赖收集时可以将这个函数与相应的响应式属性关联起来。- activeEffect.deps:
activeEffect.deps
是一个数组,用来存储当前副作用函数依赖的所有dep
集合。也就是说,activeEffect.deps
中的每一个元素都是一个dep
,而每个dep
都包含了当前副作用函数所依赖的响应式属性。
举一个栗子就是:假设我们有一个响应式对象和一个依赖于这个对象的副作用函数:
const state = reactive({ count: 0 });const effectFn = effect(() => {console.log(state.count);
});
在执行 effectFn
的过程中,Vue 会执行以下操作:
-
依赖收集:
effectFn
会读取state.count
,因此state.count
对应的dep
集合会被收集,并且effectFn
会被添加到这个dep
集合中。 -
记录依赖:同时,Vue 还会将这个
dep
集合添加到activeEffect.deps
中,以便将来进行依赖关系的清理。 -
触发更新:当
state.count
的值发生变化时,Vue 会遍历dep
集合中的所有副作用函数,并触发它们重新执行。 -
清理旧的依赖:如果在下一次执行
effectFn
之前,某些依赖关系已经不再存在(例如副作用函数不再依赖某个属性),Vue 会通过遍历activeEffect.deps
来清理这些不再需要的依赖。
总结
Vue2的响应式
- 原理:通过Object.definePropoty() 实现对数据每个属性的劫持,通过get和set实现了响应式
- 问题:对于数组数据无法通过数组下标直接操作,无法通过 length直接设置数组长度,无法 直接给对象或数组添加属性
- 解决:通过 vm.$set() 或 this.set() 来对对象或数组进行操作
Vue3响应式
- 原理:Ref响应式还是通过Object.definePropoty()对数据劫持,通过get和set实现响应式。reactive则是通过 Proxy对数据进行代理劫持,实现响应式。然后通过Reflect实现对源数据的操作
- 优势:Proxy是实现对象的监听,而不是对某个属性的监听。而且是惰性的,嵌套对象只有在被访问时才会被转化为响应式。这种方式避免了不必要的性能开销,尤其是在处理大型数据结构时。