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

215 阅读11分钟

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

1. 前言

刚好在写完上一期的源码阅读笔记时,若川大大建议现在可以不用学习Flow类型检查,可以学习下TypeScript,最近公司也在使用TypeScript,正好借着阅读Vue3源码的机会学习下优秀的代码,以及强化自己的知识;

最近在活动中也学习到了很多,在不停的意识到强大的人不是没有理由的,也慢慢学会了保持谦逊,反思着过往那个无知的自我(感觉曾经的我真印证了罗翔老师的一句话,‘越是知识匮乏的人,越有一种莫名其妙的勇气和自豪感’),当不断学习之后才发现曾经的我有多么的无知与可笑;但好在改变在任何时候都不会晚,虽然无法赶上他人的步伐,但坚持不断超过昨天的我,终有一日也能耀眼,加油!!!

2. 测试环境搭建

因为TypeScript是无法直接在浏览器控制台执行的,而自己又是边学边看,所以免不了要进行各种实验,应此在VsCode搭建了一个本地TypeScript运行环境,简单写下搭建步骤(方法来自于网络)

2.1 使用npm安装依赖包

 npm install -g ts-node
 npm install -g typescript
  • 这里我是全局安装的,只安装在当前项目目录下应该也是可以的,但我没有尝试

2.2 安装配置Code Runner插件

  • VSCode插件商店中找到Code Runner,安装插件

  • 安装完成后设置插件,选中Run In Terminal选项

    屏幕截图 2022-05-10 003444.png

2.3 运行

  • 直接运行需要执行的ts文件,结果会在终端输出

3. 工具函数

在写下本文时Vue最新版本为3.2.33,文章中所有函数以该版本中工具函数为准,源码位置vuejs/core/packages/shared/src/index.ts

EMPTY_OBJ

空对象

 export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__
   ? Object.freeze({})
   : {}
  • 定义了一个空对象,TypeScript中使用:定义类型

  • 定义了空对象类型{ readonly [key: string]: any }

  • readonlyTypeScript中用于修饰只读类型,详细说明可参见官网

  • [key: string]: any定义了对象为key-value对并且key的类型为stringvalue类型为any任意类型,虽然TypeScript中提供了any用于接受任意类型的值,但是不要滥用,之前我就被公司大佬吐槽了,老是喜欢定义any,仿佛就是在用JavaScript完全失去了Type的意义;

  • __DEV__猜测应该是一个当前运行环境的判断

  • Object.freeze({})冻结对象,使对象不可被修改;但是注意的是在返回{}时,因为定义了readonly所以在用EMPTY_OBJ["name"]="xinxinzi"添加属性时会报错,但用官网的编辑器运行时还是添加进去属性了,不知道是不是我用法的问题

     const EMPTY_OBJ: { readonly [key: string]: any } = {};
     EMPTY_OBJ["name"]="xinxinzi";
     Object.defineProperty(EMPTY_OBJ, "job", {value:"coder",enumerable:true});
     console.log(EMPTY_OBJ); // { "name": "xinxinzi", "job": "coder" } 
    

EMPTY_ARR

空数组

 export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []
  • 定义了一个空数组

NOOP

空函数

 export const NOOP = () => {}
  • 定义了一个空函数

NO

永假函数

 /**
  * Always return false.
  */
 export const NO = () => false
  • 函数永远返回false

isOn

判断字符串是否以on开始

 const onRE = /^on[^a-z]/
 export const isOn = (key: string) => onRE.test(key)

isModelListener

是否为模型监听

 export const isModelListener = (key: string) => key.startsWith('onUpdate:')
  • 判断字符串是否以'onUpdate:'开头
  • startWith(),判断字符串是否以给定子字符串开头,返回boolean,更多用法可见MDN

extend

定义继承

 export const extend = Object.assign
  • 使用Object.assign将源对象的所有可枚举属性的值分配到目标对象中,返回目标对象
  • assign更多说明MDN

remove

删除任意类型数组中的某一项

 export const remove = <T>(arr: T[], el: T) => {
   const i = arr.indexOf(el)
   if (i > -1) {
     arr.splice(i, 1)
   }
 }
  • <T>TypeScript中泛型的定义
  • indexOf()查找到数组中对应的项
  • splice(i, 1)删除数组中第i

hasOwnProperty

自身属性判断函数

 const hasOwnProperty = Object.prototype.hasOwnProperty
  • 使用hasOwnProperty判断对象自身是否拥有属性

hasOwn

是否包含属性

 export const hasOwn = (
   val: object,
   key: string | symbol
 ): key is keyof typeof val => hasOwnProperty.call(val, key)
  • val类型为object-对象

  • key类型为string-字符串或symbol- 唯一符号

  • |可用于TypeScript中多个类型的申明

  • is可以用来判断一个变量是否属于某个接口或类型

     interface Person {
       name: string;
       job: string;
     }
     const isPerson = (value: any): value is Person => {
       return (
         typeof (value as Person)["name"] !== "undefined" &&
         typeof (value as Person)["job"] !== "undefined"
       );
     };
     console.log(isPerson({ name: "xinxinzi", job: "coder" })); // true
     console.log(isPerson({ name: "xinxinzi" })); // false
     console.log(isPerson({ job: "coder" })); // false
    

    注意的是,is并没有提供判断的功能,只是用于声明了函数的返回值是表示value是否为Person的布尔值,具体判断逻辑是在后面的的函数体中实现的,因此也会有如下这样的情况:

     const isPerson = (value: any): value is Person => true;
     console.log(isPerson({ name: "xinxinzi", job: "coder" })); // true
     console.log(isPerson({ name: "xinxinzi" })); // true
     console.log(isPerson({ job: "coder" })); // true
    
  • keyof用于获取某类型的所有键,返回联合类型

     interface Person {
       name: string;
       job: string;
     }
     type Key = keyof Person; // "name" | "job"
    

    声明一个类型为Key的变量,其变量的值只可以为'name''job'

  • typeofJavaScript中用于获取类型,但TypeScript中添加了一个新的用法用于直接引用变量或者属性的类型

     // 获取变量的类型
     let n = "xinxinzi";
     let s: typeof n; // s的类型为string
     // 获取到函数的类型
     function f() {
       return false;
     }
     type T = ReturnType<typeof f>; // 类型T为boolean,<>表示泛型所以只能接受类型,但f是函数名(函数的值),所以需要使用typeof取到值的类型
    

    代码中的ReturnType<T>用于获取取函数类型的返回值类型的方法,有一点点拗口,代码看起来应该比较简洁

     type T = ReturnType<() => string>;
     // 等同于
     type T = string;
    
  • 所以此处key is keyof typeof val的逻辑是首先typeof获取到传入对象val的类型,然后keyof获取到对象类型中的所有Key,最后用is表示key是否是对象中的Key,如经过一番扩写,下面代码可以用于判断对象key是否属于val:Person

     interface Person {
       name: string;
       job: string;
     }
     let val: Person;
     type V = typeof val;
     type Key = keyof V;
     const has = (key: string): key is Key => val.hasOwnProperty(key);
    

isArray

定义数组判断函数

 export const isArray = Array.isArray
  • 使用Array原生方法isArray

isMap

判断是否为Map

 export const isMap = (val: unknown): val is Map<any, any> =>
   toTypeString(val) === '[object Map]'
  • Map<any, any>TypeScriptMap对象的定义,表示Map中的keyvalue都是any类型的数据
  • toTypeString为工具中定义的方法,用来获取对象类型的字符串表示
  • 这里有一个不太理解的地方,toTypeString在它的函数体声明前就被使用了,但是按理它不是使用function声明的应该不会有变量提升,有点不太理解??

isSet

判断是否为Set

 export const isSet = (val: unknown): val is Set<any> =>
   toTypeString(val) === '[object Set]'
  • Set<any>TypeScriptSet对象的定义,且Setvalueany类型的数据
  • 仍然还是使用toTypeString获取到对象类型的字符串表示

isDate

判断是否为Date日期类型

 export const isDate = (val: unknown): val is Date => val instanceof Date
  • 使用instanceof运算符,判断值是否为Date或者继承自Date

isFunction

判断是否为Function函数类型

 export const isFunction = (val: unknown): val is Function =>
   typeof val === 'function'
  • 使用typeof运算符判断对象是否为function类型

isString

判断是否为String字符串类型

 export const isString = (val: unknown): val is string => typeof val === 'string'
  • 仍然是使用typeof进行类型判断

isSymbol

判断是否为Symbol类型

 export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'
  • typeof类型判断

isObject

判断是否为Object类型

 export const isObject = (val: unknown): val is Record<any, any> =>
   val !== null && typeof val === 'object'
  • Record<>TypeScript中用于构建一个对象类型,详情及更多其它关键字可产看官网
  • 判断不为空,且类型为object

isPromise

判断是否为Promise对象

 export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
   return isObject(val) && isFunction(val.then) && isFunction(val.catch)
 }
  • Promise<T>TypeScriptpromise对象的定义
  • Vue2中判断方式基本相同,判断本身是对象,对象包含.then.catch两个方法

toTypeString

获取对象类型的字符串表示

 export const objectToString = Object.prototype.toString
 export const toTypeString = (value: unknown): string =>
   objectToString.call(value)
  • 定义objectToStringObject.prototype.toString,用于获取对象类型的字符串表示
  • toTypeString返回一个unknown未知类型的对象的类型的字符串表示
  • unknown表示未知类型,官网说明

toRawType

获取对象的类型字符串

 export const toRawType = (value: unknown): string => {
   // extract "RawType" from strings like "[object RawType]"
   return toTypeString(value).slice(8, -1)
 }
  • Vue2中该方法的功能相同,都是返回原始类型

isPlainObject

判断是否为纯粹的对象

 export const isPlainObject = (val: unknown): val is object =>
   toTypeString(val) === '[object Object]'
  • 判断对象类型的字符串表示是否为'[object Object]'

isIntegerKey

判断是否为整数形式的Key

 export const isIntegerKey = (key: unknown) =>
   isString(key) &&
   key !== 'NaN' &&
   key[0] !== '-' &&
   '' + parseInt(key, 10) === key
  • key是字符串,并且key可以转化为数字,并且key转化为数字时非负,并且key的每个字符都为十进制的数字且不包含前导0

isReservedProp

判断是否为保留属性

 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对象,vue2中也存在过,返回一个方法,用于查找key是否在Map中,这里函数的声明并没有在index中,而是同目录下的makeMap.ts文件中,我也给它扒了下来

     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]
     }
    

    函数功能和vue2中并无二致

isBuiltInDirective

是否为内置的指令

 export const isBuiltInDirective = /*#__PURE__*/ makeMap(
   'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
 )
  • 仍旧是使用makeMap创建对应的判断方法

cacheStringFunction

创建函数类型的缓存,Vue2工具函数中cachedTypeScript实现

 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
 }
  • <T extends (str: string) => string>定义泛型TT继承自(str: string) => string方法
  • cacheStringFunction参数接收一个T类型并且返回值类型也是T
  • const cache: Record<string, string> = Object.create(null)创建一个缓存对象
  • 返回一个参数为string,返回值为cache中对应str项的方法

camelize

将连字符分隔字符串转化为驼峰格式

 const camelizeRE = /-(\w)/g
 /**
  * @private
  */
 export const camelize = cacheStringFunction((str: string): string => {
   return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
 })
  • /-(\w)/g,全局匹配-[a-zA-Z0-9_]格式字符串,使用可视化正则生成效果

    jex.im_regulex_ (2).png

  • 通过replace()替换匹配项

hyphenate

将驼峰命名转化为连字符分隔的字符串

 const hyphenateRE = /\B([A-Z])/g
 /**
  * @private
  */
 export const hyphenate = cacheStringFunction((str: string) =>
   str.replace(hyphenateRE, '-$1').toLowerCase()
 )
  • /\B([A-Z])/g,匹配非单词边界的大写字符

    jex.im_regulex_.png

  • '-$1'参数是replace()方法中的特殊参数,在第一个参数为RegExp正则对象时有效,表示第一个括号匹配的字符串,最大支持100

capitalize

首字母大写

 export const capitalize = cacheStringFunction(
   (str: string) => str.charAt(0).toUpperCase() + str.slice(1)
 )
  • charAt()获取字符串中指定字符
  • slice(1)提取字符串的一部分,返回一个新字符串

toHandlerKey

转化为Handler形式的字符串

 export const toHandlerKey = cacheStringFunction((str: string) =>
   str ? `on${capitalize(str)}` : ``
 )
  • 转化为onA形式的字符串
  • on${capitalize(str)}ES6中新增语法,用于方便的拼接字符串,等同于'on' + capitalize(str)

hasChanged

判断是否有变化

 export const hasChanged = (value: any, oldValue: any): boolean =>
   !Object.is(value, oldValue)
  • 传入新旧两个值,通过is()方法判断是否为同一值

  • is()的判断相等的依据如下,引用自MDN

    • 都是 undefined

    • 都是 null

    • 都是 true 或都是 false

    • 都是相同长度、相同字符、按相同顺序排列的字符串

    • 都是相同对象(意味着都是同一个对象的值引用)

    • 都是数字且

      • 都是 +0
      • 都是 -0
      • 都是 NaN
      • 都是同一个值,非零且都不是 NaN
  • Object.is()===的区别

     Object.is(+0, -0); // false
     +0 === -0; // true
     Object.is(Number.NaN, NaN); // true
     Number.NaN === NaN; // false
    

invokeArrayFns

调用方法数组

 export const invokeArrayFns = (fns: Function[], arg?: any) => {
   for (let i = 0; i < fns.length; i++) {
     fns[i](arg)
   }
 }
  • fns: Function[]传入一个方法数组

  • arg?: any参数,任意类型,TypeScript中一个比较有意思的运算符,有以下几种用法:

    1. 代表可选参数,也就是上文代码中的用法,会自动为参数加上一个undefined类型,在传递参数时可以不传

    2. 代表可选属性,常用在类型,接口等的定义中,如下

       interface Person {
         name: string;
         job?: string;
       }
      

      这里的job就是可选参数,在实现接口时可以不要该属性,人可以没有工作(🙃bushi)

    3. 用于判断对象是否为nullundefined,如下

       function getData(data: any){
         let name = data?.name
       }
      

      data.name这样的写法在dataundefined时就会报错,但data?.name可以避免这样的情况

    4. ??类似于||表示取或,但是有一点区别,如下

       console.log(0 || 5);
       console.log(0 ?? 5);
      

      当值为0时会被||排除,而??不会

def

为对象定义一个只可以被编辑描述符和删除的属性

 export const def = (obj: object, key: string | symbol, value: any) => {
   Object.defineProperty(obj, key, {
     configurable: true,
     enumerable: false,
     value
   })
 }
  • obj: object需要定义属性的对象
  • key: string | symbol定义的keystringsymbol
  • value: any属性的值为任意类型
  • 使用Object.defineProperty()进行定义,详细参见MDN

toNumber

转化为Number

 export const toNumber = (val: any): any => {
   const n = parseFloat(val)
   return isNaN(n) ? val : n
 }
  • 将传入的参数值转化为通过parseFloat()转化为数字,若不可转换,返回参数本身
  • parseFloat()函数更多用法可参见MDN

getGlobalThis

获取全局的this指向

 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
         : {})
   )
 }
  • 依次判断_globalThis,不为空则不进行赋值,直接返回_globalThis
  • 赋值时,存在typeof globalThis !== 'undefined',存在则取global的值
  • 否则判断是否存在typeof self !== 'undefined',存在则取self的值
  • 否则判断是否存在typeof window !== 'undefined',存在则取window
  • 否则判断是否存在typeof global !== 'undefined', 存在则取global
  • 否则使用空对象{}

4. 总结

通过对Vue3中工具函数的学习,同时学习TypeScript,目前也算是勉强入门了吧,前方道路还很漫长,但唯有不断前行而已。加油,诸君!!