携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情 >>
- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第24期,链接:juejin.cn/post/707976…
无论是在我们自己的项目中还是在开源项目中,基于复用的原则,我们都会将一些工具函数进行抽离,构建成自己的工具函数库,本篇文章就来学习vue2中的一些工具函数。跟其进行对照,完善优化自己的工具函数库。
学习大纲概览
函数学习
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等,一目了然。