# 初探Vue3

陈明浩 / 2021-07-04

# 前言

总所周知,Vue3 使用 TypeScript 重写了整个底层逻辑,引用 Vue 作者尤雨溪的原话来解释为什么要用 TypeScript :

重写的主要原因一个是类型系统,一个是内部逻辑分层。Vue 2 项目先基于 JavaScript,中期加入了 Flow 做类型检查,导致类型覆盖不完整。Flow 本身又破坏性更新频繁,工具链支持也不理想,所以决定转为用 TypeScript 重写。Vue 2 的内部逻辑分层不够清晰,对于长期维护是一个负担,这也是一个不重写就很难彻底改善的问题。

下面我们来看下 Vue3 相较于 Vue2 做出了那些升级吧。

# definedProperty 和 Proxy

Vue3 底层除了使用 TypeScript 重写以外,另一个最重要的改动就是使用 Proxy 替换了 definedProperty。

  • 相同点:两者都可以劫持对象的 get 和 set ,监听对象的属性值变动,及时更新视图。

  • 不同点:definedProperty 无法监听到对象属性的新增;Proxy 可以监听到对象属性的新增,且支持监听 ES6 新增的数据类型 Symbol、Set、Map。

# diff 算法的速度提升

  1. Vue3 也对老生常谈的 diff 算法做了一定的优化,其中最明显的优化就是静态节点的编译提升。其核心思想就是将静态节点缓存在常量中,在每次执行 Dom diff 的时候跳过对静态节点的编译,具体表现如下:
<div>Hello World</div>

const hoisted = createVNode('div', null, 'Hello World')
function render() {
  // .......
}

// 如果在模板中有很多静态节点连在一起,vue 会将连续的静态节点编译成一长串字符串,做到大量的静态节点预字符串化
  1. 在动态节点方面,Vue compiler 会识别动态节点中的变量,给节点打上 flag,如:
<div>{{ data }}</div>

// 给 div 打上 TEXT 标签,表示该节点中内容是动态的
function render() {
  _createBlock('div', .......) /* TEXT */
}

<div :class="class">{{ data }}</div>

// 给 div 打上 TEXT, CLASS 标签,表示该节点中内容和类名是动态的
function render() {
  _createBlock('div', .......) /* TEXT, CLASS */
}

以上一静一动的优化,大大的提升了 diff 算法的速度,使得 Vue3 在性能方面更进一步。

# 新增的API

  • setup
  1. setup 可以说是 Vue3 CompositionApi 中最为重要的一个更新,熟悉 React 的朋友都知道,在 React 中有 hooks,而 setup 就是 Vue 中的 hooks。
  2. setup 和 React hooks 最实用也是最常用的就是帮助我们在代码中实现逻辑关注点分离和逻辑复用,举一个最简单的例子:
/* 未使用 setup */
export default defineComponent({
  data() {
    return {
      count: 0
    }
  },
  methods: {
    add() {
      this.count += 1
    },
    subtract() {
      this.count -= 1
    }
  }
})

/* 使用 setup */

// count.ts
import { ref } from 'vue'

export default function useCount() {
  const count = ref(0)
  const add = () => count.value += 1
  const subtract = () => count.value -= 1

  return {
    count,
    add,
    subtract
  }
}

// MyComponent.vue
import { defineComponent } from 'vue'
import useCount from 'count'

export default defineComponent({
  setup() {
    const { count, add, subtract } = useCount()

    return {
      count // return 的属性和方法可以直接在模板中调用
    }
  }
})

不难看出,setup 体现出的是一种函数式编程的思想,这种思想其实是和 React hooks 大致相同的。计数器这个例子虽然小,但是我们可以想象下,如果有一个复杂的组件,代码上千行,通过 setup 实行逻辑关注点分离,在后期维护中我们可以很容易就定位到需要修改的代码位置。

  1. setup 在 vue 实例化的时候是早于 created 生命周期前执行的,因此在 setup 中无法使用 this,所以也就无法使用 data 或者 methods 中的属性或方法。
  2. setup 接受两个参数 propscontext
setup(props, context) {}

// 也可以用解构的方式写
setup(props, { attrs, slots, emit }) {

  // attrs 和 slots 不应该使用解构的方式调用,因为他们会随着组件的更新而更新,如果需要应该这两个属性的更新,请在 onUpdated 生命周期中调用

  // 需要注意的是,不能使用正常的方式解构 props,会让解构出来的数据失去响应性
  const { title } = props // ×

  // 需要使用 vue 提供的 toRefs 解构 props,是数据保持响应性
  const { title } = toRefs(props) // √

}
  1. setup 中的生命周期
// setup 中的生命周期就是在 option api 的生命周期前加上 on
setup() {

  // 值得注意的是,setup 里没有 beforeCreate 和 created 这两个生命周期,因为 setup 本身就是在这两个生命周期之间执行的,所以不需要显式的定义他们

  onBeforeMount(() => {})
  onMounted(() => {})
  onBeforeUpdate(() => {})
  onUpdated(() => {})
  onBeforeUnmount(() => {})
  onUnmounted(() => {})
  onErrorCaptured(() => {})
  onRenderTracked(() => {})
  onRenderTriggered(() => {})

}
  1. setup 中的 watch 和 watchEffect
// 首先是 watch,其行为和 vue2 中的 watch 没有太大的区别,只是在 setup 中写法更改了而已
// 监听单一数据源
const count = ref(0)
watch(count, (newVal, oldVal) => {})

const state = Reactive({ count: 0 })
watch(() => state.count, (newVal, oldVal) => {})

// 监听多数据源
watch([count0, count1], ([newVal0, newVal1], [oldVal0, oldVal1]) => {})

// 看完了 watch ,再来看看 watchEffect ,首先说说他们两者的区别
// 1. 无需显式指定监听的数据源,且无法监听数据源前后的值,只能监听数据源的更改做出相应的操作
// 2. 在组件初始化的时候就会执行一遍,而 watch 是惰性的,只有在数据源变更的时候才会执行
// 3. 返回一个回调,可以手动停止监听
// 4. 传入 watchEffect 的函数可以接受一个 onInvalidate 作为入参,可以在适当的时机清楚执行副作用
// 5. 接受选项配置,自定义在组件更新前后执行
const stopWatch = watchEffect(
  onInvalidate => {
    if (count.value > 5) {
      onInvalidate(() => console.log(count.value))
    }
  },
  {
    flush: 'post', // 默认是 pre,即在组件更新前执行,post 是组件更新后,sync 将执行时机变为同步执行
    // 这两项仅在开发时有效,都接受一个有关数据源的变更信息作为参数
    onTrack(e) {}, // 在数据源变更时调用
    onTrigger(e) {} // 在数据源变更导致执行回调的时候调用
  }
)

stopWatch()
  • Teleport

Teleport 中文翻译为传送,其实它与 React 中的 Portals 功能是类似的,就是将被包含的模板(DOM节点)渲染到指定的位置

// 将节点渲染到 body 下
<teleport to="body">
  <p>Hello World</P>
</teleport>

// 将节点渲染到 id="A" 的节点中
<teleport to="#A">
  <p>Hello World</P>
</teleport>

// 当多个模板同时渲染到同一个节点中时,会按照先后顺序插入
<teleport to="#A">
  <p>Hello</P>
</teleport>
<teleport to="#A">
  <p>World</P>
</teleport>

  • 多片段

在 Vue3 更新后,template 中再次支持了多个根节点的写法,即:

<template>
  <div>A</div>
  <div>B</div>
  <div>C</div>
</template>

为什么说是再次呢,因为在 远古的 Vue1.0时代,是支持了这样的写法的,但是在 Vue2.* 取消了。其中一个很关键的原因就是 Vue2 新增了 transition 组件,transition 是无法支持给多节点模板添加过渡的,如:

// children
<template>
  <div>A</div>
  <div>B</div>
  <div>C</div>
</template>

// father
<template>
  <transition>
    <child />
  </transition>
</template>

// 此时控制台会收到一条 warn
// Component inside <Transition> renders non-element root node that cannot be animated.

# 变动的API

  • new Vue
// vue2 中我们需要在 Vue 对象上挂在好我们需要的插件和各类配置,在最后才进行实例化
// 而 Vue3 中不再使用 new Vue() 的方式注册根实例,而是采用函数式编程的方式:createApp
// 几乎所有 Vue3 的 api 都采用了 ES6 的 Module 方式引入,这样可以有更好的 Tree-shaking,减少了构建体积

// vue2
import Vue from 'vue'
import ElementUI from 'element-ui'

Vue.use(ElementUI)
Vue.filter(/* */)
Vue.directive(/* */)
Vue.mixin(/* */)
Vue.config.** = /**/

/* 这时候如果实例化多个 Vue 实例,将会受到相同配置的影响 */
const app1 = new Vue({
  el: '#app1'
})
const app2 = new Vue({
  el: '#app2'
})

// Vue3
import { createApp } from 'vue'

const app = createApp({})
app.use(/**/)
  • 自定义全局api
// 以往我们在 vue2 中经常把一些常用的方法和属性挂在到 Vue 实例上,以便于在组件内快速的调用
// 现在 Vue3 针对这一方式做出了优化

// Vue3
const app = createApp()
/* 所有自定义属性和方法统一挂载在 config.globalProperties 上 */
app.config.globalProperties.$http = {}
  • directive
// Vue3 的指令中,生命周期同步为和组件的生命周期一致

const app = creatApp()

/* 所有生命周期参数均接收 el, binding, vnode, prevVnode */
app.directive('focus', {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {},
  unmounted() {}
})
  • 具名v-model
// 在以往 vue2 中,如果我们需要向自定义组件中传入多个同步的参数,往往会使用 .sync 绑定组件上的属性
// 而在 Vue3 中,我们可以使用具名的 v-model 绑定多个属性值

<my-component v-model:title="hello world" v-model:content="hello world" />

// MyComponent.vue
export default defineComponent({
  props: {
    title: String,
    content: String.
  }.
  methods: {
    changeTitle(value) {
      this.$emit('update:title', value)
    },
    changeContent(value) {
      this.$emit('update:content', value)
    }
  }
})
  • v-for 和 ref
// 先让我们看代码示例

// 以下是 vue2 中常用的两种在 v-for 中绑定节点的方式,这两种方式都使我们在代码里不那么容易的定位到我们需要的节点上
<div v-for="n in 10" :key="n" ref="divs"></div>
<div v-for="n in 10" :key="n" :ref="`divs-${n}`"></div>

// 而在 Vue3 中我们可以使用一个函数直接绑定到节点的 ref 属性上
<div v-for="n in 10" :key="n" :ref="setRefs"></div>

export default defineComponent({
  data() {
    return {
      els: []
    }
  },
  methods: {
    setRefs(el) {
      this.els.push(el)
    }
  }
})
  • 生命周期
// 直接对比新旧版本的生命周期

// vue2
export default {
  beforeCreate() {},
  created() {},
  beforeMounte() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeDestroy(){},
  destroy() {}
}

// Vue3
export default defineComponent({
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUmount(){},
  unmounted() {}
})
  • 异步组件
// 在 vue2 中定义一个异步组件,我们通常使用 import 函数或者是用 option api定义
const myAsyncComponent = () => import('./MyAsyncComponent.vue')
const myAsyncComponent = {
  component: () => import('./MyAsyncComponent.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

// 在 Vue3 中的异步组件都需要通过 defineAsyncComponent 来定义
import { defineAsyncComponent } from 'vue'

const myAsyncComponent = defineAsyncComponent(() => import('./myAsyncComponent.vue'))
const myAsyncComponent = defineAsyncComponent({
  loader: () => import('./myAsyncComponent.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

# 移除的API

  • sync 修饰符
// vue2 版本我们通常用 .sync 实现子组件和父组件的属性同步更新
<my-component :text.sync="myText" />

// Vue3 版本中移除了 .sync ,用命名的v-model代替
<my-component v-model:text="myText" />
  • 按键修饰符
  1. vue2 版本中可以使用数字的按键修饰符,在Vue3中已经被抛弃,请使用具名修饰符。
// vue2 数字按键修饰符
<my-component @keyup.13=”enter“ />

// Vue3 具名按键修饰符
<my-component @keyup.enter=”enter“ />
<my-component @keyup.scroll-lock=”enter“ />
  1. 同时移除了按键修饰符别名配置 config.keyCodes
  • $on $off $once
// 这三个 api 的移除对项目中影响最大的无疑是组件通讯 event bus,vue2 项目中,我们可以使用 event bus 实现组件间的通信,但是这在 Vue3 中已经不再被支持了

const Bus = new Vue()
Bus.$on('change', 'change info')
Bus.$once('change', 'change info')
Bus.$off('change')
  • $set
// 由于在 Vue3 中使用了 Proxy ,能够很好的监听对象的变化,如:属性的新增,所以 $set 在 Vue3 中就变得没有意义了

this.$set(data, {})
  • filter
// vue2 中我们一般使用 filter 过滤或者格式化文档中的数据,但实际上 filter 就是普通的函数,所以在 Vue3 中直接取消了对 filter 的支持,官方建议我们使用普通的函数对数据进行处理

// vue2
<div>{{ date | formatter }}</div>

filters: {
  formatter(value) {
    return moment(value).formatter('YYYY-MM-DD HH:mm:ss')
  }
}

// Vue3
<div>{{ formatter(date) }}</div>

methods: {
  formatter(value) {
    return moment(value).formatter('YYYY-MM-DD HH:mm:ss')
  }
}
  • $destroy $beforeDestroy
// Vue3 中将 $beforeDestroy 和 $destroy 这两个生命周期替换为 $beforeUnmount 和 $unmounted

// vue2
new Vue({
  beforeDestroy(){},
  destroy() {}
})

// Vue3
defineComponent({
  beforeUnmount(){},
  unmounted() {}
})
  • functional
// vue2 中我们可以讲 vue 实例声明为 functional ,显式的使用 render 函数渲染节点
// 而在 Vue3 中取消了这一做法,需要显式的导入 h 函数渲染节点

// vue2
new Vue({
  render(h, { props, data, children }) {
    return h('h1', {}, 'hello world!')
  }
})

// Vue3
import { h } from 'vue'

const myComponent = (props, { attrs, slots }) => {
  return h('h1', {}, 'hello world!')
}

# 实验性API

  • script setup

script setup 是 Vue3 提供的一种新型编程语法糖,目前在 3.1.* 版本上就可以尝鲜体验到该语法糖的便捷,下面来看看该新特性的使用方式吧

<script setup>
  import { 
    ref, 
    defineProps, 
    defineEmits, 
    useAttrs, 
    useSlots, 
    defineExpose 
  } from 'vue'

  // 导入的组件将被自动注册
  import MyComponent from 'MyComponent'

  // 在 script setup 中声明的任何变量,都会被默认 return ,供模板使用,
  // 但是,不使用 vue 提供的 ref 或 Reactive 声明变量的话,是没有响应性的,如:
  let data1 = 'hello'
  let data2 = ref('world')

  const setData1 =  () => {
    data1 = 'Hello'
  }

  const setData2 =  () => {
    data2.value = 'World'
  }

  // defineProps 可以声明传入该组件的 props 变量
  const props = defineProps({
    content: String
  })

  // defineEmits 定义 emit 的声明
  const emit = defineEmits(['click', 'change'])

  // useAttrs, useSlots 定义传入组件的属性和节点
  const attrs = useAttrs()
  const slots = useSlots()

  // defineExpose 可以将定义的变量暴露出去,供父组件调用
  defineExpose({ data2 })
</script>
  • style vars

style vars 是一种 js-in-css 的语法糖,它可以让我们在 style 标签中绑定 script setup 中声明的变量:

<script setup>
  import { ref } from 'vue'

  const color = ref('red')

  const setColor = () => {
    color.value = 'blue'
  }
</script>

<style vars>
.title {
  color: v-bind(color);
}
</style>

# 总结

以上是我对 Vue3 目前的认知和体验,Vue3 配合 Vite 的开发效率会比以往的提升不少,如果是 script setup 和 style vars 正式发布的话,会使我们的 Vue 代码更加的优雅。Vue3 的社区和配套轮子已经不断的在被完善当中,相信在不久的将来会很快的被广泛应用,先一步学习 Vue3 可以让我们在即将到来的技术更新浪潮中站稳脚跟,不用再说“学不动了”。