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) {// 保存vmthis.$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;// 將原生節點拷貝到fragmentwhile (child = el.firstChild) {fragment.appendChild(child);}return fragment;},init: function () {// 編譯fragmentthis.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:clickvar attrName = attr.name;// 判斷是否是指令屬性if (me.isDirective(attrName)) {// 得到表達式(屬性值): testvar exp = attr.value;// 得到指令名: on:clickvar 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-htmlhtml: function (node, vm, exp) {this.bind(node, vm, exp, 'html');},// 解析: v-modelmodel: 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-classclass: 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) {// 得到事件名/類型: clickvar eventType = dir.split(':')[1],// 根據表達式得到事件處理函數(從methods中): test(){}fn = vm.$options.methods && vm.$options.methods[exp];// 如果都存在if (eventType && fn) {// 綁定指定事件名和回調函數的DOM事件監聽, 將回調函數中的this強制綁定為vmnode.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 = {// 更新節點的textContenttextUpdater: function (node, value) {node.textContent = typeof value == 'undefined' ? '' : value;},// 更新節點的innerHTMLhtmlUpdater: function (node, value) {node.innerHTML = typeof value == 'undefined' ? '' : value;},// 更新節點的classNameclassUpdater: function (node, value, oldValue) {var className = node.className;className = className.replace(oldValue, '').replace(/s$/, '');var space = className && String(value) ? ' ' : '';node.className = className + space + value;},// 更新節點的valuemodelUpdater: function (node, value, oldValue) {node.value = typeof value == 'undefined' ? '' : value;}
};
最后
未完待續...
接下來,還有一個更有趣的東西
下一章繼續~