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

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

    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>
      ';
     for(let key in arr) { 
    __p+='
        <li>...</li>
      ';
     } 
    __p+='
    </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>

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

  • 相关阅读:
    熟悉常用的HBase操作
    爬虫大作业
    熟悉常用的HDFS操作
    数据结构化与保存
    获取全部校园新闻
    爬取校园新闻首页的新闻的详情,使用正则表达式,函数抽离+网络爬虫基础练习
    中文词频统计
    英语词频统计
    AXIOS中文文档
    overload方法重载
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/9742383.html
Copyright © 2020-2023  润新知