Vue
现在已经迭代到 3+ 版本,阅读官方文档的过程中发现作者的一些理念和思路很合我口味,很多概念与方案都是基于解决实际问题提出并实现的,且在权衡利弊后勇于打破常规,比如如何看待关注点分离?。可见,Vue 之所以流行,不单单因为作者是国人,更应该是由于 Vue 作为新一代的解决方案提升了前端编程的体验与效率。
本文介绍几个核心概念。
选项式 vs 组合式
Vue 提供两种代码的书写风格——选项式
和组合式
。可简单理解为:前者面向对象编程;后者函数式编程。
选项式
:如果你有微信小程序的开发经验,就知道选项式是什么样子,其实就是将组件的逻辑封装到一个对象中,这个对象预定义多个字段和方法(如 data、methods 和 mounted),开发人员需要在适当的地方组织代码。对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致,同时,响应性相关的细节由框架本身处理,对初学者而言更为友好。
组合式
:传统的自由无约束的编码风格,顶层就是各个成员变量和 functions,及一些钩子函数。似乎回到了 js 最初的模样,在对象、类、prototype 这些概念普及以前,大多数代码就是一坨变量加一坨 function,然后 onclick 调用。但是 Vue 的组合式风格依托其底层的依赖注入系统,及完善的响应式 API,使得情况不像看上去那么简单,而是呈现出一种螺旋向上的味道,耐人寻味。
官方文档对这两种风格有一些比较,个人比较倾向于组合式,所以本文 Vue 代码都是组合式的。
响应式基础
所谓响应式,就是视图会随着 JS 对象状态的改变而自动改变(也就是MVVM
模式),有这种效果的对象就叫作响应式对象
(其实就是 JavaScript Proxy)。在组合式 API 中,我们需要显式声明响应式对象,有两种方式——reactive()
和ref()
。
reactive()
该 API 返回的对象,是传入对象的代理对象,其所有属性及深层的子属性,都是响应式的。响应式对象的内嵌对象也是响应式对象,就算给它赋值普通对象,如:
const proxy = reactive({})
const raw = {}
proxy.nested = raw // proxy.nested 自动就是响应式对象
console.log(proxy.nested === raw) // false,代理对象和原始对象不是全等的
reactive() 的注意事项和原理
reactive() 有一定的局限:它仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的基础类型无效;需要尽量避免对一个响应式变量重新赋值,除非我们有办法将新对象和视图重新建立连接;且当我们将响应式对象的属性(基础类型)赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性,如:
let state = reactive({ count: 0 })
// 官方文档这里的表述不是很准确,下面是我的表述:
// 表面上看是重新赋值的 state 状态变化没有引起视图的变化,似乎响应连接丢失了,
// 其实原对象上的响应式连接还在,但是原对象在此处已无法继续访问,所以响应式连接在不在不重要了,
// 重建响应就需要建立视图和新对象的连接。
state = reactive({ count: 1 })
let n = state.count // 基础类型赋值,失去响应性连接
n++ // 不影响 state
let { count } = state
count++ // 同上
// state.count 值传递给基础类型形参,也失去响应性连接了
callSomeFunction(state.count)
对于这些情况,有后端经验的同学如果将 reactive() 得到的响应式对象类比成引用类型对象就很好理解,这就是引用类型和值类型在使用过程中需要注意的一些点——变量赋值,如果是引用类型的话,那么指向的是对象的内存地址(新旧对象的内存地址自然是不一样的);如果是值类型,虽然代码看上去都是指向 state.count,其实是拷贝源值到自己的内存块,拷贝完了之后就和源没有关系了。
对于引用类型的“问题”,只要注意点就好了,但是在响应式的场景下,值类型的“拷贝”特性确实让人有点闹心。有没有类似于后端的装箱
操作呢?
ref()
该 API 返回的也是响应式对象,它用于将值类型(基础类型)封装成引用类型(对象类型)。也就是说,我们将上一小节代码改造一下,就能保持基础类型数据在各个变量间传递后的响应性,如下:
const state = reactive({
count: ref(0)
})
let n = state.count // 现在 state.count 是引用类型,所以它和 n 指向的是同一个对象
n.value++ // 需要用 value 操作值
// 注意 value 也是响应式的,也就是传递给它的普通对象会自动转为响应式对象,和 reactive() 那边的情况一样
// 同时要注意直接替换掉整个对象会导致出现响应连接丢失的问题(上面提到过)
n.value = { name: 'Tony' }
简言之,我们可以将 ref 对象就看作引用类型对象,就能很快理解它的特性了。唯一要注意的是访问和操作它的值需要 .value,但是在某些时候框架也会帮我们自动解包(不需要使用 .value),可以参看官方文档。
组合式函数
是利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。说白了,就是业务逻辑封装,它表现形式不是对象,但是有状态,状态作为响应式对象对外暴露使用(如果有的话)。推测是为了和对象形式区分,才称之为组合式函数(就像选项式风格和组合式风格的区别)。
Vue 2 的用户可能会对mixins
选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里,然而 mixins 没有自我范围的约束,就像页面里使用引入的 js 文件,容易和其它 js 文件产生命名冲突,对象来源也不清晰,编码时不注意的话也容易产生模块和模块之间隐性的依赖。
其它几个 API
nextTick()
:DOM 更新是有间隔时间的,在间隔时间内每个组件发生的所有状态改变汇总后一次更新。可以给该函数传递一个回调,在最近的一次 DOM 更新后执行。类似于 HTML5 新增的 window.requestAnimationFrame()
。
watchEffect(callback)
:callback 中涉及到的响应式对象状态的变更会触发 callback 执行,如下:
const count = ref(0)
watchEffect(() => console.log(count.value)) // 马上执行一次,-> 输出 0
count.value++ // -> 输出 1
watch()
:同 watchEffect() 不同在于,watch() 需要显式地给它传递要监听的响应式对象。
构建工具 Vite
伴随 Vue 3 一起出来的还有新的构建工具Vite
。下面会简单介绍 Vite 涉及到的关键技术和工具,以及同其它构建工具的比较。
ESM
不同于之前的CJS,AMD,CMD等,ESM
是 ECMA 标准化模块系统,也就是说我们可以直接在浏览器中去执行 import,动态引入模块。作为 ECMA 标准,目前 ESM 已经得到 92% 以上浏览器的支持。
ESM 的执行可以分为三个步骤:
- 构建: 确定模板依赖关系,下载并将所有的文件解析为模块记录;
- 实例化: 将模块记录转换为一个模块实例,为所有的模块分配内存空间,依照导出、导入语句把模块指向对应的内存地址;
- 运行:运行代码,填充内存空间。
ESM 使用引用模式指向模块,也就是说如果引用的模块已经存在,那么直接返回模块的内存地址。而 CJS 采用的是拷贝模式,即所有导出模块都是独立的实例。可见前者比后者的效率要高。
基于 ESM,还能做到按需加载模块(碰到 import 再去请求加载文件)。但是我们一般只在开发环境下使用这个特性(不需要每次改动都导致整个 bundle 模块全量打包编译),原因如下段所述。
尽管原生ESM
现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码预先进行Tree Shaking
(移除那些没被使用的代码)、懒加载和 chunk 分割(以获得更好的缓存)。
Rollup
Rollup
就是基于 ESM 模块的打包工具,比Webpack
和Browserify
使用的 CommonJS 模块机制更高效。Rollup 能针对源码进行 Tree Shaking,以及 Scope Hoisting 以减小输出文件大小提升运行性能。
Esbuild
Esbuild
提供了与Webpack
、Rollup
等工具相似的资源打包能力,但其打包速度却是其他工具的 10~100 倍,原因有二:
- 大多数前端打包工具都是基于 JavaScript 实现的,边运行边解释。而 Esbuild 则选择使用 Go 语言编写,编译为机器语言,在启动的时候直接执行,性能更高;
- JavaScript 本质上是一门单线程语言,直到引入
Web Worker
后才有可能在浏览器、Node 中实现多线程操作,目前大部分打包工具未必有使用 Web Worker 提供的多线程能力。而 GO 则没这方面的“缺陷”,更不用说还有成熟的协程特性。
但是,虽然Esbuild
快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些重要功能仍然还在持续开发中——特别是代码分割和 CSS 处理方面(ESM 小节提到的加载性能)。就目前来说,Rollup 在应用打包方面更加成熟和灵活。所以,我们一般在开发时,使用Esbuild
进行构建,而在生产环境,则是使用 Rollup 进行打包。
HMR 热更新
Webpack ——重新编译,请求变更后模块的代码,客户端重新加载。
Vite ——请求变更的模块,再重新加载。
Vite 通过chokidar
监听文件系统的变更,使相关模块与其临近的 HMR 边界连接失效,只对发生变更的模块重新加载,这样 HMR 更新速度就不会因为应用体积的增加而变慢而 Webpack 还要经历一次打包构建。所以 HMR 场景下,Vite 表现也要好于 Webpack。
关于构建,需要注意的是,如果使用传统 方式引入 Vue 的话,那么就不会涉及到构建步骤,但同时将无法使用单文件组件 (SFC) 语法。传统方式一般用在对现有项目进行局部 Vue 改造的场景下。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.e1idc.net