本文源码版本为vue 3.3.4,主要基于打包后的源码梳理,沉下心,忽略次要内容,小白也能读懂
1.第一步
调用createRef函数开始创建
function ref(value) {
return createRef(value, false);
}
附:
// createRef第二个参数用于区分是否浅响应,默认为false,可以对比shallowRef实现
function shallowRef(value) {
return createRef(value, true);
}
2.第二步
createRef函数的实现也很简单,判断目标值是否已经是一个响应式的ref对象,若是则直接返回原值,否则使用目标参数构造一个RefImpl类
function createRef(rawValue, shallow) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
附:
// isRef函数实现 __v_isRef在此处还不明确其具体用途,下一步可以看到相关实现
function isRef(r) {
return !!(r && r.__v_isRef === true);
}
3.第三步
下面来看RefImpl类的具体实现
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow; // 数据是否浅层响应
this.dep = void 0; // 副作用桶
this.__v_isRef = true; // 标记数据是响应式的ref对象(这也是isRef实现的依据)
this._rawValue = __v_isShallow ? value : toRaw(value); // 记录数据原始值
this._value = __v_isShallow ? value : toReactive(value); // 原值或响应式代理对象
}
get value() {
trackRefValue(this); // 收集副作用
return this._value;
}
set value(newVal) {
// newVal是否浅响应或只读代理对象
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
// newVal若是浅响应或只读代理对象,则newVal不做处理,否则调用toRaw获取newVal的原始值
newVal = useDirectValue ? newVal : toRaw(newVal);
/**
* 比较新值(newVal)和当前记录的原始值,若发生改变则:
* 更新当前记录的原始值_rawValue
* 更新_value
* 触发副作用
*/
if (shared.hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
}
附:
// toRaw实现 只要目标数据'__v_raw'属性存在值则递归调用 返回最后一次'__v_raw'属性取得的值
// 可以看到这里出现了新的__v_开头的属性,到此时其实我们并不明确其作用,但由于之前已经出现了同样开头的__v_isRef属性,我们可以结合其命名推断它的大概用途
// raw /rɔ:/ (信息)未经处理的,原始的
function toRaw(observed) {
const raw = observed && observed["__v_raw"];
return raw ? toRaw(raw) : observed;
}
// hasChanged实现 Object.is静态方法确定两个值是否为相同值
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
到这一步我们可以说一个响应式的ref对象已经创建完成了,对目标值.value的读写操作都由该响应式ref对象的getter和setter实现,同时在getter和setter中分别实现了副作用的收集和触发。当然,我们可以看到在构造函数和setter中,对于非浅响应式数据,其_value均通过toReactive函数进行了创建,toReactive的实现非常简单。
// 判断目标数据是否是object类型,若是则调用reactive创建响应式对象,否则返回原数据
const toReactive = (value) => shared.isObject(value) ? reactive(value) : value;
附:
// isObject实现
const isObject = (val) => val !== null && typeof val === "object";
到了这里,我们可以清晰地知道使用ref创建响应式数据比reactive多了哪些步骤,且对于对象类型的数据,最终也是通过reactive实现响应式的。还在天天总结ref和reactive有什么不同的同学可以歇一歇了,这块的源代码是非常简单和清晰的。
4.reactive实现
-
先来看
reactive源码// 首先判断目标数据是否已经是只读代理对象 若是则直接返回该只读代理对象 否则调用createReactiveObject创建响应式数据 function reactive(target) { if (isReadonly(target)) { return target; } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap ); } 附: // isReadonly实现 新的__v_开头的属性'__v_isReadonly' function isReadonly(value) { return !!(value && value["__v_isReadonly"]); } -
createReactiveObject实现function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandlers, proxyMap) { // 判断目标数据是否是object类型,若不是则打印警告信息,返回目标数据;若是则继续执行 if (!shared.isObject(target)) { { console.warn(`value cannot be made reactive: ${String(target)}`); } return target; } // 出现了新的__v_开头属性__v_isReactive,同样结合命名有个大概推测 先跳过 if (target["__v_raw"] && !(isReadonly2 && target["__v_isReactive"])) { return target; } // 从proxyMap(此时为reactiveMap)中获取key为目标元素的数据 const existingProxy = proxyMap.get(target); // 如果存在 则表示响应式数据(代理对象)已经创建 直接返回取得的代理对象;否则继续执行 if (existingProxy) { return existingProxy; } // 获取目标数据在创建代理对象时应该使用的类型 若为0则是无效的,直接返回目标数据 const targetType = getTargetType(target); if (targetType === 0 /* INVALID */) { return target; } // 使用Proxy构造目标数据的代理对象,根据targetType的不同分别使用collectionHandlers或baseHandlers作为代理配置 const proxy = new Proxy( target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers ); // 以目标数据为key,其代理对象为值,添加到proxyMap中(此时为reactiveMap) proxyMap.set(target, proxy); return proxy; } 附: // 可以看到形参proxyMap在reactive中调用时,传入的实参为reactiveMap,reactiveMap的定义如下: const reactiveMap = /* @__PURE__ */ new WeakMap(); // getTargetType实现 // 新的__v_开头属性__v_skip(skip 跳过,略过),暂时不明确其含义,跳过 // 判断目标数据属性__v_skip的值是否为true或者目标数据是否可扩展,若value["__v_skip"]为true或不可扩展则返回0,否则从targetTypeMap中获取toRawType(value)返回的值 // 默认情况下对象是可扩展的,可以通过Object.preventExtensions()、Object.seal()、Object.freeze()、Reflect.preventExtensions()中的任意一种方法将对象标记为不可扩展 function getTargetType(value) { return value["__v_skip"] || !Object.isExtensible(value) ? 0 /* INVALID */ : targetTypeMap(shared.toRawType(value)); } // targetTypeMap实现 // 根据targetTypeMap的返回值我们不难发现createReactiveObject在构造代理对象时,Object和Array类型的数据使用了baseHandlers作为代理配置;而Map、Set、WeakMap、WeakSet则都使用了collectionHandlers作为代理配置 function targetTypeMap(rawType) { switch (rawType) { case "Object": case "Array": return 1 /* COMMON */; case "Map": case "Set": case "WeakMap": case "WeakSet": return 2 /* COLLECTION */; default: return 0 /* INVALID */; } } // toRawType相关实现 const objectToString = Object.prototype.toString; const toTypeString = (value) => objectToString.call(value); const toRawType = (value) => { return toTypeString(value).slice(8, -1); }; -
我们知道,
Vue响应式的实现原理就是拦截对象的读取和设置操作,在对对象进行读取操作时收集相应的副作用,在对对象进行设置操作时,再将读取时收集的副作用取出来一一执行,这样就实现了响应式。// 以《Vue.js设计与实现》书中的原例说明: const obj = { text: 'hello world' } function effect() { // effect执行时会读取obj.text document.body.innerText = obj.text } // 如果我们手动操作obj.text的值使其发生变化(例如obj.text = 'hello vue3'),effect函数能够重新执行,那么很明显body的innerText也同样会发生相应的变化,和我们修改后的obj.text值保持一致,这样的对象obj我们称之为响应式数据。副作用的收集和触发有兴趣的同学可以参考
《Vue.js设计与实现》的第四章,再查看最新源码梳理其流程。抛开副作用的收集和触发,不难发现,响应式数据的另一个核心就是拦截读取和设置操作,对于不同类型的数据,有不同的内置操作方法,因此针对不同类型的数据,在创建代理时需要使用不同的代理配置,而又由于
Proxy的局限性,在创建响应式数据时Reflect也是必不可少的。下表是
Proxy的代理配置提供的方法名称(引用自Proxy和Reflect):内部方法 Handler 方法 何时触发 [[Get]]get读取属性 [[Set]]set写入属性 [[HasProperty]]hasin操作符[[Delete]]deletePropertydelete操作符[[Call]]apply函数调用 [[Construct]]constructnew操作符[[GetPrototypeOf]]getPrototypeOfObject.getPrototypeOf [[SetPrototypeOf]]setPrototypeOfObject.setPrototypeOf [[IsExtensible]]isExtensibleObject.isExtensible [[PreventExtensions]]preventExtensionsObject.preventExtensions [[DefineOwnProperty]]definePropertyObject.defineProperty, Object.defineProperties [[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptor, for..in,Object.keys/values/entries[[OwnPropertyKeys]]ownKeysObject.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in,Object.keys/values/entries -
对数据的拦截和设置操作作为响应式的核心之一,从实现层面来说是简单清晰的,但从实现细节来讲,是复杂而精细的。接下来我们仅以
baseHandlers这一代理配置的get方法进行梳理。// 在createReactiveObject的实现中,对Object和Array类型使用了baseHandlers作为代理配置 // 而reactive调用createReactiveObject时传递的实参为mutableHandlers // mutableHandlers定义 const mutableHandlers = { get: get$1, set: set$1, deleteProperty, has: has$1, ownKeys }; // 我们以继续看get方法的具体实现 const get$1 = /* @__PURE__ */ createGetter(); // 继续找到createGetter的实现 function createGetter(isReadonly2 = false, shallow = false) { /** * @param target 目标对象 * @param key 被获取的目标属性名 * @param receiver Proxy或者继承Proxy的对象 */ return function get2(target, key, receiver) { // 目标属性为'__v_isReactive'时,返回!isReadonly2,此时为true if (key === "__v_isReactive") { return !isReadonly2; // 目标属性为'__v_isReadonly'时,返回isReadonly2,此时为false } else if (key === "__v_isReadonly") { return isReadonly2; // 目标属性为'__v_isShallow'时,返回shallow,此时为false } else if (key === "__v_isShallow") { return shallow; // 此处的条件由多个三元运算符构成,根据是否只读、是否浅响应使用对应的WeakMap // 此处的WeakMap为reactiveMap // 然后从reactiveMap中取出目标对象的值,若值存在且等于receiver,说明代理对象已存在 // 同时此时访问的目标属性为'__v_raw'时,就返回原目标对象 } else if (key === "__v_raw" && receiver === (isReadonly2 ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap).get(target)) { return target; } /** * 通过上面对各个属性的判断及返回值,结合命名,我想我们对【在前面跳过的几个以__v_开头的属性的其中一些】的作用应该比较清晰了 * __v_isReactive:标识数据是否是一个响应式的reactive对象 * __v_raw:返回一个响应式reactive对象的原始数据 * 因此我们可以知道上文第三步中提到的toRaw其作用是获取目标数据的原始值 * 在createReactiveObject中我们跳过的第二个if条件是判断目标数据是否已经是一个代理对象 */ // 判断目标对象是否数组 const targetIsArray = shared.isArray(target); // 判断是否只读模式,若不是只读模式,则进入条件执行 if (!isReadonly2) { // 对数组的一些特殊操作单独处理 if (targetIsArray && shared.hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver); } // 对hasOwnProperty单独处理 if (key === "hasOwnProperty") { return hasOwnProperty; } } // 获取目标对象中属性为key的值 const res = Reflect.get(target, key, receiver); // 判断目标属性是否是内置symbol拥有的键或者是不需要追踪的键,若是,直接返回上一步取得的结果res if (shared.isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res; } // 如果不是只读模式,调用track收集对应的副作用 if (!isReadonly2) { track(target, "get", key); } // 如果是浅响应 直接返回res if (shallow) { return res; } // 如果res已经是一个响应式ref对象,且目标对象是数组、目标属性是整数,则直接返回res对象;否则解包res返回其值 if (isRef(res)) { return targetIsArray && shared.isIntegerKey(key) ? res : res.value; } // 如果res是对象,返回res的代理对象,根据是否只读模式调用readonly或reactive来创建 if (shared.isObject(res)) { return isReadonly2 ? readonly(res) : reactive(res); } // 未触发以上情况 直接返回res return res; }; } 附: const reactiveMap = /* @__PURE__ */ new WeakMap(); const shallowReactiveMap = /* @__PURE__ */ new WeakMap(); const readonlyMap = /* @__PURE__ */ new WeakMap(); const shallowReadonlyMap = /* @__PURE__ */ new WeakMap(); // builtInSymbols实现 可以替换shared.isSymbol为具体实现在控制台运行查看 const builtInSymbols = new Set( /* @__PURE__ */ Object.getOwnPropertyNames(Symbol).filter((key) => key !== "arguments" && key !== "caller").map((key) => Symbol[key]).filter(shared.isSymbol) ); // isNonTrackableKeys实现 const isNonTrackableKeys = /* @__PURE__ */ shared.makeMap(`__proto__,__v_isRef,__isVue`); function makeMap(str, expectsLowerCase) { const map = /* @__PURE__ */ Object.create(null); const list = str.split(","); for (let i = 0; i < list.length; i++) { map[list[i]] = true; } return expectsLowerCase ? (val) => !!map[val.toLowerCase()] : (val) => !!map[val]; } // readonly代理实现 function readonly(target) { return createReactiveObject( target, true, readonlyHandlers, readonlyCollectionHandlers, readonlyMap ); } // arrayInstrumentations实现 对数组的特殊读取和隐式修改length属性的操作等进行单独处理 const arrayInstrumentations = /* @__PURE__ */ createArrayInstrumentations(); function createArrayInstrumentations() { const instrumentations = {}; ["includes", "indexOf", "lastIndexOf"].forEach((key) => { instrumentations[key] = function(...args) { const arr = toRaw(this); for (let i = 0, l = this.length; i < l; i++) { track(arr, "get", i + ""); } const res = arr[key](...args); if (res === -1 || res === false) { return arr[key](...args.map(toRaw)); } else { return res; } }; }); ["push", "pop", "shift", "unshift", "splice"].forEach((key) => { instrumentations[key] = function(...args) { pauseTracking(); const res = toRaw(this)[key].apply(this, args); resetTracking(); return res; }; }); return instrumentations; } // hasOwnProperty实现 // 获取原数据 收集副作用 使用原数据调用hasOwnProperty检查key是否存在 // 此处若通过代理对象调用hasOwnProperty将发生循环调用,造成栈溢出或无限循环 function hasOwnProperty(key) { const obj = toRaw(this); track(obj, "has", key); return obj.hasOwnProperty(key); } // shared相关实现 const hasOwnProperty = Object.prototype.hasOwnProperty; const hasOwn = (val, key) => hasOwnProperty.call(val, key); const isArray = Array.isArray; const isSymbol = (val) => typeof val === "symbol"; const isString = (val) => typeof val === "string"; const isIntegerKey = (key) => isString(key) && key !== "NaN" && key[0] !== "-" && "" + parseInt(key, 10) === key;
到了这里我们发现在以上的梳理过程中,有一个__v_skip属性的作用还不明确,实际上我们在vuejs/core的源码中可以找到答案。
// 简单来说__v_skip属性为true的对象,createReactiveObject会从getTargetType函数得到一个表示无效的值,然后直接返回原数据,而不会创建代理
/**
* Marks an object so that it will never be converted to a proxy. Returns the
* object itself.
*
* @example
* ```js
* const foo = markRaw({})
* console.log(isReactive(reactive(foo))) // false
*
* // also works when nested inside other reactive objects
* const bar = reactive({ foo })
* console.log(isReactive(bar.foo)) // false
* ```
*
* **Warning:** `markRaw()` together with the shallow APIs such as
* {@link shallowReactive()} allow you to selectively opt-out of the default
* deep reactive/readonly conversion and embed raw, non-proxied objects in your
* state graph.
*
* @param value - The object to be marked as "raw".
* @see {@link https://vuejs.org/api/reactivity-advanced.html#markraw}
*/
export function markRaw<T extends object>(value: T): Raw<T> {
def(value, ReactiveFlags.SKIP, true)
return value
}
附:
export const enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
对数据的拦截和代理的复杂、精细远不止上面代码所呈现出来的部分,仅以上面的get代理配置实现来说,我们仅仅是走了一遍它的实现,而没有完全探究每一步操作的原因,比如为什么用Reflect.get(target, key, receiver)来返回值,就涉及到访问器属性对普通属性访问这一情况的考虑,再比如我们通过in操作符,for ... in 循环读取对象也发生了读取操作,但创建代理时其代理配置只有 get,set,deleteProperty,has,ownKeys几项配置,这些读取操作是如何拦截追踪的,这又涉及到了ECMA规范。
有兴趣的同学可以去深入学习,over~