0%

微软在今年宣布推出了 windows 的下一代操作系统 windows 11。说好的 win10 是最后一代 windows 操作系统呢?6 月微软就放出了预览版,不过我没有第一时间升级。依稀记得 win10 是在我高中的时候推出的,我也是第一时间给我的电脑装上了 win10,那个时候我还没怎么接触 linux,甚至觉得 win10 是我见到过的最酷的操作系统了(lll ¬ ω ¬)。不过后来微软在对开发者友好上确实做的不错,推出了新的 windows terminal,官方的包管理器等,docker 等软件也对 win10 有了更好的支持,win10 作为开发环境也挺省心的。

10 月 5 日,微软发布了 win11 的正式版,win10 可以无损免费升级到 win11。于是我花了半天的时间折腾了以下,成功将系统升级为 win11,这里记录一下过程。

win11 升级要求

忽略硬件要求,主要就这两项

  • 开启 tpm2.0
  • 开启安全启动
    我的平台是 b450m + ryzen r7-2700,硬件上肯定是达标的,这两项技术也支持,可能需要调整的是 bios 的设置

升级过程

首先下载微软官方的 win11 升级助手

还需要下载 windows 健康检查应用

运行 windows 健康检查应用,发现不满足 tpm 和 安全启动的条件。
进入 bios 发现 tpm 可以开启,安全启动必须关闭 scm support 才出现相应选项

原来我之前的系统还是使用 mbr 方式引导的,而安全启动要求必须以纯 ufei 方式启动,因此我需要将引导方式从 mbr 转变为 gpt。

其实也不麻烦,有现成的工具可用,下载一个傲梅分区助手,对系统盘进行 mbr 转 gpt 的操作,等它帮你慢慢操作就行了,自己玩玩手机,半小时就搞定了,很稳。

之后再进 bios 开启安全启动。这时候进入系统,运行健康检查,发现满足升级条件,此时打开升级助手,就可以开始下载升级文件了。之后就是等待就好了。

win11

升级成功!其实感觉就是 win10 换了个皮肤。图标的风格更加扁平与圆润。底部栏与开始菜单移到了中间,肉眼可见的所有东西都加上了圆角。我不懂设计,说不出个所以然来,win11 有啥优点暂时也体验不出来,就当换个皮肤了,先用着吧 O(∩_∩)O。

yarn add

-E 选项明确指定版本
—audit 检查在已安装的包中所有已经的安全问题并输出

yarn autoclean

autoclean 命令通过从依赖项中删除不必要的文件和文件夹来释放空间。它减少了项目节点模块文件夹中的文件数量,这在直接将包签入版本控制的环境中非常有用。

yarn bin

输出 yarn 安装可执行文件的目录

yarn cache

可以管理下载的包缓存

yarn create

是一个快捷指令

yarn create react-app my-app 就相当于

1
2
yarn global add create-react-app
create-react-app my-app

peerDependencies

举个例子,项目中引入了 pakcageA,依赖 A 中的 package.json 中的 dependencies 中包含了 packageB,那么项目的目录结构是这样的

1
2
3
4
5
MyProject
|- node_modules
|- PackageA
|- node_modules
|- PackageB

我们在项目中,可以直接通过下面语句引用 PackegeA,但是不能直接引用 PackageB。所以,为了解决这个问题,在 MyProject 项目 package.json 中我们必须直接声明对 PackageB 的依赖并安装。

而如果是在 peerDependencies 声明的 packgeB 的话,得到的目录结构是

1
2
3
4
MyProject
|- node_modules
|- PackageA
|- PackageB

app.json

window

backgroundColor 不是背景的颜色 而是下拉页面的背景色

生命周期

组件间的生命周期会有些区别

  • onLoad 页面加载,一个页面只会调用一次,可以获得 query 参数
  • onShow 页面显示 、页面切入前台都会调用
  • onHide 页面隐藏/切入后台时触发。 如 wx.navigateTo 或底部 tab 切换到其他页面,小程序切入后台等
  • onReady 页面初次渲染完成,表示可以与视图层进行交互,像动态改变页面标题的就可以在这处理, wx.setNavigationBarTitle
  • onUnload 页面卸载时触发。如 wx.redirectTo 或 wx.navigateBack 到其他页面时。

小程序从启动到关闭,生命周期函数的执行情况
1、初次打开: 会执行小程序的生命周期钩子函数:onLaunch -> onShow -> onReady
2、使用 navigateTo 离开当前页面: 保留所离开的页面,执行 onHide
3、使用 navigateBack 离开当前页面: 销毁当前页面,执行 onHide -> onUnload
4、使用 switchTabTo 离开当前页面: 销毁所有非 tab 页面,但保留所有已经加载的 tab 页面

components 中的生命周期函数
1、 组件实例化: created 节点树还未导入, 无法使用 setData
2、节点树导入完成: attached 可以使用 setData 来初始化数据,但无法操作节点
3、组件布局完成: ready 组件布局完成,可以获取到节点信息也可以操作节点
4、组件实例被移动到节点树另一个位置: moved
5、组件实例从页面节点移除: detached

事件类型

有冒泡和非冒泡两种

WXML 的冒泡事件列表:
类型 触发条件 最低版本
touchstart 手指触摸动作开始
touchmove 手指触摸后移动
touchcancel 手指触摸动作被打断,如来电提醒,弹窗
touchend 手指触摸动作结束
tap 手指触摸后马上离开
longpress 手指触摸后,超过 350ms 再离开,如果指定了事件回调函数并触发了这个事件,tap 事件将不被触发 1.5.0
longtap 手指触摸后,超过 350ms 再离开(推荐使用 longpress 事件代替)
transitionend 会在 WXSS transition 或 wx.createAnimation 动画结束后触发
animationstart 会在一个 WXSS animation 动画开始时触发
animationiteration 会在一个 WXSS animation 一次迭代结束时触发
animationend 会在一个 WXSS animation 动画完成时触发
touchforcechange 在支持 3D Touch 的 iPhone 设备,重按时会触发 1.9.90

bind 和 catch 的区别

bind 系列事件绑定不会阻止冒泡事件向上冒泡,但是 catch 系列事件绑定可以阻止冒泡事件向上冒泡。

template

template 可以类比于传统后端模板引擎引用代码片段
在一个 wxml 文件中可以定义多个 template,用 name 进行区分

如何使用

import
1
2
<import src="/templates/item.wxml" />
<template is="item2" data="{{text:'foobar'}}"></template>

import 有作用域的概念,即只会 import 目标文件中定义的 template,而不会 import 目标文件 import 的 template。
如:C import B,B import A,在 C 中可以使用 B 定义的 template,在 B 中可以使用 A 定义的 template,但是 C 不能使用 A 定义的 template。

inlcude

include 可以将目标文件除了 template wxs 外的整个代码引入,相当于是拷贝到 include 位置,如:

响应式数据访问

在 page 或者 component 中的生命周期或者 methods 中

1
2
3
4
5
Page({
data:{
a:'xxx
}
})

访问

需要使用 this.data.a 进行访问,相比与 vue,需要多一层 data 属性,vue 内部对 data 属性的访问是做了代理的,而小程序则没有这层代理

设置

这种方式很 react

1
2
3
this.setData({
a:'xxxx';
})

坑点

required 不支持绝对路径
设置 tabbar 坑! 首页必须包含在其中,否则不能显示
npm 安装 weui-wxss 构建 npm 提示 没有找到可以构建的 npm 模块 是因为 weui-wxss 没有导出模块的设置
数据库 获取不到数据 这个是数据库访问权限设置的问题

原生 API

路由

wx.switchTab 只能跳转到带有 tabbar 的页面 关闭其他所有非 tabBar 页面,跳转路径不能带参数
wx.reLaunch 关闭所有页面,打开到应用内的某个页面 路由可以带有参数
wx.redirectTo 关闭当前页面,跳转到不带 tabbar 的页面,路由可以带有参数

以上方法可接受一个 Object 作为参数 参数格式如下

属性 类型 默认值 必填 说明
url string
success function 接口调用成功的回调函数
fail function 接口调用失败的回调函数
complete function 接口调用结束的回调函数(调用成功、失败都会执行)

wx.navigateBack 关闭当前页面,返回上一页面或者多级页面。可通过 getCurrentPages 获取当前的页面栈,决定需要返回几层。
它不接受 url 参数,可以接受一个 delta 作为参数,决定要返回几层

属性 类型 默认值 必填 说明
delta number 1 返回的页面数,如果 delta 大于现有页面数,则返回到首页。

wx.navigateTo 保留当前页面,跳转到应用某个页面,但是不能跳转到 tabbar 页面。相比于以上方法,他多了一个可选参数。

属性 类型 默认值 必填 说明
events Object 页面间通信接口,用于监听被打开页面发送到当前页面的数据。基础库 2.7.3 开始支持。

界面

wx.startPullDownRefresh 可以手动条用 onPllDownRefresh 声明周期的方法
wx.pageScrollTo 将页面滚动到目标位置,支持选择器和滚动距离两种方式定位

填坑中

写在前面

我的 switch 是我在上大三的时候,也就是 19 年上半年吧,买的可以软破的机器,然后自己折腾的破解。我是一个 JRPG 的爱好者,当时买 switch 的首要原因就是特别想玩它上面独占的八方旅人,其二就是当时还没发售的火纹新作,对塞尔达野吹倒是很无感。不得不说,折腾破解的过程还是很有趣的,去各种资源站,论坛下游戏的过程也很有趣,但是自己却很难再静下心去打通一款游戏了,可能确实是老了。之后很长一段时间,我都认为打游戏这种毫无产出的行为,是在浪费时间,这导致我的 switch 吃灰了很长时间。至今为止,switch 上我还没有打通任何一款游戏。

但是最近又心血来潮,想着把 switch 上的八方旅人打通,再尝试一下勇气默示录 2。大学的时候因为穷,给 switch 只配了 64gb 的内存卡,这让我折腾 switch 一直面临着严重的容量焦虑,有时想安装点新游戏,就必须删掉一些其他游戏,好不自在。
我想升级内存卡很久了,之前一直觉得反正也不怎么玩了,就一直没换。但是现在又有玩游戏的想法了,还是要提升一下体验的。
我的 switch 破解是大气层单系统破解。现在主流的破解方式都是制作虚拟系统,这个虚拟系统是独立于 switch 本机系统的。可以自由选择启动 switch 本机系统或者虚拟系统,可以在 switch 本机系统中玩正版游戏联网,在虚拟系统中玩学习版的游戏,虚拟系统可以随意折腾,挂掉了重新安装即可,总之优点多多。趁这次机会,我准备把破解方式也升级成虚拟系统的方式。

因此这次折腾要完成以下内容

  • 首先买一张内存卡,容量要大大大
  • 将游戏从之前的内存卡迁移到新内存卡
  • 新的内存卡上创建虚拟系统
  • 升级虚拟系统的系统版本

动手之前的情况

  • 机器是软破机,大气层版本 0.10.4
  • switch 系统版本 10.0.3,之前有过几次升级,使用的是大白兔的离线免熔断升级

计划实施

  • 首先是内存卡,最后买了闪迪 200G a1,这样除去虚拟系统的 30G,尚有 100 多 G,足够我用了。闲鱼 120 元购得
  • 将原内存卡所有内容到新内存卡,新内存卡插入 swtich,可以正常启动大气层,并且游戏都在
  • 之后看教程进行虚拟系统的创建,这里强力推荐下公众号鹿枫堂游戏分享,无论是破解教程,还是游戏下载,在这个公众号里都能找到
  • 重启 switch 进入 rcm 模式,创建虚拟系统 emummc,等待几分钟创建完毕,然后设置虚拟系统为启用状态
  • 然后升级大气层,用电脑读取内存卡,将大气层相关的文件删除,其中 atomosphere/contents 文件夹和 switch/checkpoint/saves 文件夹需要备份,之后将最新版的大气层文件拷贝到 sd 卡中,大气层升级完成
  • 重启 switch,这时候查看系统版本,发现带有后缀 E,说明当前启动的是虚拟系统
  • 升级 switch 系统,首先在公众号里面下载最新的固件包,将固件包拷贝到内存卡中,启动 switch,进入相册使用 daybreank 进行系统升级
  • switch 再次重启后,虚拟系统的版本已经更新

坑点

上述操作搞定后,进入 switch,发现之前 switch 内存卡中的游戏全部丢失,点击系统找不到数据文件,需要重新安装。还好游戏的 nsp 文件我都还保存着,于是重新安装了一遍,安装完进入游戏,存档还在,可以接受。
之后才知道虚拟系统默认在 SD00 文件夹内,该文件夹内有一个 emummc.ini 的配置文件,里面可以配置 nintendo 文件夹的路径,而这个文件夹就是游戏文件所在的文件夹

我备份了 atomosphere/contents 文件夹,拷回新内存卡后,大气层启动报错,无奈只好放弃这些备份的还原。这个文件夹主要是一些汉化和金手指,我也不怎么用这些,所以影响不大。感觉可能是因为没有配置 nintendo 文件夹路径,没有找到对应游戏安装位置的原因。

前言

这里说明一下打包工具的概念,打包工具是用来将多个 js 文件和他们的依赖组合成一个或者多个 bundle 文件的工具。前端知名的打包工具有 webpack,rollup,gulp 等。

打包过程

我们的前端项目一般都会使用 webpack 进行构建。随着项目规模不断增加,引入的依赖逐渐增多,我们开启开发服务器和进行生产构建的时间会不断增加。目前我的公司的项目开启开发服务器需要 1 分钟左右,进行生产构建需要 5 分钟以上,而这已经是进行微前端拆分后的结果,如果还是单体巨石应用的结构,那画面太美不敢想象。webpack 的速度实在是太慢,esbuild 就是为了解决这一痛点而被创造出来的。

esbuild 是使用 go 语言编写的一个新一代的 JavaScript 打包工具,它的作者是 figma 的 CTO Evan Wallace。得益于 go 语言编译型语言的优点,打包速度可以达到 webpack 的几十倍。一些知名的热门项目已经内置了 esbuild,比如 vite,snowpack。今天我们就跟随 esbuild 的官方文档,学习一下 esbuild 的相关知识。

下文的主要内容包括

  • 为什么 esbuild 这么快
  • 上手使用 esbuild
  • esbuild plugin api 简介
  • esbuild 目前的局限

esbuild 为什么那么快

在文档中作者给出了 esbuild 与其他打包工具对 three.js 进行的打包基准测试结果,我们可以看到 esbuild 的速度惊人的快,最高是 rollup 和 webpack 的百倍。
| Bundler | Time | Relative slowdown |Absolute speed| Output size|
|—-|—-|—-|—-|—-|
|esbuild |0.37s |1x |1479.6 kloc/s |5.81mb|
|esbuild (1 thread) |1.61s |4x |340.0 kloc/s |5.81mb|
|rollup + terser |37.79s |102x |14.5 kloc/s |5.81mb|
|parcel 2 |39.28s |106x |13.9 kloc/s |5.87mb|
|webpack 4 |43.07s |116x |12.7 kloc/s |5.97mb|
|webpack 5 |55.25s |149x |9.9 kloc/s |5.84mb|

这里大家一定非常好奇 esbuild 是如何做到如此之快的,在官方文档的 FAQ 中给出了以下解释:

go 语言编写

go 是为了并行而被设计出来的编译型语言,速度相比使用脚本语言 JavaScript 编写的同类工具有很大的优势。
尽管 v8 引擎提供了 JIT 的特性,很大程度上提升了 Javascript 的性能,但是对于命令行程序,它依然太慢了。 每次运行打包器时,JavaScript VM 都会在没有任何优化的情况下运行打包程序的代码。在 esbuild 忙于解析 JavaScript 时,node 还在忙于解析打包程序的 JavaScript。
在线程间通信方面, Go 在线程之间可以共享内存,而 JavaScript 必须在线程之间传递序列化数据,这一点显然共享内存的效率更高。
Go 和 JavaScript 都有并行的垃圾收集器,但是 Go 的堆在所有线程之间共享,而对于 JavaScript, 每个 JavaScript 线程中都有一个单独的堆。

充分使用并行

esbuild 内部的算法专门被设计为可以最大程度利用 cpu 资源的形式。esbuild 的工作流程可大致分为三个阶段,解析,链接和代码生成。其中作为主要工作的解析代码和代码生成全部并行方式处理。因为所有的线程共享了内存,不同入口文件的打包工作可以使用相同的导入的 Javascript 模块数据。
这里需要额外说明一下,Go 语言是一门为并发编程设计的语言,它原生支持协程。

代码完全自己编写

esbuild 不使用第三方库,自己来编写实现 js/ts 的编译相关模块,带来了很多细节上的性能优化。

高效使用内存

理想情况下,编译器以近乎 O(n) 的复杂度处理输入,如果需要处理大量数据,内存访问速度可能会严重影响性能。如果你能更少地遍历代码,编译器的运行速度就会更快。
esbuild 只会访问 js/ts 的 AST 三次。
第一次是对于词法解析,作用域设置,以及声明符号
第二次是绑定符号,压缩语法, JSX/TS 转 JS,ESNext 转 ES2015
第三次是压缩标识符,压缩空白字符,生成代码与生成 sourcemap
esbuild 最大程度重用了 AST 数据,当它们还在 cpu 的缓存中时。

综合以上的几个方面,使得 esbuild 的速度比起其他打包工具高出一个数量级。

开始使用

esbuild 提供了三种方式来使用 esbuild,分别是 命令行,npm 包 和 go 包
esbuild 提供了两种调用类型主要 API 类型,Transform API 和 Build API
下面分别简单介绍一下它们的使用

npm 包方式

首先安装 esbuild

1
npm install esbuild

可以看到 esbuild 的 npm 包结构如下:

│ esbuild.exe
│ install.js
│ package.json
│ README.md

├─bin
│ esbuild

├─lib
│ main.d.ts
│ main.js

esbuild 是一个 go 语言编译的可执行程序, 包在安装时会根据所在平台安装对应的可执行程序,windows 系统下就是 esbuild.exe 。lib 下 main.js 提供了对于 esbuild 可执行程序调用的封装。

以下是官网的示例

1
2
3
4
5
6
require("fs").writeFileSync("in.ts", "let x: number = 1");
require("esbuild").buildSync({
entryPoints: ["in.ts"],
outfile: "out.js",
});
require("fs").readFileSync("out.js", "utf8");

命令行方式

使用 npm 安装 esbuild 后,我们也可以通过命令行的方式调用 esbuild。esbuild 在 package.json 中定义了 bin 的配置,该文件是一个 node 脚本,用以子进程方式调用 esbuild 可执行文件。

根据 npm 的约定,当 package.json 中定义了 bin 选项时,包安装时会自动将脚本软链接到 node_modules/.bin 目录下。当使用 npm 执行定义在 scripts 中的命令时,会自动将 node_modules/.bin 目录下添加到 PATH 中。通过这种方式就可以以命令行方式调用 esbuild。

Transform API

Transform API 对单个字符串进行操作,它不访问文件系统。这使得它非常适合在没有文件系统(例如浏览器)的环境中使用,或者作为另一个工具链的一部分。

1
2
3
4
5
6
7
8
9
require('esbuild').transformSync('let x: number = 1', {
loader: 'ts',
})
=>
{
code: 'let x = 1;\n',
map: '',
warnings: []
}

社区中有很多基于 esbuild Transform API 打造的其他打包工具的插件,比如

Build API

esbuild 的 Build API 做的事情和 webpack,rollup 等一样,需要入口文件,用于将一系列文件及其以来打包输出到一个或多个文件中。

1
2
3
4
5
6
7
8
require("fs").writeFileSync("in.ts", "let x: number = 1");
require("esbuild").buildSync({
entryPoints: ["in.ts"],
outfile: "out.js",
watch: true,
});
require("fs").readFileSync("out.js", "utf8");
("let x = 1;\n");

设置 watch 选项为 true 既可以开启开发服务器模式,esbuild 会监听文件系统的变化并自动重新 build。

内置 loaders

esbuild 内置的 loaders 可以处理以下文件类型:JavaScript,typescript,jsx,json,css,text,binary,base64,dataurl,external file

除了 external,其他类型的 loader 会在处理相关扩展名的文件时自动调用,也可以通过 loader 手动设置调用规则。

需要注意的是 esbuild 对于 js 的处理。如果你想要用 esbuild 转译 es5 的代码,需要将target 设置为 es5,这可以避免 esbuild 进行错误的转译,比如将{x:x} 转译为 {x}

esbuild 目前对将es6+ 的代码转译为 es5 的代码的支持不完善,如 letconst 等语法不会被转译。

插件机制

同 rollup 和 和 webpack 一样,esbuild 提供了插件机制,目前 esbuild 的插件 api 还在实验阶段,在未来可能会有很大的变动。
esbuild 目前只支持在 build API 中使用插件,不支持 transform API

编写 esbuild 插件

一个 esbuild 的插件是一个对象,有一个 name 属性和一个 setup 函数。在 build API 中,我们可以设置 plugins 数组。插件对象中的 setup 函数会在 build 每次运行时调用一次。

下面是文档中的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let envPlugin = {
name: "env",
setup(build) {
// Intercept import paths called "env" so esbuild doesn't attempt
// to map them to a file system location. Tag them with the "env-ns"
// namespace to reserve them for this plugin.
build.onResolve({ filter: /^env$/ }, (args) => ({
path: args.path,
namespace: "env-ns",
}));

// Load paths tagged with the "env-ns" namespace and behave as if
// they point to a JSON file containing the environment variables.
build.onLoad({ filter: /.*/, namespace: "env-ns" }, () => ({
contents: JSON.stringify(process.env),
loader: "json",
}));
},
};

require("esbuild")
.build({
entryPoints: ["app.js"],
bundle: true,
outfile: "out.js",
plugins: [envPlugin],
})
.catch(() => process.exit(1));

这个插件的作用是让 esbuild 将以 env 结尾的文件作为 json 文件解析。可以看到 esbuild 的插件编写非常简单,只有少量的关键概念和关键 API,下面我们来简单了解一下。

两个关键观念

Namespaces 命名空间

每个模块有一个关联的命名空间。默认情况下,esbuild 在 file 命名空间下工作,它对应于真实文件系统上的文件。
esbuild 也可以处理虚拟的模块,即并不对应于文件系统上的文件。举个例子,比如从 stdin 中提供的模块。

可以用插件去创建虚拟模块,标注模块在一个特定的命名空间,以使用其他的插件进行特殊处理。

Filters 过滤器

每一个回调函数必须提供一个正则表达式作为过滤器,那些不匹配的模块将被跳过处理。
你应该尽可能使用过滤正则表达式而非使用 js 代码来处理,因为 Filters 是在 esbuild 内部处理的,效率更高。

两个主要回调

onResolve 回调

使用 onResolve 添加的回调会在每次 esbuild build 过程中的路径解析阶段被调用。这个回调用来自定义 esbuild 解析路径的行为。

下面这个例子中,插件将以 images 开头的导入路径重定向到 /public/images 目录,将 http 开头的路径标记为 external。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let exampleOnResolvePlugin = {
name: "example",
setup(build) {
let path = require("path");

// Redirect all paths starting with "images/" to "./public/images/"
build.onResolve({ filter: /^images\// }, (args) => {
return { path: path.join(args.resolveDir, "public", args.path) };
});

// Mark all paths starting with "http://" or "https://" as external
build.onResolve({ filter: /^https?:\/\// }, (args) => {
return { path: args.path, external: true };
});
},
};
onLoad 回调

使用 onLoad 添加的回调会在所有没有被标记为 external 的模块中运行,模块由路径和命名空间共同标识。它用来自定义模块返回的内容,告诉 esbuild 如何去转译它。

下面这个例子中,插件将以 txt 文件以 utf8 方式获取内容后,以 json 数据形式读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let exampleOnLoadPlugin = {
name: "example",
setup(build) {
let fs = require("fs");

// Load ".txt" files and return an array of words
build.onLoad({ filter: /\.txt$/ }, async (args) => {
let text = await fs.promises.readFile(args.path, "utf8");
return {
contents: JSON.stringify(text.split(/\s+/)),
loader: "json",
};
});
},
};

require("esbuild")
.build({
entryPoints: ["app.js"],
bundle: true,
outfile: "out.js",
plugins: [exampleOnLoadPlugin],
})
.catch(() => process.exit(1));

esbuild 插件的局限

esbuild 的插件无法做到覆盖全部的用户场景。它只提供了自定义模块家在方式和自定义模块返回内容的能力,没有提供给我们直接访问 js ast 的能力,这是出于为了保持 esbuild 卓越性能的考虑,但这也导致很多 babel 插件提供的基于 js ast 的转译功能都无法使用 esbuild 的插件机制来实现。

结论

esbuild 作为打包工具的性能表现是极为出色的,但是现阶段并不能代替 webpack。一方面,webpack 经过多年的发展,生态非常成熟,可以覆盖到几乎所有的使用场景,另一方面,esbuild 本身受限的插件机制使得它注定无法完成一些事情。
比如说我们使用 webpack 中使用的 基于 ast 的 babel 插件,是没办法迁移到 esbuild 中去的,因为 esbuild 根本不提供访问 ast 的能力。
对于基于 webpack 的老项目,通常包含了很多基于 webpack 生态的 loader,插件等,将它们迁移到 esbuild 是一项艰巨且风险巨大的工作。新的项目没有技术债务,可以使用 esbuild 作为构建工具,可以极大提升开发体验。
但是,把 esbuild 作为纯的 js/ts 转译工具,或者作为代码压缩工具,作为其他构建工具工具链的一部分,是可以的融入到老项目中,提升构建速度的。

参考

esbuild 官方文档
「 不懂就问 」esbuild 为什么这么快?

今天被时区的问题困扰了好久,踩了不少坑,在此记录一下。

go 中的时区

go 中 time.Now() 取的是系统的时区。

time.parse 默认采用的是 UTC 时间进行解析,UTC 时即世界协调时间。

我们中国使用的北京时间是东八区时间,领先了 UTC 八个小时。

举个例子,现在是北京时间 2021/2/21 15:00:00,用 time.parse 解析得到的时间 2021/2/21 15:00:00 +0:00,而这个解析到的时间对应到北京时间是 2021/2/21 23:00:00 +8:00

time.Now() 取到的系统时间是 2021/2/21 15:00:00 +8:00,这种情况下做时间的比较,是无法得到正确的结果的,同时如果使用 gorm 等事件保存到数据库中,保存的时间也是比正确的时间晚了 8 小时的。

所以应当使用 time.parseInLocation

容器中的时区

使用 docker-compose,可以设置 environment 中的 TZ 设置时区,前提是镜像中安装了 tzdata 这个软件。如果没有,设置时无法生效的。

mysql 中的时区

默认会采用系统时区,可以在 my.cnf 中手动设置默认的时区。

写在前面

大学开始,慢慢养成了记录技术类博客的习惯,当然这也是比较实用主义的做法,因为接触的知识真的很多,如果不记录下来,过一段时间就是忘光光了,这也就是知识管理的重要性。

但是生活其实也并不只是工作学习,并且随着年龄的增长,我越来越感觉到时间的珍贵,对于逝去的时间,如果不记录些什么,就感觉白白浪费了。所以也想要尝试记录生活,留住感动,去抓住逝去的时光。今后的话,大概会写一些出去玩的游记和游戏记录相关的内容吧。另外本人的文笔不好,希望也可以慢慢练习吧。

出发

作为一个平日深居简出的宅宅,其实出去玩对我是没有什么吸引力的。之前在杭州实习,呆了几个月,甚至都没有去西湖走一遭,现在感觉还是挺遗憾的。说实话,这次出去很大一个原因是怕万一别人问起我春节不回家去了哪里玩,如果说一直霉在出租屋里,总感觉不太好。本来计划着初三出去的,显然我丝毫没有意识到初三是情人节的事实,这出去还不被虐成狗啊。初三是出去不成了,初四又是慵懒的一天,就这样拖到了初五,下定决心一定要出去了,在网上搜深圳有哪些好玩的地方,然后想起之前同事跟我提过可以去深圳湾公园,地铁直达的样子,搜了一下,感觉靠谱,出发!

深圳湾公园

从塘朗地铁站出发,经过两次转线,大约历时一个小时,到达 9 号线深圳湾公园站。深圳湾这块绝对是个游玩的胜地,光是地铁站的出口有好几个,分别去往不同的地方。去往深圳湾公园的出口沿着扶梯上去,就来到了公园的门口。意外的是人是挺多的,远远望去都是人,除了必须要带上口罩外,完全看不到疫情带来的影响。

来了公园这边,想做的事情当然就是看海了。想到跟大海零距离接触,虽然说不上激动,但是对于我这个在内陆地区长大的人来说,这种感觉还是挺奇妙的。

之前跟同事聚餐完后一个人跑到人才公园那边感受了一下,看到的是深圳这座大都市海滨的华灯璀璨,让我不禁感慨这才是深圳该有的样子。

在 B 站视频里见过的深圳湾地标-春笋。

春笋

夜景

深圳湾夜景

但是在深圳湾公园这边,感受到的则是一种宁静。看着大海的浪潮起伏,感受着湿润的海风朝自己扑来,最惊喜的是可以“文明观鸟”。

文明观鸟

在最靠近这群海鸟的地方,有几个人用装着半米长镜头的相机在拍摄,真是专业的不行,海岸边有公园的工作人员举着宣传语,让人们“文明观鸟”,不要擅自给海鸟喂食,人们无知善意的举动,实际上是可能伤害它们的。

公园绵延海岸线十几公里,我从入口一路往东走,就到了红树林,这边的海岸是生长着红树林的湿地,这边的滩涂上,有那种一跳一跳的鱼,不时会有几只一跃而起,很是神奇。

就这样沿着海岸线走着走着,想到既然来了,晚上去再去别的地方玩玩好了。本来是想要去民俗村的,但是订票的时候显示票已售罄的。看到世界之窗也在附近,看评价似乎挺好,于是就定了夜场的票。订票的时候已经是 5 点了,6 点是夜场入门的时间,在公园又溜了一会后,我决定步行前往。但是这里我犯了一个错误,我没有用地图的导航功能,我从公园的出口出来了,想要过马路到对面,但是实际上这里必须走地铁的地下通道才能过去,绕了一大圈,花了不少时间,还是骑单车在 6 点半的时候赶到了入口。

世界之窗

公园的入口处,是巴黎卢浮宫的微型仿制建筑,上面有江泽民主席的题字,看到这个,真的忍不住要膜一下。

世界之窗

深圳是中国向世界开放的窗口,世界之窗这个主题公园里,浓缩了世界著名的景观,也是中国人认识世界的一个窗口。

入园后,我突然意识到了一个问题,几乎所有的游客,要么就是家长带小孩,要么就是情侣一起,最次也是几个好哥们同行,似乎只有我是一个人来玩的。不过好在我内心还是很强大的,即使是一个人,来感受下氛围也好,毕竟钱都已经花了。当然有了这次的前车之鉴,下次我是绝对不会再一个人来这种主题公园了。。。

公园的正中心是缩小版的埃菲尔铁塔,然后分为东西两边两大块区域。

东区

我一开始去了东边。地图标注一些的景观,实际上都只是这种微缩的模型,比如下面的天皇居所,还有泰姬陵,一开始让我感觉略微有点失望,就这?

天皇の住居

然后,再走不远就到了日本主题的地区。这里有租借和服的,我看到有小姐姐租借了穿的,是真的好漂亮,我要是女生也真的好想穿上试试。

日本馆的和风是很有味道的
日本馆

这个富士山就挺让人一言难尽的
富士山

逛完了这里,天色已经暗了下来了,这些景观的观看效果大大打了折扣。东区的话,还有东南亚,欧洲和大洋洲的景观。下面是科隆大教堂,历史上这座教堂断断续续修了几百年,是天主教最著名的几座教堂之一,即使只看模型,也还是能被他的宏伟震撼。只是这里的灯光,把它变得有点魔幻。。。

科隆大教堂

凯旋门,拿皇万岁!
凯旋门

差不多逛完了东区,已经快要 7 点了,这时我才知道,景区的一些免费项目是有开放时间限制的。之前在网上了解的像飞跃美利坚之类的必玩项目,7 点就结束了,后悔自己没有多做点功课了。

这个时候,人都在往中心的埃菲尔铁塔赶了。这里 7 点 15 会有灯光秀的表演,在表演开始之前,这里有几个演员穿着奇异的装束在这里打鼓,我是真的不知道他们打扮的是啥东西,只是想感叹钱难挣啊,这么打扮我感觉是挺难受的。

灯光秀准时开始,配合这恢弘的 BGM,效果还是很震撼的,但是时间嘛比较短,大概 5 分钟就结束了,让人有点意犹未尽。

演出盛世纪

7 点 30 在中心的广场有歌舞剧演出,我到的时候,外场中间的位置已经坐满了(内场是额外花钱的)。我只能在边界找了个位置坐下,角度可以说非常差了。

歌舞剧把中国上下五千年的文化浓缩到了一台歌舞剧中,从后羿射日,到春秋战国,再到大汉王朝,再到大唐盛世。大秦的部分确实把秦军的气势展现了出来,大汉王朝则是把汉的儒学文化和张骞通西域的开放包容展现了出来,大唐的部分更是分成了四幕,从盛世长安到雪域高原。
刚开始看的确实挺震撼的,但是看多了,要么是一群男的舞刀弄枪,要么是一群女的在哪里跳舞,要么是几个外国人上来耍耍鞭子流星锤啥的,就有点审美疲劳了。渐渐的外场的人走的越来越多,内场的人倒是没见有走的,毕竟是花钱了的 😄。我是看到了郑和下西洋那一幕,觉的没啥意思了,就溜了。

外籍演员演出

这之后我本来是打算赶快把西区逛一逛的,刚向西没过多久,就看到路上的人都在往中间赶,原来是有烟花的演出。现在春节都是禁止燃放烟花了,能看到烟花演出,确实不容易的,我也就跟着也往中间跑去了,可等待感到了中间,烟花都已经放完了,算了,就是感受个氛围吧。

到了中间,听到广播在说 9 点 15 凯撒宫那边会有梦幻欧洲灯光表演,人群都在往凯撒宫那边去,我也就跟着过去了。我跟着人群进到了宫殿内部,这里其实是一个剧场,我刚开始挺怀疑的,这里也能有灯光表演吗?等到了 9 点 15,表演也没有开始,然后挺广播才知道,这里面要开始的是一个外籍演员的专场演出,灯光表演是在外面。不过既然在里面坐下了,我也就等到它的演出开始了。

整场演出十分精彩,印象最深的是里面肯尼亚黑人表演的杂技,看了除了牛逼想不出别的词语来形容了。这些黑人演员的力气和胆子是真的大,各种杂技动作难度非常之高。最吸引研究的就是里面的毛妹了,有大毛也有二毛,有一说一,确实都是大美人,身材也都是顶级的。感觉整场演出里俄罗斯的人最多,为什么他们要跑来深圳打工呢,因为俄罗斯经济不景气吗?最后是小丑的滑稽剧表演,小时候看央视的正大综艺看过这种表演,在现场看这种演示还是生平第一次,最后也是在一片欢乐祥和中结束了整场演出。

遗憾

看完演出已经 10 点了,显然已经没有时间再去西区了,想想是挺遗憾的。不过生活嘛,本来就是充满遗憾的,这次来本来就挺仓促的,一个人的话,甚至不能好好拍点照片留念,不过我是觉得已经体验到挺多了,以后如果有机会,再和朋友好好玩一次吧。

最后

我是在开工前的夜晚赶出来的这篇游记。我在这段时间一直被焦虑的情绪困扰,这次出来玩了一趟过后,确实轻松了不少。长假已经过去,是时候收拾好心情,迎接年后的新的工作挑战了。

想要学习编译原理这门课已经很久了,自己之前也有尝试看龙书或者听公开课,可是这样学到的公式性的知识太多,每次都没有坚持学习下去。

放假这几天学习了极客时间上的《编译原理之美 》这门专栏,把讲编译器前端部分的看完了。这个老师讲的是真的好,比较浅显的语言来讲解教科书上用大段公式说明的知识,同时结合了大量的实践内容,帮助我快速了解了编译原理的核心要点的同时,同时教授了我动手的能力。

之前面试 bilibili 的时候,面试官就问过我如何用代码实现验证一个 json 的有效性,当时回答很糟糕,完全没有使用编译技术这方面的意识。

其实编译技术就很好处理这种问题,走一边词法分析和语法分析的流程,在解析 token 或者 构建 ast 的过程中如果出错了,那么这个 json 就是非法的了。

既是为了练手,也是填上之前面试的这个坑,下面就简单实现一个 jsonParser。

写在前面

写一个 jsonParse 其实很简单,但是自己还是花了不少时间来做这件事,也走了一些弯路,还抄了不少参考资料才写好的 😓。

资料列表:

首先要解析 json,我们必须要了解 json 的语法规则。接触了 json 这么久,我还是第一次看 json 的语法规则定义,有不少新发现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
json
element

value
object
array
string
number
"true"
"false"
"null"

object
'{' ws '}'
'{' members '}'

members
member
member ',' members

member
ws string ws ':' element

array
'[' ws ']'
'[' elements ']'

elements
element
element ',' elements

element
ws value ws

string
'"' characters '"'

characters
""
character characters

character
'0020' . '10FFFF' - '"' - '\'
'\' escape

escape
'"'
'\'
'/'
'b'
'f'
'n'
'r'
't'
'u' hex hex hex hex

hex
digit
'A' . 'F'
'a' . 'f'

number
integer fraction exponent

integer
digit
onenine digits
'-' digit
'-' onenine digits

digits
digit
digit digits

digit
'0'
onenine

onenine
'1' . '9'

fraction
""
'.' digits

exponent
""
'E' sign digits
'e' sign digits

sign
""
'+'
'-'

ws
""
'0020' ws
'000A' ws
'000D' ws
'0009' ws

通过看规范,我们知道 json 支持 \u0020-\uffff unicode 的,支持部分转义字符,支持数字的科学计数法表示,这些在我们写 parser 时都是要考虑进去的。

规则定义属性名是 string,也就是可以是空串,或者包含转义字符。

1
2
3
4
{
"": "",
"\\n": "1"
}

这样的 json 是合法的,至少我之前是想不到的。

具体实现

lexer 的写法参考了专栏的示例,用了 DFA 的方式,状态转移图直接参考 json 官网的弹珠图就好了。

举个典型的,数字的状态转移。

number的弹珠图

我们定义如下状态

NumberMinus,
NumberZeroStart,
NumberNormal,
NumberDot,
NumberAfterDot,
NumberE,
NumberESign,
NumberAfterE,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
case DFAState.NumberMinus:
if (ch === CharacterCodes._0) {
this.state = DFAState.NumberZeroStart;
this.token.value += this.getCh();
} else if (this.isDigit(ch)) {
this.state = DFAState.NumberNormal;
this.token.value += this.getCh();
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberZeroStart:
if (ch === CharacterCodes.dot) {
this.state = DFAState.NumberAfterDot;
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;
case DFAState.NumberNormal:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
} else if (ch === CharacterCodes.dot) {
this.state = DFAState.NumberDot;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.e || ch === CharacterCodes.E) {
this.state = DFAState.NumberE;
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;
case DFAState.NumberDot:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
this.state = DFAState.NumberAfterDot;
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberE:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
this.state = DFAState.NumberAfterE;
} else if (
ch === CharacterCodes.plus ||
ch === CharacterCodes.minus
) {
this.state = DFAState.NumberESign;
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberESign:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
this.state = DFAState.NumberAfterE;
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberAfterDot:
if (ch === CharacterCodes.e || ch === CharacterCodes.E) {
this.state = DFAState.NumberE;
this.token.value += this.getCh();
} else if (this.isDigit(ch)) {
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;
case DFAState.NumberAfterE:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;


initState(ch: any) {
// 省略。。。。
this.token = new Token();
if (ch === CharacterCodes.minus) {
this.state = DFAState.NumberMinus;
this.token.type = TokenType.NumberType;
this.token.value += this.getCh();
} else if (ch === CharacterCodes._0) {
this.state = DFAState.NumberZeroStart;
this.token.type = TokenType.NumberType;
this.token.value += this.getCh();
} else if (this.isDigit(ch)) {
this.state = DFAState.NumberNormal;
this.token.type = TokenType.NumberType;
this.token.value += this.getCh();
} // 省略。。。
}

只是将图中状态的转移用代码的形式表现出来,做这类工作的时候,分析的过程比编码要重要的多。

parser 也很简单,用递归下降的方式很容易实现。

完整代码如下,只是实现了基本的功能,没做什么错误处理

lexer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
import { CharacterCodes } from "./CharacterCodes";
import { Token, TokenType } from "./token";

enum DFAState {
Initial,
OpenBracket,
CloseBracket,
OpenBraces,
CloseBraces,
Colon,
Comma,
NumberMinus,
NumberZeroStart,
NumberNormal,
NumberDot,
NumberAfterDot,
NumberE,
NumberESign,
NumberAfterE,
StringNormal,
StringSlash,
StringSlashU,
StringInHex,
StringEnd,
NullType,
TrueType,
FalseType,
}

const nullSteps = ["n", "nu", "nul", "null"];
const trueSteps = ["t", "tr", "tru", "true"];
const falseSteps = ["f", "fa", "fal", "fals", "false"];

enum LexErrorType {
EOF,
InvalidKeyword,
InvalidNumber,
InvalidStringCharacter,
UnexeceptedCharacter,
}

const wschar = [
CharacterCodes.space,
CharacterCodes.lineFeed,
CharacterCodes.tab,
CharacterCodes.formFeed,
CharacterCodes.backspace,
CharacterCodes.carriageReturn,
];

export class ErrorObject {
type!: LexErrorType;
constructor(type: LexErrorType) {
this.type = type;
}
}

export class Lexer {
script: string;
pos: number = 0;
state: DFAState = DFAState.Initial;
token: Token = new Token();
tokenList: Token[] = [];
hexTemp = "";

isDigit(ch: number): boolean {
return ch > CharacterCodes._0 && ch < CharacterCodes._9;
}
isHex(ch: number): boolean {
return (
this.isDigit(ch) ||
(ch >= CharacterCodes.a && ch <= CharacterCodes.f) ||
(ch >= CharacterCodes.A && ch <= CharacterCodes.F)
);
}

constructor(script: string) {
this.script = script;
}

read() {
if (this.pos < this.script.length) {
return this.script.charCodeAt(this.pos++);
} else {
throw LexErrorType.EOF;
}
}

getCh() {
return this.script.substring(this.pos - 1, this.pos);
}

tokenize(): Token[] {
let ch;
try {
while ((ch = this.read())) {
try {
switch (this.state) {
case DFAState.Initial:
case DFAState.OpenBracket:
case DFAState.CloseBracket:
case DFAState.OpenBraces:
case DFAState.CloseBraces:
case DFAState.Colon:
case DFAState.Comma:
case DFAState.StringEnd:
this.initState(ch);
break;
case DFAState.NumberMinus:
if (ch === CharacterCodes._0) {
this.state = DFAState.NumberZeroStart;
this.token.value += this.getCh();
} else if (this.isDigit(ch)) {
this.state = DFAState.NumberNormal;
this.token.value += this.getCh();
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberZeroStart:
if (ch === CharacterCodes.dot) {
this.state = DFAState.NumberAfterDot;
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;
case DFAState.NumberNormal:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
} else if (ch === CharacterCodes.dot) {
this.state = DFAState.NumberDot;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.e || ch === CharacterCodes.E) {
this.state = DFAState.NumberE;
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;
case DFAState.NumberDot:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
this.state = DFAState.NumberAfterDot;
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberE:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
this.state = DFAState.NumberAfterE;
} else if (
ch === CharacterCodes.plus ||
ch === CharacterCodes.minus
) {
this.state = DFAState.NumberESign;
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberESign:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
this.state = DFAState.NumberAfterE;
} else {
throw new ErrorObject(LexErrorType.InvalidNumber);
}
break;
case DFAState.NumberAfterDot:
if (ch === CharacterCodes.e || ch === CharacterCodes.E) {
this.state = DFAState.NumberE;
this.token.value += this.getCh();
} else if (this.isDigit(ch)) {
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;
case DFAState.NumberAfterE:
if (this.isDigit(ch)) {
this.token.value += this.getCh();
} else {
this.initState(ch);
}
break;
case DFAState.StringNormal:
if (ch >= 0 && ch <= 0x1f) {
throw new ErrorObject(LexErrorType.InvalidStringCharacter);
} else if (ch === CharacterCodes.doubleQuote) {
this.token.value += this.getCh();
this.state = DFAState.StringEnd;
} else if (ch === CharacterCodes.backslash) {
this.token.value += this.getCh();
this.state = DFAState.StringSlash;
} else {
this.token.value += this.getCh();
}
break;
case DFAState.StringSlash:
if (
[
CharacterCodes.n,
CharacterCodes.t,
CharacterCodes.f,
CharacterCodes.b,
CharacterCodes.r,
CharacterCodes.doubleQuote,
CharacterCodes.slash,
CharacterCodes.backslash,
].includes(ch)
) {
this.token.value += this.getCh();
this.state = DFAState.StringNormal;
} else if (ch === CharacterCodes.u) {
this.token.value += this.getCh();
this.hexTemp = "";
this.state = DFAState.StringSlashU;
} else {
throw new ErrorObject(LexErrorType.InvalidStringCharacter);
}
break;
case DFAState.StringSlashU:
const character = this.getCh();
if (this.isHex(ch)) {
this.token.value += character;
this.hexTemp += character;
if (this.hexTemp.length === 4) {
this.state = DFAState.StringNormal;
}
} else {
throw new ErrorObject(LexErrorType.InvalidStringCharacter);
}
break;
case DFAState.NullType:
{
let character = this.getCh();
if (nullSteps.includes(this.token.value + character)) {
this.token.value += character;
} else {
if (this.token.value === "null") {
this.initState(ch);
} else {
throw new ErrorObject(LexErrorType.InvalidKeyword);
}
}
}
break;
case DFAState.TrueType:
{
let character = this.getCh();
if (trueSteps.includes(this.token.value + character)) {
this.token.value += character;
} else {
if (this.token.value === "true") {
this.initState(ch);
} else {
throw new ErrorObject(LexErrorType.InvalidKeyword);
}
}
}
break;
case DFAState.FalseType:
{
let character = this.getCh();
if (falseSteps.includes(this.token.value + character)) {
this.token.value += character;
} else {
if (this.token.value === "false") {
this.initState(ch);
} else {
throw new ErrorObject(LexErrorType.InvalidKeyword);
}
}
}
break;
}
} catch (err) {
if (err instanceof ErrorObject) {
this.handleError(err);
break;
}
}
}
} catch (err) {
switch (err) {
case LexErrorType.EOF:
console.log("解析完毕");
break;
}
}
this.tokenList.push(this.token);
return this.tokenList;
}

initState(ch: any) {
if (this.token.value) {
this.tokenList.push(this.token);
}
this.token = new Token();
if (ch === CharacterCodes.doubleQuote) {
this.state = DFAState.StringNormal;
this.token.type = TokenType.StringType;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.minus) {
this.state = DFAState.NumberMinus;
this.token.type = TokenType.NumberType;
this.token.value += this.getCh();
} else if (ch === CharacterCodes._0) {
this.state = DFAState.NumberZeroStart;
this.token.type = TokenType.NumberType;
this.token.value += this.getCh();
} else if (this.isDigit(ch)) {
this.state = DFAState.NumberNormal;
this.token.type = TokenType.NumberType;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.openBracket) {
this.state = DFAState.OpenBracket;
this.token.type = TokenType.OpenBracket;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.closeBracket) {
this.state = DFAState.CloseBracket;
this.token.type = TokenType.CloseBracket;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.openBrace) {
this.state = DFAState.OpenBraces;
this.token.type = TokenType.OpenBraces;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.closeBrace) {
this.state = DFAState.CloseBraces;
this.token.type = TokenType.CloseBraces;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.colon) {
this.state = DFAState.Colon;
this.token.type = TokenType.Colon;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.comma) {
this.state = DFAState.Comma;
this.token.type = TokenType.Comma;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.n) {
this.state = DFAState.NullType;
this.token.type = TokenType.NullType;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.t) {
this.state = DFAState.TrueType;
this.token.type = TokenType.TrueType;
this.token.value += this.getCh();
} else if (ch === CharacterCodes.f) {
this.state = DFAState.FalseType;
this.token.type = TokenType.FalseType;
this.token.value += this.getCh();
} else if (wschar.includes(ch)) {
// 跳过空白字符
} else {
throw new ErrorObject(LexErrorType.UnexeceptedCharacter);
}
}

handleError(err: ErrorObject) {
console.log(`在位置${this.pos - 1}遇到错误`);
console.log(`错误字符 ${this.getCh()}`);

switch (err.type) {
case LexErrorType.InvalidKeyword:
console.log("非法关键字");
case LexErrorType.UnexeceptedCharacter:
console.log("不被接受的字符: " + this.getCh());
}
}
}

parser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import { Lexer } from "./lexer";
import { Token, TokenType } from "./token";

enum AstNodeType {
JsonObject,
JsonObjectProperty,
JsonArray,
JsonString,
JsonNumber,
JsonTrueType,
JsonFalseType,
JsonNullType,
}

enum ParseErrorType {
Error,
}

class ParseErrorObject {
type!: ParseErrorType;
constructor(type: ParseErrorType) {
this.type = type;
}
}

class AstNode {
type: AstNodeType | null = null;
children: AstNode[] = [];
value: any = undefined;
parent: AstNode | null = null;
}

class Parser {
tokenList: Token[];
pos = 0;
peek() {
return this.tokenList[this.pos];
}
read() {
return this.tokenList[this.pos++];
}

constructor(tokenList: any[]) {
this.tokenList = tokenList;
}

parseProgram() {
try {
let parseResult = this.parseValue() as AstNode;
return parseResult.value;
} catch (err) {
console.log("错误位置", this.pos, this.read());
}
}

parseValue() {
let parseResult;
parseResult = this.parseNumber();
if (parseResult !== false) {
return parseResult;
}
parseResult = this.parseString();
if (parseResult !== false) {
return parseResult;
}
parseResult = this.parseTrueType();
if (parseResult !== false) {
return parseResult;
}
parseResult = this.parseFalseType();
if (parseResult !== false) {
return parseResult;
}
parseResult = this.parseNullType();
if (parseResult !== false) {
return parseResult;
}
parseResult = this.parseArray();
if (parseResult !== false) {
return parseResult;
}
parseResult = this.parseObject();
if (parseResult !== false) {
return parseResult;
}
throw new ParseErrorObject(ParseErrorType.Error);
}

parseObject() {
const astNode = new AstNode();
astNode.type = AstNodeType.JsonObject;
astNode.value = {};
if (this.peek().type !== TokenType.OpenBraces) {
return false;
}
this.read();
while (true) {
if (this.peek().type === TokenType.CloseBraces) {
this.read();
break;
}
const properyAstNode = this.parseObjectProperty() as AstNode;
properyAstNode.parent = astNode;
astNode.children.push(properyAstNode);
astNode.value[properyAstNode.value[0]] = properyAstNode.value[1];
if (this.peek().type === TokenType.Comma) {
this.read();
} else {
if (this.peek().type === TokenType.CloseBraces) {
this.read();
break;
} else {
throw new ParseErrorObject(ParseErrorType.Error);
}
}
}
return astNode;
}

parseArray() {
const astNode = new AstNode();
astNode.type = AstNodeType.JsonArray;
astNode.value = [];
if (this.peek().type !== TokenType.OpenBracket) {
return false;
}
this.read();

while (true) {
if (this.peek().type === TokenType.CloseBracket) {
this.read();
break;
}
const valueAstNode = this.parseValue() as AstNode;
valueAstNode.parent = astNode;
astNode.children.push(valueAstNode);
astNode.value.push(valueAstNode.value);
if (this.peek().type === TokenType.Comma) {
this.read();
} else {
if (this.peek().type === TokenType.CloseBracket) {
this.read();
break;
} else {
throw new ParseErrorObject(ParseErrorType.Error);
}
}
}
return astNode;
}

parseObjectProperty() {
if (this.peek().type !== TokenType.StringType) {
return false;
}
const propertyNameToken = this.read();
if (this.peek().type !== TokenType.Colon) {
throw new ParseErrorObject(ParseErrorType.Error);
}
this.read();
const valueAstNode = this.parseValue() as AstNode;
const astNode = new AstNode();
astNode.type = AstNodeType.JsonObjectProperty;
valueAstNode.parent = astNode;
astNode.children.push(valueAstNode);
astNode.value = [JSON.parse(propertyNameToken.value), valueAstNode.value];
return astNode;
}

parseNumber() {
if (this.peek().type !== TokenType.NumberType) {
return false;
}
const astNode = new AstNode();
astNode.type = AstNodeType.JsonNumber;
const token = this.read();
astNode.value = JSON.parse(token.value);
return astNode;
}

parseString() {
if (this.peek().type !== TokenType.StringType) {
return false;
}
const astNode = new AstNode();
astNode.type = AstNodeType.JsonString;
const token = this.read();
astNode.value = JSON.parse(token.value);
return astNode;
}

parseTrueType() {
if (this.peek().type !== TokenType.TrueType) {
return false;
}
const astNode = new AstNode();
astNode.type = AstNodeType.JsonTrueType;
const token = this.read();
astNode.value = JSON.parse(token.value);
return astNode;
}

parseFalseType() {
if (this.peek().type !== TokenType.FalseType) {
return false;
}
const astNode = new AstNode();
astNode.type = AstNodeType.JsonFalseType;
const token = this.read();
astNode.value = JSON.parse(token.value);
return astNode;
}

parseNullType() {
if (this.peek().type !== TokenType.NullType) {
return false;
}
const astNode = new AstNode();
astNode.type = AstNodeType.JsonNullType;
const token = this.read();
astNode.value = JSON.parse(token.value);
return astNode;
}
}

const lexer = new Lexer(
`{"name":"\\n\\uaaaatest project",\n\n"number":1.5E5, "array":[1,2,true], "description":"test project","private":true,"workspaces":{"shell":"shell"}}`
);

const tokenList = lexer.tokenize();

tokenList.forEach((token, index) => {
console.log(
index,
token.value.padEnd(20, " "),
TokenType[token.type as TokenType]
);
});

const parser = new Parser(tokenList);

console.log(parser.parseProgram());

上一篇基本上搞懂了怎么定义响应式对象的,这里研究一下怎么触发侦测的

api/watch.ts

watch

之前比较疑惑的是,为什么 watch 的第一个参数需要传递一个 getter 或者 ref,而不能传递 reactive.prop
其实这里是调用了 Vue.$watch 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// @vue/composition-api/src/apis/watch.ts
const stop = vm.$watch(getter, callback, {
immediate: options.immediate,
deep: deep,
sync: isSync,
});

// vue/src/core/instance/state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
}

// vue/src/core/observer/watcher.js
export default class Watcher {
// ... 省略各种定义

constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// 省略无关内容
// parse expression for getter
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
// 开发环境下警告 省略
}
}
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
}

可以看到 $watch 要求传入 getter 或者一个路径字符串,这个 getter 是要在进行依赖收集的时候被调用的
composition-api 中的 watch 使用了 vue2 自身的 watch 机制

至于 watch 的响应式原理,大致如下吧

watch

通过看源码也搞清了一个点,这里的 childObj 和深度监听一点关系都没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// vue/src/core/observer/index.js
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 无关代码 略过
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
// 无关代码 略过

它的作用是:每个 Observer 对象都有一个 dep 属性,如果使用 Vue.$set 给对象动态添加新属性,会给赋予该属性响应式并调用该对象的 dep 的 notify

看下面的例子应该就可以完全理解了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default {
data() {
return {
ttt: {
a: 1,
},
};
},
mounted() {
setTimeout(() => {
this.ttt.a = 3; // 无法触发 watch
this.$set(this.ttt, "a", 5); // 无法触发 watch 因为不是新属性,直接赋值后就返回了
this.$set(this.ttt, "b", 5); // 可以触发,因为 ttt 的 __ob__ 上的 dep 调用了 notify
this.ttt = {}; // 可以触发
}, 1000);
},
watch: {
ttt(val) {
console.log("watch", val);
},
},
};

那么看一下深度监听是怎么实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// vue/src/core/observer/watcher.js
class Watcher {
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
}

export function traverse(val: any) {
_traverse(val, seenObjects);
seenObjects.clear();
}

function _traverse(val: any, seen: SimpleSet) {
let i, keys;
const isA = Array.isArray(val);
if (
(!isA && !isObject(val)) ||
Object.isFrozen(val) ||
val instanceof VNode
) {
return;
}
if (val.__ob__) {
const depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return;
}
seen.add(depId);
}
if (isA) {
i = val.length;
while (i--) _traverse(val[i], seen);
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) _traverse(val[keys[i]], seen);
}
}

就是递归遍历,深度访问对象所有的值,触发对象所有值的依赖收集。
还有一个点,Vue.$watch 返回了一个方法用于停止监听,看下它是怎么实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// vue/src/core/instance/state.js
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// 省略
return function unwatchFn() {
watcher.teardown();
};
};
// vue/src/core/observer/watcher.js
class Watcher {
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}

// vue/src/core/observer/dep.js
class Dep {
removeSub(sub: Watcher) {
remove(this.subs, sub);
}
}

// vue/src/shared/util.js
/**
* Remove an item from an array.
*/
export function remove(arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1);
}
}
}

就是把 watcher 从所有依赖的属性的 subs 中删除

computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// @vue/composition-api/src/apis/computed.ts
export function computed<T>(
options: Option<T>["get"] | Option<T>
): ComputedRef<T> | WritableComputedRef<T> {
const vm = getCurrentInstance()?.proxy;
let get: Option<T>["get"], set: Option<T>["set"] | undefined;
if (typeof options === "function") {
get = options;
} else {
get = options.get;
set = options.set;
}

let computedSetter;
let computedGetter;

if (vm && !vm.$isServer) {
const { Watcher, Dep } = getVueInternalClasses();
let watcher: any;
computedGetter = () => {
if (!watcher) {
watcher = new Watcher(vm, get, noopFn, { lazy: true });
}
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
};

computedSetter = (v: T) => {
if (set) {
set(v);
}
};
} else {
// fallback
const computedHost = defineComponentInstance(getVueConstructor(), {
computed: {
$$state: {
get,
set,
},
},
});

vm && vm.$on("hook:destroyed", () => computedHost.$destroy());

computedGetter = () => (computedHost as any).$$state;
computedSetter = (v: T) => {
(computedHost as any).$$state = v;
};
}

return createRef<T>(
{
get: computedGetter,
set: computedSetter,
},
!set
);
}

通过 createRef 传递 get 和 set 创建了 Ref。
computed 是惰性的(初始时 lazy 和 dirty 都为 true),只有在被求值的时候才会计算,然后进行相应的依赖收集

1
2
3
4
5
6
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}

这里有点难理解的,其实是对应了这种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default {
name: "App",
data() {
return {
t: 5,
};
},
mounted() {
setTimeout(() => {
this.t = 10;
console.log(this.tttt);
}, 1000);
},
computed: {
tt() {
return this.t;
},
ttt() {
return this.tt;
},
},
};

当 computed 依赖于另外的 computed 时,访问 t 的 getter 只会给 tt 的 watcher 添加订阅,但是我们想要的给 tt 和 ttt 的 watcher 都添加订阅
Vue 对于 Dep.target 使用了一个栈去管理,而这种嵌套的 computed 的访问也是栈式的。
访问 ttt 的 getter 时,执行 ttt 的 watcher 的 evaluate(),ttt 的 watcher 入栈,然后嵌套访问 tt 的 getter,然后执行 tt 的 watcher 的 evaluate(),tt 的 watcher 入栈。tt 这一层的 evaluate() 执行完毕后 tt 的 watcher 退栈,这时 Dep.target 是指向 ttt.watcher 的,执行 watcher.depend(),就可以给 ttt 的 watcher 添加订阅了。

分析的是 @vue/composition-api 的代码,版本是 V1.0.0-rc.1
vue 的版本是 2.6.12

因为对于 reactive 和 ref 各自的行为和用法尚有不明之处,所以看一下源码,记录一下笔记

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├─scripts
├─src
│ ├─apis
│ ├─component
│ ├─reactivity
│ ├─types
│ └─utils
├─test
│ ├─apis
│ ├─helpers
│ ├─ssr
│ ├─types
│ └─v3
│ ├─reactivity
│ └─runtime-core
└─test-dts

这次主要关注 src/reactivity 和 src/apis 中的代码

reactivity/reactive.ts

reactive

reactive 和 ref 相关的方法在 reactivity 目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @vue/composition-api/src/reactivity/reactive.ts
export function reactive<T extends object>(obj: T): UnwrapRef<T> {
if (
!(isPlainObject(obj) || isArray(obj)) ||
isRaw(obj) ||
!Object.isExtensible(obj)
) {
return obj as any;
}

const observed = observe(obj);
setupAccessControl(observed);
return observed as UnwrapRef<T>;
}

然后是看 observe 是啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @vue/composition-api/src/reactivity/reactive.ts
function observe(obj) {
var Vue = getRegisteredVueOrDefault();
var observed;
if (Vue.observable) {
observed = Vue.observable(obj);
} else {
var vm = defineComponentInstance(Vue, {
data: {
$$state: obj,
},
});
observed = vm._data.$$state;
}
// in SSR, there is no __ob__. Mock for reactivity check
if (!hasOwn(observed, "__ob__")) {
def(observed, "__ob__", mockObserver(observed));
}
return observed;
}

如果 Vue 上有 Vue.observe,使用 Vue.observe

1
2
3
4
5
// vue/src/core/global-api/index.js
Vue.observable = function (obj) {
observe(obj);
return obj;
};

Vue.observe 直接返回了传入的对象,也就是说,reactive 的作用只是给对象增加响应式,返回的值是对象本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vue/src/core/observer/index.js
function observe(value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return;
}
var ob;
if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob;
}

如果之前没有被 observe 过,使用 new Observer(value) 添加响应式属性,否则什么都不做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vue/src/core/observer/index.js
var Observer = function Observer(value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};

可以看到这个构造方法给对象增加了 __ob__属性。

那么看以下代码

1
2
3
4
5
let a = {
a: 100,
);
let b = reactive(a);
reactive(b)

这么使用 reactive 是没问题的,同时 a,b 这里指向的时同一个响应式对象,reactive(b) 这句实际上没有任何作用

显然我们不能重新给 a 或 b 赋值,因为响应性是赋予在 a,b 指向的对象上的

然后看下 setupAccessControl 干了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @vue/composition-api/src/reactivity/reactive.ts
function setupAccessControl(target: AnyObject): void {
if (
!isPlainObject(target) ||
isRaw(target) ||
Array.isArray(target) ||
isRef(target) ||
isComponentInstance(target) ||
accessModifiedSet.has(target)
)
return;

accessModifiedSet.set(target, true);

const keys = Object.keys(target);
for (let i = 0; i < keys.length; i++) {
defineAccessControl(target, keys[i]);
}
}

accessModifiedSet 是一个 weakmap,如果之前对对象设置过,就不再设置了
对对象的每一个属性调用 defineAccessControl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// @vue/composition-api/src/reactivity/reactive.ts
/**
* Auto unwrapping when access property
*/
export function defineAccessControl(target: AnyObject, key: any, val?: any) {
if (key === "__ob__") return;
if (isRaw(target[key])) return;

let getter: (() => any) | undefined;
let setter: ((x: any) => void) | undefined;
const property = Object.getOwnPropertyDescriptor(target, key);
if (property) {
if (property.configurable === false) {
return;
}
getter = property.get;
setter = property.set;
if (
(!getter || setter) /* not only have getter */ &&
arguments.length === 2
) {
val = target[key];
}
}

// 如果 val 是一个对象 递归调用 defineAccessControl
setupAccessControl(val);
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function getterHandler() {
const value = getter ? getter.call(target) : val;
// if the key is equal to RefKey, skip the unwrap logic
if (key !== RefKey && isRef(value)) {
return value.value;
} else {
return value;
}
},
set: function setterHandler(newVal) {
if (getter && !setter) return;

const value = getter ? getter.call(target) : val;
// If the key is equal to RefKey, skip the unwrap logic
// If and only if "value" is ref and "newVal" is not a ref,
// the assignment should be proxied to "value" ref.
if (key !== RefKey && isRef(value) && !isRef(newVal)) {
value.value = newVal;
} else if (setter) {
setter.call(target, newVal);
} else {
val = newVal;
}
setupAccessControl(newVal);
},
});
}

function setupAccessControl(target: AnyObject): void {
if (
!isPlainObject(target) ||
isRaw(target) ||
Array.isArray(target) ||
isRef(target) ||
isComponentInstance(target) ||
accessModifiedSet.has(target)
)
return;

accessModifiedSet.set(target, true);

const keys = Object.keys(target);
for (let i = 0; i < keys.length; i++) {
defineAccessControl(target, keys[i]);
}
}

如果 val 是一个对象 递归调用 defineAccessControl
defineAccessControl 的作用是 reactive 对象在读写属性时,如果属性是一个 Ref 对象,可以自动拆装箱

shallowReactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// @vue/composition-api/src/reactivity/reactive.ts
export function shallowReactive(obj: any): any {
if (
!(isPlainObject(obj) || isArray(obj)) ||
isRaw(obj) ||
!Object.isExtensible(obj)
) {
return obj as any;
}

const observed = observe({});
setupAccessControl(observed);

const ob = (observed as any).__ob__;

for (const key of Object.keys(obj)) {
let val = obj[key];
let getter: (() => any) | undefined;
let setter: ((x: any) => void) | undefined;
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property) {
if (property.configurable === false) {
continue;
}
getter = property.get;
setter = property.set;
}

Object.defineProperty(observed, key, {
enumerable: true,
configurable: true,
get: function getterHandler() {
const value = getter ? getter.call(obj) : val;
ob.dep?.depend();
return value;
},
set: function setterHandler(newVal) {
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
ob.dep?.notify();
},
});
}
return observed;
}

只对响应式对象的属性手动设置响应式并管理依赖收集,如果属性是对象,不会深度添加响应式

isReactive

1
2
3
4
// @vue/composition-api/src/reactivity/reactive.ts
export function isReactive(obj: any): boolean {
return Boolean(obj?.__ob__ && !obj.__ob__?.__raw__);
}

就是看对象是否有 __ob__ 属性并且没有使用 markRaw 标记

markRaw && isRaw && toRaw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// @vue/composition-api/src/reactivity/reactive.ts
export function markRaw<T extends object>(obj: T): T {
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
return obj;
}

// set the vue observable flag at obj
const ob = createObserver();
ob.__raw__ = true;
def(obj, "__ob__", ob);

// mark as Raw
rawSet.set(obj, true);

return obj;
}
export function isRaw(obj: any): boolean {
return Boolean(obj?.__ob__ && obj.__ob__?.__raw__);
}
export function toRaw<T>(observed: T): T {
if (isRaw(observed) || !Object.isExtensible(observed)) {
return observed;
}

return (observed as any)?.__ob__?.value || observed;
}

markRaw 给目标添加了 vue observable 的 flag,这样 Vue 应该就不会再给对象添加响应式属性了
rawSet 也是一个 weakmap
isRaw 通过 markRaw 设置的 __ob__.__raw__ flag 来判断
toRaw 是获取响应式对象的原本对象
这里 和 vue3 实现的有些差异,通过之前我们知道 reactive 是在原对象上增加响应式属性__ob__的,而 vue3 是通过 proxy 实现的,所以 toRaw 拿到的是被代理的原始对象,而这里这个 toRaw 有点意义不明

reactivity/ref.ts

这个文件上来就是一堆类型定义,看的我人晕掉了。。

ref && isRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @vue/composition-api/src/reactivity/ref.ts
export const RefKey = "composition-api.refKey";

export function ref<T extends object>(
raw: T
): T extends Ref ? T : Ref<UnwrapRef<T>>;
export function ref<T>(raw: T): Ref<UnwrapRef<T>>;
export function ref<T = any>(): Ref<T | undefined>;
export function ref(raw?: unknown) {
if (isRef(raw)) {
return raw;
}

const value = reactive({ [RefKey]: raw });
return createRef({
get: () => value[RefKey] as any,
set: (v) => ((value[RefKey] as any) = v),
});
}

好家伙,上来就是三个重载,然后 Ref<UnwrapRef<T>>这玩意好绕,先不管了
如果传入的对象已经是 Ref 对象,直接返回,否则通过 reactive 创建一个响应式对象,然后返回通过 createRef 创建的对象

1
2
3
4
// @vue/composition-api/src/reactivity/ref.ts
export function isRef<T>(value: any): value is Ref<T> {
return value instanceof RefImpl;
}

通过 instanceOf 判断是否是 Ref 对象是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// @vue/composition-api/src/reactivity/ref.ts
export function createRef<T>(options: RefOption<T>, readonly = false) {
const r = new RefImpl<T>(options);
// seal the ref, this could prevent ref from being observed
// It's safe to seal the ref, since we really shouldn't extend it.
// related issues: #79
const sealed = Object.seal(r);

readonlySet.set(sealed, true);
return sealed;
}
class RefImpl<T> implements Ref<T> {
readonly [_refBrand]!: true;
public value!: T;
constructor({ get, set }: RefOption<T>) {
proxy(this, "value", {
get,
set,
});
}
}
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noopFn,
set: noopFn,
};

export function proxy(
target: any,
key: string,
{ get, set }: { get?: Function; set?: Function }
) {
sharedPropertyDefinition.get = get || noopFn;
sharedPropertyDefinition.set = set || noopFn;
Object.defineProperty(target, key, sharedPropertyDefinition);
}

createRef 就是创建一个 RefImpl 对象,然后代理了其中 value 属性的 getter 和 setter
Object.seal 让一个对象密封,并返回被密封后的对象。密封对象是指那些不能添加新的属性,不能删除已有属性,以及不能修改已有属性的可枚举性、可配置性、可写性,但可以修改已有属性的值的对象。

toRef && toRefs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// @vue/composition-api/src/reactivity/ref.ts
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
const v = object[key];
if (isRef<T[K]>(v)) return v;

return createRef({
get: () => object[key],
set: (v) => (object[key] = v),
});
}
export function toRefs<T extends Data = Data>(obj: T): ToRefs<T> {
if (!isPlainObject(obj)) return obj as any;

const ret: any = {};
for (const key in obj) {
ret[key] = toRef(obj, key);
}

return ret;
}

把对象的某个属性变成 Ref 类型
如果已经是 Ref 类型了,直接返回,否则返回通过 createRef 创建的对象
需要注意传入的 object 必须是一个响应式对象,否则返回的 Ref 是不具有响应式特性的,因为无法进行依赖收集相关的操作。

unref

1
2
3
4
// @vue/composition-api/src/reactivity/ref.ts
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
return isRef(ref) ? (ref.value as any) : ref;
}

返回 Ref 的 value 属性

shallowRef

1
2
3
4
5
6
7
8
9
10
11
// @vue/composition-api/src/reactivity/ref.ts
export function shallowRef(raw?: unknown) {
if (isRef(raw)) {
return raw;
}
const value = shallowReactive({ [RefKey]: raw });
return createRef({
get: () => value[RefKey] as any,
set: (v) => ((value[RefKey] as any) = v),
});
}

很好理解,这里的 reactive 对象是通过 shallowReactive 创建的

customRef

1
2
3
4
5
6
7
8
9
10
11
12
// @vue/composition-api/src/reactivity/ref.ts
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
const version = ref(0);
return createRef(
factory(
() => void version.value,
() => {
++version.value;
}
)
);
}

允许传入一个工厂方法,属于比较高级的应用,略过

triggerRef

1
2
3
4
5
6
// @vue/composition-api/src/reactivity/ref.ts
export function triggerRef(value: any) {
if (!isRef(value)) return;

value.value = value.value;
}

vue3 文档上的例子,手动触发响应式侦测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const shallow = shallowRef({
greet: "Hello, world",
});

// Logs "Hello, world" once for the first run-through
watchEffect(() => {
console.log(shallow.value.greet);
});

// This won't trigger the effect because the ref is shallow
shallow.value.greet = "Hello, universe";

// Logs "Hello, universe"
triggerRef(shallow);

proxyRefs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// @vue/composition-api/src/reactivity/ref.ts
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
if (isReactive(objectWithRefs)) {
return objectWithRefs as ShallowUnwrapRef<T>;
}
const value: Record<string, any> = reactive({ [RefKey]: objectWithRefs });

for (const key of Object.keys(objectWithRefs)) {
proxy(value, key, {
get() {
if (isRef(value[key])) {
return value[key].value;
}
return value[key];
},
set(v: unknown) {
if (isRef(value[key])) {
return (value[key].value = unref(v));
}
value[key] = unref(v);
},
});
}

return value as ShallowUnwrapRef<T>;
}

看了一下,是 vue3 的新特性方法,这里也跟进了
如果 objectWithRefs 的属性是 Ref 的话,自动进行拆装箱