一套代码小程序WebNative运行的探索03——处理模板及属性
接上文: 一套代码小程序&Web&Native运行的探索02
对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
我们在研究如果小程序在多端运行的时候,基本在前端框架这块陷入了困境,因为市面上没有框架可以直接拿来用,而Vue的相识度比较高,而且口碑很好,我们便接着这个机会同步学习Vue也解决我们的问题,我们看看这个系列结束后,会不会离目标进一点,后续如果实现后会重新整理系列文章......
参考:
https://github.com/fastCreator/MVVM(极度参考,十分感谢该作者,直接看Vue会比较吃力的,但是看完这个作者的代码便会轻易很多,可惜这个作者没有对应博客说明,不然就爽了)
https://www.tangshuang.net/3756.html
https://www.cnblogs.com/kidney/p/8018226.html
https://github.com/livoras/blog/issues/13
上文中我们借助HTMLParser这种高级神器,终于将文本中的表达式替换了出来,这里单纯说文本这里也有以下问题:这段是不支持js代码的,+-、三元代码都不支持,所以以上都只是帮助我们理解,还是之前那句话,越是单纯的代码,越是考虑少的代码,可能越是能理解实现,但是后续仍然需要补足,我们这里还是要跟Vue对齐,这样做有个好处,当你不知道怎么做的时候,可以看看Vue的实现,当你思考这么做合不合适的时候,也可以参考Vue,那可是经过烈火淬炼的,值得深度学习,我们今天的任务比较简单便是完整的处理完style、属性以及表达式处理,这里我们直接在fastCreator这个作者下的源码开始学习,还有种学习源码的方法就是抄三次......
我们学习的过程,先将代码写到一起方便理解,后续再慢慢拆分,首先是MVVM类,我们新建libs文件夹,先新建两个js文件,一个html-parser一个index(框架入口文件)
libs--index.js--html-parser.jsindex.html
1 import HTMLParser from './html-parser.js' 2 3 functionarrToObj(arr) {4 let map ={};5 for(let i = 0, l = arr.length; i < l; i++) {6 map[arr[i].name] =arr[i].value7 }8 returnmap;9 }10 11 functionhtmlParser(html) {12 13 //存储所有节点 14 let nodes =[];15 16 //记录当前节点位置,方便定位parent节点 17 let stack =[];18 19 HTMLParser(html, {20 /* 21 unary: 是不是自闭和标签比如 <br/> input22 attrs为属性的数组23 */ 24 start: function( tag, attrs, unary ) { //标签开始 25 /* 26 stack记录的父节点,如果节点长度大于1,一定具有父节点27 */ 28 let parent = stack.length ? stack[stack.length - 1] : null;29 30 //最终形成的node对象 31 let node ={32 //1标签, 2需要解析的表达式, 3 纯文本 33 type: 1,34 tag: tag,35 attrs: arrToObj(attrs),36 parent: parent,37 //关键属性 38 children: []39 };40 41 //如果存在父节点,也标志下这个属于其子节点 42 if(parent) {43 parent.children.push(node);44 }45 //还需要处理<br/> <input>这种非闭合标签 46 //... 47 48 //进入节点堆栈,当遇到弹出标签时候弹出 49 stack.push(node)50 nodes.push(node);51 52 //debugger; 53 },54 end: function( tag ) { //标签结束 55 //弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出 56 stack.pop();57 58 //debugger; 59 },60 chars: function( text ) { //文本 61 //如果是空格之类的不予处理 62 if(text.trim() === '') return;63 text =text.trim();64 65 //匹配 {{}} 拿出表达式 66 let reg = /\{\{(.*)\}\}/;67 let node = nodes[nodes.length - 1];68 //如果这里是表达式{{}}需要特殊处理 69 if(!node) return;70 71 if(reg.test(text)) {72 node.children.push({73 type: 2,74 expression: RegExp.$1,75 text: text76 });77 } else{78 node.children.push({79 type: 3,80 text: text81 });82 }83 //debugger; 84 }85 });86 87 returnnodes;88 89 }90 91 export defaultclass MVVM {92 /* 93 暂时要求必须传入data以及el,其他事件什么的不管94 95 */ 96 constructor(opts) {97 98 //要求必须存在,这里不做参数校验了 99 this.$el = typeof opts.el === 'string' ?document.getElementById(opts.el) : opts.el;100 101 //data必须存在,其他不做要求 102 this.$data =opts.data;103 104 //模板必须存在 105 this.$template =opts.template;106 107 //存放解析结束的虚拟dom 108 this.$nodes =[];109 110 //将模板解析后,转换为一个函数 111 this.$initRender();112 113 //渲染之 114 this.$render();115 debugger;116 }117 118 $initRender() {119 let template = this.$template;120 let nodes =htmlParser(template);121 this.$nodes =nodes;122 }123 124 //解析模板生成的函数,将最总html结构渲染出来 125 $render() {126 127 let data = this.$data;128 let root = this.$nodes[0];129 let parent = this._createEl(root);130 //简单遍历即可 131 132 this._render(parent, root.children);133 134 this.$el.appendChild(parent);135 }136 137 _createEl(node) {138 let data = this.$data;139 140 let el = document.createElement(node.tag || 'span');141 142 for (let key innode.attrs) {143 el.setAttribute(key, node.attrs[key])144 }145 146 if(node.type === 2) {147 el.innerText =data[node.expression];148 } else if(node.type === 3) {149 el.innerText =node.text;150 }151 152 returnel;153 }154 _render(parent, children) {155 let child = null;156 for(let i = 0, len = children.length; i < len; i++) {157 child = this._createEl(children[i]);158 parent.append(child);159 if(children[i].children) this._render(child, children[i].children);160 }161 }162 163 164 }
index
1 /* 2 * Modified at https://github.com/blowsie/Pure-JavaScript-HTML5-Parser3 */ 4 5 //Regular Expressions for parsing tags and attributes 6 let startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:@][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,7 endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/,8 attr = /([a-zA-Z_:@][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g9 10 //Empty Elements - HTML 5 11 let empty = makeMap("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr")12 13 //Block Elements - HTML 5 14 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")15 16 //Inline Elements - HTML 5 17 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")18 19 //Elements that you can, intentionally, leave open 20 //(and which close themselves) 21 let closeSelf = makeMap("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr")22 23 //Attributes that have their values filled in disabled="disabled" 24 let fillAttrs = makeMap("checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected")25 26 //Special Elements (can contain anything) 27 let special = makeMap("script,style")28 29 functionmakeMap(str) {30 var obj = {}, items = str.split(",");31 for (var i = 0; i < items.length; i++)32 obj[items[i]] = true;33 returnobj;34 }35 36 export default functionHTMLParser(html, handler) {37 var index, chars, match, stack = [], last =html;38 stack.last = function() {39 return this[this.length - 1];40 };41 42 while(html) {43 chars = true;44 45 //Make sure we're not in a script or style element 46 if (!stack.last() || !special[stack.last()]) {47 48 //Comment 49 if (html.indexOf("<!--") == 0) {50 index = html.indexOf("-->");51 52 if (index >= 0) {53 if(handler.comment)54 handler.comment(html.substring(4, index));55 html = html.substring(index + 3);56 chars = false;57 }58 59 //end tag 60 } else if (html.indexOf("</") == 0) {61 match =html.match(endTag);62 63 if(match) {64 html = html.substring(match[0].length);65 match[0].replace(endTag, parseEndTag);66 chars = false;67 }68 69 //start tag 70 } else if (html.indexOf("<") == 0) {71 match =html.match(startTag);72 73 if(match) {74 html = html.substring(match[0].length);75 match[0].replace(startTag, parseStartTag);76 chars = false;77 }78 }79 80 if(chars) {81 index = html.indexOf("<");82 83 var text = index < 0 ? html : html.substring(0, index);84 html = index < 0 ? "": html.substring(index);85 86 if(handler.chars)87 handler.chars(text);88 }89 90 } else{91 html = html.replace(new RegExp("([\\s\\S]*?)<\/" + stack.last() + "[^>]*>"), function(all, text) {92 text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, "$1$2");93 if(handler.chars)94 handler.chars(text);95 96 return "";97 });98 99 parseEndTag("", stack.last());100 }101 102 if (html ==last)103 throw "Parse Error: " +html;104 last =html;105 }106 107 //Clean up any remaining tags 108 parseEndTag();109 110 functionparseStartTag(tag, tagName, rest, unary) {111 tagName =tagName.toLowerCase();112 113 if(block[tagName]) {114 while (stack.last() &&inline[stack.last()]) {115 parseEndTag("", stack.last());116 }117 }118 119 if (closeSelf[tagName] && stack.last() ==tagName) {120 parseEndTag("", tagName);121 }122 123 unary = empty[tagName] || !!unary;124 125 if (!unary)126 stack.push(tagName);127 128 if(handler.start) {129 var attrs =[];130 131 rest.replace(attr, function(match, name) {132 var value = arguments[2] ? arguments[2] :133 arguments[3] ? arguments[3] :134 arguments[4] ? arguments[4] :135 fillAttrs[name] ? name : "";136 137 attrs.push({138 name: name,139 value: value,140 escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') //" 141 });142 });143 144 if(handler.start)145 handler.start(tagName, attrs, unary);146 }147 }148 149 functionparseEndTag(tag, tagName) {150 //If no tag name is provided, clean shop 151 if (!tagName)152 var pos = 0;153 154 //Find the closest opened tag of the same type 155 else 156 for (var pos = stack.length - 1; pos >= 0; pos--)157 if (stack[pos] ==tagName)158 break;159 160 if (pos >= 0) {161 //Close all the open elements, up the stack 162 for (var i = stack.length - 1; i >= pos; i--)163 if(handler.end)164 handler.end(stack[i]);165 166 //Remove the open elements from the stack 167 stack.length =pos;168 }169 }170 };
html-parser
这个时候我们的index代码量便下来了:
1 <!doctype html> 2 <html> 3 <head> 4 <title>起步</title> 5 </head> 6 <body> 7 8 <divid="app"> 9 10 </div> 11 12 <scripttype="module"> 13 14 import MVVM from'./libs/index.js' 15 16 let html=`17 <div class="c-row search-line"data-flag="start"ontap="clickHandler"> 18 <div class="c-span9 js-start search-line-txt"> 19 {{name}}</div> 20 <input type="text"> 21 <br> 22 </div> 23 `24 25 let vm= newMVVM({26 el:'app',27 template: html,28 data: {29 name:'叶小钗' 30 }31 })32 33 </script> 34 </body> 35 </html>
我们现在来更改index.js入口文件的代码,这里特别说一下其中的$mount方法,他试试是要做一个这样的事情:
//模板字符串<divid= "app">{{message}}</div>
//render函数 functionanonymous() {with(this){return _h('div',{attrs:{"id":"app"}},["\n "+_s(message)+"\n"])} }
将模板转换为一个函数render放到参数上,这里我们先简单实现,后续深入后我们重新翻下这个函数,修改后我们的index.js变成了这个样子:
1 import HTMLParser from './html-parser.js' 2 3 4 //工具函数 begin 5 6 functionisFunction(obj) {7 return typeof obj === 'function' 8 }9 10 11 functionmakeAttrsMap(attrs, delimiters) {12 const map ={}13 for (let i = 0, l = attrs.length; i < l; i++) {14 map[attrs[i].name] =attrs[i].value;15 }16 returnmap;17 }18 19 20 21 //dom操作 22 functionquery(el) {23 if (typeof el === 'string') {24 const selector =el25 el =document.querySelector(el)26 if (!el) {27 return document.createElement('div')28 }29 }30 returnel31 }32 33 functioncached(fn) {34 const cache = Object.create(null)35 return functioncachedFn(str) {36 const hit =cache[str]37 return hit || (cache[str] =fn(str))38 }39 }40 41 let idToTemplate = cached(function(id) {42 var el =query(id)43 return el &&el.innerHTML;44 })45 46 47 48 //工具函数 end 49 50 //模板解析函数 begin 51 52 const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g53 const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g 54 55 const buildRegex = cached(delimiters =>{56 const open = delimiters[0].replace(regexEscapeRE, '\\$&')57 const close = delimiters[1].replace(regexEscapeRE, '\\$&')58 return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')59 })60 61 62 functionTextParser(text, delimiters) {63 const tagRE = delimiters ?buildRegex(delimiters) : defaultTagRE64 if (!tagRE.test(text)) {65 return 66 }67 const tokens =[]68 let lastIndex = tagRE.lastIndex = 0 69 let match, index70 while ((match =tagRE.exec(text))) {71 index =match.index72 //push text token 73 if (index >lastIndex) {74 tokens.push(JSON.stringify(text.slice(lastIndex, index)))75 }76 //tag token 77 const exp = match[1].trim()78 tokens.push(`_s(${exp})`)79 lastIndex = index + match[0].length80 }81 if (lastIndex <text.length) {82 tokens.push(JSON.stringify(text.slice(lastIndex)))83 }84 return tokens.join('+')85 }86 87 //******核心中的核心 88 functioncompileToFunctions(template, vm) {89 let root;90 let currentParent;91 let options =vm.$options;92 let stack =[];93 94 //这段代码昨天做过解释,这里属性参数比昨天多一些 95 HTMLParser(template, {96 start: function(tag, attrs, unary) {97 98 let element ={99 vm: vm,100 //1 标签 2 文本表达式 3 文本 101 type: 1,102 tag,103 //数组 104 attrsList: attrs,105 attrsMap: makeAttrsMap(attrs), //将属性数组转换为对象 106 parent: currentParent,107 children: []108 };109 110 if(!root) {111 vm.$vnode = root =element;112 }113 114 if(currentParent && !element.forbidden) {115 currentParent.children.push(element);116 element.parent =currentParent;117 }118 119 if(!unary) {120 currentParent =element;121 stack.push(element);122 }123 124 },125 end: function(tag) {126 //获取当前元素 127 let element = stack[stack.length - 1];128 let lastNode = element.children[element.children.length - 1];129 //删除最后一个空白节点,暂时感觉没撒用呢 130 if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') {131 element.children.pop();132 }133 134 //据说比调用pop节约性能相当于stack.pop() 135 stack.length -= 1;136 currentParent = stack[stack.length - 1];137 138 },139 //处理真实的节点 140 chars: function(text) {141 if (!text.trim()) {142 //text = ' ' 143 return;144 }145 //解析文本节点 exp: a{{b}}c => 'a'+_s(a)+'b' 146 let expression =TextParser(text, options.delimiters)147 if(expression) {148 currentParent.children.push({149 type: 2,150 expression,151 text152 })153 } else{154 currentParent &¤tParent.children.push({155 type: 3,156 text157 })158 }159 }160 161 });162 163 returnroot;164 165 }166 167 168 //模板解析函数 end 169 170 //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染 171 //要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说 172 functioninitData(vm, data) {173 if(isFunction(data)) {174 data =data()175 }176 vm.$data =data;177 }178 179 //全局数据保证每个MVVM实例拥有唯一id 180 let uid = 0;181 182 export defaultclass MVVM {183 constructor(options) {184 this.$options =options;185 186 //我们可以在传入参数的地方设置标签替换方式,比如可以设置为['<%=', '%>'],注意这里是数组 187 this.$options.delimiters = this.$options.delimiters || ["{{", "}}"];188 189 //唯一标志 190 this._uid = uid++;191 192 if(options.data) {193 //194 initData(this, options.data);195 }196 197 this.$mount(options.el);198 199 }200 201 //解析模板compileToFunctions,将之形成一个函数 202 //很多网上的解释是将实例挂载到dom上,这里有些没明白,我们后面点再看看 203 $mount(el) {204 let options = this.$options;205 206 el = el &&query(el);207 this.$el =el;208 209 //如果用户自定义了render函数则不需要解析template 210 //这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode 211 if(!options.render) {212 let template =options.template;213 if(template) {214 if(typeof template === 'string') {215 //获取script的template模板 216 if (template[0] === '#') {217 template =idToTemplate(template)218 }219 } else if(template.nodeType) {220 //如果template是个dom结构,只能有一个根节点 221 template =template.innerHTML;222 }223 }224 225 //上面的代码什么都没做,只是确保正确的拿到了template数据,考虑了各种情况 226 //下面这段是关键,也是我们昨天干的事情 227 if(template) {228 //***核心函数***/ 229 let render = compileToFunctions(template, this);230 options.render =render;231 }232 233 234 }235 236 237 238 }239 240 241 }242 243 //过去的代码 244 functionarrToObj(arr) {245 let map ={};246 for(let i = 0, l = arr.length; i < l; i++) {247 map[arr[i].name] =arr[i].value248 }249 returnmap;250 }251 252 functionhtmlParser(html) {253 254 //存储所有节点 255 let nodes =[];256 257 //记录当前节点位置,方便定位parent节点 258 let stack =[];259 260 HTMLParser(html, {261 /* 262 unary: 是不是自闭和标签比如 <br/> input263 attrs为属性的数组264 */ 265 start: function( tag, attrs, unary ) { //标签开始 266 /* 267 stack记录的父节点,如果节点长度大于1,一定具有父节点268 */ 269 let parent = stack.length ? stack[stack.length - 1] : null;270 271 //最终形成的node对象 272 let node ={273 //1标签, 2需要解析的表达式, 3 纯文本 274 type: 1,275 tag: tag,276 attrs: arrToObj(attrs),277 parent: parent,278 //关键属性 279 children: []280 };281 282 //如果存在父节点,也标志下这个属于其子节点 283 if(parent) {284 parent.children.push(node);285 }286 //还需要处理<br/> <input>这种非闭合标签 287 //... 288 289 //进入节点堆栈,当遇到弹出标签时候弹出 290 stack.push(node)291 nodes.push(node);292 293 //debugger; 294 },295 end: function( tag ) { //标签结束 296 //弹出当前子节点,根节点一定是最后弹出去的,兄弟节点之间会按顺序弹出,其父节点在最后一个子节点弹出后会被弹出 297 stack.pop();298 299 //debugger; 300 },301 chars: function( text ) { //文本 302 //如果是空格之类的不予处理 303 if(text.trim() === '') return;304 text =text.trim();305 306 //匹配 {{}} 拿出表达式 307 let reg = /\{\{(.*)\}\}/;308 let node = nodes[nodes.length - 1];309 //如果这里是表达式{{}}需要特殊处理 310 if(!node) return;311 312 if(reg.test(text)) {313 node.children.push({314 type: 2,315 expression: RegExp.$1,316 text: text317 });318 } else{319 node.children.push({320 type: 3,321 text: text322 });323 }324 //debugger; 325 }326 });327 328 returnnodes;329 330 }331 332 class MVVM1 {333 /* 334 暂时要求必须传入data以及el,其他事件什么的不管335 336 */ 337 constructor(opts) {338 339 //要求必须存在,这里不做参数校验了 340 this.$el = typeof opts.el === 'string' ?document.getElementById(opts.el) : opts.el;341 342 //data必须存在,其他不做要求 343 this.$data =opts.data;344 345 //模板必须存在 346 this.$template =opts.template;347 348 //存放解析结束的虚拟dom 349 this.$nodes =[];350 351 //将模板解析后,转换为一个函数 352 this.$initRender();353 354 //渲染之 355 this.$render();356 debugger;357 }358 359 $initRender() {360 let template = this.$template;361 let nodes =htmlParser(template);362 this.$nodes =nodes;363 }364 365 //解析模板生成的函数,将最总html结构渲染出来 366 $render() {367 368 let data = this.$data;369 let root = this.$nodes[0];370 let parent = this._createEl(root);371 //简单遍历即可 372 373 this._render(parent, root.children);374 375 this.$el.appendChild(parent);376 }377 378 _createEl(node) {379 let data = this.$data;380 381 let el = document.createElement(node.tag || 'span');382 383 for (let key innode.attrs) {384 el.setAttribute(key, node.attrs[key])385 }386 387 if(node.type === 2) {388 el.innerText =data[node.expression];389 } else if(node.type === 3) {390 el.innerText =node.text;391 }392 393 returnel;394 }395 _render(parent, children) {396 let child = null;397 for(let i = 0, len = children.length; i < len; i++) {398 child = this._createEl(children[i]);399 parent.append(child);400 if(children[i].children) this._render(child, children[i].children);401 }402 }403 404 405 }
index.js
这里仅仅是到输出vnode这步,接下来是将vnode转换为函数render,在写这段代码之前我们来说一说Vue中的render参数,事实上,我们new Vue的时候可以直接传递render参数:
1 newVue({2 render: function() {3 return this._h('div', {4 attrs:{5 a: 'aaa' 6 }7 }, [8 this._h('div')9 ])10 }11 })
他对应的这段代码:
1 newVue({2 template: '<div class="aa">Hello World! </div>' 3 })
真实代码过程中的过程,以及我们上面代码的过程是,template 字符串 => 虚拟DOM对象 ast => 根据ast生成render函数......,这里又涉及到了另一个需要引用的工具库snabbdom
snabbdom-render
https://github.com/snabbdom/snabbdom,Vue2.0底层借鉴了snabdom,我们这里先重点介绍他的h函数,h(help帮助创建vnode)函数可以让我们轻松创建vnode,这里再对Virtual DOM做一个说明,这段话是我看到觉得很好的解释的话(https://github.com/livoras/blog/issues/13):
我们一段js对象可以很容易的翻译为一段HTML代码:
1 var element ={2 tagName: 'ul', //节点标签名 3 props: { //DOM的属性,用一个对象存储键值对 4 id: 'list' 5 },6 children: [ //该节点的子节点 7 {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},8 {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},9 {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},10 ]11 }
1 <ulid='list'> 2 <liclass='item'>Item 1</li> 3 <liclass='item'>Item 2</li> 4 <liclass='item'>Item 3</li> 5 </ul>
同样的,我们一段HTML代码其实属性、参数是很有限的,也十分轻易的能转换成一个js对象,我们如果使用dom操作改变了我们的html结构,事实上会形成一个新的js对象,这个时候我们将渲染后形成的js对象和渲染前形成的js对象进行对比,便可以清晰知道这次变化的差异部分,然后拿着差异部分的js对象(每个js对象都会映射到一个真实的dom对象)做更新即可,关于Virtual DOM文章作者对此做了一个总结:
① 用js对象表示DOM树结构,然后用这个js对象树结构生成一个真正的DOM树(document.create***操作),插入文档中(这个时候会形成render tree,看得到了)
② 当状态变化时(数据变化时),重新构造一颗新的对象树,和之前的作对比,记录差异部分
③ 将差异部分的数据更新到视图上,更新结束
他这里描述的比较简单,事实上我们根据昨天的学习,可以知道框架事实上是劫持了没个数据对象,所以每个数据对象做了改变,会影响到哪些DOM结构是有记录的,这块我们后面章节再说,我们其实今天主要的目的还是处理文本和属性生成,却不想提前接触虚拟DOM了......
其实我们之前的js对象element就已经可以代表一个虚拟dom了,之所以引入snabbddom应该是后面要处理diff部分,所以我们乖乖的学吧,首先我们定义一个节点的类:
1 class Element {2 constructor(tagName, props, children) {3 this.tagName =tagName;4 this.props =props;5 this.children =children;6 }7 }
上面的dom结构便可以变成这样了:
1 new Element('ul', {id: 'list'}, [2 new Element('li', {class: 'item'}, ['Item 1']),3 new Element('li', {class: 'item'}, ['Item 2']),4 new Element('li', {class: 'item'}, ['Item 3'])5 ])
似乎代码有点不好看,于是封装下实例化操作:
1 class Element {2 constructor(tagName, props, children) {3 this.tagName =tagName;4 this.props =props;5 this.children =children;6 }7 }8 9 functionel(tagName, props, children) {10 return newElement(tagName, props, children)11 }12 13 el('ul', {id: 'list'}, [14 el('li', {class: 'item'}, ['Item 1']),15 el('li', {class: 'item'}, ['Item 2']),16 el('li', {class: 'item'}, ['Item 3'])17 ])
然后就是根据这个js对象生成真正的DOM结构,也就是上面的html字符串:
1 <!doctype html> 2 <html> 3 <head> 4 <title>起步</title> 5 </head> 6 <body> 7 8 <scripttype="text/javascript"> 9 //***虚拟dom部分代码,后续会换成snabdom 10 class Element {11 constructor(tagName, props, children) {12 this.tagName=tagName;13 this.props=props;14 this.children=children;15 }16 render() {17 //拿着根节点往下面撸 18 let root=document.createElement(this.tagName);19 let props= this.props;20 21 for(let nameinprops) {22 root.setAttribute(name, props[name]);23 }24 25 let children= this.children;26 27 for(let i= 0, l=children.length; i<l; i++) {28 let child=children[i];29 let childEl;30 if(childinstanceofElement) {31 //递归调用 32 childEl=child.render();33 }else{34 childEl=document.createTextNode(child);35 }36 root.append(childEl);37 }38 39 this.rootNode=root;40 returnroot;41 }42 }43 44 functionel(tagName, props, children) {45 return newElement(tagName, props, children)46 }47 48 let vnode=el('ul', {id:'list'}, [49 el('li', {class:'item'}, ['Item 1']),50 el('li', {class:'item'}, ['Item 2']),51 el('li', {class:'item'}, ['Item 3'])52 ])53 54 let root=vnode.render();55 56 document.body.appendChild(root);57 58 </script> 59 60 </body> 61 </html>
饶了这么大一圈子,我们再回头看这段代码:
1 newVue({2 render: function() {3 return this._h('div', {4 attrs:{5 a: 'aaa' 6 }7 }, [8 this._h('div')9 ])10 }11 })
这个时候,我们对这个_h干了什么,可能便有比较清晰的认识了,于是我们回到我们之前的代码,暂时跳出snabbdom
解析模板
在render中,我们有这么一段代码:
1 //没有指令时运行,或者指令解析完毕 2 functionnodir(el) {3 let code4 //设置属性 等值 5 const data =genData(el);6 //转换子节点 7 const children = genChildren(el, true);8 code = `_h('${el.tag}'${9 data ? `,${data}` : '' //data 10 }${11 children ? `,${children}` : '' //children 12 })`13 returncode14 }
事实上这个跟上面那坨代码完成的工作差不多(同样的遍历加递归),只不过他这里还有更多的目的,比如这段代码最终会生成这样的:
_h('div',{},[_h('div',{},["\n "+_s(name)]),_h('input',{}),_h('br',{})])
这段代码会被包装成一个模板类,等待被实例化,显然到这里还没进入我们的模板解析过程,因为里面出现了_s(name),我们如果加一个span的话会变成这样:
1 <divclass="c-row search-line"data-flag="start"ontap="clickHandler"> 2 <divclass="c-span9 js-start search-line-txt"> 3 {{name}}</div> 4 <span>{{age+1}}</span> 5 <inputtype="text"> 6 <br> 7 </div>
_h('div',{},[_h('div',{},["\n "+_s(name)]),_h('span',{},[_s(age+1)]),_h('input',{}),_h('br',{})])
真实运行的时候这段代码是这个样子的:
这段代码很纯粹,不包含属性和class,我们只需要处理文本内容替换即可,今天的任务比较简单,所以接下来的流程后便可以得出第一阶段代码:
1 <!doctype html> 2 <html> 3 <head> 4 <title>起步</title> 5 </head> 6 <body> 7 8 <divid="app"> 9 10 </div> 11 12 <scripttype="module"> 13 14 import MVVM from'./libs/index.js' 15 16 let html=`17 <div class="c-row search-line"data-flag="start"ontap="clickHandler"> 18 <div class="c-span9 js-start search-line-txt"> 19 {{name}}</div> 20 <span>{{age+1}}</span> 21 <input type="text"> 22 <br> 23 </div> 24 `25 26 let vm= newMVVM({27 el:'#app',28 template: html,29 data: {30 name:'叶小钗',31 age:30 32 }33 })34 35 </script> 36 </body> 37 </html>
1 import HTMLParser from './html-parser.js' 2 3 4 //工具函数 begin 5 6 functionisFunction(obj) {7 return typeof obj === 'function' 8 }9 10 11 functionmakeAttrsMap(attrs, delimiters) {12 const map ={}13 for (let i = 0, l = attrs.length; i < l; i++) {14 map[attrs[i].name] =attrs[i].value;15 }16 returnmap;17 }18 19 20 21 //dom操作 22 functionquery(el) {23 if (typeof el === 'string') {24 const selector =el25 el =document.querySelector(el)26 if (!el) {27 return document.createElement('div')28 }29 }30 returnel31 }32 33 functioncached(fn) {34 const cache = Object.create(null)35 return functioncachedFn(str) {36 const hit =cache[str]37 return hit || (cache[str] =fn(str))38 }39 }40 41 let idToTemplate = cached(function(id) {42 var el =query(id)43 return el &&el.innerHTML;44 })45 46 47 48 //工具函数 end 49 50 //模板解析函数 begin 51 52 const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g53 const regexEscapeRE = /[-.*+?^${}()|[\]/\\]/g 54 55 const buildRegex = cached(delimiters =>{56 const open = delimiters[0].replace(regexEscapeRE, '\\$&')57 const close = delimiters[1].replace(regexEscapeRE, '\\$&')58 return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')59 })60 61 62 functionTextParser(text, delimiters) {63 const tagRE = delimiters ?buildRegex(delimiters) : defaultTagRE64 if (!tagRE.test(text)) {65 return 66 }67 const tokens =[]68 let lastIndex = tagRE.lastIndex = 0 69 let match, index70 while ((match =tagRE.exec(text))) {71 index =match.index72 //push text token 73 if (index >lastIndex) {74 tokens.push(JSON.stringify(text.slice(lastIndex, index)))75 }76 //tag token 77 const exp = match[1].trim()78 tokens.push(`_s(${exp})`)79 lastIndex = index + match[0].length80 }81 if (lastIndex <text.length) {82 tokens.push(JSON.stringify(text.slice(lastIndex)))83 }84 return tokens.join('+')85 }86 87 functionmakeFunction(code) {88 try{89 return newFunction(code)90 } catch(e) {91 return function(){};92 }93 }94 95 //***虚拟dom部分代码,后续会换成snabdom 96 class Element {97 constructor(tagName, props, children) {98 this.tagName =tagName;99 this.props =props;100 this.children = children ||[];101 }102 render() {103 //拿着根节点往下面撸 104 let el = document.createElement(this.tagName);105 let props = this.props;106 107 for(let name inprops) {108 el.setAttribute(name, props[name]);109 }110 111 let children = this.children;112 113 for(let i = 0, l = children.length; i < l; i++) {114 let child =children[i];115 let childEl;116 if(child instanceofElement) {117 //递归调用 118 childEl =child.render();119 } else{120 childEl =document.createTextNode(child);121 }122 el.append(childEl);123 }124 returnel;125 }126 }127 128 functionel(tagName, props, children) {129 return newElement(tagName, props, children)130 }131 132 //***核心中的核心,将vnode转换为函数 133 134 const simplePathRE = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/ 135 const modifierCode ={136 stop: '$event.stopPropagation();',137 prevent: '$event.preventDefault();',138 self: 'if($event.target !== $event.currentTarget)return;',139 ctrl: 'if(!$event.ctrlKey)return;',140 shift: 'if(!$event.shiftKey)return;',141 alt: 'if(!$event.altKey)return;',142 meta: 'if(!$event.metaKey)return;' 143 }144 145 const keyCodes ={146 esc: 27,147 tab: 9,148 enter: 13,149 space: 32,150 up: 38,151 left: 37,152 right: 39,153 down: 40,154 'delete': [8, 46]155 }156 157 158 functioncodeGen(ast) {159 //解析成h render字符串形式 160 const code = ast ? genElement(ast) : '_h("div")' 161 //把render函数,包起来,使其在当前作用域内 162 return makeFunction(`with(this){ debugger; return${code}}`)163 }164 165 functiongenElement(el) {166 //无指令 167 returnnodir(el)168 }169 170 //没有指令时运行,或者指令解析完毕 171 functionnodir(el) {172 let code173 //设置属性 等值 174 const data =genData(el);175 //转换子节点 176 const children = genChildren(el, true);177 code = `_h('${el.tag}'${178 data ? `,${data}` : '' //data 179 }${180 children ? `,${children}` : '' //children 181 })`182 returncode183 }184 185 functiongenChildren(el, checkSkip) {186 const children =el.children187 if(children.length) {188 const el = children[0]189 //如果是v-for 190 //if (children.length === 1 && el.for) { 191 //return genElement(el) 192 //} 193 const normalizationType = 0 194 return `[${children.map(genNode).join(',')}]${195 checkSkip196 ? normalizationType ? `,${normalizationType}` : '' 197 : '' 198 }`199 }200 }201 202 functiongenNode(node) {203 if (node.type === 1) {204 returngenElement(node)205 } else{206 returngenText(node)207 }208 }209 210 functiongenText(text) {211 return text.type === 2 ?text.expression : JSON.stringify(text.text)212 }213 214 functiongenData(el) {215 let data = '{' 216 //attributes 217 if(el.style) {218 data += 'style:' + genProps(el.style) + ',' 219 }220 if(Object.keys(el.attrs).length) {221 data += 'attrs:' + genProps(el.attrs) + ',' 222 }223 if(Object.keys(el.props).length) {224 data += 'props:' + genProps(el.props) + ',' 225 }226 if(Object.keys(el.events).length) {227 data += 'on:' + genProps(el.events) + ',' 228 }229 if(Object.keys(el.hook).length) {230 data += 'hook:' + genProps(el.hook) + ',' 231 }232 data = data.replace(/,$/, '') + '}' 233 returndata234 }235 236 functiongenProps(props) {237 let res = '{';238 for (let key inprops) {239 res += `"${key}":${props[key]},`240 }241 return res.slice(0, -1) + '}' 242 }243 244 //******核心中的核心 245 functioncompileToFunctions(template, vm) {246 let root;247 let currentParent;248 let options =vm.$options;249 let stack =[];250 251 //这段代码昨天做过解释,这里属性参数比昨天多一些 252 HTMLParser(template, {253 start: function(tag, attrs, unary) {254 255 let element ={256 vm: vm,257 //1 标签 2 文本表达式 3 文本 258 type: 1,259 tag,260 //数组 261 attrsList: attrs,262 attrsMap: makeAttrsMap(attrs), //将属性数组转换为对象 263 parent: currentParent,264 children: [],265 266 //下面这些属性先不予关注,因为底层函数没有做校验,不传要报错 267 events: {},268 style: null,269 hook: {},270 props: {},//DOM属性 271 attrs: {}//值为true,false则移除该属性 272 273 };274 275 if(!root) {276 vm.$vnode = root =element;277 }278 279 if(currentParent && !element.forbidden) {280 currentParent.children.push(element);281 element.parent =currentParent;282 }283 284 if(!unary) {285 currentParent =element;286 stack.push(element);287 }288 289 },290 end: function(tag) {291 //获取当前元素 292 let element = stack[stack.length - 1];293 let lastNode = element.children[element.children.length - 1];294 //删除最后一个空白节点,暂时感觉没撒用呢 295 if(lastNode && lastNode.type === 3 && lastNode.text.trim === '') {296 element.children.pop();297 }298 299 //据说比调用pop节约性能相当于stack.pop() 300 stack.length -= 1;301 currentParent = stack[stack.length - 1];302 303 },304 //处理真实的节点 305 chars: function(text) {306 if (!text.trim()) {307 //text = ' ' 308 return;309 }310 //解析文本节点 exp: a{{b}}c => 'a'+_s(a)+'b' 311 let expression =TextParser(text, options.delimiters)312 if(expression) {313 currentParent.children.push({314 type: 2,315 expression,316 text317 })318 } else{319 currentParent &¤tParent.children.push({320 type: 3,321 text322 })323 }324 }325 326 });327 328 //***关键代码*** 329 //将vnode转换为render函数,事实上可以直接传入这种render函数,便不会执行这块逻辑,编译时候会把这块工作做掉 330 returncodeGen(root);331 332 }333 334 335 //模板解析函数 end 336 337 //因为我们后面采用setData的方式通知更新,不做响应式更新,这里也先不考虑update,不考虑监控,先关注首次渲染 338 //要做到更新数据,DOM跟着更新,事实上就是所有的data数据被监控(劫持)起来了,一旦更新都会调用对应的回调,我们这里做到更新再说 339 functioninitData(vm, data) {340 if(isFunction(data)) {341 data =data()342 }343 344 //这里将data上的数据移植到this上,后面要监控 345 for(let key indata) {346 347 //这里有可能会把自身方法覆盖,所以自身的属性方法需要+$ 348 vm[key] =data[key];349 }350 351 vm.$data =data;352 }353 354 //全局数据保证每个MVVM实例拥有唯一id 355 let uid = 0;356 357 export defaultclass MVVM {358 constructor(options) {359 this.$options =options;360 361 //我们可以在传入参数的地方设置标签替换方式,比如可以设置为['<%=', '%>'],注意这里是数组 362 this.$options.delimiters = this.$options.delimiters || ["{{", "}}"];363 364 //唯一标志 365 this._uid = uid++;366 367 if(options.data) {368 //369 initData(this, options.data);370 }371 372 this.$mount(options.el);373 374 let _node = this._render().render();375 this.$el.appendChild( _node)376 377 }378 379 //解析模板compileToFunctions,将之形成一个函数 380 //很多网上的解释是将实例挂载到dom上,这里有些没明白,我们后面点再看看 381 $mount(el) {382 let options = this.$options;383 384 el = el &&query(el);385 this.$el =el;386 387 //如果用户自定义了render函数则不需要解析template 388 //这里所谓的用户自定义,应该是用户生成了框架生成那坨代码,事实上还是将template转换为vnode 389 if(!options.render) {390 let template =options.template;391 if(template) {392 if(typeof template === 'string') {393 //获取script的template模板 394 if (template[0] === '#') {395 template =idToTemplate(template)396 }397 } else if(template.nodeType) {398 //如果template是个dom结构,只能有一个根节点 399 template =template.innerHTML;400 }401 }402 403 //上面的代码什么都没做,只是确保正确的拿到了template数据,考虑了各种情况 404 //下面这段是关键,也是我们昨天干的事情 405 if(template) {406 //***核心函数***/ 407 let render = compileToFunctions(template, this);408 options.render =render;409 }410 }411 412 return this;413 }414 415 _render() {416 let render = this.$options.render417 let vnode418 try{419 //自动解析的template不需要h,用户自定义的函数需要h 420 vnode = render.call(this, this._h);421 } catch(e) {422 warn(`render Error : ${e}`)423 }424 returnvnode425 }426 427 _h(tag, data, children) {428 returnel(tag, data, children)429 }430 431 _s(val) {432 return val == null 433 ? '' 434 : typeof val === 'object' 435 ? JSON.stringify(val, null, 2)436 : String(val)437 }438 439 }
libs/index.js
之前我们图简单,一直没有解决属性问题,现在我们在模板里面加入一些属性:
1 <div class="c-row search-line" data-name="{{name}}" data-flag="start" ontap="clickHandler"> 2 <div class="c-span9 js-start search-line-txt"> 3 {{name}}</div> 4 <span>{{age+1}}</span> 5 <input type="text" value="{{age}}"> 6 <br> 7 </div>
情况就变得有所不同了,这里多加一句:
1 setElAttrs(el, delimiters)2 //==>3 function setElAttrs(el, delimiters) {4 var s = delimiters[0], e = delimiters[1];5 var reg = new RegExp(`^${s}(\.+\)${e}$`);6 var attrs = el.attrsMap;7 for (let key in attrs) { 8 let value = attrs[key]; 9 var match = value.match(reg) 10 if (match) { 11 value = match[1]; 12 if (isAttr(key)) { 13 el.props[key] = '_s('+value+')'; 14 } else { 15 el.attrs[key] = value; 16 } 17 } else { 18 if (isAttr(key)) { 19 el.props[key] = "'" + value + "'"; 20 } else { 21 el.attrs[key] = "'" + value + "'"; 22 } 23 } 24 25 } 26 }
这段代码会处理所有的属性,如果是属性中包含“{{}}”关键词,便会替换,不是我们的属性便放到attrs中,是的就放到props中,这里暂时不太能区分为什么要分为attrs何props,后续我们这边给出代码,于是我们的index.js变成了这个样子:
_h('div',{attrs:{"data-name":name,"data-flag":'start',"ontap":'clickHandler'},props:{"class":'c-row search-line'}},[_h('div',{props:{"class":'c-span9 js-start search-line-txt'}},["\n "+_s(name)]),_h('span',{},[_s(age+1)]),_h('input',{props:{"type":'text',"value":_s(age)}}),_h('br',{})])
1 <div id="app"> 2 <div class="c-row search-line" data-name="叶小钗" data-flag="start" ontap="clickHandler"> 3 <div class="c-span9 js-start search-line-txt"> 4 叶小钗</div> 5 <span>31</span> 6 <input type="text" value="30"> 7 <br> 8 </div> 9 </div>
然后我们来处理class以及style,他们是需要特殊处理的:
<div class="c-row search-line {{name}} {{age}}" style="font-size: 14px; margin-left: {{age}}px " data-name="{{name}}" data-flag="start" ontap="clickHandler"><div class="c-span9 js-start search-line-txt">{{name}}</div><span>{{age+1}}</span><input type="text" value="{{age}}"><br> </div>
生成了如下代码:
1 <div class="c-row search-line 叶小钗 30" data-name="叶小钗" data-flag="start" ontap="clickHandler" style="font-size: 14px; margin-left: 30px ;"> 2 <div class="c-span9 js-start search-line-txt"> 3 叶小钗</div> 4 <span>31</span> 5 <input type="text" value="30"> 6 <br> 7 </div>
虽然这段代码能运行,无论如何我们的属性和class也展示出来了,但是问题却不少:
① 这段代码仅仅就是为了运行,或者说帮助我们理解
② libs/index.js代码已经超过了500行,维护起来有点困难了,连我自己都有时候找不到东西,所以我们该分拆文件了
于是,我们暂且忍受这段说明性(演示性)代码,将之进行文件分拆
文件分拆
文件拆分后代码顺便传到了github上:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
这里简单的解释下各个文件是干撒的:
1 ./libs2 ..../codegen.js 代码生成器,传入一个ast(js树对象),转换为render函数3 ..../helps.js 处理vnode的相关工具函数,比如处理属性节点,里面的生成函数感觉该放到utils中4 ..../html-parser.js 第三方库,HTML解析神器,帮助生成js dom树对象5 ..../instance.js 初始化mvvm实例工具类6 ..../mvvm.js 入口函数7 ..../parser.js 模板解析生成render函数,核心8 ..../text-parser.js 工具类,将{{}}做替换生成字符串9 ..../utils.js 工具库 10 ..../vnode.js 虚拟树库,暂时自己写的,后续要换成snabbdom 11 ./index.html 入口文件
今天的学习到此位置,明天我们来处理数据更新相关
转载于:https://www.cnblogs.com/yexiaochai/p/9699698.html
一套代码小程序WebNative运行的探索03——处理模板及属性相关推荐
- 一套代码小程序WebNative运行的探索02
接上文:一套代码小程序&Web&Native运行的探索01,本文都是一些探索性为目的的研究学习,在最终版输出前,内中的内容可能会有点乱 参考: https://github.com/f ...
- 一套代码小程序WebNative运行的探索01
前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 并且用小程序翻写了之前一个demo:[组件化开发]前端进阶篇之如何编写可维护可升级的代码 之前一直在跟业务方打交道 ...
- 【一套代码小程序NativeWeb阶段总结篇】可以这样阅读Vue源码
前言 前面我们对微信小程序进行了研究:[微信小程序项目实践总结]30分钟从陌生到熟悉 在实际代码过程中我们发现,我们可能又要做H5站又要做小程序同时还要做个APP,这里会造成很大的资源浪费,如果设定一 ...
- 1000套微信小程序源码源代码带后台带运行截图预览图学习资料网盘下载
1000多套微信小程序源码带后台+教程+不同行业的源码集合 o2o行业 | - 盒马鲜生 | - 轻客洗衣 互联网行业 | - 云文档 | - 仿ofo共享单车 | - 仿美团外卖 | - 仿饿了么 ...
- uni app 调用网络打印机_一套代码,七端运行-uni-app
为什么要使用uni-app uni-app 是一个使用 Vue.js 开发跨平台应用的前端框架,开发者编写一套代码,可编译到iOS.Android.H5.小程序等多个平台. 同时在别的方面它也有很大优 ...
- 免费分享20套微信小程序源码 源码免费下载【强烈推荐】
淘源码:国内知名的源码免费下载平台 微信小程序源码包括:商城系统源码.点餐外卖源码.垃圾分类识别源码.预约洗车源码.物业管理源码.校园跑腿源码.驾考学习源码.会议预约源码.图书管理源码.智能停车源码. ...
- 阿里云ARMS重磅推出小程序监控,助力小程序稳定运行
2018年是小程序蓬勃发展的一年,各大公司如腾讯.阿里.百度.头条等都陆续推出了自己的小程序,小程序已成为一个未来必然的趋势.移动互联网的新风口.据数据统计,目前已上线的微信小程序已超过100万,支付 ...
- 使用uniapp做微信小程序,在小程序编辑器运行时编译报错:appid不合法,导致启用不了。
使用uniapp做微信小程序,在小程序编辑器运行时编译报错:appid不合法,导致启用不了.这个好坑爹啊,我这么淑女的小可爱都想骂人了,我好累,写个博客发泄发泄吧! 复现我的问题:神操作--代码跟别人 ...
- uniapp写微信小程序怎么运行到微信开发工具上
1.选择运行>运行到小程序模拟器>运行到微信开发者工具 2.这样unpackage中就多一个文件mp-weixin 3.把这个文件导入到微信小程序中就行啦
最新文章
- Tomcat:Connection reset by peer: socket write error
- 多天线技术是LTE的重要演进方向已成为产业共识
- Server Error in '/' Application. 报错
- Oracle使用impdb/expdb数据泵导入/导出数据库
- android给后台传递json,将服务中的JSON数据发送到Android中的UI
- android 6.0版本名字,棉花糖Marshmallow 是Android 6.0的名字
- 企业微信H5_消息推送概述,发送应用消息示例
- 数据字符集mysql主从数据库,分库分表等笔记
- mysql begin end 用法_数据库:Mysql中“select ... for update”排他锁分析
- 【网络】HTTPS 怎么保证数据传输的安全性
- java 重载 调用指定_java 方法重载的时候,同一个类,父子类,调用哪个方法的问题...
- jquery手机横屏竖屏判断显示
- sql 时间字符串转换
- Atitit 价值观与理念总结 Diy自力更生尽可能 提高独立性 减少依赖 大而全优先于小而美 适度分权防止集权导致大决策失误 方式多种多样 综合是王道 简单快速优先 努力实现人们喜闻乐见的服务 信
- SLAM--卡尔曼滤波、粒子滤波
- jQuery实现表单提交验证
- rar压缩包加密以及rar密码破解的教程
- 芯邦主控的U盘量产教程
- 怎么样添加桌面我的计算机,怎么样把我的电脑添加到桌面上
- 看完清华最新发布的毕业生去向,我沉默了
热门文章
- wgan 不理解 损失函数_[图像盲去噪与GAN]GCBD翻译理解
- 局域网即时通讯软件java_如何选择企业即时通讯软件?
- 计算机科学技术考研内容,计算机科学技术考研考什么科目
- openim php sdk,imsdk_restapi-php-sdk
- scala写入mysql_spark rdd转dataframe 写入mysql的实例讲解
- by mybatis 自定义order_MyBatis动态SQL实现ORDER BY和LIMIT的控制?
- djangosave保存数据太慢_PaddlePaddle从入门到炼丹八——模型的保存与使用
- Linux下飞鸽传书项目设计书,Linux 下飞鸽传书设计实现
- 突破次元壁障,Python爬虫获取二次元女友
- 通过ssh访问git项目