-
Notifications
You must be signed in to change notification settings - Fork 60
MVVM
mvvm.js
是一个非常轻量的 mvvm 库(仅 28kb), mvvm
对 sugar
没有任何依赖,指令系统功能齐全,能满足基本的 mvvm 模式开发需求。
- v-el DOM 元素注册索引
- v-text 文本填充
- v-html 布局填充
- v-show 控制元素显示/隐藏
- v-if 控制元素内容渲染
- v-else v-show, v-if 的 else 板块
- v-model 双向数据绑定
- v-bind 属性绑定
- v-on 事件绑定
- v-for 循环列表
- v-pre 跳过编译
- v-cloak 编译到达状态
- v-custom 自定义指令
- v-once 禁用数据绑定指令
- v-hook v-if/v-for 节点更新钩子函数
指令名称采用 v-
的缩写主要是因为 v
是 view
的首字母,代表着视图指令,还有就是,我也是个 Vue 粉 :-) ,虽然指令名称、功能和使用方法都和 Vue 很相似,但是实现的细节有很大不同。
/*
* <必选> view 接受一个 DOM 元素,作为编译的目标元素
* <必选> model 接受一个对象作为数据模型,字段不能包含*字符(会被解析成乘号)
* <可选> computed 接受一个对象作为计算属性的声明,属性对应的必须是一个有返回值的取值函数
* <可选> methods 接受一个对象作为 v-on 事件函数的声明
* <可选> watches 接受一个对象作为批量 watch 的 callback 函数声明
* <可选> watchAll 接受一个函数作为整个 model 变化的回调
* <可选> customs 接受一个对象作为自定义指令刷新函数的声明
* <可选> hooks 接收一个对象作为 v-if/v-for 节点更新的钩子函数声明
* <可选> context 接受一个 v-on 事件、watches 回调函数和 watchAll 的 this 指向的执行上下文,如不指定则为 model
* <可选> lazy 接受一个布尔值告诉 MVVM 是否进行手动编译目标元素
*/
let vm = new MVVM({
view: element,
model: {
a: 1,
title: 'xxdk',
items: []
},
computed: {
b: function () {
// this 指向 model
return this.a + 1;
}
},
methods: {
vmClick: function () {
// this 为 context 字段指定的上下文,未指定则为 model
}
},
watches: {
// 简单 watch 直接传一个 callback 函数即可
title: function () {
// this 为 context 字段指定的上下文,未指定则为 model
},
// deep watch 传一个对象指定 handler 和 deep 参数
items: {
handler: function (newVal, oldVal, fromDeep) {
// this 为 context 字段指定的上下文,未指定则为 model
},
deep: true
}
},
watchAll: function (param, newVal, oldVal) {
// this 为 AppEdit 模块本身
// param.path 为变化值的访问路径
// param.action 数组操作的参数
},
customs: {
selfdir: function (initOrNewValue, oldVal) {
// this 指向指令对象, this.el 指向该自定义指令所在的元素节点
}
},
hooks: {
afterAppend: function (el, index, guid) {
// 当用在 v-for 时,el 是每一段列表选项,index 是下标 guid 是有 vfor 内部生成的一段选项唯一 id
},
beforeRemove: function (el, isElse) {
// 当用在 v-if 时,el 是 if 节点,isElse 是否是 else 板块
}
},
context: this // methods 和 watches 中的回调上下文
});
vm.$data // 数据模型对象
// 返回整个数据模型(即 vm.$data)保持引用关系
vm.get();
// 返回单个数据,层级用 . 分隔表示
vm.get('title');
// 与 vm.get() 相同,只是返回数据模型的副本,不保持引用关系
vm.getCopy();
// 设置单个 vm 数据
vm.set('title', 'New title');
// 同时设置多个 vm
vm.set({
'title': 'xxx',
'items': [...]
});
// 重置单个 vm 为初始状态
vm.reset('title');
// 重置整个 vm 为初始状态
vm.reset();
// 实现对数据的观察,callback 参数返回 [newValue, oldValue, fromDeep, args]
// deep 为 true 时则会观察对象或数组深层的变化,比如 v-for 数组的所有子项的变化
vm.watch(expression, callback, deep);
// 手动挂载编译,当 lazy 为 true 或者后期需要重新编译时调用
vm.mount();
// 销毁函数,销毁数据对象和视图
vm.destroy();
以下所有指令的使用都可以在 jsfiddle 进行预览或修改测试
-
说明:
将指令所在的节点注册到数据模型对象中,方便通过
$els
访问这个元素 -
示例:
<div v-el="elm"></div>
var els = vm.$els; els.elm.textContent = 'hello mvvm ~';
-
说明:
更新元素的
node.textContent
在元素标签内部使用{{field}}
也将编译为一个v-text
指令 -
示例:
<h1 v-text="title"></h1> <h2>The title is: {{ title }}.</h2>
-
说明:
更新元素的
node.innerHTML
布局内容不会进行指令的编译 -
示例:
<div v-html="html"></div>
-
说明:
根据指令值(Boolean)来切换显示/隐藏元素的
style.display
值,隐藏统一设为none
由隐藏变更为显示状态的时候如果元素有行内样式设置了display
的显示值将自动切换回原来的行内值,切换过程仍旧在文档中保留该元素,v-show
可以添加一个v-else
兄弟节点进行 toggle 切换 -
示例:
<h1 v-show="showTitle">big title</h1> <div v-show="showGreet">Nice to meet you !</div> <div v-else>Same here ~</div>
-
说明:
根据指令的值(Boolean)切换渲染元素,在切换时元素及它的数据绑定将会被销毁或重建,切换过程将会从文档移除该元素的所有子节点,
v-if
也可以添加一个v-else
兄弟节点进行 toggle 切换 -
示例:与
v-show
用法相同 -
注意:
v-if
和v-for
不能用于同一个节点,因为 DOM 移除会混乱,当需要在循环列表上销毁/重建一条数据应当直接操作数据即可。
-
说明:
参见
v-show
与v-if
. 注意的是v-else
不能单独使用,前一兄弟元素必须有v-if
或v-show
指令
-
说明:
在表单控件上实现双向绑定,只能够在
input[type=text, password, radio, checkbox]
select
和textarea
元素上使用。radio
,checkbox
和select
的选中值都是以数据为准。 -
示例:见表单类型示例
-
示例:
<input type="text" v-model="title"> <textarea v-model="title"></textarea>
text 和 textarea 都会通过监听元素
input
和change
事件来实现数据同步。如果不需要在input
中同步数据,而是希望在change
中同步数据的情况可以增加一个lazy
参数实现;如果需要input
事件有延迟更新,可以通过debounce
参数设置一个延迟时间:<!-- 只有在 change 失去焦点时才会更新 model --> <input type="text" v-model="title" lazy> <!-- 在用户每次输入后的 500ms 才会更新 model --> <input type="text" v-model="desciption" debounce="500">
此外,还可以加上
trim
属性来移除输入字符的前后空格:<input type="text" v-model="title" trim>
-
示例:
<label> <input type="radio" v-model="sex" value="boy"> 男生 </label> <label> <input type="radio" v-model="sex" value="girl"> 女生 </label> <br/> <div>Your Sex is: {{ sex }}</div>
由于表单 value 的值永远是字符串,所以绑定数值都默认是
String
类型的:<label> <input type="radio" v-model="sel" value="123"> Radio </label>
vm.$data.sel === '123' // true
如果希望数据模型中的值对应为为
Number
类型,只需要在表单加上number
属性即可:<label> <input type="radio" v-model="sel" value="123" number> Radio </label>
vm.$data.sel === 123 // true
-
示例:
单个 checkbox 绑定一个布尔值:
{ isCheck: true }
<label> <input type="checkbox" v-model="isCheck"> Checked </label> <br/> <div>Selected: {{ isCheck }}</div>
多个 checkbox 绑定到一个数组:
phones: ['apple', 'mzx']
<label> <input type="checkbox" value="apple" v-model="phones"> 苹果 </label> <label> <input type="checkbox" value="xmi" v-model="phones"> 小米 </label> <label> <input type="checkbox" value="mzx" v-model="phones"> 魅族 </label> <br/> <div>Selected: {{ phones }}</div>
同样的,需要数据模型转换为
Number
类型的只需在表单元素加上number
属性即可,参见radio
-
示例:
单选绑定到一个指定的字符 value:
selected: 'apple'
<select v-model="selected"> <option value="apple">苹果</option> <option value="xmi">小米</option> <option value="mzx">魅族</option> </select> <span>Selected: {{ selected }}</span>
多选绑定到一个数组:
selected: ['apple', 'mzx']
<select v-model="selected" multiple> <option value="apple">苹果</option> <option value="xmi">小米</option> <option value="mzx">魅族</option> </select> <span>Selected: {{ selected }}</span>
同样的,需要数据模型转换为
Number
类型的只需在表单元素加上number
属性即可,参见radio
-
说明:
动态绑定一个或多个
attribute
包括class
、style
以及其他属性 -
示例:
绑定单个:
<div v-bind:id="vid"></div>
绑定多个属性的 Object 结构:
<div v-bind="{id: vid, 'data-id': vdid, class: classObject, style: styleObject}"></div>
-
示例:
绑定
class
到一个字段,通过vm.$data.cls = 'class'
切换class
:<div class="static" v-bind:class="cls"></div>
绑定
class
到一个数组:{ 'classA': 'oneClass', 'classB': 'twoClass' }
<div v-bind:class="[classA, classB]"></div>
绑定
class
到一个对象,键名作为 classname,键值作为是否添加的布尔值:'classObject': { 'class-a': true, 'class-b': false }
<div v-bind:class="classObject"></div>
绑定多个
class
的 JSON 结构:{ 'isA': true, 'isB': false }
<div v-bind:class="{'class-a': isA, 'class-b': isB}"></div>
-
示例:
绑定
style
到一个对象,键名作为property
键值作为value
:'styleObject': { 'display': 'none', 'padding': '10px' }
<div v-bind:style="styleObject"></div>
绑定
style
的 JSON 结构:<div v-bind:style="{'display': vdsp, 'border': vbrd, 'padding': vpd}"></div>
-
说明:
为元素绑定原生事件,支持 6 种事件修饰符:
-
.self
只在当前元素触发 -
.stop
阻止冒泡 -
.prevent
阻止默认事件 -
.capture
使用捕获 -
.keyCode
键盘事件指定按键 -
.one
绑定的事件只触发一次
可以在事件名后指定事件回调的参数(默认为原生事件对象 e ),比如
$event
替换为 e 。 -
-
示例:
使用事件修饰符:
<div class="outSide" v-on:click.self="clickOutside"> <p class="inSide" v-on:click.stop="clickInside"> <input type="checkbox" v-on:click.stop.prevent="clickCheckbox"> 点击不会勾上也不会冒泡 </p> </div>
指定按下 enter 键触发
<input type="text" v-on:keyup.13="pressEnter"/>
使用内联语句传递自定义参数:
<button v-on:click="clickBtn($event, 123, 'abc')"></button>
使用内联语句可以支持更多的逻辑处理,不必去新定义一个事件函数:
<span>{{ title }}</span> <button v-on:click="if (ok) { title = 'success' } else { title = 'failed' }">Change Status</button>
<button v-on:click="$event.target.textContent = Math.random()">Change text</button>
注意:使用内联语句的时候
v-on
内部将生成一个执行上下文为数据模型model
的匿名函数作为事件的监听器。一次绑定多个事件(这种情况下不支持传参和指定事件修饰符):
<button v-on="{click: clickBtn, mouseover: overBtn}"></button>
在
v-for
列表中的元素支持将绑定事件指定为一个删除事件$remove
标识来删除列表选项:<ul> <li v-for="item of items"> <span v-on:click="$remove"></span> </li> </ul>
-
说明:
构建基于源数据重复的动态列表,语法为:
alias in/of iterator
支持push, pop, shift, unshift, splice, sort, reverse
数组操作,支持内嵌指令,嵌套 v-for ,在循环作用域内$index
用在其他指令将会替换为当前选项的下标(Number)列表数组操作中
push, pop, shift, unshift, splice
会尽量少的 dom 操作来更新列表视图(部分更新),而sort, reverse
以及替换整个数组时将会重新编译整个 vfor 循环列表(暂时没做片段缓存)另外,观察数组添加了两个变异方法:
$set(index, value)
(对应的 dom 片段会销毁并重新构建)和$remove(item)
方便数组的设值和删除需要注意的是,应用了
v-for
指令的节点如果含有其他指令,则优先编译v-for
指令 -
示例:
循环列表及嵌套循环列表:
items: [ { 'text': 'aaa', 'sps': [ {'text': 'aaa111'}, {'text': 'aaa222'} ] }, { 'text': 'bbb', 'sps': [ {'text': 'bbb111'}, {'text': 'bbb222'} ] } ]
<ul> <li v-for="item in items"> <b v-text="item.text"></b> <span v-for="sp in item.sps"> <i v-text="sp.text"></i> <i v-text="item.text"></i> </span> </li> </ul>
动态的 option 的 select 表单:
<select v-model="selected"> <option v-for="option in options" v-bind:value="option.value"> {{ option.text }} </option> </select>
$set
的用法:vm.items.$set(1, {'text': 'aaa'});
$remove
的用法:<ul> <li v-for="item in items"> <span v-on:click="eventRemove(item)">×</span> </li> </ul>
eventRemove: function (item) { vm.items.$remove(item); }
-
说明
跳过编译这个元素和它的所有子元素,跳过大量没有指令的节点可以提高编译的速度。
-
示例
<div v-pre>{{ this will not be compiled }}</div>
-
说明
提供一个标记编译状态的指令,与 CSS 一起使用可以在编译到达之前隐藏文本的 mustache 标记,该指令不做任何事,直到编译到这个元素才移除该指令。
-
示例
[v-cloak] { display: none; }
<div v-cloak> {{ title }} div will not show before compile </div>
-
说明
自定义指令,该指令支持在实例化配置
customs
中指定一个刷新函数来实现特定的功能,刷新函数会在指令初始化时调用一次,如果指令的依赖(指令值)发生变化,该刷新函数会再次被调用。(定义指令名称需要格外注意的是:元素属性会自动将大写字母转成小写字母) -
示例(利用自定义指令实现字符串自动重复 3 次的功能)
<div id="demo"> <span v-custom:repeat_text="text"></span> </div>
new MVVM({ 'view': document.querySelector('#demo'), 'model': { 'text': 'abc' }, 'customs': { // 刷新函数的作用域为指令的解析实例 // this.el 为指令所在的元素节点 // 参数 initOrNewVal 为解析初始值或依赖变更后的新值 'repeat_text': function refreshFunc (initOrNewVal, oldVal) { // es6 String.repeat(times) https://github.com/DrkSephy/es6-cheatsheet#repeat- this.el.textContent = initOrNewVal.repeat(3); } } });
最终解析结果为:
<div id="demo"> <span>abcabcabc</span> </div>
-
说明
元素的指令以及该元素所有子元素的指令都只渲染一次(指令初始值),并且不会进行任何的数据绑定。如果确定元素的所有子节点的视图不会或不需要根据数据的改变而改变(相当于弃用数据驱动),用
v-once
可以减少部分数据绑定带来的内存占用。 -
示例
<div v-once> <span>This will never change! {{ title }}</span> <input type="text" v-model="text"> </div> <h1 v-once v-on:click="clickTitle('text', 123)">{{ text }}</h1>
-
说明
v-if
和v-for
都涉及到 DOM 片段的移除和添加,v-hook
的作用是在片段移除之前或添加之后调用相应的处理钩子 -
示例
hooks: { afterItemAppend: function (el, index, guid) { // 当用在 v-for 时,el 为当前添加的列表选项,index 为下标,guid 为每一段的唯一 id // afterItemAppend 会在片段编译完成并且添加到列表文档后调用 }, beforeItemRemove: function (el, index, guid) { // el 为即将被移除列表选项,index 为移除之前的下标,guid 为被移除片段的唯一 id // beforeItemRemove 会在片段被 vm 从父节点移除之前调用 }, afterRender: function (el, isElse) { // 当用在 v-if 时,el 为 if 节点,isElse 为否是 else 板块被渲染 // afterRender 会在 el 被 vm 编译并渲染完成之后调用 }, beforeRender: function (el, isElse) { // beforeRender 会在 el 被 vm 清空销毁之前调用 } }
<ul> <li v-for="item in items" v-hook:after="afterItemAppend" v-hook:before="beforeItemRemove"></li> </ul> <div v-if="show" v-hook:after="afterRender" v-hook:before="beforeRender">xxdk</div> <div v-else>txgc</div>