vue源码探究(第四弹)
结束了上一part的数据代理,这一部分主要讲讲vue的模板解析,感觉这个有点难理解,而且内容有点多,hhh。
模板解析
废话不多说,先从简单的入手。
按照之前的套路,先举一个例子:
<div id="test"> <p>{{name}}</p></div><script type="text/javascript" src="js/mvvm/compile.js"></script><script type="text/javascript" src="js/mvvm/mvvm.js"></script><script type="text/javascript" src="js/mvvm/observer.js"></script><script type="text/javascript" src="js/mvvm/watcher.js"></script><script type="text/javascript"> new MVVM({ el: '#test', data: { name: '喵喵喵' } }) // 这时候,我们的页面还是渲染出 喵喵喵</script>
接下来讲讲内部的相关实现:
我们的MVVM中的构造函数中有什么东西,可以解析我们的模板呢?
// 创建一个用来编译模板的compile对象this.$compile = new Compile(options.el || document.body, this)
什么是Compile?
一行一行注释着解读
function Compile(el, vm) { // 保存vm this.$vm = vm; // 保存el元素 this.$el = this.isElementNode(el) ? el : document.querySelector(el); // 如果el元素存在 if (this.$el) { // 1. 取出el中所有子节点, 封装在一个framgment对象中 // 这里的node2Fragment 就是将node -> 放入 Fragment中,documentFragment将node进行批量处理 this.$fragment = this.node2Fragment(this.$el); // 2. 编译fragment中所有层次子节点 this.init(); // 3. 将fragment添加到el中 this.$el.appendChild(this.$fragment); }}Compile.prototype = { node2Fragment: function (el) { var fragment = document.createDocumentFragment(), child; // 将原生节点拷贝到fragment while (child = el.firstChild) { fragment.appendChild(child); } return fragment; }, init: function () { // 编译fragment this.compileElement(this.$fragment); }, compileElement: function (el) { // 得到所有子节点 var childNodes = el.childNodes, // 保存compile对象 me = this; // 遍历所有子节点 [].slice.call(childNodes).forEach(function (node) { // 得到节点的文本内容 var text = node.textContent; // 正则对象(匹配大括号表达式) var reg = /\{\{(.*)\}\}/; // {{name}} // 这里提出一个问题,为什么这里的正则匹配要用/\{\{(.*)\}\}/,而不是/\{\{.*\}\}/呢? // 其实/\{\{.*\}\}/就可以匹配到{{xxx}},这里加一个()的意义是,用于.$1,来取得{{}}中的值,eg:name // 如果是元素节点 if (me.isElementNode(node)) { // 编译元素节点的指令属性 me.compile(node); // 如果是一个大括号表达式格式的文本节点 } else if (me.isTextNode(node) && reg.test(text)) { // 编译大括号表达式格式的文本节点 me.compileText(node, RegExp.$1); // RegExp.$1: 表达式 name } // 如果子节点还有子节点 if (node.childNodes && node.childNodes.length) { // 递归调用实现所有层次节点的编译 me.compileElement(node); } }); }, compile: function (node) { // 得到所有标签属性节点 var nodeAttrs = node.attributes, me = this; // 遍历所有属性 [].slice.call(nodeAttrs).forEach(function (attr) { // 得到属性名: v-on:click var attrName = attr.name; // 判断是否是指令属性 if (me.isDirective(attrName)) { // 得到表达式(属性值): test var exp = attr.value; // 得到指令名: on:click 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) { // 调用编译工具对象解析 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; }};// 指令处理集合 var compileUtil = { // 解析: v-text/{{}} text: function (node, vm, exp) { this.bind(node, vm, exp, 'text'); }, // 解析: v-html html: function (node, vm, exp) { this.bind(node, vm, exp, 'html'); }, // 解析: v-model 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; }); }, // 解析: v-class class: function (node, vm, exp) { this.bind(node, vm, exp, 'class'); }, // 真正用于解析指令的方法 bind: function (node, vm, exp, dir) { /*实现初始化显示*/ // 根据指令名(text)得到对应的更新节点函数 // 取到一个object的属性,有2个方法,一个是obj. 一个是obj[] // 当我们要取得属性是一个变量的时候,使用obj[] var updaterFn = updater[dir + 'Updater']; // 如果存在调用来更新节点 updaterFn && updaterFn(node, this._getVMVal(vm, exp)); // 创建表达式对应的watcher对象 new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/ // 当对应的属性值发生了变化时, 自动调用, 更新对应的节点 updaterFn && updaterFn(node, value, oldValue); }); }, // 事件处理 eventHandler: function (node, vm, exp, dir) { // 得到事件名/类型: click var eventType = dir.split(':')[1], // 根据表达式得到事件处理函数(从methods中): test(){} fn = vm.$options.methods && vm.$options.methods[exp]; // 如果都存在 if (eventType && fn) { // 绑定指定事件名和回调函数的DOM事件监听, 将回调函数中的this强制绑定为vm node.addEventListener(eventType, fn.bind(vm), false); } }, // 得到表达式对应的value _getVMVal: function (vm, exp) { // 这里为什么要forEach呢? // 如果你的exp是a.b.c.c.d呢 就需要forEach 如果只是一层 当然不需要遍历啦 var val = vm._data; exp = exp.split('.'); exp.forEach(function (k) { val = val[k]; }); return val; }, _setVMVal: function (vm, exp, value) { var val = vm._data; exp = exp.split('.'); exp.forEach(function (k, i) { // 非最后一个key,更新val的值 if (i < exp.length - 1) { val = val[k]; } else { val[k] = value; } }); }};// 包含多个用于更新节点方法的对象 var updater = { // 更新节点的textContent textUpdater: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value; }, // 更新节点的innerHTML htmlUpdater: function (node, value) { node.innerHTML = typeof value == 'undefined' ? '' : value; }, // 更新节点的className 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; }, // 更新节点的value modelUpdater: function (node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value; }};
最后
未完待续...
接下来,还有一个更有趣的东西
下一章继续~