一套代碼小程序&Web&Native運行的探索05——snabbdom

接上文:一套代碼小程序&Web&Native運行的探索04——數據更新javascript

對應Git代碼地址請見:https://github.com/yexiaochai/wxdemo/tree/master/mvvmhtml

參考:
java

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>

這一大坨代碼,是可運行的代碼,其中打了不少斷點寫了不少註釋,剔除了不少無用的代碼,想要了解指令系統的朋友能夠看看,這裏如何自定義指令,你們也能夠思考下是怎麼實現的,今天的學習暫時到這裏,咱們明天來看看組件一塊的實現

相關文章
相關標籤/搜索