Vue3 与 Vue2 的对比

136 阅读1分钟

一、Vue3 新优势

1.1 options API -> composition API

Composition API 字面意思是组合 API,它是为了实现基于函数的逻辑复用机制而产生的。

1)声明变量

const { reactive } = Vue
let App = {
    template: `
        <div>
            {{ message }}
        </div>`,
    setup() {
        const state = reactive({ message: "Hello World!!!" })
        return {
            ...state
        }
    }
}
Vue.createApp().mount(App, '#app')

2) 双向绑定

const { reactive } = Vue
let App = {
    template: `
        <div>
            <input v-model="state.value" />
            <span>{{ state.value }}</span>
        </div>`,
    setup() {
        const state = reactive({ value: '' })
        return {
            state
        }
    }
        
}
Vue.createApp().mount(App, '#app')

注:setup 实际上是一个组件的入口,它运行在组件被实例化的时候,props 属性被定义之后,实际上等价于 Vue2 的 beforeCreate 和 created。reactive 是用于创建一个响应式数据对象的 API,几乎等价于 Vue2 中的 Vue.observable(),为了避免与 rxjs 中的 observable 混淆,在 Vue3 中进行了重命名。

3)观察属性

import { reactive, watchEffect } from 'vue'

const state = reactive({
    count: 0
})

watchEffect(() => {
    document.body.innerHTML = `count is ${state.count}`
})

return {
    ...state
}

注:watchEffect 和 2.x 中的 watch 类似,但是它不需要把被依赖的数据源和副作用回调分开。组合式 API 同样提供了一个 watch 函数,其行为和 2.x 的功能完全一致。

4)ref

注:Vue3 允许用户创建单个的响应式对象。

const App = {
    template: `
        <div>
            {{ value }}
        </div>`,
    setup() {
        const value = ref(0)
        
        return {
            value
        }
    }
}
Vue.createApp().mount(App, '#app')

5) 计算属性

setup() {
    const state = reactive({
        count: 0,
        double: computed(() => state.count * 2)
    })
    
    function increment() {
        state.count++
    }
    
    return {
        state,
        increment
    }
}

6) 生命周期的变更

Vue2Vue3
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestoryonBeforeUnmount
destoryedonUnmounted
errorCapturedonErrorCaptured
import { onMounted } from 'vue'

export default {
    setup() {
        onMounted(() => {
            console.log('component is mounted!')
        })
    }
}

1.2 性能优化

  • 重构了虚拟 DOM,保持兼容性,使 DOM 脱离模版渲染,提升性能。
  • 优化了模版编译过程,增加 patchFlag,遍历节点的时候会跳过静态节点。
  • 高效的组件初始化。
  • 组件 upload 的过程性能提升 1.3 ~ 2 倍。
  • SSR 速度提升 2 ~ 3 倍。

Vue3如何实现 diff 和 vdom 的优化?

1)编译模版的静态标记

举例:

<div id="app">
    <p>周一呢</p>
    <p>明天就周二了</p>
    <div>{{ week }}</div>
</div>

在 Vue2 会被解析成以下代码

function render() {
    with(this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('p', [_v("周一呢")]), _c('p', [_v("明天就周二了")]), _c('div', [_v(_s(week))])])
    }
}

可以看出,两个p标签是完全静态的,以至于在后续的渲染中,其实没有任何变化,但是在Vue2.x中依然会使用_c新建成一个vdom,在diff的时候仍然需要去比较,这样就造成了性能消耗。

在 Vue3 中

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("p", null, "周一呢"),
        _createVNode("p", null, "明天就周二了"),
        _createVNode("div", null, _toDisplayString(_ctx.week), 1 /* TEXT */)
    ]))
}

只有当_createVNode的第四个参数不为空的时候,这时,才会被遍历,而静态节点就不会被遍历到。

同时我们可以发现在Vue3最后一个非静态的节点编译后,出现/* TEXT */,这是为了标记当前内容的类型以便进行diff,只需要去比较相同类型的内容,这样就不会浪费时间对其他类型进行遍历了。

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 // 表示具有动态插槽的元素
}

2)事件储存

注:绑定的事件会储存在缓存中

<div id="app">
    <button @click="handleClick">周五啦</button>
</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("button", {
            onClick: _cache[1] || (_cache[1] = ($event, ...args) => (_ctx.handleClick($event, ...args)))
        }, "周五啦")
    ]))
}

在代码中我们可以看出在绑定点击事件的时候,会生成并缓存了一个内联函数在 cache 中,变成一个静态的节点。

3)静态提升

<div id="app">
    <p>周一了</p>
    <p>周二了</p>
    <div>{{ week }}</div>
    <div :class="red: isRed">周三呢</div>
</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("p", null, "周一了", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "周二了", -1 /* HOISTED */)

export function render(_ctx, _cache) {
    return (_openBlock(), _createBlock("div", _hoisted_1, [
        _hoisted_2,
        _hoisted_3,
        _createVNode("div", null, _toDisplayString(_ctx.week), 1 /* TEXT */),
        _createVNode("div", {
            class: { red: _ctx.isRed }
        }, "周三呢", 2 /* CLASS */)
    ]))
}

在这里可以看出来将一些静态的节点放在 render 函数的外部,这样就避免了每次 render 都会去生成一次静态节点。

1.3 提供了 tree shaking

  • 打包的时候自动去除没用到的 Vue 模块

1.4 更好的 ts 支持

  • 类型定义提示
  • tsx 支持
  • class 组件的支持

1.5 全家桶修改

vite 的使用,放弃原来 Vue2.x 使用的 webpack。

  1. 开发服务器启动后不需要进行打包操作。
  2. 可以自定义开发服务器:const { createSever } = require('vite')
  3. 热模块替换的性能和模块数量无关,替换变快,即时替换。
  4. 生产环境和 rollup 捆绑。

二、Vue2 和 Vue3 响应式对比

2.1 Vue2.x 使用的是 defineProperty,有两个难解决的问题

  1. 只能做第一层属性的响应,再往深处就无法实现。
  2. 数组问题:defineProperty 无法检测数组长度的变化,准确的说,是无法检测到通过改变 length 的方法而增加的长度。
// length 的属性被初始化成为了
enumberable: false
configurable: false
writable: true

// 所以说直接去删除或者修改 length 属性是不行的
var a = [1, 2, 3]
Object.defineProperty(a, 'length', {
    enumberable: true,
    configurable: true,
    writable: true,
})

// Uncaught TypeError: Cannot redefine property: length 

2.2 Vue3 使用的是 Proxy 和 Reflect,直接代理整个对象

function reactive(data) {
    if(typeof data !== 'object' || data === null) {
        return data
    }
    const observed = new Proxy(data, {
        get(target, key, receiver) {
            // Reflect 有返回值不报错
            let ret = Reflect.get(target, key, receiver)
            
            // 多层代理
            return typeof ret !== 'object' ? ret : reactive(ret)
        },
        set(target, key, value, receiver) {
            effective()
            // Proxy + reflect
            const ret = Reflect.set(target, key, value, receiver)
            return ret
        },
        deleteProperty(target, key) {
            const ret = Reflect.deleteProperty(target, key)
            return ret
        }
    })
    return observed
}

2.3 总结

  1. 由于Object.defineProperty 只能对属性进行挟持,所以需要遍历对象的每一个属性,性能较低,而 Proxy 是直接代理整个对象。
  2. Object.defineProperty 对新增属性需要手动进行 Observe。在新增属性时,Object.defineProperty 需要重新遍历对象,对其新增的属性进行挟持。所以使用 Vue2 给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
  3. Proxy 支持13中拦截操作,这是 defineProperty 所不具有的新标准性能红利。
  4. Proxy 作为新标准,长远来看Js引擎肯定会继续优化 Proxy,但 getter 和 setter 基本不会再有针对性的优化了。
  5. Proxy 兼容性差,目前并没有一个完整支持 Proxy 所以拦截方法的 Polyfill 方案。