Vue响应式原理是学习Vue的核心,了解其响应式原理,可以对Vue整体掌握更加熟练,学习Vue我们不仅要掌握其使用方式,更要掌握其源码,做到融会贯通,在面试中惊艳面试官
- 简述其原理:
Vue通过Object.definePerperty,根据其内部get(),set(),当读取数据时 执行get方法,当修改数据时执行set方法,以检测数据变化,当数据变化时,会通知观察者watcher,观察者会自动触发当前组件(子组件不会重新渲染),生成虚拟dom树,Vue会遍历旧的dom树和新的dom树中的每一个节点,并记录下来,将所有记录的不同点。局部修改到真实的dom树上。
- 在具体的实现上 Vue用到了几个核心组件,接下来我们注意解析这些组件
- Observer:把普通对象变为响应式的对象
- Dep:记录依赖,派发更新
- Watcher:响应式数据改变,通过watcher来执行相应的函数 如render
- Scheduler:调度器:用来维护一个任务队列,防止数据改变 watcher重复执行
剖析响应式原理之Observer组件
- 我们先简单概述一下Observer的功能
- Observer要实现的目标非常简单,就是把一个普通的对象转化为一个响应式的对象,为了实现这一点,Observer把对象的每个属性通过Object.defineproperty转化为带有getter 和setter属性,这样一来 当我们方位或者设置属性时,Vue就会有机会做一些别的事情
2. Observer是Vue中内部的构造器,我们可以使用Vue.observable(object)间接使用该功能,在组件的生命周期中,它发生在 beforeCreate 组件之后 created 之前
3. 具体实现上他会递归遍历对象的所有属性,以完成深度的属性转换。
4. 由于遍历只能遍历到对象的当前属性,所以无法检测到对象的增加和删除,因此Vue增加了delete两个实例方法,让开发者通过这两个实例方法增加和删除属性。
5. 对于数组 Vue会该改变他的隐式原型,之所以这样做,是因为vue需要监听哪些可能改变数组内容方法。
总之,Observer的目标,就是让一个对象,他的属性的读取,赋值,内部数组的变化都能被Vue感知到。
- 手写Observer
1.了解Object.defineproperty
Object.defineproperty(data,key,descriptor)中有三个属性:1)参数data:为监听的对象,参数key为监听对象的key值,descriptor:是一个对象 对象中有get() set()方法,当读取对象的值时会执行get()方法,当修改数据时会执行set()方法。
- 看代码
const obj = {
name:'zhangzhuo',
age:18,
love:{
love1:"h1",
love2:"h2",
love3:"h3"
},
arr:[1,2,3]
}
/**
*
*
**/
function defineReactive(data,key,value){
Object.defineProperty(data,key,{
get(){//get()函数return的返回值是什么,读取的值就是什么,不写return返回为undefined
console.log("数据被读取了");
return "读取的值"
},
set(newVal){//newVal参数为改变后的值,即重新赋予的值
console.log(newVal);
console.log("数据被改变了");
}
})
}
defineReactive(obj,'name')
console.log(obj.name);
obj.name="zhangsan"
2.由于每一个value的值都是不一样的所以我们要再写一个observer函数 用来循环data对象
const obj = {
name: "zhangzhuo",
age: 18,
love: {
love1: "h1",
love2: "h2",
love3: "h3",
},
arr: [1, 2, 3],
};
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
get() {
//get()函数return的返回值是什么,读取的值就是什么,不写return返回为undefined
console.log("数据被读取了");
return value;
},
set(newVal) {
//newVal参数为该变后的值
if(value===newVal){//判断改变的值是否相等,如果相等不渲染,优化性能
return
}
value = newVal;
render()//值改变渲染页面
},
});
}
function render(){
console.log('页面渲染了');
}
function observer(data) {
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
observer(obj);
console.log(obj.name);
obj.name = "zhangsan";
3. 当我们要监听对象里面的深层对象时,需要进行递归观察观察
name: "zhangzhuo",
age: 18,
love: {
love1: "h1",
love2: "h2",
love3: "h3",
love4: {
like: "paly",
},
},
arr: [1, 2, 3],
};
function defineReactive(data, key, value) {
observer(value);
Object.defineProperty(data, key, {
get() {
//get()函数return的返回值是什么,读取的值就是什么,不写return返回为undefined
console.log("数据被读取了");
return value;
},
set(newVal) {
//newVal参数为该变后的值
if (value === newVal) {
//判断改变的值是否相等,如果相等不渲染,优化性能
return;
}
value = newVal;
render(); //值改变渲染页面
},
});
}
function render() {
//该函数让页面渲染
console.log("页面渲染了");
}
function observer(data) {
if (typeof data === "object") {
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
}
observer(obj);
obj.love.love1="sss"
console.log(obj.love.love1);
这也体现了使用Object.defineProperty的劣势
- 劣势:
- 需要进行递归观察:耗费性能
- 数组不能响应(使用数组的变异方法)
- 对象的增和删不能响应
- 通过索引该数组,当页面中用到了这个数据,那么页面渲染
- 即使能够增改数组索引存在的值,但是vue也不会那么做,再实际项目中,有大量的数据存在于数组中,当我们更改数组时,会执行循环遍历,耗费性能。性能的代价和用户体验不成正比,所以使用数组的变异方法改变数组。
4. 数组响应式的处理
Vue在数组的原型上重写了数组方法
1. 通过改变其原型,然后执行render函数来渲染数据
2. 利用数组的splice方法来书写$set $delete方法来增加和删除数据
const data = {
name: "shanshan",
age: 18,
shan: {
name: "shanshan",
age: 18,
obj: {},
},
arr: [1, 2, 3],
};
/**
* 重写数组上的原型方法
*/
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
["push", "pop", "shift", "unshift", "sort", "splice", "reverse"].forEach(
(method) => {
arrayMethods[method] = function () {
arrayProto[method].call(this, ...arguments);
render();
};
}
);
function defineReactive(data, key, value) {
observer(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newVal) {
if (value === newVal) {
return;
}
value = newVal;
render();
},
});
}
function observer(data) {
if (Array.isArray(data)) {
data.__proto__ = arrayMethods;
return;
}
if (typeof data === "object") {
for (let key in data) {
defineReactive(data, key, data[key]);
}
}
}
function render() {
console.log("页面渲染啦");
}
/**
* 判断数据是否是数组,如果是则执行其splice方法来更改数据
* @param {*} data 监听的数据
* @param {*} key 监听的key值
* @param {*} value 监听的值
* @returns
*/
function $set(data, key, value) {
if (Array.isArray(data)) {
data.splice(key, 1, value);
return value;
}
defineReactive(data, key, value);
render();
return value;
}
/**
* 判断数据是否是数组,如果是则执行其splice方法来删除数据
* @param {*} data 监听的数据
* @param {*} key 监听的key值
*/
function $delete(data, key) {
if (Array.isArray(data)) {
data.splice(key, 1);
return;
}
delete data[key];
render();
}
observer(data);
刨析到这里远远还不够
剖析Dep
到这里我们还有两个问题没有解决,也就是说读取属性时我们要做什么事情,属性变化时我们要做什么事情。这个问题就要依赖Dep来解决。Dep含义是Dependency表示依赖的意思。 Vue中会为响应式中的每一个属性,对象本身创建一个Dep实例,每一个Dep实例都有能力做以下两件事情
- 记录依赖:记录是谁在用我
- 派发依赖:我变了,我要通知那些用到我的人 当读取响应式中的对象时,会进行依赖收集:有人用到了我 当改变了某个属性时,会派发更新,那些用到我的人听好了 我更新了
以下html里面的代码都是被render渲染出来的,那么就会执行getter,h1中用到了obj.a obj.b,它被渲染出来就会执行render函数,就也是对于属性obj.a obj.b来说 render函数用到了我,那么就会被记录依赖,k没有被渲染,不会被dep记录
Watcher
这里又出现了一个问题,就是Dep如何知道谁在用我?我该把任务派发给谁? 解决这个问题,需要依赖一个东西 就是Watcher。 当某个函数执行的过程中,用到了响应式中的数据,响应式数据无法知道是谁用到了我,因此Vue通过一招巧妙的方式来解决这个问题。
我们不是直接执行一个函数,而是把这个函数交给Watcher函数执行,Watcher是一个对象,每一个函数执行都应该创建一个Watcher,通过Watcher来执行。
Watcher会设置一个全局变量,让全局变量记录当前负责执行的Watcher等于自己,在执行函数中,如果发生了依赖记录Dep.depend(),那么Dep就会把这个全局变量记录下来,表示:有一个Watcher用到了我这个属性
当Dep进行派发更新时,他会通知之前记录的Watcher:我变了
每一个Vue组件实例,都至少记录一个Wachter,改Watcher中记录了该组件的render函数。
Watcher首先会把render函数运行一次进行依赖收集,于是那些在render函数中用到的响应式数据就会记录这个watcher
当数据变化时,dep就会通知该watcher,而watcher将会重新渲染rendr函数,从而让页面重新渲染同时重新记录当前依赖。
Scheduler:调度器
现在还剩下最后一个问题,就是Dep通知Watcher之后,如果Watcher执行重新运行对应的函数,就有可能导致函数的频繁运行,从而导致效率低下,
试想:如果一个交给Watcher的函数,他里面用到了a,b,c,d 那么a,b,c,d属性都会被记录依赖,于是下面的代码就会被出发4次更新:
state.a = "new data"
state.b = "new data"
state.c = "new data"
state.d = "new data"
这样显然是不合适的,因此,watcher收到派发更新的通知后,实际上不是立即执行,而是把自己交给一个叫作调度器的东西
调度器维护一个执行队列,该队列同一个watcher仅会执行一次,队列中的watcher不是立即执行,而是通过一个叫nextTick的工具方法,把这些需要执行的watcher放到一个事件循环的微队列中 nextTick的具体方法是通过Promise完成的
nextTick(()=>{
Promise().resolove().then(fn)
})
也就是说数据变化时,render函数的执行是异步的,并且在微队列中