源码学习—VUE2中的那些工具函数

938 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情 >>

无论是在我们自己的项目中还是在开源项目中,基于复用的原则,我们都会将一些工具函数进行抽离,构建成自己的工具函数库,本篇文章就来学习vue2中的一些工具函数。跟其进行对照,完善优化自己的工具函数库。

学习大纲概览

image.png

函数学习

emptyObject 创建一个冻结对象

var emptyObject = Object.freeze({});

Object.freeze()该方法可以冻结一个对象,冻结之后,则该对象不能添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。该方法是浅冻结,也就是只能使第一层无法修改。

延伸一下,如何判断一个对象是否是被冻结的。可以使用Object.isFrozen()方法。

如何对被冻结的对象,进行解冻呢。JS没有直接提供对应的api,但可以通过克隆一个具有相同属性的新对象,通过修改新对象的属性来达到解冻的目的。

let newObject = Object.assign({}, emptyObject);

数据类型判断相关的方法

这里学习下对基础类型数据的判断,vue2对其进行了统一的封装,运用api的单一职责,一个函数只干一件事。

isUndef 是否是未定义

判断是否是undedined或null,这个在实际开发中是非常常用的方法。无论是参数的校验,还是对后端返回值的校验,都会用来进行做防御性处理。

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

isDef 是否已定义

跟上面的isUndef正好相反。表示该值已经进行了定义。跟下面的isTrue和isFalse都是为了更准确的判断。

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

isTrue

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

isFalse

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

isPrimitive

判断是否是一个基础类型的值。是否是字符串、数字、symbol、布尔。

function isPrimitive (value) { 
return (                      
    typeof value === 'string' ||
    typeof value === 'number' ||
    typeof value === 'symbol' ||
    typeof value === 'boolean'
 )                             
}

isObject

快速判断是否是一个对象,并排除null。主要用于从基础类型中区分出对象来。用typeOf运算法来进行判断,除了基础数据类型,其余都会判断为object。由于null也会判断为object,所以对其进行剔除。

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

具体对象的判断

如判断是否是数组、正则、对象等。

首先对Object.prototype.toString这么长链路的调用,进行简化操作,减少代码重复编写。

var _toString = Object.prototype.toString;
isPlainObject 判断是否是一个纯对象
function isPlainObject (obj) {
    return _toString.call(obj) === '[object Object]' 
}

isRegExp 判断是否是正则表达式

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

isValidArrayIndex 判断是否是有效的数组索引

function isValidArrayIndex (val) {
    var n = parseFloat(String(val));
    return n >= 0 && Math.floor(n) === n && isFinite(val)
}

看源码之后,就是为了更加准确的判断值是否是一个正整数并且可以为0。因为我们的数组索引就是这样的值。

  • parseFloat()方法。解析一个字符串,并返回一个浮点数。这里做的一个防御性编码,对传进来的数据类型进行统一处理为数字。
  • Math.floor(),向下取整。
  • isFinite()用来判断传进来的参数是否是一个有限数值。

作为开源的库,里面的实现还是很严谨的,预防各种边界条件,值得学习。

源码中的应用

看到这个工具函数,一时想不起它的应用场景,然后就去源码中翻了下,发现使用在设置对象属性的时候,对传进来的key做校验。

简单看下源码中的使用:

function set (target, key, val) {
  if (isUndef(target) || isPrimitive(target)
  ) {
    warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
  }
}

isPromise 判断是否是一个promise

function isPromise (val) {
    return (
      isDef(val) &&
      typeof val.then === 'function' &&
      typeof val.catch === 'function'
    )
  }
  • 我理解这里isDef判断,是为了防止传入undefined和null这种情况,因为这两个值直接.xx话会报错。

转化数据类型的方法

toString 将转入的参数转换为字符串。

function toString (val) {
    return val == null
      ? ''
      : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
        ? JSON.stringify(val, null, 2)
        : String(val)
  }
  • val == null这里为什么用双等。这样直接可以对undefined和null进行了判断。undefined == null; // true ; null == null; // true。简洁。这种情况直接返回空字符串。
  • 如果是数组或者纯对象的话。并且传进来的对象的toString没有重写,可以直接用。JSON.stringify做转换。
  • 其他情况直接用String做转换。

toNumber 转换成数字

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

非数字的情况下,直接将传入的值在返回。

toArray 将类数组转为数组

  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
  }
  • 支持从哪个位置开始,默认从0开始。

extend 合并两个对象

  function extend (to, _from) {
    for (var key in _from) {
      to[key] = _from[key];
    }
    return to
  }

这里其实也可以用Object.assgin({}, {});

toObject 将传入的数组转成对象

function toObject (arr) {
    var res = {};
    for (var i = 0; i < arr.length; i++) {
      if (arr[i]) {
        extend(res, arr[i]);
      }
    }
    return res
  }

这块想到的场景就是将数组扁平为对象,更利于一些查找操作。

判断对象属性相关的函数

makeMap 创建一个对象

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]; }
  }
  • 第一个参数是以,分割的字符串。
  • 第二个参数,传入是否需要小写转换。
  • 返回值是返回了一个函数,用来判断传进来的key是否在生成的map中。
  • Object.create(null)返回一个没有原型链的空对象,防止后续用来判断的key存在于原型链中。

该方法类似于一个工厂函数,创建有关键key的对象,用于判断。

isBuiltInTag 是否是内置的tag

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

isBuiltInTag('slot'); // true
isBuiltInTag('slot1'); // false

用于判断框架中一些内置的有特殊含义的key。

isReservedAttribute 是否是保留属性

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

创建了一个key,ref,slot,slot-scope,is作为key的对象,用于后续判断是否在对象中存在这些保留使用的关键字。

hasOwn 检测是否是对象独有属性,不是原型链上的。

var hasOwnProperty = Object.prototype.hasOwnProperty;
  function hasOwn (obj, key) {
    return hasOwnProperty.call(obj, key)
  }
  • call的使用真的是学到了。改变执行上下文,并且也防止hasOwnProperty被传入的对象重写。

命名转换类的工具函数

cached 利用闭包,缓存数据

function cached (fn) {
    var cache = Object.create(null);
    return (function cachedFn (str) {
      var hit = cache[str];
      return hit || (cache[str] = fn(str))
    })
  }
  • 入参是一个回调函数,用来处理传进来的数据
  • 创建一个没有原型链的对象
  • 利用一个闭包将处理完后的数据进行缓存,以防后续重复进行处理。

camelize 连字符转小驼峰

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

camelize('on-click'); // onClick

这里不深究正则相关的内容。这里的正则表示的就是将连字符-跟后面跟着的字符,并将字符转换大写,替换掉匹配的连字符跟后边的字符。

这种方法还是很常用的,在定义组件中我们传递的属性,都会做这层转换。

capitalize 首字母转大写

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

hyphenate 小驼峰转连字符

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

其他工具函数

remove 移除数组中的一项

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

这个方法相当的实用,在开发中只要涉及到列表的增删的操作,都少不了移除数组中的某一项。

其实也可以封装一个,使用index删除的方法,虽然简单,但在其中做一些兜底处理,会使其更加强壮。比如数组的判断,索引的判断等等。

array的splice方法是一个相对耗性能的方法,删除一项,其后面的元素都要移动。这里可以通过将删除的那一项,置位null。无论是在js中还是在渲染模板中,对每一项做一个判断,这样来减少删除操作造成的性能损耗。

polyfillBind和nativeBind

  • polyfillBind bind方法的垫片,也就是bind的兼容写法。
  • nativeBind原生bind方法。
function polyfillBind (fn, ctx) {
    function boundFn (a) {
      var l = arguments.length;
      return l
        ? l > 1
          ? fn.apply(ctx, arguments)
          : fn.call(ctx, a)
        : fn.call(ctx)
    }

    boundFn._length = fn.length;
    return boundFn
  }

  function nativeBind (fn, ctx) {
    return fn.bind(ctx)
  }
    var bind = Function.prototype.bind
    ? nativeBind
    : polyfillBind;

主要是针对老浏览器不支持bind方法的一种兼容操作。这里不做过多的表述,在现代浏览器这种垫片会越来越少的。扩展的话可以多去了解一下如何手写call、apply及bind。

noop 空函数

function noop (a, b, c) {}

了解了下源码中的使用,组要是做一个空函数赋值。看注释是为了防止flow转换时留下无用的转译代码。

no 一直返回false

var no = function (a, b, c) { return false; };

也是看了源码中的使用。主要是用作兜底返回一个函数执行返回false的兜底操作。简单写下源码的使用。

// 定义
function parseHTML (html, options) {
    var isUnaryTag$$1 = options.isUnaryTag || no;
}
// 使用
 var unary = isUnaryTag$$1(tagName) || !!unarySlash;

identity 返回参数本身

var identity = function (_) { return _; };

genStaticKeys 生成包含静态属性的字符串

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

looseEqual 判断宽松相等

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
    }
  }
  • 因为针对引用类型,也就是数组、对象等,虽然看起来一样,但是进行对比其永远都是不相等的。该函数就是为了让看起来完全相同的引用类型对比得到相等的结果
  • 针对数组、对象、日期进行递归的判断,对其每一项都判断相等的话,则判定为宽松相等。

looseIndexOf 宽松版的indexOf

因为原生的indexOf是用严格相等来判断,因此在针对对象的判断是时,都是不相等的。这里用上面实现的looseEqual来实现一个宽松相等的indexOf

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

once 使函数只执行一次

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

保证函数只会执行一次,利用闭包的特性,保存执行状态。举个例子:

function test() {
    console.log('=====test');
}
var a = once(test);
a(); // =====test
a(); // 什么都不输出了

收获

  • 防御型代码的处理。正常开发中,也会针对性的对函数的入参做参数校验,但都是直接判断类型,几乎没有这种封装,通过这些工具函数的学习,可以将日常开发中经常用到的类型判断进行封装,统一管理,并在语义化上做的更加易读。
  • 闭包缓存的使用。可以利用闭包将我们需要的数据进行缓存。尤其是我们的一些处理数据的工具函数,可能会频繁的使用,如果不进行缓存的话,每次都会重新执行一次处理逻辑,在性能上肯定是不如直接将处理好的数据进行缓存,后续直接拿来使用。
  • 小技巧noop函数、no函数、identity函数的使用。可以将我们的一些默认操作,通过这样的封装,使代码更具可读,且减少逻辑判断,值得学习。
  • 函数的命名。非常语义化,规范的命名方式,is、to、has等,一目了然。
参考