前言
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第2期,链接:# 【若川视野 x 源码共读】第2期 | vue3 工具函数。
1. EMPTY_OBJ空对象
const EMPTY_OBJ: { readonly [key: string]: any } = (process.env.NODE_ENV !== 'production')
? Object.freeze({})
: {}
知识点:
{ readonly [key: string]: any }
表示EMPTY_OBJ是一个对象并且属性是只读的,其索引签名是字符串形式的,值可以是任意值。
const obj:{ readonly [key: string]: any } = {
name:'acwrong',
age:18
}
// obj.name = 'hello' error
console.log(obj.name)
console.log(obj.age)
Object.freeze()
:作用是冻结对象,使其最外层属性不能被更改
const EMPTY_OBJ_1 = Object.freeze({});
EMPTY_OBJ_1.name = 'acwrong';
console.log(EMPTY_OBJ_1.name); // undefined
const EMPTY_OBJ_2 = Object.freeze({ props: { name: 'acwrong' } });
EMPTY_OBJ_2.props.name = 'ACWRONG';
EMPTY_OBJ_2.props2 = 'props2';
console.log(EMPTY_OBJ_2.props.name); // 'ACWRONG'
console.log(EMPTY_OBJ_2.props2); // undefined
console.log(EMPTY_OBJ_2);
- Node.js中的
process
对象:process对象 — JavaScript 标准参考教程(alpha)
2. EMPTY_ARR 空数组
const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];
3. NOOP空函数
const NOOP = () => {}
使用场景:
- 可以用于判断
- 方便代码压缩
4. 永远返回false的函数
const NO = () => false
//可以用做全局变量来调用,也方便进行代码压缩
5. isOn 判断字符串是不是 on 开头,并且 on 后首字母不是小写字母
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)
^
表示以….为开头,^on
表示以on开头- 而当^放在方括号[]中就表示反向匹配,如
[^a-z]
表示不在a-z内的字符
6.isModelListener监听器
判断字符串是不是以onUpdate:
为开头,重点在于掌握startsWith
方法的使用。
const isModelListener = (key:string)=>key.startsWith('onUpdate:')
7. extent浅拷贝合并对象
const extend = Object.assign
const data = {name:'acwrong'}
const data2 = {age:18}
const newData = extend(data,data2) //{name:'acwrong',age:18}
console.log(data === newData) //true
8. 封装移除数组中的值为某个数的函数
const remove = <T>(arr:T[],el:T) =>{
const i = arr.indexOf(el)
if(i > -1){
arr.splice(i,1)
}
}
// 例子:
const arr = [1, 2, 3];
remove(arr, 3);
console.log(arr); // [1, 2]
splice
是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置。
9.hasOwn判断自身所拥有的属性
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol
): key is keyof typeof val => hasOwnProperty.call(val, key)
key is keyof typeof val
的意思:TypeScript – 简单易懂的 keyof typeof 分析 – 掘金- 为什么不直接使用
obj.hasOwnProperty(key)
,而要大费周章的自己封装一个呢?这是为了防止开发者意外的修改obj.hasOwnProperty方法,而通过使用hasOwnProperty.call(val, key)
的形式,我们可以确保始终调用原始的hasOwnProperty
方法,而不受对象自身可能存在的同名属性的影响,这种写法确保了hasOwn
函数能够准确地调用原始的hasOwnProperty
方法,而不受潜在的同名属性的干扰,以确保准确性和可靠性。
10. isArray
const isArray = Array.isArray;
isArray([]); //true
const fakerArr = {__proto__:Array.prototype,length:0}
isArray(fakerArr) // false
- isArray传入一个类数组会返回false
11. isMap判断是不是Map对象
const isMap = (val:unknown):val is Map<any,any> => Object.prototype.toString.call(val) === "[object Map]"
const isMap = (val: unknown): val is Map<any, any> =>
toTypeString(val) === '[object Map]'
const objectToString = Object.prototype.toString
const toTypeString = (value: unknown): string =>
objectToString.call(value)
思考面试题:Map和Object的区别?Map – JavaScript | MDN
Map的键可以是各种类型的值,而Object的键只能是字符串或者symbol。
12. isSet判断是不是Set对象
const isSet = (val: unknown): val is Set<any> =>
toTypeString(val) === '[object Set]'
13. isDate判断是不是Date对象
const isDate = (val: unknown): val is Date => val instanceof Date
14. isFunction判断是不是函数
const isFunction = (val: unknown): val is Function =>
typeof val === 'function'
15. isString判断是不是字符串
const isString = (val: unknown): val is string => typeof val === 'string'
16. isSymbol判断是不是Symbol
const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
17. isObject判断是不是对象
const isObject = (val: unknown): val is Record<any, any> =>
val !== null && typeof val === 'object'
Record的概念:Documentation – Utility Types
?18. isPromise判断是不是Promise
const isPromise = (val) => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};
或者使用:
if (Promise.resolve(obj) === obj) {
// 对象是 Promise 对象或类 Promise 的对象
} else {
// 对象不是 Promise 对象或类 Promise 的对象
}
Promise.resolve() 方法将传入的参数转换为一个 Promise 对象。如果传入的对象本身就是 Promise 对象,Promise.resolve() 将返回原始的 Promise 对象。因此,通过比较 Promise.resolve(obj) 和 obj,我们可以判断 obj 是否是 Promise 对象。
19. objectTostring+toTypeString对象转字符串
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
20. toRawType 对象转字符串方便类型判断
const toRawType = (value: unknown): string => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString(value).slice(8, -1)
}
使用该函数,我们可以判断Array等数据类型。
function isArray(value: unknown): boolean {
return toRawType(value) === 'Array';
}
// 示例用法
const value1 = [1, 2, 3];
console.log(isArray(value1)); // true
const value2 = 'Hello';
console.log(isArray(value2)); // false
21.isPlainObject判断是不是存粹的对象
const isPlainObject = (val: unknown): val is object =>
toTypeString(val) === '[object Object]'
22. makeMap&&isReservedProp
- makeMap方法传入一个带有逗号的字符串,利用逗号将其分成一个个子字符串
- 使用Object.create(null)创建一个对象,然后循环地将每个子字符串作为一个对象的键,且其值全部为true;
- 返回一个函数,该函数用来检测接收的参数是否是对象中的键。巧妙地利用闭包来延长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]
}
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'
)
// 保留的属性
isReservedProp('key'); // true
isReservedProp('ref'); // true
isReservedProp('onVnodeBeforeMount'); // true
isReservedProp('onVnodeMounted'); // true
isReservedProp('onVnodeBeforeUpdate'); // true
isReservedProp('onVnodeUpdated'); // true
isReservedProp('onVnodeBeforeUnmount'); // true
isReservedProp('onVnodeUnmounted'); // true
shared/src/general.ts下的isBuiltInDirective也用到了makeMap方法:
const isBuiltInDirective = /*#__PURE__*/ makeMap(
'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
)
23. 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
}
在源码中的后续几个方法中使用到了:
// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成
// () 小括号是 分组捕获
const camelizeRE = /-(\w)/g;
/**
* @private
*/
// 连字符 - 转驼峰 on-click => onClick
const camelize = cacheStringFunction((str) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
// \B 是指 非 \b 单词边界。
const hyphenateRE = /\B([A-Z])/g;
/**
* @private
*/
const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase());
// 举例:onClick => on-click
const hyphenateResult = hyphenate('onClick');
console.log('hyphenateResult', hyphenateResult); // 'on-click'
/**
* @private
*/
// 首字母转大写
const capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));
/**
* @private
*/
// click => onClick
const toHandlerKey = cacheStringFunction((str) => (str ? `on${capitalize(str)}` : ``));
const result = toHandlerKey('click');
console.log(result, 'result'); // 'onClick'
关于这边的replace
方法,这边有介绍String.prototype.replace() – JavaScript | MDN
24. hasChanged判断是不是有变化
const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
Object.is() 静态方法确定两个值是否为相同值:Object.is() – JavaScript | MDN
25. invokeArrayFns执行数组里的函数
const invokeArrayFns = (fns: Function[], arg?: any) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg)
}
}
// 例子:这边copy若川的例子
const arr = [
function(val){
console.log(val + '的博客地址是:https://lxchuan12.gitee.io');
},
function(val){
console.log('百度搜索 若川 可以找到' + val);
},
function(val){
console.log('微信搜索 若川视野 可以找到关注' + val);
},
]
invokeArrayFns(arr, '我');
当我们要一批函数时(可能会使用相同的参数),可以使用这种方法比较方便。
26. toNumber转数字
const looseToNumber = (val: any): any => {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
toNumber('111'); // 111
toNumber('a111'); // 'a111'
parseFloat('a111'); // NaN
isNaN(NaN); // true
其实 isNaN
本意是判断是不是 NaN
值,但是不准确的。 比如:isNaN('a')
为 true。 所以 ES6 有了 Number.isNaN
这个判断方法,为了弥补这一个API。
Number.isNaN('a') // false
Number.isNaN(NaN); // true
这边重点关注isNaN和Number.isNaN是不一样的就可以了。
27. globalThis全局对象
let _globalThis: any
const getGlobalThis = (): any => {
return (
_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {})
)
}
在不同的环境下,全局this的不一样的。
全局属性 globalThis 包含全局的 this 值。globalThis – JavaScript | MDN
通过上面的代码,我们就可以在不同的环境下使用不同的getGlobalThis,并且只需要进行一次判断。
总结
通过学习本期源码,学习到了很多实用的函数和编程思想,感受到了闭包的强大之处(如第23个函数做缓存真的是太妙)。还趁此学习一些typescript的相关内容,了解之前没有接触过的API。
接下去会继续阅读源码,继续进步~