【源码共读 第24期】Vue2 工具函数

265 阅读3分钟

代码下载

仓库连接: github.com/vuejs/vue/b…

工具函数

2.1 emptyObject

Object.freeze 创建一个冻结的空对象,返回的对象,不可修改、新增、删除属性,原型也不可以操作

var emptyObject = Object.freeze({})

2.2 isUndef

判断对象是否未定义,是否 undefined 或者 null

function isUndef(v) {
    return v === undefined || v === null
}

2.3 isDef

判断对象是否已定义

function isDef(v) {
    return v !== undefined && v !== null
}

2.4 isTrue

判断是否是布尔值 true

function isTrue(v) {
    return v === true
}

2.5 isFalse

判断是否是布尔值 false

function isFalse(v) {
    return v === false
}

// js 中的假值
// 0
// null
// undefined
// false
// ""
// NaN

2.6 isPrimitive

判断是否原始类型 boolean number string symbol。

function isPrimitive(value) {
    return (
        typeof value === 'string' ||
        typeof value === 'number' ||
        // $flow-disable-line
        typeof value === 'symbol' ||
        typeof value === 'boolean'
    )
}
// mdn 的原始类型 null undefined boolean number string symbol bigint
// 由于 flow 不支持符号,而 vue 需要使用符号,因此只有在使用它们的行上禁用错误才有意义

2.7 isObject

判断是否是引用类型,null 需要单独判断 typeof null === 'object' 是 true

function isObject(obj) {
    return obj !== null && typeof obj === 'object'
}

2.8 toRawType

通过原型上的判断 toString 方法一个对象的原始类型

var _toString = Object.prototype.toString

function toRawType(value) {
    return _toString.call(value).slice(8, -1)
}
// 从 index 8 获取到倒数第二位
// '[object String]'  String
// '[object Number]'  Number

2.9 isPlainObject

判断是否为纯对象 {xx: xx}

function isPlainObject(obj) {
    return _toString.call(obj) === '[object Object]'
}

2.10 isRegExp

判断是否为正则对象

function isRegExp(v) {
    return _toString.call(v) === '[object RegExp]'
}

2.11 isValidArrayIndex

判断是否是正确一个的下标

// isFinite 函数用来判断被传入的参数值是否为一个有限数值
// Math.floor 向下取整 5.9 --> 5  5.4 --> 5  -5.1 --> -6
// Math.round 四舍五入

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

2.12 isPromise

判断是否是 Promise 对象

// 判断已定义且有 then catch 个属性,函数类型
function isPromise(val) {
    return (
        isDef(val) &&
        typeof val.then === 'function' &&
        typeof val.catch === 'function'
    )
}

2.13 toString

重写 toString 方法

// 如果是数组或者普通对象且原型上的toString方法没有被覆盖,则直接调用JSON.stringify
function toString(val) {
    return val == null ?
        '' :
        Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ?
        JSON.stringify(val, null, 2) :
        String(val)
}
// JSON.stringify 接收3个参数,p1 需要转换的对象 p2 replacer 函数或者 [字段过滤数组] p3  指定缩进用的空白字符串
let obj = {
    name: 'kwg',
    age: 123,
    sex: '男'
}
JSON.stringify(obj) // '{"name":"kwg","age":123,"sex":"男"}'
JSON.stringify(obj, null, 2) // '{\n  "name": "kwg",\n  "age": 123,\n  "sex": "男"\n}'
JSON.stringify(
    obj,
    (key, value) => {
        console.log('key:', key, 'value:', value)
        if (key == 'name') {
            return undefined
        }
        return value
    },
    2
)
// key:  value: {name: 'kwg', age: 123, sex: '男'}
// VM2281:2 key: name value: kwg
// VM2281:2 key: age value: 123
// VM2281:2 key: sex value: 男
// '{\n  "age": 123,\n  "sex": "男"\n}'

// 在开始时,replacer 函数会被传入一个空字符串作为 key 值,代表着要被 stringify 的这个对象。随后每个对象或数组上的属性会被依次传入
// 如果返回 undefined,该属性值不会在 JSON 字符串中输出

疑问: val.toString === _toString 为什么需要判断 toString 是否被重写来执行 JSON.stringify ?

2.14 toNumber

转换为数字,转换失败则返回原始数据

function toNumber(val) {
    var n = parseFloat(val)
    return isNaN(n) ? val : n
}

2.15 makeMap

通过传入的字符串和一个是否忽略大小写的标记,解析,返回一个函数,此用于判断传入的 key 是否存在 map 中

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]
        }
}

2.16 isBuiltInTag

isBuiltInTag(val) 判断 val 是否是 slot | component, 且忽略大小写

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

2.17 isReservedAttribute

isReservedAttribute(val) 判断 val 是否是 key, ref, slot, slot-scope, is

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

2.18 remove

删除数组中的一个元素,先判断删除的下标是否存在,存在则删除对应元素

function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item)
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

2.19 hasOwn

hasOwn,拦截原型上面的 hasOwnProperty 方法,检测一个对象是否含有特定的自身属性,非原型上的继承属性

var hasOwnProperty = Object.prototype.hasOwnProperty

function hasOwn(obj, key) {
    return hasOwnProperty.call(obj, key)
}

2.20 cached

数据缓存,已经转换过的就不用再次转换了

function cached(fn) {
    var cache = Object.create(null)
    return function cachedFn(str) {
        var hit = cache[str]
        return hit || (cache[str] = fn(str))
    }
}

2.21 camelize

- 分割的字符串,转换转换驼峰

// 匹配-分割的首字母
var camelizeRE = /-(\w)/g
var camelize = cached(function(str) {
    // replace 的第二个参数是函数的话,函数第一个参数是当前匹配的字符,第二个是匹配到的字符将传入的
    return str.replace(camelizeRE, function(_, c) {
        return c ? c.toUpperCase() : ''
    })
})

2.22 capitalize

首字母大写

var capitalize = cached(function(str) {
    // 将第一个字母大写,并加上剩余的字母,slice(1)从第二个字母开始截取
    return str.charAt(0).toUpperCase() + str.slice(1)
})

2.23 hyphenate

将驼峰命名用'-'连接

/**
 * \b 单词边界 '[js] lesson_01.mp4' 可以通过#拆分为 '[#js#] #lesson_01#.#mp4#'
 * \B 非单词边界  '[js] lesson_01.mp4' 可以通过#拆分为 '#[j#s]# l#e#s#s#o#n#_#0#1.m#p#4'
 * /\B([A-Z])/ 匹配非单词边界和大写字母,formItem 替换成 form-item
 */
var hyphenateRE = /\B([A-Z])/g
var hyphenate = cached(function(str) {
    return str.replace(hyphenateRE, '-$1').toLowerCase()
})

2.24 polyfillBind

bind 垫片,通过 call 和 apply 兼容不支持 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)
}

// 判断是否支持bind,支持就走原生bind,否则就走兼容方案
var bind = Function.prototype.bind ? nativeBind : polyfillBind

2.25 toArray

类数组转换为数组,从第几位开始转换。全部转换的话,可以直接使用 Array.from

// 类数组:都具有 length,都可以进行 for 循环
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
}

2.26 extend

两个对象合并, 后者覆盖前者

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

2.27 toObject

将一个对象集合的所有元素,合并成单个对象

function toObject(arr) {
    var res = {}
    for (var i = 0; i < arr.length; i++) {
        if (arr[i]) {
            extend(res, arr[i])
        }
    }
    return res
}
toObject([{
    name: 'kwg'
}, {
    age: 8
}]) // {name: 'kwg', age: 8}

2.28 noop

空函数

function noop(a, b, c) {}

2.29 no

永远返回布尔值 false

var no = function(a, b, c) {
    return false
}

2.30 identity

永远返回一个相同的值

var identity = function(_) {
    return _
}

2.31 genStaticKeys

收集所有 staticKeys,并且逗号分割

/**
 * reduce 接受一个 reducer 函数参数和一个 初始值 initialValue
 * reducer 函数有四个参数 
 *   previousValue 上次的计算值如果,初次调用默认是 array[0],initialValue 有值就是initialValue
 *   currentValue 数组中正在处理的元素,初次调用若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0],否则为 array[1]
 *   currentIndex 数组中正在处理的元素的索引
 *   array 用于遍历的数组
 */
function genStaticKeys(modules) {
    return modules
        .reduce(function(keys, m) {
            return keys.concat(m.staticKeys || [])
        }, [])
        .join(',')
}

2.32 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) {
                // 对象直接对比key长度是否一致
                // 通过用A[key] 的key对比B[key]的数值
                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) {
        // 其他类型直接转换成 string 进行比较
        return String(a) === String(b)
    } else {
        return false
    }
}

2.33 looseIndexOf

模糊查询查找一个值在数组中的index,对比原则使用 looseEqual,找不到则返回 -1

function looseIndexOf(arr, val) {
    for (var i = 0; i < arr.length; i++) {
        if (looseEqual(arr[i], val)) {
            return i
        }
    }
    return -1
}

2.34 once

确保函数只执行一次

function once(fn) {
    var called = false
    return function() {
        if (!called) {
            called = true
            fn.apply(this, arguments)
        }
    }
}

let test = once(() => {
    console.log('111')
})
test() // 111
test() // undefined
test() // undefined

总结

部分工具需要结合源码才能更深刻认识到具体的用处,结合 cached 相关部分代码了解了一下正则,功能很强大。顺便推荐一本正则的书《JavaScript 正则表达式迷你书》