一套代码小程序&Web&Native运行的探索05——snabbdom
接上文:一套代码小程序&Web&Native运行的探索04——数据更新
对应Git代码地址请见:https://github.com/yexiaochai/wxdemo/tree/master/mvvm
https://github.com/fastCreator/MVVM(极度参考,十分感谢该作者,直接看Vue会比较吃力的,但是看完这个作者的代码便会轻易很多,可惜这个作者没有对应博客说明,不然就爽了)
https://www.tangshuang.net/3756.html
https://www.cnblogs.com/kidney/p/8018226.html
http://www.cnblogs.com/kidney/p/6052935.html
https://github.com/livoras/blog/issues/13
根据最近的学习,离我们最终的目标还有一段距离,但是对于Vue实现原理却慢慢有了体系化的认识,相信本系列结束后,如果能完成我们跨端代码,哪怕是demo的实现,都会对后续了解Vue或者React这里源码提供深远的帮助,平时工作较忙,这次刚好碰到假期,虽然会耽搁一些时间,我们试试这段时间运气可好,能不能在这个阶段取得不错的进展,好了我们继续完成今天的学习吧
到目前的地步,其中一些代码比较散乱,没有办法粘贴出来做讲解了,我这边尽量写注释,这里文章记录的主要目的还是帮助自己记录思路
昨天,我们完成了最简单的模板到DOM的实现,以及执行setData时候页面重新渲染工作,只不过比较粗暴还没有引入snabbdom进行了重新渲染,今天我们来完成其中的事件绑定部分代码
这里我们先不去管循环标签这些的解析,先完成事件绑定部分代码,这里如果只是想实现click绑定便直接在此处绑定事件即可:
1 class Element { 2 constructor(tagName, props, children, vm) { 3 this.tagName = tagName; 4 this.props = props; 5 this.children = children || []; 6 this.vm = vm.vm; 7 } 8 render() { 9 //拿着根节点往下面撸 10 let el = document.createElement(this.tagName); 11 let props = this.props.props; 12 let scope = this; 13 14 let events = this.props.on; 15 16 for(let name in props) { 17 el.setAttribute(name, props[name]); 18 } 19 20 for(name in events) { 21 let type = Object.keys(this.props.on); 22 type = type[0]; 23 el.addEventListener(type, function (e) { 24 scope.vm.$options.methods[scope.props.on[type]] && scope.vm.$options.methods[scope.props.on[type]].call(scope.vm, e); 25 }) 26 } 27 28 let children = this.children; 29 30 for(let i = 0, l = children.length; i < l; i++) { 31 let child = children[i]; 32 let childEl; 33 if(child instanceof Element) { 34 //递归调用 35 childEl = child.render(); 36 } else { 37 childEl = document.createTextNode(child); 38 } 39 el.append(childEl); 40 } 41 return el; 42 } 43 }
显然,这个不是我们要的最终代码,事实上,事件如何绑定dom如何比较差异渲染,我们这块不需要太多关系,我们只需要引入snabbdom即可,这里便来一起了解之
snabbdom
前面我们对snabbdom做了初步介绍,暂时看来MVVM框架就我这边学习的感觉有以下几个难点:
① 第一步的模板解析,这块很容易出错,但如果有志气jQuery源码的功底就会比较轻易
② 虚拟DOM这块,要对比两次dom树的差异再选择如何做
只要突破这两点,其他的就会相对简单一些,而这两块最难也最容易出错的工作,我们全部引用了第三方库HTMLParser和snabbdom,所以我们都碰上了好时代啊......
我们很容易将一个dom结构用js对象来抽象,比如我们之前做的班次列表中的列表排序:
这里出发的因子就有出发时间、耗时、价格,这里表示下就是:
1 let trainData = { 2 sortKet: 'time', //耗时,价格,发车时间等等方式排序 3 sortType: 1, //1升序,2倒叙 4 oData: [], //服务器给过来的原生数据 5 data: [], //当前筛选条件下的数据 6 }
这个对象有点缺陷就是不能与页面映射起来,我们之前的做法就算映射起来了,也只会跟一个跟节点做绑定关系,一旦数据发生变化便全部重新渲染,这个还是小问题,比较复杂的问题是半年后筛选条件增加,这个页面的代码可能会变得相当难维护,其中最难的点可能就是页面中的dom关系维护,和事件维护
而我们想要的就是数据改变了,DOM自己就发生变化,并且以高效的方式发生变化,这个就是我们snabbdom做的工作了,而之前我们用一段代码说明过这个问题:
var element = { tagName: 'ul', // 节点标签名 props: { // DOM的属性,用一个对象存储键值对 id: 'list' }, children: [ // 该节点的子节点 {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]}, {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]}, ] }
1 <ul id='list'> 2 <li class='item'>Item 1</li> 3 <li class='item'>Item 2</li> 4 <li class='item'>Item 3</li> 5 </ul>
真实的虚拟DOM会翻译为这样:
class Element { constructor(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; } } function el(tagName, props, children) { return new Element(tagName, props, children) } el('ul', {id: 'list'}, [ el('li', {class: 'item'}, ['Item 1']), el('li', {class: 'item'}, ['Item 2']), el('li', {class: 'item'}, ['Item 3']) ])
这里很快就能封装一个可运行的代码出来:
<!doctype html> <html> <head> <title>起步</title> </head> <body> <script type="text/javascript"> //***虚拟dom部分代码,后续会换成snabdom class Element { constructor(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; } render() { //拿着根节点往下面撸 let root = document.createElement(this.tagName); let props = this.props; for(let name in props) { root.setAttribute(name, props[name]); } let children = this.children; for(let i = 0, l = children.length; i < l; i++) { let child = children[i]; let childEl; if(child instanceof Element) { //递归调用 childEl = child.render(); } else { childEl = document.createTextNode(child); } root.append(childEl); } this.rootNode = root; return root; } } function el(tagName, props, children) { return new Element(tagName, props, children) } let vnode = el('ul', {id: 'list'}, [ el('li', {class: 'item'}, ['Item 1']), el('li', {class: 'item'}, ['Item 2']), el('li', {class: 'item'}, ['Item 3']) ]) let root = vnode.render(); document.body.appendChild(root); </script> </body> </html>
我们今天要做的事情,便是把这段代码写的更加完善一点,就要进入第二步,比较两颗虚拟树的差异了,而这块也是snabbdom的核心,当然也比较有难度啦
PS:这里借鉴:https://github.com/livoras/blog/issues/13
实际代码中,会对两棵树进行深度优先遍历,这样会给每个节点一个唯一的标志:
在深度优先遍历的时候,每到一个节点便与新的树进行对比,如果有差异就记录到一个对象中:
1 //遍历子树,用来做递归的 2 function diffChildren(oldNodeChildren, newNodeChildren, index, patches) { 3 4 let leftNode = null; 5 let curNodeIndex = index; 6 7 for(let i = 0, l = oldNodeChildren.length; i < l; i++) { 8 let child = oldNodeChildren[i]; 9 let newChild = newNodeChildren[i]; 10 11 //计算节点的标识 12 curNodeIndex = (leftNode && leftNode.count) ? curNodeIndex + leftNode.count + 1 : curNodeIndex + 1; 13 dfsWalk(child, newChild) 14 leftNode = child; 15 } 16 } 17 18 //对两棵树进行深度优先遍历,找出差异 19 function dfsWalk(oldNode, newNode, index, patches) { 20 //将两棵树的不同记录之 21 patches[index] = []; 22 diffChildren(oldNode.children, newNode.children, index, patches); 23 } 24 25 //对比两棵树的差异 26 function diff(oldTree, newTree) { 27 //当前节点标志 28 let index = 0; 29 //记录每个节点的差异 30 let patches = {}; 31 //深度优先遍历 32 return patches; 33 }
patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同
这里已经做好了工具流程遍历节点得出差异,而我们的差异有:
① 替换原来的节点,例如把div换成section
② 移动、删除、新增子节点,例如把p与ul顺序替换
③ 这个比较简单,修改节点属性
④ 这个也比较简单,修改文本内容
这里给这几种类型的定义:
let REPLACE = 0 let REORDER = 1 let PROPS = 2 let TEXT = 3
节点替换首先判断tagname是否一致即可:
patches[0] = [{ type: REPALCE, node: newNode // el('section', props, children) }]
如果给div新增属性,便记录之:
patches[0] = [{ type: REPALCE, node: newNode // el('section', props, children) }, { type: PROPS, props: { id: "container" } }]
如果是文本节点便记录之:
patches[2] = [{ type: TEXT, content: "Virtual DOM2" }]
以上都比较常规,不会做太大改变,情况比较多的是REODER(Reorder重新排列),比如将这里div的子节点顺序变成了div-p-ul,这个该如何对比,其实这个情况可能会直接被替换掉,这样DOM开销太大,这里牵扯到了列表对比算法,有点小复杂:
假如现在对英文字母进行排序,久的顺序:
a b c d e f g h i
然后对节点进行了一系列的操作,新增j节点,删除e节点,移动h节点,于是有了:
a b c h d f g i j
知道了新旧顺序,现在需要我们写一个算法计算最小插入、删除操作(移动是删除+插入),这块具体我们不深入,有兴趣移步至,这里代码,我们最终形成的结果是:
patches[0] = [{
type: REORDER,
moves: [{remove or insert}, {remove or insert}, ...]
}]
于是我们将这段寻找差异的代码放入前面的遍历代码:
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) } function dfsWalk (node, walker, patches) { var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异 var len = node.childNodes ? node.childNodes.length : 0 for (var i = 0; i < len; i++) { // 深度遍历子节点 var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } if (currentPatches) { applyPatches(node, currentPatches) // 对当前节点进行DOM操作 } } function applyPatches (node, currentPatches) { currentPatches.forEach(function (currentPatch) { switch (currentPatch.type) { case REPLACE: node.parentNode.replaceChild(currentPatch.node.render(), node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) }
这个就是我们snabbdom中重要的patch.js的实现,而Virtual DOM算法主要就是:
① 虚拟DOM element的定义
② 差异的定义与实现
③ 将差异部分代码补足形成新树的patch部分
// 1. 构建虚拟DOM var tree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: blue'}, ['simple virtal dom']), el('p', ['Hello, virtual-dom']), el('ul', [el('li')]) ]) // 2. 通过虚拟DOM构建真正的DOM var root = tree.render() document.body.appendChild(root) // 3. 生成新的虚拟DOM var newTree = el('div', {'id': 'container'}, [ el('h1', {style: 'color: red'}, ['simple virtal dom']), el('p', ['Hello, virtual-dom']), el('ul', [el('li'), el('li')]) ]) // 4. 比较两棵虚拟DOM树的不同 var patches = diff(tree, newTree) // 5. 在真正的DOM元素上应用变更 patch(root, patches)
有了以上知识,我们现在来开始使用snabbdom,相比会得心应手
应用snabbdom
var snabbdom = require("snabbdom"); var patch = snabbdom.init([ // 初始化补丁功能与选定的模块 require("snabbdom/modules/class").default, // 使切换class变得容易 require("snabbdom/modules/props").default, // 用于设置DOM元素的属性(注意区分props,attrs具体看snabbdom文档) require("snabbdom/modules/style").default, // 处理元素的style,支持动画 require("snabbdom/modules/eventlisteners").default, // 事件监听器 ]); //h是一个生成vnode的包装函数,factory模式?对生成vnode更精细的包装就是使用jsx //在工程里,我们通常使用webpack或者browserify对jsx编译 var h = require("snabbdom/h").default; // 用于创建vnode,VUE中render(createElement)的原形 var container = document.getElementById("container"); var vnode = h("div#container.two.classes", {on: {click: someFn}}, [ h("span", {style: {fontWeight: "bold"}}, "This is bold"), " and this is just normal text", h("a", {props: {href: "/foo"}}, "I\"ll take you places!") ]); // 第一次打补丁,用于渲染到页面,内部会建立关联关系,减少了创建oldvnode过程 patch(container, vnode); //创建新节点 var newVnode = h("div#container.two.classes", {on: {click: anotherEventHandler}}, [ h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"), " and this is still just normal text", h("a", {props: {href: "/bar"}}, "I\"ll take you places!") ]); //第二次比较,上一次vnode比较,打补丁到页面 //VUE的patch在nextTick中,开启异步队列,删除了不必要的patch //nextTick异步队列解析,下面文章中会详解 patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
这里可以看到,我们传入h的要求是什么样的格式,依次有什么属性,这里还是来做一个demo:
1 <div id="container"> 2 </div> 3 4 <script type="module"> 5 "use strict"; 6 import { patch, h, VNode } from './libs/vnode.js' 7 var container = document.getElementById("container"); 8 function someFn(){ console.log(1)} 9 function anotherEventHandler(){ console.log(2)} 10 11 var oldVnode = h("div", {on: {click: someFn}}, [ 12 h("span", {style: {fontWeight: "bold"}}, "This is bold"), 13 " and this is just normal text", 14 h("a", {props: {href: "/foo"}}, "I\"ll take you places!") 15 ]); 16 17 // 第一次打补丁,用于渲染到页面,内部会建立关联关系,减少了创建oldvnode过程 18 let diff = patch(container, oldVnode); 19 //创建新节点 20 var newVnode = h("div", {on: {click: anotherEventHandler}}, [ 21 h("span", {style: {fontWeight: "normal", fontStyle: "italic"}}, "This is now italic type"), 22 " and this is still just normal text", 23 h("a", {props: {href: "/bar"}}, "I\"ll take you places!") 24 ]); 25 //第二次比较,上一次vnode比较,打补丁到页面 26 //VUE的patch在nextTick中,开启异步队列,删除了不必要的patch 27 //nextTick异步队列解析,下面文章中会详解 28 patch(oldVnode, newVnode); // Snabbdom efficiently updates the old view to the new state 29 function test() { 30 return { 31 oldVnode,newVnode,container,diff 32 } 33 } 34 </script>
所以我们现在工作变得相对简单起来就是根据HTML模板封装虚拟DOM结构即可,如果不是我们其中存在指令系统甚至可以不用HTMLParser,所以我们改下之前的代码,将我们自己实现的丑陋vnode变成snabbdom,这里详情还是看github:https://github.com/yexiaochai/wxdemo/tree/master/mvvm。接下来,我们来解决其中的指令
指令系统
这里所谓的指令用的最多的也就是:
① if
② for
对应到小程序中就是:
<block wx:for="{{[1, 2, 3]}}"> <view> {{index}}: </view> <view> {{item}} </view> </block>
<block wx:if="{{true}}"> <view> view1 </view> <view> view2 </view> </block>
Vue中的语法是:
<ul id="example-1"> <li v-for="item in items"> {{ item.message }} </li> </ul>
<h1 v-if="ok">Yes</h1> <h1 v-else>No</h1>
大同小异,我们来看看如何处理这种代码,这里也开始进入数组对象的处理,这里便引入了指令系统,我们这里单独说下这块代码
框架里面的for或者if这种指令代码因为要要保证框架性,首先写的很分散,其次用起来也很绕,就很不好理解,所以这里需要单独拎出来说下
之前我们使用的模板一般就是js代码,直接被翻译为了js函数,比如这段代码:
<ul> <% for(let key in arr) { %> <li>...</li> <% } %> </ul>
会被大概翻译为这个样子:
var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');}; with(obj||{}){ __p+='<ul>\n '; for(let key in arr) { __p+='\n <li>...</li>\n '; } __p+='\n</ul>'; } return __p;
而MVVM类框架执行的是相同的逻辑,只不过代码实现上面因为要考虑映射关系就复杂的多了:
<ul> <li m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}} </li> </ul>
翻译后基本就是这个代码:
with (this) { debugger ;return _h('ul', {}, [_l((arr), function(val, key, index) { return _h('li', { attrs: { "m-for": '(val, key, index) in arr' } }, ["索引 " + _s(key + 1) + " :" + _s(val)]) })]) }
所有的这一切都是为了形成虚拟树结构,最终要的是这样的东西
所以指令是其中的工具,一个过程,帮助我们达到目的,为了帮助理解,我们这边单独抽一段代码出来说明这个问题,这里再强调一下指令系统在整体流程中的意义是:
我们最终目标是将模板转换为snabbdom中的vnode,这样他便能自己渲染,而这里的过程是
模板 => HTMLParser解析模板 => 框架element对象 => 解析框架element对象中的属性,这里包括指令 => 将属性包含的指令相关信息同步到element对象上(因为每个标签都会有element对象)=> 生成用于render的函数(其实就是将element转换为snabbdom可识别的对象) => 生成snabbdom树后,调用pacth即可完成渲染
所以指令系统在其中的意义便是:解析element中的指令对应的代码,方便后续生成render匿名函数罢了,这就是为什么指令系统的实现包含了两个方法:
① template2Vnode,这个事实上是将模板中与指令相关的信息放到element对象上方便后续vnode2render时候使用
② vnode2render,便是将之前存到element中与生成最终函数有关的字段拿出来拼接成函数字符串,调用的时候是在mvvm实例对象下,所以可以取到传入的data以及method
之所以设计的比较复杂是为了让大家方便新增自定义指令,这里仍然先上一段简单的说明性代码:
1 <!doctype html> 2 <html> 3 <head> 4 <title>指令系统演示</title> 5 </head> 6 <body> 7 8 <script type="module"> 9 10 //需要处理的模板,我们需要将他转换为虚拟dom vnode 11 let html = ` 12 <ul> 13 <li m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</li> 14 </ul> 15 ` 16 //这里我们为了降低学习成本将这段模板再做一次简化,变成这样 17 html = '<div m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</div>'; 18 19 20 //处理element元素生成render函数 21 function genElement(el) { 22 //这里如果有自定义指令也会被拿出来 23 if (!el.processed) { 24 //如果没有这个指令会递归调用 25 el.processed = true; 26 let hooks = el.vm.hooks; 27 for (let hkey in hooks) { 28 if (el[hkey] && hooks[hkey].vnode2render) { 29 return hooks[hkey].vnode2render(el, genElement); 30 } 31 } 32 } 33 //不带hook的情况,这个就是普通的标签 34 return nodir(el) 35 } 36 37 function nodir(el) { 38 let code 39 40 //转换子节点 41 const children = genChildren(el, true); 42 code = `_h('${el.tag}'${ 43 ',{}' 44 }${ 45 children ? `,${children}` : '' // children 46 })` 47 return code 48 } 49 50 function genChildren(el, checkSkip) { 51 const children = el.children 52 if (children.length) { 53 const el = children[0] 54 // 如果是v-for 55 if (children.length === 1 && el.for) { 56 return genElement(el) 57 } 58 const normalizationType = 0 59 return `[${children.map(genNode).join(',')}]${ 60 checkSkip 61 ? normalizationType ? `,${normalizationType}` : '' 62 : '' 63 }` 64 } 65 } 66 67 68 //将element转换为render函数 69 function compileToFunctions(el) { 70 let vm = el.vm; 71 let render = genElement(el); 72 73 render = `with(this){ debugger; return ${render}}`; 74 75 return new Function(render); 76 77 } 78 79 function genNode(node) { 80 if (node.type === 1) { 81 return genElement(node) 82 } else { 83 return genText(node) 84 } 85 } 86 87 function genText(text) { 88 return text.type === 2 ? text.expression : JSON.stringify(text.text) 89 } 90 91 //我们依旧定义个MVVM的类 92 class MVVM { 93 constructor(options) { 94 this.$data = options.data; 95 this.template = options.template; 96 97 //将data中的数据装填到实例上,以便后续函数组装使用 98 for(let name in this.$data) { 99 this[name] = this.$data[name]; 100 } 101 102 this.compile(); 103 104 } 105 106 //解析模板生成虚拟dom,这里是重要的一步将模板变成方法 107 compile() { 108 109 let element = this.html2Elment(); 110 this.element = element; 111 112 this.initHooks(); 113 this.setElDrictive(element); 114 //因为设置属性已经被我们手动做了这里便不需要处理了 115 116 let hooks = this.hooks; 117 //这里,我们需要将有的钩子执行,主要是为了处理指令 118 for(let hkey in hooks) { 119 //如果对象上面已经装载了这个指令,并且具有模板到node的函数定义则执行 120 //这里之所以需要模板上具有,因为对象数据需要在这里取 121 if(element[hkey] && hooks[hkey].template2Vnode) { 122 //调用这个钩子,事实上这个钩子要往对象实例上面加东西 123 //这个会将循环相关的指令,比如要循环的对象放到for字段,将值放到alias,将迭代器属性关键词放到iterator 124 hooks[hkey].template2Vnode(element, element[hkey], this); 125 } 126 } 127 128 //上面做了指令系统第一步,将模板中的属性存到element对应对象上,这里开始调用之 129 this.$render = compileToFunctions(element) 130 131 //执行渲染 132 let vnode = this.$render(); 133 134 console.log(html, element, vnode) 135 debugger; 136 137 } 138 139 140 initHooks() { 141 //需要处理的指令钩子,本来该放到prototype上 142 this.hooks = { 143 'for': { 144 template2Vnode: function (el, dir) { 145 //(val, key, index) in arr 146 let exp = dir.expression 147 148 //for in 或者 for of 这种循环 149 const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ 150 //取出迭代器关键词 151 const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ 152 153 //获取数组 154 //(key ,index) in arr 155 //[0] (key ,index) in arr,[1] (key ,index),[2] arr 156 const inMatch = exp.match(forAliasRE) 157 if (!inMatch) { 158 warn(`Invalid v-for expression: ${exp}`) 159 return 160 } 161 162 //上面的正则其实是为了取出迭代器中的字符串,后面好组装函数 163 //这里开始重新组装对象上的for指令,这里把循环的对象指向了数组关键词 164 el.for = inMatch[2].trim() 165 //(val, key, index) 166 let alias = inMatch[1].trim() 167 168 //关键词拿出来 169 const iteratorMatch = alias.match(forIteratorRE) 170 if (iteratorMatch) { 171 el.alias = iteratorMatch[1].trim(); 172 el.iterator1 = iteratorMatch[2].trim() 173 if (iteratorMatch[3]) { 174 el.iterator2 = iteratorMatch[3].trim() 175 } 176 } else { 177 el.alias = alias 178 } 179 180 }, 181 //将node对象转换为函数 182 //因为之前已经用上面的函数 183 //将循环相关的指令,比如要循环的对象放到for字段,将值放到alias,将迭代器属性关键词放到iterator 184 //所以这里直接取出关键词使用即可 185 vnode2render: function (el, genElement) { 186 //一个状态机 187 if(el.forProcessed) return null; 188 189 //取出相关属性 190 let exp = el.for; 191 let alias = el.alias; 192 193 //注意这个字符串里面的代码会执行,最新js语法 194 let iterator1 = el.iterator1 ? `,${el.iterator1}` : ''; 195 let iterator2 = el.iterator2 ? `,${el.iterator2}` : ''; 196 197 /* 198 输出 199 _l((arr), function(val,key,index) { 200 console.log(arguments); 201 }) 202 */ 203 let _render = ` _l((${exp}), function(${alias}${iterator1}${iterator2}) { 204 console.log(arguments); 205 return ${genElement(el)} 206 }) 207 ` 208 console.log('render', _render); 209 210 return _render 211 212 } 213 } 214 }; 215 } 216 217 //渲染for时,返回多个render 218 //因为_l调用的时候是处在mvvm实例作用域,所以这里传入的时候是一个数组 219 _l(val, render) { 220 let ret, i, l, keys, key 221 if (Array.isArray(val) || typeof val === 'string') { 222 ret = new Array(val.length) 223 for (i = 0, l = val.length; i < l; i++) { 224 ret[i] = render(val[i], i) 225 } 226 } else if (typeof val === 'number') { 227 ret = new Array(val) 228 for (i = 0; i < val; i++) { 229 ret[i] = render(i + 1, i) 230 } 231 } else if (isObject(val)) { 232 keys = Object.keys(val) 233 ret = new Array(keys.length) 234 for (i = 0, l = keys.length; i < l; i++) { 235 key = keys[i] 236 ret[i] = render(val[key], key, i) 237 } 238 } 239 return ret 240 } 241 242 _s(val) { 243 return val == null 244 ? '' 245 : typeof val === 'object' 246 ? JSON.stringify(val, null, 2) 247 : String(val) 248 } 249 250 _h(sel, data, children) { 251 252 debugger; 253 254 return 255 } 256 257 //解析指令 258 setElDrictive(el) { 259 //解析指令,这里主要是解析for与if 260 let attrs = el.attrs; 261 262 //判断m-xxx这种类型的正则 263 const drictiveRE = /^m\-(\w+)(\:[^\.]+)?\.?([^\:]+)?/ 264 265 for(let name in attrs) { 266 let darr = name.match(drictiveRE); 267 if(darr){ 268 269 //没有什么其他目的,就是将属性中的指令挪到对象上 270 el[darr[1]] = { 271 name: darr[1], 272 expression: attrs[name], 273 arg: darr[2] && darr[2].slice(1) 274 } 275 276 } 277 } 278 279 } 280 281 //将模板转换为js对象,这里要调用HTMLParser 282 html2Elment() { 283 //我们这里简化代码,直接返回解析后的结果即可 284 //...一大段调用htmlParser,包括递归调用生成js对象的过程,略 285 return { 286 vm: this, 287 tag: 'div', 288 attrs: { 289 'm-for': '(val, key, index) in arr' 290 }, 291 children: [ 292 { 293 type: 2, 294 text: '索引 {{key + 1}} :{{val}}', 295 expression: '"索引 "+_s(key + 1)+" :"+_s(val)' 296 } 297 ] 298 } 299 300 } 301 302 } 303 304 //然后我们在这里实例化即可 305 new MVVM({ 306 template: html, 307 data: { 308 arr: [ 309 '叶小钗', '素还真', '一页书' 310 ] 311 } 312 }) 313 314 </script> 315 </body> 316 </html>
这一大坨代码,是可运行的代码,其中打了很多断点写了很多注释,剔除了很多无用的代码,想要了解指令系统的朋友可以看看,这里如何自定义指令,大家也可以思考下是怎么实现的,今天的学习暂时到这里,我们明天来看看组件一块的实现