本文作者:qiaoqingyi

vue3响应式原理代理与反射(vue3 响应式原理)

qiaoqingyi 2023-01-18 662

本篇文章给大家谈谈vue3响应式原理代理与反射,以及vue3 响应式原理对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。

本文目录一览:

【手把手教你搓Vue响应式原理】(一)初识Vue响应式

在讲这个之前,首先要明白一点,这个所谓的响应式,其实本身就是对 MVVM 的理解。

MVVM 其实就是所谓的 Modal View ViewModal 。

简单理解,就是你的 data 中的数据,和 template 模板中的界面,本身就是两个东西。

但是, Vue 给你做了一层中间的 ViewModal ,让视图上的改变能反映到 data 中, data 中的改变能反映到视图上。

在这个反映过程中,ViewModal就是视图和数据的一个桥梁。

同样是让 a + 1 。

在 Vue 中,这个桥梁是你看不见的,因为 Vue 都帮你完成了视图和数据的变化传递。

而 React 就是侵入式的,因为要显式地声明 setState ,通过它,来设置变量的同时,设置视图的改变。

所以,所谓的侵入式,其实就是对于桥梁的侵入。

所以, Vue 的神奇之处就在于,不需要我们手动地显示调用 setState ,也就是这个桥梁, Vue 已经帮我们桥接上了。

要让 data 改变的同时,视图也发生改变,所以,问题的所在,就是我们需要监听,什么时候,这个变量发生了变量。

然而, ES5 中,就有那么一个特性,可以做到对于数据的劫持(监听)。

它就是 Object.defineProperty 。

Object.defineProperty( obj, prop, descriptor ) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象,与此同时,它可以对 对象的一些额外底层的属性进行设置 。例如可以设置 writable , enumerable , configurable 等属性。

后面的额外属性设置,才是我们使用它的重点。

但是,我们使用的不是上面的几个属性,最主要的还是它的 get set ,可以对属性值的获取和设置操作进行拦截。

get主要是可以对值的获取进行拦截,,它必须要传入一个 return ,并且, 该函数的返回值会被用作属性的值 。我们可以来看一个例子:

由于设置了 get ,所以,输出 a.name 的时候直接会被拦截,走 get() 中的 return 所以,此时, a.name 的值应该是 你已经被拦截了!。

set主要是可以对值的设置进行拦截,该方法会接受一个参数,那就是 被赋予的新值 。我们可以来看一个例子:

由于设置了 set ,所以,设置值的时候会被拦截,走 set() 中的方法。

所以, Vue 能自动获取data中的改变,反映到视图的原因,就是有对于变量的获取和设置的劫持,当变量发生改变的同时, Vue 能在第一时间知道,并且对视图做出相应的改变操作。

而这把钥匙就是 Object.defineProperty 。

【尚硅谷】Vue源码解析之数据响应式原理

Object.defineProperty() - MDN

Proxy(vue响应式原理:数据侦测--数据劫持和数据代理)

Object.defineProperty : 通过设定对象属性getter/setter方法来监听数据的变化,同时getter也用于依赖收集,而setter在数据变更时通知订阅者更新视图。

1.无法检测到对象属性的新增或删除

由于js的动态性,可以为对象追加新的属性或者删除其中某个属性,这点对经过Object.defineProperty方法建立的响应式对象来说,只能追踪对象已有数据是否被修改,无法追踪新增属性和删除属性,这就需要另外处理。

2.不能监听数组的变化

vue在实现数组的响应式时,它使用了一些hack,把无法监听数组的情况通过重写数组的部分方法来实现响应式,这也只限制在数组的push/pop/shift/unshift/splice/sort/reverse七个方法,其他数组方法及数组的使用则无法检测到。

Proxy,字面意思是代理,是ES6提供的一个新的API,用于修改某些操作的默认行为,可以理解为在目标对象之前做一层拦截,外部所有的访问都必须通过这层拦截,通过这层拦截可以做很多事情,比如对数据进行过滤、修改或者收集信息之类。借用 proxy的巧用 的一幅图,它很形象的表达了Proxy的作用。

ES6原生提供的Proxy构造函数,用法如下:

其中obj为Proxy要拦截的对象,handler用来定制拦截的操作,返回一个新的代理对象proxy;Proxy代理特点:

1.Proxy的代理针对的是整个对象,而不是像Object.defineProperty针对某个属性。只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性

2.Proxy也可以监听数组的变化

参考:

能说说vue的响应式原理吗?

Vue 是一个 MVVM 框架,核心是双向数据绑定,VM(视图模型)是作为 V(视图) 和 M(模型)的桥梁。下面是对 Vue 响应式(双向数据绑定)的理解,如果错误尽请指出,一起交流,共同进步。

Vue响应式原理核心是 数据劫持,采用 ES5 的 object.defineproperty 的 getter 和 setter 方法。从一个例子出发:

首先,在Vue初始化阶段,通过 observer 对 data 中的属性进行递归的劫持,包括 name、job_ undergo、a、b等

在 get阶段也就是初始化视图时,为每一个劫持的属性分配一个 依赖收集器,主要收集当前属性的观察者对象,例子中 name 属性在模板中有两处被使用,那么 name 属性的依赖收集器中就存放两个观察者对象

当点击按钮时,将 name 修改为 lisi 时,会触发 observer 的 setter 函数,将 value 更新为 lisi 最新值,然后通知依赖收集器数据发生了更新。

依赖收集就是发布订阅模式,依赖收集器会通知所有的观察者对象,当前name 属性有两个观察者对象。

观察者对象调用对应的回调函数进行相关的处理和DOM更新

以上是纯响应式原理的分析和总结,下面配一张流程图:

手写 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 响应式的核心:

图片

vue响应式原理是什么?

当一个vue实例加载时,会进行初始化,将他的配置项options和mixins的内容合并,以options为主,而在初始化data时,会对data对象进行数据劫持,并做代理,通过Object。

definproperty劫持数据后vue会查找当前属性有无依赖项既被watch,或者依赖当前属性的值,如果有,就会注册依赖既deps,而注册deps时会在wather内添加新的更新目标。

当数据发生变更时,会触发deps的更新方法,调用所有的watcher,watcher又会触发对应deps的更新,直到所有依赖项更新完毕。

扩展资料:

Vue 是一个 MVVM框架,核心是双向数据绑定,VM(视图模型)是作为V(视图)和M(模型)的桥梁。对Vue响应式(双向数据绑定)的理解,如果错误尽请指出,一起交流,共同进步。Vue响应式原理核心是 数据劫持,采用 ES5 的 object.defineproperty 的 getter 和 setter 方法。

Vue.js 最显著的一个功能是响应系统 —— 模型只是普通对象,修改它则更新视图。这让状态管理非常简单且直观,不过理解它的原理也很重要,可以避免一些常见问题。下面我们开始深挖 Vue.js 响应系统的底层细节。

参考资料来源:百度百科-Vue·js前端开发技术

vue3响应式原理代理与反射(vue3 响应式原理)

关于vue3响应式原理代理与反射和vue3 响应式原理的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。

阅读
分享