Vue3响应式及CompositionAPI实现
依赖收集effect.ts(副作用函数)
- 将模板转换的render函数放置在effect函数内,则实现了数据响应式。
流程简述
- 通过
reactive
创建响应式数据 - 在模板中使用该数据(访问触发Proxy.get),并将该模板转化为
render
渲染函数置于effect
副作用函数中。// template => render => effect(render) // effect函数*默认*会先执行一次(绑定依赖)。
- 依赖收集:将正在执行的effect函数抛出为
全局变量
,并在渲染时通过响应式数据的get方法进行依赖收集绑定
。 - effect与响应式对象的属性
互成依赖
,两者为多对多关系
。- 一个effect可收集多个属性为依赖。存储对象
deps
。 - 一个属性可存在于多个
effect
中。存储对象weakMap:(对象:map(属性:set(effect)))
。
- 一个effect可收集多个属性为依赖。存储对象
- 最后,当响应式数据改变时,根据依赖找到对应的
effect函数=>effect(render())
执行即可。 - 全流程均处于同步状态,精心设计利用了单线程进行依赖绑定。
核心:副作用函数effect
- effect -- 副作用函数,如果此函数依赖的数据发生变化,则重新执行该函数(类似computed)
export function effect(fn, options: any = {}) {
/*
创建响应式的effect,effect可以根据状态变化,重新执行
且可以嵌套着写,且可以传入自定义的调度函数[scheduler],可实现异步更新
*/
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run(); // 默认先执行一次
// effect执行完可以重复执行其中的方法 runner._effect.run/stop等,绑定this指向
const runner = _effect.run.bind(_effect);
runner._effect = _effect;
return runner;
}
响应式类 - ReactiveEffect
- 将effect转为响应式(嵌套监听逻辑)
// activeEffect 当前激活的effect函数
// 根据单线程原理可以追踪到每一个effect
export let activeEffect = undefined;
export class ReactiveEffect {
/**
* 为实例新增属性
* public => this.active = true 默认为激活状态
* => this.parent = null 为实例新增父亲[树形结构],避免嵌套函数回退时,全局属性丢失
* => this.deps = [] [分支切换]effect记录keys,记录被谁收集过,当重新执行effect时,要清空上次收集的依赖
* => this.scheduler = getters 当用户需要自定义调度时,可以使用scheduler
*/
public active = true;
public parent = undefined;
public deps = []; //
constructor(public fn, public scheduler?) {}
run() {
// 若函数不需要响应式数据,则说明未激活,直接执行函数即可
if (!this.active) {
this.fn();
}
try {
// 此时activeEffect为null或者其父亲(当effect嵌套时,要精准绑定响应式数据与effect的关系)
this.parent = activeEffect;
// 依赖收集,先改变全局activeEffect值,让渲染函数与依赖关联
activeEffect = this;
// 执行effect函数前,要清空上次的依赖
clearupEffect(this);
return this.fn();
} finally {
activeEffect = this.parent;
}
}
stop() {
if (this.active) {
this.active = false;
clearupEffect(this);
}
}
}
// 执行effect函数前,要清空上次的依赖
function clearupEffect(effect) {
const { deps } = effect;
for (let i = 0; i < deps.length; i++) {
// 此时操作的 deps[i] === targetMap.get(target).get(key)
deps[i].delete(effect); // 调用对应的key删除对应的effect -- [Set]对象引用
}
deps.length = 0; // 删除effect中的deps记录
}
依赖绑定函数track
/**
* 对象[属性]收集函数[多对多]
*
* 树形结构targetMap -- 源Map
* 第一层:以对象为键,Map为值 targetMap.set(target, (depsMap = new Map()))
* 一个对象[targetMap] -- 多个属性[target]
* 第二层:以属性为键,Set为值[保证其唯一性],
* 一个对象[key] -- 多个属性[Set]
*
*/
// 弱引用 -- WeakMap键只能是引用类型的对象
// 不会导致内存泄漏,原对象删除时,该引用也删除
const targetMap = new WeakMap();
export function track(target, type, key) {
if (!activeEffect) return; // 全局环境下访问属性值,不做处理
let depsMap = targetMap.get(target); // 第一次没有
if (!depsMap) {
targetMap.set(target, (depsMap = new Map())); // 注入target标识
}
let dep = depsMap.get(key); // dep => 属性key对应的唯一值Set对象
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
// 给当前的dep -- Set对象,绑定effect
trackEffects(dep);
}
依赖更新执行effect函数trigger
/**
*
* trigger 通知effect执行函数
*/
export function trigger(taregt, type, key, value, oldVal) {
// 获取对象值
const depsMap = targetMap.get(taregt);
// 触发的值,不在模板中
if (!depsMap) return;
let effects = depsMap.get(key); // 找到属性收集到的effects
// 通知收集的依赖执行
if (effects) {
// 拷贝一份,防止清理与添加的操作同时发生,关联引用
triggerEffect(effects);
}
}
封装effect常用逻辑收集、调用
// 封装收集依赖
export function trackEffects(dep) {
if (activeEffect) {
let shouldTrack = !dep.has(activeEffect); // 性能优化,由用户判断
if (shouldTrack) {
// 为dep对象绑定effect
dep.add(activeEffect);
// 反向记录,当effect绑定值被切换时[flag ? state.name : state.age],通知key值,别更新该effect
activeEffect.deps.push(dep);
}
}
}
// 封装触发effect函数
export function triggerEffect(effects) {
effects = new Set(effects);
effects.forEach((effect) => {
/**
* 防止effect中再次修改同一属性值[循环引用]
* run的时候会删除上一个effects,然后再添加本次执行的deps。
* 深拷贝一份,解除关联引用。循环数组时,delete和add同时运行,会造成栈溢出。
*/
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
});
}
数据代理(reactive实现)
代理流程简述
reactive
:将对象代理,并返回代理对象。- 考虑对象中有访问器、函数,在其中通过this访问对象的属性。
- 劫持时通过
Reflect
获取、修改属性,并将Proxy
对象传入,改变其this
指向为Proxy
对象。 - 保证访问器、函数内的源对象属性访问也能被劫持到。
- 劫持时通过
- 处理已代理过的对象,通过标识符
IS_REACTIVE
,标识数据是否已代理过 - 若已代理过,则直接返回该对象。
- 细节:
- 未被代理的对象无该属性拦截。
- 已被代理的对象在访问器设置了逻辑。
- 在不新增属性的情况下实现了标识判断。
// 导入值本来应该作为公共导出的,为了方便,下面直接贴上了
// import { isObject } from "@vue/shared";
// import { mutableHandle, ReactiveFlags } from "./baseHandle";
// 判断对象是否为响应式对象
export const isReactive = (obj) => {
return obj && obj[ReactiveFlags.IS_REACTIVE];
};
// 响应式标识
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
}
export const isObject = (example) => {
return typeof example === "object" && example != null;
};
// 数据拦截核心逻辑 -- new Proxy(mutableHandle)
export const mutableHandle = {
// 拦截器逻辑
get(target, key, receiver) {
/**
activeEffect ,当响应式数据被访问时,可以监听到,并关联到该渲染函数
console.log("key", key);
为代理对象新增已代理标识
*/
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
// 收集依赖函数
track(target, "get", key);
/**
* 懒递归,当访问代理对象A中的值B依旧为对象时,将B代理并返回,且不会重复代理
* receiver => mutableHandle -- 代理对象
* 监控代理对象获取值,Reflect 可以改变对象中的访问器this指向,指向代理对象receiver
*
*/
let res = Reflect.get(target, key, receiver);
if (isObject(res)) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
// 通知更新
let oldVal = target[key];
let result = Reflect.set(target, key, value, receiver);
if (oldVal != result) {
trigger(target, "set", key, value, oldVal);
}
// target指向源对象,receiver指向代理对象
return Reflect.set(target, key, value, receiver);
},
}
// reactive方法逻辑
export function reactive(obj) {
if (!isObject(obj)) return;
if (obj[ReactiveFlags.IS_REACTIVE]) {
return obj;
}
const proxyObj = new Proxy(obj, mutableHandle);
return proxyObj;
}
源码阅读补充
- 部分属性不代理
如:Symbol(内置)、__proto__等(对象内置属性)、__v_isRef、__isVue_等
- 如果对象存在原型链继承的关系
原型链上存在此属性,后子级直接修改该属性
,则只触发当前对象利用toRaw获取源对象判断是否为当前对象
- 数组处理
循环递归
- 重写数组部分方法,当数组方法需要用到源数组做操作或对比时
includes、indexOf
,内部使用源对象进行操作,保证结果准确。 - 当数组通过修改长度值为
newLen
时- 比对newLen值与数组下标项,若小于则触发更新
- 直接监听数组长度也会触发更新
- 重写通过数组方法
修改数组本身的方法,(push,pop,shift,unshift,splice)
- 当执行时,会收集数组的length属性,并触发length属性的依赖更新,及自身的依赖更新等
- 重写数组部分方法,当数组方法需要用到源数组做操作或对比时
- Reflect补充图例:
ref实现
- API们
- ref:将一个基本数据类型转换为响应式数据类型。
- 将原始值包装成对象
defineProperty
,与computed
的原理相似- 若是复杂数据类型,则走
reactive
,转为响应式数据
- 若是复杂数据类型,则走
- 并设置get访问器
收集依赖
、set触发依赖
。
- 将原始值包装成对象
- toRefs:将对象所有属性再代理一层,访问时代理到源对象上
- toRef:代理对象一个属性
- proxyRefs:反向代理,将响应式数据又反向代理成普通数据
使用
,本质还是访问.value
。- 用于模板中访问数据。
- ref:将一个基本数据类型转换为响应式数据类型。
import { isArray, isObject } from "@vue/shared";
import { trackEffects, triggerEffect } from "./effect";
import { reactive } from "./reactive";
function toReavtive(val) {
return isObject(val) ? reactive(val) : val;
}
/**
* 判断传入的值,若为引用类型,则将其响应式化,最后将其defineProperty化[拦截]
* 收集依赖
* 值发生改变:触发更新
* 判断传入的值是否为引用类型
*
*
*/
class RefImpl {
public dep = new Set();
public _value;
public __v_isRef = true;
constructor(public source) {
this.source = toReavtive(source);
}
get value() {
trackEffects(this.dep);
return this._value;
}
set value(newVal) {
if (newVal !== this.source) {
this._value = toReavtive(newVal);
this.source = newVal;
triggerEffect(this.dep);
}
}
}
/**
*
* @param source 原始值
*
*/
export const ref = (source) => {
return new RefImpl(source);
};
// 将数据做一层代理[将解构后的响应式数据再用访问器的方式包裹一次,并与源数据保持引用]。正向代理
class ObjectRefImpl {
constructor(public obj, public key) {}
get value() {
return this.obj[this.key];
}
set value(newVal) {
this.obj[this.key] = newVal;
}
}
// 将单个响应式数据包裹
export const toRef = (obj, key) => {
return new ObjectRefImpl(obj, key);
};
// 用于将多个响应式数据解构[将解构后的响应式数据再用访问器的方式包裹一次,并与源数据保持引用]
export const toRefs = (obj) => {
// 判断obj是对象还是数组
const result = isArray(obj) ? new Array(obj.length) : obj;
for (let key in result) {
toRef(result, key);
}
};
// 将ref数据反向代理,用于模板中访问ref数据不用.value
export const proxyRef = (obj) => {
return new Proxy(obj, {
get(target, key, recevier) {
let r = Reflect.get(target, key, recevier);
// 判断该数据是否是ref数据,如果不是[没有.value],直接返回
return r.__v_isRef ? r.value : r;
},
set(target, key, value, recevier) {
let oldVal = target[key];
if (oldVal.__v_isRef) {
// 获取旧值,如果旧值是ref数据,则赋值给其value值
oldVal.value = value;
return true;
} else {
// 如果不是,则直接赋值
return Reflect.set(target, key, value, recevier);
}
},
});
};
Watch原理
简介
-
使用方式:传入一个依赖:(对象/方法),一个回调方法,当依赖值改变时,执行回调。
-
例子
const obj = reactive({
name:'zs',
age:12
})
// 监听函数返回值
watch(()=>obj.name,(newVal,oldVal)=>{
console.log('obj.name change')
})
// 监听对象并自定义处理逻辑
watch(obj,(newVal,oldVal,handle)=>{
console.log(`obj's key change`)
if(newVal['name'] === oldVal['name']){
handle(()=>{
console.log(`obj's name has not change`)
})
}
})
原理简述
-
将传入的依赖都整合成函数,effect化,当依赖改变时,执行回调函数。
-
整合参数类型:都处理成函数
- 对象 -- 将该对象放置在一个函数内,并在函数内递归访问所有属性。
- 函数 -- 直接将该函数放置在effect回调内。
-
响应式处理
- 根据传入的函数执行后的
返回值
,当函数依赖属性改变时,重新获取一次该值并传入回调中。
- 根据传入的函数执行后的
-
自定义调度器
回调的第三个参数
- watch提供一种自定义逻辑回调,当触发watch更新时可执行当次未更新前传入的逻辑。
- 特殊场景下可使用:如输入框多次触发异步请求,无法判断先后,则可以在更新时抵消掉上次的逻辑。
import { isFunction, isObject } from "@vue/shared";
import { ReactiveEffect } from "./effect";
import { isReactive } from "./reactive";
/**
*
* @param source 用户传入的数据 -- 函数/响应式对象,对象无法区分新旧值。
* @param cb 回调,内有新旧值
*/
export const watch = (source, cb, options) => {
let getter;
// 判断传入的值是否为响应式对象
if (isReactive(source)) {
/*
watch本质是要转为effect,ReactiveEffect接收一个函数
传入的值为响应式对象
将getter effect化,将监听对象source放入函数内.
并在函数内递归[traversal]访问每一个属性
*/
getter = () => traversal(source);
} else if (isFunction(source)) {
// 传入的值为函数
console.log("isFunction");
// 如果是函数,则直接用其传入值作为响应式函数即可
getter = source;
} else {
return;
}
let oldVal;
let cleanup;
let onCleanup = (fn) => {
cleanup = fn;
};
let watchScheduler = () => {
/*
用户自定义的执行逻辑,第一次不执行,
watch更新时执行上一次调度传入的函数[cleanup]。
如:input框监听值并发送ajax请求,
可能出现后面的请求比前面的请求先返回,导致返回值混乱的问题
可以定义对应的锁[shock],进行顺序界定
watch(()=>state.age,(newVal,oldVal,cleanup)=>{
cleanup(()=>{
传入一个fn,将fn赋值给cleanup
在下一次effect执行时执行。
})
})
*/
if (cleanup) cleanup();
// watch调度器,当依赖变化时,调度回调执行,并传入新旧值
const newVal = effect.run();
cb(newVal, oldVal, onCleanup);
// 当回调执行完时,老值变新值,待下次执行
oldVal = newVal;
};
// 监控getter,当其依赖变化时
const effect = new ReactiveEffect(getter, watchScheduler);
oldVal = effect.run(); //获取getter第一次执行返回的对象
};
// 递归访问对象属性
function traversal(obj, set = new Set()) {
// 出口,非对象则不递归了
if (!isObject(obj)) {
return obj;
}
// 避免循环引用的问题,即属性访问自身或者自身上的引用对象
// 无限递归 -- 爆栈
if (set.has(obj)) {
return obj;
}
set.add(obj);
for (let key in obj) {
traversal(obj[key], set);
}
return obj;
}
Computed原理
简介
-
作用:得到一个
新的响应式数据(缓存)
,并通过.value
可以获取到回调返回值。 -
本质是基于
effect
函数的,缓存功能借由dirty
判断,且本身需要具备作为依赖收集上级函数的能力
。 -
例子
// 处理传过来的getter和setter
const com = computed(()=>{
return obj.a + obj.b
})
console.log(com.value); value => proxy
computed({
get(){
return obj.a + obj.b
},
set(){...}
})
实现简述
- 整合传入参数
getters、setter
,通过computedRefImpl
类处理即可 computedRefImpl
类内部实现。constructor
:- 收集内部依赖,将
getter
传入的函数effect
化,自定义依赖集合deps
分发。 - 自定义调度器,由脏值
dirty
决定是否触发更新。
- 收集内部依赖,将
value
属性访问器:- get:监听
value
属性,收集收集者们入deps
:当value
的值变化时,通知该收集者们刷新视图。 - set:执行传入的回调(若有)。
- get:监听
import { isFunction } from "@vue/shared";
import { ReactiveEffect, trackEffects, triggerEffect } from "./effect";
/*
ReactiveEffect => effect主类,用于创建effect函数
trackEffects => effect收集依赖
triggerEffect => effect触发更新,执行回调
*/
export const computed = (getterOrOptions) => {
let onlyGetter = isFunction(getterOrOptions),
setter,
getter;
// 固定参数,不管怎么传,逻辑一致
if (onlyGetter) {
// 如果effectOptions是函数,说明是回调用法,反之是访问器用法
getter = getterOrOptions;
setter = () => {
console.warn("can not set");
};
} else {
setter = getterOrOptions.set;
getter = getterOrOptions.get;
}
return new computedRefImpl(getter, setter);
};
/**
* 交由computedRefImpl处理
* 处理规则
* 将getter用effect处理一下,收集里面的依赖
* 再配合scheduler,当数据被访问时,判断dirty,进行调度
*/
class computedRefImpl {
public effect;
public _dirty = true; // 标识是否需要计算[脏值],默认取值的时候就要计算
public _v_isReadonly = true;
public _v_isRef = true;
public _value;
// 引用对象,用于收集当前computed.value的effect,当effect执行的时候全局activeEffect会修改
public dep = new Set();
constructor(getter, public setter) {
/**
computed(()=>state.name + '先生')
state.name 修改 => getter 重新执行
=> getter返回值修改(getter.value)
=> 外部effect重新执行
核心逻辑
getter => computed(getter) / computed({get:getter})
1. [getter收集底层依赖属性] -- new ReactiveEffect(getter, scheduler)
2. [computed.value作为响应式属性需要收集effect] -- computedRefImpl.get
底层依赖属性修改
1.computed重新执行并获取返回值给dirty;
2.computed.value收集的effect要触发更新[computed.value <=> effect == 一对一]
*/
this.effect = new ReactiveEffect(getter,
// 此处是一个scheduler,当getter收集的响应式属性改变,scheduler触发
() => {
console.log("scheduler");
if (!this._dirty) {
this._dirty = true;
// 触发computed.value收集的effect更新,修改视图
triggerEffect(this.dep);
}
});
}
get value() {
/**
* 需要做依赖收集,当computed.value被effect包裹的时候
需要将该effect收集起来,待computed.value修改时,才能由scheduler触发
类中的属性访问器,本质还是ES5[Object.defineProperty]实现
访问computed.value => 执行该回调
*/
trackEffects(this.dep);// 收集依赖属性
if (this._dirty) {
// 让传入的fn执行,并记录返回值
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
set(value) {
this.setter(value);
}
}
总结
- 从
template
开始 =>template => render(h) => effect(render(h))
- effect执行
- 将传入的
fn(render)
,包装成响应式函数对象effect
,拥有自己的依赖数组deps
,用于收集响应式数据
。- 该deps结构为
Set([key1,key2])
,每个key即是属性收集的effect
(对象引用)。
- 该deps结构为
- 抛出该
effect
到全局变量activeEffect
- 将传入的
- fn执行 =>
render(h)
- 读取到
响应式代理API(ref、reactive、computed等)
- 通过
proxy
代理传入的数据obj
,将其包装成响应式数据,拥有自己的依赖函数数组deps
,用于收集响应式effect
。- 该deps结构为
{obj:{key1:Set[effect1,effect2],key2:map2}
,key1中的数据与effect收集的响应式数据一致,对象引用
- 该deps被全局收集
targetMap(weakMap)
,用于判断该对象是否被收集过
- 该deps结构为
- 通过
Proxy
整体拦截对象(懒递归)
- 访问属性时调用
track、trackEffect
收集effect
入key.deps
数组。 - 修改属性时调用
trigger、triggerEffect
触发effect.deps
数组执行。
- 访问属性时调用
- 读取到
- 分支处理:执行effect函数前,需要将上一次属性收集的
effect依赖
删除(clearupEffect--对象引用)。
effect
副作用函数,如果此函数依赖的数据发生变化,则重新执行该函数 [computed/watch的原理函数]
- 收集依赖[响应式数据中的key],依赖数据变化,重新渲染页面[响应式原理=>数据驱动视图]
- 多对多
- 一个属性
<=>
多个effect - 一个effect
<=>
多个属性 - 双向记录
- 属性记录函数,为了通知函数改变
- 函数记录属性,当函数删除时,需要通知属性切断依赖
- 一个属性
- 优化:过滤更新
Map+Set
- 若存在多个相同属性绑定同个依赖则后续绑定不生效
Map+Set
- 若存在多个相同属性绑定同个依赖则后续绑定不生效
reactive
将对象通过Proxy+Reflect代理成响应式数据。
- 源码阅读
- 部分属性不代理
如:Symbol(内置)、__proto__等(对象内置属性)、__v_isRef、__isVue_等
- 如果对象存在原型链继承的关系
原型链上存在此属性,后子级直接修改该属性
,则只触发当前对象利用toRaw获取源对象判断是否为当前对象
- 数组处理
循环递归
- 重写数组部分方法,当数组方法需要用到源数组做操作或对比时
includes、indexOf
,内部使用源对象进行操作,保证结果准确。 - 当数组通过修改长度值为
newLen
时- 比对newLen值与数组下标项,若小于则触发更新
- 直接监听数组长度也会触发更新
- 重写通过数组方法
修改数组本身的方法,(push,pop,shift,unshift,splice)
- 当执行时,会收集数组的length属性,并触发length属性的依赖更新,及自身的依赖更新等
- 重写数组部分方法,当数组方法需要用到源数组做操作或对比时
- 部分属性不代理
computed
- 本质:computed内创建一个
effect
,且该effect
又作为所处环境的effect依赖项
被收集起来。- 所以当computed的依赖项修改时,不仅computed本身需要重新计算,还要通知其依赖函数渲染视图。
- 当
key修改时
=>fn重新执行,返回新的val
=>环境effect重新执行
- 核心逻辑
- 处理参数,统一
getter
和setter
- 调度
getter
=>computedRefImpl
- 收集依赖 --
computed收集底层依赖属性
,且computed.value需要作为属性需要收集effect
- 底层依赖修改 -- 1.computed重新执行并修改脏值dirty; 2.computed.value收集的effect要触发更新
computed.value <=> effect == 多对多
- 通过调度器
scheduler
和脏值dirty
触发computed函数
- 收集依赖 --
- 需要做依赖收集,当computed.value被effect包裹的时候,需要将该effect收集起来,待computed.value修改时,才能由scheduler触发
- 处理参数,统一
watch
根据传入的配置,创建响应式effect
,当依赖属性改变时,触发回调执行。
- 本质还是effect,监听对象与该effect互为依赖,传入
source,cb,options
- 判断传入的数据
source
- 是函数,则直接收集该函数的返回值
fnReturn
为依赖(不递归,若fnReturn为引用对象,只收集fnReturn,不收集其下属性)
- 是响应式对象,则将该对象用函数包裹起来,并在函数内收集该对象的每个属性
递归:const fn => travals(obj)
- 是函数,则直接收集该函数的返回值
- 将该回调函数effect化,并自定义对应的调度器
watchScheduler
,当依赖改变时,调度器执行传入的回调cb
,并将新旧值传入- 函数effect化时能获取到该响应式对象,保存起来,后期改变时传入
newVal,oldVal
- 改变后,新值赋值给旧值,下次改变时传入
- 提供用户自定义逻辑
cleanup
,判断watch执行逻辑
- 函数effect化时能获取到该响应式对象,保存起来,后期改变时传入
ref
将一个基本数据类型
转换为响应式数据类型
,并拦截其value属性
。
- 将原始值包装成对象
defineProperty
,与computed
的原理相似 - 若是复杂数据类型,则走
reactive
,转为响应式数据 - 并设置get访问器
收集依赖
、set触发依赖
。
toRefs
解构reactive对象
,将被解构的静态数据代理成对象,代理其value属性
,访问时代理到源对象上。
toRef
代理对象一个属性
proxyRefs
反向代理,访问ref数据
时,不用访问其value属性,内部返回其value属性。一般用于模板渲染ref属性。
其余API
基于核心实现的API
- shallowReactive -- 浅转换
只转化第一层
- readonly -- 深度转换且数据只读
- shallowReadonly -- 浅转换且只读
- toRaw -- 获取代理对象源数据[重写数组方法获取源数组用到,如indexOf,includes等等]
- 默认情况下,响应式数组获取值也是被代理过的,与原值不等[这会造成用户操作失误]。
- markRaw -- 标记对象不可被代理且不可枚举
转载自:https://juejin.cn/post/7239617977363071035