响应式原理
响应式机制的主要功能就是,可以把普通的JavaScript对象封装成为响应式对象,拦截数据的读取和设置操作,实现依赖数据的自动化更新。
Q: 如何才能让JavaScript对象变成响应式对象?
首先需要认识响应式数据和副作用函数,副作用函数指的是产生副作用的函数。
通过一个demo函数来理解一下:
let val = 1;
function effect() {val = 2;// 修改全局变量,产生副作用
}
effect()console.log(val)// 2
当console打印时,会触发字段的读取操作,副作用函数effect执行时,会触发设置操作。当我们能拦截一个对象的读取和设置操作时,那事情就变得简单了。
在Vue2时,通过Object.defineProperty函数实现,Vue3采用Proxy来实现。
根据如上思路,采用Proxy实现方式如下:
// 存储副作用函数的桶
const bucket = new Set();const data = { text: "summer" }
// 对原始数据的代理
const obj = new Proxy(data, {// 拦截读取操作get(target, key) {//将副作用函数effect添加到存储副作用函数的桶中bucket.add(effect)return target[key]},// 拦截设置操作set(target, key, newVal) {target[key] = newVal;// 把副作用函数从桶里取出并执行bucket.forEach(fn => fn())return true}
})
副作用函数可以是任意名字,上面的代码是帮助我们理解响应式数据的基本实现和工作原理。
从上面的代码片段中可以看出,一个响应系统的工作流程如下:
当读取操作发生时,将副作用函数收集到“桶”中;当设置操作发生时,从“桶”中取出副作用函数并执行。
核心API和响应式工具函数
reactive:
reactive是通过ES6中的Proxy特性实现的属性拦截,所以在reactive函数中我们直接返回new Proxy即可:
export function reactive(target) {if (typeof target!=='object') {console.warn(`reactive ${target} 必须是一个对象`);return target}return new Proxy(target, mutableHandlers);
}
mutableHandlers
mutableHandlers要做的事就是配置Proxy的拦截函数,这里我们只拦截get和set操作,进入到baseHandlers.ts中。
使用createGetter和createSetter来创建get和set函数,mutableHandlers就是配置了get和set的对象返回。
get直接返回读取的数据,这里的Reflect.get和target[key]实现的结果是一致的;并且返回值是对象的话,还会嵌套执行reactive,并且调用track函数收集依赖。set调用trigger函数,执行track收集的依赖。
const get = createGetter();
const set = createSetter();function createGetter(shallow = false) {return function get(target, key, receiver) {const res = Reflect.get(target, key, isRef(target) ? target : receiver);if (!isReadonly) {track(target, "get", key);}if(isObject(res)) {// 值也是对象的话,需要嵌套调用reactivereturn isReadonly ? readonly(res) : reactive(res)}return res;}
}function createSetter() {return function set(target, key, value, receiver) {const result = Reflect.set(target, key, value, isRef(target) ? target : receiver);if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, 'add', key, value)} else if (hasChanged(value, oldValue)) {trigger(target, 'set', key, value, oldValue)}}return result}
}export const mutableHandlers = {get, set
}
track
在track函数中,我们可以使用一个巨大的targetMap去存储依赖关系。map的key是我们要代理的target对象,值还是一个depsMap,存储每一个key依赖的函数,每一个key都可以依赖多个effect。代码如下:
const targetMap = new WeakMap();export function track(target,type, key) {// 没有 activeEffect,直接returnif(!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()))}deps.add(activeEffect)
}
trigger
根据targetMap的实现机制,trigger函数实现的思路就是从targetMap中,根据target和key找到对应的依赖函数集合deps,然后遍历deps执行依赖函数。代码如下:
export function trigger(target,type, key) {const depsMap = targetMap.get(target);if(!depsMap) {// 没找到依赖return;}const deps = depsMap.get(key)if(!deps) return;deps.forEach((effectFn) => {// 判断副作用函数是否存在调度器if(effectFn.scheduler) {effectFn.scheduler()} else {effectFn()}})
}
effect
我们把传递进来的fn函数通过effectFn函数包裹执行,在effectFn函数内部,把函数赋值给全局变量activeEffect;然后执行fn()的时候,就会触发响应式对象的get函数,get函数内部就会把activeEffect存储到依赖中,完成依赖的收集。
export function effect(fn, options = {}) {// effect嵌套通过队列管理const effectFn = () => {try {activeEffect = effectFn;return fn();} finally {activeEffect = null;}}if(!options.lazy) {// 没有配置lazy,直接执行effectFn()}effectFn.scheduler = options.scheduler;return effectFn;
}
effect传递的函数,可以通过lazy和scheduler来控制函数的执行时机,默认是同步执行。
scheduler
使用数组管理传递的执行任务,最后使用Promise.resolve只执行最后一次,这也是Vue中watchEffect函数的大致原理。
const obj = reactive({ count: 1});
effect(() => {console.log(obj.count)
}, {scheduler: queueJob
});
// 调度器实现
const queue: Function[] = [];
let isFlushing = false;
function queueJob(job: () => void) {if(!isFlushing) {isFlushing = true;Promise.resolve().then(() => {let fn;while(fn = queue.shift()) {fn()}})}
}
ref
ref的执行逻辑比reactive要简单一些,不需要使用Proxy代理语法,直接使用对象语法中的getter和setter配置,监听value属性即可。对象的get value方法,使用track函数去收集依赖,set value方法中使用trigger函数去触发函数的执行。
export function ref(val) {if(isRef(val)) {return val;}
}export function isRef(val) {return !!(val && val.__isRef)
}// 利用面向对象的getter和setter进行track和trigger
class RefImpl {constructor(val, isShallow: boolean) {this._rawValue = isShallow ? value : toRaw(value)this._value = isShallow ? value : toReactive(value)this.__v_isShallow = isShallow}get value() {track(this, 'value')return this._value;}set value(newValue) {const oldValue = this._rawValue;const useDirectValue =this.__v_isShallow ||isShallow(newValue) ||isReadonly(newValue);newValue = useDirectValue ? newValue : toRaw(newValue);if(hasChanged(newValue,oldValue)) {this._rawValue = newValue;this._value = useDirectValue ? newValue : toReactive(newValue);trigger(this, "value")}}
}export const hasChanged = (value: any, oldValue: any): boolean =>!Object.is(value, oldValue)
ref也可以包裹复杂的数据结构,内部会直接调用reactive来实现,这也解决了日常对ref和reactive使用时机的疑惑,现在可以全部都用ref函数,ref内部会帮我们调用reactive。
computed
computed计算属性也是一种特殊的effect函数。在computed函数,我们拦截了computed的value属性,并且定制了effect的lazy和scheduler配置,computed注册的函数就不会直接执行,而是要通过scheduler函数中对_dirty属性决定是否执行。
export function computed(getterOrOptions) {let getter, setter;if(isFunction(getterOrOptions)) {getter = getterOrOptions;setter = () => {console.warn('computed value is readonly')}} else {getter = getterOrOptions.get;setter = getterOrOptions.set;}return new ComputedRefImpl(getter, setter);
}class ComputedRefImpl {constructor(getter, setter) {this._setter = setter;this._val = undefined;this._dirty = true;// computed就是一个特殊的effect,设置lazy和执行时机this.effect = effect(getter, {lazy: true,scheduler:() => {if(!this._dirty) {this._dirty = true;trigger(this, 'value')}}})}get value() {track(this, 'value')if(this._dirty) {this._dirty = false;this._val = this.effect()}return this._val;}set value(val) {this._setter(val)}
}
watch
watch本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数;实现本质上就是利用了effect以及options.scheduler选项。
function watch(source, cb) {let getter;if(typeof source === 'function') {getter = source;} else {getter = () => traverse(source)}let oldValue, newValue;const effectFn = effect(// 调用traverse递归地读取() => getter, {lazy: true,scheduler() {// 执行副作用函数,获取新值newValue = effectFn();// 将旧值和新值作为回调函数的参数cb(newValue, oldValue);// 更新旧值oldValue = newValue;}})// 手动调用副作用函数,获取到旧值oldValue = effectFn();
}function traverse(value, seen = new Set()) {// 如果要读取的数据是原始值,或者已经被读取过,那么就不做处理if(!isObject(value) || value === null || seen.has(value)) return;seen.add(value)if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], seen)}} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, seen)})} else if (isPlainObject(value)) {for (const key in value) {traverse(value[key], seen)}for (const key of Object.getOwnPropertySymbols(value)) {if (Object.prototype.propertyIsEnumerable.call(value, key)) {traverse(value[key as any], seen)}}}return value;
}
总结
在探索Vue响应式系统的旅程中,我们深入剖析了其底层逻辑与关键实现。响应式原理作为基石,通过数据劫持(Vue2依托Object.defineProperty, Vue3借助更强大的Proxy),搭配依赖收集与派发更新机制,构建起数据与试图自动同步的桥梁。
核心API与工具函数则是响应式能力的具体延伸:reactive借助mutableHandlers等完成对象响应式转换,track精准收集依赖、trigger触发更新,effect与scheduler协同管控副作用执行;ref适配基本类型与对象的响应式需求,computed基于依赖缓存实现高效计算,watch灵活监听数据变化。这些内容共同编织出Vue响应式系统的完整生态,理解它们,方能在Vue开发中精准驾驭数据驱动视图的精髓,应对复杂场景时游刃有余,为打造高效、可维护的Vue应用筑牢基础。