发表时间:2022-03-26来源:网络
大家好,我是若川。最近组织了源码共读活动。每周读 200 行左右的源码。很多第一次读源码的小伙伴都感觉很有收获,感兴趣可以加我,拉你进群学习。
写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。
所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。比如工具函数。本文通过学习Vue3源码中的工具函数模块的源码,学习源码为自己所用。歌德曾说:读一本好书,就是在和高尚的人谈话。同理可得:读源码,也算是和作者的一种学习交流的方式。
阅读本文,你将学到:
1. 如何学习 JavaScript 基础知识,会推荐很多学习资料 2. 如何学习调试 vue 3 源码 3. 如何学习源码中优秀代码和思想,投入到自己的项目中 4. Vue 3 源码 shared 模块中的几十个实用工具函数 5. 我的一些经验分享shared模块中57个工具函数,本次阅读其中的30余个。
打开 vue-next[1], 开源项目一般都能在 README.md 或者 .github/contributing.md[2] 找到贡献指南。
而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来,项目目录结构是怎样的。怎么投入开发,需要哪些知识储备等。
我们可以在 项目目录结构[3] 描述中,找到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`[4]
也可以用github1s访问,速度更快。github1s packages/shared/src/index.ts[5]
为了降低文章难度,我按照贡献指南中方法打包把ts转成了js。如果你需要打包,也可以参考下文打包构建。
你需要确保 Node.js[6] 版本是 10+, 而且 yarn 的版本是 1.x Yarn 1.x[7]。
你安装的 Node.js 版本很可能是低于 10。最简单的办法就是去官网重新安装。也可以使用 nvm等管理Node.js版本。
node -v # v14.16.0 # 全局安装 yarn # 克隆项目 git clone https://github.com/vuejs/vue-next.git cd vue-next # 或者克隆我的项目 git clone https://github.com/lxchuan12/vue-next-analysis.git cd vue-next-analysis npm install --global yarn yarn # install the dependencies of the project yarn build可以得到 vue-next/packages/shared/dist/shared.esm-bundler.js,文件也就是纯js文件。也接下就是解释其中的一些方法。
当然,前面可能比较啰嗦。我可以直接讲 3. 工具函数。但通过我上文的介绍,即使是初学者,都能看懂一些开源项目源码,也许就会有一定的成就感。另外,面试问到被类似的问题或者笔试题时,你说看Vue3源码学到的,面试官绝对对你刮目相看。
熟悉我的读者知道,我是经常强调生成sourcemap调试看源码,所以顺便提一下如何配置生成sourcemap,如何调试。这部分可以简单略过,动手操作时再仔细看。
其实贡献指南[8]里描述了。
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。
// package.json { "version": "3.2.1", "scripts": { "dev:sourcemap": "node scripts/dev.js --sourcemap" } }会在控制台输出类似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。
// vue-next/examples/index.html const Counter = { data() { return { counter: 0 } } } Vue.createApp(Counter).mount('#counter')然后我们新建一个终端窗口,yarn serve,在浏览器中打开http://localhost:5000/examples/,如下图所示,按F11等进入函数,就可以愉快的调试源码了。
vue-next-debugger
本文主要按照源码 `vue-next/packages/shared/src/index.ts`[9] 的顺序来写。也省去了一些从外部导入的方法。
我们也可以通过ts文件,查看使用函数的位置。同时在VSCode运行调试JS代码,我们比较推荐韩老师写的code runner插件。
这里就是几个默认插件。感兴趣看英文注释查看。
process.env.NODE_ENV 是 node 项目中的一个环境变量,一般定义为:development 和production。根据环境写代码。比如开发环境,有报错等信息,生产环境则不需要这些报错警告。
onRE 是正则。^符号在开头,则表示是什么开头。而在其他地方是指非。
与之相反的是:$符合在结尾,则表示是以什么结尾。
[^a-z]是指不是a到z的小写字母。
同时推荐一个正则在线工具。
regex101[10]
另外正则看老姚的迷你书就够用了。
老姚:《JavaScript 正则表达式迷你书》问世了![11]
判断字符串是不是以onUpdate:开头
const isModelListener = (key) => key.startsWith('onUpdate:'); // 例子: isModelListener('onUpdate:change'); // true isModelListener('1onUpdate:change'); // false // startsWith 是 ES6 提供的方法ES6入门教程:字符串的新增方法[12]
很多方法都在《ES6入门教程》中有讲到,就不赘述了。
说合并可能更准确些。
const extend = Object.assign; // 例子: const data = { name: '若川' }; const data2 = extend(data, { mp: '若川视野', name: '是若川啊' }); console.log(data); // { name: "是若川啊", mp: "若川视野" } console.log(data2); // { name: "是若川啊", mp: "若川视野" } console.log(data === data2); // truesplice 其实是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置。
引申:`axios InterceptorManager` 拦截器源码[13] 中,拦截器用数组存储的。但实际移除拦截器时,只是把拦截器置为 null 。而不是用splice移除。最后执行时为 null 的不执行,同样效果。axios 拦截器这个场景下,不得不说为性能做到了很好的考虑。
看如下 axios 拦截器代码示例:
// 代码有删减 // 声明 this.handlers = []; // 移除 if (this.handlers[id]) { this.handlers[id] = null; } // 执行 if (h !== null) { fn(h); }对象API可以看我之前写的一篇文章JavaScript 对象所有API解析,写的还算全面。
ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。
可以根据文末推荐的书籍看Promise相关章节掌握。同时也推荐这本迷你书JavaScript Promise迷你书(中文版)[14]
可以 截取到 String Array 等这些类型
是 JS 判断数据类型非常重要的知识点。
JS 判断类型也有 typeof ,但不是很准确,而且能够识别出的不多。
这些算是基础知识
mdn typeof 文档[15],文档比较详细,也实现了一个很完善的type函数,本文就不赘述了。
// typeof 返回值目前有以下8种 'undefined' 'object' 'boolean' 'number' 'bigint' 'string' 'symobl' 'function'传入一个以逗号分隔的字符串,生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 map 中。第二个参数是小写选项。
/** * 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. */ function makeMap(str, expectsLowerCase) { const map = Object.create(null); const list = str.split(','); for (let i = 0; i !!map[val.toLowerCase()] : val => !!map[val]; } 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('ref'); // true isReservedProp('onVnodeBeforeMount'); // true isReservedProp('onVnodeMounted'); // true isReservedProp('onVnodeBeforeUpdate'); // true isReservedProp('onVnodeUpdated'); // true isReservedProp('onVnodeBeforeUnmount'); // true isReservedProp('onVnodeUnmounted'); // true这个函数也是和上面 MakeMap 函数类似。只不过接收参数的是函数。《JavaScript 设计模式与开发实践》书中的第四章 JS单例模式也是类似的实现。
var getSingle = function(fn){ // 获取单例 var result; return function(){ return result || (result = fn.apply(this, arguments)); } };以下是一些正则,系统学习正则推荐老姚:《JavaScript 正则表达式迷你书》问世了![16],看过的都说好。所以本文不会过多描述正则相关知识点。
// \w 是 0-9a-zA-Z_ 数字 大小写字母和下划线组成 // () 小括号是 分组捕获 const camelizeRE = /-(\w)/g; /** * @private */ // 连字符 - 转驼峰 on-click => onClick const camelize = cacheStringFunction((str) => { return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : '')); }); // \B 是指 非 \b 单词边界。 const hyphenateRE = /\B([A-Z])/g; /** * @private */ const hyphenate = cacheStringFunction((str) => str.replace(hyphenateRE, '-$1').toLowerCase()); // 举例:onClick => on-click const hyphenateResult = hyphenate('onClick'); console.log('hyphenateResult', hyphenateResult); // 'on-click' /** * @private */ // 首字母转大写 const capitalize = cacheStringFunction((str) => str.charAt(0).toUpperCase() + str.slice(1)); /** * @private */ // click => onClick const toHandlerKey = cacheStringFunction((str) => (str ? `on${capitalize(str)}` : ``)); const result = toHandlerKey('click'); console.log(result, 'result'); // 'onClick'根据 hasChanged 这个我们继续来看看:Object.is API。
Object.is(value1, value2) (ES6)
该方法用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致。不同之处只有两个:一是+0不等于-0,而是 NaN 等于自身。
Object.is('若川', '若川'); // true Object.is({},{}); // false Object.is(+0, -0); // false +0 === -0; // true Object.is(NaN, NaN); // true NaN === NaN; // falseES5可以通过以下代码部署Object.is。
Object.defineProperty(Object, 'is', { value: function() {x, y} { if (x === y) { // 针对+0不等于-0的情况 return x !== 0 || 1 / x === 1 / y; } // 针对 NaN的情况 return x !== x && y !== y; }, configurable: true, enumerable: false, writable: true });根据举例可以说明
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风格的属性定义方式:
var person = {}; person.legs = 2;以下是等价的ES5通过数据描述符定义属性的方式:
var person = {}; Object.defineProperty(person, 'legs', { value: 2, writable: true, configurable: true, enumerable: true });其中, 除了value的默认值为undefined以外,其他的默认值都为false。这就意味着,如果想要通过这一方式定义一个可写的属性,必须显示将它们设为true。或者,我们也可以通过ES5的存储描述符来定义:
var person = {}; Object.defineProperty(person, 'legs', { set:function(v) { return this.value = v; }, get: function(v) { return this.value; }, configurable: true, enumerable: true }); person.legs = 2;这样一来,多了许多可以用来描述属性的代码,如果想要防止别人篡改我们的属性,就必须要用到它们。此外,也不要忘了浏览器向后兼容ES3方面所做的考虑。例如,跟添加Array.prototype属性不一样,我们不能再旧版的浏览器中使用shim这一特性。另外,我们还可以(通过定义nonmalleable属性),在具体行为中运用这些描述符:
var person = {}; Object.defineProperty(person, 'heads', {value: 1}); person.heads = 0; // 0 person.heads; // 1 (改不了) delete person.heads; // false person.heads // 1 (删不掉)其他本文就不过多赘述了。更多对象 API 可以查看这篇文章JavaScript 对象所有API解析。
其实 isNaN 本意是判断是不是 NaN 值,但是不准确的。比如:isNaN('a') 为 true。所以 ES6 有了 Number.isNaN 这个判断方法,为了弥补这一个API。
Number.isNaN('a') // false Number.isNaN(NaN); // true获取全局 this 指向。
初次执行肯定是 _globalThis 是 undefined。所以会执行后面的赋值语句。
如果存在 globalThis 就用 globalThis。MDN globalThis[17]
如果存在self,就用self。在 Web Worker 中不能访问到 window 对象,但是我们却能通过 self 访问到 Worker 环境中的全局对象。
如果存在window,就用window。
如果存在global,就用global。Node环境下,使用global。
如果都不存在,使用空对象。可能是微信小程序环境下。
下次执行就直接返回 _globalThis,不需要第二次继续判断了。这种写法值得我们学习。
先推荐我认为不错的JavaScript API的几篇文章和几本值得读的书。
JavaScript字符串所有API全解密[18]
【深度长文】JavaScript数组所有API全解密[19]
正则表达式前端使用手册[20]
老姚:《JavaScript 正则表达式迷你书》问世了![21]
JavaScript 对象所有API解析 https://lxchuan12.gitee.io/js-object-api/
MDN JavaScript[22]
《JavaScript高级程序设计》第4版[23]
《JavaScript 权威指南》第7版[24]
《JavaScript面向对象编程2》[25] 面向对象讲的很详细。
阮一峰老师:《ES6 入门教程》[26]
《现代 JavaScript 教程》[27]
《你不知道的JavaScript》上中卷[28]
《JavaScript 设计模式与开发实践》[29]
我也是从小白看不懂书经历过来的。到现在写文章分享。
我看书的方法:多本书同时看,看相同类似的章节,比如函数。看完这本可能没懂,看下一本,几本书看下来基本就懂了,一遍没看懂,再看几遍,可以避免遗忘,巩固相关章节。当然,刚开始看书很难受,看不进。这些书大部分在微信读书都有,如果习惯看纸质书,那可以买来看。
这时可以看些视频和动手练习一些简单的项目。
比如:可以自己注册一个github账号,分章节小节,抄写书中的代码,提交到github,练习了才会更有感觉。
再比如 freeCodeCamp 中文在线学习网站[30] 网站。看书是系统学习非常好的方法。后来我就是看源码较多,写文章分享出来给大家。
文中主要通过学习 shared 模块下的几十个工具函数,比如有:isPromise、makeMap、cacheStringFunction、invokeArrayFns、def、getGlobalThis等等。
同时还分享了vue源码的调试技巧,推荐了一些书籍和看书籍的方法。
源码也不是那么可怕。平常我们工作中也是经常能使用到这些工具函数。通过学习一些简单源码,拓展视野的同时,还能落实到自己工作开发中,收益相对比较高。
[1]
vue-next: https://github.com/vuejs/vue-next
[2].github/contributing.md: https://github.com/vuejs/vue-next/blob/master/.github/contributing.md
[3]更多参考资料可以点击 阅读原文 查看
最近组建了一个江西人的前端交流群,如果你是江西人可以加我微信 ruochuan12 私信 江西 拉你进群。
推荐阅读
我在阿里招前端,该怎么帮你(可进面试群)
我读源码的经历
面对 this 指向丢失,尤雨溪在 Vuex 源码中是怎么处理的
老姚浅谈:怎么学JavaScript?

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

识别上方二维码加我微信、拉你进源码共读群
今日话题
略。欢迎分享、收藏、点赞、在看我的公众号文章~
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-02-15
2022-02-14