【一套代码小程序&Native&Web阶段总结篇】可以这样阅读Vue源码
前言
前面我们对微信小程序进行了研究:【微信小程序项目实践总结】30分钟从陌生到熟悉
在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一个规则,让我们可以先写H5代码,然后将小程序以及APP的业务差异代码做掉,岂不快哉?但小程序的web框架并不开源,不然也用不着我们在此费力了,经过研究,小程序web端框架是一套自研的MVVM框架,于是我们马上想到了借助第三方框架:
经过简单的研究,我们发现无论React或者Vue都是可以一定程度适应小程序开发模式的,市面上也有了对应的框架:mpvue&wepy
在使用以上任何一套框架体系前,我们都需要对MVVM框架有最基础的了解,不然后面随便遇到点什么问题可能都会变得难以继续,负责人要对团队负责而不是单纯只是想技术尝鲜,所以生产项目一定要使用自己能完成hold住的技术
这段时间我们尝试着去阅读Vue的源码,但现在的Vue是一个工程化产物,我们要学习的核心代码可能不到其中的1/3,多出来的是各种特性以及细节处理,在不清楚代码意图(目标)的情况下,事实上很难搞懂这段代码是要干什么,很多代码的出现都是一些精妙的小优化,知道的就会觉得十分惊艳,不知道的就会一头雾水,一来就看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>
大家对照着这个例子自己撸一下,其中有几个在业务中不太常用的知识点,第一个就是访问器属性,这里大概写个例子介绍下:
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(/<!--([\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>
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