更新了第二篇,你為什麼看不懂原始碼之Vue 3.0 面面俱到【2】
先嘮會兒嗑
記得很早之前,有個人說過,看原始碼就像武俠小說裡的 學習武林祕籍。會耍刀耍劍那是外力,學習武功祕籍才是內力,才能和別人在對波或戰前拼氣時更升一籌。
在實際開發中,想象這樣一個場景:
- 小A:我這兒遇到一個bug,不知道是不是瀏覽器相容?也可能是網路問題?會不會是框架問題!
- 你:(輕蔑的一瞥),這塊兒呀,我之前擼過它的原始碼,是XX導致了XX,讓你這塊兒程式碼產生了XX的影響。(講完後背著手頭也不回的去接杯咖啡。)
這多裝逼呀!
現實總不如意
讀框架的原始碼的確可以裝以上的逼,但......
每當你看原始碼時,就像初中時聽數學課,幾分鐘前興致盎然,低頭撿根筆,再抬頭時已 “物是人非”,原始碼彷彿變成質能方程,數學老師彷彿變成沒有鬍子的 愛因斯坦 ??!
你看到一個個大V發出來的原始碼解讀文章,為什麼別人能看懂?
你可能會想:是不是我太笨?是不是原始碼太難懂?是不是我不適合這一行......
別擔心,99%的人和你一樣。
那些大V只告訴你讀原始碼時的過程,卻沒有告訴你讀原始碼前的準備。
而我會從讀原始碼的準備階段講起。
磨刀不誤砍柴工
讀原始碼前,你的基礎功一定要紮實。
比如以下兩個方法:
export const isObject = (val: any) =>
val !== null && typeof val === 'object'
複製程式碼
export const isObject = (val: any) =>
val !== null && Object.prototype.toString.call(val) === '[object Object]'
複製程式碼
同樣的函式名,但功能不同,如果你瞭解 typeof
並且瞭解原型方法 toString
以及 call
的作用,那你很容易分辨出來這兩者的區別和作用。
當你看原始碼時,掃一眼函式體,便知道框架作者要做什麼。
你可能會問我,基礎功不行怎麼看原始碼?
emmmm 我的建議是別看了。先磨刀吧。因為你當前階段提升的最快方法是學習基礎知識。
預知此事先躬行
小A:我基礎功練好了,可以看原始碼了吧?
我:可以看,也不能看。
小A:你在逗我?
不是的,可以看的意思是,我們先大體掃一邊原始碼,這個過程小於 5分鐘。看看原始碼作者用了什麼新鮮的api 或老的你沒見過的api。
現在我們需要把 vue 3.0的原始碼clone下來,享受這短暫的五分鐘。
可以看到,vue 3.0 用了大量的
Reflect\Proxy\Symbol\Map\Set
你得確保這些api你都瞭如指掌,如果你做不到了如指掌,也得知道它們分別是幹什麼的。
在這篇文章裡我不會給你一一列舉它們的功能,我只告訴你讀原始碼前的準備。希望你能自己理解,而不是拾人牙慧。
要拾人牙慧
小A:你上文剛說不要拾人牙慧,這下又要拾人牙慧,你到底要我幹什麼?
我:上文講,學習新方法時,要自己探索,照著官方標準自己理解,而這裡,要進入原始碼的閱讀了,我們可以站在巨人們的肩膀上,對原始碼結構稍微有些瞭解。後面自己閱讀時會更容易些.
小A:我信你了。
本文將以Vue資料繫結的核心實現作為例子,所以這一步,需要你在社群找找大V們寫好的文章。比如我覺得下面這篇寫的不錯。
第一步先給文章點個?,支援下原作者。看文章時,忽略掉你已經知道的知識,比如 proxy 的用法,Reflect 的用法。然後試著手敲一遍文章中的程式碼demo。
在這篇文章裡你最好自己先實現一遍 Proxy 代理深層物件。
以下是我寫的demo
<html>
<div id='array'></div>
<script>
const array= document.querySelector('#array')
const isObject = (val) => val !== null && typeof val === 'object'
function proxyMethod(obj) {
let p2 = new Proxy(obj, {
get(target, key, val) {
const res = Reflect.get(target, key, val)
if(isObject(res)) {
return proxyMethod(res)
}
return res
},
set(target, key, val, receiver) {
const res = Reflect.set(target, key, val)
array.innerHTML = receiver
console.log('set', receiver)
return true
}
});
return p2
}
let p = {
arr: [1,2,3],
c: 2
}
let p2 = proxyMethod(p)
p2.arr.push(8)
setTimeout(() => {
p2.arr.push(6)
}, 1000)
</script>
</html>
複製程式碼
將社群的文章看的大概,瞭解原始碼的核心功能,則可以進行下一步了。
庖丁解牛
到目前為止,你知道了原始碼實現的核心,而其他的就是原始碼包裝核心之後實現功能的程式碼。舉個例子:你現在知道了發動機怎麼做,接下來要做的是,裝上傳動、裝上輪胎、裝上車架......
所以接下來你得對程式碼的全域性有個大概的瞭解,將程式碼拆開來,你需要知道輪胎在哪兒?車門在哪兒?
這個過程,我稱之為 “架構拆解”,庖丁解牛,你解架構。那如何解呢。先列舉下Vue的檔案架構。
這裡推薦一個工具,可以在命令列展示檔案的樹狀結構。
brew install tree
tree -L 1
├── compiler-core
├── compiler-dom
├── global.d.ts
├── reactivity
├── runtime-core
├── runtime-dom
├── runtime-test
├── server-renderer
├── shared
├── template-explorer
└── vue
複製程式碼
接下來逐條對資料夾進行分析。本篇只分析 reactivity
資料夾。
.
├── README.md
├── __tests__
├── api-extractor.json
├── index.js
├── package.json
└── src
複製程式碼
有 README 先看 README。
# @vue/reactivity
## Usage Note
This package is inlined into Global & Browser ESM builds of user-facing renderers (e.g. `@vue/runtime-dom`), but also published as a package that can be used standalone. The standalone build should not be used alongside a pre-bundled build of a user-facing renderer, as they will have different internal storage for reactivity connections. A user-facing renderer should re-export all APIs from this package.
For full exposed APIs, see `src/index.ts`. You can also run `yarn build reactivity --types` from repo root, which will generate an API report at `temp/reactivity.api.md`.
## Credits
The implementation of this module is inspired by the following prior art in the JavaScript ecosystem:
- [Meteor Tracker](https://docs.meteor.com/api/tracker.html)
- [nx-js/reactivity-util](https://github.com/nx-js/reactivity-util)
- [salesforce/observable-membrane](https://github.com/salesforce/observable-membrane)
## Caveats
- Built-in objects are not observed except for `Map`, `WeakMap`, `Set` and `WeakSet`.
複製程式碼
從上段看出來,這個包可以獨立使用。至於如何使用,可以看 index.ts
檔案,前提是,我們瞭解它匯出的每一物件。
我們再看看 src
的結構
.
├── baseHandlers.ts
├── collectionHandlers.ts
├── computed.ts
├── effect.ts
├── index.ts
├── lock.ts
├── operations.ts
├── reactive.ts
└── ref.ts
複製程式碼
接下來的工作是標註每一個檔案的作用。最好能做備註。從 index.ts
讀起,採用 深度優先
的方式。
接下來我會一行行讀,你可以跟隨我的腳步......
祕徑追蹤
index.ts
export { ref, isRef, toRefs, Ref, UnwrapRef } from './ref'
複製程式碼
先進入 ref 檔案看看 ref 方法怎麼來的。
ref.ts
import { track, trigger } from './effect'
import { OperationTypes } from './operations'
import { isObject } from '@vue/shared'
import { reactive } from './reactive'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)
export interface Ref<T> {
[refSymbol]: true
value: UnwrapNestedRefs<T>
}
export type UnwrapNestedRefs<T> = T extends Ref<any> ? T : UnwrapRef<T>
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)
export function ref<T>(raw: T): Ref<T> {
// 如果是物件,則用 reactive 方法 包裝 raw
raw = convert(raw)
// 返回一個 v 物件,在 取value 值時,呼叫 track 方法,在存 value 值時,呼叫 trigger方法
const v = {
[refSymbol]: true,
get value() {
track(v, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
trigger(v, OperationTypes.SET, '')
}
}
return v as Ref<T>
}
複製程式碼
可以看到,我們遇到了三個未知函式
- convert 方法裡呼叫了
reactive
方法,對ref 的引數進行了包裝。 - 在 v 物件中, 取value 值時,呼叫 track 方法,在存 value 值時,呼叫 trigger方法
繼續往下走,看看 reactive 方法的內容,我將我讀程式碼的思路寫進了備註裡,你可以檢索 MARK1 - MARK10 來逐行看思路。 reactive.ts
import { isObject, toTypeString } from '@vue/shared'
import { mutableHandlers, readonlyHandlers } from './baseHandlers'
import {
mutableCollectionHandlers,
readonlyCollectionHandlers
} from './collectionHandlers'
import { UnwrapNestedRefs } from './ref'
import { ReactiveEffect } from './effect'
// WeakMap 主要是用來儲存 {target -> key -> dep} 的連結,它更像是 依賴 Dep 的類,它包含了一組 Set,但我們只是簡單的儲存它們,以減少記憶體消耗
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap = new WeakMap<any, KeyToDepMap>()
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
const observableValueRE = /^\[object (?:Object|Array|Map|Set|WeakMap|WeakSet)\]$/
// MARK9: ->進去看
const canObserve = (value: any): boolean => {
return (
// 不是vue 物件
!value._isVue &&
// 不是 vNode
!value._isVNode &&
// 白名單: Object|Array|Map|Set|WeakMap|WeakSet
observableValueRE.test(toTypeString(value)) &&
// 沒有代理過的
!nonReactiveValues.has(value)
)
}
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// MARK1: 如果target 只讀,則返回它
if (readonlyToRaw.has(target)) {
return target
}
// MARK2: 如果target被使用者設定為只讀,則讓它只讀,並返回
if (readonlyValues.has(target)) {
return readonly(target)
}
// MARK3: 貌似到重點了
return createReactiveObject(
target,
// MARK3: 底下這一堆是 weakMap,先不管它們
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>>
export function readonly(target: object) {
// value is a mutable observable, retrieve its original and return
// a readonly version.
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
readonlyHandlers,
readonlyCollectionHandlers
)
}
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// MARK4: 很明顯,這裡只 typeof判斷,Array Date之類的也能通過
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// MARK5: 已經代理過的物件,不需要在代理
let observed = toProxy.get(target)
// MARK6: 這裡用 void 0代替了undfined,防止 undfined 被重寫。get 到新姿勢了。
if (observed !== void 0) {
return observed
}
// MARK7: 目標本身就是一個 proxy 物件
if (toRaw.has(target)) {
return target
}
// MARK8: 只有加入白名單的物件才能被代理
if (!canObserve(target)) {
return target
}
// MARK10: collectionTypes 可以看到是一個Set結構,放了幾個建構函式:[Set, Map, WeakMap, WeakSet],這個三目表示式就是區分了以上四種物件和其他物件的 handlers,我們先從 baseHandlers 看起
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
observed = new Proxy(target, handlers)
// 以下則是儲存下已經代理過的物件,以作優化。
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
// 返回代理後的物件
return observed
}
export function isReactive(value: any): boolean {
return reactiveToRaw.has(value) || readonlyToRaw.has(value)
}
export function isReadonly(value: any): boolean {
return readonlyToRaw.has(value)
}
export function toRaw<T>(observed: T): T {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}
export function markReadonly<T>(value: T): T {
readonlyValues.add(value)
return value
}
export function markNonReactive<T>(value: T): T {
nonReactiveValues.add(value)
return value
}
複製程式碼
reactive.ts 終於完了,接下來我們從 上文的 MARK10 進入 到 handler.ts
中,老規矩,這裡的程式碼思路從 MARK11 開始。
import { reactive, readonly, toRaw } from './reactive'
import { OperationTypes } from './operations'
import { track, trigger } from './effect'
import { LOCKED } from './lock'
import { isObject, hasOwn } from '@vue/shared'
import { isRef } from './ref'
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => (Symbol as any)[key])
.filter(value => typeof value === 'symbol')
)
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
const res = Reflect.get(target, key, receiver)
//MARK13: 防止key為Symbol的內建物件,比如 Symbol.iterator
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res
}
//MARK14: 從上文知道,被ref 包裝過,則返回
if (isRef(res)) {
return res.value
}
// 這裡貌似是依賴收集的,暫且不深入
track(target, OperationTypes.GET, key)
// MARK15 對深層物件再次包裝
// res 是深層物件,如果它不是隻讀物件,則呼叫 reactive 繼續代理,上文自己實現過,這裡很好理解
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
// MARK16 優化:之前有存過,直接拿就行了
value = toRaw(value)
// MARK17 key 是否為 taget 的屬性,
/**
*
* export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
這個方法是解決 陣列push時,會呼叫兩次 set 的情況,比如 arr.push(1)
第一次set,在陣列尾部新增1
第二次set,給陣列新增length屬性
hasOwnProperty 方法用來判斷目標物件是否含有指定屬性。陣列本身就有length的屬性,所以這裡是 true
*/
const hadKey = hasOwn(target, key)
const oldValue = target[key]
/**
* MARK18 如果 value 不是響應式資料,則需要將其賦值給 oldValue
*/
if (isRef(oldValue) && !isRef(value)) {
//MARK19 這將觸發 oldValue 的 set value 方法,如果 isObject(value) ,則會經過 reactive 再包裝一次,將其變成響應式資料
oldValue.value = value
return true
}
const result = Reflect.set(target, key, value, receiver)
// MARK20 target 如果只讀 或者 存在於 reactiveToRaw 則不進入條件,reactiveToRaw 儲存著代理後的物件
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
// MARK21 只看生產環境
} else {
//MARK22 屬性新增,觸發 ADD 列舉
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
//MARK23 屬性修改,觸發 SET 列舉
trigger(target, OperationTypes.SET, key)
}
}
/** MARK24 對應 MARK17
* else {}
*/
}
return result
}
function deleteProperty(target: any, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue })
} else {
trigger(target, OperationTypes.DELETE, key)
}
}
return result
}
function has(target: any, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, OperationTypes.HAS, key)
return result
}
function ownKeys(target: any): (string | number | symbol)[] {
track(target, OperationTypes.ITERATE)
return Reflect.ownKeys(target)
}
export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
// MARK11 入口函式
export const readonlyHandlers: ProxyHandler<any> = {
// MARK12: 建立 getter
get: createGetter(true),
// MARK12: 建立 setter
set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
} else {
return set(target, key, value, receiver)
}
},
deleteProperty(target: any, key: string | symbol): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(
key
)}" failed: target is readonly.`,
target
)
}
return true
} else {
return deleteProperty(target, key)
}
},
has,
ownKeys
}
複製程式碼
未完待續
到此為止,你對程式碼整體有了模糊的理解,但還有些稜角不清晰,比如:
- MARK4 那一連串
WeakMap
是幹啥的? - MARK10 下面為什麼 set 了多次?
......
在後面的文章中,我會逐條分析。讀懂原始碼是個漫長的過程,未完待續......