- 前言
大家好,我是若川。为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力组织了每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信
ruochuan02
参与。
之前写的《学习源码整体架构系列》 包含jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
20余篇源码文章。
写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。
所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。比如工具函数。本文通过学习Vue3
源码中的工具函数模块的源码,学习源码为自己所用。歌德曾说:读一本好书,就是在和高尚的人谈话。
同理可得:读源码,也算是和作者的一种学习交流的方式。
阅读本文,你将学到:
1 | js复制代码1. 如何学习 JavaScript 基础知识,会推荐很多学习资料 |
shared
模块中57个
工具函数,本次阅读其中的30余个
。
- 环境准备
2.1 读开源项目 贡献指南
打开 vue-next,
开源项目一般都能在 README.md
或者 .github/contributing.md 找到贡献指南。
而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来,项目目录结构是怎样的。怎么投入开发,需要哪些知识储备等。
我们可以在 项目目录结构 描述中,找到shared
模块。
shared
: Internal utilities shared across multiple packages (especially environment-agnostic utils used by both runtime and compiler packages).
README.md
和 contributing.md
一般都是英文的。可能会难倒一部分人。其实看不懂,完全可以可以借助划词翻译,整页翻译和百度翻译等翻译工具。再把英文加入后续学习计划。
本文就是讲shared
模块,对应的文件路径是:vue-next/packages/shared/src/index.ts
也可以用github1s
访问,速度更快。github1s packages/shared/src/index.ts
2.2 按照项目指南 打包构建代码
为了降低文章难度,我按照贡献指南中方法打包把ts
转成了js
。如果你需要打包,也可以参考下文打包构建。
你需要确保 Node.js 版本是 10+
, 而且 yarn
的版本是 1.x
Yarn 1.x。
你安装的 Node.js
版本很可能是低于 10
。最简单的办法就是去官网重新安装。也可以使用 nvm
等管理Node.js
版本。
1 | bash复制代码node -v |
可以得到 vue-next/packages/shared/dist/shared.esm-bundler.js
,文件也就是纯js
文件。接下来就是解释其中的一些方法。
当然,前面可能比较啰嗦。我可以直接讲
3. 工具函数
。但通过我上文的介绍,即使是初学者,都能看懂一些开源项目源码,也许就会有一定的成就感。
另外,面试问到被类似的问题或者笔试题时,你说看Vue3
源码学到的,面试官绝对对你刮目相看。
2.3 如何生成 sourcemap 调试 vue-next 源码
熟悉我的读者知道,我是经常强调生成sourcemap
调试看源码,所以顺便提一下如何配置生成sourcemap
,如何调试。这部分可以简单略过,动手操作时再仔细看。
其实贡献指南里描述了。
Build with Source Maps
Use the--sourcemap
or-s
flag to build with source maps. Note this will make the build much slower.
所以在 vue-next/package.json
追加 "dev:sourcemap": "node scripts/dev.js --sourcemap"
,yarn dev:sourcemap
执行,即可生成sourcemap
,或者直接 build
。
1 | json复制代码// package.json |
会在控制台输出类似vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js
的信息。
其中packages/vue/dist/vue.global.js.map
就是sourcemap
文件了。
我们在 Vue3官网找个例子,在 vue-next/examples/index.html
。其内容引入packages/vue/dist/vue.global.js
。
1 | js复制代码// vue-next/examples/index.html |
然后我们新建一个终端窗口,yarn serve
,在浏览器中打开http://localhost:5000/examples/
,如下图所示,按F11
等进入函数,就可以愉快的调试源码了。
- 工具函数
本文主要按照源码 vue-next/packages/shared/src/index.ts
的顺序来写。也省去了一些从外部导入的方法。
我们也可以通过ts
文件,查看使用函数的位置。同时在VSCode
运行调试JS代码,我们比较推荐韩老师写的code runner
插件。
3.1 babelParserDefaultPlugins babel 解析默认插件
1 | js复制代码/** |
这里就是几个默认插件。感兴趣看英文注释查看。
3.2 EMPTY_OBJ 空对象
1 | js复制代码const EMPTY_OBJ = (process.env.NODE_ENV !== 'production') |
process.env.NODE_ENV
是 node
项目中的一个环境变量,一般定义为:development
和production
。根据环境写代码。比如开发环境,有报错等信息,生产环境则不需要这些报错警告。
3.3 EMPTY_ARR 空数组
1 | js复制代码const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : []; |
3.4 NOOP 空函数
1 | js复制代码const NOOP = () => { }; |
3.5 NO 永远返回 false 的函数
1 | js复制代码/** |
3.6 isOn 判断字符串是不是 on 开头,并且 on 后首字母不是小写字母
1 | js复制代码const onRE = /^on[^a-z]/; |
onRE
是正则。^
符号在开头,则表示是什么开头。而在其他地方是指非。
与之相反的是:$
符合在结尾,则表示是以什么结尾。
[^a-z]
是指不是a
到z
的小写字母。
同时推荐一个正则在线工具。
另外正则看老姚的迷你书就够用了。
3.7 isModelListener 监听器
判断字符串是不是以onUpdate:
开头
1 | js复制代码const isModelListener = (key) => key.startsWith('onUpdate:'); |
很多方法都在《ES6入门教程》中有讲到,就不赘述了。
3.8 extend 继承 合并
说合并可能更准确些。
1 | js复制代码const extend = Object.assign; |
3.9 remove 移除数组的一项
1 | js复制代码const remove = (arr, el) => { |
splice
其实是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置。
引申:axios InterceptorManager
拦截器源码 中,拦截器用数组存储的。但实际移除拦截器时,只是把拦截器置为 null
。而不是用splice
移除。最后执行时为 null
的不执行,同样效果。axios
拦截器这个场景下,不得不说为性能做到了很好的考虑。
看如下 axios
拦截器代码示例:
1 | js复制代码// 代码有删减 |
3.10 hasOwn 是不是自己本身所拥有的属性
1 | js复制代码const hasOwnProperty = Object.prototype.hasOwnProperty; |
对象API可以看我之前写的一篇文章JavaScript 对象所有API解析,写的还算全面。
3.11 isArray 判断数组
1 | js复制代码const isArray = Array.isArray; |
3.12 isMap 判断是不是 Map 对象
1 | js复制代码const isMap = (val) => toTypeString(val) === '[object Map]'; |
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
3.13 isSet 判断是不是 Set 对象
1 | js复制代码const isSet = (val) => toTypeString(val) === '[object Set]'; |
ES6
提供了新的数据结构Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set
本身是一个构造函数,用来生成 Set
数据结构。
3.14 isDate 判断是不是 Date 对象
1 | js复制代码const isDate = (val) => val instanceof Date; |
3.15 isFunction 判断是不是函数
1 | js复制代码const isFunction = (val) => typeof val === 'function'; |
3.16 isString 判断是不是字符串
1 | js复制代码const isString = (val) => typeof val === 'string'; |
3.17 isSymbol 判断是不是 Symbol
1 | js复制代码const isSymbol = (val) => typeof val === 'symbol'; |
ES6
引入了一种新的原始数据类型Symbol
,表示独一无二的值。
3.18 isObject 判断是不是对象
1 | js复制代码const isObject = (val) => val !== null && typeof val === 'object'; |
3.19 isPromise 判断是不是 Promise
1 | js复制代码const isPromise = (val) => { |
可以根据文末推荐的书籍看Promise
相关章节掌握。同时也推荐这本迷你书JavaScript Promise迷你书(中文版)
3.20 objectToString 对象转字符串
1 | js复制代码const objectToString = Object.prototype.toString; |
3.21 toTypeString 对象转字符串
1 | js复制代码const toTypeString = (value) => objectToString.call(value); |
3.22 toRawType 对象转字符串 截取后几位
1 | js复制代码const toRawType = (value) => { |
可以 截取到 String
Array
等这些类型
是 JS
判断数据类型非常重要的知识点。
JS
判断类型也有 typeof ,但不是很准确,而且能够识别出的不多。
这些算是基础知识
mdn typeof 文档,文档比较详细,也实现了一个很完善的type
函数,本文就不赘述了。
1 | js复制代码// typeof 返回值目前有以下8种 |
3.23 isPlainObject 判断是不是纯粹的对象
1 | js复制代码const objectToString = Object.prototype.toString; |
3.24 isIntegerKey 判断是不是数字型的字符串key值
1 | js复制代码const isIntegerKey = (key) => isString(key) && |
3.25 makeMap && isReservedProp
传入一个以逗号分隔的字符串,生成一个 map
(键值对),并且返回一个函数检测 key
值在不在这个 map
中。第二个参数是小写选项。
1 | js复制代码/** |
3.26 cacheStringFunction 缓存
1 | js复制代码const cacheStringFunction = (fn) => { |
这个函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。
《JavaScript 设计模式与开发实践》书中的第四章 JS单例模式也是类似的实现。
1 | js复制代码var getSingle = function(fn){ // 获取单例 |
以下是一些正则,系统学习正则推荐老姚:《JavaScript 正则表达式迷你书》问世了!,看过的都说好。所以本文不会过多描述正则相关知识点。
1 | js复制代码// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成 |
3.27 hasChanged 判断是不是有变化
hasChanged
这个方法,值得一提的是:我刚写这篇文章时,还没有用Object.is
,后来看 git
记录发现有人 提PR 修改为Object.is
了,尤大合并了。
1 | js复制代码const hasChanged = (value, oldValue) => !Object.is(value, oldValue); |
以下是原先的源码。
1 | js复制代码// compare whether a value has changed, accounting for NaN. |
根据 hasChanged
这个我们继续来看看:Object.is
API
。
Object.is(value1, value2) (ES6)
该方法用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致。 不同之处只有两个:一是+0
不等于-0
,而是 NaN
等于自身。
1 | js复制代码Object.is('若川', '若川'); // true |
ES5
可以通过以下代码部署Object.is
。
1 | js复制代码Object.defineProperty(Object, 'is', { |
根据举例可以说明
3.28 invokeArrayFns 执行数组里的函数
1 | js复制代码const invokeArrayFns = (fns, arg) => { |
为什么这样写,我们一般都是一个函数执行就行。
数组中存放函数,函数其实也算是数据。这种写法方便统一执行多个函数。
3.29 def 定义对象属性
1 | js复制代码const def = (obj, key, value) => { |
Object.defineProperty
算是一个非常重要的API
。还有一个定义多个属性的API
:Object.defineProperties(obj, props) (ES5)
Object.defineProperty
涉及到比较重要的知识点。
在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
会引起错误。
以下是ES3风格的属性定义方式:
1 | js复制代码var person = {}; |
以下是等价的ES5通过数据描述符定义属性的方式:
1 | js复制代码var person = {}; |
其中, 除了value的默认值为undefined
以外,其他的默认值都为false
。这就意味着,如果想要通过这一方式定义一个可写的属性,必须显示将它们设为true
。
或者,我们也可以通过ES5
的存储描述符来定义:
1 | js复制代码var person = {}; |
这样一来,多了许多可以用来描述属性的代码,如果想要防止别人篡改我们的属性,就必须要用到它们。此外,也不要忘了浏览器向后兼容ES3
方面所做的考虑。例如,跟添加Array.prototype
属性不一样,我们不能再旧版的浏览器中使用shim
这一特性。
另外,我们还可以(通过定义nonmalleable
属性),在具体行为中运用这些描述符:
1 | js复制代码var person = {}; |
其他本文就不过多赘述了。更多对象 API
可以查看这篇文章JavaScript 对象所有API解析。
3.30 toNumber 转数字
1 | js复制代码const toNumber = (val) => { |
其实
isNaN
本意是判断是不是NaN
值,但是不准确的。
比如:isNaN('a')
为true
。
所以ES6
有了Number.isNaN
这个判断方法,为了弥补这一个API
。
1 | js复制代码Number.isNaN('a') // false |
3.31 getGlobalThis 全局对象
1 | js复制代码let _globalThis; |
获取全局 this
指向。
初次执行肯定是 _globalThis
是 undefined
。所以会执行后面的赋值语句。
如果存在 globalThis
就用 globalThis
。MDN globalThis
如果存在self
,就用self
。在 Web Worker
中不能访问到 window
对象,但是我们却能通过 self
访问到 Worker
环境中的全局对象。
如果存在window
,就用window
。
如果存在global
,就用global
。Node
环境下,使用global
。
如果都不存在,使用空对象。可能是微信小程序环境下。
下次执行就直接返回 _globalThis
,不需要第二次继续判断了。这种写法值得我们学习。
- 最后推荐一些文章和书籍
先推荐我认为不错的JavaScript API
的几篇文章和几本值得读的书。
JavaScript 对象所有API解析 lxchuan12.gitee.io/js-object-a…
《JavaScript面向对象编程2》 面向对象讲的很详细。
我也是从小白看不懂书经历过来的。到现在写文章分享。
我看书的方法:多本书同时看,看相同类似的章节,比如函数。看完这本可能没懂,看下一本,几本书看下来基本就懂了,一遍没看懂,再看几遍,可以避免遗忘,巩固相关章节。当然,刚开始看书很难受,看不进。这些书大部分在微信读书都有,如果习惯看纸质书,那可以买来看。
这时可以看些视频和动手练习一些简单的项目。
比如:可以自己注册一个github
账号,分章节小节,抄写书中的代码,提交到github
,练习了才会更有感觉。
再比如 freeCodeCamp 中文在线学习网站 网站。看书是系统学习非常好的方法。后来我就是看源码较多,写文章分享出来给大家。
- 总结
文中主要通过学习 shared
模块下的几十个工具函数,比如有:isPromise
、makeMap
、cacheStringFunction
、invokeArrayFns
、def
、getGlobalThis
等等。
同时还分享了vue
源码的调试技巧,推荐了一些书籍和看书籍的方法。
源码也不是那么可怕。平常我们工作中也是经常能使用到这些工具函数。通过学习一些简单源码,拓展视野的同时,还能落实到自己工作开发中,收益相对比较高。
关于
最后可以持续关注我@若川。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以点此扫码加我微信 ruochuan02
参与。
另外,想学源码,极力推荐关注我写的专栏《学习源码整体架构系列》,目前是掘金关注人数(4.2k+人)第一的专栏,写有20余篇源码文章。包含jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
、koa-compose
、vue 3.2 发布
、vue-this
、create-vue
、玩具vite
、create-vite
等20余篇源码文章。
作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan02
。前端路上 | 所知甚少,唯善学。
关注公众号若川视野,每周一起学源码,学会看源码,成为高级前端。
segmentfault
若川视野专栏,开通了若川视野专栏,欢迎关注~
掘金专栏,欢迎关注~
知乎若川视野专栏,开通了若川视野专栏,欢迎关注~
github blog,求个star
^_^~
本文转载自: 掘金