大家好,我是若川。最近組織了源碼共讀活動。每周讀 200 行左右的源碼。很多第一次讀源碼的小伙伴都感覺很有收獲,感興趣可以加我微信
ruochuan12
,拉你進群學習。
初學者也能看懂的 Vue3 源碼中那些實用的基礎工具函數
本文是紀年小姐姐源碼共讀第二期寫的筆記,非常好,可先收藏后學習。
1. 解讀前的準備
粗略閱讀了川哥的文章之后,感覺這期跟上一期不一樣。上一期主要學習如何實現某個功能,而這一期主要是學習 Vue3 源碼中的工具函數,以及 Vue3 源碼的一些調試技巧。雖然看起來偏基礎,但我覺得很考驗一個程序員的基本功和耐心。
學習目標:
1)調試源碼之打包構建項目代碼,生成 sourcemap 調試源碼
2)學習源碼中的工具函數
目標:跟著川哥的文章走完一遍調試的流程,動手敲工具函數,對外輸出記錄文檔。
資源準備:
Vue3 源碼地址:https://github.com/vuejs/vue-next
2. 源碼調試
2.1 閱讀開源項目的 README.md 和貢獻指南 contributing.md
我覺得這兩個文件對閱讀源碼的開發者來說十分重要。README.md 描述的是項目的基本信息,它可以快速了解這個項目的全貌。貢獻指南 contributing.md 會包含如何參與項目開發,項目打包/運行命令,項目目錄結構等等,它能幫助你更好地調試/參與開發源碼。在 contributing.md 中我看到了一些比較感興趣的知識點,比如打包構建格式/配置,包依賴處理。
2.2 打包構建項目代碼
安裝完依賴,直接運行yarn build
就可以打包 Vue3 的項目代碼了,打包的產物如下(以 shared 模塊為例):

這里的 cjs
,esm
是 JS 里用來實現【模塊化】的不同規則,JS 的模塊化標準還有 amd
,umd
,iife
。
CJS,CommonJS,只能在 NodeJS 上運行,使用 require("module") 讀取并加載模塊,不支持瀏覽器
ESM,ECMAScript Module,現在使用的模塊方案,使用 import export 來管理依賴,瀏覽器直接通過
<script type="module">
即可使用該寫法。NodeJS 可以通過使用 mjs 后綴或者在 package.json 添加 "type": "module" 來使用
2.3 生成 sourcemap 調試 vue-next 源碼
在貢獻指南 contributing.md 文件中描述了如何生成 sourcemap 文件:添加【--sourcemap】參數即可。
node?scripts/dev.js?--sourcemap
packages/vue/dist/vue.global.js.map 就是 sourcemap 文件了。
sourcemap 是一個信息文件,里面儲存著位置信息,轉換后的代碼的每一個位置,所對應的轉換前的位置。有了它,出錯時出錯工具將直接顯示原始代碼,而不是轉換后的代碼,方便調試。
3. 工具函數(TS 版)
3.1 babelParserDefaultPlugins:babel 解析默認插件
/***?List?of?@babel/parser?plugins?that?are?used?for?template?expression*?transforms?and?SFC?script?transforms.?By?default?we?enable?proposals?slated*?for?ES2020.?This?will?need?to?be?updated?as?the?spec?moves?forward.*?Full?list?at?https://babeljs.io/docs/en/next/babel-parser#plugins*/
const?babelParserDefaultPlugins?=?['bigInt','optionalChaining','nullishCoalescingOperator'
]?as?const
它定義了三個默認插件, as const
這個語法叫 const 斷言,它可以創建完整的 readonly 對象(只讀狀態),編譯器可以通過 as const 推斷出可用于的最具體的表達類型。
3.2 EMPTY_OBJ:空對象,EMPTY_ARR:空數組
export?const?EMPTY_OBJ:?{?readonly?[key:?string]:?any?}?=?__DEV__??Object.freeze({}):?{}export?const?EMPTY_ARR?=?__DEV__???Object.freeze([])?:?[]
Object.freeze 凍結對象,不可修改對象的最外層,這樣的寫法可以降低在開發過程中發生錯誤。
DEV ?是一個環境變量,為了避免在生產環境報錯,生產環境使用的還是 {} 和 []。
3.3 NOOP:空函數
export?const?NOOP?=?()?=>?{}
3.4 NO:永遠返回 false 的函數
export?const?NO?=?()?=>?false
3.5 isOn:判斷字符串是否以 on 開頭,并且 on 后首字母是非小寫字母
const?onRE?=?/^on[^a-z]/
export?const?isOn?=?(key:?string)?=>?onRE.test(key)
【^】符號在開頭,表示是指【以什么開頭】,在其他地方是指【非】。與之相反的是:【$】符合在結尾,則表示是以什么結尾。
日常開發中我們也經常會用到正則判斷,可以收集起來,積累的數量多了就不用每次都去搜索了????。
3.6 isModelListener:監聽器
export?const?isModelListener?=?(key:?string)?=>?key.startsWith('onUpdate:')
判斷字符串是不是以【onUpdate:】開頭
3.7 extend:合并對象
export?const?extend?=?Object.assign
其實 extend 就是 Object.assign,用于將所有可枚舉屬性的值從一個或多個源對象分配到目標對象。
3.8 ?remove:移除數組的一項
export?const?remove?=?<T>(arr:?T[],?el:?T)?=>?{const?i?=?arr.indexOf(el)if?(i?>?-1)?{arr.splice(i,?1)}
}
看源碼的實現很好理解,傳入一個數組和一個元素,判斷元素是否存在在數組中,如果存在將其刪除。
川哥的文章里有說到,splice 是一個很耗性能的方法,刪除數組中的一項,其他元素都要移動位置。所以在考慮性能的情況下,可以將刪除的元素設為 null,在使用執行時為 null 的不執行,也可達到相同的效果。
3.9 hasOwn:判斷一個屬性是否屬于某個對象
const?hasOwnProperty?=?Object.prototype.hasOwnProperty
export?const?hasOwn?=?(val:?object,key:?string?|?symbol
):?key?is?keyof?typeof?val?=>?hasOwnProperty.call(val,?key)
函數本身很好理解,利用原型的 API:hasOwnProperty 來判斷 key 是否是 obj 本身的屬性。
但【key is keyof typeof val】可能會有些迷惑,這里包含了三個 typescript 的語法,意思是函數返回的 key 是 屬于 val 對象的鍵的聯合類型。
【is】關鍵字:它被稱為類型謂詞,用來判斷一個變量屬于某個接口或類型,比如:
const?isNumber?=?(val:?unknown):?val?is?number?=>?typeof?val?===?'number'
const?isString?=?(val:?unknown):?val?is?string?=>?typeof?val?===?'string'
【keyof】關鍵字:用于獲取某種類型的所有鍵,其返回類型是聯合類型,比如:
interface?Person?{name:?string;age:?number;
}
type?K?=?keyof?Person;?//?"name"?|?"age"
【typeof】關鍵字:js 中的 typeof 只能獲取幾種類型,而在 ts 中 typeof 用來獲取一個變量聲明或對象的類型,比如:
interface?Person?{name:?string;age:?number;
}const?sem:?Person?=?{?name:?'semlinker',?age:?30?};
type?Sem?=?typeof?sem;?//?->?Person
3.10 判斷是否某種類型
//?判斷數組
export?const?isArray?=?Array.isArray//?對象轉字符串
export?const?objectToString?=?Object.prototype.toString
export?const?toTypeString?=?(value:?unknown):?string?=>objectToString.call(value)//?判斷是否?Map?對象
export?const?isMap?=?(val:?unknown):?val?is?Map<any,?any>?=>toTypeString(val)?===?'[object?Map]'//?判斷是否?Set?對象
export?const?isSet?=?(val:?unknown):?val?is?Set<any>?=>toTypeString(val)?===?'[object?Set]'//?判斷是否?Date?對象
export?const?isDate?=?(val:?unknown):?val?is?Date?=>?val?instanceof?Date//?判斷是否函數
export?const?isFunction?=?(val:?unknown):?val?is?Function?=>typeof?val?===?'function'//?判斷是否字符串
export?const?isString?=?(val:?unknown):?val?is?string?=>?typeof?val?===?'string'//?判斷是否?Symbol
export?const?isSymbol?=?(val:?unknown):?val?is?symbol?=>?typeof?val?===?'symbol'//?判斷是否對象(不包括?null)
export?const?isObject?=?(val:?unknown):?val?is?Record<any,?any>?=>val?!==?null?&&?typeof?val?===?'object'//?判斷是否?Promise
export?const?isPromise?=?<T?=?any>(val:?unknown):?val?is?Promise<T>?=>?{return?isObject(val)?&&?isFunction(val.then)?&&?isFunction(val.catch)
}
有了這些函數就可以在工作中用起來啦。
3.11 toRawType:對象轉字符串,截取后第八位到倒數第二位。
export?const?toRawType?=?(value:?unknown):?string?=>?{//?extract?"RawType"?from?strings?like?"[object?RawType]"return?toTypeString(value).slice(8,?-1)
}
可以截取到 String Array 等這些類型,這個函數可以用來做類型判斷。
3.12 ?isPlainObject:判斷是否純粹的對象
export?const?isPlainObject?=?(val:?unknown):?val?is?object?=>toTypeString(val)?===?'[object?Object]'
3.13 isIntegerKey:判斷是不是數字型的字符串 key 值
export?const?isIntegerKey?=?(key:?unknown)?=>isString(key)?&&key?!==?'NaN'?&&key[0]?!==?'-'?&&''?+?parseInt(key,?10)?===?key
第一步先判斷 key 是否是字符串類型(作為 key 值有兩種類型,string 和 symbol),第二步排除 NaN 值,第三步排除 - 值(排除負數),第四步將 key 轉換成數字再隱式轉換為字符串,與原 key 對比。
3.14 isReservedProp:判斷該屬性是否為保留屬性
/***?Make?a?map?and?return?a?function?for?checking?if?a?key*?is?in?that?map.*?IMPORTANT:?all?calls?of?this?function?must?be?prefixed?with*?\/\*#\_\_PURE\_\_\*\/*?So?that?rollup?can?tree-shake?them?if?necessary.*/
export?function?makeMap(str:?string,expectsLowerCase?:?boolean
):?(key:?string)?=>?boolean?{const?map:?Record<string,?boolean>?=?Object.create(null)const?list:?Array<string>?=?str.split(',')for?(let?i?=?0;?i?<?list.length;?i++)?{map[list[i]]?=?true}return?expectsLowerCase???val?=>?!!map[val.toLowerCase()]?:?val?=>?!!map[val]
}export?const?isReservedProp?=?/*#__PURE__*/?makeMap(//?the?leading?comma?is?intentional?so?empty?string?""?is?also?included',key,ref,'?+'onVnodeBeforeMount,onVnodeMounted,'?+'onVnodeBeforeUpdate,onVnodeUpdated,'?+'onVnodeBeforeUnmount,onVnodeUnmounted'
)//?使用:
isReservedProp("key")?//?true
isReservedProp("test")?//?false
isReservedProp("")?//?true
如何解讀這個函數?先看 makeMap,它傳入一個字符串,將這個字符串轉換成數組,并循環賦值 key 給一個空對象map,然后返回一個包含參數 val 的閉包用來檢查 val 是否是存在在字符串中。
isReservedProp("key") 其實就相當于 makeMap(str)("key")。
3.15 cacheStringFunction 緩存字符串的函數
const?cacheStringFunction?=?<T?extends?(str:?string)?=>?string>(fn:?T):?T?=>?{const?cache:?Record<string,?string>?=?Object.create(null)return?((str:?string)?=>?{const?hit?=?cache[str]return?hit?||?(cache[str]?=?fn(str))})?as?any
}//?使用例子:
//?"-"連字符轉小駝峰
//?\w:0-9a-zA-Z_,表示由數字,大小寫字母和下劃線組成
const?camelizeRE?=?/-(\w)/g
export?const?camelize?=?cacheStringFunction((str:?string):?string?=>?{return?str.replace(camelizeRE,?(_,?c)?=>?(c???c.toUpperCase()?:?''))
})
camelize("text-node")?//?"textNode"//?大寫字母轉"-"連字符
//?\B?是指?非?\B?單詞邊界。
const?hyphenateRE?=?/\B([A-Z])/g;
const?hyphenate?=?cacheStringFunction((str)?=>?str.replace(hyphenateRE,?'-$1').toLowerCase());
hyphenate("WordPress")?//?"word-press"//?首字母轉大寫
const?capitalize?=?cacheStringFunction((str:?string)?=>?str.charAt(0).toUpperCase()?+?str.slice(1)
)
const?toHandlerKey?=?cacheStringFunction((str)?=>?(str???`on${capitalize(str)}`?:?``));
toHandlerKey('click')?//?"onClick"
這個函數和上面 makeMap 函數類似,傳入一個 fn 參數,返回一個包含參數 str 的閉包,將這個 str 字符串作為 key 賦值給一個空對象 cache,閉包返回 cache[str] || (cache[str] = fn(str))。
【cache[str] || (cache[str] = fn(str))】的意思是,如果 cache 有緩存到 str 這個 key,直接返回對應的值,否則,先調用 fn(str),再賦值給 cache[str],這樣可以將需要經過 fn 函數處理的字符串緩存起來,避免多次重復處理字符串。
3.16 hasChanged:判斷值是否有變化
const?hasChanged?=?(value:?any,?oldValue:?any):?boolean?=>!Object.is(value,?oldValue)
Object.is 方法判斷兩個值是否為同一個值。
3.17 ?invokeArrayFns:執行數組里的函數
export?const?invokeArrayFns?=?(fns:?Function[],?arg?:?any)?=>?{for?(let?i?=?0;?i?<?fns.length;?i++)?{fns[i](arg)}
}
這種寫法方便統一執行多個函數。
3.18 def:定義一個不可枚舉的對象
export?const?def?=?(obj:?object,?key:?string?|?symbol,?value:?any)?=>?{Object.defineProperty(obj,?key,?{configurable:?true,enumerable:?false,value})
}
Object.defineProperty,語法:Object.defineProperty(obj, prop, descriptor),它是一個非常重要的 API,經常會在源碼中看見它。
在 ES3 中,除了一些內置屬性(如:Math.PI),對象所有的屬性在任何時候都可以被[修改、插入、刪除。
在ES5 中,我們可以設置屬性是否可以被改變或是被刪除——在這之前,它是內置屬性的特權。
ES5 中引入了屬性描述符的概念,我們可以通過它對所定義的屬性有更大的控制權,這些屬性描述符(特性)包括:value —— 獲取屬性時所返回的值。writable —— 該屬性是否可寫。enumerable —— 該屬性在 for in 循環中是否會被枚舉。configurable —— 該屬性是否可被刪除。set() —— 該屬性的更新操作所調用的函數。get() —— 獲取屬性值時所調用的函數。
另外,數據描述符(其中屬性為:enumerable,configurable,value,writable)與存取描述符(其中屬性為enumerable,configurable,set(),get())之間是有互斥關系的。在定義了set()和get()之后,描述符會認為存取操作已被定義了,其中再定義 value 和 writable 會引起錯誤。
3.19 toNumber:轉數字
??export?const?toNumber?=?(val:?any):?any?=>?{const?n?=?parseFloat(val)return?isNaN(n)???val?:?n}
3.20 getGlobalThis:全局對象
let?_globalThis:?any
export?const?getGlobalThis?=?():?any?=>?{return?(_globalThis?||(_globalThis?=typeof?globalThis?!==?'undefined'??globalThis:?typeof?self?!==?'undefined'??self:?typeof?window?!==?'undefined'??window:?typeof?global?!==?'undefined'??global:?{}))
}
第一次調用這個函數時,_globalThis 肯定為 "undefined",接著執行【||】后的語句。
typeof globalThis !== 'undefined'
如果 globalThis 不是 undefined,返回 globalThis:MDN globalThis。否則 ->typeof self !== 'undefined'
如果 self 不是 undefined,返回 self。否則 ->typeof window !== 'undefined'
如果 window 不是 undefined,返回 widow。否則 ->typeof global !== 'undefined'
如果 global 不是 undefined,返回 global。否則 ->返回 {}
第二次調用這個函數,就直接返回 _globalThis,不需要第二次繼續判斷了????
4. 感想
很多工具函數可以通過做緩存以達到優化性能的目的
Object 對象 API 解析 無論什么時候都不過時,適合反復閱讀,加深對 Object 的理解
工作中如果有用到類似的工具函數,可參考這些寫法
學習了一些 typescript 不太常見的語法:【! 非空斷言操作符】【?? 空值合并運算符】
生成 sourcemap 調試 ts 代碼
最近組建了一個湖南人的前端交流群,如果你是湖南人可以加我微信?ruochuan12?私信 湖南?拉你進群。
推薦閱讀
我在阿里招前端,該怎么幫你(可進面試群)
我讀源碼的經歷
面對 this 指向丟失,尤雨溪在 Vuex 源碼中是怎么處理的
老姚淺談:怎么學JavaScript?
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》多篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,活躍在知乎@若川,掘金@若川。致力于分享前端開發經驗,愿景:幫助5年內前端人走向前列。
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~