⚠️本文爲掘金社區首發簽約文章,未獲受權禁止轉載前端
不少時候咱們都對源碼
展示出了必定的渴求,但當被問到究竟爲何想看源碼時,答案無非也就那麼幾種: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這個庫。咱們來看一眼它在GitHub
上README.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++;
複製代碼
是否是很像Vue3
的reactive
和watchEffect
啊?其實就是咱們提早定義好一個函數,當函數裏面依賴的數據項發生變化時就會自動執行這段函數,這就是響應式!
數據驅動視圖那就更容易理解了,既然當數據發生變化時能夠執行一段函數,那麼這段函數爲何不能夠執行一段更新視圖的操做呢:
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設計模式與開發實踐》
爲發佈-訂閱模式
總結出來的三個要點:
在本例中是 dom 這個對象
在本例中是 dom.#eventObj
記住這三個要點後,再來看一眼尤大的代碼,看是否是符合這仨要點:
發佈者
:dep 對象緩存列表
:dep.subscribers發佈消息
:dep.notify()因此這是一個典型的發佈-訂閱模式
尤雨溪
的初版代碼實現的仍是有些過於簡陋了,首先用起來就很不方便,由於咱們每次定義數據時都須要這麼手寫一遍getter
和setter
、手動的去執行一下依賴收集函數以及觸發的函數。這個部分顯然是能夠繼續進行封裝的,那麼再來看一眼尤雨溪
實現的第二版:
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
的用法如出一轍了!能夠看到響應式最核心的原理其實就是發佈-訂閱
+代理模式
。不過這還不是最終版,由於他用的是ES5
的Object.defineProperty
來作的代理模式
,若是在不考慮兼容IE
的狀況下仍是ES6
的Proxy
更適合作代理
,由於Proxy
翻譯過來就是代理權
、代理人
的意思。因此Vue3
採用了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
類的定義裏,定義成了getter
和setter
,在獲取值時會進行依賴收集操做,而在修改值時會進行更新操做。
接下來又定義了一個跟Vue3
的watchEffect
名稱同樣的函數:
// 模仿 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入門教程》,寫的仍是不錯的。
剛剛那個對象在get
和set
操做中都用到了getDep
這個函數,這個函數時在後面定義的,他會用到一個叫targetToHashMap
的WeakMap
數據結構來存儲數據:
// 定義一個 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)裏存的又是什麼呢?存的是:
假設咱們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)
}
複製代碼
爲了便於你們理解,咱們使用一遍reactive
和watchEffect
函數,而後順便看看到底發生了什麼:
首先咱們用reactive
函數定義了一個對象{ num: 0 }
,這個對象會傳給Proxy
的第一個參數,此時還並無發生什麼事情,那麼接下來咱們就在watchEffect
裏打印一下這個對象的num
屬性:
此時傳給watchEffect
的這個函數會賦值給actibveEffect
這個變量上去,而後當即執行這個函數:
在執行的過程當中發現有get
操做,因而被Proxy
所攔截,走到了get
這一步:
因爲在get
操做中須要用getDep
函數,因而又把{ num: 0 }
傳給了getDep
,key 是 num,因此至關於getDep({ num: 0 }, 'num')
。進入到getDep
函數體內,須要用targetToHashMap
來獲取{ num: 0 }
這個鍵所對應的值,但目前targetToHashMap
是空的,因此根本獲取不到任何內容。因而進入判斷,新建一個Map
賦值給targetToHashMap
,至關於:targetToHashMap.set({ num: 0 }, new Map())
,緊接着就是獲取這個Map
的key
所對應的值:
因爲Map
也是空的,因此仍是獲取不到值,因而進入判斷,新建一個Dep
對象:
因爲是用getDep(...xxx).value
來獲取到這個對象的value
屬性,因此就會觸發getter
:
順着getter
咱們又來到了depend
方法中,因爲activeEffect
有值,因此進入判斷,把activeEffect
加入到subscribes
這個Set
結構中。此時依賴收集部分就暫且告一段落了,接下來咱們來改變obj.num
的值,看看都會發生些什麼:
首先會被Proxy
攔截住set
操做,而後調用getDep
函數:
獲取到dep
對象後,就會修改它的value
屬性,從而觸發setter
操做:
最後咱們來到了通知(notify)階段,在通知階段會找到咱們的緩存列表(subscribers),而後依次觸發裏面的函數:
那麼此時就會運行() => console.log(obj.num)
這個函數,你覺得這就完了嗎?固然沒有!因爲運行了obj.num
這個操做,因此又會觸發get
操做被Proxy
攔截:
獲取到咱們以前建立過的發佈者對象後,又會觸發發佈者對象的getter
操做:
一頓繞,繞到depend
方法時,咱們須要檢測一下activeEffect
這個變量:
因爲不會進入到判斷裏面去,因此執行了個寂寞(啥也沒執行),那麼接下來的代碼即是:
最終打印出了10
。
沒想到短短這麼七十來行代碼這麼繞吧?因此說抽絲剝繭的學習方法有多重要。若是直接看源碼的話,這裏面確定還會有各類各樣的判斷。好比watchEffect
如今沒作任何的判斷對吧?那麼當咱們給watchEffect
傳了一個不是函數的參數時會怎樣?當咱們給reactive
對象傳數組時又會怎樣?當傳Map
、Set
時呢?傳基本數據類型時呢?並且即便如今咱們不考慮這些狀況,就傳一個對象,裏面不要有數組等什麼其餘的東西,watchEffect
也只傳函數。那麼其實在使用體驗上仍是有一點與Vue3
的watchEffect
不一樣的地方,那就是不能在watchEffect
裏面改變響應式對象的值:
而寫成這樣就沒有問題:
但是在Vue3
的watchEffect
裏就不會出現這樣的情況。這是由於若是在watchEffect
裏對響應式對象進行賦值操做的話就又會觸發set
操做,從而被Proxy
攔截,而後又繞到notify
的方法上面去了,notify
又會把watchEffect
裏的函數運行一遍,結果又發現裏面有set
操做(由於是同一段代碼嘛),而後又會去運行notify
方法,繼續觸發set
操做形成死循環。
因此咱們還須要考慮到這種死循環的狀況,但若是真的考慮的這麼全面的話,那相信代碼量也至關大了,咱們會被進一步繞暈。因此先吃透這段代碼,而後慢慢的咱們再來看真正的源碼都是怎麼處理這些狀況的。或者也能夠先不看源碼,本身思考一下這些問題該如何去處理,而後寫出本身的邏輯來,測試沒有問題後再去跟Vue3
的源碼進行對比,看看本身實現的和尤雨溪實現的方式有何異同。
本篇文章到這裏就要告一段落了,但還沒完,這只是
響應式部分
。以後還有虛擬DOM
、diff算法
、組件化
、根組件掛載
等部分。
若是等不及看下一篇解析文章的話,也能夠直接點擊這個連接進入到codepen
裏自行鑽研尤雨溪
寫的代碼。代碼量不多,是咱們學習Vue3
原理的絕佳資料
!學會了原理以後哪怕不去看真正的源碼,在面試的時候均可以跟面試官吹兩句。由於畢竟不會有哪一個面試官考察源碼時會問:你來講一下Vue3
的某某文件的第996
行代碼寫的是什麼?考察確定也重點考察的是原理,不多會去考察各類判斷參數的邊界狀況處理。因此點贊
+關注
,跟着尤雨溪
學源碼不迷路!