第24期:vue.js工具函数源码解读

430 阅读2分钟

第24期:vue.js工具函数源码解读

一、学习前言

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

1.2 源码阅读辅助文档:juejin.cn/post/702427…

1.3 在线调试地址:github1s vue/vue/src/shared

二、学习目标

2.1 学习vue中的工具函数,学以致用

三、源码解读

3.1 Object.freeze({})

用来表示一个冻结对象(数组也是对象),当前对象的第一层不可变更。对象中有判断对象是否冻结的方法

const obj = Object.freeze({
  name: 11,
  age: 18
})
console.log(obj.age); // 18
obj.age = 20
console.log(obj)  // 18
console.log(Object.isFrozen(obj)) // true

Tips: vue项目中通常会将一些属性/方法挂载在vue原型上,也可以使用Object.freeze(),防止挂载的属性被重写

Object.freeze(Vue.prototype.$apis)

3.2 isUndef isDef isTrue isFalse

// 是否未定义
function isUndef(val) {
  return val === undefined || val === null
}
// 是否已定义
function isDef(val) {
  return val !== undefined && val !== null
}
// 是否是true
function isTrue(val) {
  return val === true
}
// 是否是false
function isFalse(val) {
  return val === false
}

3.3 Check if value is primitive. 判断数据是不是原始类型

function isPrimitive(value){
  return (typeof value === String || typeof value === Number || typeof value === Boolean || typeof value === Symbol)
}

Tips:

  1. JS中的值有两种类型:原始类型(Primitive)、对象类型(Object)。
  2. 原始类型包括:Undefined、Null、Boolean、Number、String、Symbol(es6)等。

3.4 isObject 快速判断是否是对象

function isObject(val) {
  return val !== null && typeof val === 'object'
}
typeof null === 'object' // true
isObject([])  // true
isObject({})  // true

3.5 Get the raw type string of a value, e.g., [object Object]. // 获取数据的原始类型字符串

// Object.prototype.toString() 方法返回一个表示该对象的字符串。
var _toString = Object.prototype.toString
function toRawType (value) {
  return _toString.call(value).slice(8, -1)
}
// eg:
toRawType(new Date()) // Date
toRawType('')         // String

3.6 isPlainObject 判断是否是纯对象

function isPlainObject(value) {
  return _toString.call(value) === '[object Object]'
}
// 这个可以用来区分数组和纯对象
isPlainObject([])  // false
isPlainObject({})  // true

3.7 isRegExp 是否是正则表达式

function isRegExp(value) {
  return _toString.call(value) === '[object RegExp]'
}
isRegExp(/\D/) // true

3.8 Check if val is a valid array index. 是否是有效的数组索引值

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

Tips: parseFloat() 函数 isFinite() 函数

parseFloat(string)          // 函数解析字符串并返回浮点数。
parseFloat(111)             // 111
parseFloat('222')           // 222
parseFloat('333 is number') // 333
parseFloat('  20  ')        // 20
parseFloat('to number 40')  // NaN
// tips: 只返回字符串中的第一个数字!允许前导和尾随空格。如果第一个字符不能转换为数字,parseFloat() 返回 NaN。

isFinite(testValue)  // 用来检测一个值是否为有限值
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
// tips: isFinite 方法检测它参数的数值。如果参数是 NaN,正无穷大或者负无穷大,会返回false,其他返回 true。

3.9 isPromise 是否是promise对象

function isPromise(val) {
  return isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function'
}
isPromise(new Promise())          // 报错 TypeError: Promise resolver undefined is not a function
isPromise(new Promise(() => {}))  // true promise有两个参数,resolve && reject

3.10 Convert a value to a string that is actually rendered. 将值转换为实际呈现的字符串

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

Tips: JSON.stringify(val, null, 2) 点击查阅MDN讲解

// 语法: JSON.stringify(value[, replacer [, space]])
// value 将要序列化成 一个 JSON 字符串的值。
// replacer(可选) [Function,Array,Null] 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
// space(可选) 指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为10。该值若小于1,则意味着没有空格;如果该参数为字符串(当字符串长度超过10个字母,取其前10个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var arr = [1,'error',333]
function replacer(key, value) {
  if (typeof value === "string") {
    return undefined;
  }
  return value;
}
JSON.stringify(arr,replacer,2)
// [
//   1,
//   null,
//   333
// ]
// 注意: 不能用 replacer 方法,从数组中移除值(values),如若返回 undefined 或者一个函数,将会被 null 取代。
JSON.stringify(foo,replacer,2)
// {
//   "week": 45,
//   "month": 7
// }

3.11 toNumber

function toNumber(val) {
  var n = parseFloat(val)   // 将字符串转转化返回浮点数
  return isNaN(n) ? val : n // isNaN是否为非数字; 转换失败,返回原值
}

toNumber('23.30')   // 23.3
toNumber('  50  ')  // 50
toNumber('1s')      // 1

3.12 makeMap

/**
 * @description: 生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 map 中。
 * @param {String} str                // ,号拼接的字符串
 * @param {Boolean} expectsLowerCase  // 是否大小写兼容,可选
 * @return {Function}
 */
function makeMap(str, expectsLowerCase) {
  const map = Object.create(null);
  const list = str.split(",");
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true;
  }
  return expectsLowerCase
    ? function (val) {
        return map[val.toLowerCase()];
      }
    : function (val) {
        return map[val];
      };
}
makeMap("name,age,sex", true)("Name") // true
makeMap("name,age,sex")("name")       // true

Tips: Object.create(null)创建的对象不继承Object原型链上的属性.
使用场景:
1、你需要一个非常干净且高度可定制的对象当作数据字典的时候;
2、减少hasOwnProperty造成的性能损失并且可以偷懒少些一点代码的时候
3、其他的时候,请用{}

3.13 isBuiltInTag: Check if a tag is a built-in tag.(检查标签是否为内置标签)

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

3.14 isReservedAttribute: Check if an attribute is a reserved attribute.(检查属性是否为保留属性)

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

3.15 remove: Remove an item from an array.(移除数组中的某一项)

/**
 * @description: 移除数组中的某一项,耗费性能,会改变原数组
 * @param {Array} arr // 操作的数组数据
 * @param {Any} item  // 数组的某一项元素
 * @return {Array}    // 删除的元素数组
 */
function remove(arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}

3.16 hasOwn: Check whether an object has the property.(判断是否是自有属性)

const hasOwnproperty = Object.prototype.hasOwnProperty; // 判断是否是自有属性
function hasOwn(obj, key) {
  return hasOwnproperty.call(obj, key);
}

Tips: hasOwnProperty 点击查阅详解

const obj = {
  name: "娃哈哈",
};
// 也可以用当前对象的hasOwnProperty判断属性,如果被重写过,结果会不准确
obj.hasOwnProperty("name")  // true
obj.hasOwnProperty("age")   // false
// hasOwnProperty如果被重写,此处会一直返回false,为了避免此种情况,最好是使用Object 原型上的 hasOwnProperty 属性
obj.hasOwnProperty = function () {
  return false;
};
obj.hasOwnProperty("name")  // false
hasOwn(obj, "name")         // true

3.17 cached: Create a cached version of a pure function. 利用闭包特性,缓存数据

// 利用闭包缓存数据
function cached(fn) {
  var cache = Object.create(null); // 定义一个没有原型的对象,比 {} 高效,不需要继承一大堆 Object.prototype 上的属性。
  return function cachedFn(str) {
    var hit = cache[str];
    console.log('hit', hit)
    return hit || (cache[str] = fn(str)); // 如果数据已缓存,直接返回。未缓存,将fn 函数返回值缓存
  };
}

// 转换成大写操作
const fn = function (str) {
  return str.toUpperCase()
}
// 调用 cached 时传入一个 fn 函数,这个函数对某些值进行操作,操作之后会产生返回值
const cachedTest = cached(fn)
// 执行完 cached 会返回一个函数 cachedFn,将来接收需要操作的值。函数 cachedFn 内部调用 fn 函数得到操作后的值,
// 并缓存在对象 cache 中,如果再对同一个值进行操作时,则直接从缓存中取,无需再调用函数计算。
cachedTest('name')
// hit undefined
// NAME
cachedTest('name')
// hit NAME 此处清楚看到是取得缓存数据
// NAME
cachedTest('age')
// hit undefined
// AGE

3.18 短横线连接字符转换成小驼峰

var camelizeRE = /-(\w)/g;
'tab-name'.match(camelizeRE)
console.log(RegExp.$1); // n   正则中的捕获分组
var camelize = cached(function (str) {
  // replace(regexp/substr,replacement)方法
  return str.replace(camelizeRE, function (_, c,idx,obj) {
    // replacement[String,Function] 可以是字符串也可以是函数
    console.log(_);     // -n  该函数的第一个参数是匹配模式的字符串
    console.log(c);     // n   接下来的参数是与模式中的子表达式匹配的字符串,可以有 0 个或多个这样的参数
    console.log(idx);   // 3   声明了匹配在 stringObject 中出现的位置
    console.log(obj);   // tab-name 最后一个参数是 stringObject 本身
    return c ? c.toUpperCase() : '' // c?.toUpperCase() babel编译后也是多元表达式
  });
});
camelize('tab-name') // tabName

3.19 首字符大写

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

capitalize('chat') // Chat
charAt(index)  //返回字符串中特定位置的字符。index超出字符串的长度,则返回''

3.20 小驼峰命名转换为连字符

// 小驼峰命名变为连字符
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
  return str.replace(hyphenateRE, "-$1").toLowerCase();
});

hyphenate('tabName')          // tab-name
'tabName'.match(hyphenateRE)  // ['N']

Tips: \B 和 \b

// \b 匹配一个单词边界,即字与空格间的位置。
// \B 非单词边界匹配。
const lowercaseRE = /\b([A-Z])/g
const capitalizeRE = /\B([A-Z])/g
'tabName'.match(lowercaseRE)    // null
'tabName'.match(capitalizeRE)   // ['N']
'tab Name'.match(lowercaseRE)   // ['N']
'tab Name'.match(capitalizeRE)  // null

3.21 polyfillBind 兼容老版本浏览器不支持原生的 bind 函数

function polyfillBind(fn, ctx) {
  function boundFn(a) {
    console.log(a);
    var l = arguments.length;
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)  //参数多适合用 apply,少用 call 性能更好。
        : 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

3.22 类数组转换为数组

// 类数组转换为真实数组
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 sliceToArray(likeArr) {
  // slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。
  // 原始数组不会被改变。
  return Array.prototype.slice.call(likeArr);
}

function fromToArray(likeArr) {
  // es6 新增
  // Array.from()方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
  return Array.from(likeArr)
}

function ofToArray(likeArr) {
  // es6 新增
  // Array.of() 方法创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。
  return Array.of(likeArr)
}

function extendToArray(likeArr) {
  // es6 新增 扩展运算符
  return [...likeArr]
}

(function likeFn(name, age, sex) {
  console.log(_toString.call(arguments));               // [object Arguments]
  console.log(_toString.call(toArray(arguments, 0)));   // [object Array]
  console.log(_toString.call(sliceToArray(arguments))); // [object Array]
  console.log(_toString.call(fromToArray(arguments)));  // [object Array]
  console.log(_toString.call(ofToArray(arguments)));    // [object Array]
  console.log(_toString.call(extendToArray(arguments)));// [object Array]
})("张三", 18, "男");

Tips: 类数组、数组的区别 类数组的定义:
1、拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理);
2、不具有数组所具有的方法;
3、类数组是一个普通对象,而真实的数组是Array类型。
常见的类数组有:
1、函数的参数arugments
2、DOM对象列表(比如通过 document.querySelectorAll 得到的列表)
3、jQuery 对象 (比如 $("div"))

3.21 extend合并

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

let toObj = {
  name: "小红",
  age: 20,
};
let fromObj = {
  name: "小红",
  sex: "女",
};
extend(toObj, fromObj) // {name: '小红', age: 20, sex: '女'}

3.22 toObject 数组转对象

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

3.23 noop 空函数

function noop (a, b, c) {}

四、学习总结

4.1 阅读断断续续,隔两天来接着读和写,缺少了连贯性,反思。

4.2 衍生知识点在3中Tips展示。