~/static/img/default-avatar-0.jpeg

linalu

前端开发工程师

技术文档

总结系列 - Vue3特性日记

linalu 2022-06-29

一、Composition API

先举个 Vue 2 中的简单例子,一个累加器,并且还有一个计算属性显示累加器乘以 2 的结果。

<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中使用Composition API的写法:

<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>

使用data、computed、methods、watch的组件选项来组织逻辑通常是很有效的。然而,当我们的组件开始变大,逻辑关注点的列表就会增长,导致代码难以阅读和理解。 20220629193459.jpg

如图所示,逻辑关注点按颜色进行分组,这种碎片化使得理解和维护组件变得困难,选项分离掩盖了潜在的逻辑问题。而使用Composition 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 等语句,在实际项目中使用

二、setup函数

setup() 函数是 vue3 中,专门为组件提供的新属性。它为我们使用 vue3 的 Composition API 新特性提供了统一的入口,setup 函数会在 beforeCreate 之前执行。因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

支持<srcipt setup>写法,它是在单文件组件中使用组合式API的编译时语法糖,本质上以更精简的方式来书写Composition API,我们定义的变量、函数和引入的组件,都不需要额外的生命周期,可以直接在模版中使用。

使用setup时,接收两个参数:

1、props:组件传入的属性,props是响应式的,当传入新的props时,它将会被更新。

2、context:非响应式,用来定义上下文(包含attrs、slot、emit)。上下文对象包含一些有用的属性,这些属性在vue2中需要通过this才能访问到,在setup函数中无法访问到this。

三、自定义Hooks

本质而言,hook就是一个函数,只不过hook是把 setup组合函数之中的 Composition API(ref、reactive、computed、watch、生命周期函数)等进行了封装抽离代码(公共代码,公共组件等),这样使得代码更加简洁。它类似于vue2中的mixin。我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。

四、响应式机制

响应式一直都是 Vue 的特色功能之一。Vue 中用过三种响应式解决方案,分别是 defineProperty、Proxy 和 value setter。

首先,我们来以例子来了解下Vue2的defineProperty API。

let obj = {}
let count = 1
let double = getDouble(count)
Object.defineProperty(obj,'count',{
get(){
return count
},
set(val){
count = val
double = getDouble(val)
}
})
console.log(double)  // 打印2
obj.count = 2
console.log(double) // 打印4  有种自动变化的感觉

这样就实现简易的响应式功能。但Vue2 实现响应式原理的语法中存在缺陷。如删除obj.count属性,set函数是不会执行的,double还是之前的值,这也是为什么在Vue2中,需要$delete一个专门的函数删除数据。

delete obj.count
console.log(double) // doube还是4

Vue 3 的响应式机制是基于 Proxy 实现的。先看看Proxy的例子。

let proxy = new Proxy(obj,{
    get : function (target,prop) {
        return target[prop]
    },
    set : function (target,prop,value) {
        target[prop] = value;
        if(prop==='count'){
            double = getDouble(value)
        }
    },
    deleteProperty(target,prop){
        delete target[prop]
        if(prop==='count'){
            double = NaN
        }
    }
})
console.log(obj.count,double)
proxy.count = 2
console.log(obj.count,double) 
delete proxy.count
// 删除属性后,我们打印log时,输出的结果就会是 undefined NaN
console.log(obj.count,double)

可以看出Proxy 实现的功能和 Vue 2 的 definePropery 类似,它们都能够在用户修改数据的时候触发 set 函数,从而实现自动更新 double 的功能。而且 Proxy 还完善了几个 definePropery 的缺陷,比如说可以监听到属性的删除。

Proxy 是针对对象来监听,而不是针对某个具体属性,所以不仅可以代理那些定义时不存在的属性,还可以代理更丰富的数据结构,比如 Map、Set 等,并且我们也能通过 deleteProperty 实现对删除操作的代理。

除了Proxy,Vue3中还有另一个响应式实现的逻辑,就是利用对象的 get 和 set 函数来进行监听,这种响应式的实现方式,只能拦截某一个属性的修改,这也是 Vue 3 中 ref 这个 API 的实现。在下面的代码中,我们拦截了 count 的 value 属性,并且拦截了 set 操作,也能实现类似的功能。

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)

三种实现原理的对比表格如下,更好的理解三种响应式的区别。 截屏2022062601.08.21.png

五、响应性API

理解了Vue响应式原理后,我们来看看几个响应性API的使用。

1、reactive:响应式转换是“深层”的——它影响所有嵌套property。

const obj = reactive({ count: 0 })

2、ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。如果将对象分配为ref值,则它将被reactive函数处理为深层的响应式对象。

const count = ref(0)
console.log(count.value) // 0

3、toRefs:将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。

在开发中,setup接收的参数props具有响应式,使用ES6解构则会消除它的响应式,想要使用解构后的数据则可以使用toRefs。

4、computed:接受一个getter函数,并根据getter的返回值返回一个不可变的响应式ref对象。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

5、watch:需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

// 侦听多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

6、watchEffect:立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

对比watch和watchEffect:

watchEffect不需要手动传入依赖,会先执行一次用来自动收集依赖, 无法获取到变化前的值, 只能获取变化后的值。watch惰性地执行副作用,更具体地说明应触发侦听器重新运行的状态,访问被侦听状态的先前值和当前值。

相同点:watch 与 watchEffect在手动停止侦听、清除副作用 (将 onInvalidate 作为第三个参数传递给回调)、刷新时机和调试方面有相同的行为。

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

六、生命周期

新版的生命周期函数,可以按需导入到组件中,且只能在 setup() 函数中使用。

<script lang="ts">
import { defineComponent, onBeforeMount, onBeforeUnmount, onBeforeUpdate, onErrorCaptured, onMounted, onUnmounted, onUpdated } from 'vue';
export default defineComponent({  
    setup(props, context) {
        onBeforeMount(()=> {      
            console.log('beformounted!')    
        })    
        onMounted(() => {
              console.log('mounted!')    
        })
        onBeforeUpdate(()=> {
              console.log('beforupdated!')
        })
        onUpdated(() => {
            console.log('updated!')
        })
        onBeforeUnmount(()=> {
            console.log('beforunmounted!')
        })
        onUnmounted(() => {
            console.log('unmounted!')
        })
        onErrorCaptured(()=> {
            console.log('errorCaptured!')
        })
        
        return {}
    }
});
</script>

生命周期图示: 截图.png

七、全部模块使用 TypeScript 重构

JavaScript 是弱类型的语言,Vue 2 是使用 Flow.js 来做类型校验。但现在 Flow.js 已经停止维护了,整个社区都在全面使用 TypeScript 来构建基础库。Vue 3 选择了 TypeScript,TypeScript 官方也对使用 TypeScript 开发 Vue 3 项目的团队也更加友好。 TypeScript 类型系统带来了更方便的提示,并且让我们的代码能够更健壮。

八、新组件

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

Fragment: Vue 3 组件不再要求有一个唯一的根节点,清除了很多无用的占位 div。

// Vue2
<template>
    <div>
        <span></span>
        <span></span>
    </div>
</template>

// Vue3
<template>
    <span></span>
    <span></span>
</template>

Teleport: 允许组件渲染在别的元素内,主要开发弹窗组件的时候特别有用。

使用弹窗组件时,我们即希望继续在组件内部使用Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中。 此时就需要 Teleport 上场,我们可以用包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。

const app = Vue.createApp({
  template: `
    <h1>Root instance</h1>
    <parent-component />
  `
})

app.component('parent-component', {
  template: `
    <h2>This is a parent component</h2>
    <teleport to="#endofbody">
      <child-component name="John" />
    </teleport>
  `
})

app.component('child-component', {
  props: ['name'],
  template: `
    <div>Hello, {{ name }}</div>
  `
})

Suspense: 异步组件,更方便开发有异步请求的组件。该组件有两个插槽,它们都只接收一个直接子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。

总结:

  • Composition API 组合语法带来了更好的组织代码的形式。
  • <script setup>的功能,我们不需要额外的生命周期,就可以直接在模板中使用。
  • 掌握到 Composition API 组织代码的方式,我们可以任意拆分组件的功能(自定义Hooks),提高代码的可维护性。
  • 全新的响应式系统基于 Proxy,也可以独立使用。
  • 熟悉响应性API的使用和生命周期钩子。
  • 全部的模块使用 TypeScript 重构,能够带来更好的可维护性。
  • Vue 3 内置了新的 Fragment、Teleport 和 Suspense 等组件。
作者相关知识精选
  • 节流

    // 使用时间戳
    function throttle(func, wait) {
      let preTime = 0;
    
      return function () {
        let nowTime = +new Date();
        let context = this;
        let args = arguments;
    
        if (nowTime - preTime > wait) {
          func.apply(context, args);
          preTime = nowTime;
        }
      };
    }
    
    // 定时器实现
    function throttle(func, wait) {
      let timeout;
    
      return function () {
        let context = this;
        let args = arguments;
    
        if (!timeout) {
          timeout = setTimeout(function () {
            timeout = null;
            func.apply(context, args);
          }, wait);
        }
      };
    }
    
    
    查看更多
  • 防抖

    function debounce(func, wait, immediate) {
      let timeout;
    
      return function () {
        let context = this;
        let args = arguments;
    
        if (timeout) clearTimeout(timeout);
        if (immediate) {
          let callNow = !timeout;
          timeout = setTimeout(function () {
            timeout = null;
          }, wait);
          if (callNow) func.apply(context, args);
        } else {
          timeout = setTimeout(function () {
            func.apply(context, args);
          }, wait);
        }
      };
    }
    
    
    查看更多
  • cookie、localStorage、sessionStorage

    1、cookie

    • 本身用于浏览器和 server 通讯。
    • 被“借用”到本地存储来的。
    • 可用 document.cookie = '...' 来修改。

    其缺点:

    • 存储大小限制为 4KB。
    • http 请求时需要发送到服务端,增加请求数量。
    • 只能用 document.cookie = '...' 来修改,太过简陋。

    2、localStorage 和 sessionStorage

    • HTML5 专门为存储来设计的,最大可存 5M。
    • API 简单易用, setItem getItem。
    • 不会随着 http 请求被发送到服务端。

    它们的区别:

    • localStorage 数据会永久存储,除非代码删除或手动删除。
    • sessionStorage 数据只存在于当前会话,浏览器关闭则清空。
    查看更多