接上文:一套代碼小程序&Web&Native運行的探索04——數據更新javascript
對應Git代碼地址請見:https://github.com/yexiaochai/wxdemo/tree/master/mvvmhtml
https://github.com/fastCreator/MVVM(極度參考,十分感謝該做者,直接看Vue會比較吃力的,可是看完這個做者的代碼便會輕易不少,惋惜這個做者沒有對應博客說明,否則就爽了)node
https://www.tangshuang.net/3756.htmlwebpack
https://www.cnblogs.com/kidney/p/8018226.htmlgit
http://www.cnblogs.com/kidney/p/6052935.htmlgithub
https://github.com/livoras/blog/issues/13web
根據最近的學習,離咱們最終的目標還有一段距離,可是對於Vue實現原理卻慢慢有了體系化的認識,相信本系列結束後,若是能完成咱們跨端代碼,哪怕是demo的實現,都會對後續瞭解Vue或者React這裏源碼提供深遠的幫助,平時工做較忙,此次恰好碰到假期,雖然會耽擱一些時間,咱們試試這段時間運氣可好,能不能在這個階段取得不錯的進展,好了咱們繼續完成今天的學習吧算法
到目前的地步,其中一些代碼比較散亂,沒有辦法粘貼出來作講解了,我這邊儘可能寫註釋,這裏文章記錄的主要目的仍是幫助本身記錄思路express
昨天,咱們完成了最簡單的模板到DOM的實現,以及執行setData時候頁面從新渲染工做,只不過比較粗暴尚未引入snabbdom進行了從新渲染,今天咱們來完成其中的事件綁定部分代碼
這裏咱們先不去管循環標籤這些的解析,先完成事件綁定部分代碼,這裏若是隻是想實現click綁定便直接在此處綁定事件便可:
1 class Element { 2 constructor(tagName, props, children, vm) { 3 this.tagName = tagName; 4 this.props = props; 5 this.children = children || []; 6 this.vm = vm.vm; 7 } 8 render() { 9 //拿着根節點往下面擼 10 let el = document.createElement(this.tagName); 11 let props = this.props.props; 12 let scope = this; 13 14 let events = this.props.on; 15 16 for(let name in props) { 17 el.setAttribute(name, props[name]); 18 } 19 20 for(name in events) { 21 let type = Object.keys(this.props.on); 22 type = type[0]; 23 el.addEventListener(type, function (e) { 24 scope.vm.$options.methods[scope.props.on[type]] && scope.vm.$options.methods[scope.props.on[type]].call(scope.vm, e); 25 }) 26 } 27 28 let children = this.children; 29 30 for(let i = 0, l = children.length; i < l; i++) { 31 let child = children[i]; 32 let childEl; 33 if(child instanceof Element) { 34 //遞歸調用 35 childEl = child.render(); 36 } else { 37 childEl = document.createTextNode(child); 38 } 39 el.append(childEl); 40 } 41 return el; 42 } 43 }
顯然,這個不是咱們要的最終代碼,事實上,事件如何綁定dom如何比較差別渲染,咱們這塊不須要太多關係,咱們只須要引入snabbdom便可,這裏便來一塊兒瞭解之
snabbdom
前面咱們對snabbdom作了初步介紹,暫時看來MVVM框架就我這邊學習的感受有如下幾個難點:
① 第一步的模板解析,這塊很容易出錯,但若是有志氣jQuery源碼的功底就會比較輕易
② 虛擬DOM這塊,要對比兩次dom樹的差別再選擇如何作
只要突破這兩點,其餘的就會相對簡單一些,而這兩塊最難也最容易出錯的工做,咱們所有引用了第三方庫HTMLParser和snabbdom,因此咱們都碰上了好時代啊......
咱們很容易將一個dom結構用js對象來抽象,好比咱們以前作的班次列表中的列表排序:
這裏出發的因子就有出發時間、耗時、價格,這裏表示下就是:
1 let trainData = { 2 sortKet: 'time', //耗時,價格,發車時間等等方式排序 3 sortType: 1, //1升序,2倒敘 4 oData: [], //服務器給過來的原生數據 5 data: [], //當前篩選條件下的數據 6 }
這個對象有點缺陷就是不能與頁面映射起來,咱們以前的作法就算映射起來了,也只會跟一個跟節點作綁定關係,一旦數據發生變化便所有從新渲染,這個仍是小問題,比較複雜的問題是半年後篩選條件增長,這個頁面的代碼可能會變得至關難維護,其中最難的點可能就是頁面中的dom關係維護,和事件維護
而咱們想要的就是數據改變了,DOM本身就發生變化,而且以高效的方式發生變化,這個就是咱們snabbdom作的工做了,而以前咱們用一段代碼說明過這個問題:
var element = { tagName: 'ul', // 節點標籤名 props: { // DOM的屬性,用一個對象存儲鍵值對 id: 'list' }, children: [ // 該節點的子節點 {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}, ] }
1 <ul id='list'> 2 <li class='item'>Item 1</li> 3 <li class='item'>Item 2</li> 4 <li class='item'>Item 3</li> 5 </ul>
真實的虛擬DOM會翻譯爲這樣:
class Element { constructor(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; } } function el(tagName, props, children) { return new Element(tagName, props, children) } el('ul', {id: 'list'}, [ el('li', {class: 'item'}, ['Item 1']), el('li', {class: 'item'}, ['Item 2']), el('li', {class: 'item'}, ['Item 3']) ])
這裏很快就能封裝一個可運行的代碼出來:
<!doctype html> <html> <head> <title>起步</title> </head> <body> <script type="text/javascript"> //***虛擬dom部分代碼,後續會換成snabdom class Element { constructor(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; } render() { //拿着根節點往下面擼 let root = document.createElement(this.tagName); let props = this.props; for(let name in props) { root.setAttribute(name, props[name]); } let children = this.children; for(let i = 0, l = children.length; i < l; i++) { let child = children[i]; let childEl; if(child instanceof Element) { //遞歸調用 childEl = child.render(); } else { childEl = document.createTextNode(child); } root.append(childEl); } this.rootNode = root; return root; } } function el(tagName, props, children) { return new Element(tagName, props, children) } let vnode = el('ul', {id: 'list'}, [ el('li', {class: 'item'}, ['Item 1']), el('li', {class: 'item'}, ['Item 2']), el('li', {class: 'item'}, ['Item 3']) ]) let root = vnode.render(); document.body.appendChild(root); </script> </body> </html>
咱們今天要作的事情,即是把這段代碼寫的更加完善一點,就要進入第二步,比較兩顆虛擬樹的差別了,而這塊也是snabbdom的核心,固然也比較有難度啦
PS:這裏借鑑:https://github.com/livoras/blog/issues/13
實際代碼中,會對兩棵樹進行深度優先遍歷,這樣會給每一個節點一個惟一的標誌:
在深度優先遍歷的時候,每到一個節點便與新的樹進行對比,若是有差別就記錄到一個對象中:
1 //遍歷子樹,用來作遞歸的 2 function diffChildren(oldNodeChildren, newNodeChildren, index, patches) { 3 4 let leftNode = null; 5 let curNodeIndex = index; 6 7 for(let i = 0, l = oldNodeChildren.length; i < l; i++) { 8 let child = oldNodeChildren[i]; 9 let newChild = newNodeChildren[i]; 10 11 //計算節點的標識 12 curNodeIndex = (leftNode && leftNode.count) ? curNodeIndex + leftNode.count + 1 : curNodeIndex + 1; 13 dfsWalk(child, newChild) 14 leftNode = child; 15 } 16 } 17 18 //對兩棵樹進行深度優先遍歷,找出差別 19 function dfsWalk(oldNode, newNode, index, patches) { 20 //將兩棵樹的不一樣記錄之 21 patches[index] = []; 22 diffChildren(oldNode.children, newNode.children, index, patches); 23 } 24 25 //對比兩棵樹的差別 26 function diff(oldTree, newTree) { 27 //當前節點標誌 28 let index = 0; 29 //記錄每一個節點的差別 30 let patches = {}; 31 //深度優先遍歷 32 return patches; 33 }
patches[0] = [{difference}, {difference}, ...] // 用數組存儲新舊節點的不一樣
這裏已經作好了工具流程遍歷節點得出差別,而咱們的差別有:
① 替換原來的節點,例如把div換成section
② 移動、刪除、新增子節點,例如把p與ul順序替換
③ 這個比較簡單,修改節點屬性
④ 這個也比較簡單,修改文本內容
這裏給這幾種類型的定義:
let REPLACE = 0 let REORDER = 1 let PROPS = 2 let TEXT = 3
節點替換首先判斷tagname是否一致便可:
patches[0] = [{ type: REPALCE, node: newNode // el('section', props, children) }]
若是給div新增屬性,便記錄之:
patches[0] = [{ type: REPALCE, node: newNode // el('section', props, children) }, { type: PROPS, props: { id: "container" } }]
若是是文本節點便記錄之:
patches[2] = [{ type: TEXT, content: "Virtual DOM2" }]
以上都比較常規,不會作太大改變,狀況比較多的是REODER(Reorder從新排列),好比將這裏div的子節點順序變成了div-p-ul,這個該如何對比,其實這個狀況可能會直接被替換掉,這樣DOM開銷太大,這裏牽扯到了列表對比算法,有點小複雜:
假如如今對英文字母進行排序,久的順序:
a b c d e f g h i
而後對節點進行了一系列的操做,新增j節點,刪除e節點,移動h節點,因而有了:
a b c h d f g i j
知道了新舊順序,如今須要咱們寫一個算法計算最小插入、刪除操做(移動是刪除+插入),這塊具體咱們不深刻,有興趣移步至,這裏代碼,咱們最終造成的結果是:
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]
因而咱們將這段尋找差別的代碼放入前面的遍歷代碼:
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { var currentPatches = patches[walker.index] // 從patches拿出當前節點的差別 var len = node.childNodes ? node.childNodes.length : 0 for (var i = 0; i < len; i++) { // 深度遍歷子節點 var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } if (currentPatches) { applyPatches(node, currentPatches) // 對當前節點進行DOM操做 } } function applyPatches (node, currentPatches) { currentPatches.forEach(function (currentPatch) { switch (currentPatch.type) { case REPLACE: node.parentNode.replaceChild(currentPatch.node.render(), node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
這個就是咱們snabbdom中重要的patch.js的實現,而Virtual DOM算法主要就是:
① 虛擬DOM element的定義
② 差別的定義與實現
③ 將差別部分代碼補足造成新樹的patch部分
// 1. 構建虛擬DOM var tree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: blue'}, ['simple virtal dom']), el('p', ['Hello, virtual-dom']), el('ul', [el('li')]) ]) // 2. 經過虛擬DOM構建真正的DOM var root = tree.render() document.body.appendChild(root) // 3. 生成新的虛擬DOM var newTree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: red'}, ['simple virtal dom']), el('p', ['Hello, virtual-dom']), el('ul', [el('li'), el('li')]) ]) // 4. 比較兩棵虛擬DOM樹的不一樣 var patches = diff(tree, newTree) // 5. 在真正的DOM元素上應用變動 patch(root, patches)
有了以上知識,咱們如今來開始使用snabbdom,相比會駕輕就熟
應用snabbdom
var snabbdom = require("snabbdom"); var patch = snabbdom.init([ // 初始化補丁功能與選定的模塊 require("snabbdom/modules/class").default, // 使切換class變得容易 require("snabbdom/modules/props").default, // 用於設置DOM元素的屬性(注意區分props,attrs具體看snabbdom文檔) require("snabbdom/modules/style").default, // 處理元素的style,支持動畫 require("snabbdom/modules/eventlisteners").default, // 事件監聽器 ]); //h是一個生成vnode的包裝函數,factory模式?對生成vnode更精細的包裝就是使用jsx //在工程裏,咱們一般使用webpack或者browserify對jsx編譯 var h = require("snabbdom/h").default; // 用於建立vnode,VUE中render(createElement)的原形 var container = document.getElementById("container"); var vnode = h("div#container.two.classes", {on: {click: someFn}}, [ h("span", {style: {fontWeight: "bold"}}, "This is bold"), " and this is just normal text", h("a", {props: {href: "/foo"}}, "I\"ll take you places!") ]); // 第一次打補丁,用於渲染到頁面,內部會創建關聯關係,減小了建立oldvnode過程 patch(container, vnode); //建立新節點 var newVnode = h("div#container.two.classes", {on: {click: anotherEventHandler}}, [ h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"), " and this is still just normal text", h("a", {props: {href: "/bar"}}, "I\"ll take you places!") ]); //第二次比較,上一次vnode比較,打補丁到頁面 //VUE的patch在nextTick中,開啓異步隊列,刪除了沒必要要的patch //nextTick異步隊列解析,下面文章中會詳解 patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
這裏能夠看到,咱們傳入h的要求是什麼樣的格式,依次有什麼屬性,這裏仍是來作一個demo:
1 <div id="container"> 2 </div> 3 4 <script type="module"> 5 "use strict"; 6 import { patch, h, VNode } from './libs/vnode.js' 7 var container = document.getElementById("container"); 8 function someFn(){ console.log(1)} 9 function anotherEventHandler(){ console.log(2)} 10 11 var oldVnode = h("div", {on: {click: someFn}}, [ 12 h("span", {style: {fontWeight: "bold"}}, "This is bold"), 13 " and this is just normal text", 14 h("a", {props: {href: "/foo"}}, "I\"ll take you places!") 15 ]); 16 17 // 第一次打補丁,用於渲染到頁面,內部會創建關聯關係,減小了建立oldvnode過程 18 let diff = patch(container, oldVnode); 19 //建立新節點 20 var newVnode = h("div", {on: {click: anotherEventHandler}}, [ 21 h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"), 22 " and this is still just normal text", 23 h("a", {props: {href: "/bar"}}, "I\"ll take you places!") 24 ]); 25 //第二次比較,上一次vnode比較,打補丁到頁面 26 //VUE的patch在nextTick中,開啓異步隊列,刪除了沒必要要的patch 27 //nextTick異步隊列解析,下面文章中會詳解 28 patch(oldVnode, newVnode); // Snabbdom efficiently updates the old view to the new state 29 function test() { 30 return { 31 oldVnode,newVnode,container,diff 32 } 33 } 34 </script>
因此咱們如今工做變得相對簡單起來就是根據HTML模板封裝虛擬DOM結構便可,若是不是咱們其中存在指令系統甚至能夠不用HTMLParser,因此咱們改下以前的代碼,將咱們本身實現的醜陋vnode變成snabbdom,這裏詳情仍是看github:https://github.com/yexiaochai/wxdemo/tree/master/mvvm。接下來,咱們來解決其中的指令
指令系統
這裏所謂的指令用的最多的也就是:
① if
② for
對應到小程序中就是:
<block wx:for="{{[1, 2, 3]}}"> <view> {{index}}: </view> <view> {{item}} </view> </block>
<block wx:if="{{true}}"> <view> view1 </view> <view> view2 </view> </block>
Vue中的語法是:
<ul id="example-1"> <li v-for="item in items"> {{ item.message }} </li> </ul>
<h1 v-if="ok">Yes</h1> <h1 v-else>No</h1>
大同小異,咱們來看看如何處理這種代碼,這裏也開始進入數組對象的處理,這裏便引入了指令系統,咱們這裏單獨說下這塊代碼
框架裏面的for或者if這種指令代碼由於要要保證框架性,首先寫的很分散,其次用起來也很繞,就很很差理解,因此這裏須要單獨拎出來講下
以前咱們使用的模板通常就是js代碼,直接被翻譯爲了js函數,好比這段代碼:
<ul> <% for(let key in arr) { %> <li>...</li> <% } %> </ul>
會被大概翻譯爲這個樣子:
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='<ul>\n '; for(let key in arr) { __p+='\n <li>...</li>\n '; } __p+='\n</ul>'; } return __p;
而MVVM類框架執行的是相同的邏輯,只不過代碼實現上面由於要考慮映射關係就複雜的多了:
<ul> <li m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}} </li> </ul>
翻譯後基本就是這個代碼:
with (this) { debugger ;return _h('ul', {}, [_l((arr), function(val, key, index) { return _h('li', { attrs: { "m-for": '(val, key, index) in arr' } }, ["索引 " + _s(key + 1) + " :" + _s(val)]) })]) }
全部的這一切都是爲了造成虛擬樹結構,最終要的是這樣的東西
因此指令是其中的工具,一個過程,幫助咱們達到目的,爲了幫助理解,咱們這邊單獨抽一段代碼出來講明這個問題,這裏再強調一下指令系統在總體流程中的意義是:
咱們最終目標是將模板轉換爲snabbdom中的vnode,這樣他便能本身渲染,而這裏的過程是
模板 => HTMLParser解析模板 => 框架element對象 => 解析框架element對象中的屬性,這裏包括指令 => 將屬性包含的指令相關信息同步到element對象上(由於每一個標籤都會有element對象)=> 生成用於render的函數(其實就是將element轉換爲snabbdom可識別的對象) => 生成snabbdom樹後,調用pacth便可完成渲染
因此指令系統在其中的意義即是:解析element中的指令對應的代碼,方便後續生成render匿名函數罷了,這就是爲何指令系統的實現包含了兩個方法:
① template2Vnode,這個事實上是將模板中與指令相關的信息放到element對象上方便後續vnode2render時候使用
② vnode2render,即是將以前存到element中與生成最終函數有關的字段拿出來拼接成函數字符串,調用的時候是在mvvm實例對象下,因此能夠取到傳入的data以及method
之因此設計的比較複雜是爲了讓你們方便新增自定義指令,這裏仍然先上一段簡單的說明性代碼:
1 <!doctype html> 2 <html> 3 <head> 4 <title>指令系統演示</title> 5 </head> 6 <body> 7 8 <script type="module"> 9 10 //須要處理的模板,咱們須要將他轉換爲虛擬dom vnode 11 let html = ` 12 <ul> 13 <li m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</li> 14 </ul> 15 ` 16 //這裏咱們爲了下降學習成本將這段模板再作一次簡化,變成這樣 17 html = '<div m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</div>'; 18 19 20 //處理element元素生成render函數 21 function genElement(el) { 22 //這裏若是有自定義指令也會被拿出來 23 if (!el.processed) { 24 //若是沒有這個指令會遞歸調用 25 el.processed = true; 26 let hooks = el.vm.hooks; 27 for (let hkey in hooks) { 28 if (el[hkey] && hooks[hkey].vnode2render) { 29 return hooks[hkey].vnode2render(el, genElement); 30 } 31 } 32 } 33 //不帶hook的狀況,這個就是普通的標籤 34 return nodir(el) 35 } 36 37 function nodir(el) { 38 let code 39 40 //轉換子節點 41 const children = genChildren(el, true); 42 code = `_h('${el.tag}'${ 43 ',{}' 44 }${ 45 children ? `,${children}` : '' // children 46 })` 47 return code 48 } 49 50 function genChildren(el, checkSkip) { 51 const children = el.children 52 if (children.length) { 53 const el = children[0] 54 // 若是是v-for 55 if (children.length === 1 && el.for) { 56 return genElement(el) 57 } 58 const normalizationType = 0 59 return `[${children.map(genNode).join(',')}]${ 60 checkSkip 61 ? normalizationType ? `,${normalizationType}` : '' 62 : '' 63 }` 64 } 65 } 66 67 68 //將element轉換爲render函數 69 function compileToFunctions(el) { 70 let vm = el.vm; 71 let render = genElement(el); 72 73 render = `with(this){ debugger; return ${render}}`; 74 75 return new Function(render); 76 77 } 78 79 function genNode(node) { 80 if (node.type === 1) { 81 return genElement(node) 82 } else { 83 return genText(node) 84 } 85 } 86 87 function genText(text) { 88 return text.type === 2 ? text.expression : JSON.stringify(text.text) 89 } 90 91 //咱們依舊定義個MVVM的類 92 class MVVM { 93 constructor(options) { 94 this.$data = options.data; 95 this.template = options.template; 96 97 //將data中的數據裝填到實例上,以便後續函數組裝使用 98 for(let name in this.$data) { 99 this[name] = this.$data[name]; 100 } 101 102 this.compile(); 103 104 } 105 106 //解析模板生成虛擬dom,這裏是重要的一步將模板變成方法 107 compile() { 108 109 let element = this.html2Elment(); 110 this.element = element; 111 112 this.initHooks(); 113 this.setElDrictive(element); 114 //由於設置屬性已經被咱們手動作了這裏便不須要處理了 115 116 let hooks = this.hooks; 117 //這裏,咱們須要將有的鉤子執行,主要是爲了處理指令 118 for(let hkey in hooks) { 119 //若是對象上面已經裝載了這個指令,而且具備模板到node的函數定義則執行 120 //這裏之因此須要模板上具備,由於對象數據須要在這裏取 121 if(element[hkey] && hooks[hkey].template2Vnode) { 122 //調用這個鉤子,事實上這個鉤子要往對象實例上面加東西 123 //這個會將循環相關的指令,好比要循環的對象放到for字段,將值放到alias,將迭代器屬性關鍵詞放到iterator 124 hooks[hkey].template2Vnode(element, element[hkey], this); 125 } 126 } 127 128 //上面作了指令系統第一步,將模板中的屬性存到element對應對象上,這裏開始調用之 129 this.$render = compileToFunctions(element) 130 131 //執行渲染 132 let vnode = this.$render(); 133 134 console.log(html, element, vnode) 135 debugger; 136 137 } 138 139 140 initHooks() { 141 //須要處理的指令鉤子,原本該放到prototype上 142 this.hooks = { 143 'for': { 144 template2Vnode: function (el, dir) { 145 //(val, key, index) in arr 146 let exp = dir.expression 147 148 //for in 或者 for of 這種循環 149 const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ 150 //取出迭代器關鍵詞 151 const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ 152 153 //獲取數組 154 //(key ,index) in arr 155 //[0] (key ,index) in arr,[1] (key ,index),[2] arr 156 const inMatch = exp.match(forAliasRE) 157 if (!inMatch) { 158 warn(`Invalid v-for expression: ${exp}`) 159 return 160 } 161 162 //上面的正則實際上是爲了取出迭代器中的字符串,後面好組裝函數 163 //這裏開始從新組裝對象上的for指令,這裏把循環的對象指向了數組關鍵詞 164 el.for = inMatch[2].trim() 165 //(val, key, index) 166 let alias = inMatch[1].trim() 167 168 //關鍵詞拿出來 169 const iteratorMatch = alias.match(forIteratorRE) 170 if (iteratorMatch) { 171 el.alias = iteratorMatch[1].trim(); 172 el.iterator1 = iteratorMatch[2].trim() 173 if (iteratorMatch[3]) { 174 el.iterator2 = iteratorMatch[3].trim() 175 } 176 } else { 177 el.alias = alias 178 } 179 180 }, 181 //將node對象轉換爲函數 182 //由於以前已經用上面的函數 183 //將循環相關的指令,好比要循環的對象放到for字段,將值放到alias,將迭代器屬性關鍵詞放到iterator 184 //因此這裏直接取出關鍵詞使用便可 185 vnode2render: function (el, genElement) { 186 //一個狀態機 187 if(el.forProcessed) return null; 188 189 //取出相關屬性 190 let exp = el.for; 191 let alias = el.alias; 192 193 //注意這個字符串裏面的代碼會執行,最新js語法 194 let iterator1 = el.iterator1 ? `,${el.iterator1}` : ''; 195 let iterator2 = el.iterator2 ? `,${el.iterator2}` : ''; 196 197 /* 198 輸出 199 _l((arr), function(val,key,index) { 200 console.log(arguments); 201 }) 202 */ 203 let _render = ` _l((${exp}), function(${alias}${iterator1}${iterator2}) { 204 console.log(arguments); 205 return ${genElement(el)} 206 }) 207 ` 208 console.log('render', _render); 209 210 return _render 211 212 } 213 } 214 }; 215 } 216 217 //渲染for時,返回多個render 218 //由於_l調用的時候是處在mvvm實例做用域,因此這裏傳入的時候是一個數組 219 _l(val, render) { 220 let ret, i, l, keys, key 221 if (Array.isArray(val) || typeof val === 'string') { 222 ret = new Array(val.length) 223 for (i = 0, l = val.length; i < l; i++) { 224 ret[i] = render(val[i], i) 225 } 226 } else if (typeof val === 'number') { 227 ret = new Array(val) 228 for (i = 0; i < val; i++) { 229 ret[i] = render(i + 1, i) 230 } 231 } else if (isObject(val)) { 232 keys = Object.keys(val) 233 ret = new Array(keys.length) 234 for (i = 0, l = keys.length; i < l; i++) { 235 key = keys[i] 236 ret[i] = render(val[key], key, i) 237 } 238 } 239 return ret 240 } 241 242 _s(val) { 243 return val == null 244 ? '' 245 : typeof val === 'object' 246 ? JSON.stringify(val, null, 2) 247 : String(val) 248 } 249 250 _h(sel, data, children) { 251 252 debugger; 253 254 return 255 } 256 257 //解析指令 258 setElDrictive(el) { 259 //解析指令,這裏主要是解析for與if 260 let attrs = el.attrs; 261 262 //判斷m-xxx這種類型的正則 263 const drictiveRE = /^m\-(\w+)(\:[^\.]+)?\.?([^\:]+)?/ 264 265 for(let name in attrs) { 266 let darr = name.match(drictiveRE); 267 if(darr){ 268 269 //沒有什麼其餘目的,就是將屬性中的指令挪到對象上 270 el[darr[1]] = { 271 name: darr[1], 272 expression: attrs[name], 273 arg: darr[2] && darr[2].slice(1) 274 } 275 276 } 277 } 278 279 } 280 281 //將模板轉換爲js對象,這裏要調用HTMLParser 282 html2Elment() { 283 //咱們這裏簡化代碼,直接返回解析後的結果便可 284 //...一大段調用htmlParser,包括遞歸調用生成js對象的過程,略 285 return { 286 vm: this, 287 tag: 'div', 288 attrs: { 289 'm-for': '(val, key, index) in arr' 290 }, 291 children: [ 292 { 293 type: 2, 294 text: '索引 {{key + 1}} :{{val}}', 295 expression: '"索引 "+_s(key + 1)+" :"+_s(val)' 296 } 297 ] 298 } 299 300 } 301 302 } 303 304 //而後咱們在這裏實例化便可 305 new MVVM({ 306 template: html, 307 data: { 308 arr: [ 309 '葉小釵', '素還真', '一頁書' 310 ] 311 } 312 }) 313 314 </script> 315 </body> 316 </html>
這一大坨代碼,是可運行的代碼,其中打了不少斷點寫了不少註釋,剔除了不少無用的代碼,想要了解指令系統的朋友能夠看看,這裏如何自定義指令,你們也能夠思考下是怎麼實現的,今天的學習暫時到這裏,咱們明天來看看組件一塊的實現