聊聊 Vue3 源码中的工具函数

160 阅读8分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第2期,vue3 工具函数

1.前期准备

1.1 环境配置

按照 Vue3 Contributing Guide 的要求

  • Nodejs 版本应为 16+
  • PNPM 版本应为 7+

1.2 源码运行

  • 拉取代码
git clone https://github.com/vuejs/core.git
  • 安装依赖
pnpm i
  • 运行
pnpm run dev
  • 编译
pnpm run build

2.函数解读

本文设计的函数均是 Vue3 shared 模块的内容,按照 packages/shared/src/index.ts 的顺序编写。

2.1 EMPTY_OBJ

  • 源码
    export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__ ? Object.freeze({}): {}
    
  • 解读
    空对象,开发环境使用 Object.freeze 冻结空对象。
  • 拓展
    Object.freeze 用来冻结对象,冻结的对象最外层属性不允许修改,但是允许更改更深层级的属性。

2.2 EMPTY_ARR

  • 源码
    export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []
    
  • 解读
    开发环境返回冻结的空数组,生产环境返回数组。
  • 拓展
    使用 Object.freeze 冻结的数组使用 push 数组会报错,这也是为什么生产环境直接使用空数组。

3.3 NOOP

  • 源码
    export const NOOP = () => {}
    
  • 解读
    空函数。
  • 拓展
    定义空函数的优点在于:1.方便使用,可使用 NOOP 直接赋值或做判断;2.方便压缩。

3.4 NO

  • 源码
    export const NO = () => false
    
  • 解读
    永远返回 false 的函数。
  • 拓展 优点:方便压缩。

3.5 isOn

  • 源码
    const onRE = /^on[^a-z]/
    export const isOn = (key: string) => onRE.test(key)
    
  • 解读
    判断字符串是不是 on 开头,并且 on 后首字母不是小写字母。
  • 拓展
    正则表达式: ^ 符号在开头,表示以什么开头;对应的,$ 符号在结尾,表示以什么结尾; [^a-z] 表示不是 az 的小写字母。

3.6 isModelListener

  • 源码
    export const isModelListener = (key: string) => key.startsWith('onUpdate:')
    
  • 解读
    判断字符串是否以 onUpdate: 开头,即判断是否为 v-model 指令的监听器。
  • 拓展
    Vue3 v-model 指令实现方式:
    1.定义监听 prop 属性。
    2.定义 onUpdate: 事件。

3.7 extend

  • 源码
    export const extend = Object.assign
    
  • 解读
    合并。
  • 拓展
    Object.assign 用于合并两个或多个对象,第一个参数为合并结果对象,其余参数为待合并对象,若存在相同属性,后面的参数会覆盖前面的。

3.8 remove

  • 源码
    export const remove = <T>(arr: T[], el: T) => {
        const i = arr.indexOf(el)
        if (i > -1) { arr.splice(i, 1) }
    }
    
  • 解读
    移除数组的某一项。
  • 拓展
    splice 用于添加或移除数组中的项目,此处用来移除一个数组元素,不过此方法比较耗性能,移除元素后,其他元素都会移动位置。如果出于性能考虑的话,可以将需要移除的元素设为 null

3.9 hasOwn

  • 源码
    const hasOwnProperty = Object.prototype.hasOwnProperty
    export const hasOwn = (val: object, key: string | symbol): key is keyof typeof val => hasOwnProperty.call(val, key)
    
  • 解读
    判断属性是不是对象本身所拥有的属性。
  • 拓展
    对象的属性包括 本身属性原型属性。可用过 Object.prototype.hasOwnProperty 判断属性是否为对象本身属性。
    call 可用来改变 this 指向,此处 hasOwnProperty 函数内部的 this 均指向 val。相似的方法还有 applybind

3.10 isArray

  • 源码
    export const isArray = Array.isArray
    
  • 解读
    判断是否为数组。
  • 拓展
    判断类型的方法包括诸如 typeofinstanceofObject.prototype.toString 等。不过若用来校验数组类型,typeof 返回 objectinstanceof 原理是校验是否为某个构造函数的实例,若更改对象的 prototypeArray.prototype,即便该对象不是数组,也会返回 true

3.11 isMap

  • 源码
    export const isMap = (val: unknown): val is Map<any, any> => toTypeString(val) === '[object Map]'
    
  • 解读
    判断是否为 Map 对象。
  • 拓展
    MapES6 语法,类似于 Object,也是 键值对 的形式。不同之处在于 Object 的键值只能是字符串,而 Map 的键可以是任意类型

3.12 isSet

  • 源码
    export const isSet = (val: unknown): val is Set<any> => toTypeString(val) === '[object Set]'
    
  • 解读
    判断是否为 Set 对象。
  • 拓展
    SetES6 语法,类似于 Array,不同之处在于 Array 的元素可以重复,Set 的元素不允许重复。

3.13 isDate

  • 源码
    export const isDate = (val: unknown): val is Date => toTypeString(val) === '[object Date]'
    
  • 解读
    判断是否为 Date 对象。

3.14 isFunction

  • 源码
    const isFunction = (val: unknown): val is Function => typeof val === 'function'
    
  • 解读
    判断是否为函数。

3.15 isString

  • 源码
    export const isString = (val: unknown): val is string => typeof val === 'string'
    
  • 解读
    判断是否为字符串。

3.16 isSymbol

  • 源码
    export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
    
  • 解读
    判断是否为 Symbol。
  • 拓展
    SymbolES6 新增的一种原始数据类型O,表示独一无二Symbol 是函数,不需要配合 new 关键字使用。

3.17 isObject

  • 源码
    export const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object'
    
  • 解读
    判断是否为对象。
  • 拓展
    判断 val !== null 是因为 typeof null 的值也是 object

3.18 isPromise

  • 源码
    export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
        return isObject(val) && isFunction(val.then) && isFunction(val.catch)
    }
    
  • 解读
    判断是否为 Promise。
  • 拓展
    Promise异步编程常用方式,本身是对象类型,then 参数一个函数,用作处理成功后的回调。catch 参数也是一个函数,用作处理失败后的回调。

3.19 objectToString

  • 源码
    export const objectToString = Object.prototype.toString
    
  • 解读
    对象转字符串。

3.20 toTypeString 对象转字符串

  • 源码
    export const toTypeString = (value: unknown): string => objectToString.call(value)
    
  • 解读
    对象转字符串。
  • 拓展
    通过这个能获得类似 [object String] 的结果。其中 String 是根据类型变化的。
    call 用于改变 this 指向,类似的方法还有 applybind。其中 applycall 会立即执行函数,而 bind 不会立即执行,需要手动重新调用。

3.21 toRawType

  • 源码
    export const toRawType = (value: unknown): string => {
        // extract "RawType" from strings like "[object RawType]"
        return toTypeString(value).slice(8, -1)
    }
    
  • 解读
    获取数据类型。
  • 拓展
    个人认为,使用 Object.prototype.toString 来判断类型相对来说是最准确的,虽然 typeof 也可以用来判断类型,但是会有不准确的时候,比如 tyoeof null 的结果为 object

3.22 isPlainObject

  • 源码
    export const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]'
    
  • 解读
    判断是否为纯粹的对象。
  • 拓展
    与上面 isObject 不同地方在于,isPlainObject 校验是否为纯粹的对象。isObject([]) 为 true,而 isPlainObject([]) 为 false。

3.23 isIntegerKey

  • 源码
    export const isIntegerKey = (key: unknown) => isString(key) && key !== 'NaN' && key[0] !== '-' && '' + parseInt(key, 10) === key
    
  • 解读
    判断是否为数字的字符串形式。如 '0'
  • 拓展
    字符串可以使用数组下标形式取值。相似的方法还有 charAt,如 'abc'.charAt(0)。不同之处在于,charAt 取不到值返回 '',而数组下标形式返回 undefined
    parseInt 函数的第二个参数表示进制,如 parseInt(key, 10) 表示转化为10进制。 在数字前面拼接空字符串(''),会将该数字转化为字符串。如 '' + 10 等价于 '10'

3.24 makeMap && isReservedProp

  • 源码
    /**
    * Make a map and return a function for checking if a key 
    * is in that map.
    * IMPORTANT: all calls of this function must be prefixed with
    * \/\*#\_\_PURE\_\_\*\/
    * So that rollup can tree-shake them if necessary.
    */
    export function makeMap(str: string, expectsLowerCase?: boolean): (key: string) => boolean {
        const map: Record<string, boolean> = 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]
    }
    
    export const isReservedProp = /*#__PURE__*/ makeMap(
      // the leading comma is intentional so empty string "" is also included
      ',key,ref,ref_for,ref_key,'
      +'onVnodeBeforeMount,onVnodeMounted,'
      +'onVnodeBeforeUpdate,onVnodeUpdated,'
      +'onVnodeBeforeUnmount,onVnodeUnmounted'
    )
    
  • 解读
    makeMap 函数接受一个以 逗号(,) 分隔的字符串和 是否转小写的标识。用来生成一个 map 并返回校验字符串是否存在于该 map 的函数。

3.25 cacheStringFunction 缓存

  • 源码
    const cacheStringFunction = <T extends (str: string) => string>(fn: T): T => {
        const cache: Record<string, string> = Object.create(null)
        return ((str: string) => {
            const hit = cache[str]
            return hit || (cache[str] = fn(str))
        }) as any
    }
    
  • 解读
    该函数用来缓存 fn 针对 str 的执行结果,单例模式。
  • 拓展
    使用示例:
    // 连字符转小驼峰
    // \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成
    // () 小括号是 分组捕获
    const camelizeRE = /-(\w)/g;
    const camelize = cacheStringFunction((str) => {
        return str.replace(camelizeRE, (_, c) => c ? c.toUpperCase() : '' )
    })
    

3.26 hasChanged

  • 源码
    // compare whether a value has changed, accounting for NaN.
    export const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue)
    
  • 解读
    判断值是否有变化。
  • 拓展
    Object.is 用来比较两个值是否严格相等,与 === 的行为类型,不同之处在于:对于 Object.is+0 不等于 -0,而 Nan 等于自身。

3.27 invokeArrayFns

  • 源码
    export const invokeArrayFns = (fns: Function[], arg?: any) => {
        for (let i = 0; i < fns.length; i++) {
            fns[i](arg)
        }
    }
    
  • 解读
    执行数组里的函数。
  • 拓展
    将函数存放到数组中,可以一次性执行多个函数。

3.28 def

  • 源码
    export const def = (obj: object, key: string | symbol, value: any) => {
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: false,
            value
        })
    }
    
  • 解读
    定义对象属性。
  • 拓展
    Object.defineProperty 用来定义对象的单个属性的 API。
    Object.defineProperties(obj, props) 可用来定义同时定义对象的多个属性的 API。
    可以通过设置属性描述符 来控制对象属性的操作,如是否可写,是否可枚举,是否可删除等。
    属性描述符包括:
    • value——当试图获取属性时所返回的值。
    • writable——该属性是否可写。
    • enumerable——该属性在for in循环中是否会被枚举。
    • configurable——该属性是否可被删除。
    • set()——该属性的更新操作所调用的函数。
    • get()——获取属性值时所调用的函数。 属性描述符 可分为 数据描述符(enumerableconfigurablevaluewritable) 和 存取描述符(enumerableconfigurableset()get()) 之间存在 互斥关系。如设置了 get()set(),则认为存取操作已经被设置,此时若在设置 writablevalue 会引起错误。
      除了 value 描述符默认为 undefined,其余描述符默认值均为 false

3.29 toNumber

  • 源码
    export const toNumber = (val: any): any => {
        const n = parseFloat(val)
        return isNaN(n) ? val : n
    }
    
  • 解读
    转数字。
  • 拓展
    parseFloat 函数会解析参数,并返回一个浮点数,若无法转换成浮点数,返回 Nan
    isNan 函数用来判断参数是否为 NaN,但是并不精准,比如 isNan('a111') 返回 true。ES6 新增了 Number.isNan 用来弥补该情况。

3.30 getGlobalThis

  • 源码
    let _globalThis: any
    export const getGlobalThis = (): any => {
        return (
            _globalThis || 
            (_globalThis = 
                typeof globalThis !== 'undefined' 
                    ? globalThis 
                    : typeof self !== 'undefined' 
                    ? self 
                    : typeof window !== 'undefined' 
                    ? window 
                    : typeof global !== 'undefined' 
                    ? global : {}))
    }
    
  • 解读
    返回全局 this 执行。
  • 拓展
    第一次执行,_globalThis 对象肯定是 undefined
    若存在 globalThis,返回 globalThis
    否则,若存在 self,返回 self。在 Web Worker 中不能访问 window 对象,当时可以通过 self 访问 Worker 环境中的全局对象。
    否则,若存在 window 对象,返回 window
    否则,若存在 global 对象,返回 globalNode 环境下,使用 global
    否则,返回 空对象
    下次再执行该函数,直接返回 _globalThis,避免重复判断。

好啦!以上便是「 聊聊 Vue3 源码中的工具函数 」的全部内容,感谢阅读。
欢迎各路大佬讨论、批评、指正,共同进步才是硬道理!