Vue3響應式系統源碼解析-Ref篇

2021-03-02 前端早茶
前言的前言閱讀本文需要有一定的TypeScript基礎,要求不高,看過一遍TS的文檔即可。

我們閱讀源碼的原因是什麼?無非是1:學習;2:更好的使用這個庫。如果只是想大致的了解下原理,倒不必花時間閱讀源碼,幾句話,幾張圖就能搞清楚,網上搜搜應該就有很多。因此,閱讀源碼的過程一定是要對不明白的地方深入了解,肯定是很費時間的。

在這過程中,有些知識點,跟庫本身可能沒什麼關係,但如果不懂,又難繼續理解。對於這些知識點,我會儘量少的解釋,但會貼上儘量完善的文檔,方便不了解的同學先閱讀學習。

鑑於篇幅太長,信息量較大,我會將文章拆開,邊寫邊發,有興趣的同學可以連載閱讀,寫完以後再匯總一篇,方便時間充沛的同學一股腦看。

前言

在上篇文章中說道,ref是最影響源碼閱讀的文件。但如果不先搞明白它,看其他的只會更暈。我先幫大家理清ref的邏輯跟概念。

由於現在(2019/10/9)vue@3還未正式發版,大家還不熟悉其相關的用法。上篇文章雖然介紹了不少,但其實還是有不少疑問。在閱讀本篇文章之前,如果有時間,建議先閱讀Vue官方對Composition API的介紹: 1. Vue Composition API 2. Ref Vs Reactive

讀完關於Composition API的介紹,會對了解本庫有更多認識,便於更好的理解源碼。

ref跟reactive是整個源碼中的核心,通過這兩個方法創建了響應式數據。要想完全吃透reactivity,必須先吃透這兩個。

Ref

ref最重要的作用,其實是提供了一套Ref類型,我們先來看,它到底是個怎麼樣的數據類型。(為了更好的做解釋,我會調整源碼中的接口、類型、函數等聲明順序,並會增加一些注釋方便閱讀)

// 生成一個唯一key,開發環境下增加描述符 'refSymbol'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)

// 聲明Ref接口
export interface Ref<T = any> {
// 用此唯一key,來做Ref接口的一個描述符,讓isRef函數做類型判斷
[refSymbol]: true
// value值,存放真正的數據的地方。關於UnwrapNestedRefs這個類型,我後續單獨解釋
value: UnwrapNestedRefs<T>
}

// 判斷是否是Ref數據的方法
// 對於is關鍵詞,若不熟悉,見:http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
export function isRef(v: any): v is Ref {
return v ? v[refSymbol] === true : false
}

// 見下文解釋
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

要想了解UnwrapNestedRefs與UnwrapRef,必須先要了解ts中的infer。如果之前不了解,請先閱讀相關文檔。看完文檔,再建議去google一些案例看看加深下印象。

現在我們假設你了解了infer概念,也了解了它的日常用法。再來看源碼:

// 不應該繼續遞歸的引用數據類型
type BailTypes =
| Function
| Map<any, any>
| Set<any>
| WeakMap<any, any>
| WeakSet<any>

// 遞歸地獲取嵌套數據的類型
// Recursively unwraps nested value bindings.
export type UnwrapRef<T> = {
// 如果是ref類型,繼續解套
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
// 如果是數組,循環解套
array: T extends Array<infer V> ? Array<UnwrapRef<V>> : T
// 如果是對象,遍歷解套
object: { [K in keyof T]: UnwrapRef<T[K]> }
// 否則,停止解套
stop: T
}[T extends Ref
? 'ref'
: T extends Array<any>
? 'array'
: T extends BailTypes
? 'stop' // bail out on types that shouldn't be unwrapped
: T extends object ? 'object' : 'stop']

// 聲明類型別名:UnwrapNestedRefs
// 它是這樣的類型:如果該類型已經繼承於Ref,則不需要解套,否則可能是嵌套的ref,走遞歸解套
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

如果還是懵,建議後續再去看看infer的相關介紹。在這我們直接拋結果:

Ref是這樣的一種數據結構:它有個key為Symbol的屬性做類型標識,有個屬性value用來存儲數據。這個數據可以是任意的類型,唯獨不能是被嵌套了Ref類型的類型。 具體來說就是不能是這樣 Array<Ref> 或者這樣 { [key]: Ref }。但很奇怪的是,這樣Ref<Ref> 又是可以的。具體為什麼也不知道,所以我勇敢地提了個PR...

(果然Ref<Ref> 是不夠完美的,2019.10.10晚,我這PR被合併了。大家遇到疑問時,也可以勇敢的提PR,說不定就被合了....)

另外,Map、Set、WeakMap、WeakSet也是不支持解套的。說明Ref數據的value也有可能是Map<Ref>這樣的數據類型。

說回Ref,從上篇文章中,我們已經了解到,Ref類型的數據,是一種響應式的數據。然後我們看其具體實現:

// 從@vue/shared中引入,判斷一個數據是否為對象
// Record<any, any>代表了任意類型key,任意類型value的類型
// 為什麼不是 val is object 呢?可以看下這個回答:https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: any): val is Record<any, any> =>
val !== null && typeof val === 'object'

// 如果傳遞的值是個對象(包含數組/Map/Set/WeakMap/WeakSet),則使用reactive執行,否則返回原數據
// 從上篇文章知道,這個reactive就是將我們的數據轉成響應式數據
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)

export function ref<T>(raw: T): Ref<T> {
// 轉化數據
raw = convert(raw)
const v = {
[refSymbol]: true,
get value() {
// track的代碼在effect中,暫時不看,能猜到此處就是監聽函數收集依賴的方法。
track(v, OperationTypes.GET, '')
// 返回剛剛被轉化後的數據
return raw
},
set value(newVal) {
// 將設置的值,轉化為響應式數據,賦值給raw
raw = convert(newVal)
// trigger也暫時不看,能猜到此處就是觸發監聽函數執行的方法
trigger(v, OperationTypes.SET, '')
}
}
return v as Ref<T>
}

其實最難理解的就在於這個ref函數。我們看到,這裡也定義了get/set,卻沒有任何Proxy相關的操作。在之前的信息中我們知道reactive能構建出響應式數據,但要求傳參必須是對象。但ref的入參是對象時,同樣也需要reactive做轉化。那ref這個函數的目的到底是什麼呢?為什麼需要有它?

在文章開頭,我貼了這份官方介紹Ref vs Reactive,這其中其實已經說的很明白。

However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:

對於基本數據類型,函數傳遞或者對象解構時,會丟失原始數據的引用,換言之,我們沒法讓基本數據類型,或者解構後的變量(如果它的值也是基本數據類型的話),成為響應式的數據。

// 我們是永遠沒辦法讓`a`或`x`這樣的基本數據成為響應式的數據的,Proxy也無法劫持基本數據。
const a = 1;
const { x: 1 } = { x: 1 }

但是有時候,我們確實就是想一個數字、一個字符串是響應式的,或者就是想利用解構的寫法。那怎麼辦呢?只能通過創建一個對象,也即是源碼中的Ref數據,然後將原始數據保存在Ref的屬性value當中,再將它的引用返回給使用者。既然是我們自己創造出來的對象,也就沒必要使用Proxy再做代理了,直接劫持這個value的get/set即可,這就是ref函數與Ref類型的由來。

不過單靠ref還沒法解決對象解構的問題,它只是將基本數據保持在一個對象的value中,以實現數據響應式。對於對象的解構還需要另外一個函數:toRefs。

export function toRefs<T extends object>(
object: T
): { [K in keyof T]: Ref<T[K]> } {
const ret: any = {}
// 遍歷對象的所有key,將其值轉化為Ref數據
for (const key in object) {
ret[key] = toProxyRef(object, key)
}
return ret
}
function toProxyRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
const v = {
[refSymbol]: true,
get value() {
// 注意,這裡沒用到track
return object[key]
},
set value(newVal) {
// 注意,這裡沒用到trigger
object[key] = newVal
}
}
return v as Ref<T[K]>
}

通過遍歷對象,將每個屬性值都轉成Ref數據,這樣解構出來的還是Ref數據,自然就保持了響應式數據的引用。但是源碼中有一點要注意,toRefs函數中引用的是toProxyRef而不是ref,它並不會在get/set中注入track跟trigger,也就是說,向toRefs傳入一個正常的對象,是不會返回一個響應式的數據的。必須要傳遞一個已經被reactive執行返回的對象才能有響應式的效果。感覺這點可以優化,暫時也不知道小右這樣做的原因是什麼。由於這裡會牽扯到track跟trigger,而這兩個在我寫本文時還沒研究,就沒膽子提PR了。

到這,我們就把ref的源碼給看完了。

相關焦點

  • 實現最精簡的響應式系統來學習Vue的data、computed、watch源碼
    導讀記得初學Vue源碼的時候,在defineReactive、Observer、Dep、Watcher等等內部設計源碼之間跳來跳去,發現再也繞不出來了。Vue發展了很久,很多fix和feature的增加讓內部源碼越來越龐大,太多的邊界情況和優化設計掩蓋了原本精簡的代碼設計,讓新手閱讀源碼變得越來越困難,但是面試的時候,Vue的響應式原理幾乎成了Vue技術棧的公司面試中高級前端必問的點之一。
  • Vue2和Vue3使用層面上有什麼區別
    Vue 3 有 createApp(),而 Vue 2 的是 new Vue() createApp(組件),new Vue() v-model代替以前的v-model和.sync vue3
  • VUE 響應式原理源碼:帶你一步精通 VUE
    ,怎麼能說自己熟練使用 VUE,要是沒有寫過一個簡易版的 VUE 怎麼能說自己精通 VUE,這篇文章通過300多行代碼,帶你寫一個簡易版的 VUE,主要實現 VUE 數據響應式 (數據劫持結合發布者-訂閱者)、數組的變異方法、編譯指令,數據的雙向綁定的功能。
  • 為什麼 Vue3 的 ref 讓很多大佬操碎了心?
    /Foo.vue'// declaring a variable that compiles to a refref: count = 1const inc = () => {  count++}// access the raw ref object by prefixing with $console.log($count.value
  • Spring-Task源碼解析
    scheduler"pool-size="3"/><beanid="task"class="task.Task"/><task:scheduled-tasksscheduler="scheduler"><task:scheduledref
  • MetInfo 勞保用品電力器具公司響應式免費網站模板已上線,含整站源碼
    模板編號:medu002模板源碼:開源,內含米拓企業建站系統完整開源源碼使用限制:遵守《米拓企業建站系統最終用戶許可協議》即可免費使用演示地址:https
  • VUE 響應式原理源碼:帶你一步精通 VUE | 原力計劃
    學過 VUE 如果不了解響應式的原理,怎麼能說自己熟練使用 VUE,要是沒有寫過一個簡易版的 VUE 怎麼能說自己精通 VUE,這篇文章通過300多行代碼
  • 《前端會客廳》對話winter和尤雨溪,深度探尋Vue3設計思想(上)
    瀏覽器版本過低,暫不支持視頻播放Toc:體驗vue3的三種姿勢Composition ApiFragment Teleport suspense尺寸內部架構響應式系統獨立自定義渲染器api性能:vue2初始化 所有的數據都要走defineProperty ;vue3用proxy 動態的決定返回什麼 做了攔截,初始工作量減少 組件實例化的提升還是明顯,首次mount 一會代碼看看。
  • H5大咖分享:Vue3為何使用Proxy實現數據監聽
    前言vue3響應式數據放棄了Object.define Property,而使用Proxy來代替它。我們知道,在 vue2 中,實現數據監聽是使用Object.defineProperty --> 實現方法可看:vue 數據雙向綁定原理。
  • 關於 ref 的一切
    這,就是關於ref的一切。ref的數據結構為什麼string類型的ref prop將會被廢棄?React.createRef我們直接看React.createRef的源碼:function createRef(): RefObject {  const refObject = {    current: null,  };  return refObject
  • 超詳細 ElementUI 源碼分析 —— Select(模板篇)
    今天詳細分析 Select 源碼,在看到源碼足足有九百行時,我整個人都是懵的,這是迄今為止讀的最多的一篇源碼,大概瀏覽了之後發現它裡面有很多很多的知識點,光導入的模塊就有 16 個,裡面包含著各種組件、混入以及工具函數,所以鑑於本文篇幅有限,我打算分兩部分來寫,分別為:「模板篇」和「方法篇
  • 「計算機畢設」一個精美的JAVA博客系統源碼分享
    前言大家好,我是程式設計師it分享師,今天給大家帶來一個精美的博客系統源碼!可以自己買一個便宜的雲伺服器,當自己的博客網站,記錄一下自己學習的心得。開發技術博客系統源碼基於SpringBoot,shiro,Bootstrap,JPA等技術開發而成。有前臺網頁和後臺管理兩個訪問地址。具體實現了文章,圖片視頻的發布;按標籤分類,支持ueditor,markdown編輯器;支持評論回復,關注,QQ,微博等第三方的登錄;站內搜索,響應式布局和訂閱功能。
  • 詳細 preact hook 源碼逐行解析
    export function applyRef(ref, value, vnode) { try { if (typeof ref == "function") ref(value); else ref.current = value; } catch (e) { options.
  • 給PHP開發者講講PHP源碼(2)
    如果你錯過了那篇文章,在你開始讀這篇文章之前也許你應該讀一下它。在這篇文章中,我們談論的是定位PHP內部函數的定義,以及理解它們的原理。如何找到函數的定義作為開始,讓我們嘗試找出strpos函數的定義。
  • Jetpack源碼解析--ViewModel基本使用及源碼解析
    1.背景Jetpack源碼解析系列文章:1. Android_Jetpack組件---Naviagtion源碼解析2. Jetpack源碼解析—Navigation為什麼切換Fragment會重繪?3. Jetpack源碼解析---用Lifecycles管理生命周期4.
  • APScheduler 源碼閱讀(二) job
    分析源碼主要還是針對 APScheduler 下的幾個關鍵的模塊這一篇主要瞅瞅 job 事件Job 記錄自己的觸發條件 triggers, 記錄自己的所屬的任務存儲 jobstores, 記錄自己交給誰執行 executors
  • vue中的 ref 和 $refs
    Vue提供了ref、$ref。本次,我們就詳細講講這兩個屬性。ref 被用來給元素或子組件註冊引用信息。引用信息將會註冊在父組件的 $refs 對象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子組件上,引用就指向組件實例:<!-- `vm.
  • 抖音解析網站源碼 抖音解析保存在哪裡
    抖音解析網站源碼有沒有呢,這是很多有這方面需求的小夥伴們都關心的問題。就讓小編帶大家了解抖音解析保存在哪裡吧~
  • Spring Core Container 源碼分析三:Spring Beans 初始化流程分析
    解析並註冊 bean definitions 流程該部分參考新的博文 Spring Core Container 源碼分析七:註冊 Bean DefinitionsDo Get Bean 流程Do Get Bean 流程的入口是 AbstractBeanFactory#doGetBean 方法,主流程圖如下,