Skip to content
TANG edited this page Nov 7, 2017 · 35 revisions

mvvm.js 是一个非常轻量的 mvvm 库(仅 28kb), mvvmsugar 没有任何依赖,指令系统功能齐全,能满足基本的 mvvm 模式开发需求。

支持的指令列表:

指令名称采用 v- 的缩写主要是因为 vview 的首字母,代表着视图指令,还有就是,我也是个 Vue 粉 :-) ,虽然指令名称、功能和使用方法都和 Vue 很相似,但是实现的细节有很大不同。

实例化 MVVM

/*
 * <必选>  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();

MVVM 指令系统

以下所有指令的使用都可以在 jsfiddle 进行预览或修改测试

v-el

  • 说明:

    将指令所在的节点注册到数据模型对象中,方便通过 $els 访问这个元素

  • 示例:

    <div v-el="elm"></div>
    var els = vm.$els;
    els.elm.textContent = 'hello mvvm ~';

v-text

  • 说明:

    更新元素的 node.textContent 在元素标签内部使用 {{field}} 也将编译为一个 v-text 指令

  • 示例:

    <h1 v-text="title"></h1>
    <h2>The title is: {{ title }}.</h2>

v-html

  • 说明:

    更新元素的 node.innerHTML 布局内容不会进行指令的编译

  • 示例:

     <div v-html="html"></div>

v-show

  • 说明:

    根据指令值(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>

v-if

  • 说明:

    根据指令的值(Boolean)切换渲染元素,在切换时元素及它的数据绑定将会被销毁或重建,切换过程将会从文档移除该元素的所有子节点,v-if 也可以添加一个 v-else 兄弟节点进行 toggle 切换

  • 示例:v-show 用法相同

  • 注意:v-ifv-for 不能用于同一个节点,因为 DOM 移除会混乱,当需要在循环列表上销毁/重建一条数据应当直接操作数据即可。

v-else

  • 说明:

    参见 v-showv-if. 注意的是 v-else 不能单独使用,前一兄弟元素必须有 v-ifv-show 指令

v-model

  • 说明:

    在表单控件上实现双向绑定,只能够在input[type=text, password, radio, checkbox] selecttextarea 元素上使用。radio, checkboxselect 的选中值都是以数据为准。

  • 示例:见表单类型示例

v-model [text & textarea]

  • 示例:

     <input type="text" v-model="title">
     <textarea v-model="title"></textarea>

    text 和 textarea 都会通过监听元素 inputchange 事件来实现数据同步。如果不需要在 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>

v-model [radio]

  • 示例:

     <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

v-model [checkbox]

  • 示例:

    单个 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

v-model [select]

  • 示例:

    单选绑定到一个指定的字符 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

v-bind

  • 说明:

    动态绑定一个或多个 attribute 包括 classstyle 以及其他属性

  • 示例:

    绑定单个:

     <div v-bind:id="vid"></div>

    绑定多个属性的 Object 结构:

     <div v-bind="{id: vid, 'data-id': vdid, class: classObject, style: styleObject}"></div>

v-bind:class

  • 示例:

    绑定 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>

v-bind:style

  • 示例:

    绑定 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>

v-on

  • 说明:

    为元素绑定原生事件,支持 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>

v-for

  • 说明:

    构建基于源数据重复的动态列表,语法为: 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);
     }

v-pre

  • 说明

    跳过编译这个元素和它的所有子元素,跳过大量没有指令的节点可以提高编译的速度。

  • 示例

     <div v-pre>{{ this will not be compiled }}</div>

v-cloak

  • 说明

    提供一个标记编译状态的指令,与 CSS 一起使用可以在编译到达之前隐藏文本的 mustache 标记,该指令不做任何事,直到编译到这个元素才移除该指令。

  • 示例

     [v-cloak] {
     	display: none;
     }
     <div v-cloak>
     	{{ title }}
     	div will not show before compile
     </div>

v-custom

  • 说明

    自定义指令,该指令支持在实例化配置 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

  • 说明

    元素的指令以及该元素所有子元素的指令都只渲染一次(指令初始值),并且不会进行任何的数据绑定。如果确定元素的所有子节点的视图不会或不需要根据数据的改变而改变(相当于弃用数据驱动),用 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-hook

  • 说明

    v-ifv-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>