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

185 阅读5分钟

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

1. 前言

文章主要根据若川大佬【若川视野 x 源码共读】第24期 | vue2工具函数文章的引导,结合自己看代码时的一些理解而写的总结,文章中也存在一些自己还未理解的地方;此外,若文章中有不合理或错误的地方还望各位读者告知,万分感谢。

2. 工具函数

emptyObject

定义空对象

 var emptyObject = Object.freeze({})
  • Object.freeze() 冻结对象,冻结后不允许对对象进行任何修改

isUndefined

是否未定义

 function isUndef (v) {
     return v === undefined || v === null
 }
  • 包含两种undefinednull基本数据类型;不包含NaN判断,NaN表示'Not A Number'非基本数据类型
  • undefined 声明但未赋值
  • null 变量值为空
  • NaN数据类型转换时,被转换数据不含可以转换的部分

isDefined

是否已定义

 function isDef (v) {
     return v !== undefined && v !== null
 }
  • 刚好与上面的判断相反,判断是否不为undefinednull

isTrue

是否为true

 function isTrue (v) {
     return v === true
 }
  • 此处应该是包含了两层判断,v是否为boolean以及是否为booleantrue

isFalse

是否为false

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

isPrimitive

是否为基本数据类型

 function isPrimitive (value) {
     return (
       typeof value === 'string' ||
       typeof value === 'number' ||
       // $flow-disable-line
       typeof value === 'symbol' ||
       typeof value === 'boolean'
     )
 }
  • 判断是否为除undefinednull外的其余四种基本数据类型stringnumberbooleansymbol

  • symbol: ES6新增的一个基本数据类型,表示一个独一无二的值,常用来定义对象的唯一属性名

     let s1 = Symbol("symbol"); // 创建一个symbol实例
     let s2 = Symbol("symbol");
     s1 === s2; // false
     ​
     /** 对象中的使用 **/
     let name1 = Symbol("name");
     let name2 = Symbol("name");
     let obj = {
       [name1]:"Jack"
     }
     obj1[name2] = "Rose";
     //以symbol作为属性不能使用点运算符添加属性,用点来添加属性是添加常规字符串属性
     //obj.n = "tom";
     console.log(obj); // {Symbol(name): "Jack", Symbol(name): "Rose"}
     obj[name1]; // Jack 
     obj.name2; // undefined
     // 不会被for...in遍历
     // 不会被Object.keys(obj)、Object.values(obj)和Object.getOwnPropertyNames(obj)返回
     // 通过Object.getOwnPropertySymbols()和Reflect.ownKeys()可取到值
     console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(name), Symbol(name)]
     console.log(Reflect.ownKeys(obj)); // [Symbol(name), Symbol(name)]
    

isObject

是否为对象

 function isObject (obj) {
     return obj !== null && typeof obj === 'object'
 }
  • 不为nulltypeof'object'

  • typeof返回具体结果 ,参考自ECMA262

    Type of valResult
    Undefined"undefined"
    Null"object"
    Boolean"boolean"
    Number"number"
    String"string"
    Symbol"symbol"
    BigInt"bigint"
    Object (does not implement [[Call]])"object"
    Object (implements [[Call]])"function"

_toString

定义_toString方法

var _toString = Object.prototype.toString;
  • Object.prototype指向对象的原型。

toRawType

获取对象的原型类型

function toRawType (value) {
    return _toString.call(value).slice(8, -1)
}
  • toRawType(2)输出Number
  • toRawType("2")输出String
  • toRawType(Symbol("name"))输出Symbol

isPlainObject

是否为纯粹的对象

function isPlainObject (obj) {
    return _toString.call(obj) === '[object Object]'
}
  • 原理和上一个方法相同,观察对象的原型类型是否为Object即可
  • 这里要是我写的代码的话可能直接就是return toRawType(obj) === 'Object',不知道大佬有啥其它的考量

isRegExp

是否为正则表达式

function isRegExp (v) {
    return _toString.call(v) === '[object RegExp]'
}
  • 和上面一样的对比原型类型,没啥说法

isValidArrayIndex

是否为有效数组索引

function isValidArrayIndex (val) {
    var n = parseFloat(String(val));
    return n >= 0 && Math.floor(n) === n && isFinite(val)
}
  • parseFloat(String(val))排除非数字类型,转换为float避免将浮点数转为整数;
  • n >= 0 && Math.floor(n) === n保证n为非负整数,isFinite(n)不为Infinity;顺带试了试,欸Math.floor(Infinity) === Infinity
  • MDN关于Infinity的更多说明

isPromise

是否为Promise对象

function isPromise (val) {
    return (
      isDef(val) &&
      typeof val.then === 'function' &&
      typeof val.catch === 'function'
    )
}
  • isDef(val)是否已定义
  • typeof val.then === 'function' && typeof val.catch === 'function'存在thencatch两个方法

toString

转化为字符串

function toString (val) {
    return val == null
      ? ''
      : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
        ? JSON.stringify(val, null, 2)
        : String(val)
}
  • null转为''空串
  • 数组或对象(且对象toString方法等于_toString)使用JSON.stringify()转化
  • 其它使用String()强制转换

toNumber

转化为数字

function toNumber (val) {
    var n = parseFloat(val);
    return isNaN(n) ? val : n
}
  • 使用parseFloat(val)保证浮点数和整数都能正确转换
  • 判断结果是否为NaN,是则说明原字符串无法转化,返回原字符串

makeMap

创建一个Map对象并返回一个用于验证key是否在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]; }
}
  • Object.create(null)创建空对象
  • str.split(','),分割原字符串,说明第一个参数期望为一个,分隔的字符串
  • 遍历分割完的结果数组,map[list[i]] = true将数组中的字符串作为对象属性值,并将值设置为true;从这里可以看出这里的Map并不等同于JavaScript中的内置对象Map
  • 返回一个方法,用于查找key是否在Map中,当第二个参数为true时,将方法会将查询的key全都转化为小写字母

isBuiltInTag

检查是否为内置的Tag

var isBuiltInTag = makeMap('slot,component', true);
  • 使用上一函数makeMap生成检查方法,不区分大小写
  • slotcomponent应该是有保留用途吧(大胆猜测,捂脸)

isReservedAttribute

检查是否为保留属性

var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
  • 依旧是使用makeMap生成检查方法,但此处区分大小写
  • keyrefslotslot-scopeis从函数名理解了,为Vue保留属性

remove

从数组中删除元素的方法

function remove (arr, item) {
    if (arr.length) {
      var index = arr.indexOf(item);
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
}
  • if (arr.length)保证数组不为空
  • arr.indexOf(item)查找元素在数组中的对应下标
  • if (index > -1)元素存在时return arr.splice(index, 1)删除对应元素并返回删除元素的值
  • Array.splice的更多用法,如:截取和替换

hasOwnProperty

检查对象是否包含属性

 var hasOwnProperty = Object.prototype.hasOwnProperty;
  • Object.prototype.hasOwnProperty该方法返回一个布尔值,只是对象自身是否包含指定属性;方法会忽略从原型链上继承到的属性。

hasOwn

检查指定对象是否包含指定的属性

function hasOwn (obj, key) {
    return hasOwnProperty.call(obj, key)
}
  • 使用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() : ''; })
});

capitalize

将字符串首字母转化为大写

var capitalize = cached(function (str) {
    return str.charAt(0).toUpperCase() + str.slice(1)
});
  • str.charAt(0)获取字符串首字母

hyphenate

将驼峰格式转化为连字符分隔字符串

var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
    return str.replace(hyphenateRE, '-$1').toLowerCase()
});
  • \B([A-Z])匹配非单词边界的A-Z,即单词中(非单词首尾)的大写字母

polyfillBind

???暂时还没理解是做什么用的函数

/**
 * Simple bind polyfill for environments that do not support it,
 * e.g., PhantomJS 1.x. Technically, we don't need this anymore
 * since native bind is now performant enough in most browsers.
 * But removing it would mean breaking code that was able to run in
 * PhantomJS 1.x, so this must be kept for backward compatibility.
 */

/* istanbul ignore next */
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 函数。同时兼容写法,对参数的多少做出了判断,使用callapply实现,据说参数多适合用 apply,少用 call 性能更好。

需要补习下bind和作用域相关知识

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
}
  • 两个参数:list需要转化的对象,start从第几项开始转化
  • while (i--) { ret[i] = list[i + start]; }遍历转化对象的每一项

extend

属性到目标对象

function extend (to, _from) {
    for (var key in _from) {
      to[key] = _from[key];
    }
    return to
}
  • from对象的所有属性添加到to对象中

toObject

将数组转化为对象

function toObject (arr) {
    var res = {};
    for (var i = 0; i < arr.length; i++) {
      if (arr[i]) {
        extend(res, arr[i]);
      }
    }
    return res
}
  • 注意因为使用extend进行转化,所以如果数组是字符串数组后面的字符串的每个字符会覆盖前一项对应索引的项,所以有点不太理解这个函数的作用,貌似只能转化正确只有一项的数组

noop

空函数

/**
 * Perform no operation.
 * Stubbing args to make Flow happy without leaving useless transpiled code
 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
 */
function noop (a, b, c) {}
  • 我不李姐.jpg

no

永假函数(我这样称呼它)

var no = function (a, b, c) { return false; };
  • 如其名,永远返回false

identity

返回自身

var identity = function (_) { return _; };
  • 函数返回传入的参数自身

genStaticKeys

生成静态属性

function genStaticKeys (modules) {
    return modules.reduce(function (keys, m) {
      return keys.concat(m.staticKeys || [])
    }, []).join(',')
}
  • 不太理解函数的作用,函数基本是理解了,就是将对象数组中的每项的staticKeys属性值拼成一个,号分隔的字符串

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
    }
}
  • 当两值严格相等时返回true

  • 当两值不严格相等时,分下面几种情况:

    1. 两值都为Object时,情况比较多,逐一讨论

      • 均为数组时:长度相等,且数组对应每项均宽松相等返回true,否则返回false
      • 任一不为数组但均继承Date对象时,getTime()返回结果严格相等则返回true,否则false
      • 均不为数组时,两对象属性数量相同,且对应属性值宽松相等返回true,否则返回false
      • 在不满足上述三种情况时返回false
    2. 两值都不为Object时,转化为String两值严格相等,返回true

    3. 上述都不满足,返回false

  • instanceof用于检测构造函数的prototype属性是否出现在某个实例对象原型链上 MDN

looseIndexOf

宽松indexOf

/**
 * Return the first index at which a loosely equal value can be
 * found in the array (if value is a plain object, the array must
 * contain an object of the same shape), or -1 if it is not present.
 */
function looseIndexOf (arr, val) {
    for (var i = 0; i < arr.length; i++) {
      if (looseEqual(arr[i], val)) { return i }
    }
    return -1
}
  • 返回数组arr中第一个与目标val宽松相等的值的索引,未找到返回-1indexOf()的宽松相等版本

once

确保一个方法只被调用一次

function once (fn) {
    var called = false;
    return function () {
      if (!called) {
        called = true;
        fn.apply(this, arguments);
      }
    }
}
  • 添加called标志位保证传入后被转化的函数只能执行一次

3. 总结

通过查看源码自己也学到了很多新的知识,还能复习巩固下已掌握的知识,顺便还能学习优秀的编码习惯,一石三鸟;同时也暴露出了自己很多薄弱的点,在阅读源码时只是浅尝辄止还未深挖,但其中如noop方法注释中关于Flow方法调用检查的内容单独也可以总结为一篇学习笔记了,之后可以看下这方面的相关知识,再立一个发篇学习笔记的flag。

\