今天给各位分享vue3响应式的知识,其中也会对vue3响应式原理进行解释,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在开始吧!
本文目录一览:
vue3响应式数据原理
Effect 原理解析 与 实现
引言:
vue、react 框架的核心都是数据驱动视图也就是model = view,实现的核心也就是 数据响应。
主要就三步:
一、effect:副作用函数
1.类似于vue2.0中watch 的升级版,如果函数中用到的响应式的数据发生了变化,则会执行该函数
二、proxy 与reflect
Object.defineProperty API 的一些缺点:
vue3源码的调试方法:
三、响应式api reactive的实现
三、Effect的依赖收集与响应触发 (分-总-分-问题)
手写 Vue3 响应式系统:核心就一个数据结构
响应式是 Vue 的特色,如果你简历里写了 Vue 项目,那基本都会问响应式实现原理。
而且不只是 Vue,状态管理库 Mobx 也是基于响应式实现的。
那响应式是具体怎么实现的呢?
与其空谈原理,不如让我们来手写一个简易版吧。
响应式
首先,什么是响应式呢?
响应式就是被观察的数据变化的时候做一系列联动处理。
就像一个 社会 热点事件,当它有消息更新的时候,各方媒体都会跟进做相关报道。
这里 社会 热点事件就是被观察的目标。
那在前端框架里,这个被观察的目标是什么呢?
很明显,是状态。
状态一般是多个,会通过对象的方式来组织。所以,我们观察状态对象的每个 key 的变化,联动做一系列处理就可以了。
我们要维护这样的数据结构:
图片
状态对象的每个 key 都有关联的一系列 effect 副作用函数,也就是变化的时候联动执行的逻辑,通过 Set 来组织。
每个 key 都是这样关联了一系列 effect 函数,那多个 key 就可以放到一个 Map 里维护。
这个 Map 是在对象存在的时候它就存在,对象销毁的时候它也要跟着销毁。(因为对象都没了自然也不需要维护每个 key 关联的 effect 了)
而 WeakMap 正好就有这样的特性,WeakMap 的 key 必须是一个对象,value 可以是任意数据,key 的对象销毁的时候,value 也会销毁。
所以,响应式的 Map 会用 WeakMap 来保存,key 为原对象。
这个数据结构就是响应式的核心数据结构了。
比如这样的状态对象:
const obj = {
a: 1,
b: 2
}
它的响应式数据结构就是这样的:
const depsMap = new Map();
const aDeps = new Set();
depsMap.set('a', aDeps);
const bDeps = new Set();
depsMap.set('b', bDeps);
const reactiveMap = new WeakMap()
reactiveMap.set(obj, depsMap);
创建出的数据结构就是图中的那个:
图片
图片
然后添加 deps 依赖,比如一个函数依赖了 a,那就要添加到 a 的 deps 集合里:
effect(() = {
console.log(obj.a);
});
也就是这样:
const depsMap = reactiveMap.get(obj);
const aDeps = depsMap.get('a');
aDeps.add(该函数);
这样维护 deps 功能上没啥问题,但是难道要让用户手动添加 deps 么?
那不但会侵入业务代码,而且还容易遗漏。
所以肯定不会让用户手动维护 deps,而是要做自动的依赖收集。
那怎么自动收集依赖呢?
读取状态值的时候,就建立了和该状态的依赖关系,所以很容易想到可以代理状态的 get 来实现。
通过 Object.defineProperty 或者 Proxy 都可以:
const data = {
a: 1,
b: 2
}
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
get(targetObj, key) {
let depsMap = reactiveMap.get(targetObj);
if (!depsMap) {
reactiveMap.set(targetObj, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
return targetObj[key]
}
})
effect 会执行传入的回调函数 fn,当你在 fn 里读取 obj.a 的时候,就会触发 get,会拿到对象的响应式的 Map,从里面取出 a 对应的 deps 集合,往里面添加当前的 effect 函数。
这样就完成了一次依赖收集。
当你修改 obj.a 的时候,要通知所有的 deps,所以还要代理 set:
set(targetObj, key, newVal) {
targetObj[key] = newVal
const depsMap = reactiveMap.get(targetObj)
if (!depsMap) return
const effects = depsMap.get(key)
effects effects.forEach(fn = fn())
}
基本的响应式完成了,我们测试一下:
图片
打印了两次,第一次是 1,第二次是 3。
effect 会先执行一次传入的回调函数,触发 get 来收集依赖,这时候打印的 obj.a 是 1
然后当 obj.a 赋值为 3 后,会触发 set,执行收集的依赖,这时候打印 obj.a 是 3
依赖也正确收集到了:
图片
结果是对的,我们完成了基本的响应式!
当然,响应式不会只有这么点代码的,我们现在的实现还不完善,还有一些问题。
比如,如果代码里有分支切换,上次执行会依赖 obj.b 下次执行又不依赖了,这时候是不是就有了无效的依赖?
这样一段代码:
const obj = {
a: 1,
b: 2
}
effect(() = {
console.log(obj.a ? obj.b : 'nothing');
});
obj.a = undefined;
obj.b = 3;
第一次执行 effect 函数,obj.a 是 1,这时候会走到第一个分支,又依赖了 obj.b。
把 obj.a 修改为 undefined,触发 set,执行所有的依赖函数,这时候走到分支二,不再依赖 obj.b。
把 obj.b 修改为 3,按理说这时候没有依赖 b 的函数了,我们执行试一下:
图片
第一次打印 2 是对的,也就是走到了第一个分支,打印 obj.b
第二次打印 nothing 也是对的,这时候走到第二个分支。
但是第三次打印 nothing 就不对了,因为这时候 obj.b 已经没有依赖函数了,但是还是打印了。
打印看下 deps,会发现 obj.b 的 deps 没有清除
图片
所以解决方案就是每次添加依赖前清空下上次的 deps。
怎么清空某个函数关联的所有 deps 呢?
记录下就好了。
我们改造下现有的 effect 函数:
let activeEffect
function effect(fn) {
activeEffect = fn
fn()
}
记录下这个 effect 函数被放到了哪些 deps 集合里。也就是:
let activeEffect
function effect(fn) {
const effectFn = () = {
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
对之前的 fn 包一层,在函数上添加个 deps 数组来记录被添加到哪些依赖集合里。
get 收集依赖的时候,也记录一份到这里:
图片
这样下次再执行这个 effect 函数的时候,就可以把这个 effect 函数从上次添加到的依赖集合里删掉:
图片
cleanup 实现如下:
function cleanup(effectFn) {
for (let i = 0; i effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
effectFn.deps 数组记录了被添加到的 deps 集合,从中删掉自己。
全删完之后就把上次记录的 deps 数组置空。
我们再来测试下:
图片
无限循环打印了,什么鬼?
问题出现在这里:
图片
set 的时候会执行所有的当前 key 的 deps 集合里的 effect 函数。
而我们执行 effect 函数之前会把它从之前的 deps 集合中清掉:
图片
执行的时候又被添加到了 deps 集合。
这样 delete 又 add,delete 又 add,所以就无限循环了。
解决的方式就是创建第二个 Set,只用于遍历:
图片
这样就不会无限循环了。
再测试一次:
图片
现在当 obj.a 赋值为 undefined 之后,再次执行 effect 函数,obj.b 的 deps 集合就被清空了,所以需改 obj.b 也不会打印啥。
看下现在的响应式数据结构:
图片
确实,b 的 deps 集合被清空了。
那现在的响应式实现是完善的了么?
也不是,还有一个问题:
如果 effect 嵌套了,那依赖还能正确的收集么?
首先讲下为什么要支持 effect 嵌套,因为组件是可以嵌套的,而且组件里会写 effect,那也就是 effect 嵌套了,所以必须支持嵌套。
我们嵌套下试试:
effect(() = {
console.log('effect1');
effect(() = {
console.log('effect2');
obj.b;
});
obj.a;
});
obj.a = 3;
按理说会打印一次 effect1、一次 effect2,这是最开始的那次执行。
然后 obj.a 修改为 3 后,会触发一次 effect1 的打印,执行内层 effect,又触发一次 effect2 的打印。
也就是会打印 effect1、effect2、effect1、effect2。
我们测试下:
图片
打印了 effect1、effet2 这是对的,但第三次打印的是 effect2,这说明 obj.a 修改后并没有执行外层函数,而是执行的内层函数。
为什么呢?
看下这段代码:
图片
我们执行 effect 的时候,会把它赋值给一个全局变量 activeEffect,然后后面收集依赖就用的这个。
当嵌套 effect 的时候,内层函数执行后会修改 activeEffect 这样收集到的依赖就不对了。
怎么办呢?
嵌套的话加一个栈来记录 effect 不就行了?
也就是这样:
图片
执行 effect 函数前把当前 effectFn 入栈,执行完以后出栈,修改 activeEffect 为栈顶的 effectFn。
这样就保证了收集到的依赖是正确的。
这种思想的应用还是很多的,需要保存和恢复上下文的时候,都是这样加一个栈。
我们再测试一下:
图片
现在的打印就对了。
至此,我们的响应式系统就算比较完善了。
全部代码如下:
const data = {
a: 1,
b: 2
}
let activeEffect
const effectStack = [];
function effect(fn) {
const effectFn = () = {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
fn()
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
for (let i = 0; i effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
get(targetObj, key) {
let depsMap = reactiveMap.get(targetObj)
if (!depsMap) {
reactiveMap.set(targetObj, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps);
return targetObj[key]
},
set(targetObj, key, newVal) {
targetObj[key] = newVal
const depsMap = reactiveMap.get(targetObj)
if (!depsMap) return
const effects = depsMap.get(key)
// effects effects.forEach(fn = fn())
const effectsToRun = new Set(effects);
effectsToRun.forEach(effectFn = effectFn());
}
})
总结
响应式就是数据变化的时候做一系列联动的处理。
核心是这样一个数据结构:
图片
最外层是 WeakMap,key 为对象,value 为响应式的 Map。这样当对象销毁时,Map 也会销毁。
Map 里保存了每个 key 的依赖集合,用 Set 组织。
我们通过 Proxy 来完成自动的依赖收集,也就是添加 effect 到对应 key 的 deps 的集合里。set 的时候触发所有的 effect 函数执行。
这就是基本的响应式系统。
但是还不够完善,每次执行 effect 前要从上次添加到的 deps 集合中删掉它,然后重新收集依赖。这样可以避免因为分支切换产生的无效依赖。
并且执行 deps 中的 effect 前要创建一个新的 Set 来执行,避免 add、delete 循环起来。
此外,为了支持嵌套 effect,需要在执行 effect 之前把它推到栈里,然后执行完出栈。
解决了这几个问题之后,就是一个完善的 Vue 响应式系统了。
当然,现在虽然功能是完善的,但是没有实现 computed、watch 等功能,之后再实现。
最后,再来看一下这个数据结构,理解了它就理解了 vue 响应式的核心:
图片
vue3源码解读--data响应式的处理
目录
vue2源码
vue3源码
示例
源码
上一节,我们已经看到了组件被挂载到页面的流程。但是忽略了对options的处理。vue2中是在组件init过程中就对配置项进行了合并处理,vue3中是在组件创建即setupComponent中执行applyOptions做的这个事情
经过对组件实例的解析操作,最终拿到的dataOptions即我们实例中的data函数。可以看到,在vue3已经不允许根组件定义为对象了
显然reactive即响应式实现的关键,顺着调用关系找到createReactiveObject函数。常听人提起的Proxy赫然立于眼前
proxy和Object.defineProperty差不多,都可以拦截对象的访问和修改操作,那么接下来的重点就是看下它是如何做依赖收集和派发更新的即可
当前targetType=1,使用baseHandlers做处理器,即createGetter函数。当组件render过程中将会触发msg的访问执行到此
可以看到,如果msg是一个对象,则会递归reactive。为什么要递归处理呢?proxy不是已经把整个对象都代理到了吗?
写一个例子看一下
如果正常访问,我没有问题的,每次都可以正确的触发get
如果是拿到返回值再进行访问的话,res.c就是无效的
把第二个值也转为Proxy后就可以了
回到正题,进入track函数
在trackEffects中完成依赖收集
同样的,当set newValue时将执行到createSetter。这将执行trigger进行更新派发
总结
拿到options的data--视情况(如对象递归执行)进行reactive化--在访问时收集依赖(ReactiveEffect)--设置值时派发更新
proxy.x相当于让activeEffect去订阅data的change,proxy.x = y则相当于data发布更新,通知activeEffect执行componentUpdateFn --观察者模式
聊一聊 Vue3 中响应式原理
Vue.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响应式的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于vue3响应式原理、vue3响应式的信息别忘了在本站进行查找喔。