【源码共读】第24期 | vue2工具函数学习
前言
-
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
-
这是源码共读的第24期,链接:juejin.cn/post/707976… 。
学习目标
- 了解工具函数的封装以及场景;
- 学习源码中优秀代码和思想,投入到自己的项目中;
- 对js基础知识查漏补缺;
util工具函数知识点
Object.freeze
不可增、删、改的空对象及其原型
export const emptyObject = Object.freeze({})
- 使用场景
- 对于不经常变动的对象,使用该方法
- 小结
- Object.freeze冻结对象,在vue中使用相当于让对象不具备响应特性
- 当前表示创建一个空对象,不允许添加或修改该对象
toString
// 所有输入的值都转为字符串类型
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
let o = {
toString:()=>{
return '333'
}
}
console.log(toString(o)); // '333'
- 为什么要做val.toString === _toString的判断呢
- 因为在Object类型的数据中,可能会对toString方法重写
- 当判断出来toString不是和本来的函数一样的情况下,就需要走String类型
- 那么在转换String类型的时候到底是如何转换的呢?
- String(val)中判断val是否是string类型,不是的话就会调用val.toString()方法将其先转化为string类
toNumber
// 字符串转数字 常用于转小数的方式如parseFloat(val).toFix(2)
export function toNumber (val: string): number | string {
const n = parseFloat(val)
return isNaN(n) ? val : n
}
- 为什么用parseFloat?
- 首先想要的是,转化为数字的有可能有小数,这时候parseInt只会保留整数,小数会被舍弃,所以选择parseFloat
- 但是,尝试了输入整数后,结果返回的也是整数,那么parseFloat是如何实现的呢?
- 经过查询parseFloat函数,发现以下几点:
- 1.parseFloat只支持string类型参数,但是当我们输入number类型的时候,也能输出结果,这是为什么呢
- 原来在parseFloat中参数先被转化成string类型了,js引擎在发现parseFloat中的参数不是string类型后,参数会调用自身的toString()方法先转化为字符串类型,然后在按照其规则执行
- 2.parseFloat函数只解析十进制,如果一个字符串中包含一个可解析的整数的数【没有小数点或者小数点后全是0】,则返回整数
JSON.stringify
JSON.stringify(value[, replacer [, space]])
- 常用场景
- 把json转为字符串存储,如localStorage.setItem(key,JSON.stringify({}))
- JSON.parse(JSON.stringify()) 进行深拷贝
- 可用来格式转化json对象
- 小结
- JSON.stringify可用来转化对象,数组,时间等
call,apply,bind
// 封装bind的兼容写法,本质上还是用call,apply代替bind的实现方法
function polyfillBind (fn: Function, ctx: Object): Function {
function boundFn (a) {
const l = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
boundFn._length = fn.length
return boundFn
}
// 原生bind的写法 通过bind的方式改变this指向
function nativeBind (fn: Function, ctx: Object): Function {
return fn.bind(ctx)
}
// bind的兼容写法
export const bind = Function.prototype.bind
? nativeBind
: polyfillBind
- 使用场景
- 改变函数的this指向
- 小结
- 都可以用来改变函数的this指向;
- 三者第一个参数都是this要指向的对象,默认指向window
- 都可以传参,apply第二个参数是数组形式,call第二三..都是参数列表,bind参数传递类似与call
- apply,call是立即执行,bind是被动执行
Object.create
/**
* Create a cached version of a pure function.
*/
export function cached<R>(fn: (str: string) => R): (sr: string) => R {
const cache: Record<string, R> = Object.create(null)
return function cachedFn(str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}
}
- 使用场景
- 新创建对象,继承以实现对象上的属性和方法,不用再次封装实现
- 小结
Object.create()
方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)- 返回一个新对象,带着指定的原型对象及其属性
- 上述代码中以null为原型对象,目的是防止原型污染攻击(防止恶意脚本向Object.protoype添加属性和方法)
typeof
export function isPrimitive(value: any): boolean {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
export function isFunction(value: any): value is (...args: any[]) => any {
return typeof value === 'function'
}
- 使用场景
- 判断数据类型【大部分基础类型判读】(null,undefined,string,boolean,symbol,number)以及function和object(不能明确区分array,Date,正则等类型)
- 小结
isPromise
export function isDef<T>(v: T): v is NonNullable<T> {
return v !== undefined && v !== null
}
export function isPromise(val: any): val is Promise<any> {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
- 使用场景
- 判断对象是否是promise
- 小结
- typeof可以判断function类型
- 通过上述图片可知Promise的prototype中,then、catch都是绑定在其原型对象上的方法
基础类型相关判断
// 判断是否是undefined 其中null和undefined才会被判定为undefined
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
// 判断是真 即不是undefined和null
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
// 判断是否是true
export function isTrue (v: any): boolean %checks {
return v === true
}
// 判断是否是false
export function isFalse (v: any): boolean %checks {
return v === false
}
//判断是否是原始函数 也就是基础类型
export function isPrimitive (value: any): boolean %checks {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
引用类型的判断
// 判断是否是对象类型,即obj不是null 类型是Object
export function isObject (obj: mixed): boolean %checks {
return obj !== null && typeof obj === 'object'
}
// tostring的简写
const _toString = Object.prototype.toString
// 判断Object的原始类型,比如array、Object、Date、RegExp等
// 其实 [object Object] slice取的是object后面的]之前的数据
export function toRawType (value: any): string {
return _toString.call(value).slice(8, -1)
}
// 判断是否是原生对象 即[object Object]
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
// 判断是否是正则
export function isRegExp (v: any): boolean {
return _toString.call(v) === '[object RegExp]'
}
单一函数
// 无论传入什么参数,都不执行
export function noop (a?: any, b?: any, c?: any) {}
// 封装一个函数,无论传入什么参数,都返回false的值
export const no = (a?: any, b?: any, c?: any) => false
// 传入什么,返回什么
export const identity = (_: any) => _
looseIndexOf是findIndex的替代品?
// 判断两个数据是否相等
export function looseEqual (a: any, b: any): boolean {
if (a === b) return true
const isObjectA = isObject(a)
const isObjectB = isObject(b)
if (isObjectA && isObjectB) {
try {
const isArrayA = Array.isArray(a)
const isArrayB = Array.isArray(b)
if (isArrayA && isArrayB) {
return a.length === b.length && a.every((e, i) => {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
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 {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b)
} else {
return false
}
}
// 找到当前val的值所在的下标,类似findIndex函数
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
}
- 总结
- looseIndexOf类似findIndex函数,都是查找当前值在数组中的下标
- 不同的是当数组是[{key:value,xxx:xxx}]这种方式的,数据想要查找某个{key:value}的时候,就不太灵活了
一些功能化函数封装
// 判断是否是有效是数组下标
export function isValidArrayIndex (val: any): boolean {
const n = parseFloat(String(val))
return n >= 0 && Math.floor(n) === n && isFinite(val) //判断一个参数是否是有限的
}
// 是否有自己的属性
const hasOwnProperty = Object.prototype.hasOwnProperty
// 判断当前对象上是否存在对应的key
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
// 是否有自己的属性
const hasOwnProperty = Object.prototype.hasOwnProperty
// 判断当前对象上是否存在对应的key
export function hasOwn (obj: Object | Array<*>, key: string): boolean {
return hasOwnProperty.call(obj, key)
}
// 从原有数组的第start位置开始到结束组合成新数组返回 不影响原有数组
export function toArray (list: any, start?: number): Array<any> {
start = start || 0
let i = list.length - start // 9-5
const ret: Array<any> = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}
// 浅拷贝 即把_from中的所有的key和value都合并到to对象上,并返回合并后的对象
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
// 把数组转为对象格式,即数组中的所有value为真的且能被for in 遍历的,都合并到新对象上
export function toObject (arr: Array<any>): Object {
const res = {}
for (let i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i])
}
}
return res
}
// 移除一个选项并返回除了移除后并返回当前删除的数组
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)
}
}
}
// 函数只执行一次 内部变量标记只有没有执行过才执行函数,执行过就不在执行
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
关于小驼峰、短横线命名法、首字母大写等函数的封装
// 封装一个缓存函数,有缓存先走缓存,没有缓存就给cached函数设置key和value进行缓存
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null) // 创建一个空对象
return (function cachedFn (str: string) { //
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
const camelizeRE = /-(\w)/g
// -链接改为小驼峰写法
export const camelize = cached((str: string): string => {
return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
// 首字母大写
export const capitalize = cached((str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1)
})
const hyphenateRE = /\B([A-Z])/g
// 驼峰写法改为-连接写法 myVariableName =》 my-variable-name
export const hyphenate = cached((str: string): string => {
return str.replace(hyphenateRE, '-$1').toLowerCase()
})
判断tag是否在构建范围内
// 封装一个map 根据传入的参数,逗号分隔后key:true的方式存入{}中,并做了是否区分大小写的设置
export function makeMap (
str: string,
expectsLowerCase?: boolean
): (key: string) => true | void {
const map = Object.create(null)
const list: Array<string> = str.split(',')
for (let i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? val => map[val.toLowerCase()]
: val => map[val]
}
// 用来检查当前tag是否在tag构建的范围之内
export const isBuiltInTag = makeMap('slot,component', true)
// 是否是范围内的属性
export const isReservedAttribute = makeMap('key,ref,slot,slot-scope,is')
总结
- 通过阅读代码,了解了相关类型的判断,自己对于部分知识点的使用有了落地的场景;
- 所有的函数封装都是为功能和需求服务的,只有根据对应的需求封装出来的函数才是最适用的函数;
- 学习到了函数的单一化,一个函数只实现一个功能,无副作用;
- 通过工具函数源码阅读,对类型判断、bind/call/apply、Object.create、功能函数封装方式等有了进一步的了解,也对基础知识中有缺陷的地方也有了大概是认识;
转载自:https://juejin.cn/post/7248534178827042874