前言
@vue/shared 是 Vue3 内部工具函数库,其源码位于 Vue3 仓库 的 packages/shared
目录下,同时作为 Vue3 的一个单独的 NPM 包发布,其版本号与 Vue3 保持一致。
注意:本文基于
@vue/shared
版本v3.3.4
进行分析的。
目录结构
├─index.js // commonjs 入口文件
├─LICENSE
├─package.json
├─README.md
├─__tests__ // 单元测试目录
| ├─codeframe.spec.ts
| ├─escapeHtml.spec.ts
| ├─looseEqual.spec.ts
| ├─normalizeProp.spec.ts
| ├─toDisplayString.spec.ts
├─src
| ├─codeframe.ts
| ├─domAttrConfig.ts
| ├─domTagConfig.ts
| ├─escapeHtml.ts
| ├─general.ts
| ├─globalsAllowList.ts
| ├─index.ts
| ├─looseEqual.ts
| ├─makeMap.ts
| ├─normalizeProp.ts
| ├─patchFlags.ts
| ├─shapeFlags.ts
| ├─slotFlags.ts
| ├─toDisplayString.ts
| └typeUtils.ts
├─dist 打包输出目录
| ├─shared.cjs.js // commonjs 开发环境版本
| ├─shared.cjs.prod.js // commonjs 生产环境版本
| ├─shared.d.ts // TS 类型声明
| └shared.esm-bundler.js // ESM 规范的入口文件
接下来,我们逐个文件进行介绍。
package.json
{
...
"main": "index.js",
"module": "dist/shared.esm-bundler.js",
"types": "dist/shared.d.ts"
...
}
其中 main
字段指定了 commonjs
规范的入口文件,module
字段指定了 ESM 规范的入口文件,types
字段指定了 TS 类型声明的入口文件。
index.js
内容很简单,就是根据当前环境加载对应的模块。
'use strict'
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/shared.cjs.prod.js')
} else {
module.exports = require('./dist/shared.cjs.js')
}
在 commonjs
环境下使用:
const vueUtils = require('@vue/shared')
// ...
src 源代码目录
index.ts
其作用就是将其他文件导出,作为打包的入口文件。
export { makeMap } from './makeMap'
export * from './general'
export * from './patchFlags'
export * from './shapeFlags'
export * from './slotFlags'
export * from './globalsAllowList'
export * from './codeframe'
export * from './normalizeProp'
export * from './domTagConfig'
export * from './domAttrConfig'
export * from './escapeHtml'
export * from './looseEqual'
export * from './toDisplayString'
export * from './typeUtils'
makeMap.ts
makeMap(str: string, expectsLowerCase?: boolean)
:
str
是一个以逗号分隔的字符串,用于生成一个 map 字典expectsLowerCase
是一个布尔值,表示是否需要将字符串转换为小写
返回值:
- 一个匿名函数,该函数接收一个字符串作为参数,并返回一个布尔值,表示该字符串是否在给定的 map 中
这是一个典型的函数柯里化的应用,通过一个闭包将
map
保存在匿名函数的上下文中,这意味着map
已经被缓存,并且可以在后续的调用中复用,这也是函数柯里化的一个特点。
/**
* 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]
}
makeMap
函数在 Vue3 中有非常多的使用。
例子1: 识别模板中的 JS 全局函数
// packages/shared/src/globalsAllowList.ts
const GLOBALS_ALLOWED =
'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' +
'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console'
export const isGloballyAllowed = /*#__PURE__*/ makeMap(GLOBALS_ALLOWED)
/** @deprecated use `isGloballyAllowed` instead */
export const isGloballyWhitelisted = isGloballyAllowed
GLOBALS_ALLOWED
常量定义了允许在模板中使用(插值表达式,指令)的的全局变量,makeMap
函数接收这个常量作为参数,然后返回一个函数 isGloballyAllowed
,以后只要使用这个函数,传入一个变量名,就可以判断这个变量名是否在 GLOBALS_ALLOWED
中。
例子2: 区分组件和html原生标签
下面代码中的 isHTMLTag
函数会在模板解析用来标记是否是一个自定义组件。
// packages/shared/src/domTagConfig.ts
// These tag configs are shared between compiler-dom and runtime-dom, so they
// must be extracted in shared to avoid creating a dependency between the two.
import { makeMap } from './makeMap'
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element
const HTML_TAGS =
'html,body,base,head,link,meta,style,title,address,article,aside,footer,' +
'header,hgroup,h1,h2,h3,h4,h5,h6,nav,section,div,dd,dl,dt,figcaption,' +
'figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,' +
'data,dfn,em,i,kbd,mark,q,rp,rt,ruby,s,samp,small,span,strong,sub,sup,' +
'time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,' +
'canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,' +
'th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,' +
'option,output,progress,select,textarea,details,dialog,menu,' +
'summary,template,blockquote,iframe,tfoot'
/**
* Compiler only.
* Do NOT use in runtime code paths unless behind `__DEV__` flag.
*/
export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)
// ... 省略其他代码
general.ts
packages/shared/src/general.ts
这个文件中包含了一些通用的工具函数。
由于有些工具函数比较简单,所以只会介绍一些个人觉得常用的。
数据类型判断
在 ES6 中,我们判断一个值是否是数组,使用的是 Array.isArray()
,但在 ES5 中,我们需要使用Object.prototype.toString.call()
。
写一个兼容 ES5 和 ES6 的判断是否是数组的方法:
if (!Array.isArray) {
// 如果没有 Array.isArray 方法
// 则使用 Object.prototype.toString.call() 来判断
Array.isArray = function(arg: any) {
return Object.prototype.toString.call(arg) === '[object Array]'
}
}
同理,还可以使用 Object.prototype.toString.call()
来判断是否是一个 Number / String / Boolean / Set / Map / Date
等。
Object.prototype.toString.call(1) // '[object Number]'
Object.prototype.toString.call('a') // '[object String]'
Object.prototype.toString.call(true) // '[object Boolean]'
Object.prototype.toString.call(new Set()) // '[object Set]'
Object.prototype.toString.call(new Map()) // '[object Map]'
Object.prototype.toString.call(new Date()) // '[object Date]'
封装一些通用的判断值类型的工具函数:
const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string => objectToString.call(value)
export const isArray = Array.isArray || ((value: unknown): value is any[] => toTypeString(value) === '[object Array]') // 兼容 ES5 和 ES6 的判断是否是数组的方法
export const isMap = (val: unknown): val is Map<any, any> => toTypeString(val) === '[object Map]'
export const isSet = (val: unknown): val is Set<any> => toTypeString(val) === '[object Set]'
export const isDate = (val: unknown): val is Date => toTypeString(val) === '[object Date]'
export const isRegExp = (val: unknown): val is RegExp => toTypeString(val) === '[object RegExp]'
export const isFunction = (val: unknown): val is Function => typeof val === 'function'
export const isString = (val: unknown): val is string => typeof val === 'string'
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
export const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'
export const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]'
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return (
(isObject(val) || isFunction(val)) &&
isFunction((val as any).then) &&
isFunction((val as any).catch)
)
}
isOn
判断是否是事件名。
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)
remove
移除数组中的某个元素。
export const remove = <T>(arr: T[], el: T) => {
const i = arr.indexOf(el)
if (i > -1) {
arr.splice(i, 1)
}
}
hasOwn
判断对象是否包含某个属性。
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
toRawType
获取原始类型。
比如:
toRawType(1) // 'Number'
export const toRawType = (value: unknown): string => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1)
}
isReservedProp
判断是否是保留属性。
export const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,ref_for,ref_key,' +
'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted'
)
isBuiltInDirective
判断是否是内置指令。
export const isBuiltInDirective = /*#__PURE__*/ makeMap(
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
)
cacheStringFunction
缓存字符串函数。
和上面提到的
makeMap()
一样,都是函数柯里化的应用。
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 T
}
def
给对象定义不可枚举的属性。
export const def = (obj: object, key: string | symbol, value: any) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}
globalThis
全局属性 globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象,类似于全局对象(global object)。
在以前,JavaScript 没有标准的方式访问全局环境,比如浏览器、Node.js 或者 Web Worker。
在浏览器环境,可以使用 window
;在 Node.js 环境中,可以使用 global
;在 Web Worker 环境中,只能使用 self
。
// 用于缓存 全局 this 对象
let _globalThis: any
export const getGlobalThis = (): any => {
// 优先使用 globalThis,如果不存在则做兼容性判断
return (
_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {})
)
}
patchFlags.ts
patchFlag
会在编译的过程中生成的,用于标记 diff 的方式,不同的 patchFlag
代表不同的 diff 策略。
当组件更新时,diff 算法会根据 patchFlag
来减少计算量,实现靶向更新。
// packages/shared/src/patchFlags.ts
export const enum PatchFlags {
TEXT = 1,
CLASS = 1 << 1,
STYLE = 1 << 2,
PROPS = 1 << 3,
FULL_PROPS = 1 << 4,
HYDRATE_EVENTS = 1 << 5,
STABLE_FRAGMENT = 1 << 6,
KEYED_FRAGMENT = 1 << 7,
UNKEYED_FRAGMENT = 1 << 8,
NEED_PATCH = 1 << 9,
DYNAMIC_SLOTS = 1 << 10,
DEV_ROOT_FRAGMENT = 1 << 11,
HOISTED = -1,
BAIL = -2
}
/**
* dev only flag -> name mapping
*/
export const PatchFlagNames: Record<PatchFlags, string> = {
[PatchFlags.TEXT]: `TEXT`,
[PatchFlags.CLASS]: `CLASS`,
[PatchFlags.STYLE]: `STYLE`,
[PatchFlags.PROPS]: `PROPS`,
[PatchFlags.FULL_PROPS]: `FULL_PROPS`,
[PatchFlags.HYDRATE_EVENTS]: `HYDRATE_EVENTS`,
[PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`,
[PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`,
[PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`,
[PatchFlags.NEED_PATCH]: `NEED_PATCH`,
[PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`,
[PatchFlags.DEV_ROOT_FRAGMENT]: `DEV_ROOT_FRAGMENT`,
[PatchFlags.HOISTED]: `HOISTED`,
[PatchFlags.BAIL]: `BAIL`
}
shapeFlags.ts
用于标记虚拟DOM的类型。
export const enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
TEXT_CHILDREN = 1 << 3,
ARRAY_CHILDREN = 1 << 4,
SLOTS_CHILDREN = 1 << 5,
TELEPORT = 1 << 6,
SUSPENSE = 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
slotFlags.ts
看源码注释,目前只知道大概和 slot 的更新有关,不清楚具体的细节,以后知道了再补上。
// packages/shared/src/slotFlags.ts
export const enum SlotFlags {
STABLE = 1,
DYNAMIC = 2,
FORWARDED = 3
}
/**
* Dev only
*/
export const slotFlagsText = {
[SlotFlags.STABLE]: 'STABLE',
[SlotFlags.DYNAMIC]: 'DYNAMIC',
[SlotFlags.FORWARDED]: 'FORWARDED'
}
codeframe.ts
目前暂不清楚具体用途,以后知道了再补上。
normalizeProp.ts
用于规范化 prop
,包括 css
, style
和 props
。
globalsAllowList.ts & domTagConfig.ts & domAttrConfig.ts
这几个文件在前面介绍 makeMap()
时,已经了解过了,大致就是利用 makeMap()
函数柯里化的特点,就不赘述了。
escapeHtml.ts
先看看 escapeHtml()
的测试用例:
test('escapeHTML', () => {
expect(escapeHtml(`foo`)).toBe(`foo`)
expect(escapeHtml(true)).toBe(`true`)
expect(escapeHtml(false)).toBe(`false`)
expect(escapeHtml(`a && b`)).toBe(`a && b`)
expect(escapeHtml(`"foo"`)).toBe(`"foo"`)
expect(escapeHtml(`'bar'`)).toBe(`'bar'`)
expect(escapeHtml(`<div>`)).toBe(`<div>`)
})
可以知道,escapeHtml()
函数的作用就是对 html 字符串进行转义。
// packages/shared/src/escapeHtml.ts
const escapeRE = /["'&<>]/
export function escapeHtml(string: unknown) {
const str = '' + string
const match = escapeRE.exec(str)
if (!match) {
return str
}
let html = ''
let escaped: string
let index: number
let lastIndex = 0
for (index = match.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escaped = '"'
break
case 38: // &
escaped = '&'
break
case 39: // '
escaped = '''
break
case 60: // <
escaped = '<'
break
case 62: // >
escaped = '>'
break
default:
continue
}
if (lastIndex !== index) {
html += str.slice(lastIndex, index)
}
lastIndex = index + 1
html += escaped
}
return lastIndex !== index ? html + str.slice(lastIndex, index) : html
}
// https://www.w3.org/TR/html52/syntax.html#comments
const commentStripRE = /^-?>|<!--|-->|--!>|<!-$/g
// 转义 html 注释
export function escapeHtmlComment(src: string): string {
return src.replace(commentStripRE, '')
}
looseEqual.ts
用于比较两个值是否相等 (loose 宽松比较)。
关于
looseEqual()
的用法可以查看单元测试,位置在:packages/shared/__tests__/looseEqual.spec.ts
。
// packages/shared/src/looseEqual.ts
function looseCompareArrays(a: any[], b: any[]) {
if (a.length !== b.length) return false
let equal = true
for (let i = 0; equal && i < a.length; i++) {
equal = looseEqual(a[i], b[i])
}
return equal
}
export function looseEqual(a: any, b: any): boolean {
if (a === b) return true
let aValidType = isDate(a)
let bValidType = isDate(b)
if (aValidType || bValidType) {
return aValidType && bValidType ? a.getTime() === b.getTime() : false
}
aValidType = isSymbol(a)
bValidType = isSymbol(b)
if (aValidType || bValidType) {
return a === b
}
aValidType = isArray(a)
bValidType = isArray(b)
if (aValidType || bValidType) {
return aValidType && bValidType ? looseCompareArrays(a, b) : false
}
aValidType = isObject(a)
bValidType = isObject(b)
if (aValidType || bValidType) {
/* istanbul ignore if: this if will probably never be called */
if (!aValidType || !bValidType) {
return false
}
const aKeysCount = Object.keys(a).length
const bKeysCount = Object.keys(b).length
if (aKeysCount !== bKeysCount) {
return false
}
for (const key in a) {
const aHasKey = a.hasOwnProperty(key)
const bHasKey = b.hasOwnProperty(key)
if (
(aHasKey && !bHasKey) ||
(!aHasKey && bHasKey) ||
!looseEqual(a[key], b[key])
) {
return false
}
}
}
return String(a) === String(b)
}
export function looseIndexOf(arr: any[], val: any): number {
return arr.findIndex(item => looseEqual(item, val))
}
toDisplayString.ts
toDisplayString()
用来将插值表达式转换为字符串。
比如下面的模板:
<template>
<div>{{ msg }}</div>
</template>
经过编译后,会得到下面的渲染函数:
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("template", null, [
_createElementVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
在上面的渲染函数中,toDisplayString
函数会将插值表达式 _ctx.msg
转换为字符串。
下面是 toDisplayString()
的源代码:
// packages/shared/src/toDisplayString.ts
/**
* For converting {{ interpolation }} values to displayed strings.
* @private
*/
export const toDisplayString = (val: unknown): string => {
return isString(val)
? val
: val == null
? ''
: isArray(val) ||
(isObject(val) &&
(val.toString === objectToString || !isFunction(val.toString)))
? JSON.stringify(val, replacer, 2)
: String(val)
}
const replacer = (_key: string, val: any): any => {
// can't use isRef here since @vue/shared has no deps
if (val && val.__v_isRef) {
return replacer(_key, val.value)
} else if (isMap(val)) {
return {
[`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val]) => {
;(entries as any)[`${key} =>`] = val
return entries
}, {})
}
} else if (isSet(val)) {
return {
[`Set(${val.size})`]: [...val.values()]
}
} else if (isObject(val) && !isArray(val) && !isPlainObject(val)) {
return String(val)
}
return val
}
如果传递给 toDisplayString()
的值是数组或对象,则会使用 JSON.stringify()
来处理。
JSON.stringify(value[, replacer [, space]])
如果
replacer
参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理。
typeUtils.ts
TS
类型工具函数。
TS 水平还是太菜了,体操玩不转,不太看的懂,以后再补。
总结
首先说学到了什么:
- 对于工具函数库,首先看单元测试,就可以知道它的基本用法,然后再去源码中搜索它在哪里使用了,大概做了什么工作
makeMap()
函数柯里化这种用法值得借鉴- 了解了
globalThis
提供了一种标准的方式获取不同环境下的 全局this
对象 - 加深了对 Vue3 源码中工具函数的了解
不足:
- 单元测试虽然能看懂,但是却很少写,这块可以学习下
- TS 类型体操太菜,待学习
原文链接:https://juejin.cn/post/7325800661232582695 作者:夏影_July