什么是响应式?
当数据变化时,试图也会进行更新
响应式也是我们常说的双向绑定
中数据->试图
方向的流动.
为了加深大家对响应式理解。我做了一个小的demo。页面初始值data.name为bob。当我在控制台将data.name改为dani时,页面也更新为dani。
那么系统是如何知道我将data.name改为dani了呢?答案是—数据劫持
数据劫持
数据劫持就是当我们对数据进行读写操作时都能被某个函数拦截。
其实有两种方式能实现数据劫持: Object.defineProperty
和proxy
.
1、Object.defineProperty
对于 data={name: 'boo', age: 18}
,我们可以使用Object.defineProperty(data, 'name', { get() {…}, set() {…}})
对data.name进行劫持。我们把这种能被劫持的数据叫做响应式数据。
想要使data下所有属性都能被劫持,则需要循环data下所有的key并define property。这样就存在一个问题:
当我给data新增gender属性时data.gender = 'male'
,由于data.gender没有被define property,所以data.gender不能被劫持,也是不可相应的。
正是由于这种js的限制,defineProperty无法检测到对象新增属性,也无法检测到数组的变化!
vue对这些数据也做了优化:
对于对象
,上面提到的data.gender,vue提供了$set方法主动调用defineProperty将其转化为响应式数据。
this.$set(this.someObject,‘b’,2) // 新增响应式数据
复制代码
对于数组
,vue重写了原型的push、 pop、 unshift、shift、splice、sort、reverse
实现函数劫持。可能有小伙伴会说操作数组的方法不是10个吗,你这只写了7个呀? 因为只有这7个方法会修改到数组本身。也建议大家在开发过程中尽量使用这些方法对数组进行修改。
即使vue中做了一些努力,但是对于数组通过下标和length的方式进行修改,依旧是不能被劫持。
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
复制代码
有了缺陷我们就去优化它,优化不了就想着如何去重构了,刚好ES6提供了proxy
。刚好proxy能解决上述definePropery的所有问题。所以vue3使用proxy进行数据劫持
。
2、Proxy
const target = { a: 1, b: 2 }
var proxy = new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 拦截【读取】
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
// 拦截【新增、修改】
return res;
}
})
复制代码
可以看到我们对数据进行读写操作时,会分别被proxy的get和set拦截。
那proxy劫持数据干嘛呢?
当然我们的最终目的是想要更新页面。
更新页面的前提是,我需要知道有哪些更新页面的函数依赖了响应式数据 —— 依赖收集
当数据变化时,找到对应的更新页面的函数(依赖函数),执行并更新页面 —— 触发依赖
那vue是如何进行依赖收集和触发
的呢?让我们从源码中寻找答案吧!!
vue3如何实现响应式?
看下使用vue3的api如何实现上面的demo呢
const { reactive, effect } = VueReactivity;
// reactive将对象转化为响应式数据data
const data = reactive({name: 'bob'});
// effect副作用,使用effect的方式执行副作用。函数可以被收集到依赖中。
effect(() => {
document.querySelector('#name').innerText = data.name;
});
复制代码
使用到了两个核心函数reactive
和effect
。
reactive: 将对象转化为响应式数据data。
effect: 副作用,表示通过effect的方式执行函数fn有一定的副作用,这个副作用就是fn可能被收集到依赖集合中。
effect入参(fn
)中有使用到响应式数据data.name。所以函数会被收集到data.name
对应的依赖集合中,当data.name='dani'
时,这个依赖函数fn会被找到执行并更新页面
。
分别看下reactive和effect是如何实现的呢?
1、reactive
function reactive(target) {
const proxy = new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发执行对应的依赖函数
return res;
}
});
return proxy;
}
复制代码
可以看到在proxy的get中调用track
进行依赖收集。set中调用trigger
进行依赖触发。依赖是怎么收集起来的呢?
vue3在内存定义了一个targetMaps的变量,来存储所有响应式数据对应的依赖函数。具体数据结构如下:
通过上面的数据结构,track(target, key)
和trigger(target, key)
能很方便的通过target和key
存放、找到执行对应的依赖集合。
回顾reactive:
那么track收集的依赖函数从而而来呢?
effect的副作用就是能将函数fn收集到依赖集合中。
2、effect
function effect(fn) {
try {
activeEffect = fn; // 将当前依赖给activeEffect
fn(); // 默认执行一次依赖函数
} finally {
activeEffect = null;
}
}
复制代码
首先将fn赋值给全局变量activeEffect,方便在后续依赖收集中获取到fn。
然后执行了fn,这一步很关键,在fn的执行过程中,如果有使用读取到响应式数据,会被对应proxy的get劫持进行依赖收集。此时收集的依赖就是activeEffect,也就是正在被执行的函数fn。
换句话说:在fn的执行过程中如果有使用到响应式数据,fn就会被收集到改数据的依赖集合中。
上图可以看到effect执行fn,一个完整的依赖收集过程。当数据变化是,会被proxy的set劫持,触发对应的依赖函数执行。
3、流程
这就是vue3的响应式原理啦~~~~