【源码共读·Day3】第2期 | Vue3源码中实用的基础工具函数

179 阅读7分钟

♥️♥️♥️ 李哈哈要奋发啦~冲冲冲!!!相信只要坚持学习会有所回报的!♥️♥️♥️

1. 前言

2. 工具函数

2.1 babelParserDefaultPlugins解析默认插件

/**
 * 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'
];

2.2 EMPTY_OBJ 空对象

export const EMPTY_OBJ = (process.env.NODE_ENV !== 'production') 
? Object.freeze({}) 
: {};

// export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
//   ? Object.freeze({})
//   : {}

Object.freeze()用来冻结对象,为浅冻结,只对第一层有效。被冻结的对象有以下几个特性:

  1. 不能添加新属性
  2. 不能删除已有属性
  3. 不能修改已有属性的值
  4. 不能修改原型
  5. 不能修改已有属性的可枚举性、可配置性、可写性

注意:Object.freeze()返回值就是被冻结的对象,该对象完全等于传入的对象,所以我们一般不需要接受、收返回值。也可冻结数组,key就是下标。

如何实现深冻结?

function deepFreeze(obj){
  //获取所有属性
  let propNames = Object.getOwnPropertyNames(obj);

  //遍历
  propNames.forEach(key => {
    let prop = obj[key]
    if(key instanceof Object && prop !== null) {
      deepFreeze(prop)
    }
  })
  //冻结自身
  return Object.freeze(obj)
}

应用场景

提高性能。提升的效果随着数据量的递增而递增,对于纯展示的大数据,都可以使用Object.freeze提升性能。

2.3 EMPTY_Arr 空数组

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

2.4 NOOP空函数的使用

const NOOP = () => { };

作用:

  1. 方便判断
//摘取自源码
 if (render && instance.render === NOOP) {
    instance.render = render;
  }
  1. 方便压缩

如果没有NOOP方法,那么在很多地方我们可能都要再定义一个匿名的空函数,这样的匿名函数就会导致无法被压缩,降低了代码的压缩率。

  1. 避免代码出错
  2. 提高代码的可读性

2.5 NO 永远返回false的函数

/**
 * Always return false.
 */
const NO = () => false; 
// 默认返回false的函数赋值

源码中的使用: image.png

image.png

2.6 判断字符串是不是on开头

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

2.7 isModelListener 监听器

判断字符串是不是以“onUpdate:”开头:

const isModelListener = (key) => key.startsWith('onUpdate:');
//export const isModelListener = (key: string) => key.startsWith('onUpdate:')

// 例子:
isModelListener('onUpdate:change'); // true
isModelListener('1onUpdate:change'); // false
// startsWith 是 ES6 提供的方法

2.8 extends 继承 合并

const extend = Object.assign;

// 例子:
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5};
const returnTarget = extend(target, source);
console.log(target); // { a: 1, b: 4, c: 5}
console.log(target === returnTarget); // true

image.png

2.9 remove移除数组某个元素

const remove = (arr, el) => {
  const i = arr.indexOf(el);
  if (i > -1) {
      arr.splice(i, 1);
  }
};
// export const remove = <T>(arr: T[], el: T) => {
//   const i = arr.indexOf(el)
//   if (i > -1) {
//     arr.splice(i, 1)
//   }
// }

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

2.10 hasOwn判断是否有某个属性

const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);
// export const hasOwn = (
//   val: object,
//   key: string | symbol
// ): key is keyof typeof val => hasOwnProperty.call(val, key)

2.11-2.23数据类型

const isArray = Array.isArray;
const isMap = (val) => toTypeString(val) === '[object Map]';
const isSet = (val) => toTypeString(val) === '[object Set]';
const isDate = (val) => val instanceof Date;
const isFunction = (val) => typeof val === 'function';
const isString = (val) => typeof val === 'string';
const isSymbol = (val) => typeof val === 'symbol';
const isObject = (val) => val !== null && typeof val === 'object';
const isPromise = (val) => {
  return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};
const objectToString = Object.prototype.toString;
const toTypeString = (value) => objectToString.call(value);
const toRawType = (value) => {
  // extract "RawType" from strings like "[object RawType]"
  return toTypeString(value).slice(8, -1);
};
//判断是不是纯粹的对象
const isPlainObject = (val) => toTypeString(val) === '[object Object]';

// export const isArray = Array.isArray
// export const isMap = (val: unknown): val is Map<any, any> =>
//   toTypeString(val) === '[object Map]'
// export const isSet = (val: unknown): val is Set<any> =>
//   toTypeString(val) === '[object Set]'

// export const isDate = (val: unknown): val is Date => val instanceof Date
// export const isFunction = (val: unknown): val is Function =>
//   typeof val === 'function'
// export const isString = (val: unknown): val is string => typeof val === 'string'
// export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
// export const isObject = (val: unknown): val is Record<any, any> =>
//   val !== null && typeof val === 'object'

// export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
//   return isObject(val) && isFunction(val.then) && isFunction(val.catch)
// }

// export const objectToString = Object.prototype.toString
// export const toTypeString = (value: unknown): string =>
//   objectToString.call(value)
// export const toRawType = (value: unknown): string => {
//   // extract "RawType" from strings like "[object RawType]"
//   return toTypeString(value).slice(8, -1)
// }

// export const isPlainObject = (val: unknown): val is object =>
//   toTypeString(val) === '[object Object]'

拓展: 数据类型的判断

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

传入一个以逗号分隔的字符串,生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 map 中。第二个参数是小写选项。

const isIntegerKey = (key) => isString(key) &&
    key !== 'NaN' &&
    key[0] !== '-' &&
    '' + parseInt(key, 10) === key;
    
// export const isIntegerKey = (key: unknown) =>
//   isString(key) &&
//   key !== 'NaN' &&
//   key[0] !== '-' &&
//   '' + parseInt(key, 10) === key

// 例子:
isIntegerKey('a'); // false
isIntegerKey('0'); // true
isIntegerKey('011'); // false
isIntegerKey('11'); // true
// 其中 parseInt 第二个参数是进制。
// 字符串能用数组取值的形式取值。
//  还有一个 charAt 函数,但不常用 
'abc'.charAt(0) // 'a'
// charAt 与数组形式不同的是 取不到值会返回空字符串'',数组形式取值取不到则是 undefined

2.25 makeMap && isReservedProp

/**
 * 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];
}
// 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,' +
    'onVnodeBeforeMount,onVnodeMounted,' +
    'onVnodeBeforeUpdate,onVnodeUpdated,' +
    'onVnodeBeforeUnmount,onVnodeUnmounted');

// 保留的属性
isReservedProp('key'); // true
isReservedProp('ref'); // true
isReservedProp('onVnodeBeforeMount'); // true
// ......
isReservedProp('onVnodeUnmounted'); // true

知识点

  1. makeMap返回的是函数,函数传入的参数就是要判断是否存在的key;
  2. Object.create(null)新创建的对象除了自身属性a之外,原型链上没有任何属性,也就是没有继承Object的任何东西。{}新创建的对象继承了Object自身的方法,如hasOwnPropertytoString等,在新对象上可以直接使用。详见司想君的文章分享

2.26 cacheStringFunction 缓存

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

这个高阶函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。

内部使用闭包来缓存之前的计算结果,如果再次调用时发现已经计算过,则返回之前的结果。

源码用法:

// \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() : '')); //(_, c)的第一个参数代表匹配到的字符串,第二个参数代表第一个括号匹配的字符串
});
// \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()第二个参数是函数的时候,函数的第一个参数是。参考MDN-String.prototype.replace()

image.png

3.27 hasChanged 判断是不是有变化

const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

Object.js()ES6语法,用于比较两个数是否相同,详见阮一峰的文章

2.28 invokeArrayFns 执行数组里的函数

数组中存放函数,这种写法方便统一执行多个函数。

const invokeArrayFns = (fns, arg) => {
  for (let i = 0; i < fns.length; i++) {
      fns[i](arg);
  }
};
// export const invokeArrayFns = (fns: Function[], arg?: any) => {
//   for (let i = 0; i < fns.length; i++) {
//     fns[i](arg)
//   }
// }

2.29 def 定义对象属性

const def = (obj, key, value) => {
  Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: false,
      value
  });
};
// export const def = (obj: object, key: string | symbol, value: any) => {
//   Object.defineProperty(obj, key, {
//     configurable: true,
//     enumerable: false,
//     value
//   })
// }

学习点:

Object.defineProperty(obj, prop, descripor) vue2双向绑定原理。详见MDN描述

接收3个参数:

  • obj:要定义属性的对象
  • prop:要定义或修改的属性的名称或 Symbol
  • desCriptor:要定义或修改的属性描述符

属性描述符:

  • configurable:该属性是否可被改变(删除)
  • enumerable: 该属性在for in循环中是否会被枚举
  • writable:该属性是否可写
  • value:该属性对应的值
  • get():属性的getter函数
  • set():属性的setter函数

描述符默认值:

  • 拥有布尔值的键 configurableenumerable 和 writable 的默认值都是 false
  • 属性值和函数的键 valueget 和 set 字段的默认值为 undefined

2.30 toNumber转数字

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

toNumber('111'); // 111
toNumber('a111'); // 'a111'
parseFloat('a111'); // NaN
isNaN(NaN); // true

ES6中的Number.isNaN()方法用来检查一个值是否为NaN

2.31 getGlobalThis 全局对象

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

获取全局 this 指向。

如果存在 globalThis 就用 globalThisMDN globalThis

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

Node环境下,使用global

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

下次执行就直接返回 _globalThis,不需要第二次继续判断了。这种写法值得我们学习。

总结

通过学习源码 shared 模块下的几十个工具函数,发现也没有很难,与平时自己写的比较,其代码更加规范,简洁明了,值得学习!