写点什么

茶余饭后聊聊 Vue3.0 响应式数据那些事儿

  • 2021-02-01
  • 本文字数:5512 字

    阅读完需:约 18 分钟

茶余饭后聊聊 Vue3.0 响应式数据那些事儿

Vue3.0 为了达到更快、更小、更易于维护、更贴近原生、对开发者更友好的目的,在很多方面进行了重构:


  1. 使用 Typescript

  2. 放弃 class 采用 function-based API

  3. 重构 complier

  4. 重构 virtual DOM

  5. 新的响应式机制


今天咱就聊聊重构后的响应式数据。


尝鲜


重构后的 Vue3.0 和之前在写法上有很大的差别,早前在网络上对于 Vue3.0 这种激进式的重构方式发起了一场讨论,见仁见智。不多说先看看 Vue3.0 在写法上激进到什么程度。


<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <title>Document</title>  <script src="../packages/vue/dist/vue.global.js"></script></head><body>  <div id="app"></div>  <script>    const { reactive, computed, effect, createApp } = Vue    const App = {      template: `        <div id="box">            <button @click="add">{{ state.count }}</button>        </div>      `,      setup() {        const state = reactive({          count: 0        })        function add() {          state.count++        }        effect(() => {          console.log('count改变', state.count);        })        return {          state,          add        }      }    }    createApp().mount(App, '#app')  </script></body></html>
复制代码


确实写法上和 Vue2.x 差别有点大,还整出了个 setup。不过我的第一感觉倒不是写法上的差异,毕竟写过 React,这种写法也没啥特别的。关键是这种响应式数据的写法好像在哪里见过有没有?写过 React 项目的人可能一眼就能看出来,没错就是它 mobx,一种 React 的响应式状态管理插件


import {observable,computed,autorun} from"mobx"var numbers = observable([1,2,3]);var sum = computed(() => numbers.reduce((a, b) => a + b, 0));
var disposer = autorun(() =>console.log(sum.get()));// 输出 '6'numbers.push(4);// 输出 '10'numbers.push(5);
复制代码


再看看 Vue3.0 暴露的这几个和响应式数据相关的方法:


  1. reactive(value)

创建可观察的变量,参数可以是 JS 原始类型、引用、纯对象、类实例、数组、集合(Map|Set)。

  1. effect(fn)

effect 意思是副作用,此方法默认会先执行一次。如果 fn 中有依赖的可观察属性变化时,会再次触发此回调函数

  1. computed(()=>expression)

创建一个计算值,computed 实现也是基于 effect 来实现的,特点是 computed 中的函数不会立即执行,多次取值是有缓存机制的,expression 不应该有任何副作用,而仅仅是返回一个值。当这个 expression 依赖的可观察属性变化时,这个表达式会重新计算。


和 mobx 有异曲同工之妙。


Vue3.0 把创建响应式对象从组件实例初始化中抽离了出来,通过暴露 API 的方式将响应式对象创建的权利交给开发者,开发者可以自由的决定何时何地创建响应式对象,就冲这点 Vue3.0 我先粉了。


重构后的响应式机制带来了哪些改变?


每一个大版本的发布都意味着新功能、新特性的出现,那么重构后的响应式数据部分相比 3.0 之前的版本有了哪些方面的改变呢?下面听我娓娓道来:


对数组的全面监听


Vue2.x 中被大家吐槽的最多的一点就是针对数组只实现了 push,pop,shift,unshift,splice,sort,reverse' 这七个方法的监听,以前通过数组下标改变值的时候,是不能触发视图更新的。这里插一个题外话,很多人认为 Vue2.x 中数组不能实现全方位监听是 Object.defineProperty 不能监听数组下标的改变,这可就冤枉人家了,人家也能侦听数组下标变化的好吗,不信你看


const arr = ["2019","云","栖","音","乐","节"];arr.forEach((val,index)=>{    Object.defineProperty(arr,index,{        set(newVal){            console.log("赋值");        },        get(){            console.log("取值");            return val;        }    })})let index = arr[1];//取值arr[0] = "2050";//赋值
复制代码


没毛病,一切变化都在人家的掌握中。上面这段代码,有没有人没看懂,我假装你们都不懂,贴张图



从数组的数据结构来看,数组也是一个 Key-Value 的键值对集合,只是 Key 是数字罢了,自然也可以通过 Object.defineProperty 来实现数组的下标访问和赋值拦截了。其实 Vue2.x 没有实现数组的全方位监听主要有两方面原因:


  1. 数组和普通对象相比,JS 数组太"多变"了。比如:arr.length=0,可以瞬间清空一个数组;arr[100]=1又可以瞬间将一个数组的长度变为 100(其他位置用空元素填充),等等骚操作。对于一个普通对象,我们一般只会改变 Key 对应的 Value 值,而不会连 key 都改变了,而数组就不一样了 Key 和 Value 都经常增加或减少,因此每次变化后我们都需要重新将整个数组的所有 key 递归的使用 Object.defineProperty 加上 setter 和 getter,同时我们还要穷举每一种数组变化的可能,这样势必就会带来性能开销问题,有的人会觉得这点性能开销算个 x 呀,但是性能问题都是由小变大的,如果数组中存的数据量大而且操作频繁时,这就会是一个大问题。React16.x 在就因为在优化 textNode 的时候,移除了无意义的 span 标签,性能据说都提升了多少个百分点,所以性能问题不可小看。

  2. 数组在应用中经常会被操作,但是通常 push,pop,shift,unshift,splice,sort,reverse 这 7 种操作就能达到目的。因此,出于性能方面的考虑 Vue2.x 做出了一定的取舍。


那么 Vue3.0 怎么又走回头路去实现了数组的全面监听了呢?答案就是 Proxy 和 Reflet 这一对原生 CP 的出现,Vue3.0 使用 Proxy 作为响应式数据实现的核心,用 Proxy 返回一个代理对象,通过代理对象来收集依赖和触发更新。大概的原理像这段代码一样:


const arr = ["2019","云","栖","音","乐","节"];let ProxyArray = newProxy(arr,{    get:function(target, name, value, receiver) {        console.log("取值")        returnReflect.get(target,name);    },    set: function(target, name, value, receiver) {       console.log("赋值")       Reflect.set(target,name, value, receiver);;    } }) const index = ProxyArray[0]; //取值 ProxyArray[0]="2050" //赋值
复制代码


效果和 Object.defineProperty 一样一样的,又显得清新脱俗有没有?而且 Proxy 只要是对象都能代理,后面还会提到。当然 Vue3.0 是虽然有了新欢,但也没忘记旧爱,对于在之前版本中数组的几种方法的监听还是照样支持的。


惰性监听


什么是"惰性监听"?


简单讲就是"偷懒",开发者可以选择性的生成可观察对象。在平时的开发中常有这样的场景,一些页面上的数据在页面的整个生命周期中是不会变化的,这时这部分数据不需要具备响应式能力,这在 Vue3.0 以前是没有选择余地的,所有在模板中使用到的数据都需要在 data 中定义,组件实例在初始化的时候会将 data 整个对象变为可观察对象。


惰性监听有什么好处?


  1. 提高了组件实例初始化速度

    Vue3.0 以前组件实例在初始化的时候会将 data 整个对象变为可观察对象,通过递归的方式给每个 Key 使用 Object.defineProperty 加上 getter 和 settter,如果是数组就重写代理数组对象的七个方法。而在 Vue3.0 中,将可响应式对象创建的权利交给了开发者,开发者可以通过暴露的 reactive, compted, effect 方法自定义自己需要响应式能力的数据,实例在初始化时不需要再去递归 data 对象了,从而降低了组件实例化的时间。

  2. 降低了运行内存的使用

    Vue3.0 以前生成响应式对象会对对象进行深度遍历,同时为每个 Key 生成一个 def 对象用来保存 Key 的所有依赖项,当 Key 对应的 Value 变化的时候通知依赖项进行 update。但如果这些依赖项在页面整个生命周期内不需要更新的时候,这时 def 对象收集的依赖项不仅没用而且还会占用内存,如果可以在初始化 data 的时候忽略掉这些不会变化的值就好了。Vue3.0 通过暴露的 reactive 方法,开发者可以选择性的创建可观察对象,达到减少依赖项的保存,降低了运行内存的使用。


Map、Set、WeakSet、WeakMap 的监听


前面提到 Proxy 可以代理所有的对象,立马联想到了 ES6 里面新增的集合 Map、Set, 聚合类型的支持得益于 Proxy 和 Reflect。讲真的这之前还真不知道 Proxy 这么刚啥都能代理,二话不说直接动手用 Proxy 代理了一个 map 试试水


let map = newMap([["name","zhengcaiyun"]])let mapProxy = newProxy(map, {  get(target, key, receiver) {    console.log("取值:",key)    returnReflect.get(target, key, receiver)  }})mapProxy.get("name")
复制代码


Uncaught TypeError: Method Map.prototype.get called on incompatible receiver [object Object]


一盆凉水泼来,报错了。原来 Map、Set 对象赋值、取值和他们内部的 this 指向有关系,但这里的 this 指向的是其实是 Proxy 对象,所以得这样干


let map = newMap([['name','wangyangyang']])let mapProxy = newProxy(map, {  get(target, key, receiver) {    var value = Reflect.get(...arguments)     console.log("取值:",...arguments)    returntypeof value == 'function' ? value.bind(target) : value  }})mapProxy.get("name")
复制代码


当获取的是一个函数的时候,通过作用域绑定的方式将原对象绑定到 Map、Set 对象上就好了。


Vue3.0 是如何实现集合类型数据监听的?


眼尖的同学看完上面这段代码会发现一个问题,集合是没有 set 方法,集合赋值用的是 add 操作,那咋办呢?来看看那么 Vue3.0 是怎么处理的,上一段简化后的源码


function reactive(target: object) {  return createReactiveObject(    target,    rawToReactive,    reactiveToRaw,    mutableHandlers,    mutableCollectionHandlers  )}
function createReactiveObject( target: any, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) { //collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet]) const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers //生成代理对象 observed = newProxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { targetMap.set(target, newMap()) } return observed}
复制代码


根据 target 类型适配不同的 handler,如果是集合 (Map、Set)就使用 collectionHandlers,是其他类型就使用 baseHandlers。接下来看看 collectionHandlers


export const mutableCollectionHandlers: ProxyHandler<any> = {  get: createInstrumentationGetter(mutableInstrumentations)}export const readonlyCollectionHandlers: ProxyHandler<any> = {  get: createInstrumentationGetter(readonlyInstrumentations)}
复制代码


没有意外只有 get,骚就骚在这儿:


// 可变数据插桩对象,以及一系列相应的插桩方法const mutableInstrumentations: any = {  get(key: any) {    returnget(this, key, toReactive)  },  get size() {    return size(this)  },  has,  add,  set,  delete: deleteEntry,  clear,  forEach: createForEach(false)}// 迭代器相关的方法const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]iteratorMethods.forEach(method => {  mutableInstrumentations[method] = createIterableMethod(method, false)  readonlyInstrumentations[method] = createIterableMethod(method, true)})// 创建getter的函数function createInstrumentationGetter(instrumentations: any) {  returnfunction getInstrumented(    target: any,    key: string | symbol,    receiver: any  ) {    target =      hasOwn(instrumentations, key) && key in target ? instrumentations : target    returnReflect.get(target, key, receiver)  }}
复制代码


由于 Proxy 的 traps 跟 Map|Set 集合的原生方法不一致,因此无法通过 Proxy 劫持 set,所以作者在在这里进行了"偷梁换柱",这里新创建了一个和集合对象具有相同属性和方法的普通对象,在集合对象 get 操作时将 target 对象换成新创建的普通对象。这样,当调用 get 操作时 Reflect 反射到这个新对象上,当调用 set 方法时就直接调用新对象上可以触发响应的方法,是不是很巧妙?所以多看源码好处多多,可以多学学人家的骚操作。


IE 怎么办?


这是个实在不想提但又绕不开的话题,IE 在前端开发者眼里和魔鬼没什么区别。在 Vue3.0 之前,响应式数据的实现是依赖 ES5 的 Object.defineProperty,因此只要支持 ES5 的浏览器都支持 Vue,也就是说 Vue2.x 能支持到 IE9。Vue3.0 依赖的是 Proxy 和 Reflect 这一对出生新时代的 CP,且无法被转译成 ES5,或者通过 Polyfill 提供兼容,这就尴尬了。开发者技术前线获悉的信息,官方在发布最终版本之前会做到兼容 IE11,至于更低版本的 IE 那就只有送上一曲凉凉了。


其实也不用太纠结 IE 的问题,因为连微软自己都已经放弃治疗 IE 拥抱 Chromium 了,我们又何必纠结呢?


结语


在使用开源框架时不要忘了,我们之所以能免费试用他,靠的维护者投入的大量精力。希望我们多去发现它带来的优点和作者想通过它传递的编程思想。



头图:Unsplash

作者:赤墨

原文:https://mp.weixin.qq.com/s/vxUHy3Az58T_4MrSz0PR1Q

原文:茶余饭后聊聊 Vue3.0 响应式数据那些事儿

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-02-01 00:073453

评论

发布
暂无评论
发现更多内容

APICloud可视化编程(二)

YonBuilder低代码开发平台

前端开发 APICloud 多端开发

Java | abstract关键字【面向对象的第三大特征——多态】

Fire_Shield

Java 9月月更 abstract

详解数据计算能力的四种类型

穿过生命散发芬芳

9月月更 数据计算

信我,Java面试的苦只吃一次,看完这八股文合集,渣渣也能进大厂

钟奕礼

编程 java;

vivo鲁京辉:数据安全与隐私保护是消费者的基本权利,是企业的“铁律”

Geek_2d6073

架构实战营模块一作业

π

架构实战营

Python语法之异常处理

芯动大师

异常处理 9月月更 Python异常处理方法

Redis进阶知识点(可学习,可复习,可面试)

钟奕礼

java; 编程、

实战案例,个个是经典,不愧是阿里P7私传“并发编程核心讲义”,竟能讲的如此通透

程序知音

Java 并发编程 多线程 高并发 后端技术

挑战Python的语法练习

芯动大师

项目实战 9月月更 模块创建

后疫情时代,RTE“沉浸式”体验还能这么玩?丨RTE 2022 编程挑战赛赛后专访

声网

人工智能

天天都在谈的防火墙到底是个啥,有哪些分类?如何选择防火墙?

wljslmz

网络安全 防火墙 9月月更

模块一作业

知足🐏

01简单架构分析

神奇的叶叔叔

极客时间-架构师训练营作业-模块一

沐の爹

2022-09-28:以下go语言代码输出什么?A:1 1;B:1 2;C:2 2;D:不确定。 package main import ( “fmt“ ) func main() { var

福大大架构师每日一题

golang 福大大 选择题

未来智安加入中国通信标准化协会成为全权会员

未来智安XDR SEC

【编程实践】详解MySQL在Python中的使用(1)

迷彩

Python MySQL 数据库 pymysql 9月月更

Android 开发环境的搭建(Android Studio + 逍遥模拟器)

Android Studio 安卓开发 9月月更

Groovy closure 与Java function转换

FunTester

史上最全的Java容器集合之HashMap(源码解读)

自然

集合 Java core 9月月更

作业一

小虎

架构实战营

【最右】面向TS生态的新型Flutter框架

刘剑

typescript 小程序 移动端 动态化 flutter for web

这些Java基础知识,诸佬们都还记得嘛(学习,复习,面试都可)

钟奕礼

编程 java;

【云原生 | 从零开始学Kubernetes】十二、k8spod的生命周期与容器钩子

泡泡

Docker 云计算 云原生 k8s 9月日更

程序员未来职业规划路线,请收藏

收到请回复

Java 程序员 架构 职业规划 语言 & 开发

【编程实践】详解 MySQL 在 Python 中的使用(2)-pymysql的使用

迷彩

MySQL 数据库 增删改查 pymysql 9月月更 数据库操作

面试中经常问到的几个问题,快来看看能答对几道吧(Java)

钟奕礼

java; 编程、

开发者有话说|要不是家里穷,我也不想当码农

三掌柜

个人成长 个人感悟

史上最全的Java容器集合之equals 和 hashCode

自然

集合 Java core 9月月更

Python语法之模块和包(2)

芯动大师

9月月更 模块创建 包的应用

茶余饭后聊聊 Vue3.0 响应式数据那些事儿_语言 & 开发_政采云前端团队_InfoQ精选文章