前言

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

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

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

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

【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码-LMLPHP

在使用以上任何一套框架体系前,我们都需要对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):

【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码-LMLPHP

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

① 实例化

② Parser => HTMLParser

③ codegen

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

【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码-LMLPHP【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码-LMLPHP
  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模板,这里举个例子:

【一套代码小程序&amp;Native&amp;Web阶段总结篇】可以这样阅读Vue源码-LMLPHP【一套代码小程序&amp;Native&amp;Web阶段总结篇】可以这样阅读Vue源码-LMLPHP
  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(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/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对象来抽象,比如我们这里的班次列表排序:

【一套代码小程序&amp;Native&amp;Web阶段总结篇】可以这样阅读Vue源码-LMLPHP

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

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

10-15 16:01