[关闭]
@wy 2019-07-05T19:11:50.000000Z 字数 5321 阅读 579

盘点Vue源码中用到的工具函数

vuejs


以下摘取的函数,在 shared 目录下公用的工具方法。文件在 utile.js 中,githu地址

提取了一些常用通用的函数进行剖析,主要包含以下内容:

  1. 创建一个被冻结的空对象
  2. 判断是否是 undefinednull
  3. 判断是否不是 undefined 和 null
  4. 判断是否是原始类型
  5. 判断是否是对象类型
  6. 判断有效的数组下标
  7. 判断是否是一个 Promise 对象
  8. 删除数组中指定元素
  9. 用做缓存的高阶函数
  10. 递归判断一个对象是否和另个一个对象完全相同
  11. 函数只执行一次
  12. 自定义改变 bind 函数

1. 创建一个被冻结的空对象

  1. export const emptyObject = Object.freeze({})

一旦创建不能给这个对象添加任何属性。

2. 判断是否是 undefinednull

  1. function isUndef (v) {
  2. return v === undefined || v === null
  3. }

在源码中很多地方会判断一个值是否被定义,所以这里直接抽象成一个公共函数。
传入任意值,返回是一个布尔值。

3. 判断是否不是 undefinednull

  1. function isDef (v) {
  2. return v !== undefined && v !== null
  3. }

当传入的值,既不是 undefined 也不是 null 返回true。

4. 判断是否是原始类型

  1. function isPrimitive (value) {
  2. return (
  3. typeof value === 'string' ||
  4. typeof value === 'number' ||
  5. typeof value === 'symbol' ||
  6. typeof value === 'boolean'
  7. )
  8. }

在js中提供了两大类数据类型:
1. 原始类型(基础类型):String、Number、Boolean、Null、Undefined、Symbol
2. 对象类型:Object、Array、Function

5. 判断是否是对象类型

  1. function isObject (obj: mixed) {
  2. return obj !== null && typeof obj === 'object'
  3. }

传入的值排除掉 null,因为在js中 null 使用运算符 typeof 得到的值是 object,这是一个 bug。因为历史原因放弃修复了。具体可以参考这里查看

6. 判断有效的数组下标

  1. function isValidArrayIndex (val) {
  2. const n = parseFloat(String(val)); // 转成数字
  3. // 下标大于等于0,并且不是小数,并且是有限的数
  4. return n >= 0 && Math.floor(n) === n && isFinite(val)
  5. }
  1. let test = Symbol('test');
  2. console.log(parseFloat(test))
  3. 控制台捕获错误:Uncaught TypeError: Cannot convert a Symbol value to a string

原因是在调用 parseFloat 时,内部会调用内置的 ToString 方法,可以参考这里。而内置的 ToString 方法在遇到 Symbol 类型的值时,会抛出 TypeError 错误,可以参考这里

跟使用一些隐式转换遇到的问题一样,例如使用 + 号:

  1. let test = '' + Symbol('text');
  2. 控制台捕获错误:Uncaught TypeError: Cannot convert a Symbol value to a string

都是因为内部会调用内置的 ToString 方法造成的。

而如果手动调用 toString 方法或者调用 String,转换为字符串,则不会报错:

  1. let test = Symbol('test');
  2. console.log(test.toString()); // "Symbol(test)"
  3. console.log(String(test)) // "Symbol(test)"
  1. console.log(isFinite(Infinity)); // false
  2. console.log(isFinite(-Infinity)); // false
  3. console.log(isFinite(123)); // true

7. 判断是否是一个 Promise 对象

  1. function isPromise (val) {
  2. return (
  3. isDef(val) &&
  4. typeof val.then === 'function' &&
  5. typeof val.catch === 'function'
  6. )
  7. }

当一个对象存在 then 方法,并且也存在 catch 方法,可以判定为 Promise 对象。

8. 删除数组中指定元素

这个方法有效的避免了进行删除数组某一项时,都要进行查找位置再删除的重复工作。

  1. function remove (arr, item){
  2. if (arr.length) {
  3. const index = arr.indexOf(item)
  4. if (index > -1) {
  5. return arr.splice(index, 1)
  6. }
  7. }
  8. }

9. 用做缓存的高阶函数

用高阶函数的好处是无需暴露不同要求的缓存对象在外面,形成一个闭包。下面这个函数的技巧,应用在工作中,可以提高代码运行的效率。

  1. function cached(fn) {
  2. // 创建一个缓存对象
  3. const cache = Object.create(null)
  4. return (function cachedFn (str) {
  5. // 先从缓存对象中找,要操作的值,是否已经有了操作结果
  6. const hit = cache[str]
  7. // 如果有,则直接返回;没有,则调用函数对值进行操作,并把操作结果存在缓存对象中
  8. return hit || (cache[str] = fn(str))
  9. })
  10. }

例如以下运用,函数的作用是把字符串的首字母大写。

  1. const capitalize = cached((str) => {
  2. return str.charAt(0).toUpperCase() + str.slice(1)
  3. })

这时我们就可以调用 capitalize 对字符串进行首字母大写了。

  1. capitalize('test'); // "Test"
  2. capitalize('test'); // "Test"
  3. capitalize('test'); // "Test"

第一次调用 capitalize 函数,先从缓存对象中取值,没有,则调用计算函数进行计算结果返回,同时存入缓存对象中。这时的缓存对象为:

  1. {test: 'Test'}

再多次调用 capitalize 时,从缓存对象中取值,命中,直接返回,无需再进行计算操作。

10. 递归判断一个对象是否和另个一个对象完全相同

判断两个对象是否相同,主要是判断两个对象包含的值都是一样的,如果包含的值依然是个对象,则继续递归调用判断是否相同。

  1. function isObject (obj){
  2. return obj !== null && typeof obj === 'object'
  3. }
  4. function looseEqual (a, b) {
  5. // 如果是同一个对象,则相同
  6. if (a === b) return true
  7. // 判断是否是对象
  8. const isObjectA = isObject(a)
  9. const isObjectB = isObject(b)
  10. // 两者都是对象
  11. if (isObjectA && isObjectB) {
  12. try {
  13. // 判断是否是数组
  14. const isArrayA = Array.isArray(a)
  15. const isArrayB = Array.isArray(b)
  16. // 两者都是数组
  17. if (isArrayA && isArrayB) {
  18. // 长度要一样,同时每一项都要相同,递归调用
  19. return a.length === b.length && a.every((e, i) => {
  20. return looseEqual(e, b[i])
  21. })
  22. } else if (a instanceof Date && b instanceof Date) { // 如果都是时间对象,则需要保证时间戳相同
  23. return a.getTime() === b.getTime()
  24. } else if (!isArrayA && !isArrayB) { // 两者都不是数组,则为对象
  25. // 拿到两者的key值,存入数组
  26. const keysA = Object.keys(a)
  27. const keysB = Object.keys(b)
  28. // 属性的个数要一样,递归的判断每一个值是否相同
  29. return keysA.length === keysB.length && keysA.every(key => {
  30. return looseEqual(a[key], b[key])
  31. })
  32. } else {
  33. return false
  34. }
  35. } catch (e) {
  36. return false
  37. }
  38. } else if (!isObjectA && !isObjectB) { // 两者都不是对象
  39. // 转成字符串后,值是否一致
  40. return String(a) === String(b)
  41. } else {
  42. return false
  43. }
  44. }

例子:

  1. let a1 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
  2. let b1 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
  3. console.log(looseEqual(a1,b1)); // true
  4. let a2 = [1,2,3,{a:1,b:2,c:[1,2,3,4]}];
  5. let b2 = [1,2,3,{a:1,b:2,c:[1,2,3]}];
  6. console.log(looseEqual(a2,b2)); // false

11. 函数只执行一次

同样利用高阶函数,在闭包内操作标识的真假,来控制执行一次。

  1. function once (fn) {
  2. let called = false
  3. return function () {
  4. if (!called) {
  5. called = true
  6. fn.apply(this, arguments)
  7. }
  8. }
  9. }

实际运用:

  1. function test(){
  2. console.log('我只被执行一次');
  3. }
  4. let test2 = once(test);
  5. test2(); // 我只被执行一次
  6. test2();
  7. test2();
  8. test2();

12. 自定义 bind 函数

  1. function polyfillBind (fn, ctx) {
  2. function boundFn (a) {
  3. const l = arguments.length
  4. return l
  5. ? l > 1
  6. ? fn.apply(ctx, arguments)
  7. : fn.call(ctx, a)
  8. : fn.call(ctx)
  9. }
  10. boundFn._length = fn.length
  11. return boundFn
  12. }

自定义的 bind 函数的场景,都是用来兼容不支持原生 bind 方法的环境。 在自己模拟的 bind 函数中,实际上调用的是 callapply

这个方法写的相对简单,如果更深入了解,可以戳此查看这篇文章

如有偏差欢迎指正学习,谢谢。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注