Curtis' Spot

一文看全 Vue 3.X 带来的新变化

昨天(2020-07-18),Vue 宣布 3.X 版本正式进入 RC阶段,进入 RC 阶段意味着 Vue 3.x 的核心实现已经趋于稳定,原则上在最终发布前不会再引入新的主要特性和破坏性更改,所以现在正是开始学习 Vue 3.x 的最好时机。

在通读了 Vue 3.x 最新的官方文档后,我总结了一下 Vue 3.x 对于 Vue 2.x 的一些主要变化,分别从新功能、破坏性更改和废弃特性三个方面进行汇总,方便在开发基于 Vue 3.x 新版本应用的时候进行快速查阅。可点击右侧 TOC 导航栏进行快速查看(PC 端)。

NEW | 新增功能特性

基础响应式 API Reactivity

变化描述:
  • 新增reactive全局 API,调用后返回一个响应式的proxy对象;
  • 新增readonly全局 API,调用后根据源对象返回一个只读的proxy对象,如果源对象是响应式的,源对象发生变化时会同步变化;
  • 新增isProxy全局 API,检查指定对象是否由reactive或者readonly创建;
  • 新增isReactive全局 API,检查指定对象是否由reactive创建(经readonly创建的reactive值认为 true,e.g.,isReactive(readonly(reactive({}))) === true);
  • 新增isReadonly全局 API,检查指定对象是否由readonly创建;
  • 新增toRaw全局 API,调用后返回一个reactive对象的原始对象;
  • 新增markRaw全局 API,调用后返回一个不可被reactive将其作为源的对象;
  • 新增shallowReactive全局 API,浅reactive,只将对象的顶层reactive
  • 新增shallowReadonly全局 API,浅readonly,只将对象的顶层readonly
代码示例:

reactive

调用后返回一个响应式的proxy对象。

const obj = reactive({ count: 0 });

readonly

调用后根据源对象返回一个只读的proxy对象,如果源对象是响应式的,源对象发生变化时会同步变化。

const original = reactive({ count: 0 });

const copy = readonly(original);

watchEffect(() => {
  // works for reactivity tracking
  console.log(copy.count);
});

// mutating original will trigger watchers relying on the copy
original.count++;

// mutating the copy will fail and result in a warning
copy.count++; // warning!

isProxy

检查指定对象是否由reactive或者readonly创建。

const rt = reactive({ count: 0 });
const rd = readonly(rt);
const plainObj = {};

isProxy(rt); // true
isProxy(rd); // true
isProxy(plainObj); // false

isReactive

检查指定对象是否由reactive创建(经readonly创建的reactive值认为 true,e.g.,isReactive(readonly(reactive({}))) === true)。

import { reactive, isReactive, readonly } from 'vue';
export default {
  setup() {
    const state = reactive({
      name: 'John',
    });
    // 由普通对象创建的 readonly proxy
    const plain = readonly({
      name: 'Mary',
    });
    console.log(isReactive(plain)); // -> false

    // 由 reactive proxy 创建的 readonly proxy
    const stateCopy = readonly(state);
    console.log(isReactive(stateCopy)); // -> true
  },
};

isReadonly

检查指定对象是否由readonly创建。

const rt = reactive({ count: 0 });
const rd = readonly(rt);
const plainObj = {};

isReadonly(rt); // false
isReadonly(rd); // true
isReadonly(plainObj); // false

toRaw

调用后返回一个reactive对象的原始对象。

const foo = {};
const reactiveFoo = reactive(foo);

console.log(toRaw(reactiveFoo) === foo); // true

markRaw

调用后返回一个不可被reactive将其作为源的对象。

const foo = markRaw({});
console.log(isReactive(reactive(foo))); // false

// 嵌套 reactive 也可使用
const bar = reactive({ foo });
console.log(isReactive(bar.foo)); // false

// 被 markRaw 对象的嵌套对象不受影响
const baz = markRaw({
  nested: {},
});
const qux = reactive({
  // 即使 `baz` 被标记为 raw, 但是 baz.nested 不受影响.
  nested: baz.nested,
});

console.log(baz.nested === qux.nested); // false

shallowReactive

reactive,只将对象的顶层reactive

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2,
  },
});

// 改变 state 自身的属性是响应式的
state.foo++;
// ...但是深层嵌套对象不是响应式的
isReactive(state.nested); // false
state.nested.bar++; // non-reactive

shallowReadonly

readonly,只将对象的顶层readonly

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2,
  },
});

// 不可以改变 state 自身的属性值
state.foo++;
// ...但是深层嵌套对象的属性值可被改变
isReadonly(state.nested); // false
state.nested.bar++; // works

Refs

变化描述:
  • 新增ref全局 API,调用后返回一个基础值的响应式对象,该对象只有一个 value 固定值。当源值不是基础值(即对象)时,会静默地对该对象调用reactive
  • 新增unref全局 API,调用后返回一个ref值的 value 值,这个 API 是val = isRef(val) ? val.value : val的语法糖;
  • 新增toRef全局 API,调用后返回一个reactive对象的指定属性作为ref,该refreactive的原值互相影响;
  • 新增toRefs全局 API,调用后返回一个reactive对象的所有属性的ref集合,该ref集合的每一个属性值(即每一个 ref)与reactive的原值相互影响;
  • 新增isRef全局 API,检查指定对象是否ref
  • 新增customRef全局 API,创建自定义的ref用于细粒度的控制依赖收集和触发,需要提供一个工厂函数,该工厂函数接受参数分别为tracktrigger两个参数,返回值必须是带getset方法的对象;
  • 新增shallowRef全局 API,浅ref,因为对非基础值调用ref会隐式调用reactive,相当于对ref的 value 值调用shallowReactive
  • 新增triggerRef全局 API,改变由shallowRef创建的ref值后,手动触发ref更新以驱动computedwatch等逻辑;
代码示例:

ref

调用后返回一个基础值的响应式对象,该对象只有一个 value 固定值。当源值不是基础值(即对象)时,会静默地对该对象调用reactive

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

count.value++;
console.log(count.value); // 1

unref

调用后返回一个ref值的 value 值,这个 API 是val = isRef(val) ? val.value : val的语法糖。

function useFoo(x: number | Ref<number>) {
  const unwrapped = unref(x); // unwrapped is guaranteed to be number now
}

toRef

调用后返回一个reactive对象的指定属性作为ref,该refreactive的原值互相影响。

const state = reactive({
  foo: 1,
  bar: 2,
});

const fooRef = toRef(state, 'foo');

fooRef.value++;
console.log(state.foo); // 2

state.foo++;
console.log(fooRef.value); // 3

toRefs

调用后返回一个reactive对象的所有属性的ref集合,该ref集合的每一个属性值(即每一个 ref)与reactive的原值相互影响。

const state = reactive({
  foo: 1,
  bar: 2,
});

const stateAsRefs = toRefs(state);
/*
Type of stateAsRefs:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// The ref and the original property is "linked"
state.foo++;
console.log(stateAsRefs.foo.value); // 2

stateAsRefs.foo.value++;
console.log(state.foo); // 3

isRef

检查指定对象是否ref

const count = ref(0);
const sum = 0;

isRef(count); // true
isRef(sum); // false

customRef

创建自定义的ref用于细粒度的控制依赖收集和触发,需要提供一个工厂函数,该工厂函数接受参数分别为tracktrigger两个参数,返回值必须是带getset方法的对象。

<input v-model="text" />
function useDebouncedRef(value, delay = 200) {
  let timeout;
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          value = newValue;
          trigger();
        }, delay);
      },
    };
  });
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello'),
    };
  },
};

shallowRef

因为对非基础值调用ref会隐式调用reactive,相当于对ref的 value 值调用shallowReactive

const foo = shallowRef({});
// mutating the ref's value is reactive
foo.value = {};
// but the value will not be converted.
isReactive(foo.value); // false

triggerRef

新增triggerRef全局 API,改变由shallowRef创建的ref值后,手动触发ref更新以驱动computedwatch等逻辑。

const shallow = shallowRef({
  greet: 'Hello, world',
});

// Logs "Hello, world" once for the first run-through
watchEffect(() => {
  console.log(shallow.value.greet);
});

// This won't trigger the effect because the ref is shallow
shallow.value.greet = 'Hello, universe';

// Logs "Hello, universe"
triggerRef(shallow);

Computed

变化描述:
  • 新增computed全局 API,调用该 API 创建动态计算值时,需要提供一个计算函数或者带有getset函数的对象字面量;
代码示例:

类型注解:

// read-only
function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>

// writable
function computed<T>(options: { get: () => T; set: (value: T) => void }): Ref<T>

使用计算函数的情况:

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

console.log(plusOne.value); // 2

plusOne.value++; // error

使用对象字面量的情况:

const count = ref(1);
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1;
  },
});

plusOne.value = 1;
console.log(count.value); // 0

WatchEffect

变化描述:
  • 新增watchEffect全局 API,调用该函数侦听变化时,会立即执行一次;
  • 调用该函数后返回stop函数,调用stop后停止侦听;
  • watchEffect的第一个参数为 handle 函数,该 handle 函数接收onInvalidate函数,onInvalidatewatchEffect被重新触发或者被终止时触发;
  • watchEffect的第二个参数为侦听选项,值为对象字面量,
    • 选项flush,用于控制watchEffect的执行时机:
      pre  -> 在组件更新前运行
      sync -> 在组件更新时同步运行
      post -> 在组件更新后运行(默认)
      
    • 选项onTrackwatchEffect依赖收集时调用,用于 debugger,只在开发模式(development)可用;
    • 选项onTriggerwatchEffect重新运行时调用,用于 debugger,只在开发模式(development)可用;
代码示例:

类型注解:

function watchEffect(
  effect: (onInvalidate: InvalidateCbRegistrator) => void,
  options?: WatchEffectOptions
): StopHandle

interface WatchEffectOptions {
  flush?: 'pre' | 'post' | 'sync'
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}

interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}

type InvalidateCbRegistrator = (invalidate: () => void) => void

type StopHandle = () => void

默认使用方式:

const count = ref(0);

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

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

// later 手动结束 watchEffect
setTimeout(() => {
  stop();
}, 200);

同步 Invalidation sync:

watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value);
  onInvalidate(() => {
    // id 发生了变化或者 watcher 被终止
    // 手动终止上一个未结束的异步操作
    token.cancel();
  });
});

异步 Invalidation async:

const data = ref(null)
watchEffect(async onInvalidate => {
  onInvalidate(() => {...}) // 在异步函数 resolve 前注册清理函数
  data.value = await fetchData(props.id)
})

参数选项:

watchEffect(
  () => {
    /* side effect */
  },
  {
    flush: 'pre', // 改变执行时机
    // 收集依赖时 debug
    onTrack(e) {
      debugger;
    },
    // 被触发时 debug
    onTrigger(e) {
      debugger;
    },
  },
);

Watch

变化描述:
  • 新增watch全局 API,调用该函数侦听变化时,默认执行方式为lazy
  • 可同时侦听多个属性;
  • watch的第二个参数为 handle 函数,该 handle 函数接收onInvalidate函数,onInvalidatewatch被重新触发或者被终止时触发;
  • watch的第三个参数为侦听选项,值为对象字面量,
    • 选项flush,用于控制watch的执行时机:
      pre  -> 在组件更新前运行
      sync -> 在组件更新时同步运行
      post -> 在组件更新后运行(默认)
      
    • 选项onTrackwatch依赖收集时调用,用于 debugger,只在开发模式(development)可用;
    • 选项onTriggerwatch重新运行时调用,用于 debugger,只在开发模式(development)可用;
代码示例:

类型注解:

// 侦听单个属性源
function watch<T>(
  source: WatcherSource<T>,
  callback: (
    value: T,
    oldValue: T,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options?: WatchOptions
): StopHandle

// 侦听多个属性源
function watch<T extends WatcherSource<unknown>[]>(
  sources: T
  callback: (
    values: MapSources<T>,
    oldValues: MapSources<T>,
    onInvalidate: InvalidateCbRegistrator
  ) => void,
  options? : WatchOptions
): StopHandle

type WatcherSource<T> = Ref<T> | (() => T)

type MapSources<T> = {
  [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}

// 从 `watchEffect` 类型扩展
interface WatchOptions extends WatchEffectOptions {
  immediate?: boolean // default: false
  deep?: boolean
  // flush?: 'pre' | 'post' | 'sync'
  // onTrack?: (event: DebuggerEvent) => void
  // onTrigger?: (event: DebuggerEvent) => void
}

interface DebuggerEvent {
  effect: ReactiveEffect
  target: any
  type: OperationTypes
  key: string | symbol | undefined
}

type InvalidateCbRegistrator = (invalidate: () => void) => void

type StopHandle = () => void

侦听单属性:

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

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

// later 手动结束 watch
setTimeout(() => {
  stop();
}, 200);

侦听多属性:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
});

同步 Invalidation sync:

const data = ref(null);
watch(data, (data, prevData, onInvalidate) => {
  const token = performAsyncOperation(data.value);
  onInvalidate(() => {
    // id 发生了变化或者 watcher 被终止
    // 手动终止上一个未结束的异步操作
    token.cancel();
  });
});

异步 Invalidation async:

const data = ref(null);
watch(data, async (data, prevData, onInvalidate) => {
  onInvalidate(() => {...}); // 在异步函数 resolve 前注册清理函数
  data.value = await fetchData(props.id);
})

参数选项:

const data = ref(null);
watch(
  data,
  (data, prevData) => {
    /* side effect */
  },
  {
    immediate: true, // 立即执行
    deep: true, // 深度侦听
    flush: 'pre', // 改变执行时机
    // 收集依赖时 debug
    onTrack(e) {
      debugger;
    },
    // 被触发时 debug
    onTrigger(e) {
      debugger;
    },
  },
);

setup

变化描述:
  • 添加setup组件选项,在组件创建前被调用,接收propscontext两个参数;
代码示例:

类型注解:

interface Data {
  [key: string]: unknown;
}

interface SetupContext {
  attrs: Data;
  slots: Slots;
  emit: (event: string, ...args: unknown[]) => void;
}

function setup(props: Data, context: SetupContext): Data;

使用<template>的方式:

<!-- MyBook.vue -->
<template>
  <div>{{ readersNumber }} {{ book.title }}</div>
</template>

<script>
  import { ref, reactive } from 'vue';

  export default {
    setup() {
      const readersNumber = ref(0);
      const book = reactive({ title: 'Vue 3 Guide' });

      // expose to template
      return {
        readersNumber,
        book,
      };
    },
  };
</script>

使用render function 的方式:

// MyBook.vue

import { h, ref, reactive } from 'vue';

export default {
  setup() {
    const readersNumber = ref(0);
    const book = reactive({ title: 'Vue 3 Guide' });
    // 注意返回的 render 函数内使用 ref 类型的值,应该取它的 value 值
    return () => h('div', [readersNumber.value, book.title]);
  },
};

setup 专用生命周期钩子

变化描述:
  • 增加setup()专用的生命周期钩子,除beforeCreatecreated外其它钩子与选项内的钩子相同,如同下表:

    选项 API setup()内 API
    beforeCreate N/A
    created N/A
    beforeMount onBeforeMount
    mounted onMounted
    beforeUpdate onBeforeUpdate
    updated onUpdated
    beforeUnmount onBeforeUnmount
    unmounted onUnmounted
    errorCaptured onErrorCaptured
    renderTracked onRenderTracked
    renderTriggered onRenderTriggered
代码示例:
import { onMounted, onUpdated, onUnmounted } from 'vue';

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!');
    });
    onUpdated(() => {
      console.log('updated!');
    });
    onUnmounted(() => {
      console.log('unmounted!');
    });
  },
};

Teleport

变化描述:
  • 添加<teleport>组件;
  • 需要通过 prop to<teleport>组件提供一个目标元素,值可选项为HTMLElement或者是一个合法的querySelector字符串;
  • <teleport>组件将会移动它的 children 元素到上面指定的 DOM;
  • 在 virtual DOM 的层面上,children 元素仍属于<teleport>的后代,因此<teleport>包含的其它自定义子组件可以访问到祖先组件的注入(injections);
  • 多个<teleport>组件具有相同的to目标时,将会按照组件顺序 append 到目标 DOM 内;
代码示例:
<body>
  <div id="app">
    <h1>Move the #content with the portal component</h1>
    <teleport to="#endofbody">
      <div id="content">
        <p>
          this will be moved to #endofbody.<br />
          Pretend that it's a modal
        </p>
        <Child />
      </div>
    </teleport>
  </div>
  <div id="endofbody"></div>
  <script>
    new Vue({
      el: '#app',
      components: {
        Child: { template: '<div>Placeholder</div>' },
      },
    });
  </script>
</body>
<!-- result-->

<div id="app">
  <!-- -->
</div>
<div id="endofbody">
  <div id="content">
    <p>
      this will be moved to #endofbody.<br />
      Pretend that it's a modal
    </p>
    <div>Placeholder</div>
  </div>
</div>

多个<teleport>目标为同一个 DOM 的情况:

<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>
<!-- result-->

<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

多根元素 Fragments(multi-root node)

变化描述:
  • 不再限制自定义组件只能具有一个根元素,支持多个根元素的情况;
代码变化对比:

2.x

<!-- Layout.vue -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

3.x

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main
  ><!-- 手动绑定组件外部未声明为 prop 的属性 -->
  <footer>...</footer>
</template>

Suspense

变化描述:
  • 增加<suspense>组件,用于控制数据显示;
  • 需要提供两个<template>作为<suspense>的子元素,id 为default的模板装载的内容为正常显示内容,id 为fallback的模板装载的内容为无内容时显示;
代码示例:
<template>
  <Suspense>
    <template #default>
      <div v-for="item in articleList" :key="item.id">
        <article>
          <h2>{{ item.title }}</h2>
          <p>{{ item.body }}</p>
        </article>
      </div>
    </template>
    <template #fallback>
      Articles loading...
    </template>
  </Suspense>
</template>

<script>
  import getArticleList from 'getArticleList';
  export default {
    async setup() {
      let articleList = await getArticleList();
      return {
        articleList,
      };
    },
  };
</script>

自定义事件 Events

变化描述:
  • 增加组件emits选项,用于定义该组件需要关注的事件,当该选项包含原生事件(e.g.,click)时,该原生事件将被组件的自定义事件覆盖;
  • emits选项内可指定事件的校验函数,用于事件触发时校验提供的参数是否合理;
代码示例:

使用数组结构定义:

app.component('custom-form', {
  emits: ['in-focus', 'submit'],
});

使用对象结构定义:

app.component('custom-form', {
  emits: {
    // 不校验事件
    click: null,

    // 校验触发的事件
    submit: ({ email, password }) => {
      if (email && password) {
        return true;
      } else {
        console.warn('Invalid submit event payload!');
        return false;
      }
    },
  },
  methods: {
    submitForm() {
      this.$emit('submit', { email, password });
    },
  },
});

自定义渲染器 Renderer

变化描述:
  • 支持使用 APIcreateRenderer自定义渲染器,调用该 API 需要返回rendercreateApp两个全局 API;
代码示例:
import { createRenderer } from 'vue'
const { render, createApp } = createRenderer<Node, Element>({
  patchProp,
  ...nodeOps
});

BREAKING | 破坏性更改

全局 API Global

变化描述:
  • 提供的新的createApp api 用以声明式实例化应用;

  • 部分Vue构造函数的静态方法转变为全局方法,并移除$开头的同名实例方法,整体变化如下:

    2.x 静态方法 3.x 全局方法
    Vue.nextTick nextTick
    Vue.observable reactive
    Vue.version version
    Vue.compile compile
    Vue.set set
    Vue.delete delete
  • 部分全局 API 变为实例 API,整体变化如下:

    2.x 全局 API 3.x 实例 API
    Vue.config app.config
    Vue.config.productionTip N/A
    Vue.config.ignoredElements app.config.isCustomElement
    Vue.component app.component
    Vue.directive app.directive
    Vue.mixin app.mixin
    Vue.use app.use
  • 提供内部帮助函数作为全局 API,例如hTransitionwithDirectivesvShow...

代码实现:

createApp

import { createApp } from 'vue';

const app = createApp({});
app.mount('#app');

全局方法:

import { shallowMount } from '@vue/test-utils';
import { MyComponent } from './MyComponent.vue';
import { nextTick } from 'vue';

test('an async feature', async () => {
  const wrapper = shallowMount(MyComponent);

  // execute some DOM-related tasks

  await nextTick();

  // run your assertions
});

实例方法:

const app = createApp(MyApp);

app.component('button-counter', {
  data: () => ({
    count: 0,
  }),
  template: '<button @click="count++">Clicked {{ count }} times.</button>',
});

app.directive('focus', {
  mounted: el => el.focus(),
});

// now every application instance mounted with app.mount(), along with its
// component tree, will have the same “button-counter” component
// and “focus” directive without polluting the global environment
app.mount('#app');

内部帮助函数:

import { h, Transition, withDirectives, vShow } from 'vue';

export function render() {
  return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])]);
}

双向绑定 v-model

变化描述:
  • 自定义组件内的v-model的 prop 和 event 默认名称变更如下:
    prop: value -> modelValue;
    event: input -> update:modelValue;
    
  • v-bind.sync修饰符和自定义组件的model选项移除,并用v-model上的参数作为替代;
  • 自定义组件支持多个自定义的v-model绑定;
  • v-model支持自定义修饰符;
代码变化对比:

2.x

默认使用方式:

<ChildComponent v-model="pageTitle" />

<!-- 等价于下面的写法: -->

<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

自定义v-model的 prop 和 event 的使用方式:

<!-- ParentComponent.vue -->

<ChildComponent v-model="pageTitle" />
// ChildComponent.vue

export default {
  model: {
    prop: 'title',
    event: 'change',
  },
  props: {
    // 释放 `value` prop 以用作其它用途
    value: String,
    // 使用 `title` 替换默认的 `value` v-model 值
    title: {
      type: String,
      default: 'Default title',
    },
  },
};
<!-- 等价于下面的写法: -->

<ChildComponent :title="pageTitle" @change="pageTitle = $event" />

使用v-bind.sync的方式:

// 在子组件内 emit 以 :update 开头的自定义事件

this.$emit('update:title', newValue);
<!-- 监听自定义的 update 事件 -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

<!-- 等价于下面的写法: -->

<ChildComponent :title.sync="pageTitle" />

3.x

默认使用方式:

<ChildComponent v-model="pageTitle" />

<!-- 等价于下面的写法: -->
<!-- value -> modelValue -->
<!-- input -> update:modelValue -->

<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />

自定义v-model和多个v-model的方式(.sync因这种实现方式而不再有用,所以废除):

<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 等价于下面的写法: -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" :content="pageContent" @update:content="pageContent = $event" />


v-model自定义修饰符:

<div id="app">
  <my-component v-model.capitalize="myText"></my-component>
  {{ myText }}
</div>
const app = Vue.createApp({
  data() {
    return {
      myText: '',
    };
  },
});

app.component('my-component', {
  props: {
    modelValue: String,
    // 默认修饰符的 prop 为 modelModifiers,自定义 v-model 的修饰符形如:[propName]Modifiers,
    modelModifiers: {
      default: () => ({}),
    },
  },
  methods: {
    emitValue(e) {
      let value = e.target.value;
      // 根据修饰符可以做相应处理
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1);
      }
      this.$emit('update:modelValue', value);
    },
  },
  template: `<input
    type="text"
    :value="modelValue"
    @input="emitValue">`,
});

app.mount('#app');

自定义 v-model 情况下的自定义修饰符:

<my-component v-model:foo.capitalize="bar"></my-component>
app.component('my-component', {
  props: ['foo', 'fooModifiers'],
  template: `
    <input type="text"
      :value="foo"
      @input="$emit('update:foo', $event.target.value)">
  `,
  created() {
    console.log(this.fooModifiers); // { capitalize: true }
  },
});

渲染函数 API Render

变化描述:
  • h需要从Vue全局引入,以取代原render函数内提供的createElement函数;
  • render 函数不再接受任何参数;
  • VNodes 的 props 结构扁平化;
代码变化对比:

render 函数参数

2.x

// Vue 2 Render Function Example
export default {
  render(h) {
    return h('div');
  },
};

3.x

// Vue 3 Render Function Example
import { h } from 'vue';

export default {
  render() {
    return h('div');
  },
};

render 函数签名变化

2.x

// Vue 2 Render Function Example
export default {
  render(h) {
    return h('div');
  },
};

3.x

import { h, reactive } from 'vue';

export default {
  setup(props, { slots, attrs, emit }) {
    const state = reactive({
      count: 0,
    });

    function increment() {
      state.count++;
    }

    // return the render function
    return () =>
      h(
        'div',
        {
          onClick: increment,
        },
        state.count,
      );
  },
};

VNode props 格式

2.x

// 2.x
{
  class: ['button', 'is-outlined'],
  style: { color: '#34495E' },
  attrs: { id: 'submit' },
  domProps: { innerHTML: '' },
  on: { click: submitForm },
  key: 'submit-button'
}

3.x

// 3.x Syntax
{
  class: ['button', 'is-outlined'],
  style: { color: '#34495E' },
  id: 'submit',
  innerHTML: '',
  onClick: submitForm,
  key: 'submit-button'
}

函数式组件 Functional Component

变化描述:
  • 函数式组件的性能优化已经在 3.x 被忽略,建议只使用有状态的组件;
  • 函数式组件只能使用简单函数创建,该函数接收propscontext两个参数;
  • 单文件组件的functional属性移除;
  • { functional: true }选项移除;
代码变化对比:

2.x

<template>的情况:

export default {
  functional: true,
  props: ['level'],
  render(h, { props, data, children }) {
    return h(`h${props.level}`, data, children);
  },
};

<template>的情况:

<template>
  <template functional>
    <component :is="`h${props.level}`" v-bind="attrs" v-on="listeners" />
  </template>

  <script>
    export default {
      props: ['level'],
    };
  </script></template
>

3.x

import { h } from 'vue';

const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots);
};

DynamicHeading.props = ['level'];

export default DynamicHeading;

异步组件 Async Component

变化描述:
  • 使用新的defineAsyncComponent方法定义异步组件;
  • 定义组件带选项时,component更名为loader
  • 新的loader选项必须返回一个 Promise
  • 定义组件带选项时,loading更名为LoadingComponent
  • 定义组件带选项时,error更名为errorComponent
  • 新增定义组件选项retryWhen,用于控制当组件加载失败时,指定情况才进行重试加载;
  • 新增定义组件选项maxRetries,控制重试加载的次数;
  • 新增定义组件选项suspensible,默认为 true,为 true 时,定义的loadingComponenterrorComponent将被无视;
代码变化对比:

2.x

import ErrorComponent from './components/ErrorComponent.vue';
import LoadingComponent from './components/LoadingComponent.vue';

// 无选项异步组件
Vue.component('async-component', () => import('./my-async-component'));

// 带选项异步组件
const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000,
});

3.x

import ErrorComponent from './components/ErrorComponent.vue';
import LoadingComponent from './components/LoadingComponent.vue';
import { defineAsyncComponent } from 'vue';

// 无选项异步组件
const asyncPage = defineAsyncComponent(() => import('./my-async-component'));

// 带选项异步组件
const asyncPageWithOptions = defineAsyncComponent({
  // component 更名为 loader
  loader: () => import('./my-async-component'),
  // A component to use while the async component is loading
  loadingComponent: LoadingComponent,
  // A component to use if the load fails
  errorComponent: ErrorComponent,
  // Delay before showing the loading component. Default: 200ms.
  delay: 200,
  // The error component will be displayed if a timeout is
  // provided and exceeded. Default: Infinity.
  timeout: 3000,
  // A function that returns a boolean indicating whether the async component should retry when the loader promise rejects
  retryWhen: error => error.code !== 404,
  // Maximum allowed retries number
  maxRetries: 3,
  // Defining if component is suspensible
  suspensible: false,
});

Data 选项

变化描述:
  • data组件选项声明不再接受对象字面量,只接受function的声明方式;
代码变化对比:

2.x

<!-- Object Declaration -->
<script>
  const app = new Vue({
    data: {
      apiKey: 'a1b2c3',
    },
  });
</script>

<!-- Function Declaration -->
<script>
  const app = new Vue({
    data() {
      return {
        apiKey: 'a1b2c3',
      };
    },
  });
</script>

3.x

<script>
  import { createApp } from 'vue';

  createApp({
    data() {
      return {
        apiKey: 'a1b2c3',
      };
    },
  }).mount('#app');
</script>

自定义元素 Elements

变化描述:
  • 自定义元素白名单现在在模板编译期间执行,并且应该通过编译器选项而不是运行时配置来配置;
  • 特殊的is prop 现在限定只能在<component>上使用;
  • 提供新的v-is指令以支持 2.x 的使用情况;
代码变化对比:

2.x

// 这可以使 Vue 忽略在外部定义的自定义元素
// (e.g., 使用 Web Components APIs 定义的元素)

Vue.config.ignoredElements = ['plastic-button'];

3.x

使用打包工具选项定义的情况:

// in webpack config
rules: [
  {
    test: /\.vue$/,
    use: 'vue-loader',
    options: {
      compilerOptions: {
        isCustomElement: tag => tag === 'plastic-button',
      },
    },
  },
  // ...
];

使用运行时定义的情况(注意这种配置对预编译的模板无效):

const app = Vue.createApp({});
app.config.isCustomElement = tag => tag === 'plastic-button';

插槽 Slots

变化描述:
  • 指定 slot 的方式由对象字面量变为函数式;
  • this.$scopedSlots移除;
代码变化对比:

2.x

// 2.x Syntax
h(LayoutComponent, [h('div', { slot: 'header' }, this.header), h('div', { slot: 'content' }, this.content)]);
// 2.x 在组件内获取指定的槽
this.$scopedSlots.header;

3.x

// 3.x Syntax
h(
  LayoutComponent,
  {},
  {
    header: () => h('div', this.header),
    content: () => h('div', this.content),
  },
);
// 3.x 在组件内获取指定的槽
this.$slots.header;

动态绑定属性处理

变化描述:
  • 删除 Vue 内部定义的“枚举属性”(contenteditabledraggablespellcheck)概念,并将这些“枚举属性”当作非布尔值处理;
  • 当 DOM 上的动态属性绑定值为false时,不再删除该属性。如果需要删除属性,使用null或者undefined
属性处理变化对比:

2.x

绑定表达式 foo(常规值) draggable(枚举值)
:attr="null" N/A draggable="false"
:attr="undefined" N/A N/A
:attr="true" foo="true" draggable="true"
:attr="false" N/A draggable="false"
:attr="0" foo="0" draggable="true"
attr="" foo="" draggable="true"
attr="foo" foo="foo" draggable="true"
attr foo="" draggable="true"

3.x

绑定表达式 foo(常规值) draggable(枚举值)
:attr="null" N/A N/A *
:attr="undefined" N/A N/A
:attr="true" foo="true" draggable="true"
:attr="false" foo="false" * draggable="false"
:attr="0" foo="0" draggable="0" *
attr="" foo="" draggable="" *
attr="foo" foo="foo" draggable="o" *
attr foo="" draggable="" *

*:发生变化


自定义指令 Directives

变化描述:
  • 重命名自定义指令的生命周期钩子,使其与组件的生命周期一致(部分):

    - bind → beforeMount
    
    - inserted → mounted
    
    - beforeUpdate: 新钩子,在元素更新前调用
    
    - update → 已删除
    
    - beforeUnmount 新钩子,在元素卸载前调用
    
    - unbind -> unmounted
    
  • 当指令应用在自定义组件时,需要在组件内使用v-bind="$attrs"手动绑定到具体的 DOM,因为可能存在多根元素的情况;

代码变化对比:

2.x

<p v-highlight="yellow">Highlight this text bright yellow</p>
Vue.directive('highlight', {
  bind(el, binding, vnode) {
    el.style.background = binding.value;
  },
});

3.x
使用在原生 DOM 的情况:

<p v-highlight="yellow">Highlight this text bright yellow</p>
const app = Vue.createApp({});

app.directive('highlight', {
  beforeMount(el, binding, vnode) {
    el.style.background = binding.value;
  },
  mounted() {},
  beforeUpdate() {}, // new
  updated() {},
  beforeUnmount() {}, // new
  unmounted() {},
});

使用在自定义组件的情况:

组件外部:

<custom-component v-highlight="yellow" />

组件内部:

<template>
  <p v-bind="$attrs">Highlight this text bright yellow</p>
  <p v-bind="$attrs">Highlight this text bright yellow</p>
  <p>Keep this text normal</p>
</template>

动画组件 Transition Component

变化描述:
  • <transition>作为组件根元素时,在组件外部使用v-if或者v-show改变显隐值将不再生效,需要暴露指定值来控制显隐以驱动 transition 动画;
  • v-enter transition class 更名为v-enter-from
  • v-leave transition class 更名为v-leave-from
代码变化对比:

<transition>作为组件根元素:

2.x

<!-- modal component -->
<template>
  <transition>
    <div class="modal"><slot /></div>
  </transition>
</template>

<!-- usage -->
<modal v-if="showModal">hello</modal>

3.x

<!-- modal component -->
<template>
  <transition>
    <div v-if="show" class="modal"><slot /></div>
  </transition>
</template>

<!-- usage -->
<modal :show="showModal">hello</modal>

class 更名:

2.x

/* before */
.v-enter,
.v-leave-to {
  opacity: 0;
}
.v-leave,
.v-enter-to {
  opacity: 1;
}

3.x

/* after */
.v-enter-from,
.v-leave-to {
  opacity: 0;
}
.v-leave-from,
.v-enter-to {
  opacity: 1;
}

侦听器 API Watch

变化描述:
  • 更改形如a.b.c的键路径观察方式为计算函数式,如() => this.a.b.c
代码变化对比:

2.x

// 键路径
vm.$watch('a.b.c', function(newVal, oldVal) {
  // 做点什么
});

3.x

vm.$watch(
  () => vm.a.b.c,
  (newVal, oldVal) => {
    // 做点什么
  },
);

// 多依赖的情况
vm.$watch(
  () => vm.a.b.c + vm.d.e.f,
  (newVal, oldVal) => {
    // 做点什么
  },
);

响应式注入 Provide / Inject

变化描述:
  • 通过refcomputedreactive api,现在可以注入响应式的值和该值对应的更新方法;
代码变化对比:

2.x

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar',
  },
  // ...
};

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created() {
    console.log(this.foo); // => "bar"
  },
  // ...
};

3.x

在组件选项内定义的情况:

app.component('todo-list', {
  // ...
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length),
    };
  },
});

setup函数内定义的情况:

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
  import { provide, reactive, ref } from 'vue'
  import MyMarker from './MyMarker.vue

  export default {
    components: {
      MyMarker
    },
    setup() {
      const location = ref('North Pole')
      const geolocation = reactive({
        longitude: 90,
        latitude: 135
      })

      const updateLocation = () => {
        location.value = 'South Pole'
      }

      provide('location', location)
      provide('geolocation', geolocation)
      provide('updateLocation', updateLocation)
    }
  }
</script>
<!-- src/components/MyMarker.vue -->
<script>
  import { inject } from 'vue';

  export default {
    setup() {
      const userLocation = inject('location', 'The Universe');
      const userGeolocation = inject('geolocation');
      const updateUserLocation = inject('updateUserLocation'); // 注入更新方法

      return {
        userLocation,
        userGeolocation,
        updateUserLocation, // 可再导出更新方法
      };
    },
  };
</script>

REMOVED | 废弃功能特性

事件 API Events

变化描述:
  • $on$off$once实例方法移除。应用实例不再提供事件中心的接口。
代码变化对比:

2.x

// eventHub.js

const eventHub = new Vue();

export default eventHub;
// ChildComponent.vue
import eventHub from './eventHub';

export default {
  mounted() {
    // adding eventHub listener
    eventHub.$on('custom-event', () => {
      console.log('Custom event triggered!');
    });
  },
  beforeDestroy() {
    // removing eventHub listener
    eventHub.$off('custom-event');
  },
};
// ParentComponent.vue
import eventHub from './eventHub';

export default {
  methods: {
    callGlobalCustomEvent() {
      eventHub.$emit('custom-event'); // if ChildComponent is mounted, we will have a message in the console
    },
  },
};

3.x

$on$off$once实例方法移除。保留$emit实例方法作为子组件发射自定义事件的 api。


事件修饰符 KeyCode

变化描述:
  • v-on指令的修饰符不再支持 keyCodes;
  • 移除config.keyCodes;
代码变化对比:

2.x

<!-- keyCode version -->
<input v-on:keyup.13="submit" />

<!-- alias version -->
<input v-on:keyup.enter="submit" />
Vue.config.keyCodes = {
  f1: 112,
};
<!-- keyCode version -->
<input v-on:keyup.112="showHelpText" />

<!-- custom alias version -->
<input v-on:keyup.f1="showHelpText" />

3.x

<!-- Vue 3 Key Modifier on v-on -->
<input v-on:keyup.delete="confirmDelete" />

过滤器 Filters

变化描述:
  • 完全移除Filters
代码变化对比:

2.x

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountBalance | currencyUSD }}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true,
      },
    },
    filters: {
      currencyUSD(value) {
        return '$' + value;
      },
    },
  };
</script>

3.x

使用计算属性或者自定义方法处理:

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountInUSD }}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true,
      },
    },
    computed: {
      accountInUSD() {
        return '$' + this.accountBalance;
      },
    },
  };
</script>

行内模板属性 inline-template

变化描述:
  • 完全移除inline-template方法;
代码变化对比:

2.x

<my-component inline-template>
  <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
  </div>
</my-component>

3.x

不再支持inline-template

总结

通过学习 Vue 3.x 的新特性可以体会到,Vue 在性能工程化可移植性这三个大方向里面作出了很大的努力,主要体现在这五个方面:

  • Tree-shaking support;
    • 大多数可选功能现在都支持 Tree-shaking,例如 Composition API、v-model<transition>
    • 最小可运行的 HelloWorld 例子的文件总大小: 13.5kb, 只使用 Composition API 的情况下最小可达到 11.75kb;
    • 全运行时打包总大小为: 22.5kb,功能更多更强大,但是大小比 Vue 2 更小;
  • Composition API;
    • 可与原有的选项 API 同时存在,更灵活和更易于 2.x 升级 3.x,同时利于开发人员  过渡升级版本;
    • 更加灵活的逻辑组合和复用,大大地提升了复杂组件的可维护性;
    • Reactivity 模块可作为独立的库使用,使得 Vue 3.x 在与其它框架配合使用时更加灵活;
  • Fragment, Teleport, Suspense;
    • Fragment 特性的到来,可以使<template>中不再局限于单一的根节点,同时render函数也可以返回包含多个 Vnode 的数组;
    • Teleport 组件让原来的复杂组件的嵌套关系变得更加灵活和清晰;
    • Suspense 组件大大减少了书写异步数据 DOM 渲染和异步组件的代码量;
  • Better TypeScript support;
    • Vue 3.x 库代码编写由原来的flow转换为ts,所以在编写业务代码时使用 ts 可以大大受益;
    • API 在 js 和 ts 环境下的使用一致;
    • 支持 TSX;
    • Class 组件仍然可用(需要引入vue-class-component@next);
  • Custom Renderer API;
    • 允许生成自定义的 renderer,降低了 Vue 3.x 迁移到其它平台的难度,比如小程序、Weex 等;

← Markdown基本语法与扩展语法 控制前端业务重复请求的一个新思路 →

订阅本博客的最新内容

如果我有新的想法,会第一时间通过邮件与你分享.

请放心我不会发送任何辣鸡邮件给你.

你可以 随时 取消订阅.

讨论请发邮件到 lkangd@gmail.com

未经授权,禁止转载

通过支付宝 lkangd@foxmail.com 或赞赏码赞助此文

reward-code