聊一聊 Vue3 中响应式原理
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊一聊 Vue3 中响应式原理相关的知识,希望对你有一定的参考价值。
参考技术AVue.js 3.0 "One Piece" 正式发布已经有一段时间了,真可谓是千呼万唤始出来啊!
相比于 Vue2.x , Vue3.0 在新的版本中提供了更好的性能、更小的捆绑包体积、更好的 TypeScript 集成、用于处理大规模用例的新 API 。
在发布之前,尤大大就已经声明了响应式方面将采用 Proxy 对于之前的 Object.defineProperty 进行改写。其主要目的就是弥补 Object.defineProperty 自身的一些缺陷,例如无法检测到对象属性的新增或者删除,不能监听数组的变化等。
而 Vue3 采用了新的 Proxy 实现数据读取和设置拦截,不仅弥补了之前 Vue2 中 Object.defineProperty 的缺陷,同时也带来了性能上的提升。
今天,我们就来盘一盘它,看看 Vue3 中响应式是如何实现的。
The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object. MDN
Proxy - 代理,顾名思义,就是在要访问的对象之前增加一个中间层,这样就不直接访问对象,而是通过中间层做一个中转,通过操作代理对象,来实现修改目标对象。
关于 Proxy 的更多的知识,可以参考我之前的一篇文章 —— 初探 Vue3.0 中的一大亮点——Proxy ! ,这里我就不在赘述。
Vue3 中响应式核心方法就是 reactive 和 effect , 其中 reactive 方法是负责将数据变成响应式, effect 方法的作用是根据数据变化去更新视图或调用函数,与 react 中的 useEffect 有点类似~
其大概用法如下:
默认会执行一次,打印 Hello , 之后更改了 data.name 的值后,会在触发执行一次,打印 World 。
我们先看看 reactive 方法的实现~
reactive.js
首先应该明确,我们应该导出一个 reactive 方法,该方法有一个参数 target ,目的就是将 target 变成响应式对象,因此返回值就是一个响应式对象。
reactive 方法基本结构就是如此,给定一个对象,返回一个响应式对象。
其中 isObject 方法用于判断是否是对象,不是对象不需要代理,直接返回即可。
reactive 方法的重点是 Proxy 的第二个参数 handler ,它承载监控对象变化,依赖收集,视图更新等各项重大责任,我们重点来研究这个对象。
handler.js
在 Vue3 中 Proxy 的 handler 主要设置了 get , set , deleteProperty , has , ownKeys 这些属性,即拦截了对象的读取,设置,删除, in 以及 Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法。
这里我们偷个懒,暂时就考虑 set 和 get 操作。
handler.get()
get 获取属性比较简单,我们先来看看这个,这里我们用一个方法创建 getHanlder 。
这里推荐使用了 Reflect.get 而并非 target[key] 。
可以发现, Vue3 是在取值的时候才去递归遍历属性的,而非 Vue2 中一开始就递归 data 给每个属性添加 Watcher ,这也是 Vue3 性能提升之一。
handler.set()
同理 set 操作,我们也是用一个方法创建 setHandler 。
Reflect.set 会返回一个 Boolean 值,用于判断属性是否设置成功。
完事后将 handler 导出,然后在 reactive 中引入即可。
测试几组对象貌似没啥问题,其实是有一个坑,这个坑也跟数组有关。
如上例子,如果我们选择代理数组,在 setHandler 中打印其 key 和 value 的话会得到 3 4 , length 4 这两组值:
如果不作处理,那么会导致如果更新视图的话,则会触发两次,这肯定是不允许的,因此,我们需要将区分新增和修改这两种操作。
Vue3 中是通过判断 target 是否存在该属性来区分是新增还是修改操作,需要借助一个工具方法 —— hasOwnProperty 。
这里我们将上述的 createSetter 方法修改如下:
如此一来,我们调 push 方法的时候,就只会触发一次更新了,非常巧妙的避免了无意义的更新操作。
effect.js
光上述构造响应式对象并不能完成响应式的操作,我们还需要一个非常重要的方法 effect ,它会在初始化执行的时候存储跟其有关的数据依赖,当依赖数据发生变化的时候,则会再次触发 effect 传递的函数。
其基本雏形如下,入参是一个函数,还有个可选参数 options 方便后面计算属性等使用,暂时不考虑:
createReactiveEffect 就是为了将 fn 变成响应式函数,监控数据变化,执行 fn 函数,因此该函数是一个高阶函数。
createReactiveEffect 将原来的 fn 转变成一个 reactvieEffect , 并将当前的 effect 挂到全局的 activeEffect 上,目的是为了一会与当前所依赖的属性做好对应关系。
我们必须要将依赖属性构造成 prop : [effect,effect] 这种结构,才能保证依赖属性变化的时候,依次去触发与之相关的 effect ,因此,需要在 get 属性的时候,做属性的依赖收集,将属性与 effect 关联起来。
依赖收集 —— track
在获取对象的属性时,会触发 getHandler ,再次做属性的依赖收集,即 Vue2 中的发布订阅。
在 setHandler 中获取属性的时候,做一次 track(target, key) 操作。
整个 track 的数据结构大概是这样
目的就是将 target , key , effect 之间做好对应的关系映射。
打印 targetMap 的结构如下:
**触发更新 —— trigger **
上述已经完成了依赖收集,剩下就是监控数据变化,触发更新操作,即在 setHandler 中添加 trigger 触发操作。
这样一来,获取数据的时候通过 track 进行依赖收集,更新数据的时候再通过 trigger 进行更新,就完成了整个数据的响应式操作。
再回头看看我们先前提到的例子:
控制台会依次打印 Hello ***** effect ***** 以及 World ***** effect ***** , 分别是首次渲染触发跟更新数据重渲染触发,至此功能实现!
整体来说, Vue3 相比于 Vue2 在很多方面都做了调整,数据的响应式只是冰山一角,但是可以看出尤大团队非常巧妙的利用了 Proxy 的特点以及 es6 的数据结构和方法。另外, Composition API 的模式跟 React 在某些程度上有异曲同工之妙,这种设计模式让我们在实际开发使用中更加的方法快捷,值得我们去学习,加油!
最后附上仓库地址 github ,欢迎各位大佬批评斧正~
以上是关于聊一聊 Vue3 中响应式原理的主要内容,如果未能解决你的问题,请参考以下文章
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
Vue3官网-高级指南(十六)vue响应式原理步骤本质(副作用effect响应式状态reactive响应式原始值ref(解包)构造响应值toRefs)proxy(代理this)