这篇文章主要介绍Vue3.0响应式系统的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!
引子
先来段代码,大家可以直接复制哦,注意引用的文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="../packages/vue/dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { reactive, computed, effect, watch, createApp } = Vue
const App = {
template: `
<div id="box">
<button @click="increment">{{ state.count }}</button>
</div>
`,
setup() {
const state = reactive({
count: 0
})
function increment(e) {
state.count++
}
effect(() => {
console.log('count改变', state.count);
})
return {
state,
increment
}
}
}
createApp().mount(App, '#app')
</script>
</body>
</html>
这段代码,想必大家都看得懂,点击后count增加,视图也随之更新,effect监听了count改变,那么为什么effect能观察到count变化呢,还有为什么reactive可以实现响应式?
effect
为什么要先说这个函数呢,因为它和其他函数都息息相关,只有先了解它才能更好的理解其他响应式API
上源码
export function effect(
fn: Function,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
if ((fn as ReactiveEffect).isEffect) {
fn = (fn as ReactiveEffect).raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
if判断,判断如果传入的fn函数,它已经是effect了,也就是一个标识,直接获取该函数上的raw属性,这个属性后面会讲到
调用createReactiveEffect
如果options中有lazy,就会立即调用effect,其实本质上调用的还是传入的fn函数
// 了解一下options有哪些
{
lazy?: boolean // 是否立即调用fn
computed?: boolean // 是否是computed
scheduler?: (run: Function) => void // 在调用fn之前执行
onTrack?: (event: DebuggerEvent) => void // 在依赖收集完成之后调用
onTrigger?: (event: DebuggerEvent) => void // 在调用fn之前执行,源码上来看和scheduler调用时机一样,只是传入参数不同
onStop?: () => void // 清除依赖完成后调用
}
返回effect
createReactiveEffect
上面提到了createReactiveEffect函数,我们来看看它的实现
function createReactiveEffect(
fn: Function,
options: ReactiveEffectOptions
): ReactiveEffect {
// 又包装了一层函数
const effect = function effect(...args): any {
return run(effect as ReactiveEffect, fn, args)
} as ReactiveEffect
effect.isEffect = true // 标识effect
effect.active = true // 如果active
effect.raw = fn // 传入的回调
effect.scheduler = options.scheduler
effect.onTrack = options.onTrack
effect.onTrigger = options.onTrigger
effect.onStop = options.onStop
effect.computed = options.computed
effect.deps = [] // 用于收集依赖
return effect
}
注意,敲黑板,这里有个run函数,很重要,因为它保存了依赖
function run(effect: ReactiveEffect, fn: Function, args: any[]): any {
if (!effect.active) {
return fn(...args)
}
if (activeReactiveEffectStack.indexOf(effect) === -1) {
cleanup(effect)
try {
activeReactiveEffectStack.push(effect)
return fn(...args)
} finally {
activeReactiveEffectStack.pop()
}
}
}
他把依赖存储在了一个全局的数组中activeReactiveEffectStack, 他以栈的形式存储,调用完依赖后,会弹出,大家要留意一下这里,后面会用到
怎么样,是不是很简单~
reactive
export function reactive(target: object) {
// 如果target是已经被readonly对象,那么直接返回对应的proxy对象
if (readonlyToRaw.has(target)) {
return target
}
// 如果target是已经被readonly对象,那么直接返回对应的真实对象
if (readonlyValues.has(target)) {
return readonly(target)
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
前两个if是用来处理这种情况的
// 情况一
const state1 = readonly({ count: 0 })
const state2 = reactive(state1)
// 情况二
const obj = { count: 0 }
const state1 = readonly(obj)
const state2 = reactive(obj)
可以看到reactive它的参数是被readonly的对象,reactive不会对它再次创建响应式,而是通过Map映射,拿到对应的对象,即Proxy <==> Object的相互转换。
createReactiveObject创建响应式对象,注意它的参数
createReactiveObject(
target,
rawToReactive, // Object ==> Proxy
reactiveToRaw, // Proxy ==> Object
mutableHandlers, // get set has ...
mutableCollectionHandlers // 很少会用,不讲了~
)
以上就是reative一开始所做的一些事情,下面继续分析createReactiveObject
createReactiveObject
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 如果不是对象,在开发环境报出警告
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
let observed = toProxy.get(target)
// 如果目标对象已经有proxy对象,直接返回
if (observed !== void 0) {
return observed
}
// 如果目标对象是proxy的对象,并且有对应的真实对象,那么也直接返回
if (toRaw.has(target)) {
return target
}
// 如果它是vnode或者vue,则不能被观测
if (!canObserve(target)) {
return target
}
// 判断被观测的对象是否是set,weakSet,map,weakMap,根据情况使用对应proxy的,配置对象
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
第一个if,判断是否是对象,否则报出警告
toProxy拿到观测对象的Proxy对象,如果存在直接返回
// 这种情况
const obj = { count: 0 }
const state1 = reative(obj)
const state2 = reative(obj)
toRaw拿到Proxy对象对应的真实对象,如果存在直接返回
// 这种情况
const obj = { count: 0 }
const state1 = reative(obj)
const state2 = reative(state1)
有些情况无法被观测,则直接返回观测对象本身
const canObserve = (value: any): boolean => {
return (
!value._isVue &&
!value._isVNode &&
observableValueRE.test(toTypeString(value)) &&
!nonReactiveValues.has(value)
)
}
设置handlers,即get,set等属性访问器, 注意:collectionHandlers是用来处理观测对象为Set,Map等情况,很少见,这里就不讲了
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
然后创建了Proxy对象,并把观测对象和Proxy对象,分别做映射
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
然后在targetMap做了target ==> Map的映射,这又是干嘛,注意:targetMap是全局的
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
在这里先给大家卖个关子,targetMap非常重要,是用来保存依赖的地方
讲完了reactive,可以回到一开始的引子
依赖收集
说到依赖收集,不得不提到,依赖的创建,那么Vue3.0是在哪里创建了渲染依赖呢,大家可以找到下面这段代码以及文件
// vue-next\packages\runtime-core\src\createRenderer.ts
function setupRenderEffect(
instance: ComponentInternalInstance,
parentSuspense: HostSuspsenseBoundary | null,
initialVNode: HostVNode,
container: HostElement,
anchor: HostNode | null,
isSVG: boolean
) {
// create reactive effect for rendering
let mounted = false
instance.update = effect(function componentEffect() {
// ...
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}
代码特别长,我剪掉了中间部分,大家还记得effect有个选项lazy吗,没错,它默认是false,也就会立即调用传入的componentEffect回调,在它内部调用了patch实现了组件的挂载。
敲黑板,关键来了,还记得effect调用,内部会调用run方法吗
function run(effect: ReactiveEffect, fn: Function, args: any[]): any {
if (!effect.active) {
return fn(...args)
}
if (activeReactiveEffectStack.indexOf(effect) === -1) {
cleanup(effect)
try {
activeReactiveEffectStack.push(effect)
return fn(...args)
} finally {
activeReactiveEffectStack.pop()
}
}
}
这里进行了第一步的依赖收集,保存在全局数组中,为了方便触发get的对象,将依赖收集到自己的deps中
然后就是调用patch,进行组件挂载
if (!mounted) {
const subTree = (instance.subTree = renderComponentRoot(instance))
// beforeMount hook
if (instance.bm !== null) {
invokeHooks(instance.bm)
}
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
initialVNode.el = subTree.el
// mounted hook
if (instance.m !== null) {
queuePostRenderEffect(instance.m, parentSuspense)
}
mounted = true
}
至于它内部实现,我就不讲了,不是本文重点,然后我们去编译的地方看看
//vue-next\packages\runtime-core\src\component.ts
function finishComponentSetup(
instance: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null
) {
const Component = instance.type as ComponentOptions
if (!instance.render) {
if (Component.template && !Component.render) {
if (compile) {
Component.render = compile(Component.template, {
onError(err) {}
})
} else if (__DEV__) {
warn(
`Component provides template but the build of Vue you are running ` +
`does not support on-the-fly template compilation. Either use the ` +
`full build or pre-compile the template using Vue CLI.`
)
}
}
if (__DEV__ && !Component.render) {
warn(
`Component is missing render function. Either provide a template or ` +
`return a render function from setup().`
)
}
instance.render = (Component.render || NOOP) as RenderFunction
}
// ...其他
}
上面的代码是编译部分,我们来看看例子中编译后是什么样
(function anonymous(
) {
const _Vue = Vue
const _createVNode = Vue.createVNode
const _hoisted_1 = { id: "box" }
return function render() {
with (this) {
const { toString: _toString, createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock("div", _hoisted_1, [
_createVNode("button", { onClick: increment }, _toString(state.count), 9 /* TEXT, PROPS */, ["onClick"])
]))
}
}
})
可以看到,编译的代码中,有使用到state.count,那么就会触发get访问器,从而收集依赖,至于为什么能直接访问到属性,原因是由于with设置了上下文,下面我们具体分析get
get
// vue-next\packages\reactivity\src\baseHandlers.ts
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res
}
// _isRef
if (isRef(res)) {
return res.value
}
track(target, OperationTypes.GET, key)
// 如果该属性对应的值还是对象,就继续递归创建响应式
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
调用Reflect.get获取属性值
如果key是symbol并且是Symbol的一个属性,就直接返回该值
// 这种情况
const key = Symbol('key')
const state = reative({
[key]: 'symbol value'
})
state[key]
如果值为Ref返回该值的value,看到这里如果大家有了解过ref api的话就知道了,由于ref它自己实现了自己的get,set,所以不再需要执行后面的逻辑,这个在后面会讲
调用track
递归深度观测,使整个对象都为响应式
下面我会详细讲解
track
在讲它之前,先了解它有哪些参数
target: any, // 目标对象
type: OperationTypes, // 追踪数据变化类型,这里是get
key?: string | symbol // 需要获取的key
export const enum OperationTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear',
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
export function track(
target: any,
type: OperationTypes,
key?: string | symbol
) {
if (!shouldTrack) {
return
}
// 获取activeReactiveEffectStack中的依赖
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (effect) {
if (type === OperationTypes.ITERATE) {
key = ITERATE_KEY
}
// 获取目标对象对应的依赖map
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取对应属性的依赖
let dep = depsMap.get(key as string | symbol)
// 如果该依赖不存在
if (!dep) {
// 设置属性对应依赖
depsMap.set(key as string | symbol, (dep = new Set()))
}
// 如果属性对应依赖set中不存在该依赖
if (!dep.has(effect)) {
// 添加到依赖set中
dep.add(effect)
effect.deps.push(dep)
if (__DEV__ && effect.onTrack) {
// 调用onTrack钩子
effect.onTrack({
effect,
target,
type,
key
})
}
}
}
}
activeReactiveEffectStack我两次提到,从它这里拿到了依赖,注意后面执行完依赖后,会从它里面弹出
如果effect存在
从targetMap中获取对象,对饮的Map,具体的数据结构类似这样
const state = reative({
count: 0
})
effect(() => {
console.log(state.count)
})
// 依赖大致结构(随便写的,不太规范)
{
target(state):Map {
count: Set (componentEffect渲染依赖, user自己添加的依赖)
}
}
如果该对象不存在Map,就初始化一个
如果该Map中属性对应的Set不存在,就初始化一个Set
添加依赖到Set中
添加依赖到effect自身的deps数组中
最后调用onTrack回调
// 调用onTrack钩子
effect.onTrack({
effect,
target,
type,
key
})
OK,Track实现大体就这样,是不是也很简单,有了这些基础,后面要讲的一些API就很容易理解了
set
当我们点击按钮后,就会触发set属性访问器
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
value = toRaw(value)
const hadKey = hasOwn(target, key)
const oldValue = target[key]
// 如果旧的值是ref,而新的值不是ref
if (isRef(oldValue) && !isRef(value)) {
// 直接更改原始ref即可
oldValue.value = value
return true
}
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
判断旧值是ref,新值不是ref
// 这种情况
const val = ref(0)
const state = reative({
count: val
})
state.count = 1
// 其实state.count最终还是ref,还是能通过value访问
state.count.value // 1
调用Reflect.set修改值
开发环境下,拿到新旧值组成的对象,调用trigger,为什么开发环境要这么做呢,其实是为了方便onTrigger能拿到新旧值
trigger(target, OperationTypes.ADD, key, extraInfo)
可以看到第二个参数和track是一样的enum,有两种情况,一种我们设置了新的属性和值,另一种修改了原有属性值,下面我们来看看trigger实现。
trigger
export function trigger(
target: any,
type: OperationTypes,
key?: string | symbol,
extraInfo?: any
) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
// effect set
const effects: Set<ReactiveEffect> = new Set()
// computed effect set
const computedRunners: Set<ReactiveEffect> = new Set()
if (type === OperationTypes.CLEAR) {
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// 添加effect到set中
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key as string | symbol))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
// 执行set中的effect
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
computedRunners.forEach(run)
effects.forEach(run)
}
看到这个函数开始的targetMap,大家应该很清楚要干嘛了吧,没错,拿到对象的Map,它包含了属性的所有依赖
如果没有Map直接返回
创建了两个Set,要干嘛用呢
// 用来保存将要执行的依赖
const effects: Set<ReactiveEffect> = new Set()
// computed依赖,因为trigger不仅是要处理effect,watch,还要处理computed惰性求值的情况
const computedRunners: Set<ReactiveEffect> = new Set()
处理三种情况CLEAR,ADD,DELETE,SET(这里没有标识)
// effect set
const effects: Set<ReactiveEffect> = new Set()
// computed effect set
const computedRunners: Set<ReactiveEffect> = new Set()
function addRunners(
effects: Set<ReactiveEffect>,
computedRunners: Set<ReactiveEffect>,
effectsToAdd: Set<ReactiveEffect> | undefined
) {
if (effectsToAdd !== void 0) {
effectsToAdd.forEach(effect => {
if (effect.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
}
可以看到,三种情况实际上都差不多,唯一的区别就是,如果添加的对象是数组,就会拿到length属性的依赖,用于修改数组长度
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
执行属性对应的依赖
// 执行set中的effect
const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}
computedRunners.forEach(run)
effects.forEach(run)
function scheduleRun(
effect: ReactiveEffect,
target: any,
type: OperationTypes,
key: string | symbol | undefined,
extraInfo: any
) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(
extend(
{
effect,
target,
key,
type
},
extraInfo // { oldValue, newValue: value }
)
)
}
if (effect.scheduler !== void 0) {
effect.scheduler(effect)
} else {
effect()
}
}
最后调用了scheduleRun,它内部会分别执行onTrigger,scheduler,effect
需要注意的是,只有开发环境才会执行onTrigger,这也是为什么,前面要这么判断
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
}
readonly
有了前面的基础,readonly看起来会非常简单,唯一的区别就是rawToReadonly,rawToReadonly, readonlyHandlers
export function readonly(target: object) {
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
readonlyHandlers,
readonlyCollectionHandlers
)
}
前两个大家应该能猜出来了,关键是最后这个readonlyHandlers,区别就在set
set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Set operation on key "${key as any}" failed: target is readonly.`,
target
)
}
return true
} else {
return set(target, key, value, receiver)
}
}
它的实现很简单,不过LOCKED有是什么鬼,大家可以找到lock.ts
//vue-next\packages\reactivity\src\lock.ts
export let LOCKED = true
export function lock() {
LOCKED = true
}
export function unlock() {
LOCKED = false
}
看似简单,但是却非常重要,它能够控制被readonly的对象能够暂时被更改,就比如我们常用的props,它是无法被修改的,但是Vue内部又要对他进行更新,那怎么办,话不多说,我们再源码中看他具体应用
// vue-next\packages\runtime-core\src\componentProps.ts
export function resolveProps(
instance: ComponentInternalInstance,
rawProps: any,
_options: ComponentPropsOptions | void
) {
const hasDeclaredProps = _options != null
const options = normalizePropsOptions(_options) as NormalizedPropsOptions
if (!rawProps && !hasDeclaredProps) {
return
}
const props: any = {}
let attrs: any = void 0
const propsProxy = instance.propsProxy
const setProp = propsProxy
? (key: string, val: any) => {
props[key] = val
propsProxy[key] = val
}
: (key: string, val: any) => {
props[key] = val
}
unlock()
// 省略一些修改props操作。。
lock()
instance.props = __DEV__ ? readonly(props) : props
instance.attrs = options
? __DEV__ && attrs != null
? readonly(attrs)
: attrs
: instance.props
}
这里前后分别调用了unlock和lock,这样就可以控制对readonly属性的修改
那么readonly的讲解就到这了
computed
export function computed<T>(
getterOrOptions: (() => T) | WritableComputedOptions<T>
): any {
const isReadonly = isFunction(getterOrOptions)
const getter = isReadonly
? (getterOrOptions as (() => T))
: (getterOrOptions as WritableComputedOptions<T>).get
const setter = isReadonly
? null
: (getterOrOptions as WritableComputedOptions<T>).set
let dirty: boolean = true
let value: any = undefined
const runner = effect(getter, {
lazy: true,
computed: true,
scheduler: () => {
dirty = true
}
})
return {
_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
trackChildRun(runner)
return value
},
set value(newValue) {
if (setter) {
setter(newValue)
} else {
// TODO warn attempting to mutate readonly computed value
}
}
}
}
首先是前面这段
const isReadonly = isFunction(getterOrOptions)
const getter = isReadonly
? (getterOrOptions as (() => T))
: (getterOrOptions as WritableComputedOptions<T>).get
const setter = isReadonly
? null
: (getterOrOptions as WritableComputedOptions<T>).set
大家都知道computed是可以单独写一个函数,或者get,set访问的,这里不多讲
然后调用了effect,这里lazy设置为true, scheduler可以更改dirty为true
const runner = effect(getter, {
lazy: true,
computed: true,
scheduler: () => {
dirty = true
}
})
然后我们具体来看看,返回的对象
{
_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
trackChildRun(runner)
return value
},
set value(newValue) {
if (setter) {
setter(newValue)
} else {
// TODO warn attempting to mutate readonly computed value
}
}
}
先说说set吧,尤大似乎还没写完,只是单纯能修改值
然后是get,注意dirty的变化,如果computed依赖了state中的值,初次渲染时,他会调用依赖,然后dirty = false,关键来了,最后执行了trackChildRun
function trackChildRun(childRunner: ReactiveEffect) {
const parentRunner =
activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (parentRunner) {
for (let i = 0; i < childRunner.deps.length; i++) {
const dep = childRunner.deps[i]
if (!dep.has(parentRunner)) {
dep.add(parentRunner)
parentRunner.deps.push(dep)
}
}
}
}
由于computed是依赖了state中的属性的,一旦在初始时触发了get,执行runner,就会将依赖收集到activeReactiveEffectStack中,最后才是自己的依赖,栈的顶部是state属性的依赖
if (!dep.has(parentRunner)) {
dep.add(parentRunner)
parentRunner.deps.push(dep)
}
所以最后这段代码实现了state属性变化后,才导致了computed依赖的调用,从而惰性求值
ref
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)
export function ref<T>(raw: T): Ref<T> {
raw = convert(raw)
const v = {
_isRef: true,
get value() {
track(v, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
trigger(v, OperationTypes.SET, '')
}
}
return v as Ref<T>
}
ref的实现真的很简单了,前面已经学习了那么多,相信大家都能看懂了,区别就是convert(raw)对传入的值进行了简单判断,如果是对象就设置为响应式,否则返回原始值。
以上是“Vue3.0响应式系统的示例分析”这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注天达云行业资讯频道!