vue中reactive和ref的区别是什么?下面本篇文章带大家深入源码彻底搞清vue3中reactive和ref的区别,希望对大家有所帮助!
在vue3的日常开发中,我发现很多人都是基于自己的习惯reactive或ref一把梭,虽然这样都可以实现需求,既然这样那为什么已经有了reactive还需要再去设计一个ref呢?这两者的实际运用场景以及区别是什么呢?
并且关于ref的底层逻辑,有的人说ref的底层逻辑还是reactive。有的人说ref的底层是class,value只是这个class的一个属性,那这两种说法哪种正确呢?都有没有依据呢?
抱着这样的疑问我们本次就深入源码,彻底搞清vue3中reactive和ref的区别。(学习视频分享:vue)
立即学习“前端免费学习笔记(深入)”;
不想看源码的童鞋,可以直接拉到后面看总结
reactive
源码地址:packages/reactivity/reactive.ts
首先我们看一下vue3中用来标记目标对象target类型的ReactiveFlags
- // 标记目标对象 target 类型的 ReactiveFlagsexport const enum ReactiveFlags { SKIP = '__v_skip', IS_REACTIVE = '__v_isReactive', IS_READONLY = '__v_isReadonly', RAW = '__v_raw'}export interface Target { [ReactiveFlags.SKIP]?: boolean // 不做响应式处理的数据 [ReactiveFlags.IS_REACTIVE]?: boolean // target 是否是响应式 [ReactiveFlags.IS_READONLY]?: boolean // target 是否是只读 [ReactiveFlags.RAW]?: any // 表示proxy 对应的源数据, target 已经是 proxy 对象时会有该属性}
登录后复制
reactive
- export function reactive(target: T): UnwrapNestedRefsexport function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. // 如果目标对象是一个只读的响应数据,则直接返回目标对象 if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } // 创建 observe return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap )}
登录后复制
reactive函数接收一个target对象,如果target对象只读则直接返回该对象
若非只读则直接通过createReactiveObject创建observe对象
createReactiveObject
看着长不要怕,先贴createReactiveObject完整代码,我们分段阅读
- /** * * @param target 目标对象 * @param isReadonly 是否只读 * @param baseHandlers 基本类型的 handlers * @param collectionHandlers 主要针对(set、map、weakSet、weakMap)的 handlers * @param proxyMap WeakMap数据结构 * @returns */function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler, proxyMap: WeakMap) { // typeof 不是 object 类型的,在开发模式抛出警告,生产环境直接返回目标对象 if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // target is already a Proxy, return it. // exception: calling readonly() on a reactive object // 已经是响应式的就直接返回(取ReactiveFlags.RAW 属性会返回true,因为进行reactive的过程中会用weakMap进行保存, // 通过target能判断出是否有ReactiveFlags.RAW属性) // 例外:对reactive对象进行readonly() if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // target already has corresponding Proxy // 对已经Proxy的,则直接从WeakMap数据结构中取出这个Proxy对象 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // only a whitelist of value types can be observed. // 只对targetTypeMap类型白名单中的类型进行响应式处理 const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } // proxy 代理 target // (set、map、weakSet、weakMap) collectionHandlers // (Object、Array) baseHandlers const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy}
登录后复制
首先我们看到createReactiveObject接收了五个参数
- target: Target, isReadonly: boolean, baseHandlers: ProxyHandler, collectionHandlers: ProxyHandler, proxyMap: WeakMap
登录后复制
target 目标对象
isReadonly 是否只读
baseHandlers 基本类型的 handlers 处理数组,对象
collectionHandlers 处理 set、map、weakSet、weakMap
proxyMap WeakMap数据结构存储副作用函数
这里主要是通过ReactiveFlags.RAW和ReactiveFlags.IS_REACTIVE判断是否是响应式数据,若是则直接返回该对象
- if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target }
登录后复制
对于已经是Proxy的,则直接从WeakMap数据结构中取出这个Proxy对象并返回
- const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy }
登录后复制
这里则是校验了一下当前target的类型是不是Object、Array、Map、Set、WeakMap、WeakSet,如果都不是则直接返回该对象,不做响应式处理
- // 只对targetTypeMap类型白名单中的类型进行响应式处理 const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target }
登录后复制
校验类型的逻辑
- function getTargetType(value: Target) { return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID : targetTypeMap(toRawType(value))}function targetTypeMap(rawType: string) { switch (rawType) { case 'Object': case 'Array': return TargetType.COMMON case 'Map': case 'Set': case 'WeakMap': case 'WeakSet': return TargetType.COLLECTION default: return TargetType.INVALID }}
登录后复制
所有的前置校验完后,就可以使用proxy 代理target对象了
这里使用了一个三目运算符通过TargetType.COLLECTION来执行不同的处理逻辑
(set、map、weakSet、weakMap) 使用 collectionHandlers(Object、Array) 使用 baseHandlers
- // proxy 代理 target // (set、map、weakSet、weakMap) collectionHandlers // (Object、Array) baseHandlers const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy
登录后复制
现在对createReactiveObject的执行逻辑是不是就很清晰了
到这里还没有结束,createReactiveObject中最后proxy是如何去代理target的呢?这里我们用baseHandlers举例,深入baseHandlers的内部去看看
baseHandlers
源码地址:packages/reactivity/baseHandlers.ts
在reactive.ts中我们可以看到一共引入了四种 handler
- import { mutableHandlers, readonlyHandlers, shallowReactiveHandlers, shallowReadonlyHandlers} from './baseHandlers'
登录后复制mutableHandlers 可变处理readonlyHandlers 只读处理shallowReactiveHandlers 浅观察处理(只观察目标对象的第一层属性)shallowReadonlyHandlers 浅观察 && 只读
我们以mutableHandlers为例
- // 可变处理// const get = /*#__PURE__*/ createGetter()// const set = /*#__PURE__*/ createSetter()// get、has、ownKeys 会触发依赖收集 track()// set、deleteProperty 会触发更新 trigger()export const mutableHandlers: ProxyHandler
登录后复制
这里的get和set分别对应着createGetter()、createSetter()
createGetter()
先上完整版代码
- /** * 用于拦截对象的读取属性操作 * @param isReadonly 是否只读 * @param shallow 是否浅观察 * @returns */function createGetter(isReadonly = false, shallow = false) { /** * @param target 目标对象 * @param key 需要获取的值的键值 * @param receiver 如果遇到 setter,receiver 则为setter调用时的this值 */ return function get(target: Target, key: string | symbol, receiver: object) { // ReactiveFlags 是在reactive中声明的枚举值,如果key是枚举值则直接返回对应的布尔值 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( // 如果key是raw receiver 指向调用者,则直接返回目标对象。 // 这里判断是为了保证触发拦截 handle 的是 proxy 本身而不是 proxy 的继承者 // 触发拦的两种方式:一是访问 proxy 对象本身的属性,二是访问对象原型链上有 proxy 对象的对象的属性,因为查询会沿着原型链向下找 key === ReactiveFlags.RAW && receiver === (isReadonly ? shallow ? shallowReadonlyMap : readonlyMap : shallow ? shallowReactiveMap : reactiveMap ).get(target) ) { return target } const targetIsArray = isArray(target) // 如果目标对象 不为只读、是数组、key属于arrayInstrumentations:['includes', 'indexOf', 'lastIndexOf']方法之一,即触发了这三个方法之一 if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { // 通过 proxy 调用,arrayInstrumentations[key]的this一定指向 proxy return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) // 如果 key 是 symbol 内置方法,或者访问的是原型对象__proto__,直接返回结果,不收集依赖 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res } // 不是只读类型的 target 就收集依赖。因为只读类型不会变化,无法触发 setter,也就会触发更新 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 如果是浅观察,不做递归转化,就是说对象有属性值还是对象的话不递归调用 reactive() if (shallow) { return res } // 如果get的结果是ref if (isRef(res)) { // ref unwrapping - does not apply for Array + integer key. // 返回 ref.value,数组除外 const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } // 由于 proxy 只能代理一层,如果子元素是对象,需要递归继续代理 if (isObject(res)) { // Convert returned value into a proxy as well. we do the isObject check // here to avoid invalid value warning. Also need to lazy access readonly // and reactive here to avoid circular dependency. return isReadonly ? readonly(res) : reactive(res) } return res }}
登录后复制
看着长,最终就是track()依赖收集
track()依赖收集内容过多,和trigger()触发更新一起,单开一篇文章
createSetter()
- /** * 拦截对象的设置属性操作 * @param shallow 是否是浅观察 * @returns */function createSetter(shallow = false) { /** * @param target 目标对象 * @param key 设置的属性名称 * @param value 要改变的属性值 * @param receiver 如果遇到setter,receiver则为setter调用时的this值 */ return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { let oldValue = (target as any)[key] // 如果模式不是浅观察模式 if (!shallow) { // 拿新值和老值的原始值,因为新传入的值可能是响应式数据,如果直接和 target 上原始值比较是没有意义的 value = toRaw(value) oldValue = toRaw(oldValue) // 目标对象不是数组,旧值是ref,新值不是ref,则直接赋值,这里提到ref if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else { // in shallow mode, objects are set as-is regardless of reactive or not } // 检查对象是否有这个属性 const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) // 赋值 const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original // reactive是proxy实例才触发更新,防止通过原型链触发拦截器触发更新 if (target === toRaw(receiver)) { if (!hadKey) { // 如果不存在则trigger ADD trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 如果新旧值不相等则trigger SET trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result }}
登录后复制
trigger()触发更新
ref
源码地址:packages/reactivity/src/ref.ts
接收一个可选unknown,接着直接调用createRef()
- export function ref(value?: unknown) { return createRef(value, false)}
登录后复制
与ref的区别就是在调用createRef()时第二个值传的是true
- export function shallowRef(value?: unknown) { return createRef(value, true)}
登录后复制
看一下官方文档上对shallowRef的解释
createRef
通过isRef()判断是否是ref数据,是则直接返回该数据,不是则通过new RefImpl创建ref数据
在创建时会传两个值一个是rawValue(原始值),一个是shallow(是否是浅观察),具体使用场景可看上面ref和shallowRef的介绍
- function createRef(rawValue: unknown, shallow: boolean) { // 是否是 ref 数据 if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow)}
登录后复制
isRef()
通过__v_isRef只读属性判断是否是ref数据,此属性会在RefImpl创建ref数据时添加
- export function isRef(r: any): r is Ref { return Boolean(r && r.__v_isRef === true)}
登录后复制
RefImpl
- class RefImpl { private _value: T private _rawValue: T public dep?: Dep = undefined // 只读属性 __v_isRef 判断是否是ref数据的静态标识 public readonly __v_isRef = true constructor(value: T, public readonly _shallow: boolean) { this._rawValue = _shallow ? value : toRaw(value) // 非浅观察用toRaw()包裹原始值 this._value = _shallow ? value : toReactive(value) // 非浅观察用toReactive()处理数据 } get value() { // 依赖收集 trackRefValue(this) return this._value } set value(newVal) { newVal = this._shallow ? newVal : toRaw(newVal) // 非浅观察用toRaw()包裹值 // 两个值不相等 if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal this._value = this._shallow ? newVal : toReactive(newVal) triggerRefValue(this, newVal) // 触发依赖,派发更新 } }}
登录后复制
根据RefImpl我们可以看到ref的底层逻辑,如果是对象确实会使用reactive进行处理,并且ref的创建使用的也是RefImpl class实例,value只是RefImpl的属性
在我们访问和设置 ref的value值时,也分别是通过get和set拦截进行依赖收集和派发更新的
toReactive
我们来看一下toReactive()这个方法,在RefImpl中创建ref数据时会调用toReactive()方法,这里会先判断传进来的值是不是对象,如果是就用reactive()包裹,否则就返回其本身
- export const toReactive = (value: T): T => isObject(value) ? reactive(value) : value
登录后复制
trackRefValue
ref的依赖收集方法
- export function trackRefValue(ref: RefBase) { if (isTracking()) { ref = toRaw(ref) if (!ref.dep) { ref.dep = createDep() } if (__DEV__) { trackEffects(ref.dep, { target: ref, type: TrackOpTypes.GET, key: 'value' }) } else { trackEffects(ref.dep) } }}
登录后复制
triggerRefValue
ref的派发更新方法
- export function triggerRefValue(ref: RefBase, newVal?: any) { ref = toRaw(ref) if (ref.dep) { if (__DEV__) { triggerEffects(ref.dep, { target: ref, type: TriggerOpTypes.SET, key: 'value', newValue: newVal }) } else { triggerEffects(ref.dep) } }}
登录后复制
总结
看完reactive和ref源码,相信对本文一开始的几个问题也都有了答案,这里也总结了几个问题:
问:ref的底层逻辑是什么,具体是如何实现的
答:ref底层会通过 new RefImpl()来创造ref数据,在new RefImpl()会首先给数据添加__v_isRef只读属性用来标识ref数据。而后判断传入的值是否是对象,如果是对象则使用toReactive()处理成reactive,并将值赋给RefImpl()的value属性上。在访问和设置ref数据的value时会分别触发依赖收集和派发更新流程。
问:ref底层是否会使用reactive处理数据
答:RefImpl中非浅观察会调用toReactive()方法处理数据,toReactive()中会先判断传入的值是不是一个对象,如果是对象则使用reactive进行处理,不是则直接返回值本身。
问:为什么已经有了reactive还需要在设计一个ref呢?
答: 因为vue3响应式方案使用的是proxy,而proxy的代理目标必须是非原始值,没有任何方式能去拦截对原始值的操作,所以就需要一层对象作为包裹,间接实现原始值的响应式方案。
问:为什么ref数据必须要有个value属性,访问ref数据必须要通过.value的方式呢?
答:这是因为要解决响应式丢失的问题,举个例子:
- // obj是响应式数据const obj = reactive({ foo: 1, bar: 2 })// newObj 对象下具有与 obj对象同名的属性,并且每个属性值都是一个对象// 该对象具有一个访问器属性 value,当读取 value的值时,其实读取的是 obj 对象下相应的属性值 const newObj = { foo: { get value() { return obj.foo } }, bar: { get value() { return obj.bar } }}effect(() => { // 在副作用函数内通过新对象 newObj 读取 foo 的属性值 console.log(newObj.foo)})// 正常触发响应obj.foo = 100
登录后复制
可以看到,在现在的newObj对象下,具有与obj对象同名的属性,而且每个属性的值都是一个对象,例如foo 属性的值是:
- { get value() { return obj.foo }}
登录后复制
该对象有一个访问器属性value,当读取value的值时,最终读取的是响应式数据obj下的同名属性值。也就是说,当在副作用函数内读取newObj.foo时,等价于间接读取了obj.foo的值。这样响应式数据就能够与副作用函数建立响应联系
(学习视频分享:vue、vue)
以上就是详解vue3中reactive和ref的区别(源码解析)的详细内容,更多请关注【创想鸟】其它相关文章!