``一、vue3概述
Vue是一套构建用户界面的渐进式框架,为MVVM(Model-View-ViewModel)架构,架构视图如下:
- View(视图)层:称为UI用户界面,即提供给用户可视化的界面
- ViewModel(业务逻辑层):处理业务逻辑,如js、ts代码
- Model(数据)层:涉及与数据存储进行交互存储的部分,以及对数据的增删改查
当用户通过view层触发某些数据改动,则view通知viewmodel层,由viewmodel层去改变Model数据,
反过来当数据发生改变时,通过viewmodel层去改变view视图的内容。
二、Vue2与Vue3开发对比-组合式APi与Options-Api
Vue3中引入了组合式APi的开发模式,如下图:
options api的书写风格比较分散,data中定义响应式变量,在methods中定义方法,在watch中监听等等,所以一个功能会分散开来。
组合式api的特点是,将特定功能的相关响应式数据、函数方法集合到一起,这样在后期维护时,可以快速定位到某个功能的所有代码,若该功能代码量十分大,还可以进行逻辑拆分处理,如下图,详细介绍见Vue3 -- 组合式API_北辰.two的博客-CSDN博客_vue3组合式api :
三、vue3新特性
1. 重写双向数据绑定
vue2实现双向绑定
- 在vue2中,双向绑定基于Object.defineProperty()实现对一个对象的劫持:
以下为vue2的/src/core/observe/index.ts(vue2版本为2.7.10)中尤大实现响应式数据的实现:
可以看到在30行使用了Object.defineProperty()、33与64行设置了get和set,来实现对目标对象的劫持,从而实现双向绑定。
Object.defineProperty是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小。将它替换为es6的Proxy,在目标对象之上架了一层拦截,代理的是对象而不是对象的属性。这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大。
javascript引擎在解析的时候希望对象的结构越稳定越好,如果对象一直在变,可优化性降低,proxy不需要对原始对象做太多操作。
object.defineProperty方法可以对指定对象上创建一个新属性或者修改一个已有属性,实现对数据对象的属性的数据劫持,通过set和get使得劫持的数据对象具有响应能性,可以成为响应式对象。
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实现,vue3响应性的特点是
- 当一个值被读取时进行追踪
- 当某个值改变时进行检测
- 重新运行代码来读取原始值
相关对象有effect
、reactive
、Ref
这些语句在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方法与传统对象操作方法映射表如下:
刚刚提到了,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相关
- Vue2和Vue3响应式区别和理解_codeMing_的博客-CSDN博客_vue2和vue3响应式的区别
- Proxy是代理,Reflect是干嘛用的? « 张鑫旭-鑫空间-鑫生活
- JS 反射机制及 Reflect 详解 - Leophen - 博客园
- JS代理对象和反射
- js中的Proxy_拾玥花开的博客-CSDN博客_js proxy
- js中的Proxy
- 一起來了解 Javascript 中的 Proxy 與 Reflect
Vdom相关
此处评论已关闭