【若川视野 x 源码共读】第24期 | Vue2工具函数

609 阅读3分钟

前言

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

源码

Object.freeze 冻结对象

var emptyObject = Object.freeze({});

Object.freeze()方法以一个对象为参数,冻结这个对象;它可以保留对象的可枚举性,可配置性,可写性和原型不被修改;它返回被冻结的对象,但不创建冻结副本。

同时,它也只能冻结对象的第一层数据,无法深冻结,类似浅拷贝。

我们可以通过Object.isFrozen()对某个数据进行判断,是否冻结。

源码中,将emptyObject定义为一个默认的全局变量。

isUndef 判断数据是否未定义

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

isDef 判断数据是否已定义

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

该方法,一度想过对isUndef进行取反校验

isPrimitive 判断数据是否是原始值

function isPrimitive (value) {
  return (
    typeof value === 'string' ||
    typeof value === 'number' ||
    // $flow-disable-line
    typeof value === 'symbol' ||
    typeof value === 'boolean'
  )
}

有没有觉得很奇怪,缺少了nullundefined,起初一直以为这个方法是用来校验是否为基本类型,但是看了一下源码中使用该方法的地方,传入的数据不存在这两种类型,因此该方法就不做这两种类型的判断了,否则函数命名可能就是isBaseType

isObject ​判断数据是否是对

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

没有对常规对象做精细化校验,或许是对引用类型进行若判断

isTrue 判断数据是否是true

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

isFalse 判断数据是否是false

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

isPlainObject 判断数据是否是普通对象

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

isValidArrayIndex 判断数据是否是可用的数组索引

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

isRegExp 判断数据是否是正则表达式

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

isPromise 判断数据是否是Promise

function isPromise (val) {
  return (
    isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

toRawType 获取数据类型

var _toString = Object.prototype.toString;
​
function toRawType (value) {
  return _toString.call(value).slice(8, -1);
}

该方法也弥补了isObject的不足

满足数组索引值的条件,大于等于0 且 是一个正整数 且 是一个有限制

toString 将数据转化为String

function toString (val) {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

toNumber 将数据转化为Number

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

toArray 将数据转为数组

将支持length属性且可读取每一个下标位置的数据(String, Array, arguments),转为数组,且支持转换起始位置

function toArray (list, start) {
  start = start || 0
  let i = list.length - start
  const ret = new Array(i)
  while (i--) {
    ret[i] = list[i + start]
  }
  return ret
}

remove 删除数组中的某一项

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

hasOwn 判断数据是否有某个属性

const hasOwnProperty = Object.prototype.hasOwnProperty
export function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

cached 缓存

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

这个高阶函数比较有意思,同时设计的也很巧妙,通过执行cached方法,创建了一个公共的缓存函数和缓存数据闭包cache,每次执行公共的函数时,会优先读取cache里是否有这个数据,有的话直接返回,否则执行处理函数后,并缓存数据,提高了同一个数据的处理效率。

camelize 将中划线的变量名转为小驼峰

const camelizeRE = /-(\w)/g
function camelize = cached((str) => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})
// first-name -> firstName

capitalize 将变量名首字母大写

function capitalize = cached((str) => {
  return str.charAt(0).toUpperCase() + str.slice(1)
})
// firstName -> FirstName

hyphenate 将小驼峰变量名转为中划线的变量名

const hyphenateRE = /\B([A-Z])/g
function hyphenate = cached((str) => {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
})
// firstName -> first-name

polyfillBind Bind补充

// 补充Bind
function polyfillBind (fn, ctx) {
  function boundFn (a) {
    const l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a) // 更多的时候会使用arguments[0],源码更简洁
      : fn.call(ctx)
  }
​
  boundFn._length = fn.length; // fn函数的形参数量
  return boundFn
}
​
// 原生Bind
function nativeBind (fn, ctx) {
  return fn.bind(ctx)
}
​
// 校验浏览器是否支持Bind,优化Bind
const bind = Function.prototype.bind
  ? nativeBind
  : polyfillBind

这里比较绕,需要理解callapplybind的异同之处。

extend 合并两个数据

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

toObject 将对象数组中每一项合至一个对象

function toObject (arr) {
  const res = {}
  for (let i = 0; i < arr.length; i++) {
    if (arr[i]) {
      extend(res, arr[i])
    }
  }
  return res
}

noop 空函数

function noop (a, b, c) {}

no 返回false

const no = (a, b, c) => false

identity 返回相同数据

const identity = (_) => _

genStaticKeys 从编译器模块生成包含静态键的字符串

function genStaticKeys (modules) {
  return modules.reduce((keys, m) => {
    return keys.concat(m.staticKeys || [])
  }, []).join(',')
}

looseEqual 校验两个值是否大致相同

这个是工具函数里代码最多的一个函数

function looseEqual (a, b) {
  //如果两个值完全相等,返回true
  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
  }
}

looseIndexOf 返回数组中存在某个数据的下标

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

once 函数只执行一次

通过闭包声明变量called,校验当前函数是否执行

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

makeMap 生成一个属性Map

校验某个数据中是否存在key属性,这个方法类似于catched

function makeMap (str, expectsLowerCase) {
  const map = Object.create(null)
  const list = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

isBuiltInTag 校验是否是内置tag

export const isBuiltInTag = ('slot,component', true)

isReservedAttribute 校验是否是保留属性

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

知识拓展

JSON.stringify

查看用法

Bind、Apply、Call

感悟

看完源码之后,印象比较深刻的还是catchedmakeMap工具函数,将某一类的数据统一化处理,实现了代码设计的高内聚、低耦合,提高了代码的可拓展性和简洁性