Vue 3.0 新特性 预览

1,008 阅读6分钟

新特性总结

  • 全局API和内置组件/功能支持tree-shaking
  • 常驻代码大小控制在10kb gzipped上下
  • 编译器架构重构,更多的编译时优化
  • 添加TypeScript支持,使外部贡献者更有信心做改动
  • 代码采用monorepo结构,内部分层更清晰

一、使Vue更快(性能优化)

基于Proxy的响应式实现,性能整体优于Object.defineProperty

长远来看,JS引擎会继续优化Proxy,但Object.defineProperty(getter/setter)基本不会再有针对性的优化

Virtual DOM 重构

2.x版本的VDOM更新
  • Vue虽然能够保证触发组件更新的最小化,但在单个组件内部依然需要遍历该组件的整个VDOM树
  • 但如果碰到下列只有少量动态节点时,这些遍历都是性能的浪费
  • 因此:传统的VDOM的性能与模板带下正相关,与动态节点数目无关
3.x 动静分离优化,提示更新效率

React 解决方法: 时间分片

  • 通过模板静态分享生成更优化的VDOM渲染函数

  • 先将模板切分为block,每个block内部动态节点位置都是固定的

  • 每个block的根节点会记录自己所包含的动态节点(包含子block的根节点)

  • 更新时,只需要直接遍历动态节点

  • block Tree

  • 最简单情况

  • v-if情况

  • 优化效果

    • 新策略将VDOM更新性能与模板大小解耦,变为与动态节点数目相关
    • 整体性能提升2~5倍

二、Function-based API

单个函数内部的变量会被压缩,对压缩更友好

替换Class API

  • class api(vue-property-derocator和vue-class-component) 对vue而已只是扩展了对TypeScript和与React类似的装饰器写法,其余并没有其他的优点,
  • Derocator提案严重不稳定是的依赖他的方案具有很大风险
  • Props和其他需要注入到this的属性导致类型声明存在很大问题

逻辑复用

  • Mixin
    • 混入的属性来源不清楚
    • 命名空间冲突
  • 高阶组件(HOC)
    • Props来源不清楚
    • Props命名空间冲突
    • 多余的组件实例造成性能浪费
  • Scoped Slots
    • 来源清楚
    • 无命名空间冲突
    • 多余的组件造成性能浪费
  • Composition Functions (Function-based API提供)
    • 即函数组合
    • 无额外实例开销

优点

  • 更灵活的逻辑复用能力
  • 更好的TypeScript类型推导支持
  • 更好的性能
  • Tree-shaking优化
    • 基于函数的 API 每一个函数都可以作为 named ES export 被单独引入,这使得它们对 tree-shaking 非常友好。没有被使用的 API 的相关代码可以在最终打包时被移除
  • 代码容易压缩
    • 所有的函数名和 setup 函数体内部的变量名都可以被压缩,但对象和 class 的属性/方法名却不可以

使用Function-based API

官方提供了一个vue-function-api依赖库,以便vue2.x也可以使用function api编程,不过它只有Function-based API的setup、value、computed、watch、lifecycle、provide、inject等部分API

// npm install vue-function-api --save
// 在2.x中使用
import Vue from 'vue'
import { plugin } from 'vue-function-api'
Vue.use(plugin)
setup函数

是Function-Based API的入口函数,类似data(),setup()的返回值就是注入页面模板的变量,可以像data一样直接使用

import { value } from 'vue'
function useMousePosition() { // 组件内容复用 Composition Function
const x = value(0) const y = value(0) const update = e => {
        x.value = e.pageX
       y.value = e.pageY
    }
    onMounted(() => {
       window.addEventListener('mousemove', update)
    })
    onUnmounted(() => {
       window.removeEventListener('mousemove', update)
    })
    return { x, y }
}
const MyComponent = {
 setup(props) {
   const msg = value('hello')
   const {x, y} = useMousePosition()
   const appendName = () => {
     msg.value = `hello ${props.name}`
   }
   return {
     msg,
     appendName
   }
 },
 template: `<div @click="appendName">{{ msg }}</div>`
}
  • 函数API配合Render实现
    • 如果你的组件不使用模版,你也可以选择在 setup() 中直接返回一个渲染函数
import { value, createElement as h } from 'vue'
const MyComponent = {
  setup(initialProps) {
    const count = value(0)
    const increment = () => { count.value++ }

    return (props, slots, attrs, vnode) => (
      h('button', {
        onClick: increment
      }, count.value)
    )
  }
} 
value()返回的是一个value wrapper(包装对象)
  • 一个包装对象只有一个属性.value,指向内部被包装的值,该值可以被修改

  • 为什么要使用

    • 我们知道在 JavaScript 中,原始值类型如 string 和 number 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。
    • 因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器
  • Value Unwrapping(包装对象的自动展开)

    • 当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值
const MyComponent = {
  setup() {
    return {
      count: value(0)
    }
  },
  template: `<button @click="count++">{{ count }}</button>`
}
生命周期函数

所有现有的生命周期钩子都会有对应的 onXXX 函数(只能在 setup() 中使用):

import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    // destroyed 调整为 unmounted
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}
Computed Value (计算值)

计算值的行为跟计算属性 (computed property) 一样:只有当依赖变化的时候它才会被重新计算。 computed(getter, setter) 返回的是一个只读的包装对象,它可以和普通的包装对象一样在 setup() 中被返回 ,也一样会在渲染上下文中被自动展开。默认情况下,如果用户试图去修改一个只读包装对象,会触发警告

import { value, computed } from 'vue'
const count = value(0)
const countPlusOne = computed(() => count.value + 1)
console.log(countPlusOne.value) // 1
count.value++
console.log(countPlusOne.value) // 2
Watchers
  • watch() API 提供了基于观察状态的变化来执行副作用的能力。
  • watch() 接收的第一个参数被称作 “数据源”,它可以是:
    • 一个返回任意值的函数
    • 一个包装对象
    • 一个包含上述两种数据源的数组
  • 第二个参数是回调函数。回调函数只有当数据源发生变动时才会被触发
  • 观察的对象可以是:Props,包装对象等
// double 是一个计算包装对象
const double = computed(() => count.value * 2)
watch(double, value => {
  console.log('double the count is: ', value)
}) // -> double the count is: 0
count.value++ // -> double the count is: 2
  • 停止观察
const stop = watch(...)
// stop watching
stop()

// 如果 watch() 是在一个组件的 setup() 或是生命周期函数中被调用的,那么该 watcher 会在当前组件被销毁时也一同被自动停止:
export default {
  setup() {
    // 组件销毁时也会被自动停止
    watch(/* ... */)
  }
}
纯 TypeScript 的 Props 类型声明

3.0 的 props 选项不是必须的,如果你不需要运行时的 props 类型检查,你也可以选择完全在 TypeScript 的类型层面声明 props 的类型:

import { createComponent, createElement as h } from 'vue'
interface Props {
  msg: string
}
const MyComponent = createComponent({
  setup(props: Props) {
    return () => h('div', props.msg)
  }
})
类型推导

createComponent 从概念上来说和 2.x 的 Vue.extend 是一样的,但在 3.0 中它其实是单纯为了类型推导而存在的,内部实现是个 noop(直接返回参数本身)。它的返回类型可以用于 TSX 和 Vetur 的模版自动补全。如果你使用单文件组件,则 Vetur 可以自动隐式地帮你添加这个调用

import { createComponent } from 'vue'
const MyComponent = createComponent({
  // props declarations are used to infer prop types
  props: {
    msg: String
  },
  setup(props) {
    props.msg // string | undefined

    // bindings returned from setup() can be used for type inference
    // in templates
    const count = value(0)
    return {
      count
    }
  }
})

参考文献