``一、vue3概述

Vue是一套构建用户界面的渐进式框架,为MVVM(Model-View-ViewModel)架构,架构视图如下:
image.png

  • View(视图)层:称为UI用户界面,即提供给用户可视化的界面
  • ViewModel(业务逻辑层):处理业务逻辑,如js、ts代码
  • Model(数据)层:涉及与数据存储进行交互存储的部分,以及对数据的增删改查

当用户通过view层触发某些数据改动,则view通知viewmodel层,由viewmodel层去改变Model数据,
反过来当数据发生改变时,通过viewmodel层去改变view视图的内容。

二、Vue2与Vue3开发对比-组合式APi与Options-Api

Vue3中引入了组合式APi的开发模式,如下图:
image.png
options api的书写风格比较分散,data中定义响应式变量,在methods中定义方法,在watch中监听等等,所以一个功能会分散开来。

组合式api的特点是,将特定功能的相关响应式数据、函数方法集合到一起,这样在后期维护时,可以快速定位到某个功能的所有代码,若该功能代码量十分大,还可以进行逻辑拆分处理,如下图,详细介绍见Vue3 -- 组合式API_北辰.two的博客-CSDN博客_vue3组合式apiimage.png

三、vue3新特性

1. 重写双向数据绑定

vue2实现双向绑定

  • 在vue2中,双向绑定基于Object.defineProperty()实现对一个对象的劫持:

以下为vue2的/src/core/observe/index.ts(vue2版本为2.7.10)中尤大实现响应式数据的实现:
code.png
可以看到在30行使用了Object.defineProperty()、33与64行设置了get和set,来实现对目标对象的劫持,从而实现双向绑定。

Object.defineProperty是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小。将它替换为es6的Proxy,在目标对象之上架了一层拦截,代理的是对象而不是对象的属性。这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大。
javascript引擎在解析的时候希望对象的结构越稳定越好,如果对象一直在变,可优化性降低,proxy不需要对原始对象做太多操作。
object.defineProperty方法可以对指定对象上创建一个新属性或者修改一个已有属性,实现对数据对象的属性的数据劫持,通过set和get使得劫持的数据对象具有响应能性,可以成为响应式对象。

在vue2中还重写了数组的方法,如push、pop方法等。

object.defineProperty的劣势

Vue2和Vue3响应式区别和理解_codeMing_的博客-CSDN博客_vue2和vue3响应式的区别
object.defineProperty(obj,prop,desc)在实现双向绑定时具有以下缺陷:

  • object.defineProperty对数组支持不是很好,因此在vue2中对数组的操作方法进行了重写,当通过下标或者length修改数组时object.defineProperty无法监听到直接修改数组的操作。(需要使用filter、map、concat、slice等vue操作方法生成新数组并对其赋值)。
  • object.defineProperty直接操作对象的属性,颗粒度较小,性能开销较大,若对象中属性过多,需要给每个属性都绑定defineProperty,效率低下(而proxy代理劫持的是目标对象而不是属性,使得对目标对象属性的操作变为 )。
  • 只能监听对象属性的修改、读取,如果进行删除、增加操作,则无法响应式刷新页面(解决方案是通过Vue.set(this.obj,key,value)vue.delete(this.obj,key)等方法对监听对象进行操作)。

    vue3实现双向绑定

    相比于vue2中使用Object.defineProperty()来实现数据响应式,在vue3中,使用基于es6的Proxy+Reflect结合来劫持对象实现了双向绑定,代码如下(vue3版本为3.2.40中的packges/reactivity/src/reactive.ts):
    vue3-proxy.png
    vue3的响应性就是基于Proxy实现,vue3响应性的特点是

  1. 当一个值被读取时进行追踪
  2. 当某个值改变时进行检测
  3. 重新运行代码来读取原始值

相关对象有effectreactiveRef这些语句在vue3中可以声明响应式数据,在后续会介绍详细用法。

Proxy介绍

在es6中添加了Proxy类,意为代理,可以监听拦截目标对象,通过创建一个代理对象new Proxy()避免直接操作对象,通过操作代理对象,实现对目标对象的增删改查操作以及监听等等行为。

可以理解为Proxy是一个拦截层,当需要拦截代理一个目标对象时,外部想要访问目标对象,必须要经过创建的proxy实例对象,外部通过proxy对象可以实现对目标对象的访问、修改、过滤等操作,例如设置满足某种条件才能成功赋值、限制外部无法访问修改以"_"开头的变量。
使用方法为:new Proxy(target,handle)

  • target指要代理的对象。
  • handler中定义了基本操作的逻辑,常用的有:

    • get(target, propKey, receiver) 拦截对象属性的读取
    • set(target, propKey, value, receiver) 拦截对象属性的设置
    • 其他的代理方法:has、deleteProperty、ownKeys、getOwnPropertyDescriptor等等

    Reflect介绍

    Reflect意为反射,是一个对象不是类,它的属性定义了一套操作Object的API,与底层的操作对应,可以复制即有对象的特性,这些方法是静态方法,可以像Math.round()这样不用new即可调用。

Reflect对象的静态方法一一映射了proxy的处理器方法,Reflect提供的所有静态方法和Proxy第2个handle参数方法相同。Reflect方法与传统对象操作方法映射表如下:
image.png
刚刚提到了,Reflect 下的方法与Proxy下的handle参数方法相同,那么reflect存在的意义是什么呢?

  • Reflect操作的方法会有返回值,例如Reflect.set()的返回值是true或者false,相比于传统对象赋值input.prop = 'xxx'之后无法知道是否赋值成功(有时赋值会失败)需要额外的操作检查赋值成功,而Reflect操作有返回值,可以知道操作是否执行成功。
  • 另一个存在的意义是,Reflect.set()与Reflect.get()中的可选参数receiver,这个参数可以理解成函数调用过程中的this,具体作用可以看接下来的一个例子:

    let obj = { // 传统对象
    foo: 1,
    get bar() {
      return this.foo;
    },
    };
    
    let proxyObj = new Proxy(obj, { // 代理obj对象
    get(target, prop, receiver) {
      return target[prop] // 这里并没有使用reflect方法
    },
    });
    
    let newObj = {
    __proto__: proxyObj, // 将newObj指向proxyObj的内存,相当于newObj继承了proxyObj
    foo: 2
    }
    
    console.log(newObj.bar); // 1

    上面这段代码的作用是:新建一个obj对象,其foo为1,并且有一个bar方法,返回this.foo属性的值,接着声明了一个proxyObj来代理obj对象,再声明一个newObj指向了proxyObj对象,同时改写foo属性的值为2。

原意是期望通过改写foo值之后,打印出来的值为2,但是实际打印出来的值为1。

现在分析一下流程,第19行console.log执行时,读取newObj.bar属性,newObj.bar是一个访问器属性,因此经过proxyObj的代理,访问的是obj中的getter函数,返回this.foo从而读取了foo属性,因此我们认为副作用函数(操作符负效应指的是对它们求值可能会影响将来求值的结果,如赋值操作符、递增减操作符、delete操作符,副作用函数是指内部使用了负效应的操作符)与属性foo之间也应该建立联系,当我们修改newObj.foo的值时应该能够触发响应,使得副作用函数重新执行才对,然而实际并非如此,当尝试修改newObj.foo时,在上段代码的console.log前一行插入newObj.foo++;得到的结果仍然为1(而不是我们想的1先改成2再加1为3,所以上方代码代理时失去了响应式)。

副作用函数(newObj.foo++;)并没有执行,那么问题出现在哪里?

其实问题就在obj对象中的getter访问器函数,里面的this指向的是谁?首先第10行的target是原始对象obj,所以target[key]相当于obj.bar,而obj.bar是一个访问器属性,它返回了this.foo的值,这里的this指的是传递过来的原始对象obj,因此this.foo的值是1,而不是改写之后的值2。

想要实现预期改写的值,就轮到reflect的reciver发挥作用,修改代码如下:

let obj = { // 传统对象
    foo: 1,
    get bar() {
        return this.foo;
    },
};

let proxyObj = new Proxy(obj, { // 代理obj对象
    get(target, prop, receiver) {
        // return target[prop]
        return Reflect.get(target, prop, receiver);
    },
});

let newObj = {
    __proto__: proxyObj, // 将proxyObj内存指向proxyObj,相当于newObj继承了proObj
    foo: 2
}

console.log(newObj.bar); // 2

重点在第11行,使用Reflect.get(target, prop, receiver)代替target[prop],这样代理对象的get拦截函数接受到receiver参数,它代表谁在读取属性,例如此处的newObj代理对象在读取bar属性,所以receiver就是newObj,所以访问器属性bar的getter函数内的this指向的是代理对象newObj。可以看到this的指向由原始变量obj变成了代理对象newObj,从而使得副作用函数与响应式数据间建立了联系。

Proxy get/set()方法需要的返回值正是Reflect的get/set方法的返回值(proxy对象支持的操作就是Reflect定义的API),可以天然配合使用,比直接对象赋值/获取值要更方便和准确。

2. vdom性能优化

Vdom (虚拟dom)凭借着出色的性能成为了目前的主流的前端框架的渲染方案。在vue3中对Vdom进行了重写,
在Vue2中,每次更新diff,都是全量对比,Vue3则只对比带有标记的,这样大大减少了非动态内容的对比消耗
首先介绍一个网站Vue Template Explorer,这个网站是官方演示Vdom的示例网站。

在演示界面中创建一组静态dom:

<div>Hello World</div>
<div>Hello World</div>
<div>{{msg}}</div>
<div class="a" :id='item'>{{item}}</div>

观察其编译后的Vdom:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("div", null, "Hello World"),
    _createElementVNode("div", null, "Hello World"),
    _createElementVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
    _createElementVNode("div", {
      class: "a",
      id: _ctx.item
    }, _toDisplayString(_ctx.item), 9 /* TEXT, PROPS */, ["id"])
  ], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST
  • 每个_createElementVNode函数中都是刚刚我们创建的一个dom,例如第5行对应的就是div标签,内容为"Hello World",这样为Vdom最基础的形式。
  • 第7行,我们创建的是一个动态的dom元素,Vdom除了模拟基础的信息外,还添加了一个标记1 /* TEXT */,标记成为patch flag(静态标记)
  • 8-12行编译对应的<div class="a" :id='item'>{{item}}</div>可以看到class="a"是静态的因此没有添加patchflag,而id属性因为是可能发生变化,因此打上了PROPS的静态标签。

    patch flag(静态标记)
    patchFlag在创建vnode的时候,会根据vnode的内容是否是动态可以变化,为其添加PatchFlag。之后在diff的时候,只会比较且更新有PatchFlag的节点,而不会去做全量更新,大大提高了效率。

除了上面提到的1 /* TEXT */在vue3源码的packages/shared/src/patchFlags.ts中还枚举了很多patchFlag,总结如下:

export const enum PatchFlags {
  TEXT = 1,// 1 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512  表示只需要non-props修补的元素 (non-props不知道怎么翻才恰当~)
  DYNAMIC_SLOTS = 1 << 10,  // 1024 动态的solt
  DEV_ROOT_FRAGMENT = 1 << 11, //2048 表示仅因为用户在模板的根级别放置注释而创建的片段。 这是一个仅用于开发的标志,因为注释在生产中被剥离。

  //以下两个是特殊标记
  HOISTED = -1,  // 表示已提升的静态vnode,更新时调过整个子树
  BAIL = -2 // 指示差异算法应该退出优化模式
}

通过位运算符做一些枚举,每一个patchflag对应的是一个位运算符计算之后的数,之后根据这个数去做对应的处理更新。

3. 支持fragments

在vue2中每个页面的开发只允许在一个根结点内开发,而在vue3中允许我们支持多个根节点,也就是可以这样写页面:

/* App.vue */
  <template>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </template>

  <script>
    export default {};
  </script>

或者:

// app.js
import { defineComponent, h, Fragment } from 'vue';

export default defineComponent({
  render() {
    return h(Fragment, {}, [
      h('header', {}, ['...']),
      h('main', {}, ['...']),
      h('footer', {}, ['...']),
    ]);
  }
});

这样就不用像vue2中很多时候会添加一些没有意义的节点用于包裹,实现fragments底层原理就是做了一个虚拟dom,当页面中出现多个根结点,那么编译时 vue 会在这些元素节点上添加一个 <Fragment></Fragment> 标签。

同时还支持了JSX和TSX写法,新增Suspense teleport 和多 v-model 用法,后续展开介绍,留个坑(东西太多了,泪目)。

4. 支持Tree-Shaking的支持

在vue3中引入tree-shaking机制,将全局api分块,按需注入编译(而在vue2中,因为是option api开发模式,即使不使用的vue功能函数也会被打包),有效降低打包之后的体积。
例如在vue3中使用到了computed功能函数,需要使用ES6 模块系统import/export引入:import { computed } from "vue";,如果没有使用computed,则在编译时不会把相关函数打包进去。

5. Composition Api

新增了Setup 语法糖式编程,引入新的响应式编程方法,如ref、reactive、toRefs、toRaws、watch、computed,后续再详细介绍。

参考资料

学习Vue3 第一章_小满zs的博客-CSDN博客
ref、reactive相关

reflect与proxy相关

Vdom相关

最后修改:2024 年 03 月 14 日
如果觉得我的文章对你有用,请随意赞赏