Vue2源码中的工具函数

Vue2源码中的工具函数


vue2 源码分析 工具函数

打开 vue 仓库

我们可以在 项目目录结构 描述中,找到 shared 模块。 shared: contains utilities shared across the entire codebase.

本文就是讲 shared 模块,对应的文件路径是:vue/vue/src/shared

源代码的代码,使用了 Flow 类型,为了方便理解,可以直接看源码仓库中的打包后的 dist/vue.js 14 行到 379 行

1.1 emptyObject

var emptyObject = Object.freeze({});

冻结对象。第一层无法修改。对象中也有判断是否冻结的方法。

Object.isFrozen(emptyObject); // true

1.2 isUndef 是否是未定义

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

这里说一个另外的注意点,在 ES3 中undefined是可以被赋值的,而在 ES5 之后全局的undefined就不能赋值了,但局部的还是可以被赋值修改。

(function () {
  var undefined = 10;
  // 这里会输出10
  console.log(undefined); // 10
})();

所以判断undefined更严谨的是使用 void 0 因为在 void 后面随便跟上一个表达式,返回的都是 undefined,而其中最短的就是 void 0 ,且 void 是不能被重写的。

function isUndefined(obj) {
  return obj === void 0;
}

在 ES5 之后,void 0 也是有用武之地的,用 void 0 代替 undefined 能节省不少字节的大小,事实上,不少 JavaScript 压缩工具在压缩过程中,正是将 undefinedvoid 0 代替掉了。

JavaScript 中假值有六个

false;
null;
undefined;
0;
""(空字符串);
NaN;

为了判断准确,Vue2 源码中封装了 isDef、 isTrue、isFalse 函数来准确判断

1.3 isDef 是否是已经定义

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

1.4 isTrue 是否是 true

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

1.5 isFalse 是否是 false

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

1.6 isPrimitive 判断值是否是基本类型

判断是否是字符串、或者数字、或者 symbol、或者布尔值。

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

1.7 isObject 判断是对象(引用类型)

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

// 例子:
isObject([]); // true
// 有时不需要严格区分数组和对象

1.8 toRawType 获得变量类型

Object.prototype.toString() 方法返回一个表示该对象的字符串

var _toString = Object.prototype.toString;

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

// 例子:
toRawType(""); // 'String'
toRawType(); // 'Undefined'
toRawType([]); // 'Array'

1.9 isPlainObject 是否是纯对象

function isPlainObject(obj) {
  return _toString.call(obj) === "[object Object]";
}

// 上文 isObject([]) 也是 true
// 这个就是判断对象是纯对象的方法。
// 例子:
isPlainObject([]); // false
isPlainObject({}); // true

1.10 isRegExp 是否是正则表达式

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

// 例子:
// 判断是不是正则表达式
isRegExp(/niko/); // true

1.11 isValidArrayIndex 是否是可用的数组索引值

数组可用的索引值是 0 (‘0’)、1 (‘1’) 、2 (‘2’) …

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

Math.floor(n) === n可以用来判断是否是整数。 该全局 isFinite() 函数用来判断被传入的参数值是否为一个有限数值(finite number)。在必要情况下,参数会首先转为一个数值。

isFinite(Infinity); // false
isFinite(NaN); // false
isFinite(-Infinity); // false

isFinite(0); // true
isFinite(2e64); // true, 在更强壮的Number.isFinite(null)中将会得到false

isFinite("0"); // true, 在更强壮的Number.isFinite('0')中将会得到false

1.12 isPromise 判断是否是 promise

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

// 例子:
isPromise(new Promise()); // true

这里用 isDef 判断其实相对 isObject 来判断 来说有点不严谨,但是够用。

1.13 toString 转字符串

转换成字符串。是数组或者对象并且对象的 toString 方法是 Object.prototype.toString,用 JSON.stringify 转换。

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

*这里的_toString 在 1.8 中定义,isPlainObject 在 1.9 中定义

1.14 toNumber 转数字

转换成数字。如果转换失败依旧返回原始字符串。

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

toNumber("a"); // 'a'
toNumber("1"); // 1
toNumber("1a"); // 1
toNumber("a1"); // 'a1'

1.15 makeMap 生成一个 map (对象)

传入一个以逗号分隔的字符串,生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 map 中。第二个参数是小写选项,为 true 时大小写不敏感。

function makeMap(str, expectsLowerCase) {
  // Object.create(null) 没有原型链的空对象
  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];
      };
}
// 该方法应用在1.16 1.17

1.16 isBuiltInTag 是否是内置的 tag

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

// 返回的函数,第二个参数不区分大小写
isBuiltInTag("slot"); // true
isBuiltInTag("component"); // true
isBuiltInTag("Slot"); // true
isBuiltInTag("Component"); // true

1.17 isReservedAttribute 是否是保留的属性

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

isReservedAttribute("key"); // true
isReservedAttribute("ref"); // true
isReservedAttribute("slot"); // true
isReservedAttribute("slot-scope"); // true
isReservedAttribute("is"); // true
isReservedAttribute("IS"); // undefined

1.18 remove 移除数组中的中一项

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

splice 其实是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置。 axios InterceptorManager 拦截器源码 中,拦截器用数组存储的。但实际移除拦截器时,只是把拦截器置为 null 。而不是用 splice 移除。最后执行时为 null 的不执行,效果是相同的。axios 拦截器这个场景下,不得不说为性能做到了很好的考虑。因为拦截器是用户自定义的,理论上可以有无数个,所以做性能考虑是必要的。 axios 拦截器代码示例:

// 代码有删减
// 声明
this.handlers = [];

// 移除
if (this.handlers[id]) {
  this.handlers[id] = null;
}

// 执行
if (h !== null) {
  fn(h);
}

1.19 hasOwn 检测是否是自己的属性

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

// 例子:

// 特别提醒:__proto__ 是浏览器实现的原型写法,后面还会用到
// 现在已经有提供好几个原型相关的API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 则是函数里 this 显示指定以为第一个参数,并执行函数。

hasOwn({ __proto__: { a: 1 } }, "a"); // false
hasOwn({ a: undefined }, "a"); // true
hasOwn({}, "a"); // false
hasOwn({}, "hasOwnProperty"); // false
hasOwn({}, "toString"); // false
// 是自己的本身拥有的属性,不是通过原型链向上查找的。

1.20 cached 缓存

利用闭包特性,缓存数据

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

1.21 camelize 连字符转小驼峰

on-click => onClick

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

这里的 replace 方法参数问题, 可以看JS 中 replace 方法这篇博客的例 7

1.22 capitalize 首字母转大写

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

1.23 hyphenate 小驼峰转连字符

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

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

由于现在浏览器基本都是支持原生bind的,所以直接用 polyfillBind写个例子:

class Dog {
  say = function (word) {
    console.log(this.name + " say: " + word);
  };
}
const dog = new Dog();
class Cat {
  constructor() {
    this.name = "mao";
  }
}
const cat = new Cat();
fn = polyfillBind(dog.say, cat);
fn("hello"); // mao say: hello

1.25 toArray 把类数组转成真正的数组

把类数组转换成数组,支持从指定位置开始,默认从 0 开始。

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;
}

// 例子:
function fn() {
  // 使用1.8 toRawType 方法得到参数arguments的类型是 Arguments, 不是Array, 也就是个类数组结构
  var arr1 = toArray(arguments);
  console.log(arr1); // [1, 2, 3, 4, 5]
  var arr2 = toArray(arguments, 2);
  console.log(arr2); // [3, 4, 5]
}
fn(1, 2, 3, 4, 5);

1.26 extend 合并

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

const person = { name: "Niko" };
const person2 = extend(person, { age: 26, gender: "male" });
console.log(person); // {name: 'Niko', age: 26, gender: 'male'}
console.log(person2); // {name: 'Niko', age: 26, gender: 'male'}
console.log(person === person2); // true

1.27 toObject 数组转对象

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

toObject(["Niko", "Nikola"]);
// {0: 'N', 1: 'i', 2: 'k', 3: 'o', 4: 'l', 5: 'a'}

1.28 noop 空函数

function noop(a, b, c) {}

1.29 no 一直返回 false

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

1.30 identity 返回参数本身

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

1.31 genStaticKeys 生成静态属性

function genStaticKeys(modules) {
  return modules
    .reduce(function (keys, m) {
      return keys.concat(m.staticKeys || []);
    }, [])
    .join(",");
}
genStaticKeys([{ staticKeys: ["a", "b", "c"] }, { staticKeys: ["d", "e"] }]);
// a,b,c,d,e

1.32 looseEqual 宽松相等

由于数组、对象等是引用类型,所以两个内容宽松相等,严格相等都是不相等。

var a = {};
var b = {};
a === b; // false
a == b; // 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;
  }
}

1.33 looseIndexOf 宽松的 indexOf

原生的indexOf判断是严格相等的,该函数实现的是宽松相等的 indexOf。

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

1.34 once 确保函数只执行一次

利用闭包特性,存储状态

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

const fn1 = once(function () {
  console.log("无论几次调用,只执行一次");
});

fn1(); // '无论几次调用,只执行一次'
fn1(); // 不输出
fn1(); // 不输出
fn1(); // 不输出

1.35 生命周期等

Vue 生命周期

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

原文链接:https://juejin.cn/post/7024276020731592741

© 2025 Niko Xie