从原理上大致带你看看vue3更新了哪些常用功能

346 阅读11分钟

vue3其实在去年就已经有非常多的同学和大牛开始在探索和使用了, 我个人也摸索了大概有小半年, 中间遇到过的麻烦事也挺多的,虽然vue官网提供了完整的迁徙文档, 但是失去了对比, 一切就显得没有那么直观了, 所以希望这篇博客对于vue3新功能的总结可以帮助新手少走弯路, 也希望可以得到大牛指教

在这篇博文中, 我们将要聊聊如下几个内容:

  • vite脚手架的原理(对比webpack)
  • vue3在性能上的进一步突破(如: 静态提升, 预字符串化, patch flag等)
  • vue3在组件, API和数据代理上的新变化
  • vue3推出的reactivity apicomposition api

vite脚手架的原理(对比webpack)

vite的使用我就不怎么说了, 官网直接手把手教你怎么用

vite官网: vitejs.dev/guide/

我们主要来看看vite究竟快在哪里

打开vite官网我们会发现映入眼帘的第一句话就直接交代了vite的作用:

Vite (French word for "fast", pronounced /vit/) is a build tool that aims to provide a faster and leaner development experience for modern web projects

vite官方说vite能够给开发者带来更加好的开发体验, 聊到这儿, 我们得想想, 我们使用vue-cli的时候, 开发体验就很差吗? 这里因为看博文的每个同学所接触到项目和团队规模不一致, 我相信有的同学可能能够直接肯定的回答yes, 有的还有疑惑, 没事, 我们来看看webpack他的一个运作原理之后我们在聊聊使用vue-cli可能会出现影响开发体验的点

什么叫开发体验, 这个我想了一下还是说一下吧, 简而言之就是你在开发代码的时候的体验呗, 开发体验由哪些点组成呢, 一个就是你敲下yarn dev / start以后的本地开发环境的开启速度(如果你敲了yarn dev以后需要好几分钟你的项目才能在localhost: 8080跑起来, 你说体验好不好), 第二个就是当你改动了项目文件以后, 整个热替换的速度。

webpack的编译原理

其实说高大上一点是编译原理, 但是事实上我们要聊得就是使用vue-cli搭建的工程当你敲下yarn start / dev命令去要求开启localhost:8000开发服务器的这个过程中都发生了什么

根据上图, 我们可以能够基本窥探到webpack他在开启开发服务器时候的基本流程, 那么我们来总结一下步骤:

  1. webpack根据入口模块找寻所有的依赖并进行打包(这个打包过程还蛮繁琐的, 我画了一张图大家可以看一下)

  2. 走完上面的编译过程以后, webpack会去走一个生成chunk的流程, 然后还会去走合并chunk和输出目录的流程, 这些细节都非常繁琐, 这里就不多作赘述了, 这一步和上一步我们称之为打包

  3. 当走完打包步骤以后, webpack才会利用webpack-dev-server插件来帮助我们开启localhost:8080端口, 这个时候我们就可以访问开发服务器了

  4. 在开发过程中, 如果有文件产生了变化, webpack会直接重新收集该文件所依赖的所有依赖并重新走打包流程

那么webpack这样做有什么好处呢?

由于我们的代码都会被webpack进行编译, 我们首先就不用去管什么commonjs规范, 什么es6module规范, 代码你想怎么写就怎么写, 反正webpack最终会给你编译成webpack_require, 所以你也不用管什么浏览器兼容性, 想咋来就咋来

这样做又有什么坏处呢?

你的模块文件越多, 你所要打包的东西就越多, 这些东西没有打包好之前, 你甭想开启localhost:8080开发服务器

大家可以仔细想想哈, 这样做带来的优点我们几乎完全不依赖(因为如果你在开发中又写commonjs规范的代码又写es6Module规范的代码还加上什么cmd规范的代码, 你的上级肯定会说你精神错乱, 行迹疯迷), 这是不被认可的编写代码的方式, 但是这样做带来的缺点在某些情况下就有些致命了, 当你项目足够庞大的时候, 你可能每次开启开发服务器要几分钟每次热替换你要等几十秒, 你想想你一天要热替换多少次, 要等多少次几十秒, 这个时间还是蛮恐怖的

vite的编译原理

vite的快就是快在编译上, 话不多说, 我们直接来看看vite的编译原理

步骤总结:

  1. 当我们在terminal敲下yarn dev / start的时候, vite无需等待(其实也需要, 但是他的等待是一个做缓存的过程), 直接开启localhost:8080服务器环境, 开发者可以直接访问
  2. webpack是将所有的module打包成一个js然后引入index.html, 而vite直接开启localhost并打开index.html, 但是我们会发现但凡你使用vite构建的工程, index.html在一开始就被引入了main.js, 这样意味着index.html一旦打开浏览器就会再次去请求main.js
  3. 当发现main.js依赖了其他的文件, 比如App.vue, 浏览器又会再次向开发服务器请求App.vue, 这个时候开发服务器会将App.vue编译成JS给到浏览器, 如此反复, 有依赖就请求, 没依赖到的模块鸟都不鸟你
  4. 当需要热替换的时候, vite只会让浏览器重新请求被更改的文件, 而不是所有依赖的文件全部重新打包

这样做好在哪里?

这不用我说了吧, 一个是将所有的杂七杂八的模块都打包以后再开启开发服务器, 一个是按需加载模块, 当模块有几千个的时候熟快熟慢我相信不用多说

这样做有什么缺陷?

首先我们在工程里使用了import模块化语句(并且在index.html引入main.js的时候打入type为module的状态)以后, 这个时候浏览器就会用最新的es6 Module的形式来请求资源, 这意味着会有两个缺陷

  • 一旦你是不支持es6module的老版本浏览器, 那么你将永远只能引入最基础的main.js, 不支持浏览器模块化的话他根本不认识import是什么东西
  • 你将不能使用其他模块化规范的代码, 因为同理, 浏览器只认识es6 module, require是啥他都不知道

但是这两个缺陷致不致命? 不致命, 首先你作为开发者你肯定是用最新版本的浏览器进行开发, 其次我上面也聊过了, 企业压根就不允许你精神分裂一下这个规范一下那个规范的, 所以这两个问题都不是很致命, 但是vite对于构建速度的提升是非常大的, 特别是项目越大他越明显

可能有的同学会说, 那既然是考虑到import一个模块就会发送一个请求, 那比如有的node_modules中的模块依赖了特别多的模块不是要发几千上百个请求? 还有的node_modules中的模块是commonjs导出的又怎么办?vite在内部其实也用到了webpack来处理这些问题, 你可以去看看官网的依赖预构建章节

vite的功能还不远止于此, 如果有兴趣的同学可以去vite官网看看

vue3在性能上的进一步突破(如: 静态提升, 预字符串化, patch flag等)

每一次新版本的发布, vue总是再给我们带来新的惊喜, 在vue3中, 尤雨溪又将性能考虑到了极致, 具他在b站分享的时候说: vue3的客户端渲染效率较vue2提升了1.3-2倍, ssr的渲染效率提升了2-3倍

静态提升和预字符串化

我们知道哈, 最终不管你在模板里写的是什么, 都会被vue编译成render函数去进行渲染, 而这些性能优化多数都是在render函数中做的文章

静态提升分为静态节点的提升和静态属性的提升

静态节点提升

首先什么是静态节点, 静态节点必须满足以下两个条件

  1. 必须是vue元素节点
  2. 元素节点上不能动态绑定状态和属性
<div>i am a static node</div> // 静态节点
<div class="wrapper">{{ info }}</div> // 动态节点
<div :class="{
  'diabled': isDisable
}">hello</div> // 这也是动态节点

那么什么叫做静态节点提升呢?

vue检测到你书写的是静态节点以后, 它将不会使用createNode去创建节点, 而是直接缓存节点

// Test.vue
// vue3支持多根节点, 这个我们后续会说原理
<template>
  <div>i am static node</div>
  <div>{{ infos }}</div>
</template>

<script>
export default {
  data: () => ({
    infos: "i am dynamics node"
  })
}
</script>

我们可以打开浏览器, 查看network, 我们之前有说过, 使用vite后会有一个按需加载的机制, 所以我们可以在network中看到整个vite给到我们的组件文件, 我们可以看到给到我们的文件是已经经过编译的, 而使用到的编译工具是@vue/compiler-sfc, 有兴趣的同学可以去了解一下, 我就不做多赘述了

上面的代码最终会被vue编译成如下形式:

// 我们会看到第一个静态节点的div, 并不是在render函数内部去创建的
// 而是在外部创建的, 这样也意味着, render函数中永远使用到的都是这
// 同样一个引用, 这就是静态节点的提升
// 其他的代码我们不用看, 只用看这一行就能够知道静态节点提升的含义了
const _hoisted_1 = createVNode("div", null, "i am static node", -1)

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (openBlock(),createBlock(_Fragment, null, [
    _hoisted_1,
    _createVNode("div", null, _toDisplayString(_ctx.infos), 1)
  ], 64))
}

静态属性提升

上面我们说到了静态节点, 那么什么是静态属性呢?

无非就是没有动态绑定属性值的属性呗

// class是静态属性 disable是动态属性
<div class="wrapper"></div> 
<div :disable="isDisable"></div>

跟静态节点一样, 静态属性将会被缓存到render函数外部

假设我们有模板是这样的:

<template>
  <div class="wrapper">{{ infos }}</div> 
  <div :disable="isDisable"></div>
</template>

<script>
export default {
  data: () => ({
    isDisable: false,
    infos: "hello"
  })
}
</script>

打开浏览器, 我们会发现上面的代码会被编译成

// 我们会发现即使第一个div是动态节点, 但是只要他上面有静态属性
// 则静态属性就会被提升到render函数外部
const _hoisted_1 = { class: "wrapper" }

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("div", _hoisted_1, _toDisplayString(_ctx.infos), 1 /* TEXT */),
    _createVNode("div", { disable: _ctx.isDisable }, null, 8 /* PROPS */, ["disable"])
  ], 64 /* STABLE_FRAGMENT */))
}

注意如果你的节点上又有动态节点, 又有静态节点, 那你是跑不掉的, 一定会被放进render函数内部的

预字符串化

预字符串化就是当你的连续的静态节点数量一多(至于他怎么判定的我们后续再说)的话, 他就会直接帮你把这一连串的静态节点数量直接变成字符串, 不会一个一个的去保存引用

假设我们的模板是这样

<template>
  <div class="wrapper"></div> 
  <div class="wrapper"></div> 
  <div class="wrapper"></div> 
  <div class="wrapper"></div> 
  <div class="wrapper"></div>
  <div :disable="isDisable"></div>
</template>

<script>
export default {
  data: () => ({
    isDisable: false,
    infos: "hello"
  })
}
</script>

被编译后的结果如下:

// 我们会看到vue直接调用了createStaticNodes方法去生成静态节点, 而
// 生成的静态节点也不会进入vue的虚拟dom树比对环节
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div class=\"wrapper\"></div><div class=\"wrapper\"></div><div class=\"wrapper\"></div><div class=\"wrapper\"></div><div class=\"wrapper\"></div>", 5)

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _hoisted_1,
    _createVNode("div", { disable: _ctx.isDisable }, null, 8 /* PROPS */, ["disable"])
  ], 64 /* STABLE_FRAGMENT */))
}

我们可以大致理解一下上面的代码vue2和3各自生成的虚拟dom树如下图:

如图所示, 当需要进行patch比对的时候, vue2没有动静态节点的区分, 都会进行比对, 而vue3有了静态节点, 静态节点他看都不看, 直接去比对动态节点, 这样怎么看效率都会提高不少

缓存事件处理函数

vue3中, 我们绑定的事件处理函数也会得到缓存, 话不多说, 直接看代码

<template>
  <div @click="clickHandler"></div> 
  <div @click="secHandler"></div>
</template>

<script>
export default {
  methods: {
    clickHandler() {
      
    },
    secHandler() {

    }
  }
}
</script>

上面的代码会被编译成如下结果

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("div", {
      // 我们会发现_cache作为一个缓存池会被作为参数传递给render函数
      // 当我们首次创建虚拟dom的时候, 会往_cache中缓存我们的事件
      // 后续再进行虚拟dom创建时就不会再重新创建引用了
      onClick: _cache[1] || (_cache[1] = (...args) => ($options.clickHandler && $options.clickHandler(...args)))
    }),
    _createVNode("div", {
      onClick: _cache[2] || (_cache[2] = (...args) => ($options.secHandler && $options.secHandler(...args)))
    })
  ], 64 /* STABLE_FRAGMENT */))
}

有点写累了, 我打王者荣耀去了, 下周再续更吧~

patch flag 和 block tree

loading

vue3在组件, API和数据代理上的新变化

loading

vue3推出的reactivity apicomposition api