學(xué)Vue3核心概念與面試官斗智斗勇(一) 收集觸發(fā)依賴

本文章依據(jù)閱讀源碼的理解進(jìn)行編寫。如果有什么錯(cuò)誤的地方,歡迎指正交流學(xué)習(xí)。
最近也在幫助想入行前端的朋友進(jìn)行學(xué)習(xí),如果有需要交流學(xué)習(xí),可以添加微信 gdgzyw。
聊天、學(xué)習(xí)、打游戲都闊以~

學(xué)習(xí)源碼最快的方式就是理解概念后,自己寫一個(gè)簡版的功能。所以我們得先搭一個(gè)環(huán)境,這里采用測試驅(qū)動(dòng)的方式進(jìn)行。

初始化項(xiàng)目

初始化 package.json 和安裝依賴

yarn init -y
yarn add -D @babel/core @babel/preset-env @babel/preset-typescript @types/jest babel-jest jest

添加 scripts 用于啟動(dòng) jest

package.json

{
  // ...
  "scripts": {
    "test": "jest"
  },
  // ...
}

根目錄創(chuàng)建 babel.config.js

module.exports = {
  presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'],
}

創(chuàng)建 tsconfig.json 文件

{
  "compilerOptions": {
    "target": "es2016",
    "lib": [
      "DOM",
      "es6"
    ],
    "module": "commonjs",
    "types": [
      "jest"
    ],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitAny": false,
    "skipLibCheck": true
  }
}

至此我們的項(xiàng)目就初始化完成了。如果你需要用 git 來管理,可以自行 git init。

編寫測試用例

創(chuàng)建 src/reactivity/tests/effect.spec.ts 文件

describe('effect', () => {
  it('happy path', () => {
    const bank = reactive({
      money: 100,
    });

    let myMoney;
    effect(() => {
      myMoney = bank.money * 2;
    });
    expect(myMoney).toBe(200);
    bank.money = 50;
    expect(myMoney).toBe(100);
  });
});

創(chuàng)建 scr/reactivity/tests/reactive.spec.ts

describe('reactive', () => {
  it('happy path', () => {
    const origin = {money: 100};
    const bank = reactive(origin);
    expect(bank).not.toBe(origin);
    expect(bank.money).toBe(100);
  });
});

現(xiàn)在我們運(yùn)行 yarn test 測試用例是跑不通的。對(duì)應(yīng)的函數(shù)我們還沒有創(chuàng)建。下面正式開始我們的編碼環(huán)節(jié)。

編寫 reactive 函數(shù)

通過上面的測試用例,我們可以知道,我們接收一個(gè)對(duì)象的值,并且對(duì)他進(jìn)行一個(gè)攔截。所以我們可以直接返回一個(gè) proxy 的代理對(duì)象。

export function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      return Reflect.set(target, key, value)
    },
  })
}

Reflect.get(target, key) 等同于 target[key]

接著我們?yōu)榱私y(tǒng)一出口可以創(chuàng)建 src/reactivity/index.ts 文件

index.ts

export * from './reactive'

接著我們?nèi)?reactive.spec.ts 中引入我們的函數(shù)

import {reactive} from '../index'

跑一下測試

yarn test reactive

提示 PASS 至此發(fā)現(xiàn)這個(gè)的單側(cè)已經(jīng)跑通。接著我們可以開始寫另一個(gè)單測。

編寫 effect 函數(shù)

一樣的,我們觀察一下測試用例的參數(shù)。
可以發(fā)現(xiàn)他接受一個(gè)回調(diào),所以我們參數(shù)是一個(gè)回調(diào)函數(shù)。
接著我們思考一下如何將我們上一個(gè) reactive 的函數(shù)與這個(gè)回調(diào)函數(shù)產(chǎn)生關(guān)聯(lián)。

image.png

定義 tagetMap 變量,用于對(duì)象的分組。
定義 depsMap 變量,用于對(duì)象中每個(gè) key 的依賴分組。
通過 reactive 定義對(duì)象,在 get 的時(shí)候,我們?cè)?targetMap
中將對(duì)象添加到 Map 中作為分類。接著創(chuàng)建 Set 用 key 作為分類保存到 Set 中。

回顧我們的單側(cè)流程,我們先定義了個(gè) reactive 對(duì)象。
接著我們?cè)?effect 函數(shù)中執(zhí)行了回調(diào)函數(shù),回調(diào)函數(shù)中我們會(huì)讀取到 reactive 的值,從而觸發(fā)了 get 操作。所以我們需要在 get 操作中進(jìn)行依賴收集。

定義一個(gè) ReactiveEffect 類,收集我們的回調(diào)函數(shù)

src/reactivity/effect.ts

let activeEffect
class ReactiveEffect {
  private readonly _fn: any
  constructor(fn) {
    this._fn = fn
  }
  run() {
    activeEffect = this
    this._fn()
  }
}

export function effect(fn) {
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}

在初始化的時(shí)候,我們保存回調(diào)函數(shù)到 _fn 中,在我們執(zhí)行 run 方法的時(shí)候。會(huì)觸發(fā)我們的回調(diào)函數(shù)。

定義一個(gè) track 的函數(shù),完成收集依賴這個(gè)操作

src/reactivity/effect.ts

const targetMap = new Map()
export function track(target, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  deps.add(activeEffect)
}

如果此時(shí)我們需要設(shè)置 reactive 的值,我們會(huì)觸發(fā) set 操作。所以觸發(fā)依賴的操作需要在 set 中進(jìn)行。

定義 trigger 函數(shù),觸發(fā)所有依賴

export function trigger(target, key) {
  const depsMap = targetMap.get(target)
  const deps = depsMap.get(key)
  for (const dep of deps) {
    dep.run()
  }
}

回到 reactive 文件,將 track 和 trigger 寫到對(duì)應(yīng)的操作中。

src/reactivity/index.ts

// ...
export * from './effect'

src/reactivity/reactive.ts

import {target, trigger} from './index'

export function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key)
      return Reflect.get(target, key)
    },
    set(target, key, value) {
      const res = Reflect.set(target, key, value)
      trigger(target, key)
      return res
    },
  })
}

回到我們的單側(cè),將這幾個(gè)庫引入

src/reactivtiy/tests/effect.spec.ts

import {effect,reactive} from '../index' 

// ...

至此我們收集依賴和觸發(fā)依賴的核心邏輯已經(jīng)實(shí)現(xiàn)。我們現(xiàn)在可以跑 yarn test 進(jìn)行檢驗(yàn)。

結(jié)語

這篇文章是這個(gè)系列的開始,后續(xù)我會(huì)繼續(xù)分享相關(guān)內(nèi)容。慢慢完善我們對(duì) vue3 的理解。
歡迎關(guān)注我,與我深入♂溝通。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容