zoukankan      html  css  js  c++  java
  • 【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码

    前言

    前面我们对微信小程序进行了研究:【微信小程序项目实践总结】30分钟从陌生到熟悉

    在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一个规则,让我们可以先写H5代码,然后将小程序以及APP的业务差异代码做掉,岂不快哉?但小程序的web框架并不开源,不然也用不着我们在此费力了,经过研究,小程序web端框架是一套自研的MVVM框架,于是我们马上想到了借助第三方框架:

    一套代码小程序&Web&Native运行的探索01

    经过简单的研究,我们发现无论React或者Vue都是可以一定程度适应小程序开发模式的,市面上也有了对应的框架:mpvue&wepy

    在使用以上任何一套框架体系前,我们都需要对MVVM框架有最基础的了解,不然后面随便遇到点什么问题可能都会变得难以继续,负责人要对团队负责而不是单纯只是想技术尝鲜,所以生产项目一定要使用自己能完成hold住的技术

    这段时间我们尝试着去阅读Vue的源码,但现在的Vue是一个工程化产物,我们要学习的核心代码可能不到其中的1/3,多出来的是各种特性以及细节处理,在不清楚代码意图(目标)的情况下,事实上很难搞懂这段代码是要干什么,很多代码的出现都是一些精妙的小优化,知道的就会觉得十分惊艳,不知道的就会一头雾水,一来就看Vue的源码反而不利于深入了解

    出于这个原因在网上看了很多源码介绍的文章,出来就放一个Vue官方的流程图,然后一套组合模块套路,基本就把我给打晕了,查询了很多资料,还是发现一些写的比较清晰的(我感觉适合多数人的)文章:

    读懂源码:一步一步实现一个 Vue

    https://github.com/fastCreator/MVVM(特别推荐,非常不错)

    这两篇文章都有一个主旨:

    在没有相关框架经验的情况下,单单靠单步调试以及网上的源码介绍,想要读懂Vue源码是不太靠谱的做法,比较好的做法是自己照着Vue的源码写一套简单的,最基础的MVVM框架,在完成这个框架后再去阅读Vue或者React的代码要轻易的多,我这边是非常认可这个说法的,所以我们照着fastCreateor的代码(他应该是参考的Vue)也撸了一个:https://github.com/yexiaochai/wxdemo/tree/master/mvvm

    这里与其说撸了一个MVVM框架,不如说给fastCreateor的代码加上了自我理解的注释,通过这个过程也对MVVM框架有了第一步的认识,之前的文章或者代码都有些散,这里将前面学习的内容再做一次汇总,多加一些图示,帮助自己也帮助读者更好的了解,于是让我们开始吧!

    PS:下面说的MVVM框架,基本就是Vue框架,并且是自己的理解,有问题请大家拍砖

    MVVM框架的流程

    我们梳理了MVVM框架的基本流程,这里只看首次渲染的话:

    ① 解析html模板形成mvvm实例对象element(实例上的$node属性)
    ② 处理element属性,这里包括属性处理、事件处理、指令处理
    ③ 使用处理过的element对象,为每个实例创建render方法
    PS:new MVVM只会产生一个实例,每个html标签都会形成一个vnode,组件会形成独立的实例,与根实例以$parent与$children维护关系
    ④ 使用render方法创建虚拟dom vnode,vm实例element已经具备所有创建虚拟dom的必要条件,render只是利用他们,如果代码组织得好,不使用render也行
    ⑤ render执行后会生成虚拟dom vnode,借助另一个神器snabbdom开始对比新旧虚拟dom的结构,完成最终渲染
    PS:render执行时作用域在mvvm实例(vm)下

    所以整个代码核心全部是围绕着HTML=>element($node中间项,桥梁)=>render函数(执行返回vnode)=>引用snabbdom patch渲染

    而抓住几个点后,对应的几个核心技术点也就出来了:

    ① 模板解析这里对应着 HTMLParser,帮忙解决了很多问题
    ② 形成vnode需要的render函数,并且调用后维护彼此关系,这个是框架做的最多的工作
    ③ 生成真正的vnode,然后执行对比差异渲染patch操作,这块重要的工作由snabbdom接手了

    我们再把这里的目标映射成过程,就得到了这张图了(来自https://github.com/fastCreator/MVVM):

    上面一行就是首次渲染执行的流程,下面几个图就是实现数据变化时候更新试图的操作,分解到程序层面,核心就是:

    ① 实例化

    ② Parser => HTMLParser

    ③ codegen

    在此基础上再包装出数据响应模型以及组件系统、指令系统,每个模块都很独立,但又互相关联,抓住这个主干看各个分支这样就会相对比较清晰。所以网上很多几百行代码实现MVVM框架核心的就是只做最核心这一块,比如这个学习材料:https://github.com/DMQ/mvvm,非常简单清晰,为了帮助更好的理解,我们这里也写了一段比较独立的代码,包括了核心流程:

      1 <div id="app">
      2   <input type="text" v-model="name">
      3   {{name}}
      4 </div>
      5 
      6 <script type="text/javascript" >
      7 
      8   function getElById(id) {
      9     return document.getElementById(id);
     10   }
     11 
     12   //主体对象,存储所有的订阅者
     13   function Dep () {
     14     this.subs = [];
     15   }
     16 
     17   //通知所有订阅者数据变化
     18   Dep.prototype.notify = function () {
     19     for(let i = 0, l = this.subs.length; i < l; i++) {
     20       this.subs[i].update();
     21     }
     22   }
     23 
     24   //添加订阅者
     25   Dep.prototype.addSub = function (sub) {
     26     this.subs.push(sub);
     27   }
     28 
     29   let globalDataDep = new Dep();
     30 
     31   //观察者,框架会接触data的每一个与node相关的属性,
     32   //如果data没有与任何节点产生关联,则不予理睬
     33   //实际的订阅者对象
     34   //注意,只要一个数据对象对应了一个node对象就会生成一个订阅者,所以真实通知的时候应该需要做到通知到对应数据的dom,这里不予关注
     35   function Watcher(vm, node, name) {
     36     this.name = name;
     37     this.node = node;
     38     this.vm = vm;
     39     if(node.nodeType === 1) {
     40       this.node.value = this.vm.data[name];
     41     } else if(node.nodeType === 3) {
     42       this.node.nodeValue = this.vm.data[name] || '';
     43     }
     44     globalDataDep.addSub(this);
     45 
     46   }
     47 
     48   Watcher.prototype.update = function () {
     49     if(this.node.nodeType === 1) {
     50       this.node.value = this.vm.data[this.name ];
     51     } else if(this.node.nodeType === 3) {
     52       this.node.nodeValue = this.vm.data[this.name ] || '';
     53     }
     54   }
     55 
     56   //这块代码仅做功能说明,不用当真
     57   function compile(node, vm) {
     58     let reg = /{{(.*)}}/;
     59 
     60     //节点类型
     61     if(node.nodeType === 1) {
     62       let attrs = node.attributes;
     63       //解析属性
     64       for(let i = 0, l = attrs.length; i < l; i++) {
     65         if(attrs[i].nodeName === 'v-model') {
     66           let name = attrs[i].nodeValue;
     67           if(node.value === vm.data[name]) break;
     68 
     69 //          node.value = vm.data[name] || '';
     70           new Watcher(vm, node, name)
     71 
     72           //此处不做太多判断,直接绑定事件
     73           node.addEventListener('input', function (e) {
     74             //赋值操作
     75             let newObj = {};
     76             newObj[name] = e.target.value;
     77             vm.setData(newObj, true);
     78           });
     79 
     80           break;
     81         }
     82       }
     83     } else if(node.nodeType === 3) {
     84 
     85       if(reg.test(node.nodeValue)) {
     86         let name = RegExp.$1; // 获取匹配到的name
     87         name = name.trim();
     88 //          node.nodeValue = vm.data[name] || '';
     89         new Watcher(vm, node, name)
     90       }
     91     }
     92   }
     93 
     94   //获取节点
     95   function nodeToFragment(node, vm) {
     96     let flag = document.createDocumentFragment();
     97     let child;
     98 
     99     while (child = node.firstChild) {
    100       compile(child, vm);
    101       flag.appendChild(child);
    102     }
    103 
    104     return flag;
    105   }
    106 
    107   function MVVM(options) {
    108     this.data = options.data;
    109     let el = getElById(options.el);
    110     this.$dom = nodeToFragment(el, this)
    111     this.$el = el.appendChild(this.$dom);
    112 
    113 //    this.$bindEvent();
    114   }
    115 
    116   MVVM.prototype.setData = function (data, noNotify) {
    117     for(let k in data) {
    118       this.data[k] = data[k];
    119     }
    120     //执行更新逻辑
    121 //    if(noNotify) return;
    122     globalDataDep.notify();
    123   }
    124 
    125   let mvvm = new MVVM({
    126     el: 'app',
    127     data: {
    128       name: '叶小钗'
    129     }
    130   })
    131 
    132   setTimeout(function() {
    133     mvvm.setData({name: '刀狂剑痴叶小钗'})
    134   }, 3000)
    135 
    136 </script>
    最简单的mvvm例子

    大家对照着这个例子自己撸一下,其中有几个在业务中不太常用的知识点,第一个就是访问器属性,这里大概写个例子介绍下:

    var obj = { };
    // 为obj定义一个名为 name 的访问器属性
    Object.defineProperty(obj, "name", {
    
      get: function () {
        console.log('get', arguments);
      },
      set: function (val) {
        console.log('set', arguments);
      }
    })
    obj.name = '叶小钗'
    console.log(obj, obj.name)
    /*
     set Arguments ["叶小钗", callee: ƒ, Symbol(Symbol.iterator): ƒ]
     get Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
    */

    接下来我们对MVVM框架会用到的两大神器依次做下介绍

    神器HTMLPaser

    HTMLParser这个库的代码在这里可以拿到:https://github.com/yexiaochai/wxdemo/blob/master/mvvm/libs/html-parser.js

    这个库完成的功能比较简单,就是解析你传入的html模板,这里举个例子:

      1 <!doctype html>
      2 <html>
      3 <head>
      4   <title>起步</title>
      5 </head>
      6 <body>
      7 
      8 <div id="app">
      9 
     10 </div>
     11 <script  >
     12 
     13   // Regular Expressions for parsing tags and attributes
     14   let startTag = /^<([-A-Za-z0-9_]+)((?:s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:s*=s*(?:(?:"[^"]*")|(?:'[^']*')|[^>s]+))?)*)s*(/?)>/,
     15           endTag = /^</([-A-Za-z0-9_]+)[^>]*>/,
     16           attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:s*=s*(?:(?:"((?:\.|[^"])*)")|(?:'((?:\.|[^'])*)')|([^>s]+)))?/g
     17 
     18   // Empty Elements - HTML 5
     19   let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr")
     20 
     21   // Block Elements - HTML 5
     22   let block = makeMap("a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video")
     23 
     24   // Inline Elements - HTML 5
     25   let inline = makeMap("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var")
     26 
     27   // Elements that you can, intentionally, leave open
     28   // (and which close themselves)
     29   let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr")
     30 
     31   // Attributes that have their values filled in disabled="disabled"
     32   let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected")
     33 
     34   // Special Elements (can contain anything)
     35   let special = makeMap("script,style")
     36 
     37   function makeMap(str) {
     38     var obj = {}, items = str.split(",");
     39     for (var i = 0; i < items.length; i++)
     40       obj[items[i]] = true;
     41     return obj;
     42   }
     43 
     44   function HTMLParser(html, handler) {
     45     var index, chars, match, stack = [], last = html;
     46     stack.last = function () {
     47       return this[this.length - 1];
     48     };
     49 
     50     while (html) {
     51       chars = true;
     52 
     53       // Make sure we're not in a script or style element
     54       if (!stack.last() || !special[stack.last()]) {
     55 
     56         // Comment
     57         if (html.indexOf("<!--") == 0) {
     58           index = html.indexOf("-->");
     59 
     60           if (index >= 0) {
     61             if (handler.comment)
     62               handler.comment(html.substring(4, index));
     63             html = html.substring(index + 3);
     64             chars = false;
     65           }
     66 
     67           // end tag
     68         } else if (html.indexOf("</") == 0) {
     69           match = html.match(endTag);
     70 
     71           if (match) {
     72             html = html.substring(match[0].length);
     73             match[0].replace(endTag, parseEndTag);
     74             chars = false;
     75           }
     76 
     77           // start tag
     78         } else if (html.indexOf("<") == 0) {
     79           match = html.match(startTag);
     80 
     81           if (match) {
     82             html = html.substring(match[0].length);
     83             match[0].replace(startTag, parseStartTag);
     84             chars = false;
     85           }
     86         }
     87 
     88         if (chars) {
     89           index = html.indexOf("<");
     90 
     91           var text = index < 0 ? html : html.substring(0, index);
     92           html = index < 0 ? "" : html.substring(index);
     93 
     94           if (handler.chars)
     95             handler.chars(text);
     96         }
     97 
     98       } else {
     99         html = html.replace(new RegExp("([\s\S]*?)</" + stack.last() + "[^>]*>"), function (all, text) {
    100           text = text.replace(/<!--([sS]*?)-->|<![CDATA[([sS]*?)]]>/g, "$1$2");
    101           if (handler.chars)
    102             handler.chars(text);
    103 
    104           return "";
    105         });
    106 
    107         parseEndTag("", stack.last());
    108       }
    109 
    110       if (html == last)
    111         throw "Parse Error: " + html;
    112       last = html;
    113     }
    114 
    115     // Clean up any remaining tags
    116     parseEndTag();
    117 
    118     function parseStartTag(tag, tagName, rest, unary) {
    119       tagName = tagName.toLowerCase();
    120 
    121       if (block[tagName]) {
    122         while (stack.last() && inline[stack.last()]) {
    123           parseEndTag("", stack.last());
    124         }
    125       }
    126 
    127       if (closeSelf[tagName] && stack.last() == tagName) {
    128         parseEndTag("", tagName);
    129       }
    130 
    131       unary = empty[tagName] || !!unary;
    132 
    133       if (!unary)
    134         stack.push(tagName);
    135 
    136       if (handler.start) {
    137         var attrs = [];
    138 
    139         rest.replace(attr, function (match, name) {
    140           var value = arguments[2] ? arguments[2] :
    141                   arguments[3] ? arguments[3] :
    142                           arguments[4] ? arguments[4] :
    143                                   fillAttrs[name] ? name : "";
    144 
    145           attrs.push({
    146             name: name,
    147             value: value,
    148             escaped: value.replace(/(^|[^\])"/g, '$1\"') //"
    149           });
    150         });
    151 
    152         if (handler.start)
    153           handler.start(tagName, attrs, unary);
    154       }
    155     }
    156 
    157     function parseEndTag(tag, tagName) {
    158       // If no tag name is provided, clean shop
    159       if (!tagName)
    160         var pos = 0;
    161 
    162       // Find the closest opened tag of the same type
    163       else
    164         for (var pos = stack.length - 1; pos >= 0; pos--)
    165           if (stack[pos] == tagName)
    166             break;
    167 
    168       if (pos >= 0) {
    169         // Close all the open elements, up the stack
    170         for (var i = stack.length - 1; i >= pos; i--)
    171           if (handler.end)
    172             handler.end(stack[i]);
    173 
    174         // Remove the open elements from the stack
    175         stack.length = pos;
    176       }
    177     }
    178   };
    179 
    180     html = `
    181 <div id="s_wrap" class="s-isindex-wrap">
    182   <div id="s_main" class="main clearfix">
    183     <div id="s_mancard_main" class="s-mancacrd-main">
    184       <div class="s-menu-container">
    185         <div id="s_menu_gurd" class="s-menu-gurd">
    186           <div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8">
    187             <span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100">
    188               <div class="mine-icon"></div>
    189               <div class="mine-text">我的关注</div>
    190             </span>
    191             <div class="s-menus-outer">
    192               <div id="s_menus_wrapper" class="menus-wrapper"></div>
    193               <div class="s-bg-space s-opacity-white-background"></div>
    194               <span class="s-menu-music" data-id="3"></span>
    195             </div>
    196             <span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="设置">
    197               <div class="menu-icon"></div>
    198             </span>
    199           </div>
    200         </div>
    201       </div>
    202     </div>
    203   </div>
    204 </div>
    205 `
    206 
    207 HTMLParser(html,{
    208   start: function(tag, attrs, unary) {
    209     console.log('标签头', tag, attrs)
    210   },
    211   end: function (tag) {
    212     console.log('标签尾', tag)
    213   },
    214   //处理真实的节点
    215   chars: function(text) {
    216     console.log('标签字段', text.trim().length > 0 ? text : '空字符' )
    217   }
    218 })
    219 
    220 </script>
    221 
    222 
    223 </body>
    224 </html>
    HTMLParser的简单例子
     1 html = `
     2 <div id="s_wrap" class="s-isindex-wrap">
     3   <div id="s_main" class="main clearfix">
     4     <div id="s_mancard_main" class="s-mancacrd-main">
     5       <div class="s-menu-container">
     6         <div id="s_menu_gurd" class="s-menu-gurd">
     7           <div id="s_ctner_menus" class="s-ctner-menus s-opacity-blank8">
     8             <span id="s_menu_mine" class="s-menu-item s-menu-mine s-opacity-white-background current" data-id="100">
     9               <div class="mine-icon"></div>
    10               <div class="mine-text">我的关注</div>
    11             </span>
    12             <div class="s-menus-outer">
    13               <div id="s_menus_wrapper" class="menus-wrapper"></div>
    14               <div class="s-bg-space s-opacity-white-background"></div>
    15               <span class="s-menu-music" data-id="3"></span>
    16             </div>
    17             <span id="s_menu_set" class="s-menu-setting s-opacity-white-background" data-id="99" title="设置">
    18               <div class="menu-icon"></div>
    19             </span>
    20           </div>
    21         </div>
    22       </div>
    23     </div>
    24   </div>
    25 </div>
    26 `
    27 
    28 HTMLParser(html,{
    29   start: function(tag, attrs, unary) {
    30     console.log('标签头', tag, attrs)
    31   },
    32   end: function (tag) {
    33     console.log('标签尾', tag)
    34   },
    35   //处理真实的节点
    36   chars: function(text) {
    37     console.log('标签字段', text.trim().length > 0 ? text : '空字符' )
    38   }
    39 })

    这里使用HTMLParser,很容易就可以把html模板解析为element树

    神器Snabbdom

    我们很容易就可以将一根dom结构用js对象来抽象,比如我们这里的班次列表排序:

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

    let trainData = {
      sortKet: 'time', //耗时,价格,发车时间等等方式排序
      sortType: 1, //1升序,2倒叙
      oData: [], //服务器给过来的原生数据
      data: [], //当前筛选条件下的数据
    }

    这个对象有个缺陷就是不能与页面映射起来,我们需要在代码中维护数据与试图的映射关系(data与dom的关系),一旦数据发生变化便重新渲染。比较复杂的问题是半年后这个页面的维护者三易其手,而筛选条件增加、业务逻辑变化,这个页面的代码可能会变得相当难维护,其中最难的点可能就是页面中的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"]},
      ]
    }

    这个映射成dom结构就是:

    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>

    真实的VNode会翻译为这样:

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

    这里很快就能封装一个可运行的代码出来:

    //***虚拟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);

    snabbdom做的事情,便是把这段代码写的更加完善一点,并且处理里面最为复杂的比较两颗虚拟树的差异了,而这块也是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

    继续来一段例子做说明:

    <div id="container">
    </div>
    
    <script type="module">
      "use strict";
      import { patch, h, VNode } from './libs/vnode.js'
      var container = document.getElementById("container");
      function someFn(){ console.log(1)}
      function anotherEventHandler(){ console.log(2)}
    
      var oldVnode = h("div", {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过程
      let diff = patch(container, oldVnode);
      //创建新节点
      var newVnode = h("div", {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(oldVnode, newVnode); // Snabbdom efficiently updates the old view to the new state
      function test() {
        return {
          oldVnode,newVnode,container,diff
        }
      }
    </script>

    snabbdom在组件系统中的应用

    MVVM系统还有个比较关键的是组件系统,一般认为MVVM的两大特点其实是响应式数据更新(VNode相关),然后就是组件体系,这两者需要完成的工作都是让我们更高效的开发代码,一个为了解决纷乱的dom操作,一个为了解决负责的业务逻辑结构,而组件体系便会用到snabbdom中的hook:

    //创建组件
    //子组件option,属性,子元素,tag
    _createComponent(Ctor, data, children, sel) {
     Ctor.data = mergeOptions(Ctor.data);
     let componentVm;
     let Factory = this.constructor
     let parentData = this.$data
     data.hook.insert = (vnode) => {
        //...
     }
     Ctor._vnode = new VNode(sel,null,data, [], undefined, createElement(sel));
     return Ctor._vnode
    }

    使用一般流程,我们不会解析这个组件而是插入没有意义的标签:

    <my-component></my-component>
    <div m-for="(val, key, index) in arr">索引 1 :叶小钗</div>
    <div m-for="(val, key, index) in arr">索引 2 :素还真</div>
    <div m-for="(val, key, index) in arr">索引 3 :一页书</div>
     1 _createComponent(Ctor, data, children, sel) {
     2  Ctor.data = mergeOptions(Ctor.data);
     3  let componentVm;
     4  let Factory = this.constructor
     5  let parentData = this.$data
     6  data.hook.insert = (vnode) => {
     7    Ctor.data = Ctor.data || {};
     8    var el =createElement('sel')
     9    vnode.elm.append(el)
    10    Ctor.el = el;
    11    componentVm = new Factory(Ctor);
    12    vnode.key = componentVm.uid;
    13    componentVm._isComponent = true
    14    componentVm.$parent = this;
    15    (this.$children || (this.$children = [])).push(componentVm);
    16    //写在调用父组件值
    17    for (let key in data.attrs) {
    18      if (Ctor.data[key]) {
    19        warn(`data:${key},已存在`);
    20        continue;
    21      }
    22    }
    23  }
    24  Ctor._vnode = new VNode(sel,null,data, [], undefined, createElement(sel));
    25  return Ctor._vnode
    26 }

    但是我们为snabbdom设置了一个hook(钩子),当标签被插入的时候会执行这段逻辑(加粗部分代码),这里先创建了一个空标签(sel)直接插入my-component中,然后执行与之前一样的实例化流程:

    componentVm = new Factory(Ctor);

    这个会在patch后将实际的dom节点更新上去:

    this.$el = patch(this.$el, vnode); //$el现在为sel标签(dom标签)

    这个就是snabbdom hook所干的工作,同时可以看到组件系统这里有这些特点:

    ① 组件是一个独立的mvvm实例,通过parent可以找到其父亲mvvm实例,可能跟实例,也可能是另一个组件

    ② 根实例可以根据$children参数找到其下面所有的组件

    ③ 组件与跟实例通过data做交流,原则不允许在组件内部改变属性值,需要使用事件进行通信,事件通信就是在组件中的点击事件不做具体的工作,而是释放$emit(),这种东西让跟实例调用,最终还是以setData的方式改变基本数据,从而引发组件同步更新

    可以看到,只要利用好了HTMLParser以及snabbdom两大神器,我们的框架代码变化简单许多,而了解了这两大神器的使用后,再去读Vue的源码可能也会简单流畅一些

    结语

    前段时间,我们因为想要统一小程序&web&Native端的代码做了一些研究,并且模仿着实现了一个简单缺漏的mvvm框架,这样的过程中,我们抓住了mvvm框架的基本脉络,接下来我们看看mpvue是怎么做的,然后再继续我们后续的研究

    对应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

  • 相关阅读:
    java代码,继承。。。主要是传值,赋值。
    java代码继承。。。找出不能继承父类方法的问题
    java代码继承super
    HDU 6114 Chess
    #113. 【UER #2】手机的生产
    uoj 118 赴京赶考
    戏game
    序sort
    迷enc
    Jupyter 同时支持python2、python3 kernel
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/9755844.html
Copyright © 2011-2022 走看看