源码共读03:vue-next 工具函数源码

262 阅读4分钟

vue-next工具函数

本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。

这是源码共读的第3期,链接: juejin.cn/post/708499…

-2023.12.18 修正版本

本期任务目标:

    1. 如何学习 JavaScript 基础知识,推荐了很多学习资料
    1. 学习源码中优秀代码和思想,投入到自己的项目中
    1. Vue 3 源码 shared 模块中的几十个实用工具函数
export { makeMap } from './makeMap'
export * from './general'
export * from './patchFlags'
export * from './shapeFlags'
export * from './slotFlags'
export * from './globalsAllowList'
export * from './codeframe'
export * from './normalizeProp'
export * from './domTagConfig'
export * from './domAttrConfig'
export * from './escapeHtml'
export * from './looseEqual'
export * from './toDisplayString'
export * from './typeUtils'

babelParserDefaultPlugins babel 解析默认插件

/**
 * List of @babel/parser plugins that are used for template expression
 * transforms and SFC script transforms. By default we enable proposals slated
 * for ES2020. This will need to be updated as the spec moves forward.
 * Full list at https://babeljs.io/docs/en/next/babel-parser#plugins
 */
const babelParserDefaultPlugins = [    'bigInt',    'optionalChaining',    'nullishCoalescingOperator'];

EMPTY_OBJ 空对象

 const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
    ? Object.freeze({})
    : {};
 // Object.freeze({}) 冻结对象
 // 冻结的对象最外层无法修改
// 例子:
const EMPTY_OBJ1 = Object.freeze({});
EMPTY_OBJ1.name = "sam";
console.log(EMPTY_OBJ1.name); // undefined

const EMPTY_OBJ2 = object.freeze({ props: { name: 'sam' } });
EMPTY_OBJ2.props.fn = 'son';
EMPTY_OBJ2.props = 'props2';
console.log(EMPTY_OBJ1.fn); // son
console.log(EMPTY_OBJ1.props); // undefined
console.log(EMPTY_OBJ_2);
/**
* {
* props:
*   {
*      name: 'sam',
*      fn: 'son'
*   }
* }
*/

EMPTY_ARR 空数组

const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];

// 例子
EMPTY_ARR.push(1) // 报错,也就是生产为什么还是用[]
EMPTY_ARR.length = 3
console.log(EMPTY_ARR.length) // 0

NOOP 空函数

const NOOP = () => { };

// 很多库的源码都有这样的定义,比如jQuery、underscore、lodash等
// 使用场景:1. 方便实用 2.方便压缩
// 例子
// 1.
const instance = {
    render: NOOP
};
const dev = true;
if (dev) {
    instance.render = function(){
        console.log(render)
    }
}

// 可以用于判断
if (instance.render === NOOP) {
    console.log('i')
}
// 2.
// 方便压缩代码
// 如果是 function() {},不方便压缩代码

NO 永远返回 false 的函数

/** * Always return false. */
const NO = () => false;

// 除了压缩代码的好处外
// 一直返回false

isOn 判断字符串是不是 on 开头,并且 on 后首字母不是小写字母

const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);

// 例子
isOn('onChange') // true
isOn('onchange') // false
isOn('on3change') // true

onRE 是正则,^在开头,表示以什么开头。而在其他地方是指非。

与之相反的是,$符号结尾,表示以什么结尾。

[^a-z]是指不是a-z的小写字母

正则在线工具:regex101

isModelListener 监听器

const isModelListener = (key) => key.startsWith('onUpdate:');
// 判断字符串是不是以onUpdate:开头
// 例子
// isModelListener('onUpdate:change') // true
// isModelListener('1onUpdate:change') // false
// startsWith是es6提供的新方法

extend 继承 合并

const extend = Object.assign;

const data = { name: 'sam' };
const data2 = extend(data, { fn: son, name: 'samson' });
console.log(data); // { fn: son, name: 'samson' }
console.log(data2); // { fn: son, name: 'samson' }
console.log(data === data2); // true

remove 移除数组的一项

const remove = (arr, el) => {
    const i = arr.indexOf(el);
    if (i > -1) {
        arr.slice(i, 1);
    }
}

// 例子
const arr = [1,2,3,4]
remove(arr, 3);
console.log(arr); // [1,2,4]

slice其实是很耗性能的方法。删除数组的一项,其他元素都要移动位置。

延伸:axios InterceptorManager 拦截器源码 中,拦截器是由数组存储的。但是实际移除拦截器时,只是把拦截器设置为null,而不是用splice移除。最后执行时为null不执行。

axios代码示例

// 代码有删减
// 声明
this.handlers = [];
// 移除
if (this.handlers[id]) {
  this.handles[id] = null;
}

// 执行
if (h !== null) {
  fn(h);
}

hasOwn 是不是自己本身所拥有的属性

const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);

// 例子
// 特别注意:__proto__是浏览器实现的原生写法,后面还会用到
// 现在已经提供了好几个原生相关的API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 则是函数里 this 显示指定以为第一个参数,并执行函数。
hasOwn({__proto__: {a: 1}}, 'a'); // false 
hasOwn({a: undefined}, 'a'); // true
hasOwn({}, 'hasOwnProperty'); // false
hasOwn({}, 'toString'); // false

延伸:JavaScript 对象所有API解析

isArray 判断数组

const isArray = Array.isArray;

isArray([]); // true
const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
// 所以instanceof这个情况 不准确

isMap 判断是不是 Map 对象

const isMap = (val) => toTypeString(val) === '[object Map]';

// 例子
const map = new Map();
const o = { p: 'Hello World' };
map.set(o, 'content')
map.get(o); // content
isMap(map); // true

ES6提供了Map数据结构。它类似对象,也是键值对的集合,但键不限于字符串,各种类型的值 (包括对象)都可以当作键。

isSet 判断是不是 Set 对象

const isSet = (val) => toTypeString(val) === '[object Set]' ;

// 例子
const set = new set();
isSet(set); // true

ES6提供了一种新的数据结构Set,它类似于数组,但是成员的值都是唯一,没有重复的值。

Set本身是一个构造函数,用来生成Set数据结构。

isDate 判断是不是 Date 对象

const isDate = (val) => val instanceof Date;

// 例子
isDate(new Date()); // true
isDate({ __proto__: new Date() }); // true
// 实际上应该是是Object才对
// 所以使用 instanceof 判读数组也不准确
// 比如
({ __proto__: [] }) instanceof Array; // true
// 实际上是对象
// 所以使用数组本身提供的Array.isArray 是比较准确

isFunction 判断是不是函数

const isFunction = (val) => typeof val === 'function';
// 判断函数有很多方法,但这个是最常用也相对兼容性好的

isString 判断是不是字符串

const isString = (val) => typeof val === 'string';
 
 // 例子
 isString('')

isSymbol 判断是不是 Symbol

const isSymbol = (val) => typeof val === 'symbol';
// 例子
let s = Symbol()
typeof s;
// "symbol"
// Symbol是函数,不需要用new调用。

isObject 判断是不是对象

const isObject = (val) => val !== 'null' && typeof val === 'object';
// 例子
isObject(null); // false
isObject({ name: sam }); // true
// typeof null 是 ‘object’

isPromise 判断是不是 Promise

isPromise = (val) => {
    return isObject(val) && isFunction(val.then) && isFunction(val.catch);
}
// 判断是不是Promise对象
const p = new Promise((resolve, reject) => {
    resolve('sam')
})
isPromise(p); // true

延伸:JavaScript Promise迷你书(中文版)

objectToString 对象转字符串

onst objectToString = Object.prototype.toString;
// 对象转换字符串

toTypeString 对象转字符串

const toTypeString = (val) => objectToString.call(val);
// call是一个函数,修改执行函数this指向,第一个参数是执行函数里面的this指向
// 通过这个能够获得[object String],其中String是根据类型变化的。

toRawType 对象转字符串 截取后几位

const toRawType = (value) => {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
}
// 截取到
toRawType(''); // string

可以截取到String、Array这些类型

是JS判断数据类型非常重要的知识点

JS判断类型也可以使用typeof,但是不准确,而且能识别出的不多。

typeof返回值目前有8种:
string
object
null
undefined
symbol
function
bigint
boolean

isPlainObject 判断是不是纯粹的对象

const objectToString = Object.prototype.toString;
cobst toTypeString = (value) => objectToString.call(value);
const isPlainObject = (value) => toTypeString(value) === '[object object]';
// 前文已经有isObject
// isObject([])也是true,因为typeof [] 为true
// 而isPlainObject([]) 应该为false
const Ctor = function() {
    this.name = '我是构造函数';
}
isPlainObject({}); // true
isPlainObject(Ctro); //true

isIntegerKey 判断是不是数字型的字符串key值

cnst isIntegerkey = (key) => isString(key) &&
    key !== 'NaN' &&
    key[0] !== '-' &&
    '' + parseInt(key, 10) === key;
// 例子
isInteger('a'); // false
isInteger('0'); // true
isInteger('011'); // false
isInteger('11'); // true
// 其中parseInt 第二个参数是进制
// 字符串可以数组形式取值
// 还有一个charAt 函数,但不常用。
'abc'.charAt(0) // 'a'
// charAt与数组形式不同的是,取值取不到的时候返回'',数组形式取值取不到则是undefined

makeMap && isReservedProp

传入一个以逗号分隔的字符串,生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 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. 
*/
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 = makeMap(
    'key,ref' +
    '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

cacheStringFunction 缓存

const cacheStringFunction = (fn) => {
    const cache = Object.create(null);
    return ((str) => {
        const hit = cache[str];
        return hit || (cache[str] = fn(str))
    })
}

这个函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。《JavaScript 设计模式与开发实践》书中的第四章 JS单例模式也是类似的实现。

var getSingle = function(fn){
 // 获取单例
 var result;
 return function(){
     return result || (result = fn.apply(this, arguments));
 }
};

以下是一些正则:

// \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)}` : ''));

hasChanged 判断是不是有变化

// 
const hasChange = (value, oldValue) => !Object.is(value, oldValue);
// Object.is API
// Object.is(value1, value2); (ES6)
// 该方法用来比较两个值是否严格相等。它与"==="的行为基本一致。
// 不同之处有两个:1.`+0` 不等于 `-0`;2.`NaN`等于自身。

// ES5实现Object.js
Object.definePrototype(Object, 'is', {
    value: function() {x, y} {
        if (x === y) {
            return x !== 0 || 1 / x === 1 / y;
        }
        return x !== x && y !== y;
    },
    configurable: true,
    enumerable: true,
    writable: true
})
// 之前的源码
// compare whether a value has changed, accounting for NaN.
const hasChange = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);
// 例子
// 认为NaN是不变的
hasChange(NaN, NaN); // false
// Object.is 认为  NaN 和 本身 相比 是同一个值
Object.is(NaN, NaN); // true
hasChange(+0, -0); // false
// Obect.is 认为 +0 和 -0 不是同一个值
Object.is(+0, -0); // false

//场景
// watch 监测数据是不是变化了

invokeArrayFns 执行数组里的函数

const invokeArrayFns = (fns, arg) => {
    for(let i = 0; i < fns.length; i++) {
        fns[i](arg);
    }
}
// 例子
const arr = [
    function (val) {console.log('name: ' + val)},
    function (val) {console.log('my name is' + val)}
];
invokeArrayFns(arr, 'sam');
// 数组中存放函数,函数其实也算是数据,这种写法方便统一执行多个函数。

def 定义对象属性

const def = (obj, key, value) => {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: true,
        value
    })
}

Object.defineProperty算是一个比较重要的API。还有一个定义多个属性的API:

Object.defineProPerties(obj, props)(ES5)

  在ES3中,除了一些内置属性(如Math.PI),对象中所有的属性在任何时候都可以被修改、插入、删除。在ES5中,我们可以设置属性是否可以被修改或删除。ES5中引入了属性描述符的概念,我们可以通过它对所定义的属性有更大的控制权。这些属性描述符(特性)包括:

  • Value
  • Configurable
  • Enumerable
  • Writable
  • get()
  • set()

延伸:更多API可以参考这篇文章JavaScript 对象所有API解析

toNumber 转数字

const toNumber = (val) => {
 const n = ParseFloat(val);
 return isNaN(n) ? val : n;
}

getGlobalThis 全局对象

let _globalThis;
const getGlobalThis = () => {
    return (_globalThis ||
        (_globalThis =
            typeof globalThis !== 'undefined'
                ? globalThis
                : typeof self !== 'undeinfed'
                    ? self
                    : typeof window !== 'undefined'
                        ? window
                        : typeof global !== 'undefined'
                            ? global
                            : {}));
}

获取全局 this 指向。

初次执行肯定是 _globalThisundefined。所以会执行后面的赋值语句。

如果存在 globalThis 就用 globalThis

如果存在self,就用self。在 Web Worker 中不能访问到 window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象。

如果存在window,就用window

如果存在global,就用globalNode环境下,使用global

如果都不存在,使用空对象。可能是微信小程序环境下。

延伸:MDN globalThis

推荐:

JavaScript字符串所有API全解密

【深度长文】JavaScript数组所有API全解密

正则表达式前端使用手册

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

老姚浅谈:怎么学JavaScript?

JavaScript 对象所有API解析 lxchuan12.gitee.io/js-object-a…

MDN JavaScript

《JavaScript高级程序设计》第4版

《JavaScript 权威指南》第7版

《JavaScript面向对象编程2》 面向对象讲的很详细。

阮一峰老师:《ES6 入门教程》

《现代 JavaScript 教程》

《你不知道的JavaScript》上中卷

《JavaScript 设计模式与开发实践》

此文章为12月Day1源码共读,以梦为马,抓住2023年的尾巴💪💪