Vue MVVM 模式 解析
Vue MVVM模式 解析
前言
对于学习前端的朋友,Vue框架应该是耳熟能详的。Vue成为如今最火的框架,其MVVM模式也是让大家十分喜爱,本文仅解析其原理,并不是Vue的基础使用教学。
本文源码下载:https://github.com/li-car-fei/Vue-MVVM-Model
结构
先给出代码的结构,以及MVVM模式示例图
mvvm.js
在mvvm.js中,定义了mvvm类,即对应vue类,新建该实例,则对应于我们新建vue实例 new Vue()
新建mvvm实例对象时,我们需要对数据data,计算属性computed进行数据劫持,通过defineProperty对传入的属性进行响应式绑定。
然后调用observe(),对属性进行解析,给每个属性绑定一个Dep,然后调用compile(),对模板进行解析,把命令都解析出来并且给每一个命令添加一个watcher,监听属性变化。
//新建MVVM实例对象的构造函数
function MVVM(options) {
this.$options = options || {}; // 拿到所有 el data methods 等
var data = this._data = this.$options.data;
var me = this;
// 数据代理
// 实现 vm.xxx -> vm._data.xxx
//获取data中属性名数组
Object.keys(data).forEach(function (key) {
//对属性名数组每一个属性名,进行数据绑定
me._proxyData(key);
});
this._initComputed(); //计算属性绑定
observe(data, this); //调用observe函数,对data中所有层次的属性通过数据劫持实现数据绑定
this.$compile = new Compile(options.el || document.body, this) //模板解析,初始化显示
}
//MVVM的原型对象定义
MVVM.prototype = {
constructor: MVVM,
$watch: function (key, cb, options) {
new Watcher(this, key, cb);
},
// data proxy
_proxyData: function (key, setter, getter) {
var me = this;
setter = setter ||
Object.defineProperty(me, key, {
//与me._data中数据通过getter和setter绑定
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key]; // proxy代理,使得可以直接通过 this.key 的形式修改值
},
set: function proxySetter(newVal) { //vm中的setter告诉data中的setter更新数据,data中的setter再告诉监视者更新代码
me._data[key] = newVal; // proxy代理,将新设的值传到 me._data
}
});
},
_initComputed: function () {
var me = this;
var computed = this.$options.computed; // 获取 computed 对象 所有计算属性
if (typeof computed === 'object') {
Object.keys(computed).forEach(function (key) {
//computed属性遍历绑定getter和setter
Object.defineProperty(me, key, {
//判断此computed属性是不是只有getter
get: typeof computed[key] === 'function'
? computed[key] //该属性只有getter,直接调用定义的该getter
: computed[key].get, //该属性有setter和getter,在对象中,调用对象中的getter
set: function () { }
});
});
}
}
};
observer.js
在observer.js中,我们看到,对属性进行遍历劫持过程中,defineReactive()方法是最为关键的,它对每一个属性进行劫持分析并且深度递归。而在defineProperty之前给属性绑定dep作为监视者,在set中通过dep.notify()通知watcher完成响应式的功能。同时,在get中Dep.target的判断也十分重要,这一步是后面dep与watcher进行关联的关键。
//新建Observe实例
function Observer(data) { //观察者,观察data中所有数据
this.data = data;
this.walk(data); //开始观察
}
Observer.prototype = {
constructor: Observer,
walk: function (data) {
var me = this; //this指向Observe的实例
//获取data对象中所有属性名数组,遍历调用convert
Object.keys(data).forEach(function (key) { // 将data中所有值进行观察
me.convert(key, data[key]);
});
},
convert: function (key, val) {
//对传入的属性名,对应的属性值调用defineReactive响应性数据绑定
this.defineReactive(this.data, key, val);
},
defineReactive: function (data, key, val) {
// 创建属性对应的dep对象
var dep = new Dep();
//嵌套遍历子对象,若无子对象则返回
var childObj = observe(val); //递归调用,实现data中全部层次的数据劫持
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define重新定义
get: function () { //返回值,建立dep与watcher之间的关系
if (Dep.target) { // watcher中调用getter获取值前绑定了Dep.target,
dep.depend(); //建立关系
}
return val;
},
set: function (newVal) {
if (newVal === val) { //如果新值与旧值一样,不作响应,返回
return;
}
val = newVal;
// 新的值是object的话,进行嵌套监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
};
function observe(value, vm) {
//被观察的必须是对象
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value); //新建Observe实例
};
var uid = 0;
function Dep() {
this.id = uid++; //没创建一个实例都id加一
this.subs = []; //多个订阅者(监听)的数组
}
Dep.prototype = {
//增加watcher监听
addSub: function (sub) {
this.subs.push(sub); // 在watcher添加此dep时调用,此dep也添加那个watcher
},
//去建立dep和watcher之间的关系
depend: function () {
Dep.target.addDep(this); // 这里的 Dep.target 指向 watcher ,调用watcher的addDep方法,把此dep添加到watcher的dep数组中
},
//移除watcher监听
removeSub: function (sub) {
var index = this.subs.indexOf(sub);
if (index != -1) {
this.subs.splice(index, 1);
}
},
//遍历监听者watcher列表,通知更新值
notify: function () {
this.subs.forEach(function (sub) {
sub.update(); // 调用watcher 的update方法,更细视图
});
}
};
Dep.target = null;
compile.js
compile解析文档过程中,通过正则判断,node节点类型判断的方式,从文档流之中解析出vue指令,然后给每一个vue指令绑定一个watcher,在watcher之中有对应的更新视图的函数对应不同的更新方法,如css样式更新,html内容更新,{{}}内容更新等
//解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令替换数据,以及绑定相应的更新函数
function Compile(el, vm) {
this.$vm = vm;
//判断是否是节点,通过document.querySelector拿到第一个符合el选择器的元素的节点
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el); // 生成文档碎片,优化编译,防止多次修改视图
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
constructor: Compile,
node2Fragment: function (el) {
//创建文档片段
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝添加到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
init: function () {
this.compileElement(this.$fragment);
},
compileElement: function (el) {
//获取文档片段的子节点
var childNodes = el.childNodes,
me = this;
//先通过slice方法将childNodes转为数组,再遍历 可以用 [...childNodes] 代替
[].slice.call(childNodes).forEach(function (node) {
//获取子节点的text内容
var text = node.textContent;
//{{}}-内容绑定 的正则判断式
var reg = /\{\{(.*)\}\}/;
if (me.isElementNode(node)) { //判断子节点是不是元素节点
me.compile(node); //是元素节点,对其添加的键列进行处理
} else if (me.isTextNode(node) && reg.test(text)) { //判断是不是文本节点并且文本有 {{}} 数据获取
me.compileText(node, RegExp.$1.trim());
//对文本的 {{}} 数据获取进行处理,并将正则表达式匹配到的第一个子匹配字符串(绑定的值)传过去
// RegExp.$1 获得正则匹配第一个匹配的值,即是{{}}内的值
}
if (node.childNodes && node.childNodes.length) {
me.compileElement(node); //继续嵌套遍历其子节点,进行上面判断
}
});
},
compile: function (node) {
var nodeAttrs = node.attributes, //获取元素节点的attr键
me = this;
//先通过slice方法将nodeAttrs键列转为数组,再遍历 可以用 [...nodeAttrs] 代替
[].slice.call(nodeAttrs).forEach(function (attr) {
var attrName = attr.name; //获取绑定的键名
if (me.isDirective(attrName)) { //判定键名是否以 v- 开头
var exp = attr.value; //获取绑定的键值,字符串形式
var dir = attrName.substring(2); //substring字符串方法提取键名 v- 后面的内容
// 事件指令
if (me.isEventDirective(dir)) { //判断键名是不是 v-on 开头
compileUtil.eventHandler(node, me.$vm, exp, dir); //添加事件监听
} else { // 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); //普通指令处理
}
node.removeAttribute(attrName); // 删除已经处理的键attribute
}
});
},
compileText: function (node, exp) {
compileUtil.text(node, this.$vm, exp); //对文本节点存在的 {{}} 数据获取的处理,exp即匹配到的要获取的值
},
isDirective: function (attr) {
return attr.indexOf('v-') == 0;
},
isEventDirective: function (dir) {
return dir.indexOf('on') === 0;
},
isElementNode: function (node) {
return node.nodeType == 1;
},
isTextNode: function (node) {
return node.nodeType == 3;
}
};
// 非事件指令处理集合
var compileUtil = {
text: function (node, vm, exp) { // v-text
this.bind(node, vm, exp, 'text');
},
html: function (node, vm, exp) { // v-html
this.bind(node, vm, exp, 'html');
},
model: function (node, vm, exp) { // v-model
this.bind(node, vm, exp, 'model');
var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function (e) { // 添加input事件监听,实现v-model的双向数据绑定
var newValue = e.target.value; //事件触发时,先获得新值
if (val === newValue) {
return; //新值与旧值相同,返回
}
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
class: function (node, vm, exp) { // v-class
this.bind(node, vm, exp, 'class');
},
bind: function (node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater']; //获取对应的添加数据的对应方法
updaterFn && updaterFn(node, this._getVMVal(vm, exp)); //先获取具体绑定的值,再调用添加数据的方法
new Watcher(vm, exp, function (value, oldValue) { //新建watcher实例,exp对应的数据改变时,调用回调函数
updaterFn && updaterFn(node, value, oldValue); //回调函数的作用与上相似,先获取具体绑定的值,再调用添加数据的方法
});
},
// 事件处理
eventHandler: function (node, vm, exp, dir) {
var eventType = dir.split(':')[1], //获取绑定的具体事件名
fn = vm.$options.methods && vm.$options.methods[exp]; //查看methods中是否有对应的处理函数
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false); //添加事件绑定
}
},
//获取具体的绑定的值 获取 vm.data 中的指定的那个值
_getVMVal: function (vm, exp) {
var val = vm;
exp = exp.split('.'); //将匹配到的js表达式先分割
exp.forEach(function (k) { //遍历
val = val[k]; //让val最终获得到实际绑定的值
});
return val;
},
//将改变的新的值,设为绑定的数据的新值 v-model中调用 更新 vm.data 中的值
_setVMVal: function (vm, exp, value) {
var val = vm;
exp = exp.split('.'); //将匹配到的js表达式先分割
exp.forEach(function (k, i) { //遍历
// 非最后一个key,更新val的值
if (i < exp.length - 1) { //还没遍历到最后一层,继续
val = val[k];
} else { //遍历到最后一层,设立新值
val[k] = value;
}
});
}
};
//添加具体数据到元素节点的多种对应方法:
var updater = {
textUpdater: function (node, value) {
node.textContent = typeof value == 'undefined' ? '' : value; //将要获取的值添加到元素节点的text节点中,完成数据获取
},
htmlUpdater: function (node, value) { //将要获取的值添加到元素节点的innerHTML中,完成数据获取
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
classUpdater: function (node, value, oldValue) { //将要获取的值添加到元素节点的class属性中,完成双向数据获取
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
modelUpdater: function (node, value) { //将要获取的值添加到元素节点的value属性中,完成双向数据绑定
node.value = typeof value == 'undefined' ? '' : value;
}
};
watcher.js
最后我们来看watcher.js中对watcher的定义。在new watcher 新建watcher实例时,势必要去获取observer劫持了的属性的值,以完成第一次视图渲染,而在此时将Dep.target赋值为当前的watcher,如前面所说,在observer劫持的属性的get中判断Dep.target,如果有指向,则完成两者的绑定
//Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
function Watcher(vm, expOrFn, cb) {
this.cb = cb; //回调函数
this.vm = vm;
this.expOrFn = expOrFn; //绑定的键值,字符串形式
this.depIds = {}; //这个watcher所有相关联dep的容器对象
if (typeof expOrFn === 'function') {
this.getter = expOrFn; // 绑定的是函数,则直接赋给getter,用以调用函数获取值
} else {
this.getter = this.parseGetter(expOrFn.trim()); // 绑定的是表达式,trim()去除前后空格
}
this.value = this.get(); // 得到表达式的初始值
}
Watcher.prototype = {
constructor: Watcher,
update: function () { // 属性更改,视图更新
this.run();
},
run: function () {
var value = this.get(); // 先调用getter获取新的值
var oldVal = this.value; // 旧的值,绑定在初始化时的 this.value 中的
if (value !== oldVal) {
this.value = value; // 把储存老的值的 this.value 赋值新的值
this.cb.call(this.vm, value, oldVal); //调用回调函数更新界面
}
},
addDep: function (dep) {
//判断dep与watcher的关系是否已经建立
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this); // 调用dep中的addSub方法 给dep添加当前这个watcher 用于更新
this.depIds[dep.id] = dep; // 给watcher添加关联的dep
}
},
get: function () {
// 给Dep指定当前的watcher
Dep.target = this;
// 获取函数或者表达式的值,内部调用get建立dep与watcher的关系
var value = this.getter.call(this.vm, this.vm);
// 去除Dep中指定的当前watcher
Dep.target = null;
return value;
},
// 解析绑定的表达式
parseGetter: function (exp) {
if (/[^\w.$]/.test(exp)) return; // 没有层级,直接返回, 可以通过 this.exp 获取到值
var exps = exp.split('.'); // 变为值层次化的数组 [person.name] => [person,name]
return function (obj) {
for (var i = 0, len = exps.length; i < len; i++) { // 遍历获取深层的值
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
};
结语
此文对Vue MVVM 模式做简要分析,并未使用Vue源码
欢迎大家一起交流学习:
编者github地址:传送
qq:1073490398
wechat:carfiedfeifei