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

88 阅读3分钟

源码内容及地址

vue2 工具函数

源码解读

将工具函数大致分为以下几类:

  • is**:用于判断是什么
  • to**: 用于做转换
  • has**: 用于判断是否有什么
  • 辅助类方法

一、is** 部分

1.1 isUndef 判断是否为未定义,即是否是undefinednull

function isUndef (v) {
  return v === undefined || v === null
}

1.2 isDef 判断是否为已定义,即不等于undefined同时也不等于null


function isDef (v) {
  return v !== undefined && v !== null
}

1.3 isTrue 判断传入值是否为布尔值true


function isTrue (v) {
  return v === true
}

1.4 isFalse 判断传入值是否为布尔值false


function isFalse (v) {
  return v === false
}

1.5 isPrimitive 判断传入值是否为原始值

注意:此处原始值判断没有包含Null 类型Undefined 类型BigInt 类型,应该是出于当前上下文(vue2)需要

function isPrimitive (value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

1.6 isObject 用于判断是否为非空的对象

注意:当前方法使用的前提是知道传入的值是对象类型,因keyof []返回的也是'object'字符串

function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

1.7 isPlainObject 用于判断传入值是否为纯对象类型

// 获取对象的内置对象类型
var _toString = Object.prototype.toString;
/**
 * Strict object type check. Only returns true
 * for plain JavaScript objects.
 */
function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}

1.8 isRegExp 用于判断传入值是否为正则类型

function isRegExp (v) {
  return _toString.call(v) === '[object RegExp]'
}

1.9 isValidArrayIndex 检查传入值是否为有效的数组索引

  • 注意:
    • isFinite方法用来判断被传入的参数值是否为一个有限数值
    • Math.floor 向下取整
    • parseFloat函数解析一个参数(必要时先转换为字符串?这里有疑问,为啥要先转成字符串)并返回一个浮点数。
function isValidArrayIndex (val) {
  var n = parseFloat(String(val));
  return n >= 0 && Math.floor(n) === n && isFinite(val)
}

1.10 isPromise 判断传入值是否为 Promise 对象

function isPromise (val) {
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

二、to** 部分

2.1 toRawType 获取内置类型字符串

// 获取对象的内置对象类型
var _toString = Object.prototype.toString;

function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}

2.2 toString 将值转化成实际展示的字符串

注意:val == null这一步判断在valundefined时也是返回true,此处建议换成上文提到的isUndef方法,语义上会更清晰些。

function toString (val) {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

2.3 toNumber 将输入值转化成数值,转换失败则返回原始值

注意:isNaN 用于判断传入是不是 NAN(不是一个数值)

function toNumber (val) {
  var n = parseFloat(val);
  return isNaN(n) ? val : n
}

2.4 toArrary 将类数组对象转化成真正的数组

注意:类数组对象:可通过索引访问属性值并且具有length属性的对象

function toArray (list, start) {
  start = start || 0;
  var i = list.length - start;
  var ret = new Array(i);
  while (i--) {
    ret[i] = list[i + start];
  }
  return ret
}

2.5 toObject 将对象数组合并成一个对象

/**
 * 将属性混合到目标对象中,类似于 Object.assign() 方法
 */
function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}

/**
 * 将对象数组合并成一个对象
 */
function toObject (arr) {
  var res = {};
  for (var i = 0; i < arr.length; i++) {
    if (arr[i]) {
      extend(res, arr[i]);
    }
  }
  return res
}

三、has** 部分

3.1 hasOwn 检查属性是否存在于对象中

注意:该方法其实就是Object.prototype.hasOwnProperty.call(obj, key)方法的简写

var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

四、辅助类方法

4.1 通过Object.freeze方法创建一个锁定的空对象

var emptyObject = Object.freeze({});

注意:

  • Object.freeze()可以用来冻结对象类型的值(对象、数组)
  • 可通过Object.isFrozen()方法判断一个对象是否被冻结
  • 在 ES5 中,如果这个方法的参数不是一个对象(一个原始值),那么它会导致 TypeError。在 ES6 中,非对象参数将被视为要被冻结的普通对象,并被简单地返回。
  • Object.freeze()为浅冻结,只冻结对象或数组的第一层,深冻结代码实现如下
// 深冻结函数。
function deepFreeze(obj) {

   // 取回定义在 obj 上的属性名
   var propNames = Object.getOwnPropertyNames(obj);

   // 在冻结自身之前冻结属性
   propNames.forEach(function(name) {
   var prop = obj[name];

   // 如果 prop 是个对象,冻结它
   if (typeof prop == 'object' && prop !== null)
     deepFreeze(prop);
   });

   // 冻结自身 (no-op if already frozen)
   return Object.freeze(obj);
}

obj2 = {
   internal: {}
};

deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
obj2.internal.a; // undefined
  • Object.seal()密封的对象可以改变它们现有的属性。使用Object.freeze() 冻结的对象中现有属性是不可变的。
  • 解冻:原生中冻结是不可逆的,可通过深拷贝来实现解冻

4.2 makeMap 用于生成一个map并返回一个方法来验证传入值是否存在于该map

注意:此方法利用了闭包的特性

function makeMap (
  str,
  expectsLowerCase
) {
  var map = Object.create(null);
  var list = str.split(',');
  for (var i = 0; i < list.length; i++) {
    map[list[i]] = true;
  }
  return expectsLowerCase
    ? function (val) { return map[val.toLowerCase()]; }
    : function (val) { return map[val]; }
}

4.3 isBuiltInTag 检查标签是否为 Vue 内置标签

var isBuiltInTag = makeMap('slot,component', true);

4.4 isReservedAttribute 检查属性是否为保留属性

var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');

4.5 remove 从数组中移除指定项

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

上述方法优化建议: 当数组中不存在指定项时,因该返回原数组,做到输入输出一致

function remove (arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1)
    }
    
    return arr
  }
  return arr
}

4.6 cached 创建一个能够缓存传入方法的计算结果的方法,避免重复计算

function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}

4.7 camelize 将连字符转成小驼峰

var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});

4.8 capitalize 将首字母转成大写

var capitalize = cached(function (str) {
  return str.charAt(0).toUpperCase() + str.slice(1)
});

4.9 hyphenate 将驼峰法转成连字符

var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
});

4.10 genStaticKeys 获取 modules 中的 staticKeys 字符串

function genStaticKeys (modules) {
  return modules.reduce(function (keys, m) {
    return keys.concat(m.staticKeys || [])
  }, []).join(',')
}

4.11 looseEqual 判断两个值是否近似相等

解读:

  • 第一步,a === b,判断两个方法是否完全相等,是则返回true,否则继续向下执行

  • 第二步,通过isObject方法得到出入的值是否是引用类型的值,如果两个都不是引用类型,则直接转成字符串,再做全等判断,如果一个是一个不是则直接返回 false,如果两个都是引用类型,则继续向下判断

    • 判断两个值是不是数组,都是则继续递归子元素,一个是,一个不是则返回 false
    • 判断两个值是不是 Date 类型,都是,则转成时间戳在进行判断,一个是,一个不是则返回 false
    • 判断两个值是不是 Object 类型,都是,则递归判断子元素是否相等,一个是,一个不是则返回 false
function looseEqual (a, b) {
  if (a === b) { return true }
  var isObjectA = isObject(a);
  var isObjectB = isObject(b);
  if (isObjectA && isObjectB) {
    try {
      var isArrayA = Array.isArray(a);
      var isArrayB = Array.isArray(b);
      if (isArrayA && isArrayB) {
        return a.length === b.length && a.every(function (e, i) {
          return looseEqual(e, b[i])
        })
      } else if (a instanceof Date && b instanceof Date) {
        return a.getTime() === b.getTime()
      } else if (!isArrayA && !isArrayB) {
        var keysA = Object.keys(a);
        var keysB = Object.keys(b);
        return keysA.length === keysB.length && keysA.every(function (key) {
          return looseEqual(a[key], b[key])
        })
      } else {
        /* istanbul ignore next */
        return false
      }
    } catch (e) {
      /* istanbul ignore next */
      return false
    }
  } else if (!isObjectA && !isObjectB) {
    return String(a) === String(b)
  } else {
    return false
  }
}

4.12 looseIndexOf 返回数组中与 val 近似相等的值的索引

function looseIndexOf (arr, val) {
  for (var i = 0; i < arr.length; i++) {
    if (looseEqual(arr[i], val)) { return i }
  }
  return -1
}

4.13 once 限制传入方法只能执行一次

function once (fn) {
  var called = false;
  return function () {
    if (!called) {
      called = true;
      fn.apply(this, arguments);
    }
  }
}

4.14 内部常用常量

var SSR_ATTR = 'data-server-rendered';

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];

五、总结

整个工具方法阅读下来收获颇丰:

  • 友好的方法命名,能让我们更好的区分其作用;
  • 回顾了js 的数据类型:原始类型(Null 类型Undefined 类型BigInt 类型String 类型Number 类型Symbol 类型Boolean 类型)和引用类型;
  • 知道闭包的更多应用场景,计算结果缓存、调用次数限制等;
  • 回顾了对象冻结相关的知识点; ...

最主要的还是迈出了源码阅读的重要一步,发现源码阅读并没有想象中的那么难。在此感谢若川大佬提供这样一个机会帮助我们去更好的阅读学习源码。