前言
因为加入了若川大佬的源码共读活动,然后这也是我第一次开始尝试阅读源码并产出,可能这一篇更像是复现学习小组里文章的笔记,文末会给出各个大佬的引用。
本篇的学习目标是:
- 通过横向阅读小组第二期文章,复现出他人的源码阅读成果
- 学习
vue-next源码shared模块中的实用工具函数,学习理解源码的优秀代码和思想 - 未来可以将优秀代码和思想运用于自己的项目中
环境准备
环境准备参考川哥的环境准备
lxchuan12的库
为了降低我的学习难度先clone川哥的库
$ git clone https://github.com/lxchuan12/vue-next-analysis.git
$ cd vue-next-analysis/vue-next
$ yarn
$ yarn build
这时候 vue-next/packages/shared 目录下应该多了一个 dist 文件夹,里面的 shared.esm-bundler.js 就是本篇主角
tips:根据【第二期】- 骁 - vue3-shared的笔记,yarn build shared可以只有shared有dist文件夹,加速build速度
官方项目
- 官方的库可能要ts转js,先埋坑回头来填
工具函数
类别
开始前可以先阅读【第二期】- zgl - vue-next工具函数的思维导图,本篇在其分类基础上修改了部分
-
常量类型
EMPTY_OBJ空对象- ...
-
is判断
isOn- 类型判断
- ...
-
Object API
extend- ...
-
转换
camelize- ...
-
数组
-
其他
1. 常量类型
1.1 EMPTY_OBJ 空对象
Object.freeze()使得一个对象获得浅冻结,即不能向这个对象添加属性、删除已有属性、不能修改已有属性的值,不能改变已有属性的可枚举性、可配置性、可写性。MDN Object.freeze()
开发环境下,修改冻结的EMPTY_OBJ将抛出错误;生产环境下不需要错误信息
const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
? Object.freeze({})
: {};
Object.freeze()的知识点:
-
作为参数传递的对象与返回的对象都被冻结,因为两个对象全等,所以无需保存返回的对象
const obj = { name: 'xhksun', } const o = Object.freeze(obj) console.log(o === obj) // true console.log(Object.isFrozen(obj)) // true -
不能添加属性
const freezeObj = Object.freeze({}) freezeObj.name = 'xhksun' // TypeError console.log(freezeObj.name) // undefined Object.defineProperty(freezeObj, 'foo', { value: 17 }) // TypeError console.log(freezeObj.foo) // undefined -
不能修改属性
const freezeObj = Object.freeze({ foo: 1 }) freezeObj.foo = 1 // TypeError console.log(freezeObj.foo) // 1 Object.defineProperty(freezeObj, 'foo', { value: 17 }) // TypeError console.log(freezeObj.foo) // 1 -
不能删除属性
const freezeObj = Object.freeze({ foo: 1 }) delete freezeObj.foo // TypeError console.log(freezeObj) // {foo: 1} -
无法深冻结,对应要写递归
deepFreeze()const freezeObj = Object.freeze({ foo: {} }) freezeObj.foo.bar = 1 console.log(freezeObj.foo.bar) // 1
tips:Object.seal()和Object.freeze()的区别是Object.seal()可以修改对象现有属性
1.2 EMPTY_ARR 空数组
同EMPTY_OBJ,数组无法通过push添加元素
const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];
1.3 NOOP 空函数
很多库的源码中都有这样的定义函数,比如 jQuery、underscore、lodash 等
const NOOP = () => { };
好处:
-
方便判断
const instance = { render: NOOP, } console.log(instance.render === NOOP) // true instance.render = () => { console.log('render') } instance.render() // render console.log(instance === NOOP) // false -
方便压缩
1.4 NO 函数
/**
* Always return false.
*/
const NO = () => false;
2. is判断
2.1 isOn 函数
检查是否以on开头,且on后首字母不能是小写字母
const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);
正则在线工具:regex101
console.log(isOn('onChange')) // true
console.log(isOn('oncHange')) // false
console.log(isOn('onchange')) // false
console.log(isOn('on3hange')) // true
2.2 isModelListener 侦听器
检查是否以``onUpdate:`开头
const isModelListener = (key) => key.startsWith('onUpdate:');
2.3 typeof检测类型:isFunction|isString|isSymbol|isObject|isPromise
MDN typeof目前返回8种结果:"undefined","object","boolean","number","bigint","string","symbol","function"
// 判断是否是函数
const isFunction = (val) => typeof val === 'function';
// 判断是否是字符串
const isString = (val) => typeof val === 'string';
// 判断是否是Symbol
const isSymbol = (val) => typeof val === 'symbol';
// 判断是否是对象
const isObject = (val) => val !== null && typeof val === 'object';
// This stands since the beginning of JavaScript
typeof null === 'object';
// 判断是否是Promise
const isPromise = (val) => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};
2.4 Object.prototype.toString检测类型:isMap|isSet|isPlainObject
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
// 判断是否是 Map 对象
const isMap = (val) => toTypeString(val) === '[object Map]';
// 判断是否是 Set 对象
const isSet = (val) => toTypeString(val) === '[object Set]';
// 判断是否是纯粹对象
const isPlainObject = (val) => toTypeString(val) === '[object Object]';
console.log(isObject([])) // true
console.log(isPlainObject([])) // false
2.5 instanceof检测类型:isDate
// `instanceof` 操作符左边是右边的实例,原理是根据原型链向上查找的。
// 判断是否是 Date 对象
const isDate = (val) => val instanceof Date;
2.6 isArray检测数组
// 判断是否是数组
const isArray = Array.isArray;
// 使用typeof和instanceof都是不当的,typeof在纯粹对象部分举例了
// instanceof错误例子
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
2.7 isIntegerKey 判断key是正整型的字符串
一个见文知意的函数,判断是否是String,字符串不能是'NaN',确保是正数,确保是十进制的整数
const isIntegerKey = (key) => isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key;
console.log(isIntegerKey(1)) // false
console.log(isIntegerKey('NaN')) // false
console.log(isIntegerKey('-1')) // false
console.log(isIntegerKey('a')) // false
console.log(isIntegerKey('011')) // false
console.log(isIntegerKey('1.1')) // false
console.log(isIntegerKey('11')) // true
console.log(isIntegerKey('0')) // true
2.8 isReservedProp判断是否是保留属性
该函数检查是否是'','key','ref','onVnodeBeforeMount','onVnodeMounted','onVnodeBeforeUpdate','onVnodeUpdated','onVnodeBeforeUnmount','onVnodeUnmounted'这9个中的一个。
/**
* 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.
*/
function 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];
}
const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,' +
'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted');
console.log(isReservedProp('')) // true
console.log(isReservedProp('key')) // true
console.log(isReservedProp('ref')) // true
console.log(isReservedProp('onVnodeBeforeMount')) // true
console.log(isReservedProp('onVnodeMounted')) // true
console.log(isReservedProp('onVnodeBeforeUpdate')) // true
console.log(isReservedProp('onVnodeUpdated')) // true
console.log(isReservedProp('onVnodeBeforeUnmount')) // true
console.log(isReservedProp('onVnodeUnmounted')) // true
console.log(isReservedProp('notReservedProp')) // false
tips: 闭包
tips:val => !!map[val]的!!常用于类型转换和值判断
undefined和null转换为false- 任意数组,对象,函数(函数是特殊的对象)都转化为true,即使是空数组,空对象
''转换为false,非空字符串转换为true- 数值
正负0,NaN转换为false,其它转换为true,Infinity也转换为true
3. Object API
3.1 extend 合并
const extend = Object.assign;
MDN Object.assign() ,使用时候注意Object.assign()会改变第一个值:
const extend = Object.assign
const original = { a: 1, b: 2 }
const copy = extend(original, { c: 3 }) // this mutates `original` ಠ_ಠ
console.log(original === copy) // true
delete copy.a
console.log(original) // {b: 2, c: 3}
const extend = Object.assign
const original = { a: 1, b: 2 }
const copy = extend({}, original, { c: 3 })
console.log(original === copy) // false
思考:业务里对象浅拷贝的话,可以使用对象展开运算符 ... 而不是使用 Object.assign,这样可以获得一个包含确定的剩余属性的新对象
const original = { a: 1, b: 2 }
const copy = { ...original, c: 3 } // copy => { a: 1, b: 2, c: 3 }
const { a, ...noA } = copy // noA => { b: 2, c: 3 }
3.2 hasOwn判断是否自身拥有的属性
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
MDN Object.prototype.hasOwnProperty(),首先 hasOwnProperty函数区别于 in ,天生不通过原型链向上寻找:
const example = {}
example.prop = 'exists'
// `hasOwnProperty` will only return true for direct properties:
console.log(example.hasOwnProperty('prop')) // returns true
console.log(example.hasOwnProperty('toString')) // returns false
console.log(example.hasOwnProperty('hasOwnProperty')) // returns false
// The `in` operator will return true for direct or inherited properties:
console.log('prop' in example) // returns true
console.log('toString' in example) // returns true
console.log('hasOwnProperty' in example) // returns true
那么为什么要使用call ,从ESLINT中的no-prototype-builtins可以找到对应原因,主要是两点:
-
从ECMAScript 5.1开始,加入了
Object.create(在shared源码中可以看到makeMap方法,就用到了该语法MDN Object.create()),防止Object.create(null)情况下,hasOwnProperty无法判断的问题/*eslint no-prototype-builtins: "warn" */ const obj1 = Object.create(null) const obj2 = {} obj1.foo = 1 obj2.foo = 2 console.log(obj1) // {foo: 1},和obj2相比没有[[Prototype]] console.log(obj2) // {foo: 2},有[[Prototype]] try { console.log(obj1.hasOwnProperty('foo')) // throw TypeError } catch (e) { console.log(Object.prototype.hasOwnProperty.call(obj1, 'foo')) // true } console.log(obj2.hasOwnProperty('foo')) // true -
安全问题,防止类似于
{"hasOwnProperty": 1}的JSON值/*eslint no-prototype-builtins: "warn" */ const obj = { foo: 1 } obj.hasOwnProperty = 2 console.log(obj.hasOwnProperty('foo')) // TypeError console.log(Object.prototype.hasOwnProperty.call(obj, 'foo')) // true
3.3 objectToString 对象转字符串
const objectToString = Object.prototype.toString;
- 一开始疑惑为什么有了
toTypeString更安全转换函数还要保留这个?,然后在源码里找到了这样的判断语句,且只在这里使用了:
val.toString === objectToString
查看blame,可以看到确实 objectToString 和 toTypeString是一起添加但是objectToString被export出去只做了个判断
看起来是为了保留一个值用来判断val中的toString有没有改变,那么这个函数描述可能就不是用于对象转字符串,而是用于检查对象中的toString是否变更的一个常量。
3.4 toTypeString 对象转字符串
// 返回形如"[object RawType]"的结果
const toTypeString = (value) => objectToString.call(value);
3.5 toRawType 获取类型结果字符串
一个见文知意的函数,toTypeString得到的形如[object RawType]实际有效信息为RawType,这里把它截取出来并返回。
const toRawType = (value) => {
// extract "RawType" from strings like "[object RawType]"
return toTypeString$1(value).slice(8, -1);
};
3.6 hasChanged判断值变化
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
// 注意+0和-0与原来的源码结果不一样
console.log(hasChanged(+0, -0)) // true
console.log(hasChanged(NaN, NaN)) // false
原来的源码:
// compare whether a value has changed, accounting for NaN.
const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);
console.log(hasChanged(+0, -0)) // false
console.log(hasChanged(NaN, NaN)) // false
tips:NaN === NaN为false,ESLINT中也有相关规则:no-self-compare,其建议使用 typeof x === 'number' && isNaN(x) 或者 Number.isNaN()判断NaN,前者需要单独判断类型为number因为有isNaN('a')为true
MDN Object.is与===的区别在于,Object.is中+0不等于-0,而 NaN 等于自身
3.7 def 定义对象属性
单独看该函数见文知意,但是涉及到的知识点比较复杂,下面我摘录若川大佬笔记,并且更多的部分可以查看他的JavaScript 对象所有API解析
const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
});
};
摘录自若川大佬笔记的重要知识点:
在ES3中,除了一些内置属性(如:Math.PI),对象的所有的属性在任何时候都可以被修改、插入、删除。在ES5中,我们可以设置属性是否可以被改变或是被删除——在这之前,它是内置属性的特权。ES5中引入了属性描述符的概念,我们可以通过它对所定义的属性有更大的控制权。这些属性描述符(特性)包括:
value——当试图获取属性时所返回的值。
writable——该属性是否可写。
enumerable——该属性在for in循环中是否会被枚举。
configurable——该属性是否可被删除。
set()——该属性的更新操作所调用的函数。
get()——获取属性值时所调用的函数。
另外,数据描述符(其中属性为:enumerable,configurable,value,writable)与存取描述符(其中属性为enumerable,configurable,set(),get())之间是有互斥关系的。在定义了set()和get()之后,描述符会认为存取操作已被 定义了,其中再定义value和writable会引起错误。
4. 转换
cacheStringFunction 字符串缓存
和isReservedProp相类似的思路
const cacheStringFunction = (fn) => {
const cache = Object.create(null);
return ((str) => {
const hit = cache[str];
return hit || (cache[str] = fn(str));
});
};
tips: 闭包
tips:单例模式,《JavaScript 设计模式与开发实践》书中的第四章 JS单例模式
var getSingle = function(fn){ // 获取单例
var result;
return function(){
return result || (result = fn.apply(this, arguments));
}
};
4.1 camelize 转换为小驼峰的函数
kebab-case转换成lowerCamelCase
const camelizeRE = /-(\w)/g;
/**
* @private
*/
// on-click => onClick
const camelize = cacheStringFunction((str) => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''));
});
4.2 hyphenate 转换为连字符的函数
lowerCamelCase和PascalCasse转换kebab-case
// \b是单词边界,\B是非\b
const hyphenateRE = /\B([A-Z])/g;
/**
* @private
*/
// onClick,OnClick => on-click
const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase());
4.3 capitalize 转换为首字母大写的函数
首字母转换为大写
/**
* @private
*/
// onClick => OnClick
const capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1));
4.4 toHandlerKey 添加on的转换函数
转换为Handler函数
/**
* @private
*/
// click => onClick
const toHandlerKey = cacheStringFunction((str) => (str ? `on${capitalize(str)}` : ``));
4.5 toNumber 转换为数字的函数
函数将参数转换为浮点数,无法转换的情况下返回参数本身,涉及的知识点包括:MDN parseFloat(),MDN isNaN()以及 MDN Number.isNaN()
const toNumber = (val) => {
const n = parseFloat(val);
return isNaN(n) ? val : n;
};
tips:isNaN判断NaN是有例外的
console.log(isNaN(NaN)) // true
console.log(isNaN('NaN')) // true
console.log(isNaN(0)) // false
console.log(isNaN('0')) // false
console.log(isNaN('a')) // true
Number.isNaN()对字符串多做了判断
console.log(Number.isNaN(NaN)) // true
console.log(Number.isNaN('NaN')) // false
console.log(Number.isNaN(0)) // false
console.log(Number.isNaN('0')) // false
console.log(Number.isNaN('a')) // false
5. 数组
5.1 invokeArrayFns 执行数组函数
见文知意,使用统一的arg参数逐个执行函数
const invokeArrayFns = (fns, arg) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg);
}
};
5.2 remove移除数组一项
这个函数见文知意,找到给定值,并将其从数组中移除
const remove = (arr, el) => {
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 其实是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置,看下axios InterceptorManager 拦截器源码 ,思路是实际移除拦截器时,只是把拦截器置为 null 。而不是用splice移除。最后执行时为 null 的不执行,同样效果。
6. 其他
6.1 getGlobalThis 全局this
首次执行_globalThis的优先级是:globalThis>self>window>global>{}
以后就直接返回_globalThis
let _globalThis;
const getGlobalThis = () => {
return (_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}));
};
感想
- 这算是我第一次看源码并产出,感谢若川大佬组织这个活动,有了小组讨论和很多前辈的解答,学习道路就会很顺畅
- 读源码最直观的感受就是:把自己平时接触到,但是没有仔细研究的东西捋顺了,最后会有一种融会贯通的感觉