Vue3 源码系列文章会持续更新,全部文章请查阅我的掘金专栏。
本系列的文章 demo 存放于我的 Github 仓库,推荐大家下载和调试,进而加深理解。
一. 只读接口
Vue 提供了 readonly
接口,来防止指定对象的属性被修改:
const { readonly, effect } = Vue;
const div = document.querySelector('div');
const readonlyData = readonly({ // 调用 readonly 接口
msg: 'old msg',
});
effect(() => {
div.innerText = readonlyData.msg;
});
// 不生效,因为 readonlyData 是只读对象,不允许属性被修改
readonlyData.msg = 'latest msg';
readonly
的实现同样借助了 Proxy
的拦截器 —— 被 get
拦截时正常返回数据,被 set
拦截时绕过改动行为。
改动参考如下:
/** reactive.js **/
import { readonlyHandlers } from './baseHandlers.js'
export const readonlyMap = new WeakMap();
// 新增 readonly 接口
export function readonly(target) {
const existingProxy = readonlyMap.get(target);
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(
target,
// 和 reactive 的区别是使用了另一个 handler
readonlyHandlers
);
readonlyMap.set(target, proxy);
return proxy
}
/** baseHandlers.js **/
// 新增
const readonlyGet = createGetter();
// 新增
export const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
// 直接返回 true,不做修改操作
return true
},
deleteProperty(target, key) {
// 直接返回 true,不做删除操作
return true
}
}
但目前 createGetter
方法所生成的 get
拦截器是针对常规响应式对象的。对于只读属性的对象而言,它不需要执行依赖追踪操作。我们可以为 createGetter
方法新增 isReadonly
参数,来对只读的逻辑进行处理:
function createGetter(isReadonly = false) { // 新增 isReadonly 参数
return function get(target, key, receiver) {
// 新增
const targetFromMap = (isReadonly ? readonlyMap : proxyMap).get(target);
// 将 proxyMap.get(target) 改为 targetFromMap
if (key === ReactiveFlags.RAW && targetFromMap) {
return target;
}
const targetIsArray = isArray(target);
// 新增 !isReadOnly 判断条件,只读对象执行数组方法可放行(毕竟会绕过追踪)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver);
if (isSymbol(key) && builtInSymbols.has(key)) {
return res;
}
// 新增 !isReadOnly 判断条件,只读对象无须追踪
if (!isReadonly) {
track(target, key);
}
if (isObject(res)) {
// 新增 isReadOnly 判断。确保子孙属性都能被处理到
return isReadonly ? readonly(res) : reactive(res);
}
return res;
}
}
如上方代码所示,get
拦截器并非只是单纯地判断是否只读,然后返回 Reflect.get
的结果,而是会顺着原有响应式的逻辑走到最后,方便处理对象的深层属性。
为方便理解,这段代码可以简化为:
function createGetter(isReadonly = false) {
const res = Reflect.get(target, key, receiver);
/** 绕过其它响应式的逻辑... **/
/** 因为只读属性的访问不需要做任何依赖收集处理 **/
if (isObject(res)) {
// 确保深层属性都能被处理
return isReadonly ? readonly(res) : reactive(res);
}
return res;
}
二. 浅响应和浅只读接口
2.1 浅响应
有时候我们只希望对被代理对象的最外层属性做响应式的处理,对其嵌套对象的属性则不做处理(从而提升性能)。这种能力简称为浅响应,在 Vue 中对应的接口是 shallowReactive
:
const { shallowReactive, effect } = Vue;
const div = document.querySelector('div');
const shallowReactiveData = shallowReactive({
a: '[old a]',
b: {
msg: '[old b]'
}
});
effect(() => {
div.innerText = shallowReactiveData.a + ';' + shallowReactiveData.b.msg;
});
setTimeout(() => {
shallowReactiveData.a = '[latest a]';
shallowReactiveData.b.msg = '[latest b]'; // 不会生效
}, 2000);
shallowReactive
的实现原理较简单 —— 在 get
拦截器中直接返回结果即可,不对嵌套属性进行遍历。
具体实现如下:
/** reactive.js **/
import { shallowReactiveHandlers } from './baseHandlers.js'
export const shallowReactiveMap = new WeakMap();
export function shallowReactive(target) {
const existingProxy = shallowReactiveMap.get(target);
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(
target,
shallowReactiveHandlers // 浅响应专属 handler
);
shallowReactiveMap.set(target, proxy);
return proxy
}
/** baseHandlers.js **/
export const shallowReactiveHandlers = Object.assign(
{},
mutableHandlers,
{
get: createGetter(false, true),
set: createSetter()
}
)
function createGetter(isReadonly = false, shallow = false) { // 新增 shallow 参数
return function get(target, key, receiver) {
const targetFromMap = (
isReadonly ? readonlyMap :
(shallow ? shallowReactiveMap : proxyMap) // 新增判断
).get(target);
if (key === ReactiveFlags.RAW && targetFromMap) {
return target;
}
// ...
if (shallow) { // 新增,若为浅响应,直接返回属性值
return res
}
if (isObject(res)) { // 浅响应不会走到这里来,即嵌套属性不会被处理为响应式
return isReadonly ? readonly(res) : reactive(res);
}
return res;
}
}
2.2 浅只读
如果你希望让一个对象的最外层属性不被修改,但其嵌套属性依旧保持可读写的能力,可以使用 Vue 的 shallowReadonly
浅只读接口:
const { shallowReadonly } = Vue;
const div = document.querySelector('div');
const shallowReadonlyData = shallowReadonly({
a: '[old a]',
b: {
msg: '[old b]'
}
});
shallowReadonlyData.a = '[latest a]'; // 不会生效
shallowReadonlyData.b.msg = '[latest b]'; // 生效
div.innerText = shallowReadonlyData.a + ';' + shallowReadonlyData.b.msg;
浅只读的实现较为简单,它结合了 readonly
和 shallowReactive
的逻辑:
/** reactive.js **/
import { shallowReadonlyHandlers } from './baseHandlers.js'
export const shallowReadonlyMap = new WeakMap();
export function shallowReadonly(target) {
const existingProxy = shallowReadonlyMap.get(target);
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(
target,
shallowReadonlyHandlers // 浅只读专属 handler
);
shallowReadonlyMap.set(target, proxy);
return proxy
}
/** baseHandlers.js **/
export const shallowReadonlyHandlers = Object.assign(
{},
readonlyHandlers, // 复用 readonly 的 handler
{
get: createGetter(true, true) // 传入 isReadonly shallow 参数
}
)
这里复用了 readonly
的 handler,这意味着被代理对象在 set
和 deleteProperty
阶段不会做任何操作。同时重写了 get
拦截器,传入的 shallow
参数可以确保其只对最外层的属性做处理。
💡
shallowReadonly
接口代理后的对象,仅是把最外层的属性被设置为只读而已,其不具备收集依赖/触发副作用函数的能力。
三. markRaw
如果有的对象你希望永远不要被 Vue 响应式接口的 Proxy
所代理,可以使用 markRaw
方法来对该对象进行标记:
const { markRaw, reactive, effect } = Vue;
const div = document.querySelector('div');
const obj = markRaw({
msg: 'old msg'
});
const obj2 = reactive(obj);
effect(() => {
div.innerText = obj2.msg
})
obj2.msg = 'latest msg'; // 不会触发副作用函数执行
markRaw
会给对象加上一个不可枚举的私有属性 __v_skip
,在调用响应式接口时先检查对象是否存在该属性,若有则返回原始对象。
具体实现如下:
/** reactive.js **/
export function reactive(target) {
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy
}
// 新增判断,存在 markRaw 标记属性则直接返回原始内容
if(target[ReactiveFlags.SKIP]) {
return target
}
const proxy = new Proxy(
target,
mutableHandlers
);
proxyMap.set(target, proxy);
return proxy
}
export function markRaw(value) {
def(value, ReactiveFlags.SKIP, true)
return value
}
/** shared.js **/
/** def 方法的实现 **/
export const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}
我们还需给 readonly
、shallowReactive
和 shallowReadonly
都加上检查 markRaw
标记属性的逻辑,不过鉴于这些方法和 reactive
方法的结构非常相近,可以对其做进一步的封装:
function createReactiveObject(target, handler, map) {
const existingProxy = map.get(target);
if (existingProxy) {
return existingProxy
}
// 被 markRaw 标记了的对象、非对象类型或不可扩展数组是不能被代理的,直接返回原始内容
if (target[ReactiveFlags.SKIP] || !isObject(target) || !Object.isExtensible(target)) {
return target
}
const proxy = new Proxy(
target,
handler
);
map.set(target, proxy);
return proxy
}
export function reactive(target) {
return createReactiveObject(target, mutableHandlers, proxyMap)
}
export function readonly(target) {
return createReactiveObject(target, readonlyHandlers, readonlyMap)
}
export function shallowReactive(target) {
return createReactiveObject(target, shallowReactiveHandlers, shallowReactiveMap)
}
export function shallowReadonly(target) {
return createReactiveObject(target, shallowReadonlyHandlers, shallowReadonlyMap)
}
留意在 createReactiveObject
封装方法里,我们不仅对让 markRaw
标记了的对象跳过了后续的 Proxy
代理,顺便让非 Object
类型、不可扩展数组也都跳过了代理(因为它们都不是响应式接口的受理类型)。
四. 工具方法补充
Vue 提供了一些工具方法,用于识别指定对象是否被某个接口代理过。
它们的原理和 toRaw
接口的逻辑类似 —— 查询对象是否存在特定属性,查询过程会被 get
拦截器拦截,并返回规则匹配的结果。
具体实现如下:
/** reactive.js **/
export const ReactiveFlags = {
RAW: '__v_raw',
SKIP: '__v_skip',
IS_REACTIVE: '__v_isReactive', // 新增
IS_READONLY: '__v_isReadonly', // 新增
IS_SHALLOW: '__v_isShallow', // 新增
};
// 查询是否是由 shallowReactive 或 shallowReadonly 创建的代理对象
export function isShallow(value) {
return !!(value && value[ReactiveFlags.IS_SHALLOW]);
}
// 查询是否是由 readonly 创建的只读代理
export function isReadonly(value) {
return !!(value && value[ReactiveFlags.IS_READONLY]);
}
// 查询是否是由 reactive 创建的响应式代理
export function isReactive(value) {
if (isReadonly(value)) { // 处理 readonly(reactive(target)) 场景
return isReactive((value)[ReactiveFlags.RAW])
}
return !!(value && value[ReactiveFlags.IS_REACTIVE]);
}
// 查询是否由 reactive 或 readonly 创建的代理对象
export function isProxy(value) {
return isReactive(value) || isReadonly(value)
}
/** baseHandlers.js **/
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const targetFromMap = (isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: proxyMap
).get(target);
if (key === ReactiveFlags.IS_REACTIVE) { // 新增规则
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) { // 新增规则
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) { // 新增规则
return shallow
} else if (key === ReactiveFlags.RAW && targetFromMap) {
return target;
}
// ...
}
}
五. 代理集合类型
集合类型包括了 Set
、Map
、WeakSet
和 WeakMap
,它们和普通的对象类型很不一样 —— 除了获取集合元素的数量会直接访问其 size
属性,其它访问/修改集合数据的形式都是通过调用原生的接口方法来实现。
为了区别于处理常规对象的 baseHandler.js
,Vue 专门封装了一个 collectionHandlers.js
来处理集合类型的响应式逻辑。
我们先看 reactive.js
模块的改动:
/** reactive.js - 第一部分 **/
import {
mutableHandlers, readonlyHandlers,
shallowReactiveHandlers, shallowReadonlyHandlers
} from './baseHandlers.js'
// 新增
const TargetType = {
COMMON: 1,
COLLECTION: 2
}
// 新增
function targetTypeMap(rawType) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
}
}
// 新增 collectionHandlers 参数
function createReactiveObject(target, handlers, collectionHandlers, map) {
// ...
// 新增。toRawType 可获取对象类型名称,实现见右方
const targetType = targetTypeMap(toRawType(target));
const proxy = new Proxy(
target,
// 集合类型使用专属的 handlers
targetType === TargetType.COLLECTION ? collectionHandlers : handlers
);
map.set(target, proxy);
return proxy
}
/** reactive.js - 第二部分 **/
const toRawType = (value) => {
// 从类似 "[object RawType]" 的字符串中抽取出 "RawType"
return toTypeString(value).slice(8, -1)
}
export function reactive(target) {
// 新增 mutableCollectionHandlers 参数
return createReactiveObject(target, mutableHandlers,
mutableCollectionHandlers, proxyMap)
}
export function readonly(target) {
// 新增 readonlyCollectionHandlers 参数
return createReactiveObject(target, readonlyHandlers,
readonlyCollectionHandlers, readonlyMap)
}
export function shallowReactive(target) {
// 新增 shallowCollectionHandlers 参数
return createReactiveObject(target, shallowReactiveHandlers,
shallowCollectionHandlers, shallowReactiveMap)
}
export function shallowReadonly(target) {
// 新增 shallowReadonlyCollectionHandlers 参数
return createReactiveObject(target, shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers, shallowReadonlyMap)
}
我们还需要实现 collectionHandlers.js
模块中的 mutableHandlers
、readonlyHandlers
、shallowReactiveHandlers
和 shallowReadonlyHandlers
接口。
鉴于集合类型都是靠调用原生方法(或者查询 size
属性)来访问和修改数据的,我们只需要在这四个 handler 中配置 get
拦截器即可。
collectionHandlers.js
模块的实现参考:
/** collectionHandlers.js **/
function createInstrumentationGetter(isReadonly, shallow) {
return (target, key, receiver) => {
// TODO...
}
}
export const mutableCollectionHandlers = { // 响应式 handler
get: createInstrumentationGetter(false, false)
}
export const shallowCollectionHandlers = { // 浅响应 handler
get: createInstrumentationGetter(false, true)
}
export const readonlyCollectionHandlers = { // 只读 handler
get: createInstrumentationGetter(true, false)
}
export const shallowReadonlyCollectionHandlers = { // 浅只读 handler
get: createInstrumentationGetter(true, true)
}
createInstrumentationGetter
方法会在集合类型访问 size
属性,或调用 get
、has
、forEach
、枚举(keys
、values
、entries
)方法时,对依赖进行收集(执行 track
);
在调用 add
、set
、delete
、clear
方法时,触发相应的副作用函数(执行 trigger
)。
鉴于 createInstrumentationGetter
的实现很接近于我们之前对数组栈方法(例如 push
)的处理,本文不再赘述。
collectionHandlers.js
模块完整的代码可以点击这里获取。