关于vue3你需要知道的事

4,489 阅读7分钟

1. 写在前面的话

vue3在今年内应该就会推出,尤大之前也针对vue3的设计和api给出了RFC和最新征求意见稿。本人阅读后总结了一些个人心得,希望对大家有用。

2. Vue3 Function-based API RFC

vue2在近几年不太长的生命周期内,经历了大小各种项目的历练,可以说非常易于上手,方便开发。同时也暴露了不少问题亟待解决:

  1. 组件之间难以逻辑组合与复用
  2. vue2缺少类型推导,即typescript支持
  3. 打包尺寸较大,vue2打包时会将所有api的核心代码打包,不论其在开发中是否有用到
  4. definePropterty()实现的数据绑定无法实现数据对象新增属性值的变化,且vue中的数组方法也是自己polyfill实现的

vue3基于上述弊端重新设计了内部的实现方式,并且同时重新设计了组件选项和api。主要有:

重新设计了响应式数据对象,内部实现从defineProperty改为由浏览器原生支持的proxy实现。包括手动定义响应式数据(reactive)和包装对象(ref)两种api方式创建响应式数据。

  • 将数据变为响应式数据,可以追踪数据的变化,包括数组和对象的属性变化
  • 包装对象数据以变量的形式引用时(使用/修改)需要以a.value这种方式

示例代码:

// reactive响应式数据
import { reactive } from 'vue'
const state = reactive({
  count: 0
})

function increment() {
  state.count ++
}
// ref包装对象
import { ref } from 'vue'
const count = ref(0);

// 以变量的形式引用包装对象
function increment() {
  count.value ++
}

新增了watchEffect api,和vue2中的watch效果类似,监测响应式数据变化并执行副作用函数。

  • watchEffect将会在创建的时候立即执行一次,之后会在renderer flush即DOM更新后再执行回调函数,组件销毁时watch停止观察。
  • watchEffect不需要将被依赖的数据源和副作用回调函数分开,只需写出副作用函数,即可自动将副作用中的所有响应式状态的 property 作为依赖进行追踪
import { reactive, ref, watchEffect } from 'vue'

const state = reactive({
  count: 0,
})
const titleCount = ref(0);

// 当state.count、titleCount变化时,watchEffect将会自动执行副作用回调函数
watchEffect(() => {
  document.body.innerHTML = `count is ${state.count}`
  document.title = `title count is ${titleCount}`
});

重新设计了computed api,将会创建一个依赖其它状态的响应式状态。被依赖的状态可以是由reactive或ref创建响应式数据,生成的状态则是一个只读的包装对象,以变量的形式使用时需要以.value这种方式。

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

const state = reactive({
  count: 0,
})
const num = ref(0);

const double = computed(() => state.count * 2)
// 使用ref创建响应式对象时需要用.value取值计算
const triple = computed(() => num.value * 3)

watchEffect(() => {
  console.log(double.value) // -> 0
  console.log(triple.value) // -> 0
})

state.count++ // -> 2
num.value++ // -> 3

新设计了setup()组件选项,代替之前的data组件选项。这个函数是组件开始逻辑的地方,它接收组件的props属性作为参数,然后开始调用,整个组件生命周期内只会执行一次。

  • 在setup中创建响应式数据对象,并return出去暴露给模板的上下文使用
  • setup可以将函数方法返回出去,给模板使用
  • setup接收的组件属性props本身也是个可追踪的响应式数据
  • vue2中的生命周期经过重命名,只在setup中执行
import { ref } from 'vue'

const MyComponent = {
  setup(props) {
    const msg = ref('hello')
    const appendName = () => {
      msg.value = `hello ${props.name}`
    }
    return {
      msg,
      appendName
    }
  },
  template: `<div @click="appendName">{{ msg }}</div>`
}

类型推导,需要使用defineComponent函数定义组件才能支持typescript类型推导

import { defineComponent, ref } from 'vue'

const MyComponent = defineComponent({
  // props选项声明用来定义prop属性类型
  props: {
    msg: String
  },
  setup(props) {
    console.log(props.msg) // string 或 undefined

    // setup中返回的数据对象可以在模板中用于类型推断推断类型
    const count = ref(0)
    return {
      count
    }
  }
})

在了解了上面的一些基本api后,我们可以看一下尤大给出的一个基本组件写法:

import { ref, computed, watchEffect, onMounted } from 'vue'

const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  setup() {
    // reactive state
    const count = ref(0)
    // computed state
    const plusOne = computed(() => count.value + 1)
    // method
    const increment = () => { count.value++ }
    // watch
    watchEffect(() => count.value * 2, val => {
      console.log(`count * 2 is ${val}`)
    })
    // lifecycle
    onMounted(() => {
      console.log(`mounted`)
    })
    // expose bindings on render context
    return {
      count,
      plusOne,
      increment
    }
  }
}

这里只介绍了少数比较重要的vue3 api,而且不确定最终api还会不会再改动,如果想要详细查询学习的同学可以去查看官方api文档

3. vue3对比react hooks

尤大亲自说过vue3的最新设计很大程度上是借鉴了react hooks的思路。具体相似的地方至少有:

  1. 定义数据的写法方式类似,react使用useState,vue3使用ref,创建响应式数据
  2. setup设计借鉴了react hooks,抽取函数和复用逻辑的方式类似,将逻辑和数据抽取成为独立的函数进行复用。

官方文档也给出了一个将获取鼠标位置的功能抽取成独立函数的实例:

// 抽取获取鼠标x, y位置的成函数 mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', update)
  })

  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return { x, y }
}
import { useMousePosition } from './mouse'

export default {
  setup() {
    const { x, y } = useMousePosition()
    // 其他逻辑...
    return { x, y }
  },
}

如果是写过react hooks的同学会发现,这与react hooks抽取公用函数的写法如出一辙。换句话说,写过两者其一的话,都有助于我们理解另一个框架。

虽然vue3的写法类似于react hooks,但是其内部实现思路是不同的:

  1. react hooks函数组件每次渲染的时候会重新执行一遍,而vue3的setup只会在组件初始化的时候执行一次。
  2. react hooks的useState创建组件状态值,处在一个独立的闭包空间内,每次使用的时候从这个独立空间重新获取值。而vue3的包装对象则是使用proxy将数据对象变成可追踪的响应式数据,这个思路跟vue2是一样的。

vue3的设计在尤大看来至少有一下优势:

  1. 整体上更符合 JavaScript 的直觉
  2. 不受调用顺序的限制,可以有条件地被调用
  3. 不会在后续更新时不断产生大量的内联函数而影响引擎优化或是导致 GC 压力
  4. 不需要总是使用useCallback来缓存子组件的回调防止过度更新
  5. 不需要关注useEffect/useMemo/useCallback的deps依赖数组是否正确的问题

4. 新api所带来的心智负担

在阅读api设计文档时确实发现一些容易让开发者搞晕的东西,尤大对此也进行了总结。对于我个人来说有以下比较麻烦的地方:

  1. 使用ref创建的响应式数据难以区分什么时候需要使用.value方式访问
  2. 难以区分什么时候该使用reactive,什么时候该用ref创建响应式数据
  3. 写法不正确时,使用reactive创建的响应式数据有丢失响应式的可能
  4. 对setup赋予的职责过大,且创建响应式数据和响应函数的逻辑十分灵活,几乎没有限制。会导致不同开发者写出来的代码可读性差距极大。

我个人希望vue3正式推出的时候,文档可能提供api使用推荐写法,同时能提供相应的lint校验插件辅助,这样才好让不同开发者写出通用可读的代码。

5. vue3的新一代构建工具vite

随着vue3而来的构建工具不是webpack,而是vite一种新型的项目构建工具。vite作用类似webpack,但是原理与webpack不同。对比webpack有以下优势:

  1. 快读冷启动
  2. 瞬间热更新
  3. 真正的按需编译

我也研究了一下vite的技术原理:

  1. 浏览器原生支持(ie除外)的ESM(script module)api,通过在script标签中添加type="module"就可以在代码中使用export import语法,通过浏览器端直接导入、导出模块使用。
  2. ESM的原理是浏览器将import导入的模块转换成http请求加载对应的资源,需要什么模块就加载对应的模块。
  3. vite将开启一个本地的koa服务,劫持ESM中由import导入模块时的http请求,所以vite与webpack不同,不会对代码中所需要的模块进行静态分析并抽取成bundle。
  4. vite劫持后import导入的模块的http请求后,判断请求文件类型,对不同的模块文件使用不同的方法进行解析处理,如对vue文件,使用的就是vue的自带的模板解析/css解析/等功能,对css文件/ts文件等也是有相应的语法解析工具进行处理。

6. 给计划使用vue3同学的一点建议

  1. 可以对比性的使用react hooks试试看,目前react hooks也相对比较成熟了。
  2. 建议学习typescript,可以的项目可以使用typescript开发试试。

作者简介: 宫晨光,人和未来大数据前端工程师。