【源码共读】vue-next中的工具函数

265 阅读8分钟

前言

因为加入了若川大佬的源码共读活动,然后这也是我第一次开始尝试阅读源码并产出,可能这一篇更像是复现学习小组里文章的笔记,文末会给出各个大佬的引用。

本篇的学习目标是:

  1. 通过横向阅读小组第二期文章,复现出他人的源码阅读成果
  2. 学习 vue-next 源码 shared 模块中的实用工具函数,学习理解源码的优秀代码和思想
  3. 未来可以将优秀代码和思想运用于自己的项目中

环境准备

环境准备参考川哥的环境准备

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可以只有shareddist文件夹,加速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 空函数

很多库的源码中都有这样的定义函数,比如 jQueryunderscorelodash

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]!!常用于类型转换和值判断

  • undefinednull转换为false
  • 任意数组,对象,函数(函数是特殊的对象)都转化为true,即使是空数组,空对象
  • ''转换为false非空字符串转换为true
  • 数值正负0NaN转换为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可以找到对应原因,主要是两点:

  1. 从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
    
  2. 安全问题,防止类似于 {"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,可以看到确实 objectToStringtoTypeString是一起添加但是objectToStringexport出去只做了个判断

看起来是为了保留一个值用来判断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 === NaNfalse,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()——获取属性值时所调用的函数。

另外,数据描述符(其中属性为:enumerableconfigurablevaluewritable)与存取描述符(其中属性为enumerableconfigurableset()get())之间是有互斥关系的。在定义了set()get()之后,描述符会认为存取操作已被 定义了,其中再定义valuewritable引起错误

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 转换为连字符的函数

lowerCamelCasePascalCasse转换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
                            : {}));
};

感想

  1. 这算是我第一次看源码并产出,感谢若川大佬组织这个活动,有了小组讨论和很多前辈的解答,学习道路就会很顺畅
  2. 读源码最直观的感受就是:把自己平时接触到,但是没有仔细研究的东西捋顺了,最后会有一种融会贯通的感觉

待阅读清单

JavaScript Promise迷你书(中文版)

JavaScript 对象所有API解析

老姚:《JavaScript 正则表达式迷你书》问世了!

参考文献

初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

【第二期】- 骁 - vue3-shared

【第二期】- zgl - vue-next工具函数

【第2期】Vue3 工具函数