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

272 阅读3分钟

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

学习目标

1. Vue2 源码 shared 模块中的几十个实用工具函数 
2. 如何学习源码中优秀代码和思想,投入到自己的项目中 
3. 如何学习 JavaScript 基础知识,会推荐很多学习资料 

源码地址

源码解读

源码中使用了Flow 类型,它是 JavaScript 代码的静态类型检查器Flow 通过静态类型注释检查代码是否存在错误。所以js文件通过在文件开头加以下注释以开启flow类型检测

// @flow

1. emptyObject

const emptyObject = Object.freeze({})

控制对象状态的方法

JavaScript 提供了三种冻结方法,最弱的一种是Object.preventExtensions,其次是Object.seal,最强的是Object.freeze

  • Object.preventExtensions方法可以使得一个对象无法再添加新的属性。检测方法为Object.isExtensible
  • Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性。实质是把属性描述对象的configurable属性设为false,检测方法为Object.isSealed
  • Object.freeze方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量。检测方法为Object.isFrozen

2. 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'
  )
}
  • 基础类型检测,js基础类型包含 string、number、symbolboolean

  • typeof作为检测类型的一种方式,返回值包括numberstringbooleanfunctionundefinedobject六种 实用技巧: 检查一个没有声明的变量,而不报错

// 错误写法 
if (v) {  // ...  }
// 正确写法
if(typeof v === "undefined") {...}

3. toRawType 获取值原始类型

Object.prototype.toString返回一个值到底是什么类型

  • 数值:返回[object Number]
  • 字符串:返回[object String]
  • 布尔值:返回[object Boolean]
  • undefined:返回[object Undefined]
  • null:返回[object Null]
  • 数组:返回[object Array]
  • arguments 对象:返回[object Arguments]
  • 函数:返回[object Function]
  • Error 对象:返回[object Error]
  • Date 对象:返回[object Date]
  • RegExp 对象:返回[object RegExp]
  • 其他对象:返回[object Object]
/**
 * Get the raw type string of a value, e.g., [object Object].
 */
const _toString = Object.prototype.toString

export function toRawType (value: any): string {
  return _toString.call(value).slice(8, -1)
}

4. toString 将所有类型转换为字符串的方法

  • null -> ''
  • 数组或有原始toString方法的纯对象通过JSON.stringify转换
  • 其余类型使用String函数直接转换
/**
 * Convert a value to a string that is actually rendered.
 */
export function toString (val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

JSON.stringify使用

JSON.stringify(value[, replacer [, space]])

JSON.stringify的可选参数

replacer 可以为 function|Array|null

space 可以为 number|string|null space为2 意为每一级缩进的空格数为2

⚠️ 使用注意事项

JSON.stringify({x: undefined, y: Object, s: fuction(){}, z: Symbol("")});
// '{}'

JSON.stringify([undefined, Object,fuction(){}, Symbol("")],NaN, Infinity);
// '[null,null,null,null,null,null]'

JSON.stringify({[Symbol("foo")]: "foo"});
// '{}'

JSON.stringify({[Symbol.for("foo")]: "foo"}, [Symbol.for("foo")]);
// '{}'


// 不可枚举的属性默认会被忽略:
JSON.stringify(
    Object.create(
        null,
        {
            x: { value: 'x', enumerable: false },
            y: { value: 'y', enumerable: true }
        }
    )
);
// "{"y":"y"}"

5. 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)
}

6. isPromise

满足已定义并且它的then和catch都为函数类型

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

7. makeMap

由,号分割的字符串生成map并返回function 用于检测键名是否存在于map中

/**
 * 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 {
  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]
}

/**
 * Check if a tag is a built-in tag.
 */
export const isBuiltInTag = makeMap('slot,component', true)

Object.create方法有两个参数

第一个参数指定对象的__proto__

第二个参数为描述对象 与Object.defineProperties第二个参数相同 包括configurableenumerablevaluewritablegetset

8. 驼峰、连字符转换

/**
 * Camelize a hyphen-delimited string.
 */
const camelizeRE = /-(\w)/g
export const camelize = cached((str: string): string => {
  return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '')
})

/**
 * Capitalize a string.
 */
export const capitalize = cached((str: string): string => {
  return str.charAt(0).toUpperCase() + str.slice(1)
})

/**
 * Hyphenate a camelCase string.
 */
const hyphenateRE = /\B([A-Z])/g
export const hyphenate = cached((str: string): string => {
  return str.replace(hyphenateRE, '-$1').toLowerCase()
})

正则学习推荐:juejin.cn/post/684490…

  • 连字符转驼峰 全局匹配以-开头的字符
  • 驼峰转连字符需要全局匹配非单词开始或结束位置的大些字母
  • charAt 从一个字符串中返回指定的字符
  • replace(regexp|substr, newSubStr|function) 使用示例如下:
function replacer(match, p1, p2, p3, offset, string) {
  //match为匹配的子串 p1 为组1匹配的串, p2 为组2匹配的串, and p3 为组3匹配的串
  return [p1, p2, p3].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer);
console.log(newString);  // abc - 12345 - #$*%

9. looseEqual 递归各数据类型判断值相等

export function isObject (obj: mixed): boolean %checks {
  return obj !== null && typeof obj === 'object'
}

/**
 * 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 {
  //判断值类型的相等
  if (a === b) return true 
  const isObjectA = isObject(a)
  const isObjectB = isObject(b)
  if (isObjectA && isObjectB) { 
  // 都是非null的object类型
    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) { 
      // 纯对象 key长度及每项key的value相等
        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) { 
  // 函数类型/NaN 需转换为字符串再比较
    return String(a) === String(b)
  } else { 
    return false
  }
}

10. 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)
    }
  }
}

总结

  • 看懂是一回事,编码是另一回事,学习最主要的还是应用,只有会用了,它才是你的。所以我们在学习的时候要多想想为什么,如何用
  • 其次,大多数的基础知识是我们早就掌握的,但是通过读源码,仍然可以查漏补缺,把容易忘记的和不了解的地方做个记录,这样,你的知识网络才会越来越全
  • 然后就是对于自己的知识总结,要多拿出来看,这样才能记得更牢固 学习才会变得快乐

更多资料

阮一峰老师:《ES6 入门教程》

《现代 JavaScript 教程》

《你不知道的JavaScript》上中卷