>首页> IT >

视点!详解vue3中reactive和ref的区别(源码解析)

时间:2022-08-22 19:58:07       来源:转载
vue中reactive和ref的区别是什么?下面本篇文章带大家深入源码彻底搞清vue3中reactive和ref的区别,希望对大家有所帮助!

如何快速入门VUE3.0:进入学习

在vue3的日常开发中,我发现很多人都是基于自己的习惯reactiveref一把梭,虽然这样都可以实现需求,既然这样那为什么已经有了reactive还需要再去设计一个ref呢?这两者的实际运用场景以及区别是什么呢?


(资料图)

并且关于ref的底层逻辑,有的人说ref的底层逻辑还是reactive。有的人说ref的底层是classvalue只是这个class的一个属性,那这两种说法哪种正确呢?都有没有依据呢?

抱着这样的疑问我们本次就深入源码,彻底搞清vue3中reactiveref的区别。(学习视频分享: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

proxyMapWeakMap数据结构存储副作用函数


这里主要是通过ReactiveFlags.RAWReactiveFlags.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的类型是不是ObjectArrayMapSetWeakMapWeakSet,如果都不是则直接返回该对象,不做响应式处理

// 只对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,                  // 用于拦截对象的设置属性操作  deleteProperty,       // 用于拦截对象的删除属性操作  has,                  // 检查一个对象是否拥有某个属性  ownKeys               // 针对 getOwnPropertyNames,  getOwnPropertySymbols, keys 的代理方法}

这里的getset分别对应着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()依赖收集

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(是否是浅观察),具体使用场景可看上面refshallowRef的介绍

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的创建使用的也是RefImplclass实例,value只是RefImpl的属性

在我们访问设置ref的value值时,也分别是通过getset拦截进行依赖收集派发更新

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)    }  }}

总结

看完reactiveref源码,相信对本文一开始的几个问题也都有了答案,这里也总结了几个问题:

问: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的值。这样响应式数据就能够与副作用函数建立响应联系

(学习视频分享:web前端开发、编程基础视频)

以上就是详解vue3中reactive和ref的区别(源码解析)的详细内容,更多请关注php中文网其它相关文章!

关键词: 目标对象 一个对象 数据结构