本文介绍Vue.js
响应式的实现原理。在vue
官方文档中关于其响应式原理有一个简单的介绍: https://cn.vuejs.org/v2/guide/reactivity.html,其中还给出一张图来解释:
可以看到Vue
的思想也是类似MVVM
,在应用程序员看来,model
层和view
层解耦,开发者改变model
数据,UI
会自动响应数据的变化。我们将从内部原理细节去立即它的响应式原理。
我们要实现:
let vm = new MVVM({
el: '#app',
data: {
name: "luoxia",
info: {
year: 21,
addr: "China"
}
}
});
vm.$watch("name",newName=>{
console.log("name变化了哦:",newName)
});
<div id="app">
hello,{{name}}
<input type="text" l-model="name" />
</div>
原理中主要涉及到Observe
、Watcher
、Dep
几个概念,用一张图解释它们的关系:
在创建vm
实例的步骤中,有一个很重要的过程是对data
中的属性进行getter/setter
化,对data
中的每个属性进行监听,处理目标对属性的绑定,以及属性值变化后通知订阅者。Vue
使用Object.defineProperty()
来实现。
class MVVM {
constructor(options={}){
this.$options = options;
this._data = options.data;
observe(this._data);
// ...
}
}
class Observer {
constructor(data){
const keys = Object.keys(data);
for(let key of keys){
let value = data[key];
let dep = this.dep = new Dep(); // dep理解为Pub/Sub中心,也是data中属性的观察者
observe(value); //递归observe
Object.defineProperty(data,key,{
configurable: true,
get(){
if(Dep.target){
console.log("初始绑定:",key,value);
dep.addSub(Dep.target); // 通知观察者dep,让dep加入watcher到dep维护的订阅者队列
}
return value;
},
set(newVal){
if(newVal === value){
return;
}
value = newVal;
observe(newVal);
dep.notify(); // 通知观察者dep属性变化
}
})
}
}
}
function observe(data){
if(!data || typeof data !== 'object'){
return;
}
return new Observer(data);
}
在模板编译过程中,每遇到一个数据绑定,就会生成一个订阅该属性变化的watcher
,它具有update()
方法,pub/sub
中心接到来自于setter
的属性变化通知,调用所有订阅者的update()
方法,在watcher
的update()
方法中,回去调用更新UI
的方法:
class Watcher {
constructor(vm,exp,cb){
this.cb = cb; // 将用于更新UI
this.vm = vm; // vm实例
this.exp = exp; // 正则匹配出来的属性名
let arr = exp.split('.');
let val = vm;
Dep.target = this; // 用于识别初次访问该属性的getter,用于加入观察者到订阅者队列
arr.forEach(key => {
val = val[key]; //会访问该属性的getter
});
this.oldValue = val; // 记录第一次绑定时的属性值
Dep.target = null; // 防止后续访问该属性的getter重复加入订阅者队列
}
update(){
// 获取属性的最新值
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key];
});
if(this.oldValue !== val){
this.oldValue = val;
this.cb(val); // 更新UI的回调
}
}
}
Compiler
用于编译模板,在模板中会有对vm
实例中data
相应属性的绑定,在编译过程中主要做的事情:
- 匹配出绑定属性的地方
- 为该次绑定添加相应的
watcher
,watcher
回调中更新UI - 初始绑定值的替换
- 嵌入到
DOM
结构中
compile(){
let vm = this;
vm.$el = document.querySelector(this.$options.el);
let fragment = document.createDocumentFragment();
var child;
while (child = vm.$el.firstChild) {
fragment.appendChild(child); // 此时将el中的内容放入内存中
}
// 对el里面的内容进行替换
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g; // 正则匹配{{}}
if (node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}
// 初次数据的替换
node.textContent = txt.replace(reg,vm.getExpVal(RegExp.$1)).trim();
// 添加观察者
new Watcher(vm,RegExp.$1,newVal=>{
// 更新UI,用trim方法去除一下首尾空格
node.textContent = txt.replace(reg, newVal).trim();
});
}
if (node.nodeType === 1) { // 元素节点
let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组
Array.from(nodeAttr).forEach(attr => {
let name = attr.name; // l-model type
let exp = attr.value; // c text
if (name.includes('l-')){
node.value = vm[exp]; // this.c 为 2
// 数据双向绑定
node.addEventListener('input', e => {
let newVal = e.target.value;
// 相当于给this.c赋了一个新值
// 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
vm[exp] = newVal;
});
// 监听变化
new Watcher(vm, exp, function(newVal) {
node.value = newVal; // 当watcher触发时会自动将内容放进输入框中
});
}
});
}
// 如果还有子节点,继续递归replace
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment);
vm.$el.appendChild(fragment);
}
在我的理解当中,Dep
充当两种角色:
- 与
Observer
的联系:在创建Observer
时,会实例化一个Dep
对象,这个dep
实例充当属性的观察者,属性变化时,setter
内部会调用dep.notify()
用于将属性变化通知给这个dep
,而有模板对该属性进行绑定时,会调用dep.addsub()
用于将观察者加入到dep
维护的watcher
队列当中。此处体现观察者模式。 - 充当
Pub/Sub
的调度中心,内部维护watcher
队列,每次属性变化,setter
调用dep.notify()
,dep
接受到通知后,会调用每个watcher
的update()
方法。此处体现发布订阅者模式。
class Dep {
constructor(){
this.subs = [];
}
addSub(sub){
this.subs.push(sub);
return this;
}
notify(){
this.subs.map(sub=>sub.update());
}
}
总的来说,完整的思路:
- 使用
Object.defineProperty()
为属性添加观察者,而这里的观察者是Dep
,它又充当Pub/Sub
调度中心的角色 - 编译模版,每次匹配到属性的绑定,实例化一个
watcher
,实例化的时候会去访问属性的值,所以会访问属性的getter
,该getter
再通知Dep
实例将这个watcher
加入到Dep
维护的watcher
队列中 - 当属性变化时,首先会调用属性的
setter
,setter
会通知dep
实例,dep
实例接到通知后再广播给所有watcher
- 所有
watcher
接到通知后,调用自己的update()
方法,如果该watcher
监听的属性发生了变化,就要调用相应的方法更新UI
完整代码见:https://github.com/laoqiren/Reactive/blob/master/vue.js