接上文:一套代码小程序&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绑定便直接在此处绑定事件即可:
class Element {
constructor(tagName, props, children, vm) {
this.tagName = tagName;
this.props = props;
this.children = children || [];
this.vm = vm.vm;
}
render() {
//拿着根节点往下面撸
let el = document.createElement(this.tagName);
let props = this.props.props;
let scope = this; let events = this.props.on; for(let name in props) {
el.setAttribute(name, props[name]);
} for(name in events) {
let type = Object.keys(this.props.on);
type = type[0];
el.addEventListener(type, function (e) {
scope.vm.$options.methods[scope.props.on[type]] && scope.vm.$options.methods[scope.props.on[type]].call(scope.vm, e);
})
} 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);
}
el.append(childEl);
}
return el;
}
}
显然,这个不是我们要的最终代码,事实上,事件如何绑定dom如何比较差异渲染,我们这块不需要太多关系,我们只需要引入snabbdom即可,这里便来一起了解之
snabbdom
前面我们对snabbdom做了初步介绍,暂时看来MVVM框架就我这边学习的感觉有以下几个难点:
① 第一步的模板解析,这块很容易出错,但如果有志气jQuery源码的功底就会比较轻易
② 虚拟DOM这块,要对比两次dom树的差异再选择如何做
只要突破这两点,其他的就会相对简单一些,而这两块最难也最容易出错的工作,我们全部引用了第三方库HTMLParser和snabbdom,所以我们都碰上了好时代啊......
我们很容易将一个dom结构用js对象来抽象,比如我们之前做的班次列表中的列表排序:
这里出发的因子就有出发时间、耗时、价格,这里表示下就是:
let trainData = {
sortKet: 'time', //耗时,价格,发车时间等等方式排序
sortType: 1, //1升序,2倒叙
oData: [], //服务器给过来的原生数据
data: [], //当前筛选条件下的数据
}
这个对象有点缺陷就是不能与页面映射起来,我们之前的做法就算映射起来了,也只会跟一个跟节点做绑定关系,一旦数据发生变化便全部重新渲染,这个还是小问题,比较复杂的问题是半年后筛选条件增加,这个页面的代码可能会变得相当难维护,其中最难的点可能就是页面中的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
实际代码中,会对两棵树进行深度优先遍历,这样会给每个节点一个唯一的标志:
在深度优先遍历的时候,每到一个节点便与新的树进行对比,如果有差异就记录到一个对象中:
//遍历子树,用来做递归的
function diffChildren(oldNodeChildren, newNodeChildren, index, patches) { let leftNode = null;
let curNodeIndex = index; for(let i = 0, l = oldNodeChildren.length; i < l; i++) {
let child = oldNodeChildren[i];
let newChild = newNodeChildren[i]; //计算节点的标识
curNodeIndex = (leftNode && leftNode.count) ? curNodeIndex + leftNode.count + 1 : curNodeIndex + 1;
dfsWalk(child, newChild)
leftNode = child;
}
} //对两棵树进行深度优先遍历,找出差异
function dfsWalk(oldNode, newNode, index, patches) {
//将两棵树的不同记录之
patches[index] = [];
diffChildren(oldNode.children, newNode.children, index, patches);
} //对比两棵树的差异
function diff(oldTree, newTree) {
//当前节点标志
let index = 0;
//记录每个节点的差异
let patches = {};
//深度优先遍历
return patches;
}
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:
<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>
所以我们现在工作变得相对简单起来就是根据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
之所以设计的比较复杂是为了让大家方便新增自定义指令,这里仍然先上一段简单的说明性代码:
<!doctype html>
<html>
<head>
<title>指令系统演示</title>
</head>
<body> <script type="module"> //需要处理的模板,我们需要将他转换为虚拟dom vnode
let html = `
<ul>
<li m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</li>
</ul>
`
//这里我们为了降低学习成本将这段模板再做一次简化,变成这样
html = '<div m-for="(val, key, index) in arr">索引 {{key + 1}} :{{val}}</div>'; //处理element元素生成render函数
function genElement(el) {
//这里如果有自定义指令也会被拿出来
if (!el.processed) {
//如果没有这个指令会递归调用
el.processed = true;
let hooks = el.vm.hooks;
for (let hkey in hooks) {
if (el[hkey] && hooks[hkey].vnode2render) {
return hooks[hkey].vnode2render(el, genElement);
}
}
}
//不带hook的情况,这个就是普通的标签
return nodir(el)
} function nodir(el) {
let code //转换子节点
const children = genChildren(el, true);
code = `_h('${el.tag}'${
',{}'
}${
children ? `,${children}` : '' // children
})`
return code
} function genChildren(el, checkSkip) {
const children = el.children
if (children.length) {
const el = children[0]
// 如果是v-for
if (children.length === 1 && el.for) {
return genElement(el)
}
const normalizationType = 0
return `[${children.map(genNode).join(',')}]${
checkSkip
? normalizationType ? `,${normalizationType}` : ''
: ''
}`
}
} //将element转换为render函数
function compileToFunctions(el) {
let vm = el.vm;
let render = genElement(el); render = `with(this){ debugger; return ${render}}`; return new Function(render); } function genNode(node) {
if (node.type === 1) {
return genElement(node)
} else {
return genText(node)
}
} function genText(text) {
return text.type === 2 ? text.expression : JSON.stringify(text.text)
} //我们依旧定义个MVVM的类
class MVVM {
constructor(options) {
this.$data = options.data;
this.template = options.template; //将data中的数据装填到实例上,以便后续函数组装使用
for(let name in this.$data) {
this[name] = this.$data[name];
} this.compile(); } //解析模板生成虚拟dom,这里是重要的一步将模板变成方法
compile() { let element = this.html2Elment();
this.element = element; this.initHooks();
this.setElDrictive(element);
//因为设置属性已经被我们手动做了这里便不需要处理了 let hooks = this.hooks;
//这里,我们需要将有的钩子执行,主要是为了处理指令
for(let hkey in hooks) {
//如果对象上面已经装载了这个指令,并且具有模板到node的函数定义则执行
//这里之所以需要模板上具有,因为对象数据需要在这里取
if(element[hkey] && hooks[hkey].template2Vnode) {
//调用这个钩子,事实上这个钩子要往对象实例上面加东西
//这个会将循环相关的指令,比如要循环的对象放到for字段,将值放到alias,将迭代器属性关键词放到iterator
hooks[hkey].template2Vnode(element, element[hkey], this);
}
} //上面做了指令系统第一步,将模板中的属性存到element对应对象上,这里开始调用之
this.$render = compileToFunctions(element) //执行渲染
let vnode = this.$render(); console.log(html, element, vnode)
debugger; } initHooks() {
//需要处理的指令钩子,本来该放到prototype上
this.hooks = {
'for': {
template2Vnode: function (el, dir) {
//(val, key, index) in arr
let exp = dir.expression //for in 或者 for of 这种循环
const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
//取出迭代器关键词
const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ //获取数组
//(key ,index) in arr
//[0] (key ,index) in arr,[1] (key ,index),[2] arr
const inMatch = exp.match(forAliasRE)
if (!inMatch) {
warn(`Invalid v-for expression: ${exp}`)
return
} //上面的正则其实是为了取出迭代器中的字符串,后面好组装函数
//这里开始重新组装对象上的for指令,这里把循环的对象指向了数组关键词
el.for = inMatch[2].trim()
//(val, key, index)
let alias = inMatch[1].trim() //关键词拿出来
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
el.alias = iteratorMatch[1].trim();
el.iterator1 = iteratorMatch[2].trim()
if (iteratorMatch[3]) {
el.iterator2 = iteratorMatch[3].trim()
}
} else {
el.alias = alias
} },
//将node对象转换为函数
//因为之前已经用上面的函数
//将循环相关的指令,比如要循环的对象放到for字段,将值放到alias,将迭代器属性关键词放到iterator
//所以这里直接取出关键词使用即可
vnode2render: function (el, genElement) {
//一个状态机
if(el.forProcessed) return null; //取出相关属性
let exp = el.for;
let alias = el.alias; //注意这个字符串里面的代码会执行,最新js语法
let iterator1 = el.iterator1 ? `,${el.iterator1}` : '';
let iterator2 = el.iterator2 ? `,${el.iterator2}` : ''; /*
输出
_l((arr), function(val,key,index) {
console.log(arguments);
})
*/
let _render = ` _l((${exp}), function(${alias}${iterator1}${iterator2}) {
console.log(arguments);
return ${genElement(el)}
})
`
console.log('render', _render); return _render }
}
};
} //渲染for时,返回多个render
//因为_l调用的时候是处在mvvm实例作用域,所以这里传入的时候是一个数组
_l(val, render) {
let ret, i, l, keys, key
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length)
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i)
}
} else if (typeof val === 'number') {
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
} else if (isObject(val)) {
keys = Object.keys(val)
ret = new Array(keys.length)
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i]
ret[i] = render(val[key], key, i)
}
}
return ret
} _s(val) {
return val == null
? ''
: typeof val === 'object'
? JSON.stringify(val, null, 2)
: String(val)
} _h(sel, data, children) { debugger; return
} //解析指令
setElDrictive(el) {
//解析指令,这里主要是解析for与if
let attrs = el.attrs; //判断m-xxx这种类型的正则
const drictiveRE = /^m\-(\w+)(\:[^\.]+)?\.?([^\:]+)?/ for(let name in attrs) {
let darr = name.match(drictiveRE);
if(darr){ //没有什么其他目的,就是将属性中的指令挪到对象上
el[darr[1]] = {
name: darr[1],
expression: attrs[name],
arg: darr[2] && darr[2].slice(1)
} }
} } //将模板转换为js对象,这里要调用HTMLParser
html2Elment() {
//我们这里简化代码,直接返回解析后的结果即可
//...一大段调用htmlParser,包括递归调用生成js对象的过程,略
return {
vm: this,
tag: 'div',
attrs: {
'm-for': '(val, key, index) in arr'
},
children: [
{
type: 2,
text: '索引 {{key + 1}} :{{val}}',
expression: '"索引 "+_s(key + 1)+" :"+_s(val)'
}
]
} } } //然后我们在这里实例化即可
new MVVM({
template: html,
data: {
arr: [
'叶小钗', '素还真', '一页书'
]
}
}) </script>
</body>
</html>
这一大坨代码,是可运行的代码,其中打了很多断点写了很多注释,剔除了很多无用的代码,想要了解指令系统的朋友可以看看,这里如何自定义指令,大家也可以思考下是怎么实现的,今天的学习暂时到这里,我们明天来看看组件一块的实现