【若川视野 x 源码共读】第2期 | vue3 工具函数

159 阅读5分钟

前言

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

源码

EMPTY_OBJ 空对象

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

process.env.NODE_ENV是项目的环境变量配置,一般分为productiondevelopment,通过其校验当前运行环境是生产环境或开发环境。

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是我们经常用来校验数据类型的方法,相较于typeofinstanceof,也是最安全、最准确的。

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 === valueoldValue === 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
              : {})
  )
}

初次执行 _globalThisundefined,进行后续的判断。

如果存在 globalThis 就用 globalThis

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

如果存在window,就用window

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

如果都不存在,使用空对象。

下次执行就直接返回 _globalThis,不需要第二次继续判断了。

知识拓展

Object.is vs hasChanged

Object.ishasChanged方法,都是判断两个值是否是相同的值

我们从一下几个方面来看下它俩的异同点

  1. 两个值都是undefined
var val = undefined, oldVal = undefined;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
  1. 两个值都是null
var val = null, oldVal = null;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
  1. 两个值都是true或者都是false
var val = true, oldVal = true;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
  1. 两个值是由相同个数的字符按照相同的顺序组成的字符串
var val = "Hello", oldVal = "Hello";
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
  1. 两个值指向同一个对象
var obj = {};
var val = obj, oldVal = obj;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
  1. 两个值都是NaN
var val = NaN, oldVal = NaN;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false
  1. +0-0
var val = +0, oldVal = -0;
!Object.is(val, oldVal); // true
hasChanged(val, oldVal); // false

此处有分歧,需要注意

  1. NaN外的其他数字
var val = 1, oldVal = 1;
!Object.is(val, oldVal); // false
hasChanged(val, oldVal); // false