众所周知,Vue是一个响应式的 JavaScript框架。响应式作为前端开发中一个重要的设计方法,其原理也是需要掌握的。
数据驱动视图
Vue的响应式原理遵循的是一种称作“数据驱动视图”的设计思维。通常情况下的函数,都是传入数据作为参数,然后返回数据作为结果。而前端页面中最多的不是数据而是视图,而视图的变化又与数据息息相关。因此数据驱动视图就是让函数通过数据的变化来使视图发生变化。当数据一旦有变化,那么便会重新渲染视图。
基本实现
Vue2和Vue3的实现方式略有区别,Vue2使用的是Object.defineProperty实现,而Vue3则使用的是Proxy。
Vue2中的实现
我们正常给对象添加属性的话是不能自定义属性的特征的(比如是否能够被修改,以及getter和setter等),而如果我们使用Object.defineProperty的话,就可以设置这些特征。当我们通过某个函数获取属性的时候,这个函数会作为该对象的一个依赖而塞进一个集合中。当数据改变时,会调用set,然后会改变所有的依赖项。
let obj ={
val: null,
};
Object.defineProperty(obj,'key',{
get(){
// 依赖收集
console.log('get');
return obj.val;
},
set(val){
// 更新分发
console.log('set');
obj.val = val;
},
})
obj.key =1;// set
obj.key;// get 1
Vue3中的实现
Vue3中使用的则是ES6中的新特性Proxy。它的特性在于可以高效地对对一个对象的操作进行拦截。getter和setter只有在修改本身值的时候才会触发,而Proxy可以设置自己的自定义条件。
let obj ={};
let nObj=new Proxy(obj,{
//拦截get,当我们访问nObj.key时会被这个方法拦截到
get: function (target, propKey, receiver) {
console.log('get');
return Reflect.get(target, propKey, receiver);
},
//拦截set,当我们为nObj.key赋值时会被这个方法拦截到
set: function (target, propKey, value, receiver) {
console.log('set');
return Reflect.set(target, propKey, value, receiver);
}
})
nObj.key=1;// set
nObj.key// get 1
当一个对象载入的时候,Vue会遍历这个对象的全部属性,然后给所有的属性添加对应的getter和setter,然后把代理对象挂载到VM上。
监听对象属性
Vue需要一个Observer类来实现监听,也就是上面说的给属性添加对应的getter和setter。Observer类的实例会绑定为对象的ob属性,然后遍历walk方法,给每个属性加上对应的getter/setter,使用的是defineReactive方法,也就是我们上文中提到的基本实现,后面使用了一个递归函数,是为了实现深度监听,如果需要监听的对象有嵌套的话,那么就需要使用深度监听。
class Observer {
constructor(value) {
this.value = value;
if (!value || (typeof value !== 'object')) {
return;
} else {
this.walk(value);
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj,key,{
get(){
// 依赖收集
console.log('get');
return val;
},
set(newVal){
if(newVal === val){
return;
}
new Observer(val)
updateView();
},
})
}
function updateView() {
console.log('view updated')
}
const data = {
val:1,
}
new Observer(data)
data.val = 2; // view updated
依赖收集
我们已经可以做到初级的更新分发了,下面介绍依赖收集的原理。
当有地方获取数据的时候,Dep类的一个对象会接收到依赖,也就是一个Watcher类的对象。当数据发生变动的时候,Dep会通知Watcher实例,然后通过回调进行视图更新。
class Observer {
constructor(value) {
this.value = value;
if (!value || (typeof value !== 'object')) {
return;
} else {
this.walk(value);
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
class Dep {
constructor() {
this.subs = [];
}
/*添加一个观察者对象*/
addSub (sub) {
this.subs.push(sub)
}
/*依赖收集,当存在Dep.target的时候添加观察者对象*/
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有watcher对象更新视图
notify () {
this.subs.forEach((sub) => {
sub.update()
})
}
}
class Watcher {
constructor() {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
}
update () {
console.log('view updated')
}
/*添加一个依赖关系到Deps集合中*/
addDep (dep) {
dep.addSub(this)
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj,key,{
get(){
dep.depend();
console.log('get');
return val;
},
set(newVal){
if(newVal === val){
return;
}
new Observer(val)
dep.notify();
},
})
}
class Vue {
constructor (options) {
this._data = options.data
new Observer(this._data) // 所有data变成可观察的
new Watcher() // 创建一个观察者实例
console.log('render', this._data.val)
}
}
let o = new Vue({
data: {
val:1,
}
})
o._data.val = 2;
Dep.target = null;
//get render 1 view updated