《玩转 Vue 3 全家桶》学习笔记-基础入门篇

# 《玩转 Vue 3 全家桶》学习笔记-基础入门篇

# 入门

Q: 数据发生变化后,我们怎么去通知页面更新?

  • Angular 1 就是最老套的脏检查。所谓的脏检查,指的是 Angular 1 在对数据变化的检查上,遵循每次用户交互时都检查一次数据是否变化,有变化就去更新 DOM 这一方法。
  • Vue 1 的解决方案,就是使用响应式,初始化的时候,Watcher 监听了数据的每个属性,这样数据发生变化的时候,我们就能精确地知道数据的哪个 key 变了,去针对性修改对应的 DOM 即可
  • React 框架,在页面初始化的时候,在浏览器 DOM 之上,搞了一个叫虚拟 DOM 的东西,也就是用一个 JavaScript 对象来描述整个 DOM 树。我们可以很方便的通过虚拟 DOM 计算出变化的数据,去进行精确的修改。浏览器操作 DOM 一直都是性能杀手,而虚拟 DOM 的 Diff 的逻辑,又能够确保尽可能少的操作 DOM,这也是虚拟 DOM 驱动的框架性能一直比较优秀的原因之一

Q: Vue 与 React 框架的对比

  1. 响应式和虚拟 DOM

在 Vue 框架下,如果数据变了,那框架会主动告诉你修改了哪些数据;而 React 的数据变化后,我们只能通过新老数据的计算 Diff 来得知数据的变化。

  • 对于 Vue 来说,它的一个核心就是“响应式”,也就是数据变化后,会主动通知我们。响应式数据新建 Watcher 监听,本身就比较损耗性能,项目大了之后每个数据都有一个 watcher 会影响性能。
  • 对于 React 的虚拟 DOM 的 Diff 计算逻辑来说,如果虚拟 DOM 树过于庞大,使得计算时间大于 16.6ms,那么就可能会造成性能的卡顿。

React 为了突破性能瓶颈,借鉴了操作系统时间分片的概念,引入了 Fiber 架构。 通俗来说,就是把整个虚拟 DOM 树微观化,变成链表,然后我们利用浏览器的空闲时间计算 Diff。一旦浏览器有需求,我们可以把没计算完的任务放在一旁,把主进程控制权还给浏览器,等待浏览器下次空闲。

Vue 1 的问题在于响应式数据过多,这样会带来内存占用过多的问题。所以 Vue 2 大胆引入虚拟 DOM 来解决响应式数据过多的问题。

对于 Vue 2 来说,组件之间的变化,可以通过响应式来通知更新。组件内部的数据变化,则通过虚拟 DOM 去更新页面。这样就把响应式的监听器,控制在了组件级别,而虚拟 DOM 的量级,也控制在了组件的大小。

  1. template 和 JSX
  • React 的世界里只有 JSX,最终 JSX 都会在 Compiler 那一层,也就是工程化那里编译成 JS 来执行,所以 React 最终拥有了全部 JS 的动态性,这也导致了 React 的 API 一直很少,只有 state、hooks、Component 几个概念,主要都是 JavaScript 本身的语法和特性。

  • 而 Vue 的世界默认是 template,也就是语法是限定死的,比如 v-if 和 v-for 等语法。有了这些写法的规矩后,我们可以在上线前做很多优化。

Q: Vue 需不需要 React 的 Fiber 呢?

不需要;最早Vue3的提案其实是包含时间切片方案的,最后废弃的主要原因,是时间切片解决的的问题,Vue3基本碰不到

  1. Vue3把虚拟Dom控制在组件级别,组件之间使用响应式,这就让Vue3的虚拟Dom不会过于庞大;
  2. Vue3虚拟Dom的静态标记和自动缓存功能,让静态的节点和属性可以直接绕过Diff逻辑,也大大减少了虚拟Dom的Diff事件;
  3. 时间切片也会带来额外的系统复杂性

所以引入时间切片对于Vue3来说投入产出比不太理想,在后来的讨论中,Vue3的时间切片方案就被废弃了

  • computed 计算属性还内置了缓存功能,如果依赖数据没变化,多次使用计算属性会直接返回缓存结果,同我们直接写在模板里相比,性能也有了提升。

# Vue 2 常见的缺陷

  • 首先从开发维护的角度看,Vue 2 是使用 Flow.js 来做类型校验。但现在 Flow.js 已经停止维护了,整个社区都在全面使用 TypeScript 来构建基础库,Vue 团队也不例外。
  • 然后从社区的二次开发难度来说,Vue 2 内部运行时,是直接执行浏览器 API 的。但这样就会在 Vue 2 的跨端方案中带来问题,要么直接进入 Vue 源码中,和 Vue 一起维护,比如 Vue 2 中你就能见到 Weex 的文件夹。

要么是要直接改为复制一份全部 Vue 的代码,把浏览器 API 换成客户端或者小程序的。比如 mpvue 就是这么做的,但是 Vue 后续的更新就很难享受到。

  • 最后从我们普通开发者的角度来说,Vue 2 响应式并不是真正意义上的代理,而是基于 Object.defineProperty() 实现的。

这个 API 并不是代理,而是对某个属性进行拦截,所以有很多缺陷,比如:删除数据就无法监听,需要 $delete 等 API 辅助才能监听到。

  • Option API 在组织代码较多组件的时候不易维护。

对于 Option API 来说,所有的 methods、computed 都在一个对象里配置,这对小应用来说还好。但代码超过 300 行的时候,新增或者修改一个功能,就需要不停地在 data,methods 里跳转写代码。

前面这些问题并不是 Vue 2 有意为之,大部分是发展的过程中碰见的。Vue 3 就是继承了 Vue 2 具有的响应式、虚拟 DOM,组件化等所有优秀的特点,并且全部重新设计,解决了这些历史包袱的新框架,是一个拥抱未来的前端框架。

接下来我们就来具体看看 Vue 3 新特性:

# Vue3 新特性

  • RFC 机制

Vue 3 的第一个新特性和代码无关,而是 Vue 团队开发的工作方式。关于 Vue 的新语法或者新功能的讨论,都会先在 GitHub (opens new window) 上公开征求意见,邀请社区所有的人一起讨论;Vue 3 正在讨论中的新需求,任何人都可以围观、参与讨论和尝试实现。

  • 响应式系统

Vue 2 的响应式机制是基于 Object.defineProperty() 这个 API 实现的,此外,Vue 还使用了 Proxy,这两者看起来都像是对数据的读写进行拦截,但是 defineProperty 是拦截具体某个属性,Proxy 才是真正的“代理”。

// vue2
Object.defineProperty(obj, 'title', {
  get() {},
  set() {},
})

// vue3
// 
new Proxy(obj, {
  get() { },
  set() { },
})

当项目里“读取 obj.title”和“修改 obj.title”的时候被 defineProperty 拦截,但 defineProperty 对不存在的属性无法拦截,所以 Vue 2 中所有数据必须要在 data 里声明。

对数组的长度的修改等操作还是无法实现拦截,所以还需要额外的 $set 等 API。

虽然 Proxy 拦截 obj 这个数据,但 obj 具体是什么属性,Proxy 则不关心,统一都拦截了。而且 Proxy 还可以监听更多的数据格式,比如 Set、Map,这是 Vue 2 做不到的。

Proxy 也存在一些兼容性问题,这也是为什么 Vue 3 不兼容 IE11 以下的浏览器的原因。

  • 自定义渲染器

Vue 2 内部所有的模块都是揉在一起的,这样做会导致不好扩展的问题;Vue 3 是怎么解决这个问题的呢?那就是拆包:响应式、编译和运行时全部独立了。

在 Vue 3 的组织架构中,响应式独立了出来。而 Vue 2 的响应式只服务于 Vue,Vue 3 的响应式就和 Vue 解耦了,你甚至可以在 Node.js 和 React 中使用响应式。

在你想使用 Vue 3 开发小程序、开发 canvas 小游戏以及开发客户端的时候,就不用全部 fork Vue 的代码,只需要实现平台的渲染逻辑就可以。

  • 全部模块使用 TypeScript 重构

Vue 2 那个时代基本只有两个技术选型,Facebook 家的 Flow.js 和微软家的 TypeScript。Vue 2 选 Flow.js 没问题,但是现在 Flow.js 被抛弃了。Vue 3 选择了 TypeScript,TypeScript 官方也对使用 TypeScript 开发 Vue 3 项目的团队也更加友好。

大部分开源的框架都会引入类型系统,来对 JavaScript 进行限制。这样做的原因,就是我们前面提到的两点:第一点是,类型系统带来了更方便的提示;第二点是,类型系统让代码更健壮。

  • Composition API 组合语法

先看一个实现累加器例子:

<!-- vue2写法 -->

<div id="app">

  <h1 @click="add">{{count}} * 2 = {{double}}</h1>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
let App = {
  data(){
    return {
      count:1
    }
  },
  methods:{
    add(){
      this.count++
    }
  },
  computed:{
    double(){
      return this.count*2
    }
  }
}
Vue.createApp(App).mount('#app')
</script>

再看 vue3 的写法:

<!-- vue3写法 -->

<div id="app">
  <h1 @click="add">{{state.count}} * 2 = {{double}}</h1>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const {reactive,computed} = Vue
let App = {
  setup(){
    const state = reactive({
      count:1
    })
    function add(){
      state.count++
    }
    const double = computed(()=>state.count*2)
    return {state,add,double}
  }
}
Vue.createApp(App).mount('#app')
</script>

vue3的写法就是使用了Composition API。 使用 Composition API 后,代码看起来很烦琐,没有 Vue 2 中 Options API 的写法简单好懂,但 Options API 的写法也有几个很严重的问题:

  1. 由于所有数据都挂载在 this 之上,因而 Options API 的写法对 TypeScript 的类型推导很不友好,并且这样也不好做 Tree-shaking 清理代码。
  2. 新增功能基本都得修改 data、method 等配置,并且代码上 300 行之后,会经常上下反复横跳,开发很痛苦。
  3. 代码不好复用,Vue 2 的组件很难抽离通用逻辑,只能使用 mixin,还会带来命名冲突的问题。

使用 Composition API 后,虽然看起来烦琐了一些,但是带来了诸多好处:

  1. 所有 API 都是 import 引入的。用到的功能都 import 进来,对 Tree-shaking 很友好,我的例子里没用到功能,打包的时候会被清理掉 ,减小包的大小。
  2. 不再上下反复横跳,我们可以把一个功能模块的 methods、data 都放在一起书写,维护更轻松。
  3. 代码方便复用,可以把一个功能所有的 methods、data 封装在一个独立的函数里,复用代码非常容易。
  4. Composotion API 新增的 return 等语句,在实际项目中使用 <script setup> 特性可以清除, 我们后续项目中都会用到这样的操作。
  • 新的组件

Vue 3 还内置了 Fragment、Teleport 和 Suspense 三个新组件。

  1. Fragment: Vue 3 组件不再要求有一个唯一的根节点,清除了很多无用的占位 div。
  2. Teleport: 允许组件渲染在别的元素内,主要开发弹窗组件的时候特别有用。
  3. Suspense: 异步组件,更方便开发有异步请求的组件。
  • 新一代工程化工具 Vite

Vite 不在 Vue 3 的代码包内,和 Vue 也不是强绑定,Vite 主要提升的是开发的体验。

Webpack 等工程化工具的原理,就是根据你的 import 依赖逻辑,形成一个依赖图,然后调用对应的处理工具,把整个项目打包后,放在内存里再启动调试。

由于要预打包,所以复杂项目的开发,启动调试环境需要 3 分钟都很常见,Vite 就是为了解决这个时间资源的消耗问题出现的。

Vite 的工作原理,一开始就可以准备联调,然后根据首页的依赖模块,再去按需加载,这样启动调试所需要的资源会大大减少。

其他

对于 Vue 2,官方还会再维护两年,但两年后的问题和需求,官方就不承诺修复和提供解答了,现在继续用 Vue 2 其实是有这个隐患的。

Vue 3 也不是没有问题,由于新的响应式系统用了 Proxy,会存在兼容性问题。也就是说,如果你的应用被要求兼容 IE11,就应该选择 Vue 2。而且,Vue 团队也已经放弃 Vue 3 对 IE11 浏览器的支持。

微软本身也在抛弃 IE,转而推广 Edge。所以 Vue 官方在重新思考后,决定让 Vue 3 全面拥抱未来,把原来准备投入到 Vue 3 上支持 IE11 的精力转投给 Vue 2.7。

Vue 2.7 会移植 Vue 3 的一些新特性,让你在 Vue 2 的生态中,也能享受 Vue 3 的部分新特性。在 Vue 3 发布之前,Vue 2 项目中就可以基于 @vue/composition-api 插件,使用 Composition API 语法,Vue 2 会直接内置这个插件,在 Vue 2 中默认也可以用 Compositon 来组合代码。

# Vue3 用法

# createApp

在 Vue 2 中,我们使用 new Vue() 来新建应用,有一些全局的配置我们会直接挂在 Vue 上,比如我们通过 Vue.use 来使用插件,通过 Vue.component 来注册全局组件。

Vue 3 引入一个新的 API ,createApp,新增了 App 的概念。全局的组件、插件都独立地注册在这个 App 内部,很好的解决了多个实例容易造成混淆的问题。

// 我们在 Vue 上先注册了一个组件 el-counter,然后创建了两个 Vue 的实例。
// 这两个实例都自动都拥有了 el-couter 这个组件,但这样做很容易造成混淆。
Vue.component('el-counter',...)
new Vue({el:'#app1'})
new Vue({el:'#app2'})

// Vue 3 引入createApp
const { createApp } = Vue
const app = createApp({})
app.component(...)
app.use(...)
app.mount('#app1')

const app2 = createApp({})
app2.mount('#app2')

createApp 还移除了很多我们常见的写法,比如在 createApp 中,就不再支持 filter、$on、$off、$set、$delete 等 API。

# Vue 3 生态现状

  • Vue-cli4 已经提供内置选项,你当然可以选择它支持的 Vue 2。如果你对 Vite 不放心的话,Vue-cli4 也全面支持 Vue 3,这还是很贴心的。

  • vue-router 是复杂项目必不可少的路由库,它也包含一些写法上的变化,比如从 new Router 变成 createRouter;使用方式上,也全面拥抱 Composition API 风格,提供了 useRouter 和 useRoute 等方法。

  • Vuex 4.0 也支持 Vue 3,不过变化不大。有趣的是 Vue 官方成员还发布了一个 Pinia,Pinia 的 API 非常接近 Vuex5 的设计,并且对 Composition API 特别友好,更优雅一些。

  • 其他生态诸如 Nuxt、组件库 Ant-design-vue、Element 等等,都有 Vue 3 的版本发布。

# Vue 的升级

  • 在 Vue 3 的项目里,有一个 @vue/compat 的库,这是一个 Vue 3 的构建版本,提供了兼容 Vue 2 的行为。这个版本默认运行在 Vue 2 下,它的大部分 API 和 Vue 2 保持了一致。当使用那些在 Vue 3 中发生变化或者废弃的特性时,这个版本会提出警告,从而避免兼容性问题的发生,帮助你很好地迁移项目。并且通过升级的提示信息,@vue/compat 还可以很好地帮助你学习版本之间的差异。

  • 阿里妈妈出了一个自动化替换的工具,GoGoCode (opens new window)

自动化替换工具的原理很简单,和 Vue 的 Compiler 优化的原理是一样的,也就是利用编译原理做代码替换。利用 babel 分析 Vue 2 的源码,解析成 AST,然后根据 Vue 3 的写法对 AST 进行转换,最后生成新的 Vue 3 代码。

# 新的代码组织方式

# Composition API

Composition API 可以让我们更好地组织代码结构。我们需要使用 Composition API 的逻辑来拆分代码,把一个功能相关的数据和方法都维护在一起。

  • 举个例子:
// src/utils/mouse.js
// 封装一个获取鼠标的位置的方法

import {ref, onMounted,onUnmounted} from 'vue'
export function useMouse(){
    const x = ref(0)
    const y = ref(0)
    function update(e) {
      x.value = e.pageX
      y.value = e.pageY
    }
    // 组件加载的时候,会触发 onMounted 生命周期,我们执行监听 mousemove 事件,从而去更新鼠标位置的 x 和 y 的值;
    onMounted(() => {
      window.addEventListener('mousemove', update)
    })
    // 组件卸载的时候,会触发 onUnmounted 生命周期,解除 mousemove 事件。
    onUnmounted(() => {
      window.removeEventListener('mousemove', update)
    })
    return { x, y }
}

使用:

import {useMouse} from '../utils/mouse'

let {x,y} = useMouse()

因为 ref 和 computed 等功能都可以从 Vue 中全局引入,所以我们就可以把组件进行任意颗粒度的拆分和组合,这样就大大提高了代码的可维护性和复用性。

# <script setup>

使用 <script setup> 可以让代码变得更加精简,这也是现在开发 Vue 3 项目必备的写法。

  • 没有使用<script setup>
<script >
import { ref } from "vue";
export default {
  setup() {
    let count = ref(1)
    function add() {
      count.value++
    }
    return {
      count,
      add
    }
  }
}
</script>
  • 使用<script setup>:
<template>
  <div>
    <h1 @click="add">{{ count }}</h1>
  </div>
</template>

<script setup>
import { ref } from "vue";
let count = ref(1)
let color = ref('red')
function add() {
  count.value++
  color.value = Math.random()>0.5? "blue":"red"
}
</script>

<style scoped>
h1 {
  color:v-bind(color);
}
</style>>

# 响应式

响应式原理是什么呢?Vue 中用过三种响应式解决方案,分别是 defineProperty、Proxy 和 value setter。

  • Vue2 使用 defineProperty 来实现响应式;

  • Vue 3 的 reactive 函数可以把一个对象变成响应式数据,而 reactive 就是基于 Proxy 实现的。


import {reactive,computed,watchEffect} from 'vue'

let obj = reactive({
    count:1
})
let double = computed(()=>obj.count*2)
obj.count = 2

// 可以通过 watchEffect,在 obj.count 修改之后,执行数据的打印
watchEffect(()=>{
    console.log('数据被修改了',obj.count,double.value)
})
  • 在 Vue 3 中还有另一个响应式实现的逻辑,就是利用对象的 get 和 set 函数来进行监听,这种响应式的实现方式,只能拦截某一个属性的修改,这也是 Vue 3 中 ref 这个 API 的实现。

let getDouble = n => n * 2
let _value = 1
double = getDouble(_value)

let count = {
  get value() {
    return _value
  },
  set value(val) {
    _value = val
    double = getDouble(_value)

  }
}
console.log(count.value,double)
count.value = 2
console.log(count.value,double)

封装响应式函数

  • 例,封装一个 useStorage 函数:

import { ref, watchEffect, computed } from "vue";

function useStorage(name, value=[]){
    // ref 从本地存储中获取数据,封装成响应式并且返回
    let data = ref(JSON.parse(localStorage.getItem(name)|| value))
    // watchEffect 中做本地存储的同步
    watchEffect(()=>{
        localStorage.setItem(name,JSON.stringify(data.value))
    })
    return data
}


//// 使用
let title = ref("");
let todos = useStorage('todos',[])

function addTodo() {
  todos.value.push({
    title: title.value,
    done: false,
  });
  title.value = "";
}

Vue 社区中其实已经有一个类似的工具集合,也就是 VueUse,它把开发中常见的属性都封装成为响应式函数。

VueUse 这个工具包,这也是 Vue 官方团队成员的作品。VueUse 提供了一大批工具函数,包括全屏、网络请求、动画等,都可以使用响应式风格的接口去使用,并且同时兼容 Vue 2 和 Vue 3,开箱即用。

# 组件通信

  • 封装一个评级组件Rate.vue
<template>
    <div @click="onRate(num)">
        {{rate}}
    </div>
</template>

<script setup>
import { defineProps,computed,defineEmits } from 'vue';
// 通过defineProps传递属性
let props = defineProps({
    value: Number
})
let rate = computed(()=>"★★★★★☆☆☆☆☆".slice(5 - props.value, 10 - props.value))

// 通过defineEmits通信
let emits = defineEmits('update-rate')
function onRate(num){
  emits('update-rate',num)
}
</script>

使用:

<template>
<Rate :value="score" @update-rate="update"></Rate>
</template>

<script setup>
import {ref} from 'vue'
import Rate from './components/Rate.vue'
let score = ref(3)

function update(num){
  score.value = num
}
</script>

# v-model

v-model 是传递属性和接收组件事件两个写法的简写。

let props = defineProps({
    modelValue: Number,
    theme:{type:String,default:'orange'}
})
let emits = defineEmits(['update:modelValue'])

// 使用
<template>
<h1>你的评分是 {{score}}</h1>
<Rate v-model="score"></Rate>
</template>

# transition

<transition name="fade">
  <h1 v-if="showTitle">你好 Vue 3</h1>
</transition>


<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s linear;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

具体 class 的名字,Vue 的官网有一个图给出了很好的解释,图里的 v-enter-from 中的 v,就是我们设置的 name 属性。

上例中标签在进入和离开的时候,会有 fade-enter-active 和 fade-leave-active 的 class,进入的开始和结束会有 fade-enter-from 和 face-enter-to 两个 class。

  • transition-group
    <ul v-if="todos.length">
      <transition-group name="flip-list" tag="ul">
        <li v-for="todo in todos" :key="todo.title">
          <input type="checkbox" v-model="todo.done" />
          <span :class="{ done: todo.done }"> {{ todo.title }}</span>
        </li>
      </transition-group>

    </ul>
<style>
.flip-list-move {
  transition: transform 0.8s ease;
}
.flip-list-enter-active,
.flip-list-leave-active {
  transition: all 1s ease;
}
.flip-list-enter-from,
.flip-list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
</style>

transition-group 包裹渲染的 li 元素,并且设置动画的 name 属性为 flip-list。然后我们根据 v-move 的命名规范,设置 .flip-list-move 的过渡属性,就实现了列表依次出现的效果了。

Back
上次更新: 1/24/2022, 5:38:42 PM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>