likes
comments
collection
share

【源码共读】第24期 | vue2工具函数学习

作者站长头像
站长
· 阅读数 66

前言

学习目标

  • 了解工具函数的封装以及场景;
  • 学习源码中优秀代码和思想,投入到自己的项目中;
  • 对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))
  }
}

【源码共读】第24期 | vue2工具函数学习

  • 使用场景
    • 新创建对象,继承以实现对象上的属性和方法,不用再次封装实现
  • 小结
    • 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,正则等类型)
  • 小结
    • Object.prototype.toString.call(value).slice(8, -1) 主要用来区分具体引用类型,typeof用来区分主要基础类型
    • 所有使用 new 调用的构造函数都将返回非基本类型("object" 或 "function")。大多数返回对象,但值得注意的例外是 Function,它返回一个函数。

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'
  )
}

【源码共读】第24期 | vue2工具函数学习

  • 使用场景
    • 判断对象是否是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')
 

总结

  1. 通过阅读代码,了解了相关类型的判断,自己对于部分知识点的使用有了落地的场景;
  2. 所有的函数封装都是为功能和需求服务的,只有根据对应的需求封装出来的函数才是最适用的函数;
  3. 学习到了函数的单一化,一个函数只实现一个功能,无副作用;
  4. 通过工具函数源码阅读,对类型判断、bind/call/apply、Object.create、功能函数封装方式等有了进一步的了解,也对基础知识中有缺陷的地方也有了大概是认识;
转载自:https://juejin.cn/post/7248534178827042874
评论
请登录