Vue3对比Vue2有啥优点?

175 阅读16分钟

写在前面

vue 组件有 2 种 API 风格:Options API 和 Composition API

  1. Options API:使用选项对象定义组件的逻辑,如 data、methods 和 mounted。由选项定义的属性在 this 内部函数公开,指向具体实例
<template>
  <button @click="increment">count is: {{ count }}</button>
</template>
<script >
	export default {
		data() {
			return {
				count: 0
			}
		},
		methods: {
			increment() {
				this.count++;
			}
		},
		mounted() {
			console.log(`The initial count is ${this.count}.`);
		}
	}
</script>
  1. Composition API:使用导入的 API 函数定义组件的逻辑。在 SFC 中,Composition api 通常使用
<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>
<script setup >
import { ref, onMounted } from 'vue';
const count = ref(0);
function increment() {
	count.value++;
}
onMounted(() => {
    console.log(`The initial count is ${count.value}.`);
})
</script>

SFC:单文件组件

参考:cn.vuejs.org/guide/scali…

生命周期

总体变化不大,只是 vue3 中大部分的钩子名称前+"on",功能和 vue2 类似,vue3 引入了几个新的生命周期钩子,比 unMounted、onUpdated、onUnmounted 等

vue3 在组合式 API(Composite API)中使用生命周期钩子时需先引入,而 vue2 在选项式 API(Options API)中可以直接调用生命周期钩子

// vue3
<script setup>     
import { onMounted } from 'vue';   // 使用前需引入生命周期钩子
onMounted(() => { });
// 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不会被覆盖
onMounted(() => {});
</script>
// vue2
<script>     
export default {
    mounted() {   // 直接调用生命周期钩子            
    }
}
</script> 

vue2开发中常用到的钩子

  1. created:实例创建完成后被调用,element属性已经初始化但还没有被挂载。常用于异步请求数据、初始化事件等
  2. mounted:实例挂载完成后调用,DOM已经渲染到页面。进行需要DOM的操作,如获取DOM元素、初始化第三方插件
  3. beforeDestroy:实例销毁前调用,实例仍然可用,data可访问。进行清理工作、解除事件监听

常用生命周期对比如下表所示。

vue2vue3
beforeCreatesetup开始创建组件之前
createdsetup开始创建组件之前
beforeMountonBeforeMount组件挂载到节点之前
mountedonMounted组件挂载完成
beforeUpdateonBeforeUpdate组件更新之前
updatedonUpdated组件更新完成之后
beforeDestroyonBeforeUnmount组件卸载之前
destroyedonUnmounted组件卸载完成之后

Tips: setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地去定义。

多根节点

vue2 中每个模块只能有一个根元素

// vue2中在template里存在多个根节点会报错
<template>
  <header></header>
  <main></main>
</template>
// 只能存在一个根节点,需要用一个<div>来包裹着
<template>
  <div>
    <header></header>
    <main></main>
  </div>
</template>

但在 vue3 中,单个模块中可以有多个根元素

<template>
  <header></header>
  <main></main>
</template>

Composition API

Vue2 是选项 API(Options API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。

Vue3 组合式 API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。

Fragment、Teleport、Suspense

vue3 新增 3 个内置组件

  1. Fragment:用于返回多个根节点的组件
  2. Teleport:可将部分 DOM 移动到 Vue app 之外的位置。比如常见的 Dialog 弹窗。
<button @click="dialogVisible = true">显示弹窗</button>
<teleport to="body">
    <div class="dialog" v-if="dialogVisible">
        我是弹窗,我直接移动到了body标签下  
    </div>
</teleport>
  1. Suspense:等待异步组件时渲染额外内容,如兜底的 loading。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽 default ,并将 fallback 插槽用作加载状态
<template>
    <suspense>
        <template #default>
            <List />
        </template>
        <template #fallback>
            <div>Loading...</div>
        </template>
    </suspense>
</template>

在 List 组件(有可能是异步组件,也有可能是组件内部处理逻辑或查找操作过多导致加载过慢等)未加载完成前,显示 Loading...(即 fallback 插槽内容),加载完成时显示自身(即 default 插槽内容)。

响应式原理

  1. Vue2利用Object.defineProperty()对数据劫持,结合发布订阅模式的方法实现
  2. vue3 使用 proxy 对象重写了响应式系统

Object.defineProperty 基本用法:直接在一个对象上定义新的属性或修改现有的属性,并返回对象。

语法:Object.defineProperty(obj, prop, descriptor)

  • obj是目标对象,prop是要定义的属性,descriptor是属性描述符(数据描述符和存取描述符),定义属性的访问行为。
let obj = {};
let name = 'leo';
Object.defineProperty(obj, 'name', {
    enumerable: true, // 可枚举(是否可通过 for...in 或 Object.keys() 进行访问)
    configurable: true, // 可配置(是否可使用 delete 删除,是否可再次设置属性)
    // value: '',   // 任意类型的值,默认undefined
    // writable: true,   // 可重写
    get() {
        return name;
    },
    set(value) {
        name = value;
    }
});

Tips: writablevaluegettersetter 不共存。(数据描述符和访问描述符不能共用)

通过代码可知,响应式的关键在于:get和set方法

查看Vue2核心源码

defineReactive 的功能是定义一个响应式对象,给对象动态添加getter和setter

image.png

按照以上流程,就保证了无论obj结构多复杂,它的所有子属性也能变成响应式对象,这样我们访问或修改obj中一个嵌套较深的属性,也可以触发getter和setter。最后通过Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter

/**
 * Define a reactive property on an Object.
 */
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  // 一个key一个dep
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 获取key的属性描述符,如果是不可配置的对象直接 return
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if (
    (!getter || setter) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
  }
  // 递归处理,确保对象中所有key被观察到
  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 依赖收集
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend()
        }
        if (childOb) {
        // 嵌套对象,依赖收集
          childOb.dep.depend()
          // 触发数组响应式
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    // 派发更新
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
      // 依赖通知更新
        dep.notify()
      }
    }
  })
  return dep
}

那 Vue3 为何会抛弃它呢?

主要原因:

  1. 深度监听,需要一次性递归,计算量大(性能考虑)
  2. 描述符只有get和set,无法监听新增属性删除属性操作(defineProperty本身API缺陷)
  3. 无法监听对象、数组

Vue3优点:

  1. 性能更快:长远看JS引擎会继续优化Proxy
  2. 内存占用更小:Vue3只有在用到某个对象时,才对其进行数据劫持,所以响应式系统更快且占用内存更小
  3. 支持根数据增删属性拦截
  4. 支持数组拦截:支持Object、Array、Map、WeakMap、Set、WeakSet六种类型数据劫持

vue3中响应式数据核心为 reactive, reactive实现由proxy和effect组合,reactive方法的定义源码如下,源码文件

/**
 * 返回对象的响应式代理
 *
 * The reactive conversion is "deep": it affects all nested properties. A
 * reactive object also deeply unwraps any properties that are refs while
 * maintaining reactivity.
 *
 * @example
 * ```js
 * const obj = reactive({ count: 0 })
 * ```
 *
 * @param target - The source object.
 * @see {@link https://vuejs.org/api/reactivity-core.html#reactive}
 */
export function reactive<T extends object>(target: T): Reactive<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 如果目标对象是一个只读的响应数据,直接返回目标对象
  if (isReadonly(target)) {
    return target
  }
  // 调用 createReactiveObject 创建 observe
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap,
  )
}

createReactiveObject创建 observe

function createReactiveObject(
  target: Target, // 目标对象
  isReadonly: boolean, // 是否只读
  baseHandlers: ProxyHandler<any>, // 处理数组、对象
  collectionHandlers: ProxyHandler<any>, // 处理Map、Set、WeakMap、WeakSet
  proxyMap: WeakMap<Target, any>,
) {
  if (!isObject(target)) {
    // 在开发模式抛出警告,生产环境直接返回目标对象
    if (__DEV__) {
      warn(
        `value cannot be made ${isReadonly ? 'readonly' : 'reactive'}: ${String(
          target,
        )}`,
      )
    }
    return target
  }
  // target is already a Proxy, return it.
  // exception: calling readonly() on a reactive object
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  // 使用 Proxy 创建 observe 
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
}

官网:v2.cn.vuejs.org/v2/guide/re…

ref、toRef、toRefs

三者都是vue3的composition API,一般在生命周期setup中使用

  1. ref:使值类型变成响应式
  2. reactive:使引用类型变成响应式
  3. toRef:引用定义的响应对象的某个属性。使用函数返回一个响应式对象,可以使用toRef或者toRefs保持它的响应式
  4. toRefs:同上,都是用来延续引用类型响应式对象。toRef延续单个响应式对象的属性,toRefs延续响应式对象的全部属性

mixins & hooks

vue2的mixins将多个相同的逻辑抽离出来,各个组件只需引入mixins,就能实现一次写代码,多组件受益的效果

  1. mixins中的生命周期和引入mixins的组件的生命周期一起触发
  2. 组件的data、methods、filters会覆盖mixins中同名data、methods、filters
  3. 不同mixins中的同名方法,按照引入顺序,最后的覆盖前面的同名方法

vue2中mixins缺点

  1. 引入多个mixins导致变量来源不明确,不利于代码维护
  2. 如果存在重复命名的数据或方法不会合并,会导致冲突
  3. mixins和组件可能出现多对多的关系,复杂度高

为了解决这些问题,vue3的hooks直接将公共函数抽离出来放到hooks.js中,在有需要的vue文件中显示引入,很明确能知道方法来自哪个文件

VDOM

vue3 重构了 VDOM,通过优化避免了不必要的渲染

patch flag 优化静态树

Vue3 相比于 Vue2,虚拟 DOM 上增加 patchFlag 字段。我们借助 Vue3 Template Explorer 来看

<div id="app">
  <h1>vue3虚拟DOM讲解</h1>
  <p>今天天气真不错</p>
  <div>{{name}}</div>
</div>

vue3编译后的VDOM如下所示:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { id: "app" }, [
    _createElementVNode("h1", null, "vue3虚拟DOM讲解"),
    _createElementVNode("p", null, "今天天气真不错"),
    _createElementVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}
// 如果是动态绑定的val在渲染dom的时候会在 _createElementVNode函数后面追加动态标记1
// Check the console for the AST

创建动态dom时,VDOM除了模拟出dom基本信息,额外加了一个 1 /* TEXT */ ,其中 1 就是标记,这个标记我们称其为 patch flag (补丁标记)

patch flag的好处是,当我们的diff算法走到 _createElementBlock 方法时,会忽略所有静态节点,只对有标记的动态节点进行对比,并且在多层的嵌套下依旧生效;vue2中重复渲染时静态内容依旧会重建VDOM,diff时仍然需要对比

patch flag 优化静态属性

我们首先创建一个带有属性的元素

<div id="app">
  <div id="example">{{name}}</div>
</div>

vue3编译后的VDOM如下所示:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { id: "app" }, [
    _createElementVNode("div", { id: "example" }, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}

我们发现,它没有对id做特殊标记,原因是dom元素的静态属性在渲染时就已经创建了,并且不会改变,所以在后续更新中,diff算法不会管它

接下来我们创建一个属性时动态绑定的元素

<div id="app">
  <div :id="example">{{name}}</div>
</div>

vue3编译后的VDOM如下所示:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { id: "app" }, [
    _createElementVNode("div", { id: _ctx.example }, _toDisplayString(_ctx.name), 9 /* TEXT, PROPS */, ["id"])
  ]))
}

我们发现,它的patch flag变成了 9 /* TEXT, PROPS */ ,同时后面多了一个数组 ["id"]

这表示当前元素不止 TEXT 变化,它的属性 PROPS 也会变化,后面的数组内容是可能变化的属性

vue3在更新VDOM时,只会关注VDOM有变化的部分。这个优化使 Vue3 跳出了 VDOM 的性能瓶颈,同时保留了可以手写 render function 的灵活性。

静态标记值的注释如下,其中使用了位运算

// patchFlags 字段类型列举
export const enum PatchFlags { 
  TEXT = 1,   // 动态文本内容
  CLASS = 1 << 1,   // 动态类名
  STYLE = 1 << 2,   // 动态样式
  PROPS = 1 << 3,   // 动态属性,不包含类名和样式
  FULL_PROPS = 1 << 4,   // 具有动态 key 属性,当 key 改变,需要进行完整的 diff 比较
  HYDRATE_EVENTS = 1 << 5,   // 带有监听事件的节点
  STABLE_FRAGMENT = 1 << 6,   // 不会改变子节点顺序的 fragment
  KEYED_FRAGMENT = 1 << 7,   // 带有 key 属性的 fragment 或部分子节点
  UNKEYED_FRAGMENT = 1 << 8,   // 子节点没有 key 的fragment
  NEED_PATCH = 1 << 9,   // 只会进行非 props 的比较
  DYNAMIC_SLOTS = 1 << 10,   // 动态的插槽
  HOISTED = -1,   // 静态节点,diff阶段忽略其子节点
  BAIL = -2   // 代表 diff 应该结束
}

静态提升

那么vue具体是如何做到只关注变化的部分呢?

  1. 静态树的提升
  2. 静态属性的提升
<div id="app">
  <h1>vue3虚拟DOM讲解</h1>
  <p>今天天气真不错</p>
  <div id="example">{{name}}</div>
</div>

静态提升后

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("h1", null, "vue3虚拟DOM讲解", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("p", null, "今天天气真不错", -1 /* HOISTED */)
const _hoisted_4 = { id: "example" }
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _hoisted_3,
    _createElementVNode("div", _hoisted_4, _toDisplayString(_ctx.name), 1 /* TEXT */)
  ]))
}
// Check the console for the AST

我们知道, _createElementBlock方法包含处理后的 VDOM,所有的静态元素都放在了 _createElementVNode 方法之外,即他们只会在页面初始化时被渲染一次,更新的时候,对静态元素不予理会

Diff 算法优化

结合上文与源码,patchFlag 帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。

Vue3 patchChildren 源码

 const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    slotScopeIds,
    optimized = false,
  ) => {
  // 获取新老孩子节点
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children
    const { patchFlag, shapeFlag } = n2
    // fast path
    if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
        // this could be either fully-keyed or mixed (some keyed some not)
        // presence of patchFlag means children are guaranteed to be arrays
        patchKeyedChildren(...)
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
        // unkeyed
        patchUnkeyedChildren(...)
        return
      }
    }
    // children has 3 possibilities: text, array or no children.
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // text children fast path
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
      }
      if (c2 !== c1) {
        hostSetElementText(container, c2 as string)
      }
    } else {
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // prev children was array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          // two arrays, cannot assume anything, do full diff
          patchKeyedChildren(... )
        } else {
          // no new children, just unmount old
          unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
        }
      } else {
        // prev children was text OR null
        // new children is array OR null
        if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(container, '')
        }
        // mount new if array
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(...)
        }
      }
    }
  }

patchUnkeyedChildren 源码如下所示。

  const patchUnkeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length
    const newLength = c2.length
    const commonLength = Math.min(oldLength, newLength)
    let i
    for (i = 0; i < commonLength; i++) {
    // 如果新 Vnode 已经挂载,直接 clone 一份;否则新建一个节点
      const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i]))
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    }
    if (oldLength > newLength) {
      // remove old
      unmountChildren( c1, parentComponent, parentSuspense, true, false, commonLength, )
    } else {
      // mount new
      mountChildren( c2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, commonLength,)
    }
  }

事件缓存

Vue3 的cacheHandler可在第一次渲染后缓存我们的事件。相比于 Vue2 无需每次渲染都传递一个新函数

示例如下:

<button type="button" id="btnIncrement" @click="increment">
   Increment
</button>

编译为AST(不带有事件缓存)

import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("button", {
    type: "button",
    id: "btnIncrement",
    onClick: _ctx.increment
  }, " Increment ", 8 /* PROPS */, ["onClick"]))
}
// Check the console for the AST

type和id是硬编码或者静态的,因此Vue不需要将其加入到patch-flag数组中。onClick 已经绑定到引用中,在AST中,它被添加到了 patch-flag数组中为了检查任何更改。所以每次这个节点被修改或者打补丁时,Vue都会检查onClick是否存在变化,如果有则重新渲染。但我们知道,大多数情况下,其实我们并不打算更改事件侦听器

为了解决这个问题,vue新增一种编译机制,它在第一次渲染时会缓存事件,并将其存储为内联方法,在后续渲染中会直接从缓存中获取事件

带有事件缓存的AST如下:

import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("button", {
    type: "button",
    id: "btnIncrement",
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.increment && _ctx.increment(...args)))
  }, " Increment "))
}
// Check the console for the AST

观察代码可知,第一次,调用_ctx.increment 对事件进行缓存,在后续每次渲染时,将使用_cache[1]中的值

现在我们不需要将onClick放进patch-flag数组,降低了检查节点是否有更改的开销,并且减少了大量的重新渲染,我们自然能够想象到,如果是在大型组件树的情况下,这样可以防止多少次的重复渲染

打包优化

Tree-shaking

移除 JavaScript 上下文中未引用的代码。主要依赖于 import 和 export 语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用。

以 nextTick 为例子,在 Vue2 中,全局 API 暴露在 Vue 实例上,即使未使用,也无法通过 tree-shaking 进行消除。

import Vue from 'vue';
Vue.nextTick(() => {
  // 一些和DOM有关的东西
});

Vue3 中针对全局和内部的 API 进行了重构,并考虑到 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。

import { nextTick } from 'vue';   // 显式导入
nextTick(() => {
  // 一些和DOM有关的东西
});

通过这一更改,只要模块绑定器支持 tree-shaking,则 Vue 应用程序中未使用的 api 将从最终的捆绑包中消除,获得最佳文件大小。

受此更改影响的全局 API 如下所示

  • Vue.nextTick
  • Vue.observable (用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile (仅全构建)
  • Vue.set (仅兼容构建)
  • Vue.delete (仅兼容构建)

内部 API 也有诸如 transition、v-model 等标签或者指令被命名导出。只有在程序真正使用才会被捆绑打包。Vue3 将所有运行功能打包也只有约 22.5kb,比 Vue2 轻量很多。

静态组件提升

vue2中每次渲染组件都会创建一个新的响应式实例,这样会消耗一定内存和性能;vue3中通过新的编译方式会检测到静态组件,并将其转换为普通js对象,进而减少不必要的实例化

TypeScript 集成

vue3 源码使用 TS 编写

自定义渲染 API

vue3 支持创建一个自定义渲染器。通过提供平台特定的节点创建以及更改 API,我们可以在非 DOM 环境中也享受到 Vue 核心运行时的特性。

基于 API 的全局注册

vue2 使用 Vue.component, Vue.directive, Vue.mixin 等进行全局注册。但 Vue3 使用 createApp 进行全局注册

安全性

vue3 在设计时考虑到更多安全性问题、如更严格的默认内容安全策略、更好的处理潜在的跨站脚本攻击等

写在最后

总的来说,vue3 在性能、灵活性、可维护性、安全性等方面都有所提升,同时引入了一些新的功能和 API,使得我们能更方便、高效地开发。

参考

segmentfault.com/a/119000004…

cn.vuejs.org/guide/intro…

cn.vuejs.org/api/custom-…

borstch.com/blog/develo…