注意:
响应式原理和双向数据绑定原理是两回事,一般面试官会先问响应式原理再问双向数据绑定的原理
详细文章
1.响应式原理
核心是数据劫持和依赖收集,是通过数据劫持结合发布者-订阅者模式的方式来实现的。通过Object.defineProperty()为对象添加属性,然后为对象设置getter和setter方法,之后我们每次通过点语法获取属性就会执行getter方法,在这个方法中我们会把调用 此属性的依赖收集到一个集合中;而在我们给属性赋值(修改属性)时,会触发setter方法,然后去通知集合中的依赖更新,做到数据变更 驱动视图变更。
Object.defineProperty(obj, key, {
// writable:true,// 可写
// enumerable: true, // 可枚举
// configurable: true, //可删除
//拦截get,当我们访问data.key时会被这个方法拦截到
get () {
//我们在这里收集依赖
return obj[key];
},
//拦截set,当我们为data.key赋值时会被这个方法拦截到
set (newVal) {
//当数据变更时,通知依赖项变更UI
}
})
2. MVVM模式
MVVM框架的的核心就是双向绑定, 其原理是通过数据劫持+发布订阅模式相结合的方式来实现的,简单来说就是数据层发生变化的时候,可同步更新视图层,当视图层发生变化的时候,同步更新数据层
- MVVM将数据双向绑定作为核心思想,View 和 Model之间没有联系,他们通过ViewModel这个桥梁进行交互
- Model 和 ViewModel 之间的交互是双向的,因此View 的变化会自动同步到Model,而Model的变化也会立即反映到View上显示
- 当用户操作View , ViewModel 感知到变化,然后通知Model发生相应改变;反之当Model发生改变,ViewModel也能感知到变化,使View作出相应更新
3. 如何实现?
Vue 类
首先我们实现一个 Vue 类,用于创建 vue对象,它的构造方法接收一个options参数,用于初始化vue
class Vue {
constructor(options){
this.$el = options.el;
this._data = options.data;
this.$data = this._data
// 对data进行响应式处理
new Observe(this._data)
}
}
// 创建 vue 对象
new Vue ({
el:'#app',
data:{
message:'hello world'
}
})
上面的代码中我们首先创建了一个Vue的类,构造函数跟我们平时使用的Vue大致一致,为了容易理解我们这里只处理了参数el和data。
我们发现构造函数的最后一行创建了一个Observe类的对象,并传入data作为参数,这里的Observe就是对data数据进行响应式处理的类,接下来我们看一下Observe类的简单实现。
Observe类
(数据监听器:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者)
我们在Oberve类中实现对data的监听,就是通过Object.defineProperty()方法实现的数据劫持,代码如下:
class Observe {
constructor(data){
// 如果传入的数据是object
if(typeof data == 'object'){
this.walk(data);
}
}
// 这个方法遍历对象中的属性,并依次对其进行响应式处理
walk(obj){
// 获取所有属性
const keys = Object.keys(obj);
for ( let i = 0; i < keys.length; i++ ){
// 对所有属性进行监听(数据劫持)
this.defineReactive(obj,keys[i])
}
}
defineReactive(obj,key){
if (typeof obj[key] == 'object'){
// 如果属性是对象,那么递归调用walk方法
this.walk(obj[key]);
}
const dep = new Dep(); // Dep类用于收集依赖
const val = obj[key];
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
// get代理将Dep.target 即watcher对象添加到依赖集合中
get(){
// 这里在创建watcher对象时 会给Dep.target赋值
if(Dep.target){
dep.addSubs(Dep.target);
}
return val;
},
set(newVal){
val = newVal;
// 依赖的变更响应
dep.notify(newVal);
}
})
}
}
上述代码中我们使用到了Dep类,我们在劫持到的数据的get方法中收集到的依赖会被放到Dep类中保存。
Dep类
订阅器:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observe 和 订阅者 Watcher 进行统一管理。
下面代码是Dep类的实现,他有一个subs的数组,用于保存依赖,这里的依赖是我们后面要定义的Watcher,Watcher即观察者
class Dep (){
static target = null
constructor(){
this.subs=[];
}
addSubs(watcher){
this.subs.push(watcher)
}
notify(newVal){
for(let i = 0; i < this.subs.length; i++){
this.subs[i].update(newVal);
}
}
}
Watcher类
作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
观察者类,它做的事情就是观察数据的变更,它会调用data中对应属性的get方法触发依赖收集,并在数据变更后执行相应的更新
let uid = 0;
class Watcher{
// vm即一个vue对象,key要观察的属性,cb是观测到数据变化后需要做的操作,通常是指dom变更
constructor(vm,key,cb){
this.vm = vm;
this.uid = uid++;
this.cb = cb;
// 调用get触发依赖收集之前,把自身赋值给Dep.taget静态变量
Dep.target=this;
// 触发对象上代理的get方法,执行get添加依赖
this.value=vm.$data[key];
//用完即清空
Dep.target=null;
}
// 在调用set触发Dep的notify时要执行的update函数,用于响应数据变化执行run函数即dom变更
update(newVal){
// 值发生变化才变更
if(this.value !== newVal){
this.value = newVal;
this.run()
}
}
// 执行DOM更新等操作
run(){
this.cb(this.value);
}
}
通过以上的代码我们就实现了一个去除了模板编译的简易版的Vue,我们用简单化模拟dom的变更
//======测试=======
let data={
message:'hello',
num:0
}
let app=new Vue({
data:data
});
//模拟数据监听
new Watcher(app,'message',function(value){
//模拟dom变更
console.log('message 引起的dom变更--->',value); // world
})
new Watcher(app,'num',function(value){
//模拟dom变更
console.log('num 引起的dom变更--->',value); // 100
})
data.message='world';
data.num=100;
总结
1、实现一个监听器 Observe:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。并且可以拿到最新值通知订阅者。
2、实现一个订阅者 Watcher:Watcher 订阅者是 Observe 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observe 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
3、实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
4、实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。