俗话说,工欲善其事,必先利其器。在我们开始探究vue核心功能之前,先来学习一下vue源码中全局的工具函数,看看vue是如何“利其器”的。
注意,这里的工具函数对应的是src/shared/下的util.js,这些函数都是全局通用的,不涉及具体模块(响应式原理啊, 编译啊之类的)。所以介绍的时候仅从函数本身功能的角度出发来解析。阅读本篇文章的之前,你应该有良好的js基础,对于一些概念:原型,闭包,函数柯里化等都有一定的了解。
另一个建议是,你最好先了解一下flow的基本语法,它是vue2用来做代码静态检查的。由于vue3中即将使用typescript来重写,所以这里也不对flow做过多介绍了。去官网看看基本使用,会对你阅读vue2的源码有帮助。传送门: https://flow.org/
废话不多说,开始吧。
emptyObject
export const emptyObject = Object.freeze({})
作用: 创建一个不可修改的对象
拓展: 参考之前的博客,对象的三个安全级别
地址: https://blog.csdn.net/qq_25324335/article/details/79859407#t2
isUndef
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
作用: 检查一个值是不是没有定义,这里检查了它是undefined或者null类型,满足其一就表示它已定义,返回true。
isUndef
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
作用: 检查一个值是不是定义了,必须同时满足它不是undefined类型且不是null类型。
isUndef
export function isTrue (v: any): boolean %checks {
return v === true
}
作用: 检查一个值是不是true
isUndef
export function isFalse (v: any): boolean %checks {
return v === false
}
作用: 检查一个值是不是false
isPrimitive
// 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'
)
}
作用: 检查一个值的数据类型是不是简单类型(字符串/数字/symbol/布尔)
拓展: js中共有7种数据类型: Number,Undefined,Null,String,Boolean,Object,Symbol
isObject
/**
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,主要是说这几个函数是用来各司其职的检查数据类型的。我们也可以借鉴这种写法,写业务逻辑的时候,应该拆分成最小的单元,这样各单元功能明确,代码可读性也更高。
toRawType
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"等
isPlainObject
// 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]"
...
isRegExp
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
isValidArrayIndex
// 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)
}
作用: 检查一个值是不是合法的数组索引,要满足:非负数, 整数, 有限大
拓展: 这主要是检查外来的值作为数组的索引的情况。
toString
// 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时都会返回空串
JSON.stringify接收三个参数,分别是要序列化的值, 要序列化的属性, 序列化所需的空格
参考: JSON.stringify() | MDN
toNumber
// 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
}
作用: 把一个值转换成数字,转化成功,返回数字,否则原样返回。
makeMap
// 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
// 遍历元素数组,以 键=元素名, 值=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]
}
作用: 生成一个map,返回一个函数来检查一个键是不是在这个map中。详解见注释。
拓展:
函数柯里化
闭包
isBuiltInTag
// 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)
isReservedAttribute
// Check if a attribute is a reserved attribute.
export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
作用: 检查一个属性是不是vue的保留属性。同上。
remove
// Remove an item from an array
export function remove (arr: Array
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
作用: 移除数组的某一项。
hasOwn
// 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)
}
作用: 检查一个对象是否包含某属性, 这个属性不包括原型链上的属性(toString之类的)。
拓展: 这里要思考的是,为什么不直接调用对象上的hasOwnProperty方法,反而要找对象原型上的呢?原因其实很简单,因为对象上的hasOwnProperty是可以被改写的,万一被重写了方法就无法实现这种检查了。
也可以这么简写: ({}).hasOwnProperty.call(obj, key)
cached
// Create a cached version of a pure function.
export function cached
// 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)
}
作用: 为一个纯函数创建一个缓存的版本。
解析: 因为一个纯函数的返回值只跟它的参数有关,所以我们可以将入参作为key,返回值作为value缓存成key-value的键值对,这样如果之后还想获取之前同样参数的计算结果时,不需要再重新计算了,直接获取之前计算过的结果就行。
拓展: 纯函数:一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,则该函数可以称为纯函数。更多用法请百度。
camelize
// 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
// Capitalize a string.
export const capitalize = cached((str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
作用: 将一个字符串的首字母大写后返回,也是可以被缓存的函数。
使用:
capitalize('hello') // 'Hello'
1
hyphenate
// 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'
polyfillBind
/**
* 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])
nativeBind
function nativeBind (fn: Function, ctx: Object): Function {
return fn.bind(ctx)
}
作用: 原生的bind。
bind
export const bind = Function.prototype.bind
? nativeBind
: polyfillBind
作用: 导出的bind函数,如果浏览器支持原生的bind则用原生的,否则使用polyfill版的bind。
toArray
// Convert an Array-like object to a real Array.
export function toArray (list: any, start?: number): Array
// start为开始拷贝的索引,不传的话默认0,代表整个类数组元素的的转换
start = start || 0
let i = list.length - start
const ret: Array
while (i--) {
ret[i] = list[i + start]
}
return ret
}
作用: 将类数组的对象转换成一个真正的数组。实际上就是一个元素逐一复制到另一个数组的过程。
extend
// Mix properties into target object.
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
作用: 将属性混入到目标对象中,返回被增强的目标对象。
toObject
// Merge an Array of Objects into a single Object.
export function toObject (arr: Array
// 定义一个目标对象
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'}
noop
// 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) {}
作用: 一个空函数。在这里插入的参数,是为了避免 Flow 使用 rest操作符(…) 产生无用的转换代码。
no
// Always return false.
export const no = (a?: any, b?: any, c?: any) => false
作用: 总是返回false
identity
// Return same value
export const identity = (_: any) => _
作用: 返回自身。
genStaticKeys
// Generate a static keys string from compiler modules.
export function genStaticKeys (modules: Array
return modules.reduce((keys, m) => {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}
作用: 从编译器模块中生成一个静态的 键 字符串。
这个函数的作用,我目前不是很清楚编译器模块是个什么东西…就函数本身功能而言,接收一个对象数组,然后取出其中的 staticKeys 的值(是个数组), 拼成一个keys的数组,再返回这个数组的字符串形式,用,连接的,如:'hello,wolrd,vue'
looseEqual
// 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呢?因为两个对象是不相等的,这里比较的只是内部结构和数据。代码详解见注释。
looseIndexOf
export function looseIndexOf (arr: Array
for (let i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) return i
}
return -1
}
作用: 返回一个元素在数组中的索引,-1表示没找到。方法很简单,就是上面looseEqual的一个使用场景。
once
// 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了,就不会在调用了。
---------------------
作者:袁杰Jerry
来源:CSDN
原文:https://blog.csdn.net/qq_25324335/article/details/84948363
版权声明:本文为博主原创文章,转载请附上博文链接!