一、vue.js响应式原理
在 Vue.js 中,当我们修改数据时,视图随之更新,这就是 Vue.js 的双向数据绑定(也称响应式原理)。
核心:观察者模式和Object.defineProperty() 方法。
在vue中,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”(“Touch” 过程)过的数据 property 记录为依赖(Collect as Dependency 过程)。之后当依赖项的 setter 触发时,会通知 watcher(Notify 过程),从而使它关联的组件重新渲染(Trigger re-render 过程)——观察者模式。
二、组成部分
1、mvvm是什么
MVVM拆开来即为Model-View-ViewModel,有View,ViewModel,Model三部分组成。View层代表的是视图层(UI)。Model层代表的是数据,ViewModel层连接Model和View。 数据双向绑定:数据影响视图,视图影响数据。
2、组成部分
- Observer(监听器):将普通数据转换为响应式数据,从而实现响应式对象。
- Dep(订阅者,依赖收集者):主要通过addSub()方法增加观察者,通过notify()方法通知观察者,调用观察者的update更新相应的视图。
- Watcher(观察者):观察者的get()方法获取未更新之前的值,当数据更新时,调用观察者的update更新相应的视图。
3、mvvm实现
不同的框架当中,MVVM实现的原理是不同的;在vue中,采用的是数据劫持+发布订阅模式。
- 2.1、数据劫持observe 通过Object.defineProperty定义所有的属性。即当你把一个普通的JavaScript对象传入 Vue 实例作为 data 选项,Vue将遍历此对象所有的property,并使用Object.defineProperty把这些property全部转为 getter/setter(数据的响应式)。
//处理data
function Observe(data){
for( let key in data){
let val=data[key]
observe(val)
Object.defineProperty(data,key,{
enumerable:true,
get(){
return val
},
set(newVal){
if(newVal==val) return
val=newVal
observe(newVal)
}
})
}
}
//监测data
function observe(data){
if (typeof data!='object') return
return new Observe(data)
}
通过对数据(Model)进行劫持,当数据变动时,数据会触发劫持时绑定的方法,对视图进行更新。
数据代理:
function myVue(options={}){
this.$options=options
var data=this._data=this.$options.data
observe(data)
//this代理this._data
for(let key in data){
Object.defineProperty(this,key,{
enumerable:true,
get(){
return this._data[key]
},
set(newVal){
this._data[key]=newVal
}
})
}
}
- 2.2、发布订阅模式 先订阅(存储),再发布(执行)。
订阅者主要通过addSub()方法增加观察者,通过notify()方法通知观察者,调用观察者的update更新相应的视图。 通过观察者Watcher的get()方法获取未更新之前的值,给每个属性创建一个发布订阅的功能,当数据更新时,调用观察者的update更新相应的视图。
//被观察者(订阅者)
class Dep{
constructor(){
this.subs=[]
}
//添加观察者
addSub(watcher){
this.subs.push(watcher)
}
//通知观察者
notify(data){
this.subs.forEach(element => element.update(data));
}
}
//观察者
class Watcher{
constructor(vm,key,cb){
this.vm=vm
this.key=key
this.cb=cb
// 全局唯一,创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this;
// 此处通过 this.vm.$data[key] 读取属性值,触发 getter
this.oldValue = this.vm.$data[key]; // 保存变化的数据作为旧值,后续作判断是否更新
// 前面 getter 执行完后,执行下面清空
Dep.target=null
}
update(data){
console.log(`数据发生变化!`);
let oldValue = this.oldValue;
let newValue = this.vm.$data[this.key];
if (oldValue != newValue) { // 比较新旧值,发生变化才执行回调
this.cb(newValue, oldValue);
};
}
}
- 2.3、模板编译compile
使用虚拟dom的方法,先把el中的内容通过创建文档碎片的方式移入内存中,再在内存中去查找和替换,(查找页面中的双大括号的文本节点的内容和通过v-model绑定的内容,用data中的数据进行替换。),最终将结果再重新挂载回页面中。
获取template模板——》ast语法树(描述template源代码的树形结构)——》render函数——》虚拟dom——》真实dom
//模板编译
function Compile(el,vm){
// el表示要替换的范围
vm.$el=document.querySelector(el)
let fragment=document.createDocumentFragment();
// 将el中的内容移入内存中
while(child=vm.$el.firstChild){
fragment.appendChild(child)
}
//查找替换
replace(fragment)
function replace(fragment){
Array.from(fragment.childNodes).forEach((node)=>{
console.log("node",node);
let text=node.textContent
let reg=/\{\{(.*)\}\}/
//文本节点
if(node.nodeType==3&®.test(text)){
console.log("文本节点",RegExp.$1);//this.a.a this.b
let arr=RegExp.$1.split('.')// ['a','a'] ['b']
let val=vm
arr.forEach((key)=>{
val=val[key]
})
node.textContent=text.replace(reg,val)
}
if(node.childNodes){
replace(node)
}
})
}
//将文档碎片重新挂载在页面中
vm.$el.appendChild(fragment)
}