学习一波Vue3新特性

5,301 阅读5分钟

前言

去年前端界最轰动的事无非是React Hook的发布,上到react-routerreact-redux等生态库,下到React应用开发者都从class component开发方式积极拥抱Hook。那今年我认为最值得关注的是Vue3.0,目前我们可以从vue-next看到Vue3.0以及vuexvue-router等生态的开发进度,大概7、8月份Vue3.0正式版本就要落地,到时又会引起前端界的广泛讨论,这里我整理了一波Vue3的亮点,供大家参考。

Composition API

什么是composition api?先补一波英语知识,composition构成、作文的意思,动词是compose,看到compose估计很多人会想到koaredux等库的源码中有一个compose核心函数,这个函数像桥梁一样连接中间件(middleware)的调用,是构成这些优秀库中间件机制的关键!想想compose都那么牛,那composition api可以带给我们什么?能解决什么问题?这里我们从下面几个现象入手,理解composition api的设计理念。

  1. 现象一:当你维护前同事长达1000多行的祖传代码,为理解代码含义在不断的templatedatamethods等代码中痛苦跳转时

  2. 现象二:当你苦苦找不到this.xxx()方法定义,却不敢删掉,最终在各种mixin文件中找到它的真身时

这个时候真的需要composition api这样的开发方式来组织你的代码了。

功能分割

组合式 API 征求意见稿中的Vue CLI UI 文件浏览器为例,这个组件中有这样几个功能:

  • 追踪监听当前文件夹的状态并展示其中的内容

  • 处理文件夹的操作(打开、关闭、刷新...)

  • 处理新建文件夹的创建

  • 是否只展示收藏文件夹

  • 是否只展示隐藏文件夹

  • 处理当前工作目录的变化

就单个功能而言,它的代码所在位置比较分散(功能所需要的属性在dataprops中,处理数据的方法在methods中),这势必要求开发者在完成或阅读代码时上下反复跳转,同一个功能的代码不够聚合,那使用composition api要怎么写?继续引用组合式 API 征求意见稿创建新文件夹代码例子:

function useCreateFolder(openFolder) {
  // 原来的数据 property
  const showNewFolder = ref(false)
  const newFolderName = ref('')

  // 原来的计算属性
  const newFolderValid = computed(() => isValidMultiName(newFolderName.value))

  // 原来的一个方法
  async function createFolder() {
    if (!newFolderValid.value) return
    const result = await mutate({
      mutation: FOLDER_CREATE,
      variables: {
        name: newFolderName.value,
      },
    })
    openFolder(result.data.folderCreate.path)
    newFolderName.value = ''
    showNewFolder.value = false
  }

  return {
    showNewFolder,
    newFolderName,
    newFolderValid,
    createFolder,
  }
}

使用composition api有几个亮点

  • 整个函数就是一个功能
  • 函数包含创建新文件夹所依赖的数据和逻辑
  • 函数完全独立,功能可以复用

再通过一张图更直接的与options api进行对比:

这里相同的色块代表相同的功能,composition api(组合式api)让开发者就像搭积木一样将独立的功能一层一层组合成一个组件,所有的逻辑和功能都一目了然,但基于options api不能让我们这样组织代码,造成一个功能中逻辑的分散。

逻辑复用

当两个或多个组件的逻辑相同或相似时,在vue2.x中我们考虑用mixinHOC(高阶组件,Vue较少用到)slot插槽来做逻辑复用。但这几种方式都有各自的弊端:

  • 不知道代码引用来源
  • 与引入的组件属性或方法命名冲突
  • HOCslot需要额外的有状态的组件实例,从而使得性能有所损耗。

使用composition api能做到更清晰的逻辑复用,继续引用组合式 API 征求意见稿追踪鼠标位置的例子:

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

这里:

  • 所有的数据来源都非常清晰
  • 可以通过解构重命名,不存在命名冲突
  • 不再需要仅为逻辑复用而创建的组件实例

告别$set

Vue2.x使用Object.defineProperty拦截数据实现响应式系统,到了Vue3.0,响应式系统的核心api使用了Proxy。我们先看Vue2.x中的一个例子

let vm = new Vue({
  data() {
    return {
        name: 'jay'
    }
  }
})

vm.age = 20 // 并不会触发vue的响应系统

Vue2.x对新增属性是无感知的,依赖收集发生在初始化组件的过程中,Vue不能响应后来新增的属性,这是Object.defineProperty天生的特性:

const object1 = {};
// 只能对对象上特定属性做数据拦截
Object.defineProperty(object1, 'property1', {
  get() {
    return object1['property1'];
  }
  set(value) {
    object1['property1'] = value;
  }
});

Vue2.x为了让新增的数据具有响应性,加入$set api来兼容无法响应新增属性的情况。对于Proxy api,它天生能够拦截所有的属性。

const object1 = {};

let p = new Proxy(object1, {
	get(target, prop, receiver) {
		
	},
	set(target, prop, value) {
		console.log(target);  // {}
		console.log(prop);    // property1
		console.log(value);   // 1
		target[prop] = value;
	}
})
// 能拦截到新增的属性
p.property1 = 1;

Object.definePropertyProxy的区别:

  • Object.defineProperty针对对象上特定属性(不能拦截新增属性),Proxy针对handler对象(不论你是否为新增属性)

  • Proxy除了getset外还有其他多种操作符

  • Proxy不兼容IE 11,相当于IE家族不能使用Vue3.0的应用了(也许未来Vue或社区有优雅降级的方案)

有了Proxy,就不用考虑新增属性的响应行为了,是时候要跟$set说声再见了👋。

VDom的性能优化

尤大在此前直播中表示,Vue3.0在性能优化后VDom的更新(计算diff)性能提升1.3-2倍,SSR的速度提升了2-3倍。来看看Vue3VDom优化实现。

静态标记

<div id="app">
    <h1>我来组成静态节点</h1>
    <p>{{name}}</p>
</div>

可以在这里查看编译结果:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("h1", null, "我来组成静态节点"),
    _createVNode("p", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}

// Check the console for the AST

这里h1是静态节点,p为动态节点,在Vue2.x中若name发生变化,整个模板都需要重新渲染一遍,Vue3.0p标签vdom多传了参数1 /* TEXT */,意味着在diff时略过静态节点,只追踪有这参数的动态节点的更新,Vue3.0源码中还有其他类型的标记位:

export const enum PatchFlags {
  TEXT = 1,// 表示具有动态textContent的元素
  CLASS = 1 << 1,  // 表示有动态Class的元素
  STYLE = 1 << 2,  // 表示动态样式(静态如style="color: red",也会提升至动态)
  PROPS = 1 << 3,  // 表示具有非类/样式动态道具的元素。
  FULL_PROPS = 1 << 4,  // 表示带有动态键的道具的元素,与上面三种相斥
  HYDRATE_EVENTS = 1 << 5,  // 表示带有事件监听器的元素
  STABLE_FRAGMENT = 1 << 6,   // 表示其子顺序不变的片段(没懂)。 
  KEYED_FRAGMENT = 1 << 7, // 表示带有键控或部分键控子元素的片段。
  UNKEYED_FRAGMENT = 1 << 8, // 表示带有无key绑定的片段
  NEED_PATCH = 1 << 9,   // 表示只需要非属性补丁的元素,例如ref或hooks
  DYNAMIC_SLOTS = 1 << 10,  // 表示具有动态插槽的元素
}

静态节点提升

<div id="app">
    <h1>我来组成静态节点</h1>
    <p>{{name}}</p>
</div>

编译结果:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("h1", null, "我来组成静态节点", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", _hoisted_1, [
    _hoisted_2,
    _createVNode("p", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}

// Check the console for the AST

使用静态提升时,所有静态节点被提升到render方法外,这表明这些节点只会在初始化中创建一次,在更新时进行复用。

添加事件缓存

<div id="app">
    <p @click="onClick">hello</p>
</div>

编译结果:

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", { id: "app" }, [
    _createVNode("p", {
      onClick: _cache[1] || (_cache[1] = ($event, ...args) => (_ctx.onClick($event, ...args)))
    }, "hello")
  ]))
}

// Check the console for the AST

编译后会产生一个内联函数,使用内联函数对绑定事件做缓存,更新时事件处理器没有发生变化,则这个节点被认为一个静态节点。

Typescript

Vue2.x是基于options(选项)的框架,也就是用object去编写组件,但typescript是类型检查能力范围限于class/function,因此Vue2.x中我们使用Vue + typescript + vue-class-component + vue-property-decorator 的方式开发Vue应用。但typescriptVue3.0源码中占比90%以上,Vue3.0将对tsxclass component等有更好的支持。

Tree Shaking

Vue3.0做到了按需引入,更好支持tree shaking,有时候并不需要Vue全部的功能,打包时可以将无用的代码剪掉

Fragment

Vue3.0支持模板添加多个根节点,意味着render函数也可以返回数组了

结束

好了,Vue3.0的新特性还是很多的,当然该学的还是得学,别做Vue2.x钉子户了🐶