ホームページ >ウェブフロントエンド >Vue.js >vue3のreactiveとrefの違いを詳しく解説(ソースコード解析)

vue3のreactiveとrefの違いを詳しく解説(ソースコード解析)

青灯夜游
青灯夜游転載
2022-08-22 19:53:303271ブラウズ

vue の reactive と ref の違いは何ですか?次の記事では、vue3 の reactive と ref の違いを徹底的に理解するために、ソース コードを詳しく説明します。お役に立てば幸いです。

vue3のreactiveとrefの違いを詳しく解説(ソースコード解析)

vue3 の日々の開発では、多くの人が自分の習慣に基づいてシャトルを使用していることがわかりましたreactiveorrefこれで要件を満たすことはできますが、その場合、すでに reactive があるのに、なぜ別の ref を設計する必要があるのでしょうか?実際のアプリケーション シナリオと 2 つの違いは何ですか?

そして、ref の基礎となるロジックに関して、ref の基礎となるロジックは依然として reactive であると言う人もいます。 ref の最下層は class であり、valueclass の単なる属性であると言う人もいます。そうですか?毛織物?これには何か根拠があるのでしょうか?

この質問では、今回はソースコードを深く掘り下げて、vue3のreactiverefの違いを徹底的に理解していきます。 (学習ビデオ共有: vue ビデオ チュートリアル )

ソース コードを見たくない場合は、後ろにスクロールして概要を読むことができます

reactive

ソースコードアドレス: packages/reactivity/reactive.ts

まず見てみましょうvue3 ReactiveFlags<pre class="brush:php;toolbar:false">// 标记目标对象 target 类型的 ReactiveFlags export 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 对象时会有该属性 }</pre>

reactive

export function reactive<t>(target: T): UnwrapNestedRefs<t>
export 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
  )
}</t></t>
# で使用されるターゲットで## タイプ Objecttarget

reactive 関数は target オブジェクトを受け取ります。target オブジェクトが読み取り専用の場合は、戻り値が返されます。

読み取り専用でない場合は、

を直接渡します。 #createReactiveObject

長くなっても心配しないでください。まず createReactiveObject の完全なコードを投稿してください。セクションに分けて読んでみましょう <pre class="brush:php;toolbar:false">/**  *   * @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&lt;any&gt;,   collectionHandlers: ProxyHandler&lt;any&gt;,   proxyMap: WeakMap&lt;target&gt; ) {   // 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] &amp;&amp;     !(isReadonly &amp;&amp; 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 }&lt;/target&gt;&lt;/any&gt;&lt;/any&gt;</pre> まず、createReactiveObject が 5 つのパラメータを受け取ることがわかります。

  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<target></target></any></any>

target ターゲット オブジェクト

isReadonly 読み取られるかどうか-only

#baseHandlers 基本型ハンドラー 配列、オブジェクトの処理

collectionHandlers セット、マップ、weakSet、weakMap# の処理

##proxyMap

WeakMap データ構造には副作用関数が格納されます

ここでは主に

ReactiveFlags を使用します。RAWReactiveFlags.IS_REACTIVE

は、応答データである場合、オブジェクトは直接返されます

 if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
すでに Proxy
であるものについては、

WeakMap# から Proxy オブジェクトを直接取り出します## データ構造と戻り値 <pre class="brush:php;toolbar:false">  const existingProxy = proxyMap.get(target)   if (existingProxy) {     return existingProxy   }</pre> ここでは、現在の target

の型が
Object

Array## であるかどうかを確認します。 #, Map, Set

,
WeakMap

, WeakSet、何もない場合は、直接応答処理を実行せずにオブジェクトを返します<pre class="brush:php;toolbar:false"> // 只对targetTypeMap类型白名单中的类型进行响应式处理   const targetType = getTargetType(target)   if (targetType === TargetType.INVALID) {     return target   }</pre> 検証タイプのロジック<pre class="brush:php;toolbar:false">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   } }</pre>すべての事前検証が完了したら、proxy AgenttargetObjectA を使用できます。 三項演算子

は、ここでさまざまな処理ロジックを実行するために使用されます

# を通じて

TargetType.COLLECTION

##(set,map,weakSet,weakMap) collectionHandlers#を使用します(オブジェクト、配列) baseHandlers

<pre class="brush:php;toolbar:false">// 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</pre>## を使用します # これで、createReactiveObject の実行ロジックが非常に明確になりました

    まだ終わっていません。
  • createReactiveObject の最後の proxy
  • target をプロキシするにはどうすればよいですか?ここでは例として baseHandlers
  • を使用し、
baseHandlers
の奥深くに進み、

baseHandlers# を確認します。 #ソース コード アドレス: packages/reactivity/baseHandlers.tsreactive.ts では、合計 4 つのハンドラーが導入

import {
  mutableHandlers,
  readonlyHandlers,
  shallowReactiveHandlers,
  shallowReadonlyHandlers
} from './baseHandlers'

#mutableHandlers 変数処理##readonlyHandlers 読み取り専用処理

##shallowReactiveHandlers

浅い観察 処理 (ターゲット オブジェクトの第 1 レベルのプロパティのみを観察します)

shallowReadonlyHandlers

浅い観察 && 読み取り専用

We take
    例として mutableHandlers
  • <pre class="brush:php;toolbar:false">// 可变处理 // const get = /*#__PURE__*/ createGetter() // const set = /*#__PURE__*/ createSetter() // get、has、ownKeys 会触发依赖收集 track() // set、deleteProperty 会触发更新 trigger() export const mutableHandlers: ProxyHandler&lt;object&gt; = {   get,                  // 用于拦截对象的读取属性操作   set,                  // 用于拦截对象的设置属性操作   deleteProperty,       // 用于拦截对象的删除属性操作   has,                  // 检查一个对象是否拥有某个属性   ownKeys               // 针对 getOwnPropertyNames,  getOwnPropertySymbols, keys 的代理方法 }&lt;/object&gt;</pre>ここでの
  • get
  • set
  • createGetter()
  • createSetter()# に対応します。
  • ##createGetter()

最初にコードの完全版に移動します<pre class="brush:php;toolbar:false">/**  * 用于拦截对象的读取属性操作  * @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 &amp;&amp;       receiver ===         (isReadonly           ? shallow             ? shallowReadonlyMap             : readonlyMap           : shallow           ? shallowReactiveMap           : reactiveMap         ).get(target)     ) {       return target     }     const targetIsArray = isArray(target)     // 如果目标对象 不为只读、是数组、key属于arrayInstrumentations:['includes', 'indexOf', 'lastIndexOf']方法之一,即触发了这三个方法之一     if (!isReadonly &amp;&amp; targetIsArray &amp;&amp; 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   } }</pre> 長く見えますが、最終的には

track()

Dependency collectiontrack()Dependency collection の内容が多すぎます。と trigger() は一緒に更新をトリガーするため、別の記事を書きます。<pre class="brush:php;toolbar:false">/**  * 拦截对象的设置属性操作  * @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) &amp;&amp; isRef(oldValue) &amp;&amp; !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) &amp;&amp; isIntegerKey(key)         ? Number(key) &lt;p&gt;&lt;code&gt;trigger()&lt;/code&gt;触发更新&lt;/p&gt;&lt;h2 data-id=&quot;heading-7&quot;&gt;&lt;strong&gt;ref&lt;/strong&gt;&lt;/h2&gt;&lt;p&gt;源码地址:&lt;code&gt;packages/reactivity/src/ref.ts&lt;/code&gt;&lt;/p&gt;&lt;p&gt;接收一个可选&lt;code&gt;unknown&lt;/code&gt;,接着直接调用&lt;code&gt;createRef()&lt;/code&gt;&lt;/p&gt;&lt;pre class=&quot;brush:php;toolbar:false&quot;&gt;export function ref(value?: unknown) {   return createRef(value, false) }</pre><p><img src="https://img.php.cn/upload/image/632/159/341/1661168925472417.png" title="1661168925472417.png" alt="vue3のreactiveとrefの違いを詳しく解説(ソースコード解析)"></p><p>与<code>ref的区别就是在调用createRef()时第二个值传的是true

export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

看一下官方文档上对shallowRef的解释

vue3のreactiveとrefの違いを詳しく解説(ソースコード解析)

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<t> {
  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) // 触发依赖,派发更新
    }
  }
}</t>

根据RefImpl我们可以看到ref的底层逻辑,如果是对象确实会使用reactive进行处理,并且ref的创建使用的也是RefImpl class实例,value只是RefImpl的属性

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

  • toReactive

我们来看一下toReactive()这个方法,在RefImpl中创建ref数据时会调用toReactive()方法,这里会先判断传进来的值是不是对象,如果是就用reactive()包裹,否则就返回其本身

export const toReactive = <t>(value: T): T =>
  isObject(value) ? reactive(value) : value</t>
  • trackRefValue

ref的依赖收集方法

export function trackRefValue(ref: RefBase<any>) {
  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)
    }
  }
}</any>
  • triggerRefValue

ref的派发更新方法

export function triggerRefValue(ref: RefBase<any>, 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)
    }
  }
}</any>

总结

看完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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。