尤雨溪國外教程:親手帶你寫個簡易版的Vue!

⚠️本文爲掘金社區首發簽約文章,未獲受權禁止轉載前端

前言

不少時候咱們都對源碼展示出了必定的渴求,但當被問到究竟爲何想看源碼時,答案無非也就那麼幾種:vue

  • 爲了面試
  • 爲了在簡歷上寫本身會源碼
  • 瞭解底層原理 學習高手思路
  • 經過源碼來學習一些小技巧(騷操做)
  • 對框架如何實現的各類功能感到好奇
  • 內卷嚴重 不看不行 逆水行舟 不進則退
  • 本身也想造輪子 先看看別人都是怎麼作的
  • 各類公衆號和賣課的都在販賣焦慮 被洗腦洗的

但其實不多人會真正的看明白源碼,一方面是因爲代碼量實在是太多了,另外一方面則是當咱們閱讀別人代碼的時候就是容易搞得一頭霧水。由於每一個人的編碼方式以及編碼習慣都截然不同,在看一個編碼習慣與本身不一樣的人的代碼時是很累的。react

何況不只是因爲每一個人的編碼風格相差甚遠,人與人之間各自擅長的技術方向以及技術水平也都是橫當作嶺側成峯遠近高低各不一樣。刨除掉以上的種種緣由以後,更重要的一個緣由是不少人框架用的都不夠精通呢、用過的API也就那麼幾個常見的,其餘不經常使用但很高階的API都沒怎麼用過,連用都沒用明白呢,這樣的人看源碼的時候固然會被繞暈啦!git

那確定有人會說:尤雨溪他框架就必定用的很6嗎?我天天都在用他的框架寫代碼,他還不必定有我熟練呢!es6

這麼說確實有必定的道理,但若是論底層,他比誰都瞭解。之因此咱們啃不動源碼的很重要的一個緣由就是:細枝末節的東西實在是太多了,很容易令你們找不到重點。這些細枝末節的東西天然有它們存在的道理,但它們確成爲了咱們行走在鑽研源碼這條路上的絆腳石。github

題外話

怎樣學習源碼纔是最科學的方式呢?咱們來看一個例子:有一些聽起來很是高大上的高科技產品,如電磁軌道炮。各個軍事強國都在爭相探索這一領域,假設有一天,咱們一覺醒來成爲了國家電磁軌道炮首席研究員,是專門負責研究電磁軌道炮底層技術的。那麼當咱們拆解一個電磁軌道炮的時候,大機率你是看不懂它的內部構造的。由於裏面會包含許多很是複雜的高強度材料控制磁力的電極蜿蜒曲折的電線提升精準度的裝置以及一些利於使用者操控的封裝等等…面試

那麼此時的你可能就不太容易搞明白電磁軌道炮的真正原理,直到有一次在網上偶然間看到一個視頻,視頻中的人用了一些磁鐵、若干鋼珠、以及幾個咱們平常生活中可以搞到的材料來製做了一個簡易版的電磁軌道炮。這樣咱們一會兒就可以搞懂電磁軌道炮的真正原理,雖然這樣的軌道炮並不能真正的用於實戰,但只要咱們明白了最基礎的那部分,咱們就能夠在此基礎上一步步進行擴展,慢慢弄懂整個可以用於實戰的複雜軌道炮。算法

源碼也是同理,咱們按照電磁軌道炮的思路一步步來,先搞清楚最核心的基礎部分,慢慢的再一步步去進階。這樣的學習方法比咱們確定一上來就去拆解一個完整版的電磁軌道炮要強得多vue-router

既然咱們有這樣的需求,那麼做爲一個流行框架的做者就必然會有所迴應:在一次培訓的過程當中,尤雨溪帶領你們寫了一個很是微型的Vue3。不過惋惜這是他在國外辦過的爲期一天的培訓,咱們國內的觀衆並無福氣可以享受到被框架做者培訓的這麼一次教學。但好在尤雨溪已經把代碼所有上傳到了codepen上,你們能夠點擊這個連接來閱讀尤雨溪親手寫的代碼,或者也能夠選擇留在本篇文章內,看我來用中文爲你們講解尤雨溪的親筆代碼設計模式

響應式篇

尤雨溪在某次直播時曾表示過:Vue3 的源碼要比 Vue2 的源碼要好學不少Vue3在架構以及模塊的耦合關係設計方面比Vue2更好,能夠一個模塊一個模塊看,這樣比較容易理解。若是是剛上手,能夠從Reactivity看起。由於Reactivity是整個Vue3中跟外部沒有任何耦合的一個模塊。

Reactivity就是咱們常說的響應式,大名鼎鼎的React也是這個意思,不信仔細對比一下前五個字母。那麼什麼是響應式呢?想一想看React是什麼框架?MVVM對吧?MVVM的主打口號是:

數據驅動視圖!

也就是說當數據發生改變時咱們會從新渲染一下組件,這樣就可以達到一修改數據,頁面上用到這個數據的地方就會實時發生變化的效果。不過在數據發生變化時也不只僅只是可以更新視圖,還能夠作些別的呢!尤雨溪在建立@vue/reactivity這個模塊的時候,借鑑的是@nx-js/observer-util這個庫。咱們來看一眼它在GitHubREADME.md裏展現的一段示例代碼:

import { observable, observe } from '@nx-js/observer-util';

const counter = observable({ num: 0 });
const countLogger = observe(() => console.log(counter.num));

// 這行代碼將會調用 countLogger 這個函數並打印出:1
counter.num++;
複製代碼

是否是很像Vue3reactivewatchEffect啊?其實就是咱們提早定義好一個函數,當函數裏面依賴的數據項發生變化時就會自動執行這段函數,這就是響應式!

數據驅動視圖那就更容易理解了,既然當數據發生變化時能夠執行一段函數,那麼這段函數爲何不能夠執行一段更新視圖的操做呢:

import { store, view } from 'react-easy-state';

const counter = store({
  num: 0,
  up() {
    this.num++;
  }
});

// 這是一個響應式的組件, 當 counter.num 發生變化時會自動從新渲染組件
const UserComp = view(() => <div onClick={counter.up}>{counter.num}</div>);
複製代碼

react-easy-state是他們(尤雨溪借鑑的那個庫)專門針對React來進行封裝的,不難看出view這個函數就是observe函數的一個變形,observe是要你傳一個函數進去,你函數裏面想執行啥就執行啥。而view是要你傳一個組件進去,當數據變化時會去執行他們提早寫好的一段更新邏輯,那不就跟你本身在observe裏寫一段更新操做是同樣的嘛!用了這個庫寫出來的React就像是在寫Vue同樣。

源碼

理解了什麼是響應式以後就能夠方便咱們來查看源碼了,來看看尤雨溪是怎麼僅用十幾行代碼就實現的響應式

let activeEffect

class Dep {
  subscribers = new Set()
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify() {
    this.subscribers.forEach(effect => effect())
  }
}

function watchEffect(effect) {
  activeEffect = effect
  effect()
}
複製代碼

實現完了,再來看看該怎麼用:

const dep = new Dep()

let actualCount = 0
const state = {
  get count() {
    dep.depend()
    return actualCount
  },
  set count(newCount) {
    actualCount = newCount
    dep.notify()
  }
}

watchEffect(() => {
  console.log(state.count)
}) // 0

state.count++ // 1
複製代碼

若是在觀看這十幾二十來行代碼時都會以爲繞的話,那就說明你的基礎屬實不怎麼樣。由於明眼人一眼就能夠看出來,這是一個很是經典的設計模式:發佈-訂閱模式

發佈-訂閱模式

若是不太瞭解發佈-訂閱模式的話,咱們能夠簡單的來說一下。但若是你對這些設計模式早已瞭如指掌,而且可以輕鬆讀懂剛纔那段代碼的話,建議暫且先跳過這一段。

《JavaScript設計模式與開發實踐》一書中,做者曾探發佈-訂閱模式舉了一個十分生動形象的例子:

小明最近看上了一套房子,到了售樓處以後才被告知,該樓盤的房子早已售罄。好在售樓 MM 告訴小明,不久以後還有一些尾盤推出,開發商正在辦理相關手續,手續辦好後即可以購買。但究竟是何時,目前尚未人可以知道。

因而小明記下了售樓處的電話,之後天天都會打電話過去詢問是否是已經到了購買時間。除了小明,還有小紅、小強、小龍也會天天向售樓處諮詢這個問題。一個星期事後,售樓 MM 決定辭職,由於厭倦了天天回答 1000 個相同內容的電話。

固然現實中沒有這麼笨的銷售公司,實際上故事是這樣的:小明離開以前,把電話號留在了售樓處。售樓 MM 答應他,新樓盤一推出就立刻發信息通知小明。小紅、小強和小龍也是同樣,他們的電話號碼都被記載售樓處的花名冊上,新樓盤推出的時候,售樓 MM 會翻開花名冊,遍歷上面的電話號碼,依次發送一條短信來通知他們。

在剛剛的例子中,發送短信通知就是一個典型的發佈-訂閱模式,小明、小紅等購買者都是訂閱者,他們訂閱了房子開售的消息。售樓處做爲發佈者,會在合適的時候遍歷花名冊上的電話號碼,依次給購房者發佈消息。

若是你曾經用過xxx.addEventListener這個函數爲DOM添加過事件的話,那麼實際上就已經算是用過發佈-訂閱模式啦!想想是否是和售樓處的這個例子很類似:

  • 咱們須要在必定條件下幹一些事情
  • 但咱們不知道的是這個條件會在什麼時間點成立
  • 因此咱們留下咱們的函數
  • 當條件成立時自動執行

那麼咱們就來簡單的模擬一下addEventListener發生的事情以便於你們理解發佈-訂閱模式

class DOM {
    #eventObj = {
        click: [],
        mouseover: [],
        mouseout: [],
        mousemove: [],
        keydown: [],
        keyup: []
        // 還有不少事件類型就不一一寫啦
    }
    addEventListener (event, fn) {
        this.#eventObj[event].push(fn)
    }
    removeEventListener (event, fn) {
        const arr = this.#eventObj[event]
        const index = arr.indexOf(fn)
        arr.splice(index, 1)
    }
    click () {
        this.#eventObj.click.forEach(fn => fn.apply(this))
    }
    mouseover () {
        this.#eventObj.mouseover.forEach(fn => fn.apply(this))
    }
    // 還有不少事件方法就不一一寫啦
}
複製代碼

咱們來用一下試試:

const dom = new DOM()

dom.addEventListener('click', () => console.log('點擊啦!'))
dom.addEventListener('click', function () { console.log(this) })

dom.addEventListener('mouseover', () => console.log('鼠標進入啦!'))
dom.addEventListener('mouseover', function () { console.log(this) })

// 模擬點擊事件
dom.click() // 依次打印出:'點擊啦!' 和相應的 this 對象

// 模擬鼠標事件
dom.mouseover() // 依次打印出:'鼠標進入啦!' 和相應的 this 對象

const fn = () => {}
dom.addEventListener('click', fn)
// 還能夠移除監聽
dom.removeEventListener('click', fn)
複製代碼

經過這個簡單的案例應該就可以明白發佈-訂閱模式了吧?

咱們來引用一下《JavaScript設計模式與開發實踐》發佈-訂閱模式總結出來的三個要點:

  1. 首先要指定好誰充當發佈者(好比售樓處)在本例中是 dom 這個對象
  2. 而後給發佈者添加一個緩存列表,用於存放回調函數以便通知訂閱者(售樓處的花名冊)在本例中是 dom.#eventObj
  3. 最後發佈消息的時候,發佈者會遍歷這個緩存列表,依次觸發裏面存放的訂閱者回調函數(遍歷花名冊,挨個發短信)

記住這三個要點後,再來看一眼尤大的代碼,看是否是符合這仨要點:

  • 發佈者:dep 對象
  • 緩存列表:dep.subscribers
  • 發佈消息:dep.notify()

因此這是一個典型的發佈-訂閱模式

加強版

尤雨溪的初版代碼實現的仍是有些過於簡陋了,首先用起來就很不方便,由於咱們每次定義數據時都須要這麼手寫一遍gettersetter、手動的去執行一下依賴收集函數以及觸發的函數。這個部分顯然是能夠繼續進行封裝的,那麼再來看一眼尤雨溪實現的第二版:

let activeEffect

class Dep {
  subscribers = new Set()
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify() {
    this.subscribers.forEach(effect => effect())
  }
}

function watchEffect(effect) {
  activeEffect = effect
  effect()
  activeEffect = null
}

function reactive(raw) {
  // 使用 Object.defineProperty
  // 1. 遍歷對象上存在的 key
  Object.keys(raw).forEach(key => {
    // 2. 爲每一個 key 都建立一個依賴對象
    const dep = new Dep()

    // 3. 用 getter 和 setter 重寫原對象的屬性
    let realValue = raw[key]
    Object.defineProperty(raw, key, {
      get() {
        // 4. 在 getter 和 setter 裏調用依賴對象的對應方法
        dep.depend()
        return realValue
      },
      set(newValue) {
        realValue = newValue
        dep.notify()
      }
    })
  })
  return raw
}
複製代碼

能夠看到這一版實現的就比上一版好多了,並且感受尤雨溪在寫這一版代碼時比上一版更加認真。由於這版代碼裏有着詳細的註釋,因此確定是認真講解的一段代碼。只不過原來的註釋都是用英文寫的,我給它翻譯成了中文。

不過各位看官請放心,除了註釋被我翻譯成了中文之外,其餘的地方我一個字母都沒有動過,就連空格都是保持的原汁原味的縮進,爲的就是可以讓你們看到的是尤雨溪的一手代碼😋

不難看出,這版代碼在實現上用到了兩種設計模式,它們分別是代理模式以及咱們剛剛講過的發佈-訂閱模式。因此說學好設計模式是多麼重要的一件事情。若是對設計模式感興趣的話能夠去B站搜索前端學不動,目前正在連載設計模式中,我的感受比慕課網那門賣288的 JavaScript 設計模式課講的更清晰。

代理模式

代理模式相對比較簡單,都不用上代碼,借用《JavaScript設計模式核⼼原理與應⽤實踐》的做者修言舉的一個很是有趣的例子就能讓你們明白:

我有個同事,技術很強,髮型也很強。多年來由於沉迷 coding,耽誤了人生大事。迫於尋找另外一半的願望比較急切,該同事同時是多個優質高端婚戀網站的註冊VIP。工做之餘,他經常給咱們分享近期的相親情感生活進展。

「大家看,這個妹子頭像是否是超可愛!」同事哥這天發掘了一個新的婚介所,他舉起手機,朝身邊幾位瘋狂揮舞。

「哥,那是新垣結衣。。。」同事哥的同桌無奈地搖搖頭,沒有停下 coding 的手。

同事哥恢復了冷靜,嘆了口氣:「這種婚戀平臺的機制就是這麼嚴格,一進來只能看到其它會員的姓名、年齡和自我介紹。要想看到本人的照片或者取得對方的聯繫方式,得先向平臺付費成爲 VIP 才行。哎,我又要買個 VIP 了。」

我一聽,哇,這婚戀平臺把代理模式玩挺 6 啊!你們想一想,主體是同事 A,目標對象是新垣結衣頭像的未知妹子。同事 A 不能直接與未知妹子進行溝通,只能經過第三方(婚介所)間接獲取對方的一些信息,他可以獲取到的信息和權限,取決於第三方願意給他什麼——這不就是典型的代理模式嗎?

用法

這一版的響應式在使用起來就要舒服的多:

const state = reactive({
  count: 0
})

watchEffect(() => {
  console.log(state.count)
}) // 0

state.count++ // 1
複製代碼

使用方式基本上就和Vue3的用法如出一轍了!能夠看到響應式最核心的原理其實就是發佈-訂閱+代理模式。不過這還不是最終版,由於他用的是ES5Object.defineProperty來作的代理模式,若是在不考慮兼容IE的狀況下仍是ES6Proxy更適合作代理,由於Proxy翻譯過來就是代理權代理人的意思。因此Vue3採用了Proxy來重構整個響應式代碼,咱們來看一下尤雨溪寫出來的最終版(Proxy版)

Proxy 版

let activeEffect

class Dep {
  subscribers = new Set()

  constructor(value) {
    this._value = value
  }

  get value() {
    this.depend()
    return this._value
  }

  set value(value) {
    this._value = value
    this.notify()
  }

  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }

  notify() {
    this.subscribers.forEach((effect) => {
      effect()
    })
  }
}

function watchEffect(effect) {
  activeEffect = effect
  effect()
  activeEffect = null
}

// proxy version
const reactiveHandlers = {
  get(target, key) {
    const value = getDep(target, key).value
    if (value && typeof value === 'object') {
      return reactive(value)
    } else {
      return value
    }
  },
  set(target, key, value) {
    getDep(target, key).value = value
  }
}

const targetToHashMap = new WeakMap()

function getDep(target, key) {
  let depMap = targetToHashMap.get(target)
  if (!depMap) {
    depMap = new Map()
    targetToHashMap.set(target, depMap)
  }

  let dep = depMap.get(key)
  if (!dep) {
    dep = new Dep(target[key])
    depMap.set(key, dep)
  }

  return dep
}

function reactive(obj) {
  return new Proxy(obj, reactiveHandlers)
}
複製代碼

能夠看到這一版的代碼又比上一版更加複雜了點,但在用法上仍是和上一版如出一轍:

const state = reactive({
  count: 0
})

watchEffect(() => {
  console.log(state.count)
}) // 0

state.count++ // 1
複製代碼

咱們來重點講解一下最終版的代碼,這一版代碼纔是最優秀的。麻雀雖小,五臟俱全,不只作了最基本的發佈-訂閱模式+代理模式,並且還用到了許多小技巧來作了性能方面的優化。

詳解

首先尤大定義了一個名爲activeEffect的空變量,用於存放watchEffect傳進來的函數:

// 定義一個暫時存放 watchEffect 傳進來的參數的變量
let activeEffect
複製代碼

接下來定義了一個名爲Dep的類,這個Dep應該是Dependence的縮寫,意爲依賴。實際上就至關於發佈-訂閱模式中的發佈者類:

// 定義一個 Dep 類,該類將會爲每個響應式對象的每個鍵生成一個發佈者實例
class Dep {
  // 用 Set 作緩存列表以防止列表中添加多個徹底相同的函數
  subscribers = new Set()

  // 構造函數接受一個初始化的值放在私有變量內
  constructor(value) {
    this._value = value
  }

  // 當使用 xxx.value 獲取對象上的 value 值時
  get value() {
    // 代理模式 當獲取對象上的value屬性的值時將會觸發 depend 方法
    this.depend()

    // 而後返回私有變量內的值
    return this._value
  }

  // 當使用 xxx.value = xxx 修改對象上的 value 值時
  set value(value) {
    // 代理模式 當修改對象上的value屬性的值時將會觸發 notify 方法
    this._value = value
    // 先改值再觸發 這樣保證觸發的時候用到的都是已經修改後的新值
    this.notify()
  }

  // 這就是咱們常說的依賴收集方法
  depend() {
    // 若是 activeEffect 這個變量爲空 就證實不是在 watchEffect 這個函數裏面觸發的 get 操做
    if (activeEffect) {
      // 但若是 activeEffect 不爲空就證實是在 watchEffect 裏觸發的 get 操做
      // 那就把 activeEffect 這個存着 watchEffect 參數的變量添加進緩存列表中
      this.subscribers.add(activeEffect)
    }
  }

  // 更新操做 一般會在值被修改後調用
  notify() {
    // 遍歷緩存列表裏存放的函數 並依次觸發執行
    this.subscribers.forEach((effect) => {
      effect()
    })
  }
}
複製代碼

以前兩版尤大都是在外頭定義了一個變量用於保存響應式對象每個鍵所對應的值,而此次是直接把值放進了Dep類的定義裏,定義成了gettersetter,在獲取值時會進行依賴收集操做,而在修改值時會進行更新操做。

接下來又定義了一個跟Vue3watchEffect名稱同樣的函數:

// 模仿 Vue3 的 watchEffect 函數
function watchEffect(effect) {
  // 先把傳進來的函數放入到 activeEffect 這個變量中
  activeEffect = effect

  // 而後執行 watchEffect 裏面的函數
  effect()

  // 最後把 activeEffect 置爲空值
  activeEffect = null
}
複製代碼

咱們在使用時不是會在這個函數裏面再傳進一個函數麼:

watchEffect(() => state.xxx)
複製代碼

這個函數就被賦值給了activeEffect這個變量上面去,而後馬上執行這個函數,通常來講這個函數裏面都會有一些響應式對象的對吧?既然有,那就會觸發getter去進行依賴收集操做,而依賴收集則是判斷了activeEffect這個變量有沒有值,若是有,那就把它添加進緩存列表裏。等到執行完這個函數後,就當即將activeEffect這個變量置爲空值,防止不在watchEffect這個函數中觸發getter的時候也執行依賴收集操做。

接下來就是定義了一個Proxy代理的處理對象:

const reactiveHandlers = {
  // 當觸發 get 操做時
  get(target, key) {
    // 先調用 getDep 函數取到裏面存放的 value 值
    const value = getDep(target, key).value

    // 若是 value 是對象的話
    if (value && typeof value === 'object') {
      // 那就把 value 也變成一個響應式對象
      return reactive(value)
    } else {
      // 若是 value 只是基本數據類型的話就直接將值返回
      return value
    }
  },
  // 當觸發 set 操做時
  set(target, key, value) {
    // 調用 getDep 函數並將裏面存放的 value 值從新賦值成 set 操做的值
    getDep(target, key).value = value
  }
}
複製代碼

若是對Proxy不是很瞭解的話,建議看看阮一峯的《ES6入門教程》,寫的仍是不錯的。

剛剛那個對象在getset操做中都用到了getDep這個函數,這個函數時在後面定義的,他會用到一個叫targetToHashMapWeakMap數據結構來存儲數據:

// 定義一個 WeakMap 數據類型 用於存放 reactive 定義的對象以及他們的發佈者對象集
const targetToHashMap = new WeakMap()
複製代碼

接下來就是定義getDep函數啦:

// 定義 getDep 函數 用於獲取 reactive 定義的對象所對應的發佈者對象集裏的某一個鍵對應的發佈者對象
function getDep(target, key) {
  // 獲取 reactive 定義的對象所對應的發佈者對象集
  let depMap = targetToHashMap.get(target)

  // 若是沒獲取到的話
  if (!depMap) {
    // 就新建一個空的發佈者對象集
    depMap = new Map()
    // 而後再把這個發佈者對象集存進 WeakMap 裏
    targetToHashMap.set(target, depMap)
  }

  // 再獲取到這個發佈者對象集裏的某一個鍵所對應的發佈者對象
  let dep = depMap.get(key)

  // 若是沒獲取到的話
  if (!dep) {
    // 就新建一個發佈者對象並初始化賦值
    dep = new Dep(target[key])
    // 而後將這個發佈者對象放入到發佈者對象集裏
    depMap.set(key, dep)
  }

  // 最後返回這個發佈者對象
  return dep
}
複製代碼

這個地方就稍微有點繞了,咱們來上圖:

每個傳進reactive裏去的對象,都會被存在WeakMap裏的鍵上。而每個鍵所對應的值,就是一個Map

// targetToHashMap: {
const obj1 = reactive({ num: 1 })   // { num: 1 }: new Map(),
const obj2 = reactive({ num: 2 })   // { num: 2 }: new Map(),
const obj3 = reactive({ num: 3 })   // { num: 3 }: new Map()
                                    // }
複製代碼

那值(Map)裏存的又是什麼呢?存的是:

WX20210729-192101.png

假設咱們reactive了一個對象{ a: 0, b: 1, c: 2 },那麼Map裏面存的就是:

{
  'a': new Dep(0),
  'b': new Dep(1),
  'c': new Dep(2)
}
複製代碼

就是把對象的鍵放到Map的鍵上,而後在用new Dep建立一個發佈者對象,再把值傳給Dep。Vue3 之因此性能比 Vue2 強不少的其中一個很是重要的優化點就是這個Proxy。並非說Proxy的性能就比Object.defineProperty高多少,而是說在Proxy裏的處理方式比Vue2時期的好不少:Vue2的響應式是一上來就一頓遍歷+遞歸把你定義的全部數據全都變成響應式的,這就會致使若是頁面上有不少很複雜的數據結構時,用Vue2寫的頁面就會白屏一小段時間。畢竟遍歷+遞歸仍是相對很慢的一個操做嘛!

React就沒有這個毛病,固然Vue3也不會有這個毛病。從代碼中能夠看出,當咱們獲取對象上的某個鍵對應的值時,會先判斷這個值到底有沒有對應的發佈者對象,沒有的話再建立發佈者對象。並且當獲取到的值是引用類型時再把這個值變成響應式對象,等你用到了響應式對象裏的值時再去新建發佈者對象。

總結成一句話就是:Vue3是用到哪部分的數據的時候,再把數據變成響應式的。而Vue2則是無論三七二十一,剛開局就全都給你變成響應式數據。

最後一步就是定義reactive函數啦:

// 模仿 Vue3 的 reactive 函數
function reactive(obj) {
  // 返回一個傳進來的參數對象的代理對象 以便使用代理模式攔截對象上的操做並應用發佈-訂閱模式
  return new Proxy(obj, reactiveHandlers)
}
複製代碼

流程圖

爲了便於你們理解,咱們使用一遍reactivewatchEffect函數,而後順便看看到底發生了什麼:

WX20210730-132843.png

首先咱們用reactive函數定義了一個對象{ num: 0 },這個對象會傳給Proxy的第一個參數,此時還並無發生什麼事情,那麼接下來咱們就在watchEffect裏打印一下這個對象的num屬性:

WX20210730-133331.png

此時傳給watchEffect的這個函數會賦值給actibveEffect這個變量上去,而後當即執行這個函數:

WX20210730-133925.png

在執行的過程當中發現有get操做,因而被Proxy所攔截,走到了get這一步:

WX20210730-140428.png

因爲在get操做中須要用getDep函數,因而又把{ num: 0 }傳給了getDep,key 是 num,因此至關於getDep({ num: 0 }, 'num')。進入到getDep函數體內,須要用targetToHashMap來獲取{ num: 0 }這個鍵所對應的值,但目前targetToHashMap是空的,因此根本獲取不到任何內容。因而進入判斷,新建一個Map賦值給targetToHashMap,至關於:targetToHashMap.set({ num: 0 }, new Map()),緊接着就是獲取這個Mapkey所對應的值:

WX20210730-141344.png

因爲Map也是空的,因此仍是獲取不到值,因而進入判斷,新建一個Dep對象:

WX20210730-141809.png

因爲是用getDep(...xxx).value來獲取到這個對象的value屬性,因此就會觸發getter

WX20210730-142346.png

順着getter咱們又來到了depend方法中,因爲activeEffect有值,因此進入判斷,把activeEffect加入到subscribes這個Set結構中。此時依賴收集部分就暫且告一段落了,接下來咱們來改變obj.num的值,看看都會發生些什麼:

WX20210730-144029.png

首先會被Proxy攔截住set操做,而後調用getDep函數:

WX20210730-143810.png

獲取到dep對象後,就會修改它的value屬性,從而觸發setter操做:

WX20210730-144603.png

最後咱們來到了通知(notify)階段,在通知階段會找到咱們的緩存列表(subscribers),而後依次觸發裏面的函數:

WX20210730-144912.png

那麼此時就會運行() => console.log(obj.num)這個函數,你覺得這就完了嗎?固然沒有!因爲運行了obj.num這個操做,因此又會觸發get操做被Proxy攔截:

WX20210730-145721.png

獲取到咱們以前建立過的發佈者對象後,又會觸發發佈者對象的getter操做:

WX20210730-150316.png

一頓繞,繞到depend方法時,咱們須要檢測一下activeEffect這個變量:

WX20210730-150635.png

因爲不會進入到判斷裏面去,因此執行了個寂寞(啥也沒執行),那麼接下來的代碼即是:

WX20210730-151831.png

最終打印出了10

結語

沒想到短短這麼七十來行代碼這麼繞吧?因此說抽絲剝繭的學習方法有多重要。若是直接看源碼的話,這裏面確定還會有各類各樣的判斷。好比watchEffect如今沒作任何的判斷對吧?那麼當咱們給watchEffect傳了一個不是函數的參數時會怎樣?當咱們給reactive對象傳數組時又會怎樣?當傳MapSet時呢?傳基本數據類型時呢?並且即便如今咱們不考慮這些狀況,就傳一個對象,裏面不要有數組等什麼其餘的東西,watchEffect也只傳函數。那麼其實在使用體驗上仍是有一點與Vue3watchEffect不一樣的地方,那就是不能在watchEffect裏面改變響應式對象的值:

WX20210730-152653.png

而寫成這樣就沒有問題:

WX20210730-152902.png

但是在Vue3watchEffect裏就不會出現這樣的情況。這是由於若是在watchEffect裏對響應式對象進行賦值操做的話就又會觸發set操做,從而被Proxy攔截,而後又繞到notify的方法上面去了,notify又會把watchEffect裏的函數運行一遍,結果又發現裏面有set操做(由於是同一段代碼嘛),而後又會去運行notify方法,繼續觸發set操做形成死循環。

因此咱們還須要考慮到這種死循環的狀況,但若是真的考慮的這麼全面的話,那相信代碼量也至關大了,咱們會被進一步繞暈。因此先吃透這段代碼,而後慢慢的咱們再來看真正的源碼都是怎麼處理這些狀況的。或者也能夠先不看源碼,本身思考一下這些問題該如何去處理,而後寫出本身的邏輯來,測試沒有問題後再去跟Vue3的源碼進行對比,看看本身實現的和尤雨溪實現的方式有何異同。

本篇文章到這裏就要告一段落了,但還沒完,這只是響應式部分。以後還有虛擬DOMdiff算法組件化根組件掛載等部分。

若是等不及看下一篇解析文章的話,也能夠直接點擊這個連接進入到codepen裏自行鑽研尤雨溪寫的代碼。代碼量不多,是咱們學習Vue3原理的絕佳資料!學會了原理以後哪怕不去看真正的源碼,在面試的時候均可以跟面試官吹兩句。由於畢竟不會有哪一個面試官考察源碼時會問:你來講一下Vue3的某某文件的第996行代碼寫的是什麼?考察確定也重點考察的是原理,不多會去考察各類判斷參數的邊界狀況處理。因此點贊+關注,跟着尤雨溪學源碼不迷路!

往期精彩文章

相關文章
相關標籤/搜索