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

大家好,我是若川。最近组织了源码共读活动。每周读 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年内前端人走向前列。

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

今日话题

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/275543.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux manjaro系统安装后无法连接wifi,解决方案

2019独角兽企业重金招聘Python工程师标准>>> 笔记本为联想 thinkpad E480 首先通过命令lspci -k看一下原因是否为缺少wifi驱动&#xff0c;如下&#xff0c;如果没有Kernel driver in use&#xff0c;说明缺少驱动。05:00.0 Network controller: Realtek Semiconduc…

检测输入路径是否存在错误_为什么存在用户输入错误

检测输入路径是否存在错误Errors are a fact of life when using almost any type of software. Forms are the worst though. Nothing is more frustrating than filling out a form and getting a robotic message from the computer telling you that you have failed, plea…

若川邀你进 源码共读 群~长期交流学习

大家好&#xff0c;我是若川。这是一个愉快的周六~估计还是有很多读者不知道我。若川名字由来是取自&#xff1a;上善若水&#xff0c;海纳百川。顺便放两篇文章。我读源码的经历&#xff0c;跟各位读者朋友分享下公众号运营策略加我微信进 源码共读 群最近组织了近200人每周源…

2005 打开 2010 项目经验总结

下面是网上的直接复制粘贴&#xff1a;网址为 http://hi.baidu.com/zealot886/blog/item/7364d4266a2a1555ac34dea6.html/cmtid/65ff140a660e02246159f3db 这里是我自己的总结 &#xff08; 1、用vs2010 将该解决方案的所有 项目都改为 net 2.0&#xff08;方法&#xff0c;右击…

读取linux的运行状态,Linux下安装使用sar工具来获取系统运行状态

sar 找出系统瓶颈的利器sar是System Activity Reporter(系统活动情况报告)的缩写。sar工具将对系统当前的状态进行取样&#xff0c;然后通过计算数据和比例来表达系统的当前运行状态。它的 特点是可以连续对系统取样&#xff0c;获得大量的取样数据&#xff1b;取样数据和分析的…

错过校招_我们在用户测试中容易错过的事情

错过校招What makes a tool well designed? As a designer, I’ve thought about this question for a long time, and over the past few years I’ve developed a system that I now use with every new project I approach, from small startups to large companies like L…

这些 JS 中强大的操作符,总有几个你没听说过

大家好&#xff0c;我是若川。今天推荐一篇相对简单些的文章。大家应该都知道了我最近组织了源码共读活动&#xff0c; 有小伙伴表示读源码上瘾&#xff0c;也很有收获。工作0-5年都可以参与。感兴趣可以加我微信 ruochuan12 私信 源码 进群。1. 数值分割符 _2. 逗号运算符 ,3.…

es6冲刺01

1、let/const 1)作用域&#xff1a;es5中有全局作用域、函数作用域。es6中新增了块级作用域 2&#xff09;let定义的变量在所在块级作用域外失效&#xff0c;严格模式下失效后直接报错&#xff0c; 且不允许重复声明同名变量 3)const用于声明常量&#xff0c;声明时必须赋值&am…

linux网卡固件名,修改CentOS7网卡名称为传统名称eth0格式

使用CentOS7以前系统的小伙伴装完CentOS7以后发现了一个问题&#xff0c;那就是网卡名改变为了“en016777736”&#xff0c;而不是以前的eth0的简易模式了&#xff0c;如图&#xff1a;以往的CentOS7以前的系统网卡命名虽然简单方便&#xff0c;但也会带来一些问题&#xff0c;…

Baymard Institute:基于UX的最佳实践的光荣的,循证的工具

重点 (Top highlight)I realized I wanted to write this piece when I mentioned the Baymard Institute to a User Researcher with 10 years of experience and they had no idea what I was talking about. They aren’t alone! I’ve gotten plenty of raised eyebrows on…

Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

1. 前言大家好&#xff0c;我是若川。最近组织了源码共读活动&#xff0c;感兴趣的可以加我微信 ruochuan12&#xff0c;长期交流学习。之前写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4十篇源码文章。…

wireshark使用教程 linux,Linux入门教程:ubuntu下安装wireshark(以及配置非root),这个强大的工具可以捕...

Linux入门教程:ubuntu下安装wireshark(以及配置非root),这个强大的工具可以捕Wireshark是世界上最流行的网络分析工具。这个强大的工具可以捕捉网络中的数据&#xff0c;并为用户提供关于网络和上层协议的各种信息。与很多其他网络工具一样&#xff0c;Wireshark也使用pcap net…

IronPython和C#执行速度对比

其实我自己对执行速度这个问题本来并没有什么兴趣&#xff0c;因为以前的经验告诉我&#xff1a;除非是运算密集型的程序&#xff0c;否则脚本语言和编译型语言使用起来速度没有多大差别。但是我们公司有个人知道我的想法以后&#xff0c;天天在我耳边嚷嚷脚本运行速度太慢&…

基于超级账本Fabric的供应链跟踪解决方案【开源】

2019独角兽企业重金招聘Python工程师标准>>> 本项目为基于Hyperledger Fabric区块链的供应链资产跟踪解决方案&#xff0c;项目主要包括链码和Web应用两部分。Fabric链码采用GOLANG开发&#xff0c;负责维护资产的状态&#xff0c;后台为采用Node.js开发的Web应用&a…

同理心案例及故事分享_神经形态,视觉可及性和同理心

同理心案例及故事分享“A good UX designer has empathy”.“优秀的UX设计人员具有同理心”。 This is something every UX designer has heard at some point in their career. Empathy helps us get into the mindset of the user and build solutions that solve real probl…

纯CSS实现beautiful按钮

大家好&#xff0c;我是若川。邀你进源码共读群学习交流。今天分享一篇好文。可收藏&#xff5e;近期工作中遇到一个需求——实现一些酷炫的按钮&#xff0c;看到效果图之后&#xff0c;按钮确实漂亮&#xff0c;有弹跳、颜色渐变、扫光、霓虹灯&#xff0c;瞬间激起了我的好奇…

linux的内核有多小,Linux 内核有小bug?

今天读着读着Linux代码&#xff0c;竟然无意中发现Linux 0.11内核有个小bug&#xff0c;呵呵&#xff0c;人非圣贤孰能无过。// 在目录项数据块中搜索匹配指定文件名的目录项&#xff0c;首先让de 指向数据块&#xff0c;并在不超过目录中目录项数// 的条件下&#xff0c;循环执…

菜单窗口_菜单

菜单窗口The Hamburger Menu widget is on every other site nowadays. It has become synonymous with the web and, perhaps even more so, with web development. Have, for instance, a look at Dribbble or Codepen. There you’ll find a fair share of examples. They c…

怎么在PDF上修改文字,PDF修改文字的步骤

怎么在PDF文件上修改文字呢&#xff1f;其实现在的很多的PDF文件上会出现文字错误的情况&#xff0c;想要修改PDF文件上面的文字却不知道怎么修改&#xff0c;想要修改PDF文件还是比较简单的&#xff0c;使用专业的PDF编辑器就可以进行操作了&#xff0c;下面小编就为大家分享一…

读完 Vue 发布源码,小姐姐回答了 leader 的提问,并优化了项目发布流程~

大家好&#xff0c;我是若川。这是 源码共读 第三期活动&#xff0c;纪年小姐姐的第三次投稿。纪年小姐姐学习完优化了自己的项目发布流程&#xff0c;而且回答了leader对她的提问&#xff0c;来看看她的思考和实践。第三期是 Vue 3.2 发布了&#xff0c;那尤雨溪是怎么发布 Vu…