一、前言

本篇文章是笔者从事两年前端以来的第一篇源码系列的文章, 平时在搭建项目的基础架构时,必不可少的会添加 utils 工具函数, 那 Vue 源码是怎么写工具函数的呢? 带着疑问随笔者一起走进 Vue 源码的第一课工具函数

二、环境准备

为了降低难度,我们可以选择按照贡献指南中方法打包把 ts 转成了 js

node -v
#v16.9.0

# 克隆项目
git clone https://github.com/vuejs/vue-next.git
cd vue-next

# 全局安装 yarn
npm install --global yarn
yarn install

# 打包构建代码
yarn build

可以得到 vue-next/packages/shared/dist/shared.esm-bundler.js

或者直接看 vue-next/packages/shared/src/index.ts

三、工具函数

1. 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",
];

这里是几个默认插件,感兴趣可以查看

2. EMPTY_OBJ 空对象
const EMPTY_OBJ =
  process.env.NODE_ENV !== "production" ? Object.freeze({}) : {};
// Object.freeze() 作用是 冻结对象,不可增改删(包括原型),可查.
// 冻结的对象最外层无法修改。
// 例子:
const EMPTY_OBJ_1 = Object.freeze({});
EMPTY_OBJ_1.name = "工具函数";
console.log(EMPTY_OBJ_1.name); // undefined

const EMPTY_OBJ_2 = Object.freeze({ info: { name: "工具函数" } });
EMPTY_OBJ_2.info.nick = "函数";
EMPTY_OBJ_2.obj = "props2";
console.log(EMPTY_OBJ_2.info.name); // '工具函数'
console.log(EMPTY_OBJ_2.obj); // undefined
console.log(EMPTY_OBJ_2);

/**
 * {
 *  info: { name: "工具函数", nick: "函数" }
 * }
 */

process.env.NODE_ENVnode 项目中的一个环境变量,一般定义为:developmentproduction。根据环境写代码。比如开发环境,有报错等信息,生产环境则不需要这些报错警告。

3. 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
4. NOOP 空函数
// 使用场景:1. 方便判断, 2. 方便压缩

const NOOP = () => {};

// 1. 条件判断
const dev = true;
if (dev) {
  instance.render = function () {
    console.log("render");
  };
}

// 可以用作判断。
if (instance.render === NOOP) {
  console.log("i");
}

// 2. 方便压缩 如果是 function(){} ,不方便压缩代码
5. NO 永远返回 false 的函数
/**
 * Always return false.
 */
const NO = () => false;

// 有压缩代码的优势
6. isOn 判断是 on 开头,并且 on 后首字母不是小写字母
const onRE = /^on[^a-z]/;
const isOn = (key) => onRE.test(key);

// 例子:
isOn("onChange"); // true
isOn("onchange"); // false
isOn("on1"); // true

onRE是正则, ^on是指 on 开头, ^a-z是指不是 a-z 小写字母

7. isModelListener 监听器
const isModelListener = (key) => key.startsWith("onUpdate:");

// 例子:
isModelListener("onUpdate:change"); // true
isModelListener("1onUpdate:change"); // false

判断字符串是不是onUpdate:开头

startsWith() 方法用于检测字符串是否以指定的前缀开始。

8. extend 合并
const extend = Object.assign;

// 例子:
const data = { name: "工具", age: 30 };
const data2 = extend(data, { mp: "前端小溪", name: "工具函数" });
console.log(data); // { name: "工具函数", mp: "前端小溪", age: 30 }
console.log(data2); // { name: "工具函数", mp: "前端小溪", age: 30 }
console.log(data === data2); // true

将两个对象合并,如相同属性,后者覆盖前者,且存在继承关系

9. remove 删除数组某一项
const remove = (arr, el) => {
  const i = arr.indexOf(el);
  if (i > -1) {
    arr.splice(i, 1);
  }
};

// 例子:
const arr = ["a", 2, "c"];
remove(arr, "c");
console.log(arr); // ['a', 2]
10. hasOwn 是不是自己本身所拥有的属性
const hasOwnProperty = Object.prototype.hasOwnProperty;
const hasOwn = (val, key) => hasOwnProperty.call(val, key);

// 原型操作相关 API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 用来重定义 this 这个对象的
// .call 例子:
// 猫吃鱼,狗吃肉,
// 有天狗想吃鱼了
// 猫.吃鱼.call(狗,鱼)
// 狗就吃到鱼了

// 例子:
hasOwn({ name: "前端小溪" }, "name"); // true
hasOwn({ name: "前端小溪" }, "a"); // false
hasOwn({ name: "前端小溪" }, "toString"); // false
hasOwn({ __proto__: { name: "前端小溪" } }, "name"); // false

hasOwn 可判断是否自身的属性,不包含原型上的方法

11. isArray 判断是不是数组
const isArray = Array.isArray;

// 例子:
isArray([1, 2, 3, 4]); // true
var obj = {
  a: 1,
  b: 2,
};
isArray(obj); // false
isArray(new Array()); // true
isArray("Array"); // false

const fakeArr = { __proto__: Array.prototype, length: 0 };
isArray(fakeArr); // false
fakeArr instanceof Array; // true
// 所以 instanceof 这种情况 不准确
12. 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 新增 MapSet,详情可查看

13. isSet 判断是不是 Set 对象
const isSet = (val) => toTypeString(val) === "[object Set]";

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

Set 对象较多在数组去重时用到

14. isDate 判断是不是 Date 对象
const isDate = (val) => val instanceof Date;

// 例子:
isDate(new Date()); // true

//  `instanceof`会根据原型链往上查找
isDate({ __proto__: new Date() })(
  // true
  { __proto__: [] }
) instanceof Array; // true
// 判断数组时应使用Array.isArray
15. isFunction 判断是不是函数
const isFunction = (val) => typeof val === "function";

// 例子:
isFunction(() => {}); // true
16. isString 判断是不是字符串
const isString = (val) => typeof val === "string";

// 例子
isString(12); // false
isString(""); // true
isString("12"); // true
17. isSymbol 判断是不是 Symbol
const isSymbol = (val) => typeof val === "symbol";

// 例子:
let s = Symbol();

typeof s; // "symbol"

Symbol 表示独一无二的值

18. isObject 判断是不是对象
const isObject = (val) => val !== null && typeof val === "object";

// 例子:
isObject({}); // true
isObject(null); // false
// 判断不为 null 的原因是 typeof null 其实 是 object
19. isPromise 判断是不是 Promise
const isPromise = (val) => {
  return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};

// 例子
const p1 = new Promise(function (resolve, reject) {
  resolve("若川");
});
isPromise(p1); // true

判断参数是否是对象,并且存在 .then 函数和 .catch 函数

20. objectToString 对象转字符串
const objectToString = Object.prototype.toString;
21. toTypeString 对象转字符串
const toTypeString = (value) => objectToString.call(value);

// 例子:
toTypeString({ name: "张三" }); // "[object Object]"
22. toRawType 对象转字符串 截取后几位
const toRawType = (value) => {
  // extract "RawType" from strings like "[object RawType]"
  return toTypeString(value).slice(8, -1);
};

// 例子:
toRawType({ name: "张三" }); // "Object"
23. isPlainObject 判断是不是纯粹的对象
const isPlainObject = (val) => toTypeString(val) === "[object Object]";

isPlainObject({}); // true
isPlainObject([]); // false

const Person = function () {
  this.name = "前端小溪";
};
isPlainObject(new Person()); // true 构造函数是实例对象
24. isIntegerKey 判断是不是数字型的字符串 Key
const isIntegerKey = (key) =>
  isString(key) &&
  key !== "NaN" &&
  key[0] !== "-" &&
  "" + parseInt(key, 10) === key;

// 其中 parseInt 第二个参数是进制。
// 例子:
isIntegerKey(""); // false
isIntegerKey("12"); // true
isIntegerKey("012"); // false
isIntegerKey("a"); // false

判断字符串类型,并且不是 NaN ,并且不是’-'开头,并且可以被 parseInt

25. makeMap && 判断是不是纯粹的对象

makeMap 是将一个带逗号分隔的字符串,生成一个 map(键值对)对象,并返回一个可供检测key值是否存在的函数,第二个参数是否将参数转小写

/**
 * 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"
);

// 例子:
isReservedProp("key"); // true
isReservedProp("ref"); // true
isReservedProp("onVnodeBeforeMount"); // true
isReservedProp("onVnodeMounted"); // true
isReservedProp("onVnodeBeforeUpdate"); // true
isReservedProp("onVnodeUpdated"); // true
isReservedProp("onVnodeBeforeUnmount"); // true
isReservedProp("onVnodeUnmounted"); // true
26. cacheStringFunction 缓存
const cacheStringFunction = (fn) => {
  const cache = Object.create(null);
  return (str) => {
    const hit = cache[str];
    return hit || (cache[str] = fn(str));
  };
};

let add = (a, b) => a + b;
let calculate = cacheStringFunction(add);
calculate(10, 20); //30
calculate(10, 20); // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次

var count = 0;
var fn = function (n) {
  count++;
  return n < 2 ? n : fn(n - 1) + fn(n - 2);
};
fn = cacheStringFunction(fn);
for (var i = 0; i <= 10; i++) {
  fn(i);
}
console.log(count); // 12
// 不使用缓存函数count 是 453

缓存函数是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据

27. camelize 连字符转小驼峰
// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成
const camelizeRE = /-(\w)/g;
/**
 * @private
 */
const camelize = cacheStringFunction((str) => {
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ""));
});

camelize("on-click"); // onClick
28. hyphenate 小驼峰转连字符
const hyphenateRE = /\B([A-Z])/g;
/**
 * @private
 */
const hyphenate = cacheStringFunction((str) =>
  str.replace(hyphenateRE$1, "-$1").toLowerCase()
);

// 例子:
hyphenate("onClick"); // on-click
29. capitalize 字符串转 on 后首字母大写
// 首字母转大写
const capitalize = cacheStringFunction(
  (str) => str.charAt(0).toUpperCase() + str.slice(1)
);
/**
 * @private
 */
const toHandlerKey = cacheStringFunction((str) =>
  str ? `on${capitalize(str)}` : ``
);

// 例子:
toHandlerKey("click"); // onClick
30. hasChanged 判断是不是有变化
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

// Object.is ES6新增的用来比较两个值是否严格相等的方法
// 例子:
hasChanged(1, 2); // true 已经更改
31. invokeArrayFns 执行数组里的函数
const invokeArrayFns = (fns, arg) => {
  for (let i = 0; i < fns.length; i++) {
    fns[i](arg);
  }
};

// 例子:
let arr = [
  function (val) {
    console.log(`我是${val}`);
  },
  function (val) {
    console.log(`今年18岁了`);
  },
];
invokeArrayFns(arr, "前端小溪");
/**
 * 我是前端小溪
 * 今年18岁了
 */
32. def 定义对象属性
const def = (obj, key, value) => {
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: false,
    value,
  });
};

// configurable: 该属性是否可被删除
// enumerable: 该属性在for in循环中是否会被枚举。
// value: 获取属性时所返回的值。
33. toNumber 转数字

如果是 parseFloat 返回 NaN 则返回本身, NaN 返回 true, 否则返回

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

// 例子:
toNumber("12"); // 12
toNumber("a12"); // 'a12'
parseFloat("a12"); // NaN
isNaN(NaN); // true
34. 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 指向

初次执行 getGlobalThisundefined ,执行后面的赋值语句

selfWeb Worker 全局对象

window游览器 全局对象

globalNode 全局对象

四、总结

我们学习到了空对象、空数组、空函数、isMap、isSet、isPromise等函数,实现了对原生JavaScript的补充和拓展,camelize、hyphenate、toHandlerKey等函数则是Vue相关事件名称和声明周期名称的处理

我们在搭建自己项目的基础框架时,可以借鉴源码的工具函数,让我们的工具函数具有更高的可复用性和可拓展性

Logo

前往低代码交流专区

更多推荐