MVVM的实现原理入门了解3-- 模板解析与watcher(订阅者)的创建
在MVVM.js文件中第三步执行的是
this.$compile = new Compile(options.el || document.body, this)
那么执行compile.js文件
function Compile(el, vm) {
this.$vm = vm; // vm表示mvvm中的this 使其保存到Compile实例上
// vm中有el属性 判断el是元素节点还是其他节点 如果是元素节点 返回自身 如果不是使用querySelector进行查找
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
// 判断this.$el
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el); //创建文档碎片 将el中的内容插入到documentFragment中
this.init();// 进行模板解析
this.$el.appendChild(this.$fragment); //将编译完毕的文档碎片内容 从新插入到this.$el所对应的DOM元素中
}
}
Compile.prototype = {
// 创建文档碎片节点
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
//返回文档碎片节点
return fragment;
},
//编译元素
init: function() {
this.compileElement(this.$fragment); //用于编译传入的在内存中的fragment元素
},
compileElement: function(el) {
var childNodes = el.childNodes, //获取文档碎片的子节点
me = this; //Compile实例
//使文档碎片节点改为数组的形式 然后遍历数组
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent; // 使text都为node中的文本内容
var reg = /\{\{(.*)\}\}/; // 定义正则表达式 用于匹配{{ data中的属性 }}
if (me.isElementNode(node)) { //判断这个节点是否为元素节点
me.compile(node); // 如果是元素节点 判断指令类型
} else if (me.isTextNode(node) && reg.test(text)) { // 判断这个节点是否是文本节点 并且带有带括号的文本内容
me.compileText(node, RegExp.$1.trim()); // 解析node中 正则表达式中的子表达式的内容 也就是 {{ 内容 }}
}
if (node.childNodes && node.childNodes.length) { //判断node是否有子节点并且子节点的个数不为0
me.compileElement(node); //递归执行
}
});
},
//进行编译节点
compile: function(node) {
//获取节点的所用属性内容
var nodeAttrs = node.attributes,
me = this;
// 将属性节点转为数组 并遍历
[].slice.call(nodeAttrs).forEach(function(attr) {
//用attrName 保存attr.name 属性名
var attrName = attr.name;
if (me.isDirective(attrName)) {
var exp = attr.value; //data中的属性名
var dir = attrName.substring(2);
// 事件指令
if (me.isEventDirective(dir)) {
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
node.removeAttribute(attrName);
}
});
},
compileText: function(node, exp) {
// 编译指令 使用工具库的text方法 进行解析 传入节点 vm 正则表达式
compileUtil.text(node, this.$vm, 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;
}
};
// 编译指令处理集合
// 指令集合使用的都是bind绑定的
var compileUtil = {
text: function(node, vm, exp) { // text指令
// node vm实例 exp属性名 指令名
this.bind(node, vm, exp, 'text'); //执行bind
},
html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');
var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
bind: function(node, vm, exp, dir) {
// updaterFn用于对{{}} 以及 各种指令的解析
//updater为一个对象 调用 dir为指令名 结果为 var updaterFn = updater['textUpdater']
//得到更新节点的函数
var updaterFn = updater[dir + 'Updater']; //调用updater对象中的对应属性
//updaterFn存在 并且 调用updaterFn 传入 node , this._getVMVal(vm,exp)的结果
//调用函数更新节点
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 监视数据变化
//####################################################
// watcher 用于更新显示视图
// watcher中 传递参数 vm实例 exp指令表达式 回调函数新值与旧值
new Watcher(vm, exp, function(value, oldValue) {
// 回调函数 更新exp指令表达式中相对应的data属性时 节点发生改变
updaterFn && updaterFn(node, value, oldValue);
});
},
// 事件处理
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
// 从vm中得到表达式所对应的值
_getVMVal: function(vm, exp) {
var val = vm; //val为 MVVM实例化后的vm对象
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
},
_setVMVal: function(vm, exp, value) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
};
// 更新器 节点的内容 属性
var updater = {
//更新节点中的 TEXT
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
},
//更新节点中的 HTML
htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
},
//更新节点的class
classUpdater: function(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, '');
var space = className && String(value) ? ' ' : '';
node.className = className + space + value;
},
// model指令
modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};
模板解析的理解
模板解析分为三步进行
- 创建文档碎片节点 document.createDocumentFragment
- 将el对应的节点内的所有子节点appendChild(剪切操作)到documentFragment中进行处理
- 将处理好的documentFragment中所有的子节点重新appendChild到el对应的节点中
compile.js文件中对应的 是在Compile.prototype.compile为主要进行模板解析的方法
- 取出所有文档碎片节点的内容 也就是文档碎片的子节点
- 将取出子节点的伪数组转为数组 使用[].slice.call(伪数组) 或者是 Array.from() 然后进行遍历
- 对每一个子节点进行操作 主要为解析 节点属性的v-指令、事件指令以及节点内容的{{}}表达式 获取指令名称(var dir = attrName.substring(2); 用于剪切on或者v-) 并调用compileUtil进行操作
compile的文件中还包括 compileUtil 这个对象 这个对象主要用于对指令,更新节点的内容、属性,事件指令等操作
- 对应v-指令 由于普通指令传递的参数都为(node, vm, exp)且都是调用bind方法进行的操作 因此普通指令的核心为bind方法
- 事件指令主要调用addEventListener进行操作、更新操作主要使用innerHTML、innerText、className等操作自行查看即可
对应普通指令中使用的bind方法 理解
- bind方法中 首先调用视图更新 也就是 updater对象中的对应方法
- 创建watcher并传递vm实例、data中的属性名(exp)、回调函数用于更新视图
Watcher的实例化并传参
watcher.js文件中的内容
function Watcher(vm, expOrFn, cb) {
this.cb = cb; //用于更新界面的回调
this.vm = vm; //vm实例
this.expOrFn = expOrFn; //对应的data中的属性值 表达式
this.depIds = {}; //相关的dep的容器对象
// 表达式的解析
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
this.value = this.get(); //返回data中的属性对应的value
}
Watcher.prototype = {
// 数据更新时调用
update: function() {
this.run();
},
run: function() {
// 让value等于 this.get
var value = this.get(); //获取最新的值
var oldVal = this.value;//获取旧值
if (value !== oldVal) {// 如果新的值与旧的值不相等
this.value = value; //让value为新的值
// cb.call 是执行Watcher中的cb形参
// 调用回调函数
this.cb.call(this.vm, value, oldVal); //更新界面的回调函数
}
},
addDep: function(dep) {
//判断depId容器中是否该dep.id的属性名 如果没有进入 有不进入 也就是说判断watcher与dep的关系是否建立
// 有为true 没有为false
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this); //调用dep的addSub方法 传入 watcher
this.depIds[dep.id] = dep; //让depIds容器中的dep.id属性名的属性值为 dep
// depIds:{ dep.id:dep } 此时也就是将 dep保存到了 watcher中
}
},
//获取data中 对应的属性值 会导致observer中get调用
get: function() {
// 将dep.target = this 也就是说 将当前的watcher 赋值给 Dep.target
Dep.target = this;
// 获取值时会导致observer给data中的属性值添加的get方法执行
var value = this.getter.call(this.vm, this.vm); //获取data中属性的值
// 执行完毕后将 Dep.target清空·
Dep.target = null;
//返回value值
return value;
},
//这个方法是解析传递的data中的属性值 并返回
parseGetter: function(exp) {
if (/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
};
由于compile.js文件中bind方法的调用导致了 Watcher的实例化
watcher方法的使用:
- 进行初始化 保存回调cb,vm实例,exp(data中对应的属性值),创建自身属性depIds对象,this.value = this.get();
- 因为this.value = this.get() 导致了Watcher.prototype.get方法被调用
parseGetter方法主要用于解析传递的data中的属性值 防止传递的为对象形式
- 由于get方法的调用导致dep与watcher产生一系列的关系

浙公网安备 33010602011771号