MVVM 框架模型的核心是双向绑定,即当 viewModel 中 data 数据发生改变如何改变 view 中的数据,当 view 中数据发生改变后如何改变 viewModel 中的 data 数据:
上图中表明了通过 input 事件来监听 view 中数据的改变,再在 JS 中通过定义事件来改变 data 中对应数据,通过 Object.defineProperty API 来监听 data 数据的改变,然后在这个 API 的 setter 函数来通知 view 中的数据去作改变。
Object.difineProperty API 主要有两个主要的作用:
1、可以让一个对象访问另一个对象的属性,即数据代理,将一个对象的属性加入到另一个对象中;
2、可以改变一个对象的原有属性,当属性值被改变时会自动触发 setter 函数,我们可以在 setter 函数中作些操作,当获得到某个属性值时,会自动触发 getter 函数,我们也可以在 getter 函数中作些操作。
要想看懂 MVVM 源码,一定要将这个 API 搞明白:Object.defineProperty()
基本流程是这样的:
首先,我们需要为每一个 data 属性都配置一个 observer 监听器,用来监听某个属性值是否发生了改变;
然后,我们要为每一个 data 属性都匹配一个 watcher 观察者(每一个属性可以有多个观察者,但至少要有一个),当某个属性值发生变化时,让页面中的对应数据发生改变;
当 data 中某个属性发生变化后,它的 observer 会通知它的所有观察者(所有的观察者会在一个观察者列表(Dep)中来维护),让每个观察者都执行 一个 update 函数,这是一个回调函数,对页面做出更新;
还需要一个编译器,用来解析模版指令,将模版中的变量替换成对应的值。在初始化渲染页面,为每个 data 属性添加一个观察者就是在这里添加的,一旦 data 中某个属性值发生改变,就会触发 watcher 的回调函数,此回调函数与编译器初始化页面是同一个函数。
index.html
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>源码分析</title>
<style media="screen">
#app {
text-align: center;
}
</style>
</head>
<body>
<div id="app">
<h2>{{title}}</h2>
<input v-model="name">
<h1>{{name}}</h1>
<button v-on:click="clickMe">click me!</button>
</div>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./compile.js"></script>
<script src="./index.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
title: 'hello world',
name: 'canfoo'
},
methods: {
clickMe() {
this.title = 'hello world'
}
},
mounted() {
window.setTimeout(() => {
this.title = '你好'
}, 1000)
}
});
</script>
</body>
</html>
index.js
//定义一个 vue 构造函数
function Vue(options) {
//在方法中使用 this,要注意 this 的指向
let self = this;
this.data = options.data;
this.methods = options.methods;
//将 data 中每个属性拿出来放入数据代理函数中
Object.keys(this.data).forEach((key) => {
self.proxyKeys(key);
});
//给 data 中每个属性匹配一个 observer 监听器
observe(this.data);
//在这里初始化页面并且为每个属性匹配一个 watcher 观察者
new Compile(options.el, this);
//由于在 index.html 中使用了 mounted 函数,需要在实例化时,执行 mounted
options.mounted.call(this);
}
Vue.prototype = {
proxyKeys(key) {
let self = this;
//这个 API 可以让 vm 实例访问 options 对象中 data 中的所有属性,如此 this.data.name
可以访问到,this.name 也可以访问到
Object.defineProperty(self, key, {
enumerable: false,
configurable: true,
get() { //当获取到这个属性值时,这个函数会自动执行
return self.data[key];
},
set(newVal) { //当更新这个属性值时,这个函数会自动执行
self.data[key] = newVal
}
});
}
};
observer.js
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk(data) {
let self = this;
Object.keys(data).forEach((key) => {
self.defineReactive(data, key, data[key]);
});
},
defineReactive(data, key, val) {
let dep = new Dep();
//因为 observe 方法中已经有判断,可以直接调用,用来处理属性值是对象的情况,无论是对象还是
数组,只要某一个属性值或者元素发生改变,都要匹配一个监听器来监听数据的变化,所以使用递归
let childObj = observe(val);
//为每一个属性值都绑定这个 API,如此可以在取值和更新值时作些操作
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
//将某个属性的所有观察者统统加入到一个观察者列表中
if(Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if(val === newVal) return
val = newVal;
//如果新值是一个对象,要重新给每个属性或者元素一个监听器
observe(newVal);
//让每一个观察者执行 update 函数
dep.notify();
}
});
},
};
function observe(value) {
if(!value || typeof value !== 'object') return
return new Observer(value);
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
};
//在 Dep 上定义了一个 target 属性
Dep.target = null;
watcher.js
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
//执行 get 函数,让自己(指的是观察者)缓存到 Dep.target 中,执行监听器中的 getter 函数,
将自己添加到观察者列表中,并且将值保存起来,好跟新值作对比
this.value = this.get();
}
Watcher.prototype = {
update() {
this.run();
},
run() {
//在监听器中,属性值发生改变,就会执行 update 函数,也就是执行 run 函数,此时获取到的值是
新值
let newVal = this.vm.data[this.exp];
if(this.value !== newVal) {
this.value = newVal;
//执行这个回调函数,这个回调函数在 compile.js 中被定义,将会改变页面数据
this.cb.call(this, newVal);
}
},
get() {
Dep.target = this; //缓存自己
let value = this.vm.data[this.exp]; //只要取值就会自动触发监听器的 getter 函数
Dep.target = null; //释放自己,已经添加到观察者列表中之后就不用再重复添加了
return value;
}
}
compile.js
由于要遍历每一个节点,然后作相应的操作,为了尽可能避免重排和重绘,使用了文档碎片,在文档碎片中作相应的操作,然后将其一次性插入到 DOM 结构中。
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
Compile.prototype = {
init() {
if(this.el) {
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
}else {
console.log('Dom 元素不存在');
}
},
//将模版中所有子节点放入到文档碎片中
nodeToFragment(el) {
let fragment = document.createDocumentFragment(),
child = el.firstChild;
//将 DOM 结构中的节点放入到文档碎片中,这个节点真的会在 DOM 结构中消失,所以反复使用
firstChild 即可
while(child) {
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
},
compileElement(el) {
let childNodes = el.childNodes,
self = this;
//childNodes 是一个类数组,这种方式可以使类数组转变成真正的数组
[].slice.call(childNodes).forEach((node) => {
let reg = /{{(.*)}}/,
text = node.textContent;
//当遇到元素节点时,就做出相应的处理
if(self.isElementNode(node)) {
self.compile(node);
}
//当遇到文本节点时,并且文本节点中有双花括号表达式时,就做出相应的处理
if(self.isTextNode(node) && reg.test(text)){
//reg.exec() 函数的使用是为了获取到双花括号中间的字符串,即变量名,也可以使用 reg.match(),
但在全局正则对象中只能使用 exec()。
self.compileText(node, reg.exec(text)[1]);
}
//当子节点有子节点时,这个递归很重要,即便是文本节点,也会被编译,
这就是为什么元素节点中的文本节点可以被处理到的原因
if(node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
});
},
compile(node) {
//左边的表达式返回一个对象,并且有 length 属性,且其他索引不为负数,因此是一个类数组
let nodeAttrs = node.attributes,
self = this;
//类数组借用数组的 forEach 方法
Array.prototype.forEach.call(nodeAttrs, (attr) => {
let attrName = attr.name;
//如果是 v- 开头的,就是一个指令
if(self.isDirective(attrName)) {
let exp = attr.value,
dir = attrName.substring(2); //将 v- 去掉
if(self.isEventDirective(dir)) { //是否为事件指令?
self.compileEvent(node, self.vm, exp, dir);
}else { //如果不是事件指令,那就是 model 指令
self.compileModel(node, self.vm, exp, dir);
}
}
});
},
compileText(node, exp) {
let self = this,
initText = this.vm[exp];
//初始化文本节点的值
this.updateText(node, initText);
//当 data 中的值被改变时,watcher 就会通过回调函数调用 updateText 函数,使页面数据发生改变
new Watcher(this.vm, exp, function(newVal) {
self.updateText(node, newVal);
});
},
compileEvent(node, vm, exp, dir) {
let eventType = dir.split(':')[1], //取到事件名称
cb = vm.methods && vm.methods[exp]; //这是一个表达式赋值方式,当两个都为真时,
将右边的值给到 cb 变量
if(eventType && cb) {
//bind 函数跟 call 函数的差别就是不会马上执行,只会将 cb 函数中 this 指向改变
node.addEventListener(eventType, cb.bind(vm), false);
}
},
compileModel(node, vm, exp, dir) {
let self = this,
val = this.vm[exp];
this.modelUpdate(node, val);
new Watcher(this.vm, exp, function(newVal) {
self.modelUpdate(node, newVal);
});
//使用 input 事件来监听页面数据的变化,在这里定义 input 事件,一旦数据改变就改变 data 中的数据
node.addEventListener('input', function(e) {
let newVal = e.target.value;
if(val === newVal) return
self.vm[exp] = newVal;
val = newVal;
});
},
updateText(node, val) {
node.textContent = (typeof val == undefined) ? '' : val;
},
modelUpdate(node, val) {
node.value = (typeof val == undefined) ? '' : val;
},
isDirective(attr) {
return (attr.indexOf('v-') === 0);
},
isEventDirective(dir) {
return (dir.indexOf('on') === 0);
},
isElementNode(node) {
return (node.nodeType === 1);
},
isTextNode(node) {
return (node.nodeType === 3);
}
};