Vite 构建工具常用知识点总结

2022
06/09

1. 何为vite

法语意为 “快速的”,发音 /vit/,同 “veet”。

是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

2. 为什么选Vite

1)性能瓶颈

构建越来越大型的应用时,需要处理的 js 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 js 开发的工具就会开始遇到性能瓶颈。通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。

2)开发环境基于打包器的方式已经落伍

基于打包器的方式启动必须优先抓取并构建你的整个应用,然后才能提供服务。

基于打包器启动时,重建整个包的效率很低。原因显而易见:因为这样更新速度会随着应用体积增长而直线下降。

3)浏览器 ESM 支持

浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。

在开发环境下,Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

详见文档

3. Vite 与 webpack 主要差异

Webpack Vite
先打包生成bundle,再启动开发服务器 先启动开发服务器,利用新一代浏览器的ESM能力,无需打包,直接请求所需模块并实时编译
热更新(HMR)时需要把改动模块及相关依赖全部编译 热更新时只需让浏览器重新请求该模块,同时利用浏览器的缓存(源码模块协商缓存,依赖模块强缓存)来优化请求

4.Vite 开发环境构建性能高原因

Webpack 从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,这可满满都是 CPU、IO 操作啊,在 Node 运行时下性能必然是有问题。而热更新(HMR)时也很慢,即使只有很小的改动,Webpack依然需要构建完整的模块依赖图,并根据依赖图来进行转换。

Vite 开发环境编译快:

  1. 开发环境冷启动无需打包,只是使用esbuild对依赖进行预构建,将CommonJS和UMD发布的依赖转换为浏览器支持的ESM
  2. 无需分析模块之间的依赖,无需在启动开发服务器前进行编译
  3. 在一开始将应用中的模块区分为 依赖 和 源码 两类,源码模块协商缓存,依赖模块强缓存(依赖大多为开源的不会变动的node_modules模块,请求路径满足 /^\/@modules\// 格式就会被认为是依赖)

Vite 热更新更快:

  1. 利用了ESM和浏览器缓存技术,构建后的依赖请求(http头的max-age=31536000,immutable)进行强缓存,以提高页面性能。
  2. 更新速度与项目复杂度无关。

5. 预构建原理

Vite 对 js/ts 的处理没有使用如 glup, rollup 等传统打包工具,而是使用了 esbuildesbuild 是一个全新的js打包工具,底层使用了go,大量使用了并行操作,可以充分利用CPU资源。esbuild支持如babel, 压缩等的功能。esbuildrollup等工具快十几倍。

预构建:

当你首次启动 vite 时,使用esbuild在启动开发服务器前把检测到的依赖进行预构建。

  1. 将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。
  2. 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能(预先把一个模块用到的所有内部分支模块全部打包成一个bundle),这样就浏览器在请求某个模块时,便只需要发送一次请求了。
  3. 对部分文件如 tsx 进行类型解析,转换为js

当开发服务器启动后,除非遇到一个新的依赖关系导入,而这个依赖关系还没有在缓存中,Vite 将重新运行依赖构建进程并重新加载页面。

Vite 的基本实现原理:

当浏览器解析 import HelloWorld from './components/HelloWorld.vue' 时,会向当前域名发送一个请求获取对应的资源(ESM支持解析相对路径)。

 

就是启动一个 koa 服务器拦截由浏览器请求 ESM的请求。通过请求的路径找到目录下对应的文件做一定的处理最终以 ESM的格式(响应类型为js)返回给客户端。

客户端注入本质上是创建一个script标签(type=’module’),然后将其插入到head中,这样客户端在解析html是就可以执行代码了。

浏览器下载对应的文件,然后解析成模块记录。接下来会进行实例化,为模块分配内存,然后按照导入、导出语句建立模块和内存的映射关系。最后,运行上述代码,把内存空间填充为真实的值。

静态资源加载

当请求的路径符合 image, media, fonts 或 JSON 格式,会被认为是一个静态资源。静态资源将处理成ESM模块返回。

vue文件

解析vue文件是实时的,是实时编译的。不会预先编译。需要配置@vitejs/plugin-vue插件。编译时使用Vue中的@vue/compiler-sfc将Vue文件编译为AST语法树,抽离其中的 template, css, script。使用@vue/compiler-dom处理template模板转为js。

当 Vite 遇到一个 .vue 后缀的文件时。由于 .vue 模板文件的特殊性,它被拆分成 template, css, script 模块三个模块进行分别处理。最后会对 script, template, css 发送多个请求获取。

如上图中请求 App.vue 获取 script 代码,App.vue?type=template 获取 模板;App.vue?type=style 获取样式。这些代码都被插入在 App.vue 返回的代码中。

js/ts处理

Vite使用esbuild将ts转译到js,约是tsc速度的20~30倍,同时HMR更新反应到浏览器的时间会小于50ms。但是,由于esbuild转换ts到js对于类型操作仅仅是擦除,所以完全保证不了类型正确,因此需要额外校验类型,比如使用tsc –noEmit。

将ts转换成js后,浏览器便可以利用ESM直接拿到js资源。

6. 热更新原理

在客户端与服务端建立了一个 websocket 连接,当代码被修改时,服务端发送消息通知客户端去请求修改模块的代码,完成热更新。

Vite 中客户端的 websocket 相关代码在处理 html 时被写入代码中。Vite 会接受到来自服务端的消息。通过不同的消息触发一些事件。做到浏览器端的即时热模块更换(热更新)。

7. 为什么生产环境仍需打包

尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。

对比在开发环境Vite使用esbuild来构建依赖,生产环境Vite则使用了更加成熟的Rollup来完成整个打包过程。因为esbuild虽然快,但针对应用级别的代码分割、CSS处理仍然不够稳定,同时也未能兼容一些未提供ESM的SDK。

为了在生产环境中获得最佳的加载性能,仍然需要对代码进行tree-shaking、懒加载以及chunk分割(以获得更好的缓存)。

8. 构建工具和打包工具的区别?

构建过程应该包括 预编译、语法检查、词法检查、依赖处理、文件合并、文件压缩、单元测试、版本管理等 。

打包工具更注重打包这一过程,主要包括依赖管理和版本管理。

9. Vite有什么缺点?

  1. 目前 Vite 还是使用的 es module 模块不能直接使用生产环境(兼容性问题,如果你的项目不需要兼容 IE11 等低版本的浏览器,自然是可以使用的)
  2. 生产环境使用 rollup 打包可能会造成开发环境与生产环境的不一致。
  3. 很多 第三方 sdk 没有产出 ESM格式的的代码,这个需要自己去做一些兼容。目前支持 CommonJS(CJS)代码快速转化为 ESM,但是对于一些格式不规范的代码,可能还是需要单独处理。

10. 动态导入多个组件

使用import.meta.glob方法:

// 1.上面的方法相当于一次性加载了 views 目录下的所有.vue文件,返回一个对象
const modules = import.meta.glob('../views/*/*.vue');
const modules ={
    "../views/about/index.vue": () => import("./src/views/about/index.vue")
}
// 2.动态导入的时候直接,引用
const router = createRouter({
  history: createWebHistory(),
  routes: [
    // ...
    {
      path: 'xxxx',
      name: 'xxxxx',
      // 原来的方式,这个在开发中可行,但是生产中不行
      // component: () => import(`../views${menu.file}`),
      // 改成下面这样
      component: modules[`../views${filename}`]
    }
    // ...          
  ],
})

其他基础可看文档…

教程 | 配置 | 插件

React 基础与核心总结(理论篇)

2022
06/09

1. 如果你是leader,做管理系统项⽬ Vue和React 怎么选择?

  • 评估项⽬成员的⽔平,如果成员js基础较好、编码能⼒较强则选择React,否则Vue。
  • 评估系统的⼤⼩,如果想构建⽣态系统,则选择React,如果要求快速,则选择Vue。当然现在Vue3.x在对Vue2的性能优化之后,开发大型复杂的项目的性能也更好了。
  • 评估系统运⾏环境,如果你想要⼀个同时适⽤于Web端和原⽣APP的框架,请选择React(RN)。当然现在Vue也有相应的原生框架weex。

技术没有哪个更好或者是更优秀,只有适合的才是最合适的。

另外关于大型项目,能否做大型项目关键在于项目组的业务划分、部门之间的协调效率上,因为大型项目不是一两个人,三五个人能够完成的。一个项目之所以称为大项目是在于它是公司大量部门协同合作下的产物。也就是说,解决了项目划分等问题,使用vue和react都是可以的。

2. Vue和React区别

两个语言有共同点和区别。由于是两种不同的语言,区别肯定很多,这里只是列举一些比较重要的点

共同点:

  • 都是 JavaScript 的 UI 框架
  • 都是优秀的 MVVM 前端框架(对MVC和MVVM的理解),但react严格上是只针对mvc的view层,vue则是完全的mvvm模式。
  • 不同于早期的 JavaScript 框架“功能齐全”,Reat 与 Vue 只有框架的骨架,其他的功能如路由、状态管理等都是框架附属的模块。
  • 都支持数据驱动视图,不操作真实dom就能自动渲染页面。
  • 都支持native源生的方案,react的RN(reactNative)和vue的weex
  • 都支持服务端渲染
  • 都支持组件化开发和虚拟dom(virtual Dom)
  • 都支持props进行父子组件间数据通信
  • 都比较容易上手,但VUE 相较于 React 更容易上手,主要是React的JSX语法和我们平时的习惯不同

区别:

1)数据绑定方式不同

vue实现了数据的双向绑定,react可以说是单向绑定。但两者都实现了单向数据流

2)组件写法及模板语法不同

react是jsx(html,css写进js),vue是html,css,js写在同一个文件里。Vue 使⽤的是web 开发者更熟悉的模板与特性,Vue的API跟传统web模板契合度更⾼,⽐如Vue的单⽂件组件是以模板+JavaScript+CSS的组合模式,对应用的升级更方便、更容易,可以更轻松地使用和修改现有应用程序。

3)数据更新方式不同(也就是反应式系统不同),这是最大的区别

Vue提供反应式的数据,当数据改动时,界⾯就会⾃动更新。react需要setState方法对数据进行更新,这一点比Vue 数据更加可控。

React 整体是函数式的思想,在 React 中是单向数据流,推崇数据不可变的思想

Vue 的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立 Watcher 来监听,当属性变化的时候,响应式的更新对应的虚拟 DOM。

所以,React 的性能优化需要手动去做,而Vue的性能优化是自动的,但是Vue的响应式机制也有问题,就是当 state 特别多的时候,Watcher 会很多。

4)DOM更新策略不同(diff算法)

react 会自顶向下全diff;Vue 会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。

在react中,当状态发生改变时,组件树就会自顶向下的全diff, 重新render页面, 重新生成新的虚拟dom tree, 新旧dom tree进行比较, 进行patch打补丁方式,局部跟新dom. 所以react为了避免父组件跟新而引起不必要的子组件更新, 可以在shouldComponentUpdate做逻辑判断,减少没必要的render, 以及重新生成虚拟dom,做差量对比过程.

在 vue中, 通过Object.defineProperty 把这些 data 属性 全部转为 getter/setter。同时watcher实例对象会在组件渲染时,将属性记录为dep, 当dep 项中的 setter被调用时,通知watch重新计算,使得关联组件更新。(Vue3.x的更新机制优化

Diff 算法借助元素的 Key 判断元素是新增、删除、修改,从而减少不必要的元素重渲染。

5)React 内部封装较少,Vue封装内容较多

react 本身做的事情很少,很多都得靠自己进行封装。这是由于React是函数式思想,组件写在函数中。但几乎所有的功能逻辑都需要自己手动编写,这无疑提升了项目成本。

而vue很多东西都是内置的,写起来方便一些。比如一些封装好的指令、依赖注入等。

但这也说明react编写自由度更高。

6)React更新机制需要更多人为操心

React只要调用setState就会触发render重新渲染,甚至视图什么数据都没使用,它不关心也不知道是哪个数据发生了改变。当组件或节点比较多的时候,更新数据可能会造成很多不必要的虚拟DOM的构建,而react-hooks的使用也会带来一些心智负担

Vue的更新无需人为操心,Vue的发布-订阅机制能够明确知道哪个值发生了改变,然后只重新渲染该部分即可。

Vue3.x重点知识总结—基础篇(一)

2022
05/09

1. Vue3.0升级了哪些重要的功能?

  • createApp:创建vue实例的⽅式
  • emits属性,组件中的事件要先使⽤emits进⾏声明,然后在setup的形参引⼊
  • ⽣命周期
  • 多事件
  • fragment:不再限制唯⼀根节点
  • 移除.sync
  • 异步组件的写法:Vue2 直接import进来,Vue3需要使⽤defineAsyncComponent包裹⼀层
  • 移除filter
  • 新的组件:Fragment(⽚段)/Teleport(瞬移)/Suspense(不确定)
  • teleport:把组件直接to到某个dom
  • suspense:fallback,就是⼀个具名插槽
  • composition API

2. Vue3.0 生命周期及其与Vue2.x对比

3. setup中如何获取组件实例?

  • setup和其他Composition API中都没有this
  • 在Options API中仍然可以使⽤this
  • Composition API中可以使⽤getCurrentInstance()⽅法获取

4. Vite是什么?

5. Vue3插槽使用,具名插槽和匿名插槽使用,及与Vue2区别

Vue3插槽详见文档:https://cn.vuejs.org/guide/components/slots.html

与Vue2区别:https://code.zuifengyun.com/2022/09/2808.html

Vue3.x重点知识总结—基础篇(响应式API的理解)

2022
02/09

1. reactive 对比 ref

1)从定义数据角度对比

ref用来定义:基本类型数据。

reactive用来定义:对象(或数组)类型数据。

备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象。

2)从原理角度对比

ref通过Object.defineProperty()getset来实现响应式(数据劫持)。

reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。

3)从使用角度对比

ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value

reactive定义的数据:操作数据与读取数据:均不需要.value

2. 如何理解 ref、toRef 和 toRefs ?

1)ref

  • 生成值类型的响应式数据
  • 可用于模板和reactive()
  • 通过value属性修改值

2)toRef

  • 将一个响应式对象(reactive)的某个属性转为响应式数据(ref
  • 创建一个ref,具有响应式
  • 两者保持引用关系,修改任意一方会影响另一方

3)toRefs

  • 将一个响应式对象转换为一个普通对象,但这个普通对象的每个属性都是指向源对象相应属性的响应式数据ref
  • 每个单独的 ref 都是使用 toRef() 创建的
  • 两者保持引用关系,修改任意一方会影响另一方
  • toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef

4)最佳使用方式

  • reactive做对象的响应式,用ref做值响应式
  • setup中返回toRefs(state)或者toRef(state,'prop')
  • ref变量命名可以用xxxRef以区分
  • 当从组合式函数中返回响应式对象时,toRefs 相当有用。使用它,消费者组件可以解构/展开返回的对象而不会失去响应性

5)为何需要ref

  • 返回值类型数据时,会丢失响应式
  • setupcomputedhooks都有可能返回值类型的数据
  • 如果让用户自行定义ref响应式数据,会变得混乱

6)为何ref要用.value属性读写值

  • 若不丢失响应式,ref必须是一个对象,而value是其属性
  • 通过value属性的getset实现响应式
  • 用于模板语法、reactive时不需要.value,其他时候都需要

7)为何需要 toRef 和toRefs

  • 初衷:不丢失响应式的前提下,将reactive对象数据进行解构或展开
  • 前提:针对的是响应式(reactive)对象,不是普通对象
  • 注意:不创造新的响应式,而是延续原来的响应式(两者保持引用关系)

3. watch 和 watchEffect 的区别是什么?

两者都可以监听响应式数据的变化。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出响应源。它会在同步执行过程中,自动追踪回调中所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

4. shallowReactive 与 shallowRef

  • shallowReactive:只处理对象最外层属性(根级别的属性)的响应式(浅响应式)。
  • shallowRef:只处理基本数据类型的响应式,不进行对象的响应式处理(不会被深层递归地转为响应式,只有对 .value 的访问是响应式的)。
const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 更改状态自身的属性是响应式的
state.foo++

// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false

// 不是响应式的
state.nested.bar++

// ----------------------------------------

const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }

什么时候使用?

  • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive
  • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef

5. readonly 与 shallowReadonly

  • readonly: 让一个响应式数据变为只读的(深只读)。
  • shallowReadonly:让一个响应式数据变为只读的(浅只读,只有根层级的属性变为了只读)。
  • 应用场景: 不希望数据被修改时。
const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 更改状态自身的属性会失败
state.foo++

// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false

// 这是可以通过的
state.nested.bar++

6. toRaw 与 markRaw

1)toRaw

作用:将一个由reactive生成的响应式对象转为普通对象。

使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。

2)markRaw

作用:标记一个对象,使其永远不会再成为响应式对象。

应用场景:

  • 有些值不应被设置为响应式的,例如复杂的第三方类库等。
  • 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

7. provide 与 inject

  • 作用:实现父组件与后代组件间通信
  • 套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据
// 父组件中:
setup(){
    // ......
    let car = reactive({name:'奔驰',price:'40万'})
    provide('car',car)
    // ......
}

// 后代组件中:
setup(props,context){
    // ......
    const car = inject('car')
    return {car}
    // ......
}

8. 什么是hook?什么是自定义hook函数?

本质是一个高阶函数,将相关业务逻辑的方法封装到函数中,以便在 setup 函数中使用。

类似于Vue2.x中的mixin

自定义 hook 的优势:便于维护和复用代码, 让 setup 中的逻辑更清楚易懂。

Vue3.x重点知识总结—理论篇(Vue3的优势)

2022
02/09

一、Vue3相比与Vue2的优势

1. 速度更快、性能更好

1) 基于Proxy的新响应式系统(数据劫持优化)

Vue3.0如何实现响应式?

由原来的Object.definePropertygettersetter,改成了ES6 Proxy 作为其观察机制(准确说是 Proxy 配合 ReflectReflect提供了一些操作Object对象的方法),初始化时无需递归遍历数据,初始化效率更高,而且也可以监控数组。速度加倍,节省了一半内存开销。

Proxy相当于在⽬标对象之前架设⼀层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了⼀种机制,可以对外界的访问进⾏过滤和改写。

Proxy 的性能本来⽐defineProperty好,Proxy可以拦截属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性。另外有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下⼀级的属性。

  • 可以劫持整个对象
  • 可以监听对象动态属性的添加
  • 可以监听到数组的变化,包括数组的索引和数组length属性
  • 可以监听删除属性
  • 操作时不是对原对象操作,new Proxy 返回⼀个新对象(它不直接操作对象,而是代理模式,通过对象的代理对象进行操作)。

Vue2为啥不使用Proxy?答案是由于浏览器兼容性问题。目前来看,除不支持IE11及以下版本,其余浏览器均已支持。

2) 虚拟DOM重写

虚拟 DOM 重写,提供更多的编译时提示来减少运行时开销。使用更有效的代码来创建虚拟节点。

虚拟DOM静态属性缓存,避免重复patch。内存换时间。

3) Diff算法优化

算法改写,更加细粒度,效率更高。

增加了静态标记flag。标记和提升所有静态根节点,diff 的时候只⽐较动态节点内容。

Patch flag: 在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,⽽不⽤逐个逐层遍历,提⾼了虚拟dom diff的性能。

将自定义组件和普通标签进行区分、将静态节点和动态节点进行区分。

4) 静态提升(hoistStatic),靶向更新,提升VNode更新性能

静态提升、靶向更新等优化点,来提高 patchVNode (diff算法递归打补丁)性能。

静态提升:Vue2中无论元素是否参与更新,每次都会重新创建(createVNode),然后再渲染(Vue2 中的虚拟dom节点是进⾏全量的更新)。但是在Vue3中使用了静态提升后,对于静态不需要发生变化的元素,只会被创建一次,静态节点都被提升到 render ⽅法之外,在渲染时直接复用即可。(静态提升避免了静态元素节点频繁重复创建

靶向更新:Vue3将 compileruntime 结合(编译和运行相结合),对动态节点(Block VNode )进行精准标记。靶向更新的本质是为了从一颗存在动态、静态节点的 VNode Tree 中筛选出动态的节点形成 Block Tree,即 dynamicChildren,然后在 patch 时实现精准、快速的更新。(Patch flag,跳过静态节点,直接对⽐动态节点)

5) SSR渲染优化

当有大量静态内容时,这些内容会被当成纯字符串推进一个buffer里面,即使存在动态的绑定,会通过插值嵌入进去。这样会比通过虚拟dom来渲染快很多。当静态内容大到一定量级的时候,会用_createStaticVNode方法在在客户端去生成一个static node这些静态node会被直接innerHTML就不需要创建对象,然后直接根据对象渲染。

6) 事件侦听器缓存(cacheHandles)

Vue3中的事件绑定的函数会被缓存起来,再次复用时就会提升性能。在patch过程中不会重新生成新的函数。

缓存事件处理函数cacheHandler,避免每次触发更新都要重新⽣成全新的function去更新之前的函数。

2. 体积更小

vue3整个源码体积相对减少,移出了一些不常用的Api,例如:inline-templatefilter.sync$set等。

优化了打包方法,引入tree-shaking,按需编译,避免打包无用模块(例如只打包用到的ref,reactive,components等),使得打包后的bundle的体积更小,提升了运行效率。(通过摇树优化核⼼库体积,减少不必要的代码量)

另外,Composition APItree-shaking 友好,代码也更容易压缩。(Tree shaking 详述

3. 更容易维护

语法API方面(compositon Api):

  • 支持组合API(compositon Api),可与现有的Options API一起使用
  • 灵活的逻辑组织逻辑复用
  • Vue3模块可以和其他框架搭配使用

compositon Api 可以解决业务分离问题,使代码有更好的复用性。同时, 也方便后续的维护和管理setup() 的出现,使得相关的业务代码得以集中起来,方便查找和维护。我们可以把不同的业务代码进行逻辑抽离,比如使用hooks形式,更容易维护。而Vue2不同的业务代码都混杂在options中,不便管理。

4. 更接近原生

compositon Api 使得 Vue 3 逻辑代码编写更趋向于原生。

可以自定义渲染 API:createRenderer函数可以自定义渲染逻辑。我们可以自定义渲染的逻辑,甚至还可以渲染到canvaswebview等其他指定的平台。

5. 更易使用

响应式 Api 和 Hooks 直接暴露出来,哪里需要直接引用。比如 ref()toRefs()watch()等和生命周期钩子onMounted()onUpdated()onUnmounted()onBeforeMount()等。

6. 更好的TS支持

基于typescipt编写,可以享受到类型提示。对Ts支持更好。

编辑器可提供强有力的类型检查和错误提示,方便更好得调试。

二、Vue3相较于Vue2解决了哪些问题?设计目标是什么?

1. Vue3设计的初衷有两点(尤大)

  1. 利用新的语言特性(es6)
  2. 解决Vue3的一些设计和架构的问题

2. Vue2长期以来存在的一些问题

  • 随着功能的增长,复杂组件的代码变得越来越难以维护
  • 数据量大后带来的渲染和更新性能问题
  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制
  • 类型推断不够友好
  • bundle的时间太久了

3. Vue3的设计目标

  • 更小
  • 更快
  • TypeScript支持
  • API设计一致性
  • 提高自身可维护性
  • 开放更多底层功能

三、Vue3编译方面有哪些优势

Vue3更快主要体现在编译方面:

四、Vue3做了哪些优化方案

vue3从很多层面都做了优化,可以分成三个方面:

1. 源码

源码可以从两个层面展开:

1)源码管理

更好的代码管理方式: monorepo(简单的说monorepo模式就是把你组内的所有项目管理在一个git仓库中)。

monorepo将不同的模块拆分到packages目录下不同子目录中,每个package有各自的API、类型定义和测试,这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。每个魔抗可独立于Vue使用,减少引用包的体积大小。

2)TypeScript

  • 编码期间可以进行类型检查,避免类型问题的错误
  • 有利于定义接口类型,利于IDE对变量类型的推导
  • Composition API 几乎是函数,也会有更好的类型推断

2. 性能

vue3是从什么哪些方面对性能进行进一步优化呢?

3. 语法API(Composition API 的出现带来哪些新的开发体验,为啥需要这个? )

这里当然说的就是Composition API,Vue3为什么要使用Composition API取代Options API呢?

如何看待Composition APIOptions API

通常使用Vue2开发的项目,普遍会存在以下问题:

  • 代码的可读性随着组件变大而变差,组件逻辑难以阅读和理解
  • 每一种代码复用的方式,都存在缺点
  • TypeScript支持有限

以上通过使用Composition Api都能迎刃而解,其两大显著的优化:

1)优化逻辑组织

Options API

包含⼀个描述组件选项(datamethodsprops等)的对象 options;开发时,同⼀个功能逻辑的代码会被拆分到不同选项中。

假设一个组件是一个大型组件,其内部有很多处理逻辑关注点,这种碎片化使得理解维护复杂组件变得困难。

选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块(比如在methodscomputed以及一些生命周期选项中反复横跳)。

⽤mixin重⽤公⽤代码,也有问题:命名冲突数据来源不清晰。(options选项不好拆分和重用)

Compostion API

基于函数的 api,可以更灵活的组织组件的逻辑。(一般为纯函数,没有副作用)

Composition ApiReact Hooks有异曲同工之妙,是为了解决复杂业务逻辑而设计的。

将某个逻辑关注点相关的代码全都放在一个函数里(更加的高内聚,低耦合),这样当需要修改一个功能时,就不再需要在文件中或各个选项中来跳去。也方便使用hooks的方式做逻辑抽离。

Composition API 解决了几个问题:

  • 根据业务逻辑组织代码,提⾼可读性和可维护性
  • 解决在⽣命周期函数经常包含不相关的逻辑,但⼜不得不把相关逻辑分离到了⼏个不同⽅法中的问题。(如在mounted中设置定时器,但需要在beforeDestroy中来清除)
  • 更好的重⽤逻辑代码,在Options API中通过MIxins重⽤逻辑代码,容易发⽣命名冲突数据来源不清晰
  • 很少使用到 this ,减少了 this 指向不明的情况
  • Composition API 都是函数,有更好的类型推导
  • 模块化更加解耦,可以单独引用某个模块而不用全量引入Vue对象

当然,Options API并没有被废弃,与Composition API可以共存,但并不推荐这样做(可能会引起混乱)。

小型项目,业务逻辑简单的可以使用Options API,复杂的大型项目还是使用Composition API更好。

2)优化逻辑的复用:mixins

使用单个mixins没太大问题。但当组件中存在大量多个的mixins后,会存在命名冲突数据来源不清晰问题,组合式API解决了这两个问题。即使去编写更多的 hook 函数,也不会出现这两个问题。

五、Vue3里为什么要用 Proxy 替代 defineProperty ?

Object.defineProperty() 只能遍历对象属性进行劫持,Proxy 直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的,且可以解决 defineProperty 存在的问题。且提升性能。

Object.defineProperty()

此方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。文档

为什么能实现响应式:

通过 defineProperty 两个属性,getset

get:属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

set:属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined

存在弊端:

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

Proxy 对象

对对象的所有操作进行监听。文档

这里需要对 ES6 对应的 Proxy 新特性进行复盘。

六、说说Vue 3.0中Treeshaking特性?举例说明一下?

1. 是什么

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术。

专业术语叫 Dead code elimination(死码消除)。简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码。

在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。

而Vue3源码引入tree shaking 特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中。

2. 原理

Tree shaking 是基于ES6模板语法(importexports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。

Tree shaking 无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载
  • 判断哪些模块和变量未被使用,进而删除对应代码

3. 作用

通过 Tree shaking,Vue3 给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

七、都说 Composition API 和 React Hook 很像,请问他们的区别是什么?

React Hook 从实现的角度来看,React 是通过链表去实现 hooks 的调用的。需要确保每次更新时 hooks 的调用顺序一致,这让 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确。

所以有以下几个限制:

  • 不在循环中、条件语句中、函数嵌套中调用 Hook
  • 你必须确保它总是在 React Top level 调用函数 Hook
  • 使用效果、依赖关系必须手动确定

Composition API 是基于 Vue 的响应系统,和React Hook 相比:

  • setup() 函数中,一个组件实例只执行一次,而React Hook 每次重新渲染时,都需要调用 Hook,给 React 带来的 GC 比 Vue 更大的压力,性能也相对 Vue 来说比较慢
  • Compositon API 不必担心调用的顺序,它也可以在循环中、条件、在嵌套函数中任意位置使用
  • 响应式系统自动实现依赖关系收集,而且组件的性能优化是由 Vue 内部完成的,而 React Hook 的依赖关系需要手动传递,并且依赖关系的顺序必须得到保证,尤其是使用 useEffectuseMemo 等 Hook 时,否则组件性能会因为依赖关系不正确而下降。
  • reactive + ref 属于响应式数据,⽐ react 的useState,要更难理解

虽然Compoliton API区别于React Hook,但它的设计思路也是来自React Hook的参考。

八、vue3.0有哪些改进(总结)

  1. 采用TS来编写,编辑器提供强有力的类型检查和错误提示,方便更好得调试。
  2. 支持composition API。
  3. 基于Proxy的响应式系统(由原来的Object.defineProperty的getter 和setter,改变成为了ES2015 Proxy 作为其观察机制),无需递归遍历数据,初始化效率更高,而且也可以监控数组。速度加倍,节省一半内存开销。但兼容性较差,不支持IE11。
  4. 虚拟 DOM 重写,提供更多的编译时提示来减少运行时开销。使用更有效的代码来创建虚拟节点。
  5. v-dom对比算法(diff算法)改写,更加细粒度,效率更高,比如将自定义组件和普通标签进行区分。只更新vdom绑定了动态数据的部分。
  6. v-dom静态属性缓存,避免重复patch。内存换时间。
  7. 虚拟dom编译优化,在compile阶段使用有 transform 对 AST (抽象语法树)做诸多层面的转化,使「Vue」 diff 过程更快。相比之下,「Vue2.x」的编译阶段没有完整的 transform,只是 optimize 优化了一下 AST。
  8. 静态提升、靶向更新等优化点,来提高 patchVNode (diff算法递归打补丁)过程。
  9. Composition API (结构化API)方便逻辑复用,代替原先options API(配置化)。
  10. 模块化更加解耦,可以单独引用某个模块而不用全部引入vue
  11. 2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用,而3.x版本中v-if优先级更高。
  12. 3.x版本使用:ref 绑定函数的方式取代$refs方式获取dom
  13. 组件的v-model重构。具体见文档。
  14. 总之就是:1. 更快 2. 更小 3. 更容易维护 4. 更加友好 5. 更容易使用
  15. vue3性能比vue2好的原因?1.diff算法优化;2.静态提升hoistStatic;3.事件侦听器缓存 cacheHandles

React 基础及核心总结/三

2022
25/08

1. 原生属性的改写和差异

在 React 中,所有的 DOM 特性和属性(包括事件处理)都应该是小驼峰命名的方式。例如,与 HTML 中的 tabindex 属性对应的 React 的属性是 tabIndex,再比如readOnly和。例外的情况是 aria-* 和 data-* 属性以及自定义的属性,一律使用小写字母命名。比如, 可以用 aria-label 作为 aria-label

1)checked 和 defaultChecked

当 <input> 组件的 type 类型为 checkbox 或 radio 时,组件支持 checked 属性,这个属性在原生的DOM中也是有的。你可以使用它来设置组件是否被选中。这对于构建受控组件(controlled components)很有帮助。而 defaultChecked 则是非受控组件的的新增属性,用于设置组件首次挂载时是否被选中。

2)className

className 属性用于指定 CSS 的 class,此特性适用于所有常规 DOM 节点和 SVG 元素,如 <div>,<a> 及其它标签。

如果你在 React 中使用 Web Components(这是一种不常见的使用方式),请使用 class 属性代替。

3)dangerouslySetInnerHTML

innerHTML 的替换方案,类似于Vue中的v-html指令。通常来讲,使用代码直接设置 HTML 存在风险,因为很容易无意中使用户暴露于跨站脚本(XSS)的攻击。因此,你可以直接在 React 中设置 HTML,但当你想设置 dangerouslySetInnerHTML 时,需要向其传递包含 key 为 __html 的对象。

const root = ReactDOM.createRoot(document.getElementById('root'));

function createMarkup() {
  return {__html: '<h1 style="color:red" class="h">我是标题</h1>'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}

root.render(<MyComponent/>);

4)htmlFor

由于 for 在 JavaScript 中是保留字,所以 React 元素中使用了 htmlFor 来代替。

<label htmlFor="namedInput">Name:</label>
<input id="namedInput" type="text" name="name"/>

5)onChange

onChange 事件与预期行为一致:每当表单字段变化时,该事件都会被触发。我们故意没有使用浏览器已有的默认行为,是因为 onChange 在浏览器中的行为和名称不对应,并且 React 依靠了该事件实时处理用户输入。

6)selected

如果要将 <option> 标记为已选中状态,请在 select 标签的 value 中引用该选项的值,而非使用selected,这与原生Html完全不同。总的来说,这使得 <input type="text">, <textarea><select> 之类的标签都非常相似—它们都接受一个 value 属性,你可以使用它来实现受控组件。

你可以将数组传递到 value 属性中,以支持在 select 标签中选择多个选项:

<select multiple={true} value={['B', 'C']}>

7)style

不推荐将 style 属性作为直接设置元素样式的方式。在多数情况下,应使用 className 搭配外部 CSS 样式表中定义的 class。style 在 React中多用于添加动态计算的样式。

添加动态计算样式时,style 接受一个采用小驼峰命名属性的 JavaScript 对象(部分浏览器引擎前缀为大驼峰),而不是 CSS 字符串。这与 DOM 中 style 的 JavaScript 属性是一致的。

const divStyle = {
  color: 'blue',
  backgroundImage: 'url(' + imgUrl + ')',
  WebkitTransition: 'all', // note the capital 'W' here
  msTransition: 'all' // 'ms' is the only lowercase vendor prefix
};

function HelloWorldComponent() {
  return <div style={divStyle}>Hello World!</div>;
}

React 会自动添加 ”px” 后缀到内联样式为数字的属性后。如需使用 ”px” 以外的单位,请将此值设为数字与所需单位组成的字符串。

// Result style: '10px'
<div style={{ height: 10 }}>
  Hello World!
</div>

// Result style: '10%'
<div style={{ height: '10%' }}>
  Hello World!
</div>

8)value 和 defaultValue

<input><select> 和 <textarea> 组件支持 value 属性。你可以使用它为组件设置 value。这对于构建受控组件是非常有帮助。defaultValue 属性对应的是非受控组件的属性,用于设置组件第一次挂载时的 value。

2. 生命周期(常用)

1)constructor()

在 React 组件挂载之前,会调用它的构造函数。在为 React.Component 子类实现构造函数时,应在其他语句之前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。

通常,在 React 中,构造函数仅用于以下两种情况:

在 constructor() 函数中不要调用 setState() 方法。如果你的组件需要使用内部 state,请直接在构造函数中为 this.state 赋值初始 state

ps:函数式组件不存在构造函数。函数本身就是构造函数。

2)componentDidMount()

在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。如需通过网络请求获取数据,此处是实例化请求的好地方。

3)componentDidUpdate()

在更新后会被立即调用。首次渲染不会执行此方法。

当组件更新后,可以在此处对 DOM 进行操作。如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。

在这里调用 setState()时必须被包裹在一个条件语句里,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。

4)componentWillUnmount()

会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。

componentWillUnmount() 中不应调用 setState(),因为该组件在此之后将不会重新渲染。

5)static getDerivedStateFromError(error)

此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state。

一般用于构建一个用于外层嵌套的自定义组件,以便在子组件出现错误时降级显示此UI。

ps:getDerivedStateFromError() 会在渲染阶段调用,因此不允许出现副作用(这里只能是纯函数,不允许修改其余变量)。 如遇此类情况,请改用 componentDidCatch()

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以显降级 UI
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的降级  UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

6)componentDidCatch(error, info)

此生命周期在后代组件抛出错误后被调用。 它接收两个参数:

  1. error —— 抛出的错误。
  2. info —— 带有 componentStack key 的对象,其中包含有关组件引发错误的栈信息。

ps:componentDidCatch() 会在“提交”阶段被调用,因此允许执行副作用。 它应该用于记录错误之类的情况:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以显示降级 UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    // "组件堆栈" 例子:
    //   in ComponentThatThrows (created by App)
    //   in ErrorBoundary (created by App)
    //   in div (created by App)
    //   in App
    logComponentStackToMyService(info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定义的降级 UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

7)函数式组件中,useEffect() 可以取代一部分生命周期

可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。具体如下:

每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。

与 componentDidMount 或 componentDidUpdate 不同,React 会等待浏览器完成画面渲染之后才会延迟调用 useEffect,因此会使得额外操作很方便。使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。

你也可以使用多个 effect。这会将不相关逻辑分离到不同的 effect 中。但你不可将effect hook放到条件判断中(所有的hooks都不可以)。

effect 只要传递数组作为 useEffect 的第二个可选参数(deps依赖数组)即可限制每次渲染都执行,只会在传入的依赖变化时才执行。需要重点注意的是:请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量,否则你的代码会引用到先前渲染中的旧变量。

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

在每次渲染后都执行,类似于componentDidUpdate

useEffect(() => {
    document.title = `You clicked ${count} times`;
});

如果 effect 返回一个函数,React 将在执行组件卸载操作时调用它,类似于componentWillUnmount

useEffect(() => { 
    document.title = `You clicked ${count} times`;
    return ()=>{
        document.title = '';
    }
});

如果你的 effect 第二个参数传入一个空数组,则只会在组件挂载后(插入 DOM 树中)调用。类似于componentDidMount

useEffect(() => { 
    document.title = `You clicked ${count} times`; 
},[]);

3. 数据监听

useEffect 实现数据监听的效果,类似于其他框架中的watch

const [count,setCount] = useState(0)
useEffect(() => { 
    console.log('我是改变后的count',count)
}, [count]); 

React 基础及核心总结/二(组件篇)

2022
24/08

1. 内置组件

除HTML原生标签外,React还有一些内置的组件。

1)<React.StrictMode>

严格模式。StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。

Ps:严格模式检查仅在开发模式下运行;它们不会影响生产构建

你可以为应用程序的任何部分启用严格模式。

2)<React.Fragment>

是一个类似于 Vue 中的 <template> 一样的空元素,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性。还有一种缩写语法<>something...</>,短语法无法传参。

Fragment 能够在不额外创建 DOM 元素的情况下,让 render() 方法中返回多个元素。

Ps:key 是唯一可以传递给 Fragment 的属性。未来我们可能会添加对其他属性的支持,例如事件。

import React, { Fragment } from 'react';

function ListItem({ item }) {
  return (
    <Fragment>
      <dt>{item.term}</dt>
      <dd>{item.description}</dd>
    </Fragment>
  );
}

function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        <ListItem item={item} key={item.id} />
      ))}
    </dl>
  );
}

// 或者
function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        // Fragments should also have a `key` prop when mapping collections
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

3)<React.Suspense>

可以指定加载Loading指示组件,以防其组件树中的某些子组件尚未具备渲染条件。也是唯一一个可支持Loading指示的组件。

React.lazy()引入的动态组件是其支持的唯一用例。也就是说,渲染 lazy 组件依赖该组件渲染树上层的 <React.Suspense> 组件。

lazy 组件可以位于 Suspense 组件树的深处——它不必包装树中的每一个延迟加载组件。最佳实践是将 <Suspense> 置于你想展示加载指示器(loading indicator)的位置,而 lazy() 则可被放置于任何你想要做代码分割的地方。

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

const Spinner = ()=>(<div>Loading...</div>)

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}

4)<Profiler>

测量一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分。

这个标签一般很少用。文档:https://zh-hans.reactjs.org/docs/profiler.html

这个元素在生产环境是被禁用的,因为比较吃性能。

2. 组件包含关系

组件之间的包含关系作用类似于Vue/Angular中的插槽(slot)。但这种包含关系相比于插槽更加容易理解。 React 元素本质就是对象(object),所以你可以把它们当作 props,像其他数据一样传递。在 React 中没有“插槽”这一概念的限制,你可以将任何东西作为 props进行传递。

注意:React 组件无继承。如果你想要在组件间复用功能,我们建议将其提取为一个单独的 JavaScript 模块,如函数、对象或者类。组件可以直接引入(import)而无需通过 extend 继承它们。

1) 使用 children prop

可将子组件传递到外层组件中,这使得别的组件可以通过 JSX 嵌套,将任意组件作为子组件传递:

// 传递子组件
function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

// 外层插槽,props.children为自有属性
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

2) 不使用 children

自行约定所插入的位置:将所需内容传入 props,并使用相应的 prop。这类似于Vue的具名插槽。

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

// 直接将元素标签作为props属性进行传递
function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

3. 多组件对象导出

注意:组件名必须使用大写字母开头!

import React from 'react';

const MyComponents = {
  DatePicker: function DatePicker(props) {
    return <div>Imagine a {props.color} datepicker here.</div>;
  }
}

function BlueDatePicker() {
  return <MyComponents.DatePicker color="blue" />;
}

标签名只能使用点语法(<A.B>),不能使用其他表达式(错误语法:<A[b]>)。如果要解决动态标签问题, 需要首先使用表达式将标签名赋值给一个大写字母开头的变量:

import React from 'react';
import { PhotoStory, VideoStory } from './stories';

const components = {
  photo: PhotoStory,
  video: VideoStory
};

function Story(props) {
  // 正确!JSX 类型可以是大写字母开头的变量。
  const SpecificStory = components[props.storyType];
  return <SpecificStory story={props.story} />;
}

4. 组件通信

1)父子通信

父组件给子组件传递数据,使用属性进行传递,子组件通过props获取父组件传递过来的值。

export default function Accordion() {
  return (
      <Panel title="About" isActive={true}>
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive && (
        <p>{children}</p>
      )}
    </section>
  );
}

2)子父通信

父组件将一个方法通过属性传递给子组件,子组件调用props中父组件传来的方法并传参,父组件即可在该方法中获取子组件传递过来的值。说白了父子子父之间的通信都是依赖props。这和其他的框架有所不同,不存在自定义事件及emit触发机制。实际上也更加简单。这也说明,React的props可以传递任何类型的数据。

import { useState } from "react";

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        index={activeIndex}
        isActive={activeIndex === 0}
        onShow={(val) => () => setActiveIndex(val)}
      >
        美国佐治亚州立大学生物医学研究所研究人员开展的一项新研究显示,一种新的通用流感疫苗用在小鼠身上可预防甲型和乙型流感病毒的不同变种。日前这项研究发表在《公共科学图书馆・病原体》杂志上。
      </Panel>
      <Panel
        title="Etymology"
        index={activeIndex}
        isActive={activeIndex === 1}
        onShow={(val) => () => setActiveIndex(val)}
      >
        “改革开放是当代中国发展进步的必由之路,是实现中国梦的必由之路。”党的十八大以来以习近平同志为核心的党中央,统筹国内国际两个大局,推动许多领域实现历史性变革、系统性重塑、整体性重构,领导全党全国人民开创了改革开放新局面。
      </Panel>
    </>
  );
}

function Panel({ title, index, children, isActive, onShow }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow(index === 0 ? 1 : 0)}>Show</button>
      )}
    </section>
  );
}

3)多组件共享状态

可以使用Redux等工具进行状态管理。

也可以将状态移动到组件的公共父级,然后通过props将其传递给它们。这被称为提升状态,利用hooks(useState()useReducer())封装状态是编写 React 代码时最常见的事情之一。

5. 高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。具体而言,高阶组件是参数为组件,在不改变传入的组件前提下,返回值为新组件的函数。类似于纯函数,没有副作用。

// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
  // ...并返回另一个组件...
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.state = {
        data: selectData(DataSource, props)
      };
    }

    componentDidMount() {
      // ...负责订阅相关的操作...
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange() {
      this.setState({
        data: selectData(DataSource, this.props)
      });
    }

    render() {
      // ... 并使用新数据渲染被包装的组件!
      // 请注意,我们可能还会传递其他属性
      return <WrappedComponent data={this.state.data} {...this.props} />;
    }
  };
}

Nodejs 模块规范 CJS 与 ESM 及其衍生后缀 .cjs 和 .mjs 文件

2022
24/08

自 13.2.0 版本开始,Nodejs 在保留了 CommonJS(CJS)语法的前提下,新增了对 ES Modules(ESM)语法的支持。

天下苦 CJS 久已,Node 逐渐拥抱新标准的规划当然值得称赞,我们也会展望未来 Node 不再需要借助工具,就能打破两种模块化语法的壁垒……

但实际上,一切没有想象中的那么美好。

一、并不完美的 ESM 支持

1.1 在 Node 中使用 ESM

Node 默认只支持 CJS 语法,这意味着你书写了一个 ESM 语法的 js 文件,将无法被执行。

如果想在 Node 中使用 ESM 语法,有两种可行方式:

  • ⑴ 在 package.json 中新增 "type": "module" 配置项。
  • ⑵ 将希望使用 ESM 的文件改为 .mjs 后缀。

对于第一种方式,Node 会将和 package.json 文件同路径下的模块,全部当作 ESM 来解析。

第二种方式不需要修改 package.json,Node 会自动地把全部 xxx.mjs 文件都作为 ESM 来解析。

同理,如果在 package.json 文件中设置 "type": "commonjs",则表示该路径下模块以 CJS 形式来解析。 如果文件后缀名为 .cjs,Node 会自动地将其作为 CJS 模块来解析(即使在 package.json 中配置为 ESM 模式)。

我们可以通过上述修改 package.json 的方式,来让全部模块都以 ESM 形式执行,然后项目上的模块都统一使用 ESM 语法来书写。

如果存在较多陈旧的 CJS 模块懒得修改,也没关系,把它们全部挪到一个文件夹,在该文件夹路径下新增一个内容为 {"type": "commonjs"} 的 package.json 即可。

Node 在解析某个被引用的模块时(无论它是被 import 还是被 require),会根据被引用模块的后缀名,或对应的 package.json 配置去解析该模块。

1.2 ESM 引用 CJS 模块的问题

ESM 基本可以顺利地 import CJS 模块,但对于具名的 exports(Named exports,即被整体赋值的 module.exports),只能以 default export 的形式引入:

/** @file cjs/a.js **/

// named exports

module.exports = {

    foo: () => {

        console.log("It's a foo function...")

    }

}

/** @file index_err.js **/

import { foo } from './cjs/a.js'; 

// SyntaxError: Named export 'foo' not found. The requested module './cjs/a.js' is a CommonJS module, which may not support all module.exports as named exports.

foo();

/** @file index_err.js **/

import pkg from './cjs/a.js';  // 以 default export 的形式引入

pkg.foo();  // 正常执行

具体原因我们会在后续提及。

1.3 CJS 引用 ESM 模块的问题

假设你在开发一个供别人使用的开源项目,且使用 ESM 的形式导出模块,那么问题来了 —— 目前 CJS 的 require 函数无法直接引入 ESM 包,会报错:

let { foo } = require('./esm/b.js');

// Error [ERR_REQUIRE_ESM]: require() of ES Module BlogDemo3\220220\test2\esm\b.js from BlogDemo3\220220\test2\require.js not supported.

// Instead change the require of b.js in BlogDemo3\220220\test2\require.js to a dynamic import() which is available in all CommonJS modules.

//     at Object.<anonymous> (BlogDemo3\220220\test2\require.js:4:15) {

//   code: 'ERR_REQUIRE_ESM'

// }

按照上述错误陈述,我们不能并使用 require 引入 ES 模块(原因会在后续提及),应当改为使用 CJS 模块内置的动态 import 方法:

import('./esm/b.js').then(({ foo }) => {

    foo();

});

// or

(async () => {

    const { foo } = await import('./esm/b.js');

})();

开源项目当然不能强制要求用户改用这种形式来引入,所以又得借助 rollup 之类的工具将项目编译为 CJS 模块……


由上可见目前 Node.js 对 ESM 语法的支持是有限制的,如果不借助工具处理,这些限制可能会很糟心。

对于想入门前端的新手来说,这些麻烦的规则和限制也会让人困惑。

截至我落笔书写本文时, Node.js LTS 版本为 16.14.0,距离开始支持 ESM 的 13.2.0 版本已过去了两年多的时间。

那么为何 Node.js 到现在还无法打通 CJS 和 ESM?

答案并非 Node.js 敌视 ESM 标准从而迟迟不做优化,而是因为 —— CJS 和 ESM,二者真是太不一样了。

二、CJS 和 ESM 的不同点

2.1 不同的加载逻辑

在 CJS 模块中,require() 是一个同步接口,它会直接从磁盘(或网络)读取依赖模块并立即执行对应的脚本。

ESM 标准的模块加载器则完全不同,它读取到脚本后不会直接执行,而是会先进入编译阶段进行模块解析,检查模块上调用了 import 和 export 的地方,并顺腾摸瓜把依赖模块一个个异步、并行地下载下来。

在此阶段 ESM 加载器不会执行任何依赖模块代码,只会进行语法检错、确定模块的依赖关系、确定模块输入和输出的变量。

最后 ESM 会进入执行阶段,按顺序执行各模块脚本。

所以我们常常会说,CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

在上方 1.2 小节,我们曾提及到 ESM 中无法通过指定依赖模块属性的形式引入 CJS named exports:

/** @file cjs/a.js **/

// named exports

module.exports = {

    foo: () => {

        console.log("It's a foo function...")

    }

}

/** @file index_err.js **/

import { foo } from './cjs/a.js'; 

// SyntaxError: Named export 'foo' not found. The requested module './cjs/a.js' is a CommonJS module, which may not support all module.exports as named exports.

foo();

这是因为 ESM 获取所指定的依赖模块属性(花括号内部的属性),是需要在编译阶段进行静态分析的,而 CJS 的脚本要在执行阶段才能计算出它们的 named exports 的值,会导致 ESM 在编译阶段无法进行分析。

2.2 不同的模式

ESM 默认使用了严格模式(use strict),因此在 ES 模块中的 this 不再指向全局对象(而是 undefined),且变量在声明前无法使用。

这也是为何在浏览器中,<script> 标签如要启用原生引入 ES 模块能力,必须加上 type="module" 告知浏览器应当把它和常规 JS 区分开来处理。

查看 ESM 严格模式的更多限制:

https://es6.ruanyifeng.com/#docs/module#%E4%B8%A5%E6%A0%BC%E6%A8%A1%E5%BC%8F

2.3 ESM 支持“顶级 await”,但 CJS 不行。

ESM 支持顶级 await(top-level await),即 ES 模块中,无须在 async 函数内部就能直接使用 await

// index.mjs

const { foo } = await import('./c.js');

foo();

在 CSJ 模块中是没有这种能力的(即使使用了动态的 import 接口),这也是为何 require 无法加载 ESM 的原因之一。

试想一下,一个 CJS 模块里的 require 加载器同步地加载了一个 ES 模块,该 ES 模块里异步地 import 了一个 CJS 模块,该 CJS 模块里又同步地去加载一个 ES 模块…… 这种复杂的嵌套逻辑处理起来会变得十分棘手。

2.4 ESM 缺乏 __filename 和 __dirname

在 CJS 中,模块的执行需要用函数包起来,并指定一些常用的值:

NativeModule.wrapper = [

  '(function (exports, require, module, __filename, __dirname) { ',

  '\n});'

];

所以我们才可以在 CJS 模块里直接用 __filename__dirname

而 ESM 的标准中不包含这方面的实现,即无法在 Node 的 ESM 里使用 __filename 和 __dirname

参考:https://github.com/nodejs/node/blob/v4.0.0/src/node.js#L932


从上方几点可以看出,在 Node.js 中,如果要把默认的 CJS 切换到 ESM,会存在巨大的兼容性问题。

这也是 Node.js 目前,甚至未来很长一段时间,都难以解决的一场模块规范持久战。

如果你希望不借助工具和规则,也能放宽心地使用 ESM,可以尝试使用 Deno 替代 Node,它默认采用了 ESM 作为模块规范(当然生态没有 Node 这么完善)。

三、借助工具实现 CJS、ESM 混写

借助构建工具可以实现 CJS 模块、ES 模块的混用,甚至可以在同一个模块同时混写两种规范的 API,让开发不再需要关心 Node.js 上面的限制。另外构建工具还能利用 ESM 在编译阶段静态解析的特性,实现 Tree-shaking 效果,减少冗余代码的输出。

这里我们以 rollup 为例,先做全局安装:

pnpm i -g rollup

接着再安装 rollup-plugin-commonjs 插件,该插件可以让 rollup 支持引入 CJS 模块(rollup 本身是不支持引入 CJS 模块的):

pnpm i --save-dev @rollup/plugin-commonjs

我们在项目根目录新建 rollup 配置文件 rollup.config.js

import commonjs from 'rollup-plugin-commonjs';

export default {

  input: 'index.js',  // 入口文件

  output: {

    file: 'bundle.js',  // 目标文件

    format: 'iife'

  },

  plugins: [

    commonjs({

      transformMixedEsModules: true,

      sourceMap: false,

    })

  ]

};

plugin-commonjs 默认会跳过所有含 import/export 的模块,如果要支持如 import + require 的混合写法,需要带 transformMixedEsModules 属性。

接着执行 rollup --config 指令,就能按照 rollup.config.js 进行编译和打包了。

/** @file a.js **/

export let func = () => {

    console.log("It's an a-func...");

}

export let deadCode = () => {

    console.log("[a.js deadCode] Never been called here");

}

/** @file b.js **/

// named exports

module.exports = {

    func() {

        console.log("It's a b-func...")

    },

    deadCode() {

        console.log("[b.js deadCode] Never been called here");

    }

}

/** @file c.js **/

module.exports.func = () => {

    console.log("It's a c-func...")

};

module.exports.deadCode = () => {

    console.log("[c.js deadCode] Never been called here");

}

/** @file index.js **/

let a = require('./a');

import { func as bFunc } from './b.js';

import { func as cFunc } from './c.js';

a.func();

bFunc();

cFunc();

打包后的代码:

(function () {

    'use strict';

    function getAugmentedNamespace(n) {

        if (n.__esModule) return n;

        var a = Object.defineProperty({}, '__esModule', {value: true});

        Object.keys(n).forEach(function (k) {

            var d = Object.getOwnPropertyDescriptor(n, k);

            Object.defineProperty(a, k, d.get ? d : {

                enumerable: true,

                get: function () {

                    return n[k];

                }

            });

        });

        return a;

    }

    let func$1 = () => {

        console.log("It's an a-func...");

    };

    let deadCode = () => {

        console.log("[a.js deadCode] Never been called here");

    };

    var a$1 = /*#__PURE__*/Object.freeze({

        __proto__: null,

        func: func$1,

        deadCode: deadCode

    });

    var require$$0 = /*@__PURE__*/getAugmentedNamespace(a$1);

    var b = {

        func() {

            console.log("It's a b-func...");

        },

        deadCode() {

            console.log("[b.js deadCode] Never been called here");

        }

    };

    var func = () => {

        console.log("It's a c-func...");

    };

    let a = require$$0;

    a.func();

    b.func();

    func();

})();

可以看到,rollup 通过 Tree-shaking 移除掉了从未被调用过的 c 模块的 deadCode 方法,但 a、b 两模块中的 deadCode 代码段未被移除,这是因为我们在引用 a.js 时使用了 require,在 b.js 中使用了 CJS named exports,这些都导致了 rollup 无法利用 ESM 的特性去做静态解析。

常规在开发项目时,还是建议尽量使用 ESM 的语法来书写全部模块,这样可以最大化地利用构建工具来减少最终构建文件的体积。

React 基础及核心总结/一

2022
24/08

1. 元素属性及props

1)组件可以接受任意 props,包括基本数据类型,React 元素以及函数。

2)组件接到父组件传来的props是只读的,绝不能够被修改的。这也叫做单向数据流。

3)组件中的标签元素与原生的dom元素不同,其会被转义为对象(虚拟dom)。组件元素的属性与事件与原生的也不尽相同。比如class 在 JSX 中被写作 classNameonclick 在 JSX 中被写作 onClickfor 在 JSX 中被写作 htmlFor。这些新构建的属性和事件一般都是用小驼峰的书写形式。

4)不要将 props “镜像”给 state,请考虑直接使用 props。否则可能会产生BUG(当一个派生 state 值被 setState 方法更新时,你的prop是否需要更新?)。

5)属性可使用展开表达式将所有props一次性赋值:

function App1() {
  return <Greeting firstName="Ben" lastName="Hector" />;
}
// 等价于
function App2() {
  const props = {firstName: 'Ben', lastName: 'Hector'};
  return <Greeting {...props} />;
}

6)属性可以条件赋值

const Button = props => {
  const { kind, ...other } = props;
  const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
  return <button className={className} {...other} />;
};

const App = () => {
  return (
    <div>
      <Button kind="primary" onClick={() => console.log("clicked!")}>
        Hello World!
      </Button>
    </div>
  );
};

2. state 和 setState

1)不要直接修改 State,直接修改将不会重新渲染组件,而是应该使用 setState(),类似于小程序中的setData()。如this.state.comment = 'Hello';是错误的。这里也能看出,React的数据是单向绑定且单向流动。

2)Class组件中,构造函数(constructor)是唯一能够直接设置State的地方。但现在一般都使用函数式组件及hook方法取代Class组件了。

3)Class组件中,setState方法会浅合并对象(Object.assign)。而在hook方法中,useState 导出的set方法不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果。

const [state, setState] = useState({});
setState(prevState => {
  // 也可以使用 Object.assign
  return {...prevState, ...updatedValues};
});

3. Refs 和 Dom

1)Refs 创建

在React中也有refs,和Vue等其他框架一样,同样在标签中使用ref属性。Refs 允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。但非必要尽量避免能够使用props解决的问题而使用refs

在Class组件中使用React.createRef()来创建ref,或在函数式组件内部使用名为useRef()的hook来创建ref。一般情况下可以使用ref的current属性来访问该元素的dom节点或该自定义组件的实例。不可在函数组件父级中对函数组件使用ref,因为函数组件没有实例。

function MyFunctionComponent() {
   // 这里必须声明 textInput,这样 ref 才可以引用它
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }
  
  // 这样做是正确的(在函数组件内部访问元素dom)
  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  render() {
    // 这样做是错误的,无法访问的(在函数组件外部访问组件实例)
    return (
      <MyFunctionComponent ref={this.textInput} />
    );
  }
}

useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

2)回调 Refs

你可以在组件间传递回调形式的 Refs,也就是使用回调函数的形式创建Refs,这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。比如将回调函数通过props传递给子组件,这样父子组件都可以访问到子组件中的元素。

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。

回调Refs创建和使用

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 组件挂载后,让文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
    // 实例上(比如 this.textInput)
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

父子组件共享Refs

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

在函数式组件中可以使用useCallback()来创建具有变化监听的回调Ref

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}
// 没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的变化通知到我们。
// 注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。
// 在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1> 组件在整个重新渲染期间始终存在。
// 如果你希望在每次组件调整大小时都收到通知,则可能需要使用 ResizeObserver 或基于其构建的第三方 Hook。

4. 元素中表达式渲染的一些问题

falsenullundefined, and true 是合法的子元素。但它们并不会被渲染。以下的 JSX 表达式渲染结果相同:

<div />

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{undefined}</div>

<div>{true}</div>

值得注意的是有一些 “falsy” 值(被类型转义),如数字 0,仍然会被 React 渲染。这个特点和其他的一些框架会有些出入。

例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,将会渲染为数字 0

// 如下会被渲染为数字0
<div>
  {props.messages.length &&
    <MessageList messages={props.messages} />
  }
</div>

// 需要渲染右侧组件,&& 之前的表达式应总是布尔值:
<div>
  {props.messages.length > 0 &&
    <MessageList messages={props.messages} />
  }
</div>

Pinia是Vuex的良好替代品吗?

2022
25/07

介绍

Pinia 是西班牙语中菠萝发音,菠萝实际上是一组单独的花朵,它们结合在一起形成多个水果。与商店类似,每一家都是独立诞生的,但最终都是相互联系的。

Pinia 是 Vue.js 的轻量级状态管理库,最近很受欢迎。它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。

Pinia的成功可以归功于其管理存储数据的独特功能(可扩展性、存储模块组织、状态变化分组、多存储创建等)。

另一方面,Vuex也是为Vue框架建立的一个流行的状态管理库,它也是Vue核心团队推荐的状态管理库。Vuex高度关注应用程序的可扩展性、开发人员的工效和信心。它基于与Redux相同的流量架构。

Pinia 设置

Pinia 很容易上手,因为它只需要安装和创建一个store。

要安装 Pinia,您可以在终端中运行以下命令:

yarn add pinia@next 
# or with npm 
npm install pinia@next 

该版本与Vue 3兼容,如果你正在寻找与Vue 2.x兼容的版本,请查看v1分支。

Pinia是一个围绕Vue 3 Composition API的封装器。因此,你不必把它作为一个插件来初始化,除非你需要Vue devtools支持、SSR支持和webpack代码分割的情况:

//app.js 
import { createPinia } from 'pinia' 
app.use(createPinia()) 

在上面的片段中,你将Pinia添加到Vue.js项目中,这样你就可以在你的代码中使用Pinia的全局对象。这一步不可省略。

为了创建一个store,你用一个包含创建一个基本store所需的states、actions和getters的对象来调用 defineStore 方法。

import { defineStore } from 'pinia'

// main是Store的名称。它在您的应用程序中是唯一的。
// 并将出现在DevTools中
export const useMainStore = defineStore('main', {
  // a function that returns a fresh state
  state: () => ({
    counter: 0,
    name: 'Eduardo',
  }),
  // optional getters
  getters: {
    // getters receive the state as first parameter
    doubleCount: (state) => state.counter * 2,
    // use getters in other getters
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    },
  },
  // optional actions
  actions: {
    reset() {
      // `this` is the store instance
      this.counter = 0
    },
  },
})

Pinia使用

使用 Pinia,可以按如下方式访问该store:

import { useMainStore } from '@/stores/main'
import { storeToRefs } from 'pinia'

export default defineComponent({ 
  setup() { 
    const main = useMainStore() 

    // extract specific store properties
    const { counter, doubleCount } = storeToRefs(main)
 
    return { 
      // whole store
      main,
      // state or getter
      counter,
      doubleCount,
      // 计算属性方式 
      _counter: computed(() => main.counter), 
    } 
  }, 
}) 

请注意,在访问其属性时省略了 store 的 state 对象。

文档

要了解有关 Pinia 的更多信息,请查看其文档

Vuex 和 Pinia 对比

Pinia试图尽可能地接近Vuex的理念。它的设计是为了测试Vuex的下一次迭代的建议,它是成功的,因为目前有一个开放的RFC,用于Vuex 5,其API与Pinea使用的非常相似。这个项目的个人意图是重新设计使用全局Store的体验,同时保持Vue的平易近人的理念。Pinea的API与Vuex一样接近,因为它不断向前发展,使人们很容易迁移到Vuex,甚至在未来融合两个项目(在Vuex下)。

尽管 Pinia 足以取代 Vuex,但取代 Vuex 并不是它的目标,因此 Vuex 仍然是 Vue.js 应用程序的推荐状态管理库。

在现阶段,由于Pinia是轻量级的,体积很小,它适合于中小型应用。它也适用于低复杂度的Vue.js项目,因为一些调试功能,如时间旅行和编辑仍然不被支持。

将 Vuex 用于中小型 Vue.js 项目是过度的,因为它是重量级的,对性能有很大影响。因此,Vuex 适用于大规模、高复杂度的 Vue.js 项目。

Ps:2022-8:看官网的说明,意思是Pinia将会取代Vuex。

这些是目前Pinia相比Vuex的优势。

  • 极其轻巧(体积约 1KB)
  • store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见。
  • 支持多个Store。
  • 支持 Vue devtools、SSR 和 webpack 代码拆分。
  • 无需创建自定义的复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能地利用 TS 类型推断。与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易。
  • Pinia 不支持嵌套存储。相反,它允许你根据需要创建store。但是,store仍然可以通过在另一个store中导入和使用store来隐式嵌套
  • 存储器在被定义的时候会自动被命名。因此,不需要对模块进行明确的命名。
  • Pinia允许你建立多个store,让你的捆绑器代码自动分割它们
  • Pinia允许在其他getter中使用getter