大家好,我是若川。最近组织了源码共读活动。每周读 200 行左右的源码。很多第一次读源码的小伙伴都感觉很有收获,感兴趣可以加我微信ruochuan12,拉你进群学习。

初学者也能看懂的 Vue3 源码中那些实用的基础工具函数
本文是纪年小姐姐源码共读第二期写的笔记,非常好,可先收藏后学习。

1. 解读前的准备

粗略阅读了川哥的文章之后,感觉这期跟上一期不一样。上一期主要学习如何实现某个功能,而这一期主要是学习 Vue3 源码中的工具函数,以及 Vue3 源码的一些调试技巧。虽然看起来偏基础,但我觉得很考验一个程序员的基本功和耐心。

学习目标:

1)调试源码之打包构建项目代码,生成 sourcemap 调试源码

2)学习源码中的工具函数

目标:跟着川哥的文章走完一遍调试的流程,动手敲工具函数,对外输出记录文档。

资源准备:

Vue3 源码地址:https://github.com/vuejs/vue-next

2. 源码调试

2.1 阅读开源项目的 README.md 和贡献指南 contributing.md

我觉得这两个文件对阅读源码的开发者来说十分重要。README.md 描述的是项目的基本信息,它可以快速了解这个项目的全貌。贡献指南 contributing.md 会包含如何参与项目开发,项目打包/运行命令,项目目录结构等等,它能帮助你更好地调试/参与开发源码。在 contributing.md 中我看到了一些比较感兴趣的知识点,比如打包构建格式/配置,包依赖处理。

2.2 打包构建项目代码

安装完依赖,直接运行yarn build就可以打包 Vue3 的项目代码了,打包的产物如下(以 shared 模块为例):

打包后的产物

这里的 cjsesm 是 JS 里用来实现【模块化】的不同规则,JS 的模块化标准还有 amdumdiife

  • CJS,CommonJS,只能在 NodeJS 上运行,使用 require("module") 读取并加载模块,不支持浏览器

  • ESM,ECMAScript Module,现在使用的模块方案,使用 import export 来管理依赖,浏览器直接通过 <script type="module"> 即可使用该写法。NodeJS 可以通过使用 mjs 后缀或者在 package.json 添加 "type": "module" 来使用

2.3 生成 sourcemap 调试 vue-next 源码

在贡献指南 contributing.md 文件中描述了如何生成 sourcemap 文件:添加【--sourcemap】参数即可。

node scripts/dev.js --sourcemap

packages/vue/dist/vue.global.js.map 就是 sourcemap 文件了。

sourcemap 是一个信息文件,里面储存着位置信息,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错时出错工具将直接显示原始代码,而不是转换后的代码,方便调试。

3. 工具函数(TS 版)

3.1 babelParserDefaultPlugins:babel 解析默认插件

/*** List of @babel/parser plugins that are used for template expression* transforms and SFC script transforms. By default we enable proposals slated* for ES2020. This will need to be updated as the spec moves forward.* Full list at https://babeljs.io/docs/en/next/babel-parser#plugins*/
const babelParserDefaultPlugins = ['bigInt','optionalChaining','nullishCoalescingOperator'
] as const

它定义了三个默认插件, as const 这个语法叫 const 断言,它可以创建完整的 readonly 对象(只读状态),编译器可以通过 as const 推断出可用于的最具体的表达类型。

3.2 EMPTY_OBJ:空对象,EMPTY_ARR:空数组

export const EMPTY_OBJ: { readonly [key: string]: any } = __DEV__? Object.freeze({}): {}export const EMPTY_ARR = __DEV__ ? Object.freeze([]) : []

Object.freeze 冻结对象,不可修改对象的最外层,这样的写法可以降低在开发过程中发生错误。

DEV  是一个环境变量,为了避免在生产环境报错,生产环境使用的还是 {} 和 []。

3.3 NOOP:空函数

export const NOOP = () => {}

3.4 NO:永远返回 false 的函数

export const NO = () => false

3.5 isOn:判断字符串是否以 on 开头,并且 on 后首字母是非小写字母

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

【^】符号在开头,表示是指【以什么开头】,在其他地方是指【非】。与之相反的是:【$】符合在结尾,则表示是以什么结尾。

日常开发中我们也经常会用到正则判断,可以收集起来,积累的数量多了就不用每次都去搜索了????。

3.6 isModelListener:监听器

export const isModelListener = (key: string) => key.startsWith('onUpdate:')

判断字符串是不是以【onUpdate:】开头

3.7 extend:合并对象

export const extend = Object.assign

其实 extend 就是 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,在使用执行时为 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)

函数本身很好理解,利用原型的 API:hasOwnProperty 来判断 key 是否是 obj 本身的属性。

但【key is keyof typeof val】可能会有些迷惑,这里包含了三个 typescript 的语法,意思是函数返回的 key 是 属于 val 对象的键的联合类型。

  • 【is】关键字:它被称为类型谓词,用来判断一个变量属于某个接口或类型,比如:

const isNumber = (val: unknown): val is number => typeof val === 'number'
const isString = (val: unknown): val is string => typeof val === 'string'
  • 【keyof】关键字:用于获取某种类型的所有键,其返回类型是联合类型,比如:

interface Person {name: string;age: number;
}
type K = keyof Person; // "name" | "age"
  • 【typeof】关键字:js 中的 typeof 只能获取几种类型,而在 ts 中 typeof 用来获取一个变量声明或对象的类型,比如:

interface Person {name: string;age: number;
}const sem: Person = { name: 'semlinker', age: 30 };
type Sem = typeof sem; // -> Person

3.10 判断是否某种类型

// 判断数组
export const isArray = Array.isArray// 对象转字符串
export const objectToString = Object.prototype.toString
export const toTypeString = (value: unknown): string =>objectToString.call(value)// 判断是否 Map 对象
export const isMap = (val: unknown): val is Map<any, any> =>toTypeString(val) === '[object Map]'// 判断是否 Set 对象
export const isSet = (val: unknown): val is Set<any> =>toTypeString(val) === '[object Set]'// 判断是否 Date 对象
export const isDate = (val: unknown): val is Date => val instanceof Date// 判断是否函数
export const isFunction = (val: unknown): val is Function =>typeof val === 'function'// 判断是否字符串
export const isString = (val: unknown): val is string => typeof val === 'string'// 判断是否 Symbol
export const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol'// 判断是否对象(不包括 null)
export const isObject = (val: unknown): val is Record<any, any> =>val !== null && typeof val === 'object'// 判断是否 Promise
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {return isObject(val) && isFunction(val.then) && isFunction(val.catch)
}

有了这些函数就可以在工作中用起来啦。

3.11 toRawType:对象转字符串,截取后第八位到倒数第二位。

export const toRawType = (value: unknown): string => {// extract "RawType" from strings like "[object RawType]"return toTypeString(value).slice(8, -1)
}

可以截取到 String Array 等这些类型,这个函数可以用来做类型判断。

3.12  isPlainObject:判断是否纯粹的对象

export const isPlainObject = (val: unknown): val is object =>toTypeString(val) === '[object Object]'

3.13 isIntegerKey:判断是不是数字型的字符串 key 值

export const isIntegerKey = (key: unknown) =>isString(key) &&key !== 'NaN' &&key[0] !== '-' &&'' + parseInt(key, 10) === key

第一步先判断 key 是否是字符串类型(作为 key 值有两种类型,string 和 symbol),第二步排除 NaN 值,第三步排除 - 值(排除负数),第四步将 key 转换成数字再隐式转换为字符串,与原 key 对比。

3.14 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,' +'onVnodeBeforeMount,onVnodeMounted,' +'onVnodeBeforeUpdate,onVnodeUpdated,' +'onVnodeBeforeUnmount,onVnodeUnmounted'
)// 使用:
isReservedProp("key") // true
isReservedProp("test") // false
isReservedProp("") // true

如何解读这个函数?先看 makeMap,它传入一个字符串,将这个字符串转换成数组,并循环赋值 key 給一个空对象map,然后返回一个包含参数 val 的闭包用来检查 val 是否是存在在字符串中。

isReservedProp("key") 其实就相当于 makeMap(str)("key")。

3.15 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
}// 使用例子:
// "-"连字符转小驼峰
// \w:0-9a-zA-Z_,表示由数字,大小写字母和下划线组成
const camelizeRE = /-(\w)/g
export const camelize = cacheStringFunction((str: string): string => {return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})
camelize("text-node") // "textNode"// 大写字母转"-"连字符
// \B 是指 非 \B 单词边界。
const hyphenateRE = /\B([A-Z])/g;
const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase());
hyphenate("WordPress") // "word-press"// 首字母转大写
const capitalize = cacheStringFunction((str: string) => str.charAt(0).toUpperCase() + str.slice(1)
)
const toHandlerKey = cacheStringFunction((str) => (str ? `on${capitalize(str)}` : ``));
toHandlerKey('click') // "onClick"

这个函数和上面 makeMap 函数类似,传入一个 fn 参数,返回一个包含参数 str 的闭包,将这个 str 字符串作为 key 赋值给一个空对象 cache,闭包返回 cache[str] || (cache[str] = fn(str))。

【cache[str] || (cache[str] = fn(str))】的意思是,如果 cache 有缓存到 str 这个 key,直接返回对应的值,否则,先调用 fn(str),再赋值给 cache[str],这样可以将需要经过 fn 函数处理的字符串缓存起来,避免多次重复处理字符串。

3.16 hasChanged:判断值是否有变化

const hasChanged = (value: any, oldValue: any): boolean =>!Object.is(value, oldValue)

Object.is 方法判断两个值是否为同一个值。

3.17  invokeArrayFns:执行数组里的函数

export const invokeArrayFns = (fns: Function[], arg?: any) => {for (let i = 0; i < fns.length; i++) {fns[i](arg)}
}

这种写法方便统一执行多个函数。

3.18 def:定义一个不可枚举的对象

export const def = (obj: object, key: string | symbol, value: any) => {Object.defineProperty(obj, key, {configurable: true,enumerable: false,value})
}

Object.defineProperty,语法:Object.defineProperty(obj, prop, descriptor),它是一个非常重要的 API,经常会在源码中看见它。

在 ES3 中,除了一些内置属性(如:Math.PI),对象所有的属性在任何时候都可以被[修改、插入、删除。

在ES5 中,我们可以设置属性是否可以被改变或是被删除——在这之前,它是内置属性的特权。

ES5 中引入了属性描述符的概念,我们可以通过它对所定义的属性有更大的控制权,这些属性描述符(特性)包括:value —— 获取属性时所返回的值。writable —— 该属性是否可写。enumerable —— 该属性在 for in 循环中是否会被枚举。configurable —— 该属性是否可被删除。set() —— 该属性的更新操作所调用的函数。get() —— 获取属性值时所调用的函数。

另外,数据描述符(其中属性为:enumerable,configurable,value,writable)与存取描述符(其中属性为enumerable,configurable,set(),get())之间是有互斥关系的。在定义了set()和get()之后,描述符会认为存取操作已被定义了,其中再定义 value 和 writable 会引起错误

3.19 toNumber:转数字

  export const toNumber = (val: any): any => {const n = parseFloat(val)return isNaN(n) ? val : n}

3.20 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: {}))
}

第一次调用这个函数时,_globalThis 肯定为 "undefined",接着执行【||】后的语句。

  1. typeof globalThis !== 'undefined' 如果 globalThis 不是 undefined,返回 globalThis:MDN globalThis。否则 ->

  2. typeof self !== 'undefined' 如果 self 不是 undefined,返回 self。否则 ->

  3. typeof window !== 'undefined' 如果 window 不是 undefined,返回 widow。否则 ->

  4. typeof global !== 'undefined' 如果 global 不是 undefined,返回 global。否则 ->

  5. 返回 {}

第二次调用这个函数,就直接返回 _globalThis,不需要第二次继续判断了????

4. 感想

  • 很多工具函数可以通过做缓存以达到优化性能的目的

  • Object 对象 API 解析 无论什么时候都不过时,适合反复阅读,加深对 Object 的理解

  • 工作中如果有用到类似的工具函数,可参考这些写法

  • 学习了一些 typescript 不太常见的语法:【! 非空断言操作符】【?? 空值合并运算符】

  • 生成 sourcemap 调试 ts 代码

最近组建了一个湖南人的前端交流群,如果你是湖南人可以加我微信 ruochuan12 私信 湖南 拉你进群。


推荐阅读

我在阿里招前端,该怎么帮你(可进面试群)
我读源码的经历

面对 this 指向丢失,尤雨溪在 Vuex 源码中是怎么处理的
老姚浅谈:怎么学JavaScript?

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》多篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,活跃在知乎@若川,掘金@若川。致力于分享前端开发经验,愿景:帮助5年内前端人走向前列。

识别方二维码加我微信、拉你进源码共读

今日话题

略。欢迎分享、收藏、点赞、在看我的公众号文章~

学习尤雨溪写的 Vue3 源码中的简单工具函数相关推荐

  1. 跟尤雨溪一起解读Vue3源码笔记- Vue Mastery

    P1.介绍 1.什么是DOM或文档对象模型? HTML映射到一系列DOM节点,我们可以使用JavaScript进行操作 let item = document.getElementsByTagName ...

  2. 尤雨溪的5KB petite-vue源码解析

    写在开头 近期尤雨溪发布了5kb的petite-vue,好奇的我,clone了他的源码,给大家解析一波. 最近由于工作事情多,所以放缓了原创的脚步!大家谅解 想看我往期手写源码+各种源码解析的可以关注 ...

  3. 初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

    1. 前言 大家好,我是若川.最近组织了源码共读活动.每周读 200 行左右的源码.很多第一次读源码的小伙伴都感觉很有收获,感兴趣可以加我微信ruochuan12,拉你进群学习. 写相对很难的源码,耗 ...

  4. 尤雨溪写的100多行的“玩具 vite”,十分有助于理解 vite 原理

    1. 前言 大家好,我是若川.最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 想学源码,极力推荐之前我写的<学习源码整体架构系列>jQuery.underscore.l ...

  5. bigdecimal判断等于0_vue2.0源码用到的工具函数,12个简易的复用函数,看看有多简单...

    戎马:https://segmentfault.com/a/1190000019679638 1. 创建一个被冻结的空对象 export const emptyObject = Object.free ...

  6. 深入解析 Vue 的热更新原理,尤大是如何巧用源码中的细节?

    大家都用过 Vue-CLI 创建 vue 应用,在开发的时候我们修改了 vue 文件,保存了文件,浏览器上就自动更新出我们写的组件内容,非常的顺滑流畅,大大提高了开发效率.想知道这背后是怎么实现的吗, ...

  7. vue3源码中的最长递增子序列

    求解最长递增子序列是一道经典的算法题, 多数解法是使用动态规划的思想,算法的时间复杂度是O(); 而Vue.js内部使用的是维基百科提供的一套"贪心+二分查找"的算法; 贪心算法的 ...

  8. 尤雨溪-写一个mini vue

    render 主题: render 解决问题: 1.h是什么?有什么用? h是hyperScript. 作用: 使用JS去写html.因为html最后也是render成JS 1,when to use ...

  9. axios源码中的10多个工具函数,值得一学~

    大家好,我是若川.最近组织了源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列> ...

最新文章

  1. jQuery之表格变色
  2. 春运前五日全国累计发送旅客3.49亿人次 同比增0.58%
  3. GateOne Web SSH 环境搭建
  4. Win7局域网打印机共享设置(详细图文流程)
  5. web前端常用知识点
  6. [css] 如何用css实现把“我不爱996”变成“699爱不我”?
  7. CVSNT Manual
  8. 精选| 2019年3月R新包推荐
  9. 登录linux系统设置默认目录
  10. 【C语言】c语言练习题【2】(适合初学者)
  11. 使用无人机倾斜摄影测量技术采集某县城区地理信息数据并生成实景三维模型的案例
  12. 百度网盘和百度云有什么区别
  13. 企业如何规避劳务派遣中的法律风险?
  14. nginx重启(nginx -s reload 不建议使用,本人亲试,有时候无效)
  15. 5G/NR 上行免授权
  16. ZT210打印标签方法及常见问题20230110
  17. 如何分别在Android、iOS、Windows Phone三大平台进行“触摸屏”开发?
  18. avue框架的Scoped Slot自定义汇总
  19. php省市区三级联动,thinkPHP实现的省市区三级联动功能示例
  20. Apriori寻找频繁项集——python3.x实现

热门文章

  1. vue 文件及描述信息一起上传_用Vue实现一个大文件上传和断点续传
  2. wingdows安装psutil_psutil模块安装指南(win与linux)
  3. swift语言和python区别_Swift为什么能成为编程语言中的黑马?
  4. 51单片机C语言led流水灯及数码管实现秒表
  5. treeview自动从表中添加标题和列值做目录的方法2
  6. iOS iphone屏幕分析(岂止而大)
  7. opencv 和 parfor
  8. 鸟哥学习笔记六(基础篇第十一章)
  9. 计算机网络第1章(概述)
  10. python leetcode_leetcode 刷题经验,主力 python