博客更新地址啦~,欢迎访问:https://jerryyuanj.github.io/
俗话说,工欲善其事,必先利其器。在我们开始探究vue核心功能之前,先来学习一下vue源码中全局的工具函数,看看vue是如何“利其器”的。
注意,这里的工具函数对应的是src/shared/
下的util.js
,这些函数都是全局通用的,不涉及具体模块(响应式原理啊, 编译啊之类的)。所以介绍的时候仅从函数本身功能的角度出发来解析。阅读本篇文章的之前,你应该有良好的js基础,对于一些概念:原型,闭包,函数柯里化等都有一定的了解。
另一个建议是,你最好先了解一下flow
的基本语法,它是vue2用来做代码静态检查的。由于vue3中即将使用typescript来重写,所以这里也不对flow做过多介绍了。去官网看看基本使用,会对你阅读vue2的源码有帮助。传送门: https://flow.org/
废话不多说,开始吧。
export const emptyObject = Object.freeze({})
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
undefined
或者null
类型,满足其一就表示它已定义,返回true。export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
undefined
类型且不是null
类型。export function isTrue (v: any): boolean %checks {
return v === true
}
export function isFalse (v: any): boolean %checks {
return v === false
}
// Check if value is primitive
export function isPrimitive (value: any): boolean %checks {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
Number
,Undefined
,Null
,String
,Boolean
,Object
,Symbol
/**
Quick object check - this is primarily used to tell
Objects from primitive values when we know the value is a JSON-compliant type.
*/
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
obj !== null
呢?因为虽然在js中Null
与Object
是两种数据类型,但是使用typeof
操符号的结果是一样的,即 typeof null === 'object'
, 所以这里要先排除null
值的干扰上面几个
isXXX
名称的函数,源码中有这样的一行注释these helpers produces better vm code in JS engines due to their explicitness and function inlining
,主要是说这几个函数是用来各司其职的检查数据类型的。我们也可以借鉴这种写法,写业务逻辑的时候,应该拆分成最小的单元,这样各单元功能明确,代码可读性也更高。
const _toString = Object.prototype.toString
// Get the raw type string of a value e.g. [object Object]
export function toRawType (value: any): string {
return _toString.call(value).slice(8, -1)
}
作用: 获取一个值的原始类型字符串
拓展: 在任何值上调用Object原生的toString方法,都会返回一个[object NativeConstructorName]
格式的字符串。每个类在内部都有一个[[Class]]
的内部属性,这个属性就指定了这个NativeConstructorName
的名称。例如
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(1) // "[object Number]"
...
所以上述 toRawType 函数实际上是把 [object Number]
这个字符串做了截取,返回的是类型值,如 "Number", "Boolean", "Array"
等
// Strict object type check. Only returns true for plain JavaScript objects.
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
作用: 严格的类型检查,只在是简单js对象返回true
拓展: 为什么特意加一句 Only returns true for plain JavaScript objects.
呢?因为有一些值,虽然也属于js中的对象,但是有着更精确的数据类型,比如:
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(()=>{}) // "[object Function]"
Object.prototype.toString.call(null) // "[object Null]"
...
export function isRegExp (v: any): boolean {
return _toString.call(v) === '[object RegExp]'
}
作用: 检查一个值是不是正则表达式
拓展: 正则表达式不是对象吗?为什么不能直接使用typeof
操作符检查呢? 这主要是处于兼容不同浏览器的考虑:
typeof /s/ === 'function'; // Chrome 1-12 Non-conform to ECMAScript 5.1
typeof /s/ === 'object'; // Firefox 5+ Conform to ECMAScript 5.1
参考: typeof | MDN
// Check if val is a valid array index.
export function isValidArrayIndex (val: any): boolean {
const n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val)
}
// Convert a value to a string that is actually rendered.
export function toString (val: any): string {
return val == null
? ''
: typeof val === 'object'
? JSON.stringify(val, null, 2)
: String(val)
}
==
而不是 ===
, 所以当值为undefined
或者null
时都会返回空串// Convert a input value to a number for persistence.If the conversion fails, return original string.
export function toNumber (val: string): number | string {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
// Make a map and return a function for checking if a key is in that map.
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
// 先创建一个map,用来存放每一项的数据
const map = Object.create(null)
// 获取元素的集合
const list: Array<string> = str.split(',')
// 遍历元素数组,以 键=元素名, 值=true 的形式,存进map中
// 即如果 str = 'hello, world', 那么这个循环后
// map = { 'hello': true, 'world': true}
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
// 如果需要小写(expectsLowerCase = true)的话,就将 val 的值转换成小写再检查; 否则直接检查
// 这里的检查,就是检查传入的 val(键), 是不是在map中,在的话会根据该键名,找到对应的值(true)
// 这里返回的是一个函数,该函数接收一个待检查的键名称,返回查找结果(true/undefined)
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
// Check if a tag is a built-in tag.
export const isBuiltInTag = makeMap('slot,component', true)
作用: 检查一个标签是不是vue的内置标签
解析: 从上面的makeMap函数可以知道,这个isBuiltInTag是一个函数,接收一个值作为查找的键名,返回的是查找结果。通过这个makeMap('slot,component', true)
处理后,map的值已经变成
{
"slot": true,
"component": true
}
而且是检查小写,也就是说"Slot", "Component"
等格式的都会返回true, 如下:
isBuiltInTag('slot') // true
isBuiltInTag('Slot') // true
isBuiltInTag('CoMponeNt') //true
isBuiltInTag('kobe') // undefined(注意它是一个falsy的值,但是不是false)
// Check if a attribute is a reserved attribute.
export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
// Remove an item from an array
export function remove (arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
// Check whether the object has the property.
const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
hasOwnProperty
方法,反而要找对象原型上的呢?原因其实很简单,因为对象上的hasOwnProperty
是可以被改写的,万一被重写了方法就无法实现这种检查了。也可以这么简写:
({}).hasOwnProperty.call(obj, key)
// Create a cached version of a pure function.
export function cached<F: Function> (fn: F): F {
// const cache = {}
// 这个const相当于一个容器,盛放着 key-value 们
const cache = Object.create(null)
// 返回的是一个函数表达式cacheFn
return (function cachedFn (str: string) {
const hit = cache[str]
// 如果命中了,那么拿缓存值; 反之就是第一次执行计算,调用函数fn计算并且装入 cache 容器
// cache 容器中的键值对, key是函数入参, value是函数执行结果
return hit || (cache[str] = fn(str))
}: any)
}
// Camelize a hyphen-delimited string.
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
作用: 将连字符-
连接的字符串转化成驼峰标识的字符串
解析: 可以通过此例来感受下上面的cached
函数的作用,因为cache的参数, 一个转换连字符字符串的函数,是一个很纯很纯的函数,所以可以把它缓存起来。
使用:
camelize('hello-world') // 'HelloWorld'
// Capitalize a string.
export const capitalize = cached((str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
作用: 将一个字符串的首字母大写后返回,也是可以被缓存的函数。
使用:
capitalize('hello') // 'Hello'
// Hyphenate a camelCase string.
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
})
作用: 将一个驼峰标识的字符串转换成连字符-
连接的字符串
使用:
hyphenate('HelloWorld') // 'hello-world'
/**
* 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 more performant in most browsers, but removing it would be breaking for
* code that was able to run in PhantomJS 1.x, so this must be kept for
* backwards compatibility.
**/
function polyfillBind (fn: Function, ctx: Object): Function {
function boundFn (a) {
// 获取函数参数个数
// 注意这个arguments是boundFn的,不是polyfillBind的
const l = arguments.length
// 如果参数不存在,直接绑定作用域调用该函数
// 如果存在且只有一个,那么调用fn.call(ctx, a), a是入参
// 如果存在且不止一个,那么调用fn.apply(ctx, arguments)
return l
? l > 1 ?
fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length
return boundFn
}
作用: bind 函数的简单的 polyfill。因为有的环境不支持原生的 bind, 比如: PhantomJS 1.x。技术上来说我们不需要这么做,因为现在大多数浏览器都支持原生bind了,但是移除这个吧又会导致在PhantomJS 1.x 上的代码出错,所以为了向后兼容还是留住。
拓展: call与apply的区别,call接受参数是一个一个接收,apply是作为数组来接收。如:
fn.call(this, 1,2,3)
fn.apply(this, [1,2,3])
function nativeBind (fn: Function, ctx: Object): Function {
return fn.bind(ctx)
}
export const bind = Function.prototype.bind
? nativeBind
: polyfillBind
// Convert an Array-like object to a real Array.
export function toArray (list: any, start?: number): Array<any> {
// start为开始拷贝的索引,不传的话默认0,代表整个类数组元素的的转换
start = start || 0
let i = list.length - start
const ret: Array<any> = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}
// Mix properties into target object.
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
// Merge an Array of Objects into a single Object.
export function toObject (arr: Array<any>): Object {
// 定义一个目标对象
const res = {}
// 遍历对象数组
for (let i = 0; i < arr.length; i++) {
if (arr[i]) {
// 遍历这个对象,将属性都拷贝到res中
extend(res, arr[i])
}
}
return res
}
作用: 将一个对象数组合并到一个单一对象中去。
解析: 函数的输入和输出是这样的:
const arr = [{age: 12}, {name: 'jerry', age: 24}, {major: 'js'}]
const res = toObject(arr)
console.info(res) // {age: 24, name: 'jerry', major: 'js'}
// 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/)
export function noop (a?: any, b?: any, c?: any) {}
// Always return false.
export const no = (a?: any, b?: any, c?: any) => false
// Return same value
export const identity = (_: any) => _
// Generate a static keys string from compiler modules.
export function genStaticKeys (modules: Array<ModuleOptions>): string {
return modules.reduce((keys, m) => {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}
这个函数的作用,我目前不是很清楚编译器模块是个什么东西…就函数本身功能而言,接收一个对象数组,然后取出其中的
staticKeys
的值(是个数组), 拼成一个keys
的数组,再返回这个数组的字符串形式,用,
连接的,如:'hello,wolrd,vue'
// Check if two values are loosely equal - that is,
// if they are plain objects, do they have the same shape?
export function looseEqual (a: any, b: any): boolean {
// 如果全等,返回true
if (a === b) return true
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if (isObjectA && isObjectB) {
// 如果 a 和 b 都是对象的话
try {
const isArrayA = Array.isArray(a)
const isArrayB = Array.isArray(b)
// 如果 a 和 b 都是数组
if (isArrayA && isArrayB) {
// 长度相等 且 每一项都相等(递归)
return a.length === b.length && a.every((e, i) => {
return looseEqual(e, b[i])
})
} else if (!isArrayA && !isArrayB) {
// 如果 a 和 b 都不是数组
const keysA = Object.keys(a)
const keysB = Object.keys(b)
// 两者的属性列表相同,且每个属性对应的值也相等
return keysA.length === keysB.length && keysA.every(key => {
return looseEqual(a[key], b[key])
})
} else {
// 不满足上面的两种都返回 false
return false
}
} catch (e) {
// 发生异常了也返回 false
return false
}
} else if (!isObjectA && !isObjectB) {
// 如果 a 和 b 都不是对象,则转成String来比较字符串来比较
return String(a) === String(b)
} else {
// 其余返回 false
return false
}
}
loosely equal
呢?因为两个对象是不相等的,这里比较的只是内部结构和数据。代码详解见注释。export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
for (let i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) return i
}
return -1
}
looseEqual
的一个使用场景。// Ensure a function is called only once.
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
called
这个变量决定的。第一次调用后这个值就被设置成true
了,之后再调用,由于闭包的存在,called
这个标记变量会被访问到,这时已经是true
了,就不会在调用了。