一套程式碼小程式&Web&Native執行的探索05——snabbdom
接上文: ofollow,noindex" target="_blank">一套程式碼小程式&Web&Native執行的探索04——資料更新
對應Git程式碼地址請見: https://github.com/yexiaochai/wxdemo/tree/master/mvvm
https://github.com/fastCreator/MVVM(極度參考,十分感謝該作者,直接看Vue會比較吃力的,但是看完這個作者的程式碼便會輕易很多,可惜這個作者沒有對應部落格說明,不然就爽了)
https://www.tangshuang.net/3756.html
https://www.cnblogs.com/kidney/p/8018226.html
http://www.cnblogs.com/kidney/p/6052935.html
https://github.com/livoras/blog/issues/13
根據最近的學習,離我們最終的目標還有一段距離,但是對於Vue實現原理卻慢慢有了體系化的認識,相信本系列結束後,如果能完成我們跨端程式碼,哪怕是demo的實現,都會對後續瞭解Vue或者React這裡原始碼提供深遠的幫助,平時工作較忙,這次剛好碰到假期,雖然會耽擱一些時間,我們試試這段時間運氣可好,能不能在這個階段取得不錯的進展,好了我們繼續完成今天的學習吧
到目前的地步,其中一些程式碼比較散亂,沒有辦法粘貼出來做講解了,我這邊儘量寫註釋,這裡文章記錄的主要目的還是幫助自己記錄思路
昨天,我們完成了最簡單的模板到DOM的實現,以及執行setData時候頁面重新渲染工作,只不過比較粗暴還沒有引入snabbdom進行了重新渲染,今天我們來完成其中的事件繫結部分程式碼
這裡我們先不去管迴圈標籤這些的解析,先完成事件繫結部分程式碼,這裡如果只是想實現click繫結便直接在此處繫結事件即可:
1 class Element { 2constructor(tagName, props, children, vm) { 3this.tagName = tagName; 4this.props = props; 5this.children = children || []; 6this.vm = vm.vm; 7} 8render() { 9//拿著根節點往下面擼 10let el = document.createElement(this.tagName); 11let props = this.props.props; 12let scope = this; 13 14let events = this.props.on; 15 16for(let name in props) { 17el.setAttribute(name, props[name]); 18} 19 20for(name in events) { 21let type = Object.keys(this.props.on); 22type = type[0]; 23el.addEventListener(type, function (e) { 24scope.vm.$options.methods[scope.props.on[type]] && scope.vm.$options.methods[scope.props.on[type]].call(scope.vm, e); 25}) 26} 27 28let children = this.children; 29 30for(let i = 0, l = children.length; i < l; i++) { 31let child = children[i]; 32let childEl; 33if(child instanceof Element) { 34//遞迴呼叫 35childEl = child.render(); 36} else { 37childEl = document.createTextNode(child); 38} 39el.append(childEl); 40} 41return el; 42} 43 }
顯然,這個不是我們要的最終程式碼,事實上,事件如何繫結dom如何比較差異渲染,我們這塊不需要太多關係,我們只需要引入snabbdom即可,這裡便來一起了解之
snabbdom
前面我們對snabbdom做了初步介紹,暫時看來MVVM框架就我這邊學習的感覺有以下幾個難點:
① 第一步的模板解析,這塊很容易出錯,但如果有志氣jQuery原始碼的功底就會比較輕易
② 虛擬DOM這塊,要對比兩次dom樹的差異再選擇如何做
只要突破這兩點,其他的就會相對簡單一些,而這兩塊最難也最容易出錯的工作,我們全部引用了第三方庫HTMLParser和snabbdom,所以我們都碰上了好時代啊......
我們很容易將一個dom結構用js物件來抽象,比如我們之前做的班次列表中的列表排序:
這裡出發的因子就有出發時間、耗時、價格,這裡表示下就是:
1 let trainData = { 2sortKet: 'time', //耗時,價格,發車時間等等方式排序 3sortType: 1, //1升序,2倒敘 4oData: [], //伺服器給過來的原生資料 5data: [], //當前篩選條件下的資料 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 4let leftNode = null; 5let curNodeIndex = index; 6 7for(let i = 0, l = oldNodeChildren.length; i < l; i++) { 8let child = oldNodeChildren[i]; 9let newChild = newNodeChildren[i]; 10 11//計算節點的標識 12curNodeIndex = (leftNode && leftNode.count) ? curNodeIndex + leftNode.count + 1 : curNodeIndex + 1; 13dfsWalk(child, newChild) 14leftNode = child; 15} 16 } 17 18 //對兩棵樹進行深度優先遍歷,找出差異 19 function dfsWalk(oldNode, newNode, index, patches) { 20//將兩棵樹的不同記錄之 21patches[index] = []; 22diffChildren(oldNode.children, newNode.children, index, patches); 23 } 24 25 //對比兩棵樹的差異 26 function diff(oldTree, newTree) { 27//當前節點標誌 28let index = 0; 29//記錄每個節點的差異 30let patches = {}; 31//深度優先遍歷 32return 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"; 6import { patch, h, VNode } from './libs/vnode.js' 7var container = document.getElementById("container"); 8function someFn(){ console.log(1)} 9function anotherEventHandler(){ console.log(2)} 10 11var oldVnode = h("div", {on: {click: someFn}}, [ 12h("span", {style: {fontWeight: "bold"}}, "This is bold"), 13" and this is just normal text", 14h("a", {props: {href: "/foo"}}, "I\"ll take you places!") 15]); 16 17// 第一次打補丁,用於渲染到頁面,內部會建立關聯關係,減少了建立oldvnode過程 18let diff = patch(container, oldVnode); 19//建立新節點 20var newVnode = h("div", {on: {click: anotherEventHandler}}, [ 21h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"), 22" and this is still just normal text", 23h("a", {props: {href: "/bar"}}, "I\"ll take you places!") 24]); 25//第二次比較,上一次vnode比較,打補丁到頁面 26//VUE的patch在nextTick中,開啟非同步佇列,刪除了不必要的patch 27//nextTick非同步佇列解析,下面文章中會詳解 28patch(oldVnode, newVnode); // Snabbdom efficiently updates the old view to the new state 29function test() { 30return { 31oldVnode,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 11let html = ` 12<ul> 13<li m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</li> 14</ul> 15` 16//這裡我們為了降低學習成本將這段模板再做一次簡化,變成這樣 17html = '<div m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</div>'; 18 19 20//處理element元素生成render函式 21function genElement(el) { 22//這裡如果有自定義指令也會被拿出來 23if (!el.processed) { 24//如果沒有這個指令會遞迴呼叫 25el.processed = true; 26let hooks = el.vm.hooks; 27for (let hkey in hooks) { 28if (el[hkey] && hooks[hkey].vnode2render) { 29return hooks[hkey].vnode2render(el, genElement); 30} 31} 32} 33//不帶hook的情況,這個就是普通的標籤 34return nodir(el) 35} 36 37function nodir(el) { 38let code 39 40//轉換子節點 41const children = genChildren(el, true); 42code = `_h('${el.tag}'${ 43',{}' 44}${ 45children ? `,${children}` : '' // children 46})` 47return code 48} 49 50function genChildren(el, checkSkip) { 51const children = el.children 52if (children.length) { 53const el = children[0] 54// 如果是v-for 55if (children.length === 1 && el.for) { 56return genElement(el) 57} 58const normalizationType = 0 59return `[${children.map(genNode).join(',')}]${ 60checkSkip 61? normalizationType ? `,${normalizationType}` : '' 62: '' 63}` 64} 65} 66 67 68//將element轉換為render函式 69function compileToFunctions(el) { 70let vm = el.vm; 71let render = genElement(el); 72 73render = `with(this){ debugger; return ${render}}`; 74 75return new Function(render); 76 77} 78 79function genNode(node) { 80if (node.type === 1) { 81return genElement(node) 82} else { 83return genText(node) 84} 85} 86 87function genText(text) { 88return text.type === 2 ? text.expression : JSON.stringify(text.text) 89} 90 91//我們依舊定義個MVVM的類 92class MVVM { 93constructor(options) { 94this.$data = options.data; 95this.template = options.template; 96 97//將data中的資料裝填到例項上,以便後續函式組裝使用 98for(let name in this.$data) { 99this[name] = this.$data[name]; 100} 101 102this.compile(); 103 104} 105 106//解析模板生成虛擬dom,這裡是重要的一步將模板變成方法 107compile() { 108 109let element = this.html2Elment(); 110this.element = element; 111 112this.initHooks(); 113this.setElDrictive(element); 114//因為設定屬性已經被我們手動做了這裡便不需要處理了 115 116let hooks = this.hooks; 117//這裡,我們需要將有的鉤子執行,主要是為了處理指令 118for(let hkey in hooks) { 119//如果物件上面已經裝載了這個指令,並且具有模板到node的函式定義則執行 120//這裡之所以需要模板上具有,因為物件資料需要在這裡取 121if(element[hkey] && hooks[hkey].template2Vnode) { 122//呼叫這個鉤子,事實上這個鉤子要往物件例項上面加東西 123//這個會將迴圈相關的指令,比如要迴圈的物件放到for欄位,將值放到alias,將迭代器屬性關鍵詞放到iterator 124hooks[hkey].template2Vnode(element, element[hkey], this); 125} 126} 127 128//上面做了指令系統第一步,將模板中的屬性存到element對應物件上,這裡開始呼叫之 129this.$render = compileToFunctions(element) 130 131//執行渲染 132let vnode = this.$render(); 133 134console.log(html, element, vnode) 135debugger; 136 137} 138 139 140initHooks() { 141//需要處理的指令鉤子,本來該放到prototype上 142this.hooks = { 143'for': { 144template2Vnode: function (el, dir) { 145//(val, key, index) in arr 146let exp = dir.expression 147 148//for in 或者 for of 這種迴圈 149const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ 150//取出迭代器關鍵詞 151const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ 152 153//獲取陣列 154//(key ,index) in arr 155//[0] (key ,index) in arr,[1] (key ,index),[2] arr 156const inMatch = exp.match(forAliasRE) 157if (!inMatch) { 158warn(`Invalid v-for expression: ${exp}`) 159return 160} 161 162//上面的正則其實是為了取出迭代器中的字串,後面好組裝函式 163//這裡開始重新組裝物件上的for指令,這裡把迴圈的物件指向了陣列關鍵詞 164el.for = inMatch[2].trim() 165//(val, key, index) 166let alias = inMatch[1].trim() 167 168//關鍵詞拿出來 169const iteratorMatch = alias.match(forIteratorRE) 170if (iteratorMatch) { 171el.alias = iteratorMatch[1].trim(); 172el.iterator1 = iteratorMatch[2].trim() 173if (iteratorMatch[3]) { 174el.iterator2 = iteratorMatch[3].trim() 175} 176} else { 177el.alias = alias 178} 179 180}, 181//將node物件轉換為函式 182//因為之前已經用上面的函式 183//將迴圈相關的指令,比如要迴圈的物件放到for欄位,將值放到alias,將迭代器屬性關鍵詞放到iterator 184//所以這裡直接取出關鍵詞使用即可 185vnode2render: function (el, genElement) { 186//一個狀態機 187if(el.forProcessed) return null; 188 189//取出相關屬性 190let exp = el.for; 191let alias = el.alias; 192 193//注意這個字串裡面的程式碼會執行,最新js語法 194let iterator1 = el.iterator1 ? `,${el.iterator1}` : ''; 195let iterator2 = el.iterator2 ? `,${el.iterator2}` : ''; 196 197/* 198輸出 199_l((arr), function(val,key,index) { 200console.log(arguments); 201}) 202*/ 203let _render = ` _l((${exp}), function(${alias}${iterator1}${iterator2}) { 204console.log(arguments); 205return ${genElement(el)} 206}) 207` 208console.log('render', _render); 209 210return _render 211 212} 213} 214}; 215} 216 217//渲染for時,返回多個render 218//因為_l呼叫的時候是處在mvvm例項作用域,所以這裡傳入的時候是一個數組 219_l(val, render) { 220let ret, i, l, keys, key 221if (Array.isArray(val) || typeof val === 'string') { 222ret = new Array(val.length) 223for (i = 0, l = val.length; i < l; i++) { 224ret[i] = render(val[i], i) 225} 226} else if (typeof val === 'number') { 227ret = new Array(val) 228for (i = 0; i < val; i++) { 229ret[i] = render(i + 1, i) 230} 231} else if (isObject(val)) { 232keys = Object.keys(val) 233ret = new Array(keys.length) 234for (i = 0, l = keys.length; i < l; i++) { 235key = keys[i] 236ret[i] = render(val[key], key, i) 237} 238} 239return ret 240} 241 242_s(val) { 243return val == null 244? '' 245: typeof val === 'object' 246? JSON.stringify(val, null, 2) 247: String(val) 248} 249 250_h(sel, data, children) { 251 252debugger; 253 254return 255} 256 257//解析指令 258setElDrictive(el) { 259//解析指令,這裡主要是解析for與if 260let attrs = el.attrs; 261 262//判斷m-xxx這種型別的正則 263const drictiveRE = /^m\-(\w+)(\:[^\.]+)?\.?([^\:]+)?/ 264 265for(let name in attrs) { 266let darr = name.match(drictiveRE); 267if(darr){ 268 269//沒有什麼其他目的,就是將屬性中的指令挪到物件上 270el[darr[1]] = { 271name: darr[1], 272expression: attrs[name], 273arg: darr[2] && darr[2].slice(1) 274} 275 276} 277} 278 279} 280 281//將模板轉換為js物件,這裡要呼叫HTMLParser 282html2Elment() { 283//我們這裡簡化程式碼,直接返回解析後的結果即可 284//...一大段呼叫htmlParser,包括遞迴呼叫生成js物件的過程,略 285return { 286vm: this, 287tag: 'div', 288attrs: { 289'm-for': '(val, key, index) in arr' 290}, 291children: [ 292{ 293type: 2, 294text: '索引 {{key + 1}} :{{val}}', 295expression: '"索引 "+_s(key + 1)+" :"+_s(val)' 296} 297] 298} 299 300} 301 302} 303 304//然後我們在這裡例項化即可 305new MVVM({ 306template: html, 307data: { 308arr: [ 309'葉小釵', '素還真', '一頁書' 310] 311} 312}) 313 314 </script> 315 </body> 316 </html>
這一大坨程式碼,是可執行的程式碼,其中打了很多斷點寫了很多註釋,剔除了很多無用的程式碼,想要了解指令系統的朋友可以看看,這裡如何自定義指令,大家也可以思考下是怎麼實現的,今天的學習暫時到這裡,我們明天來看看元件一塊的實現