本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 前言
刚好在写完上一期的源码阅读笔记时,若川大大建议现在可以不用学习Flow类型检查,可以学习下TypeScript,最近公司也在使用TypeScript,正好借着阅读Vue3源码的机会学习下优秀的代码,以及强化自己的知识;
最近在活动中也学习到了很多,在不停的意识到强大的人不是没有理由的,也慢慢学会了保持谦逊,反思着过往那个无知的自我(感觉曾经的我真印证了罗翔老师的一句话,‘越是知识匮乏的人,越有一种莫名其妙的勇气和自豪感’),当不断学习之后才发现曾经的我有多么的无知与可笑;但好在改变在任何时候都不会晚,虽然无法赶上他人的步伐,但坚持不断超过昨天的我,终有一日也能耀眼,加油!!!
2. 测试环境搭建
因为TypeScript是无法直接在浏览器控制台执行的,而自己又是边学边看,所以免不了要进行各种实验,应此在VsCode搭建了一个本地TypeScript运行环境,简单写下搭建步骤(方法来自于网络)
2.1 使用npm安装依赖包
npm install -g ts-node
npm install -g typescript
- 这里我是全局安装的,只安装在当前项目目录下应该也是可以的,但我没有尝试
2.2 安装配置Code Runner插件
-
在
VSCode插件商店中找到Code Runner,安装插件 -
安装完成后设置插件,选中
Run In Terminal选项
2.3 运行
- 直接运行需要执行的
ts文件,结果会在终端输出
3. 工具函数
在写下本文时Vue最新版本为3.2.33,文章中所有函数以该版本中工具函数为准,源码位置vuejs/core/packages/shared/src/index.ts
EMPTY_OBJ
空对象
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
? Object.freeze({})
: {}
-
定义了一个空对象,
TypeScript中使用:定义类型 -
定义了空对象类型
{ readonly [key: string]: any } -
readonly在TypeScript中用于修饰只读类型,详细说明可参见官网; -
[key: string]: any定义了对象为key-value对并且key的类型为string,value类型为any任意类型,虽然TypeScript中提供了any用于接受任意类型的值,但是不要滥用,之前我就被公司大佬吐槽了,老是喜欢定义any,仿佛就是在用JavaScript完全失去了Type的意义; -
__DEV__猜测应该是一个当前运行环境的判断 -
Object.freeze({})冻结对象,使对象不可被修改;但是注意的是在返回{}时,因为定义了readonly所以在用EMPTY_OBJ["name"]="xinxinzi"添加属性时会报错,但用官网的编辑器运行时还是添加进去属性了,不知道是不是我用法的问题const EMPTY_OBJ: { readonly [key: string]: any } = {}; EMPTY_OBJ["name"]="xinxinzi"; Object.defineProperty(EMPTY_OBJ, "job", {value:"coder",enumerable:true}); console.log(EMPTY_OBJ); // { "name": "xinxinzi", "job": "coder" }
EMPTY_ARR
空数组
export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []
- 定义了一个空数组
NOOP
空函数
export const NOOP = () => {}
- 定义了一个空函数
NO
永假函数
/**
* Always return false.
*/
export const NO = () => false
- 函数永远返回
false
isOn
判断字符串是否以on开始
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)
/^on[^a-z]/匹配以on开头且之后的第一个字符不为小写字母,方括号中的^表示不接受方括号中的字符集合- 另外推荐两个个人觉得很好用的正则学习网站各种正则表达式测试包含几乎所有的基础匹配符,以及可视化正则表达式
isModelListener
是否为模型监听
export const isModelListener = (key: string) => key.startsWith('onUpdate:')
- 判断字符串是否以
'onUpdate:'开头 startWith(),判断字符串是否以给定子字符串开头,返回boolean,更多用法可见MDN
extend
定义继承
export const extend = Object.assign
- 使用
Object.assign将源对象的所有可枚举属性的值分配到目标对象中,返回目标对象 assign更多说明MDN
remove
删除任意类型数组中的某一项
export const remove = <T>(arr: T[], el: T) => {
const i = arr.indexOf(el)
if (i > -1) {
arr.splice(i, 1)
}
}
<T>为TypeScript中泛型的定义indexOf()查找到数组中对应的项splice(i, 1)删除数组中第i项
hasOwnProperty
自身属性判断函数
const hasOwnProperty = Object.prototype.hasOwnProperty
- 使用
hasOwnProperty判断对象自身是否拥有属性
hasOwn
是否包含属性
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
-
val类型为object-对象 -
key类型为string-字符串或symbol- 唯一符号 -
|可用于TypeScript中多个类型的申明 -
is可以用来判断一个变量是否属于某个接口或类型interface Person { name: string; job: string; } const isPerson = (value: any): value is Person => { return ( typeof (value as Person)["name"] !== "undefined" && typeof (value as Person)["job"] !== "undefined" ); }; console.log(isPerson({ name: "xinxinzi", job: "coder" })); // true console.log(isPerson({ name: "xinxinzi" })); // false console.log(isPerson({ job: "coder" })); // false注意的是,
is并没有提供判断的功能,只是用于声明了函数的返回值是表示value是否为Person的布尔值,具体判断逻辑是在后面的的函数体中实现的,因此也会有如下这样的情况:const isPerson = (value: any): value is Person => true; console.log(isPerson({ name: "xinxinzi", job: "coder" })); // true console.log(isPerson({ name: "xinxinzi" })); // true console.log(isPerson({ job: "coder" })); // true -
keyof用于获取某类型的所有键,返回联合类型interface Person { name: string; job: string; } type Key = keyof Person; // "name" | "job"声明一个类型为
Key的变量,其变量的值只可以为'name'或'job' -
typeof在JavaScript中用于获取类型,但TypeScript中添加了一个新的用法用于直接引用变量或者属性的类型// 获取变量的类型 let n = "xinxinzi"; let s: typeof n; // s的类型为string // 获取到函数的类型 function f() { return false; } type T = ReturnType<typeof f>; // 类型T为boolean,<>表示泛型所以只能接受类型,但f是函数名(函数的值),所以需要使用typeof取到值的类型代码中的
ReturnType<T>用于获取取函数类型的返回值类型的方法,有一点点拗口,代码看起来应该比较简洁type T = ReturnType<() => string>; // 等同于 type T = string; -
所以此处
key is keyof typeof val的逻辑是首先typeof获取到传入对象val的类型,然后keyof获取到对象类型中的所有Key,最后用is表示key是否是对象中的Key,如经过一番扩写,下面代码可以用于判断对象key是否属于val:Personinterface Person { name: string; job: string; } let val: Person; type V = typeof val; type Key = keyof V; const has = (key: string): key is Key => val.hasOwnProperty(key);
isArray
定义数组判断函数
export const isArray = Array.isArray
- 使用
Array原生方法isArray
isMap
判断是否为Map
export const isMap = (val: unknown): val is Map<any, any> =>
toTypeString(val) === '[object Map]'
Map<any, any>为TypeScript中Map对象的定义,表示Map中的key和value都是any类型的数据toTypeString为工具中定义的方法,用来获取对象类型的字符串表示- 这里有一个不太理解的地方,
toTypeString在它的函数体声明前就被使用了,但是按理它不是使用function声明的应该不会有变量提升,有点不太理解??
isSet
判断是否为Set
export const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]'
Set<any>为TypeScript中Set对象的定义,且Set中value为any类型的数据- 仍然还是使用
toTypeString获取到对象类型的字符串表示
isDate
判断是否为Date日期类型
export const isDate = (val: unknown): val is Date => val instanceof Date
- 使用
instanceof运算符,判断值是否为Date或者继承自Date
isFunction
判断是否为Function函数类型
export const isFunction = (val: unknown): val is Function =>
typeof val === 'function'
- 使用
typeof运算符判断对象是否为function类型
isString
判断是否为String字符串类型
export const isString = (val: unknown): val is string => typeof val === 'string'
- 仍然是使用
typeof进行类型判断
isSymbol
判断是否为Symbol类型
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
typeof类型判断
isObject
判断是否为Object类型
export const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
Record<>在TypeScript中用于构建一个对象类型,详情及更多其它关键字可产看官网- 判断不为空,且类型为
object
isPromise
判断是否为Promise对象
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
Promise<T>为TypeScript中promise对象的定义- 和
Vue2中判断方式基本相同,判断本身是对象,对象包含.then和.catch两个方法
toTypeString
获取对象类型的字符串表示
export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>
objectToString.call(value)
- 定义
objectToString为Object.prototype.toString,用于获取对象类型的字符串表示 toTypeString返回一个unknown未知类型的对象的类型的字符串表示unknown表示未知类型,官网说明
toRawType
获取对象的类型字符串
export const toRawType = (value: unknown): string => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1)
}
- 和
Vue2中该方法的功能相同,都是返回原始类型
isPlainObject
判断是否为纯粹的对象
export const isPlainObject = (val: unknown): val is object =>
toTypeString(val) === '[object Object]'
- 判断对象类型的字符串表示是否为
'[object Object]'
isIntegerKey
判断是否为整数形式的Key
export const isIntegerKey = (key: unknown) =>
isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key
key是字符串,并且key可以转化为数字,并且key转化为数字时非负,并且key的每个字符都为十进制的数字且不包含前导0
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'
)
-
makeMap创建一个Map对象,vue2中也存在过,返回一个方法,用于查找key是否在Map中,这里函数的声明并没有在index中,而是同目录下的makeMap.ts文件中,我也给它扒了下来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] }函数功能和
vue2中并无二致
isBuiltInDirective
是否为内置的指令
export const isBuiltInDirective = /*#__PURE__*/ makeMap(
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
)
- 仍旧是使用
makeMap创建对应的判断方法
cacheStringFunction
创建函数类型的缓存,Vue2工具函数中cached的TypeScript实现
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
}
<T extends (str: string) => string>定义泛型T,T继承自(str: string) => string方法cacheStringFunction参数接收一个T类型并且返回值类型也是Tconst cache: Record<string, string> = Object.create(null)创建一个缓存对象- 返回一个参数为
string,返回值为cache中对应str项的方法
camelize
将连字符分隔字符串转化为驼峰格式
const camelizeRE = /-(\w)/g
/**
* @private
*/
export const camelize = cacheStringFunction((str: string): string => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})
-
/-(\w)/g,全局匹配-[a-zA-Z0-9_]格式字符串,使用可视化正则生成效果 -
通过
replace()替换匹配项
hyphenate
将驼峰命名转化为连字符分隔的字符串
const hyphenateRE = /\B([A-Z])/g
/**
* @private
*/
export const hyphenate = cacheStringFunction((str: string) =>
str.replace(hyphenateRE, '-$1').toLowerCase()
)
-
/\B([A-Z])/g,匹配非单词边界的大写字符 -
'-$1'参数是replace()方法中的特殊参数,在第一个参数为RegExp正则对象时有效,表示第一个括号匹配的字符串,最大支持100
capitalize
首字母大写
export const capitalize = cacheStringFunction(
(str: string) => str.charAt(0).toUpperCase() + str.slice(1)
)
toHandlerKey
转化为Handler形式的字符串
export const toHandlerKey = cacheStringFunction((str: string) =>
str ? `on${capitalize(str)}` : ``
)
- 转化为
onA形式的字符串 on${capitalize(str)}为ES6中新增语法,用于方便的拼接字符串,等同于'on' + capitalize(str)
hasChanged
判断是否有变化
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
-
传入新旧两个值,通过
is()方法判断是否为同一值 -
is()的判断相等的依据如下,引用自MDN -
Object.is()和===的区别Object.is(+0, -0); // false +0 === -0; // true Object.is(Number.NaN, NaN); // true Number.NaN === NaN; // false
invokeArrayFns
调用方法数组
export const invokeArrayFns = (fns: Function[], arg?: any) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg)
}
}
-
fns: Function[]传入一个方法数组 -
arg?: any参数,任意类型,?是TypeScript中一个比较有意思的运算符,有以下几种用法:-
代表可选参数,也就是上文代码中的用法,会自动为参数加上一个
undefined类型,在传递参数时可以不传 -
代表可选属性,常用在类型,接口等的定义中,如下
interface Person { name: string; job?: string; }这里的
job就是可选参数,在实现接口时可以不要该属性,人可以没有工作(🙃bushi) -
用于判断对象是否为
null或undefined,如下function getData(data: any){ let name = data?.name }data.name这样的写法在data为undefined时就会报错,但data?.name可以避免这样的情况 -
??类似于||表示取或,但是有一点区别,如下console.log(0 || 5); console.log(0 ?? 5);当值为
0时会被||排除,而??不会
-
def
为对象定义一个只可以被编辑描述符和删除的属性
export const def = (obj: object, key: string | symbol, value: any) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}
obj: object需要定义属性的对象key: string | symbol定义的key为string或symbolvalue: any属性的值为任意类型- 使用
Object.defineProperty()进行定义,详细参见MDN
toNumber
转化为Number
export const toNumber = (val: any): any => {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
- 将传入的参数值转化为通过
parseFloat()转化为数字,若不可转换,返回参数本身 parseFloat()函数更多用法可参见MDN
getGlobalThis
获取全局的this指向
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,不为空则不进行赋值,直接返回_globalThis - 赋值时,存在
typeof globalThis !== 'undefined',存在则取global的值 - 否则判断是否存在
typeof self !== 'undefined',存在则取self的值 - 否则判断是否存在
typeof window !== 'undefined',存在则取window - 否则判断是否存在
typeof global !== 'undefined', 存在则取global - 否则使用空对象
{}
4. 总结
通过对Vue3中工具函数的学习,同时学习TypeScript,目前也算是勉强入门了吧,前方道路还很漫长,但唯有不断前行而已。加油,诸君!!