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

接上文:一套代码小程序&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 {
 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对象来抽象,比如我们之前做的班次列表中的列表排序:

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

这里出发的因子就有出发时间、耗时、价格,这里表示下就是:

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

实际代码中,会对两棵树进行深度优先遍历,这样会给每个节点一个唯一的标志:

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

在深度优先遍历的时候,每到一个节点便与新的树进行对比,如果有差异就记录到一个对象中:

 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)])
  })])
}

所有的这一切都是为了形成虚拟树结构,最终要的是这样的东西

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

所以指令是其中的工具,一个过程,帮助我们达到目的,为了帮助理解,我们这边单独抽一段代码出来说明这个问题,这里再强调一下指令系统在整体流程中的意义是:

我们最终目标是将模板转换为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>

这一大坨代码,是可运行的代码,其中打了很多断点写了很多注释,剔除了很多无用的代码,想要了解指令系统的朋友可以看看,这里如何自定义指令,大家也可以思考下是怎么实现的,今天的学习暂时到这里,我们明天来看看组件一块的实现

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

上一篇:基于Flask 实现Web微信登陆


下一篇:小程序 图片固定宽度 高度自动方法~