從零實(shí)現(xiàn)Vue3的響應(yīng)式庫(1)

Vue3 和 Vue2 的響應(yīng)式有很大的不同,由于 Vue3 使用 Proxy 代替了 defineProperty,使得 Vue3 比 Vue2 在響應(yīng)式數(shù)據(jù)處理方面有著更好的性能,更簡(jiǎn)潔高效的處理方式,還實(shí)現(xiàn)了諸多在 Vue2 上無法實(shí)現(xiàn)的功能。此外 Vue3 的響應(yīng)式庫 reactivity 是一個(gè)單獨(dú)的包,它可以不依賴 Vue 運(yùn)行,意味著我們可以將它運(yùn)行在其他框架里。事實(shí)上,Vue3 的響應(yīng)式庫的實(shí)現(xiàn)方式以及市面上其他的大多數(shù)響應(yīng)式庫(如 observer-util,meteor 等)的實(shí)現(xiàn)方式都是類似的,Vue 也是參考這些庫實(shí)現(xiàn)的,所以我們還是很有必要去研究一下的,畢竟咱也不能落伍了 ??,那么各位小伙伴們下面就跟我一起來看下這個(gè) @vue/reactivity 究竟是怎么實(shí)現(xiàn)的。

本文章的源碼已經(jīng)發(fā)在了我的 git 上,可以前往查看:reactivity

閱讀本文章之前你要先了解以下知識(shí)點(diǎn)

上面這些有不了解的同學(xué)可以直接點(diǎn)鏈接查看詳細(xì)的文檔,文章里面就不再解釋了。

--

我們首先看一個(gè)使用 reactivity 的例子

// 創(chuàng)建一個(gè)響應(yīng)式對(duì)象
const state = reactive({ count: 1 })

// 執(zhí)行effect
effect(() => {
  console.log(state.count)
})

state.count = 2 // count改變時(shí)執(zhí)行了effect內(nèi)的函數(shù),控制臺(tái)輸出2

這個(gè)例子通過 reactive 創(chuàng)建了一個(gè)響應(yīng)式對(duì)象 state,然后調(diào)用 effect 執(zhí)行函數(shù),這個(gè)函數(shù)內(nèi)部訪問了 state 的屬性,隨后我們更改這個(gè) state 的屬性,這時(shí),effect 內(nèi)的函數(shù)會(huì)再次執(zhí)行。

這樣一個(gè)響應(yīng)式數(shù)據(jù)的通常實(shí)現(xiàn)的方式是這樣的

  1. 定義一個(gè)數(shù)據(jù)為響應(yīng)式(通常通過 defineProperty 或者 Proxy 攔截 get、set 等操作)
  2. 定義一個(gè)副作用函數(shù)(effect),這個(gè)副作用函數(shù)內(nèi)部訪問到響應(yīng)式數(shù)據(jù)時(shí)會(huì)觸發(fā) 1 中的 getter,進(jìn)而可以在這里將 effect 收集起來
  3. 修改響應(yīng)式數(shù)據(jù)時(shí),就會(huì)觸發(fā) 1 中的 setter,進(jìn)而執(zhí)行 2 中收集到的 effect 函數(shù)

關(guān)于 effect: effect 在 Vue 里通常叫做副作用函數(shù),因?yàn)檫@種函數(shù)內(nèi)通常執(zhí)行組件渲染,計(jì)算屬性等其他任務(wù)。在其他庫里面可能叫觀察者函數(shù)(observe)或其他,個(gè)人能理解到是什么意思就好,由于本篇文章是分析 Vue3 的,所以統(tǒng)一叫副作用函數(shù)(effect)

根據(jù)以上的思路,我們就可以開始動(dòng)手實(shí)現(xiàn)了

reactive

首先我們需要有一個(gè) reactive 函數(shù)來將我們的數(shù)據(jù)變?yōu)轫憫?yīng)式。

// reactive.ts
import { baseHandlers } from './handlers'
import { isObject } from './utils'

type Target = object

const proxyMap = new WeakMap()

export function reactive<T extends object>(target: T): T {
  return createReactiveObject(target)
}

function createReactiveObject(target: Target) {
  // 只對(duì)對(duì)象添加reactive
  if (!isObject(target)) {
    return target
  }
  // 不能重復(fù)定義響應(yīng)式數(shù)據(jù)
  if (proxyMap.has(target)) {
    return proxyMap.get(target)
  }
  // 通過Proxy攔截對(duì)數(shù)據(jù)的操作
  const proxy = new Proxy(target, baseHandlers)
  // 數(shù)據(jù)添加進(jìn)ProxyMap中
  proxyMap.set(target, proxy)
  return proxy
}

這里主要對(duì)數(shù)據(jù)做了簡(jiǎn)單的判斷,關(guān)鍵是在const proxy = new Proxy(target, baseHandlers)中,通過 Proxy 對(duì)數(shù)據(jù)進(jìn)行處理,這里的baseHandlers就是對(duì)數(shù)據(jù)的 get,set 等攔截操作,下面來實(shí)現(xiàn)下baseHandlers

get 收集依賴

首先實(shí)現(xiàn)下攔截 get 操作,使得訪問數(shù)據(jù)的某一個(gè) key 時(shí),可以收集到訪問這個(gè) key 的函數(shù)(effect),并把這個(gè)函數(shù)儲(chǔ)存起來。

// handlers.ts
import { track } from './effect'
import { reactive, Target } from './reactive'
import { isObject } from './utils'

export const baseHandlers: ProxyHandler<object> = {
  get(target: Target, key: string | symbol, receiver: object) {
    // 收集effect函數(shù)
    track(target, key)
    // 獲取返回值
    const res = Reflect.get(target, key, receiver)
    // 如果是對(duì)象,要再次執(zhí)行reactive并返回
    if (isObject(res)) {
      return reactive(res)
    }
    return res
  }
}

這里我們攔截到 get 操作后,通過 track 收集依賴,track 函數(shù)做的事情就是把當(dāng)前的 effect 函數(shù)收集起來,執(zhí)行完 track 后,再獲取到 target 的 key 的值并返回,注意這里是判斷了下 res 是否是對(duì)象,如果是對(duì)象的話要返回reactive(res),是因?yàn)榭紤]到可能有多個(gè)嵌套對(duì)象的情況,而 Proxy 只能修改到到當(dāng)前對(duì)象,并不能修改到子對(duì)象,所以在這里要處理下,下面我們需要再實(shí)現(xiàn)track函數(shù)

// effect.ts

// 存儲(chǔ)依賴
type Deps = Set<ReactiveEffect>
// 通過key去獲取依賴,key => Deps
type DepsMap = Map<any, Deps>
// 通過target去獲取DepsMap,target => DepsMap
const targetMap = new WeakMap<any, DepsMap>()
// 當(dāng)前正在執(zhí)行的effect
let activeEffect: ReactiveEffect | undefined

// 收集依賴
export function track(target: object, key: unknown) {
  if (!activeEffect) {
    return
  }
  // 獲取到這個(gè)target對(duì)應(yīng)的depsMap
  let depsMap = targetMap.get(target)
  // depsMap不存在時(shí)新建一個(gè)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 有了depsMap后,再根據(jù)key去獲取這個(gè)key所對(duì)應(yīng)的deps
  let deps = depsMap.get(key)
  // 也是不存在時(shí)就新建一個(gè)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  // 將activeEffect添加進(jìn)deps
  if (!deps.has(activeEffect)) {
    deps.add(activeEffect)
  }
}

注意有兩個(gè) map 和一個(gè) set,targetMap => depsMap => deps,這樣就可以使我們通過 target 和 key 準(zhǔn)確地獲取到這個(gè) key 所對(duì)應(yīng)的 deps(effect),把當(dāng)前正在執(zhí)行的 effect(activeEffect)存起來,這樣在修改target[key]的時(shí)候,就又可以通過 target 和 key 拿到之前收集到的所有的依賴,并執(zhí)行它們,這里有個(gè)問題就是這個(gè)activeEffect它是從哪里來的,get 是怎么知道當(dāng)前正在執(zhí)行的 effect 的?這個(gè)問題可以先放一放,我們后面再將,下面我們先實(shí)現(xiàn)這個(gè) set。

實(shí)現(xiàn) set

// handlers.ts

export const baseHandlers: ProxyHandler<object> = {
  get() {
    //...
  },
  set(target: Target, key: string | symbol, value: any, receiver: object) {
    // 設(shè)置value
    const result = Reflect.set(target, key, value, receiver)
    // 通知更新
    trigger(target, key, value)
    return result
  }
}

我們?cè)趧偛诺?code>baseHandlers下面再加一個(gè) set,這個(gè) set 里面主要就是賦值然后通知更新,通知更新通過trigger進(jìn)行,我們需要拿到在 get 中收集到的依賴,并執(zhí)行,下面來實(shí)現(xiàn)下 trigger 函數(shù)

// effect.ts

// 通知更新
export function trigger(target: object, key: any, newValue?: any) {
  // 獲取該對(duì)象的depsMap
  const depsMap = targetMap.get(target)
  // 獲取不到時(shí)說明沒有觸發(fā)過getter
  if (!depsMap) {
    return
  }
  // 然后根據(jù)key獲取deps,也就是之前存的effect函數(shù)
  const effects = depsMap.get(key)
  // 執(zhí)行所有的effect函數(shù)
  if (effects) {
    effects.forEach((effect) => {
      effect()
    })
  }
}

這個(gè) trigger 就是獲取到之前收集的 effect 然后執(zhí)行。

其實(shí)除了 get 和 set,還有個(gè)常用的操作,就是刪除屬性,現(xiàn)在我們還不能攔截到刪除操作,下面我們來實(shí)現(xiàn)下

實(shí)現(xiàn) deleteProperty

export const baseHandlers: ProxyHandler<object> = {
  get() {
    //...
  },
  set() {
    //...
  },
  deleteProperty(target: Target, key: string | symbol) {
    // 判斷要?jiǎng)h除的key是否存在
    const hadKey = hasOwn(target, key)
    // 執(zhí)行刪除操作
    const result = Reflect.deleteProperty(target, key)
    // 只在存在key并且刪除成功時(shí)再通知更新
    if (hadKey && result) {
      trigger(target, key, undefined)
    }
    return result
  }
}

我們?cè)趧偛诺?code>baseHandlers里面再加一個(gè)deleteProperty,它可以攔截到對(duì)數(shù)據(jù)的刪除操作,在這里我們需要先判斷下刪除的 key 是否存在,因?yàn)榭赡苡脩魰?huì)刪除一個(gè)并不存在 key,然后執(zhí)行刪除,我們只在存在 key 并且刪除成功時(shí)再通知更新,因?yàn)槿绻?key 不存在時(shí),這個(gè)刪除是無意義的,也就不需要更新,再有就是如果刪除操作失敗的話,也不需要更新,最后直接觸發(fā)trigger就可以了,注意這里的第三個(gè)參數(shù)即 value 是undefined

現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了getsetdeleteProperty這三種操作的攔截,還記不記得在track函數(shù)中的activeEffect,那里留了個(gè)問題,就是這個(gè)activeEffect是怎么來的?,在最開始的例子里面,我們要通過 effect 執(zhí)行函數(shù),這個(gè)activeEffect就是在這里設(shè)置的,下面我們來實(shí)現(xiàn)下這個(gè)effect函數(shù)。

// effect.ts

type ReactiveEffect<T = any> = () => T
// 存儲(chǔ)effect的調(diào)用棧
const effectStack: ReactiveEffect[] = []

export function effect<T = any>(fn: () => T): ReactiveEffect<T> {
  // 創(chuàng)建一個(gè)effect函數(shù)
  const effect = createReactiveEffect(fn)
  return effect
}

function createReactiveEffect<T = any>(fn: () => T): ReactiveEffect<T> {
  const effect = function reactiveEffect() {
    // 當(dāng)前effectStack調(diào)用棧不存在這個(gè)effect時(shí)再執(zhí)行,避免死循環(huán)
    if (!effectStack.includes(effect)) {
      try {
        // 把當(dāng)前的effectStack添加進(jìn)effectStack
        effectStack.push(effect)
        // 設(shè)置當(dāng)前的effect,這樣Proxy中的getter就可以訪問到了
        activeEffect = effect
        // 執(zhí)行函數(shù)
        return fn()
      } finally {
        // 執(zhí)行完后就將當(dāng)前這個(gè)effect出棧
        effectStack.pop()
        // 把a(bǔ)ctiveEffect恢復(fù)
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect<T>
  return effect
}

這里主要是通過createReactiveEffect創(chuàng)建一個(gè) effect 函數(shù),fn 就是調(diào)用 effect 時(shí)傳入的函數(shù),在執(zhí)行這個(gè) fn 之前,先通過effectStack.push(effect)把這個(gè) effect 推入 effectStack 棧中,因?yàn)?effect 可能存在嵌套調(diào)用的情況,保存下來就可以獲取到一個(gè)完整的 effect 調(diào)用棧,就可以通過上面的effectStack.includes(effect)判斷是否存在循環(huán)調(diào)用的情況了,然后再activeEffect = effect設(shè)置 activeEffect,設(shè)置完之后再執(zhí)行 fn,因?yàn)檫@個(gè) activeEffect 是全局唯一的,所以我們執(zhí)行 fn 的時(shí)候,如果內(nèi)部訪問了響應(yīng)式數(shù)據(jù),就可以在 getter 里拿到這個(gè) activeEffect,進(jìn)而收集它。

現(xiàn)在基本上是完成了,現(xiàn)在通過我們寫的這個(gè) reactivity 庫就可以實(shí)現(xiàn)例子中的效果了,但是還有一些邊界情況需要考慮,下篇文章就添加一些常見的邊界情況處理。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,566評(píng)論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評(píng)論 1 322
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,683評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,899評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,135評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評(píng)論 1 282
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,528評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,844評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容