VUE2.0 的响应式原理
本篇文章篇幅较长,已经对2.0响应式原理熟悉的可直接跳过此部分,各取所需,共同交流
在vue中我们使用最多的就是响应式数据了,给我们带来了很多便利,说起响应式数据,也就是数据变了就要更新视图,我们一步一步看一下在vue2.0中是怎么实现这一功能的~
首先我们定义一个数据:
let data = {name: "yangbo"}
然后我们改变这个数据:
data.name = 'yb'
我们都知道在vue中数据改变了我们就要更新视图,所以这时我们就需要一个更新视图的方法:
function updateView(){
console.log('触发更新视图方法了')
}
这时问题来了,当我们设置这个对象的属性值的时候,怎么去触发我们的updateView()方法呢,这里我们就用到了Object.defineProperty() 方法, 我们通过这个方法来重新定义属性并给对象的属性增加getter和setter, 话不多说开始第一步吧:
我们定义一个观察数据的方法,这个方法里我们调用defineProperty方法去为数据的属性设置getter和setter;
在defineProperty方法中,我们使用了Object.defineProperty() ,当取值的时候就会走get方法,这时我们直接将value返回即可,在重新设置值的时候(比如data.name = 'yb')就会走set方法,在set方法中我们调用更新视图的方法,并将新的值赋给value
function watchData(target) {
if(typeof target !== 'object' || target === null) {
return target;
}
for (let key in target) {
defineProperty(target, key, target[key])
}
}
function defineProperty(target, key, value) {
// 重新给target 定义key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
updateView();
value = newVal;
}
}
})
};
以上的代码就完成了第一步,这时问题又来了,我们的数据不可能全是一层的,有可能是多层的数据,比如:
let data = {
name: 'yangbo',
info: {
age: '26'
}
}
所以,我们要做的是如果内层还是一个对象,那我们就要再次去观察这个对象, 这时我们在defineProperty函数中增加一行代码,这其实就形成了递归:
function defineProperty(target, key, value) {
// 增加一行代码
watchData(value);
// 重新给target 定义key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
updateView();
value = newVal;
}
}
})
};
至此可以说我们已经为数据实现响应式了,通过以上代码可以看出,如果你的Object层级很深,那么递归是会影响性能的。接下来我们思考一个问题,假如我们使用如下方式给对象赋值会触发几次更新视图的方法:
data.info = {sex: 'man'}
data.info.sex = 'woman'
答案是一次,只有data.info = {sex: 'man'}会触发更新视图的方法,因为我们上面的方法并没有为sex定义getter与setter, 所以我们要再defineProperty中再加一行代码,如下:
function defineProperty(target, key, value) {
watchData(value);
// 重新给target 定义key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
// 新增一行代码,如果设置的值是Object,则也为新的值添加getter和setter
watchData(newVal);
updateView();
value = newVal;
}
}
})
};
这时就会执行两次更新视图的方法了,到这里我们基本实现对象的响应式了;但再往细想还是有问题:假如属性不存在,新增的属性会是响应式的吗?假如属性的值是个数组该怎么做呢?
我们一起来看下,以目前的代码,以下这种为数据中数组的改变是不会触发
更新视图的方法的:
let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);
那我们来让这种方式改变数据也会触发更新视图,首先我们看下,我们调用了push方法,这个方法是Array的,所以我们需要对数组的方法进行重写,这里我们不能直接对数组原型上的方法进行重写,而且我们是拿到数组的所有方法并对其中的几个方法进行改写,本例仅对push和pop方法进行改写,我们需要创建一个和Array.property相同的Object,并且修改我们的Object并不会影响到Array.property, 这里就用到了继承,估计你在面试的时候会遇到这个问题。
let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty); // 继承
接着我们重新定义proto上的方法,记住,定义propo上面的方法的时候我们还需要用到ArrayProperty上的方法, 所以这里我们还是调用数组的原始方法,在调用之前我们来调用更新视图的方法,这里不要忘记使用call方法来改变this指向,这里我们做的就是函数劫持:
['push', 'pop'].forEach((method) => {
proto[method] = function() {
updateView();
// 调用ArrayProperty上的方法
ArrayProperty[method].call(this, ...arguments);
}
});
函数我们已经劫持了,那在watchData这个方法中,我们怎么让target找到proto上的方法呢? 我们判断一下target是不是数组,如果是数组那我们让它的proto指向proto:
function watchData(target) {
if(typeof target !== 'object' || target === null) {
return target;
}
if(Array.isArray(target)) {
// 如果是数组,让target的链指向proto
target.__proto__ = proto;
}
for (let key in target) {
defineProperty(target, key, target[key])
}
}
以上就搞定了对对象和数组的响应式,下面贴出完整代码:
let ArrayProperty = Array.prototype; // Array的原型
let proto = Object.create(ArrayProperty); // 继承
['push', 'pop'].forEach((method) => {
proto[method] = function() {
updateView();
// 调用ArrayProperty上的方法
ArrayProperty[method].call(this, ...arguments);
}
})
function updateView(val) {
console.log('触发更新视图方法了');
}
function watchData(target) {
if(typeof target !== 'object' || target === null) {
return target;
}
if(Array.isArray(target)) {
// 如果是数组,让target的链指向proto
target.__proto__ = proto;
}
for (let key in target) {
defineProperty(target, key, target[key])
}
}
function defineProperty(target, key, value) {
watchData(value);
// 重新给target 定义key 并增加getter和 setter
Object.defineProperty(target, key, {
get() {
return value
},
set(newVal) {
if (newVal !== value) {
watchData(newVal);
updateView();
value = newVal;
}
}
})
};
let data = {name: 'yangbo', phone: [1,2,3]};
watchData(data);
data.phone.push(4);
console.log(data.phone);
// let data = {name: 'yangbo', info: {age: 25}}
// watchData(data);
// data.name = 'yb';
// data.info = {sex: 'man'}
// data.info.sex = 'woman'
VUE3.0 的响应式原理
上面我们已经手写了简易版的vue2.0响应式,我们不难发现,在2.0中一上来默认就会对数据进行递归,还有就是对象不存在的属性不能被拦截,带着这些问题我们一起看下vue3.0的响应式实现原理~
首先说下我们现阶段怎么体验vue3.0 :
- 将vue3.0源码clone到本地
git clone [email protected]:vuejs/vue-next.git
- 安装依赖,然后在根目录执行
npm run dev
- 编译好的vue3.0是package/vue/dist/vue.global.js,将vue.global.js引入你的demo就可以使用了,在这之前务必要看下Vue Composition API
接下来我们使用一下vue3.0
Document
vue3.0 的响应式数据是通过es6 的Proxy来实现的,Vue.reactive()返回一个Proxy,从上面的代码可以看出,当let proxy = Vue.reactive({name: 'yangbo'});是应该触发一次更新视图的,而当变更值proxy.name = 'yb';的时候需要再触发一次更新视图,那在vue3.0中是怎么做的呢? vue3.0这里的主要逻辑是通过Vue.effect()这个方法去实现的,effect这个方法会先执行一次,当数据变化的时候会再执行,接下来我们一步一步的区实现,我们这次是用js来实现不是ts,先熟悉思路。
- 首先我们实现Vue.reactive这个方法
不知道你是否还记得上面Vue2.0实现响应式的方法,就是一上来就会对数据进行递归,带着这个问题看看vue3.0到底怎么优化的;在这之前还是需要熟悉ES6的Proxy的~
Vue.reactive返回的是一个响应式对象,所以我们在reactive方法中返回一个响应式对象:
function reactive(target) {
return createReactive(target);
}
function createReactive(target) {
// 创建响应式对象
if(!(typeof target === 'object' && target !== null)) {
return target;
}
// 创建观察者
let baseHandle = {
get(target, key, receiver) {
// 获取
let datas = Reflect.get(target, key, receiver);
return datas;
},
set(target, key, value, receiver) {
// 设置
let res = Reflect.set(target, key, value, receiver);
console.log(res);
return res;
}
}
let observed = new Proxy(target, baseHandle);
return observed;
}
let proxy = reactive({name: 'yangbo'})
proxy.name = 'yb';
console.log(proxy.name);
在vue3.0中,一般proxy会和一个api reflect来一起使用,在get方法中,Reflect.get(target, key, receiver)和target[key]这两种写法是等价的;
在我们代码的set方法中,我们同样使用Reflect.set()来设置值,并且它会返回一个布尔值来标示是否设置成功, 通过以上的代码我们已经可以成功代理一层Object,那多层的Object呢?所以我们在获取的时候,也就是get方法中判断要获取的值是不是Object, 如果Reflect的结果是Object的话,那我们直接把结果代理(递归)
get(target, key, receiver) {
// 获取
let datas = Reflect.get(target, key, receiver);
// 变更了下面这行代码
return typeof target === 'object' && target !== null ? reactive(datas) : datas;
}
和2.0相比,2.0是开始就递归,而3.0是在取值的时候判断,有必要才去递归。以上我们实现了对Object的代理,但是我们还要防止同一个Object被多次代理,所以这里我们用到了new WeakMap()弱引用
我们来定义两个WeakMap用于判断当前对象是否被代理过:
let sourceProxy = new WeakMap(); // 存放原对象和代理过的对象
/**
* sourceProxy
* {
* 原对象:代理过的对象
* }
* **/
let toRaw = new WeakMap(); // 存放被代理过的对象和原对象
/**
* toRaw
* {
* 代理过的对象:原对象
* }
* **/
然后我将createReactive方法改为如下:
function createReactive(target) {
// 创建响应式对象
if(!(typeof target === 'object' && target !== null)) {
return target;
}
// 判断target是否被代理过
if(sourceProxy.get(target)) {
return sourceProxy.get(target);
}
// 判断代理过的里面是否有target, 防止一个对象被多次代理
if (toRaw.has(target)) {
return target;
}
// 创建观察者
let baseHandle = {
get(target, key, receiver) {
// 获取
let datas = Reflect.get(target, key, receiver);
// 变更了下面这行代码
return typeof target === 'object' && target !== null ? reactive(datas) : datas;
},
set(target, key, value, receiver) {
// 设置
let res = Reflect.set(target, key, value, receiver);
console.log(res);
return res;
}
}
let observed = new Proxy(target, baseHandle);
// 设置weakmap
sourceProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
接着我们要实现依赖收集(发布订阅),还记得前面我们说的effect方法吗?它默认会执行一次,当依赖数据变化的时候会再次执行,现在我们实现这个effect,这个也是响应式的核心 ;首先我们这个effect方法接收一个函数,并且我们要把这个函数变成响应式函数,并且让这个函数默认执行一次:
function effect(fn) {
let effect = createReactiveFn(fn); // 创建响应式的函数
effect(); // 默认执行一次
}
接着我们来编写createReactiveFn方法 :
let stacks = [];
function createReactiveFn(fn) {
let eFn = function() {
// 执行fn并且将fn存入栈中
return carryOut(eFn, fn)
}
return eFn;
}
function carryOut(effect, fn) {
// 执行fn并且将fn存入栈中
stacks.push(effect);
fn();
}
在取值的时候,也就是get方法中,我们就要对值和effect做好映射,当值发生变化的时候,我们直接执行值对应的effect,接下来我们先实现这部分创建关联的方法, 最后我们要一种如下的数据结构:
{
target: {
key: [fn, fn]
}
}
我们编写subscribeTo来创建关联:
let targetMap = new WeakMap();
function subscribeTo(target, key) {
let effect = stacks[stacks.length - 1];
if (effect) { //如果有,则创建关联
let maps = targetMap.get(target);
if (!maps) {
targetMap.set(target, maps = new Map);
}
let deps = maps.get(key);
if (!deps) {
maps.set(key, deps = new Set());
}
if(!deps.has(effect)) {
deps.add(effect);
}
}
}
还记得之前写的将effect入栈吗stacks.push(effect); 当我们做完关联之后,其实这个effect在栈里就没有意义了,所以我们将它清出栈:
function carryOut(effect, fn) {
// 执行fn并且将fn存入栈中
stacks.push(effect);
fn();
stacks.pop(effect);
}
现在我们已经创建关联了,那在更新值的时候,也就是set方法中我们应该去取targetMap中的effect执行:
function trigger(target, type, key) {
let tMap = targetMap.get(target);
if (tMap) {
let keyMaps = tMap.get(key);
// 将key对应的effect执行
if (keyMaps) {
keyMaps.forEach((eff) => {
eff();
})
}
}
}
以下贴出本篇文章实现的完整代码:
let sourceProxy = new WeakMap(); // 存放原对象和代理过的对象
let toRaw = new WeakMap(); // 存放被代理过的对象和原对象
function reactive(target) {
return createReactive(target);
}
function createReactive(target) {
// 创建响应式对象
if(!(typeof target === 'object' && target !== null)) {
return target;
}
// 判断target是否被代理过
if(sourceProxy.get(target)) {
return sourceProxy.get(target);
}
// 判断代理过的里面是否有target, 防止一个对象被多次代理
if (toRaw.has(target)) {
return target;
}
// 创建观察者
let baseHandle = {
get(target, key, receiver) {
// 获取
let datas = Reflect.get(target, key, receiver);
// 订阅
subscribeTo(target, key); // 当key变化的时候重新执行effect
// 变更了下面这行代码
return typeof target === 'object' && target !== null ? reactive(datas) : datas;
},
set(target, key, value, receiver) {
// 设置
let oldValue = target[key];
let res = Reflect.set(target, key, value, receiver);
// 此处的判断是屏蔽无意义的修改
if (!target.hasOwnProperty(key)) {
// 新增属性
console.log('新增');
trigger(target, 'add', key);
} else if (oldValue !== value) {
// 修改属性
console.log('修改');
trigger(target, 'set', key);
}
console.log(res);
return res;
}
}
let observed = new Proxy(target, baseHandle);
// 设置weakmap
sourceProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
let stacks = [];
let targetMap = new WeakMap();
// 响应式
function effect(fn) {
let effect = createReactiveFn(fn); // 创建响应式的函数
effect(); // 默认执行一次
}
function createReactiveFn(fn) {
let eFn = function() {
// 执行fn并且将fn存入栈中
return carryOut(eFn, fn)
}
return eFn;
}
function carryOut(effect, fn) {
// 执行fn并且将fn存入栈中
try {
stacks.push(effect);
fn();
} finally {
stacks.pop(effect);
}
}
function subscribeTo(target, key) {
let effect = stacks[stacks.length - 1];
if (effect) { //如果有,则创建关联
let maps = targetMap.get(target);
if (!maps) {
targetMap.set(target, maps = new Map);
}
let deps = maps.get(key);
if (!deps) {
maps.set(key, deps = new Set());
}
if(!deps.has(effect)) {
deps.add(effect);
}
}
}
function trigger(target, type, key) {
let tMap = targetMap.get(target);
if (tMap) {
let keyMaps = tMap.get(key);
// 将key对应的effect执行
if (keyMaps) {
keyMaps.forEach((eff) => {
eff();
})
}
}
}
// let proxy = reactive({name: 'yangbo'})
// proxy.name = 'yb';
// console.log(proxy.name);
let proxyObj = reactive({name: 'yangbo'});
effect(() => {
console.log(proxyObj.name);
})
proxyObj.name = 'yb'
以上简单模仿了vue3.0中对响应式数据的实现,使用js实现的,感兴趣可以看看Vue3.0源码的TypeScript的写法和实现方式,本篇文章仅供参考~
参考:
Proxy
vue-next
Vue Composition API