詳解vue3中reactive和ref的區別(源碼解析)
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 类型的 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 对象时会有该属性 }
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>
reactive函數接收一個
target對象,如果
target物件唯讀則直接傳回該物件
createReactiveObject建立
observe物件
#createReactiveObject#看著長不要怕,先貼
完整程式碼,我們分段閱讀<div class="code" style="position:relative; padding:0px; margin:0px;"><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<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<target>
) {
// 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
}</target></any></any></pre><div class="contentsignin">登入後複製</div></div>
首先我們看到
接收了五個參數<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false"> target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<target></target></any></any></pre><div class="contentsignin">登入後複製</div></div>
isReadonly 是否只讀
#baseHandlers 基本類型的handlers
處理數組,物件
collectionHandlers處理set、map、weakSet、weakMap
proxyMapWeakMap資料結構儲存副作用函數##這裡主要是透過
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) 使用
-
(Object、Array) 使用
baseHandlers #<div class="code" style="position:relative; padding:0px; margin:0px;"><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><div class="contentsignin">登入後複製</div></div>
現在對
的執行邏輯是不是就很清晰了
到這裡還沒結束,
createReactiveObject
proxy是如何去代理
target的呢?這裡我們用
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<object> = { get, // 用于拦截对象的读取属性操作 set, // 用于拦截对象的设置属性操作 deleteProperty, // 用于拦截对象的删除属性操作 has, // 检查一个对象是否拥有某个属性 ownKeys // 针对 getOwnPropertyNames, getOwnPropertySymbols, keys 的代理方法 }</object>
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()
依賴收集內容過多,和
觸發更新一起,單開一篇文章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) <p><code>trigger()</code>触发更新</p><h2 id="strong-ref-strong"><strong>ref</strong></h2><p>源码地址:<code>packages/reactivity/src/ref.ts</code></p><p>接收一个可选<code>unknown</code>,接着直接调用<code>createRef()</code></p><pre class="brush:php;toolbar:false">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<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值时,也分别是通过get
和set
拦截进行依赖收集
和派发更新
的
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>
总结
看完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
的值。这样响应式数据就能够与副作用函数建立响应联系
以上是詳解vue3中reactive和ref的區別(源碼解析)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

vue3+vite:src使用require動態導入圖片報錯和解決方法vue3+vite動態的導入多張圖片vue3如果使用的是typescript開發,就會出現require引入圖片報錯,requireisnotdefined不能像使用vue2這樣imgUrl:require(' …/assets/test.png')導入,是因為typescript不支援require所以用import導入,下面介紹如何解決:使用awaitimport

tinymce是一個功能齊全的富文本編輯器插件,但在vue中引入tinymce並不像別的Vue富文本插件一樣那麼順利,tinymce本身並不適配Vue,還需要引入@tinymce/tinymce-vue,並且它是國外的富文本插件,沒有透過中文版本,需要在其官網下載翻譯包(可能需要翻牆)。 1.安裝相關依賴npminstalltinymce-Snpminstall@tinymce/tinymce-vue-S2、下載中文包3.引入皮膚和漢化包在項目public資料夾下新建tinymce資料夾,將下載的

Vue實作部落格前端,需要實作markdown的解析,如果有程式碼則需要實作程式碼的高亮。 Vue的markdown解析函式庫很多,如markdown-it、vue-markdown-loader、marked、vue-markdown等。這些庫都大同小異。這裡選用的是marked,程式碼高亮的函式庫選用的是highlight.js。具體實現步驟如下:一、安裝依賴庫在vue專案下開啟命令窗口,並輸入以下命令npminstallmarked-save//marked用於將markdown轉換成htmlnpmins

想要實現頁面的局部刷新,我們只需要實現局部元件(dom)的重新渲染。在Vue中,想要實現這效果最簡單的方式方法就是使用v-if指令。在Vue2中我們除了使用v-if指令讓局部dom的重新渲染,也可以新建一個空白元件,需要刷新局部頁面時跳轉至這個空白元件頁面,然後在空白元件內的beforeRouteEnter守衛中又跳轉回原來的頁面。如下圖所示,如何在Vue3.X中實現點擊刷新按鈕實現紅框範圍內的dom重新加載,並展示對應的加載狀態。由於Vue3.X中scriptsetup語法中組件內守衛只有o

最終效果安裝VueCropper組件yarnaddvue-cropper@next上面的安裝值針對Vue3的,如果時Vue2或想使用其他的方式引用,請訪問它的npm官方地址:官方教程。在元件中引用使用時也很簡單,只需要引入對應的元件和它的樣式文件,我這裡沒有在全域引用,只在我的元件檔案中引入import{userInfoByRequest}from'../js/api' import{VueCropper}from'vue-cropper&

前言無論是vue還是react,當我們遇到多處重複程式碼的時候,我們都會想著如何重複使用這些程式碼,而不是一個檔案裡充斥著一堆冗餘程式碼。實際上,vue和react都可以透過抽組件的方式來達到復用,但如果遇到一些很小的程式碼片段,你又不想抽到另外一個檔案的情況下,相比而言,react可以在相同文件裡面宣告對應的小元件,或透過renderfunction來實現,如:constDemo:FC=({msg})=>{returndemomsgis{msg}}constApp:FC=()=>{return(

使用Vue建構自訂元素WebComponents是一組web原生API的統稱,允許開發者建立可重複使用的自訂元素(customelements)。自訂元素的主要好處是,它們可以在使用任何框架,甚至在不使用框架的場景下使用。當你面向的最終用戶可能使用了不同的前端技術棧,或者當你希望將最終的應用與它使用的組件實現細節解耦時,它們會是理想的選擇。 Vue和WebComponents是互補的技術,Vue為使用和創建自訂元素提供了出色的支援。你可以將自訂元素整合到現有的Vue應用中,或使用Vue來構

vue3+ts+axios+pinia實作無感刷新1.先在專案中下載aiXos和pinianpmipinia--savenpminstallaxios--save2.封裝axios請求-----下載js-cookienpmiJS-cookie-s//引入aixosimporttype{AxiosRequestConfigig ,AxiosResponse}from"axios";importaxiosfrom'axios';import{ElMess
