Vue.js源码中定义了一些工具方法,浏览器端、服务端的Vue.js都可共享这些方法
Vue 2.x
基于2.6.14
-
具体路径:src\shared\util.js
-
背景:基于flow构建类型系统
-
目的:帮助程序在JS引擎中生成更好的VM代码,利于性能优化,提高效率
类型判断
isUndef & isDef
判断是否为undefined或null
function isUndef (v) {
return v === undefined || v === null
}
function isDef (v) {
return v !== undefined && v !== null
}
isTrue & isFalse
判断真假与否
function isTrue (v) {
return v === true
}
function isFalse (v) {
return v === false
}
isPrimitive
数据类型是否为基础类型(部分)
function isPrimitive (value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
eg:src\core\vdom\create-element.js 的 createElement,进行判断
if (Array.isArray(data) || isPrimitive(data)) {//……}
isObject
是否为引用类型或通过由new
构造的类型
此方法在Vue中用于快速对象检查:当已知值是符合JSON的类型时,用于从该值中识别对象
function isObject (obj) {
return obj !== null && typeof obj === 'object' // 注意 null 在 typeof 的坑
}
isPromise
是否为 Promise 函数
function isPromise (val) {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
eg:src\core\util\error.js 的 invokeWithErrorHandling,用于处理异步异常
if (res && !res._isVue && isPromise(res) && !res._handled) {//……}
toRawType
获取值的原始类型字符串
基于Object.prototype.toString.call
, 除自定义类型外,几乎通用。返回:`[object, 类型]
var _toString = Object.prototype.toString;
function toRawType (value) {
return _toString.call(value).slice(8, -1)
}
eg:src\core\util\options.js 的 normalizeProps,规范化props属性
// props属性类型不符合要求,常见的错误
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
hasOwnProperty
判断对象是否存在指定属性
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
数据转换
toNumber
转换为数字
// 将值转换为数字。若转换失败,返回原始字符串
function toNumber (val) {
var n = parseFloat(val);
return isNaN(n) ? val : n
}
toString
转换为字符串
function toString (val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2) // 针对数组或对象
: String(val)
}
其中,JSON.stringify(val, null, 2),所代表的语句为:JSON.stringify(value[, replacer [, space]])
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
}
eg: src\core\global-api\use.js 的 initUse
const args = toArray(arguments, 1) // arguments可理解传递给函数的参数的类数组对象
将类数组对象转换为数组,还有以下方法:
(1)Array.prototype.slice.call
(2)Array.from()
toObject
将对象数组合并到对象中
// 将属性赋予目标对象
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}
function toObject (arr) {
var res = {};
for (var i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i]);
}
}
return res
}
eg:src\platforms\weex\runtime\modules\style.js 的 updateStyle
if (Array.isArray(style)) {
style = vnode.data.style = toObject(style)
}
已知,v-bind:style
的数组语法可以将多个样式对象应用到同一个元素上
capitalize
首字母大写
// cached方法见下
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()
});
camelize
短横线分隔变驼峰式
var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});
缓存数据
cached
传参是函数,返回也是函数,用于缓存创建的函数,上述的capitalize、hyphenate均使用了cached方法
为什么要这样做?尤大大的巧思:利于性能优化。
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
var hit = cache[str]; // 进行缓存
return hit || (cache[str] = fn(str))
})
}
makeMap
思路:定义一个数据集合,可用于判断是否为html内置标签
/**
* @description:
* @param str 待判断字符串
* @param expectsLowerCase 是否需要小写
* @return {*}
*/
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]; }
}
eg:src\platforms\web\util\element.js 的 isHTMLTag
var isHTMLTag = makeMap(
'html,body,base,head,link,meta,style,title,' +
'......' +
'content,element,shadow,template,blockquote,iframe,tfoot'
)
isHTMLTag('iframe') // true
判断对象是否相等
looseEqual
整体思路:先进行类型判断,再递归调用
function looseEqual (a, b) {
// a、b恒等于,返回true
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) {
// 若两者为数组,当长度一致时再递归判断每个数据项,不一致时返回false
return a.length === b.length && a.every(function (e, i) {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {
// 若两者为Date类型,利用时间戳判断
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
// 若两者为对象,当长度一致时再递归判断每个属性值是否相同,不一致时返回false
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
}
}
其他
remove
删除数组项
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
once
函数只执行一次
function once (fn) {
var called = false;
return function () {
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}
Vue 3.x
基于3.2.6
-
背景: 类型检验基于
TypeScript
,舍弃flow,不得不提到Vue2.x 引人注目的注释
// src\core\instance\lifecycle.js
const propOptions: any = vm.$options.props // wtf flow?
// src\platforms\web\server\modules\dom-props.js
// $flow-disable-line (WTF?)
const attr = propsToAttrMap[key] || key.toLowerCase()
TypeScript vs Flow,尤大大后续也真香了,传送门
保留 & 补充
-
cacheStringFunction:保留,即原cached
-
makeMap:保留
-
looseEqual:保留,改写优化,补充 looseCompareArrays 方法,比较两数组是否相等
// 源码:packages\shared\src\looseEqual.ts
function looseCompareArrays(a, b) {
// 若长度不一致,返回false
if (a.length !== b.length)
return false;
let equal = true;
// 若长度一致,再逐个数组项进行对比
for (let i = 0; equal && i < a.length; i++) {
equal = looseEqual(a[i], b[i]);
}
return equal;
}
-
数据转换
-
移除toArray、toObject
其中toArray用Array.prototype.slice.call和Array.from代替
-
补充toTypeString,对象转字符串
const objectToString = Object.prototype.toString; const toTypeString = (value) => objectToString.call(value); toTypeString({num: 0}) // "[object Object]"
-
-
类型判断:补充调整,更为细化
const isArray = Array.isArray;
const isMap = (val) => toTypeString(val) === '[object Map]';
const isSet = (val) => toTypeString(val) === '[object Set]';
const isDate = (val) => val instanceof Date;
const isFunction = (val) => typeof val === 'function';
const isString = (val) => typeof val === 'string';
const isSymbol = (val) => typeof val === 'symbol';
const isObject = (val) => val !== null && typeof val === 'object';
const isPromise = (val) => {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};
// ……
新增
getGlobalThis
获取全局this指向对象
let _globalThis;
const getGlobalThis = () => {
// 若_globalThis已被定义(已执行方法),则直接返回,无需再次判断
return (_globalThis ||
(_globalThis =
typeof globalThis !== 'undefined'
? globalThis
: typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}));
};
def
定义对象属性
const def = (obj, key, value) => {
Object.defineProperty(obj, key, {
configurable: true, // 可配置的
enumerable: false, // 不可枚举的
value
});
};
isModelListener
判断当前字符串是否以onUpdate:
开头
const isModelListener = (key) => key.startsWith('onUpdate:');
String.prototype.startsWith(searchString[, position]):返回布尔值,表示参数字符串是否在原字符串的头部。
isIntegerKey
判断是否为整数键值
const isIntegerKey = (key) => isString(key) &&
key !== 'NaN' &&
key[0] !== '-' &&
'' + parseInt(key, 10) === key;
invokeArrayFns
fns是数组,各元素均为函数,遍历数组执行,利于一次性执行多个函数
const invokeArrayFns = (fns, arg) => {
for (let i = 0; i < fns.length; i++) {
fns[i](arg);
}
};
hasChanged
比较两个值是否相等,利用Object.is,与严格比较运算符(===)的行为基本一致,但注意:
- +0不等于-0
- NaN等于自身
const hasChanged = (value, oldValue) => !Object.is(value, oldValue);
hasChanged(+0, -0) // true
hasChanged(NaN, NaN) // false
链接传动门
Last but not least
如有不妥,请多指教~