前言
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
源码
EMPTY_OBJ 空对象
const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
? Object.freeze({})
: {}
process.env.NODE_ENV是项目的环境变量配置,一般分为production和development,通过其校验当前运行环境是生产环境或开发环境。
EMPTY_ARR 空数组
const EMPTY_ARR = (process.env.NODE_ENV !== 'production')
? Object.freeze([])
: []
NOOP 空函数
const NOOP = () => {};
NO 返回false的函数
const NO = () => false
isOn 判断key值是否on开头,且on后为非小写字母
const onRE = /^on[^a-z]/
const isOn = (key) => onRE.test(key);
isOn("onClick"); // true
isOn("onclick"); // false
isOn("on1Click"); // true
isModelListener 判断key值是否是监听器
// 校验key值是否以 onUpdate: 开头
const isModelListener = (key) => key.startsWith('onUpdate:')
isModelListener("onUpdate:change"); // true
isModelListener("onUpdate1:change"); // false
extend 数据合并
这个是我们常用到的将数据合并的方法,也是Object提供的API。
const extend = Object.assign;
remove 移除数组中的某一项
const remove = (arr, el) => {
const i = arr.indexOf(el);
if(i > -1) {
arr.splice(i, 1)
}
}
hasOwn 判断数据是否有某个属性
const { hasOwnProperty } = Object.prototype;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
hasOwn只能判断对象本身是否拥有某个属性,无法判断对象的原型是是否拥有;
function Person(name) { this.name = name; };
Person.prototype.age = 20;
var p = new Person("zhangsan");
hasOwn(p, 'name'); // true
hsaOwn(p, 'age'); // false
isArray 判断数据是否是数组
const isArray = Array.isArray;
isMap 判断数据是否是Map类型
const isMap = (val) => toTypeString(val) === '[object Map]';
isSet 判断数据是否是Set类型
const isSet = (val) => toTypeString(val) === '[object Set]';
isDate 判断数据是否是Date类型
const isDate = (val) => val instanceof Date;
isDate(new Date()); // true;
isDate({__proto__: Date.prototyp}); // false
// 建议使用
const isDate = (val) => toTypeString(val) === '[object Date]';
此处是通过instanceof进行校验判断,严格意义上来讲是有一定缺陷的,但是实际应用中,我们很少更改某个对象的原型__proto__
isFunction 判断数据是否是函数类型
const isFunction = (val) => typeof val === 'function';
isString 判断数据是否是字符串
const isString = (val) => typeof val === 'string';
isSymbol 判断数据是否是Symbol类型
const isSymbol = (val) => typeof val === 'symbol';
isObject 判断数据是否是对象类型
const isObject = (val) => val !== null && typeof val === 'object';
isPromise 判断数据是否是Promise类型
const isPromise = (val) => isObject(val) && isFunction(val.then) && isFunction(val.catch);
toTypeString 获取数据类型
const objectToString = Object.prototype.toString;
const toTypeString = (val) => objectToString.call(val);
Object.prototype.toString是我们经常用来校验数据类型的方法,相较于typeof、instanceof,也是最安全、最准确的。
toRawType 获取数据类型,截取后几位
const toRawType = (val) => toTypeString(val).slice(8, -1);
isPlainObject 判断数据是否是普通对象
const isPlainObject = (val) => toTypeString(val) === '[object Object]';
isIntegerKey 判断key值是不是数字型的字符串
const isIntegerKey = (key) => isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key;
makeMap 生成Map
const makeMap = (str, expectsLowerCase) => {
const map = Object.create(null);
const list = str.split(',');
for (let i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? val => !!map[val.toLowerCase()]
: val => !!map[val]
}
返回值是一个方法,主要是map,通过闭包形成内部数据的缓存;
但是第二个参数expectsLowerCase有疑惑,看代码是将形参val转小写,如果生成的map中存储的不是小写,那是不是就有问题了。
isReservedProp 判断属性是不是保留属性
const isReservedProp = makeMap(
',key,ref,' +
'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted'
)
isReservedProp('key'); // true
isReservedProp('ref'); // true
isReservedProp('mounted'); // false
cacheStringFunction 字符串缓存
const cacheStringFunction = (fn) => {
const cache = Object.create(null);
return (str) => {
const hit = cache[str];
return hit || (cache[str] = fn(str))
}
}
乍一看,跟上面的makeMap很像,都对数据做了缓存处理,但是不一样的是,makeMap的缓存是一次性生成的,而当前的是可以进行动态添加的。
但是我们对这个函数有点儿摸不着头脑,不晓得它可以处理什么,我们一起来看看它在Vue3中的实际应用吧。
1.中划线转驼峰命名
const camelizeRE = /-(\w)/g;
const camelize = cacheStringFunction((str) =>
str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
)
camelize("first-name"); // 'firstName'
camelize("-name"); // 'Name'
camelize("name"); // 'name'
2.驼峰转中划线命名
const hyphenateRE = /\B([A-Z])/g;
const hyphenate = cacheStringFunction(
(str) => str.replace(hyphenateRE, '-$1').toLowerCase()
)
hyphenate("firstName"); // 'first-name'
3.首字母大写
const capitalize = cacheStringFunction(
(str) => str.charAt(0).toUpperCase() + str.slice(1)
)
capitalize("name"); // 'Name';
4.转换为操作属性
const toHandlerKey = cacheStringFunction(
(str) => (str ? `on${capitalize(str)}` : ``)
)
toHandlerKey('click'); // 'onClick'
hasChanged 校验新数据是否更改
const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue)
一直没想明白最后为啥要判断value === value 和 oldValue === value,后来明白是为了判断NaN的情况,因为 Nan === NaN的结果是false。
补充:后来看若川的文章对此有补充,顺带补一下 git记录发现有人 提PR 修改为Object.is了,尤大合并了。
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
invokeArrayFns 遍历执行函数数组
const invokeArrayFns = (fns, arg) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg)
}
}
def 定义对象属性
const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
value
})
}
Object.defineProperty是Vue中用到的很重要的API,具体的可查阅MDN介绍
toNumber 转换为数字
const toNumber = (val) => {
const n = parseFloat(val);
return isNaN(n) ? val : n
}
getGlobalThis 获取全局this
let _globalThis;
const getGlobalThis = (): any => {
return (
_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {})
)
}
初次执行 _globalThis 是 undefined,进行后续的判断。
如果存在 globalThis 就用 globalThis。
如果存在self,就用self。在Web Worker中不能访问到window对象,但是我们却能通过self访问到Worker环境中的全局对象。
如果存在window,就用window。
如果存在global,就用global。Node环境下,使用global。
如果都不存在,使用空对象。
下次执行就直接返回 _globalThis,不需要第二次继续判断了。
知识拓展
Object.is vs hasChanged
Object.is和hasChanged方法,都是判断两个值是否是相同的值。
我们从一下几个方面来看下它俩的异同点
- 两个值都是
undefined
var val = undefined, oldVal = undefined;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
- 两个值都是
null
var val = null, oldVal = null;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
- 两个值都是
true或者都是false
var val = true, oldVal = true;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
- 两个值是由相同个数的字符按照相同的顺序组成的字符串
var val = "Hello", oldVal = "Hello";
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
- 两个值指向同一个对象
var obj = {};
var val = obj, oldVal = obj;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
- 两个值都是
NaN
var val = NaN, oldVal = NaN;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
+0和-0
var val = +0, oldVal = -0;
!Object.is(val, oldVal); // true
hasChanged(val, oldVal); // false
此处有分歧,需要注意
- 除
零和NaN外的其他数字
var val = 1, oldVal = 1;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false