本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第2期,vue3 工具函数。
1.前期准备
1.1 环境配置
按照 Vue3 Contributing Guide 的要求
- Nodejs 版本应为 16+。
- PNPM 版本应为 7+。
1.2 源码运行
- 拉取代码
git clone https://github.com/vuejs/core.git
- 安装依赖
pnpm i
- 运行
pnpm run dev
- 编译
pnpm run build
2.函数解读
本文设计的函数均是 Vue3 shared 模块的内容,按照 packages/shared/src/index.ts 的顺序编写。
2.1 EMPTY_OBJ
- 源码
export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__ ? Object.freeze({}): {} - 解读
空对象,开发环境使用Object.freeze冻结空对象。 - 拓展
Object.freeze用来冻结对象,冻结的对象最外层属性不允许修改,但是允许更改更深层级的属性。
2.2 EMPTY_ARR
- 源码
export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : [] - 解读
开发环境返回冻结的空数组,生产环境返回数组。 - 拓展
使用Object.freeze冻结的数组使用push数组会报错,这也是为什么生产环境直接使用空数组。
3.3 NOOP
- 源码
export const NOOP = () => {} - 解读
空函数。 - 拓展
定义空函数的优点在于:1.方便使用,可使用NOOP直接赋值或做判断;2.方便压缩。
3.4 NO
- 源码
export const NO = () => false - 解读
永远返回false的函数。 - 拓展 优点:方便压缩。
3.5 isOn
- 源码
const onRE = /^on[^a-z]/ export const isOn = (key: string) => onRE.test(key) - 解读
判断字符串是不是on开头,并且on后首字母不是小写字母。 - 拓展
正则表达式:^符号在开头,表示以什么开头;对应的,$符号在结尾,表示以什么结尾;[^a-z]表示不是a到z的小写字母。
3.6 isModelListener
- 源码
export const isModelListener = (key: string) => key.startsWith('onUpdate:') - 解读
判断字符串是否以onUpdate:开头,即判断是否为v-model指令的监听器。 - 拓展
Vue3 v-model 指令实现方式:
1.定义监听prop属性。
2.定义onUpdate:事件。
3.7 extend
- 源码
export const extend = Object.assign - 解读
合并。 - 拓展
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。
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) - 解读
判断属性是不是对象本身所拥有的属性。 - 拓展
对象的属性包括 本身属性 和 原型属性。可用过Object.prototype.hasOwnProperty判断属性是否为对象本身属性。
call可用来改变this指向,此处hasOwnProperty函数内部的this均指向val。相似的方法还有apply和bind。
3.10 isArray
- 源码
export const isArray = Array.isArray - 解读
判断是否为数组。 - 拓展
判断类型的方法包括诸如typeof、instanceof,Object.prototype.toString等。不过若用来校验数组类型,typeof返回object;instanceof原理是校验是否为某个构造函数的实例,若更改对象的prototype为Array.prototype,即便该对象不是数组,也会返回true。
3.11 isMap
- 源码
export const isMap = (val: unknown): val is Map<any, any> => toTypeString(val) === '[object Map]' - 解读
判断是否为 Map 对象。 - 拓展
Map是ES6语法,类似于Object,也是 键值对 的形式。不同之处在于 Object 的键值只能是字符串,而Map的键可以是任意类型。
3.12 isSet
- 源码
export const isSet = (val: unknown): val is Set<any> => toTypeString(val) === '[object Set]' - 解读
判断是否为 Set 对象。 - 拓展
Set是ES6语法,类似于Array,不同之处在于Array的元素可以重复,Set的元素不允许重复。
3.13 isDate
- 源码
export const isDate = (val: unknown): val is Date => toTypeString(val) === '[object Date]' - 解读
判断是否为 Date 对象。
3.14 isFunction
- 源码
const isFunction = (val: unknown): val is Function => typeof val === 'function' - 解读
判断是否为函数。
3.15 isString
- 源码
export const isString = (val: unknown): val is string => typeof val === 'string' - 解读
判断是否为字符串。
3.16 isSymbol
- 源码
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol' - 解读
判断是否为 Symbol。 - 拓展
Symbol是ES6新增的一种原始数据类型O,表示独一无二。Symbol是函数,不需要配合new关键字使用。
3.17 isObject
- 源码
export const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object' - 解读
判断是否为对象。 - 拓展
判断val !== null是因为typeof null的值也是object。
3.18 isPromise
- 源码
export const isPromise = <T = any>(val: unknown): val is Promise<T> => { return isObject(val) && isFunction(val.then) && isFunction(val.catch) } - 解读
判断是否为 Promise。 - 拓展
Promise是异步编程常用方式,本身是对象类型,then参数一个函数,用作处理成功后的回调。catch参数也是一个函数,用作处理失败后的回调。
3.19 objectToString
- 源码
export const objectToString = Object.prototype.toString - 解读
对象转字符串。
3.20 toTypeString 对象转字符串
- 源码
export const toTypeString = (value: unknown): string => objectToString.call(value) - 解读
对象转字符串。 - 拓展
通过这个能获得类似[object String]的结果。其中String是根据类型变化的。
call用于改变this指向,类似的方法还有apply,bind。其中apply,call会立即执行函数,而bind不会立即执行,需要手动重新调用。
3.21 toRawType
- 源码
export const toRawType = (value: unknown): string => { // extract "RawType" from strings like "[object RawType]" return toTypeString(value).slice(8, -1) } - 解读
获取数据类型。 - 拓展
个人认为,使用Object.prototype.toString来判断类型相对来说是最准确的,虽然typeof也可以用来判断类型,但是会有不准确的时候,比如tyoeof null的结果为object。
3.22 isPlainObject
- 源码
export const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]' - 解读
判断是否为纯粹的对象。 - 拓展
与上面isObject不同地方在于,isPlainObject校验是否为纯粹的对象。isObject([])为 true,而isPlainObject([])为 false。
3.23 isIntegerKey
- 源码
export const isIntegerKey = (key: unknown) => isString(key) && key !== 'NaN' && key[0] !== '-' && '' + parseInt(key, 10) === key - 解读
判断是否为数字的字符串形式。如'0'。 - 拓展
字符串可以使用数组下标形式取值。相似的方法还有charAt,如'abc'.charAt(0)。不同之处在于,charAt取不到值返回'',而数组下标形式返回undefined。
parseInt函数的第二个参数表示进制,如parseInt(key, 10)表示转化为10进制。 在数字前面拼接空字符串(''),会将该数字转化为字符串。如'' + 10等价于'10'。
3.24 makeMap && 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,ref_for,ref_key,' +'onVnodeBeforeMount,onVnodeMounted,' +'onVnodeBeforeUpdate,onVnodeUpdated,' +'onVnodeBeforeUnmount,onVnodeUnmounted' ) - 解读
makeMap函数接受一个以 逗号(,) 分隔的字符串和 是否转小写的标识。用来生成一个map并返回校验字符串是否存在于该map的函数。
3.25 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 } - 解读
该函数用来缓存fn针对str的执行结果,单例模式。 - 拓展
使用示例:// 连字符转小驼峰 // \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成 // () 小括号是 分组捕获 const camelizeRE = /-(\w)/g; const camelize = cacheStringFunction((str) => { return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '' ) })
3.26 hasChanged
- 源码
// compare whether a value has changed, accounting for NaN. export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue) - 解读
判断值是否有变化。 - 拓展
Object.is用来比较两个值是否严格相等,与===的行为类型,不同之处在于:对于Object.is,+0不等于-0,而Nan等于自身。
3.27 invokeArrayFns
- 源码
export const invokeArrayFns = (fns: Function[], arg?: any) => { for (let i = 0; i < fns.length; i++) { fns[i](arg) } } - 解读
执行数组里的函数。 - 拓展
将函数存放到数组中,可以一次性执行多个函数。
3.28 def
- 源码
export const def = (obj: object, key: string | symbol, value: any) => { Object.defineProperty(obj, key, { configurable: true, enumerable: false, value }) } - 解读
定义对象属性。 - 拓展
Object.defineProperty用来定义对象的单个属性的 API。
Object.defineProperties(obj, props)可用来定义同时定义对象的多个属性的 API。
可以通过设置属性描述符 来控制对象属性的操作,如是否可写,是否可枚举,是否可删除等。
属性描述符包括:value——当试图获取属性时所返回的值。writable——该属性是否可写。enumerable——该属性在for in循环中是否会被枚举。configurable——该属性是否可被删除。set()——该属性的更新操作所调用的函数。get()——获取属性值时所调用的函数。 属性描述符 可分为 数据描述符(enumerable,configurable,value,writable) 和 存取描述符(enumerable,configurable,set(),get()) 之间存在 互斥关系。如设置了get()和set(),则认为存取操作已经被设置,此时若在设置writable和value会引起错误。
除了value描述符默认为undefined,其余描述符默认值均为false。
3.29 toNumber
- 源码
export const toNumber = (val: any): any => { const n = parseFloat(val) return isNaN(n) ? val : n } - 解读
转数字。 - 拓展
parseFloat函数会解析参数,并返回一个浮点数,若无法转换成浮点数,返回Nan。
isNan函数用来判断参数是否为NaN,但是并不精准,比如isNan('a111')返回true。ES6 新增了Number.isNan用来弥补该情况。
3.30 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 : {})) } - 解读
返回全局 this 执行。 - 拓展
第一次执行,_globalThis对象肯定是undefined。
若存在globalThis,返回globalThis。
否则,若存在self,返回self。在Web Worker中不能访问window对象,当时可以通过self访问Worker环境中的全局对象。
否则,若存在window对象,返回window。
否则,若存在global对象,返回global。Node环境下,使用global。
否则,返回空对象。
下次再执行该函数,直接返回_globalThis,避免重复判断。
好啦!以上便是「 聊聊 Vue3 源码中的工具函数 」的全部内容,感谢阅读。
欢迎各路大佬讨论、批评、指正,共同进步才是硬道理!