iview中-formtable-渲染的问题

描述需求在表格里面渲染form表单,数字保留2位小数,不足的自动补齐。 选用选用的是 iview 的组件 Form 、Input、Table。 Form表单的数据可以双向绑定,这样在对输入的数据做补零操作后,更新Form表单的model绑定的数据,即可更新。 没有使用 InputNumber 是因为它的交互用户体验不是很友好,而选用了常用的Input。但问题不是出在这里,请继续往下看。 form+tabletest.vue<template> <div> <Form ref="formTable" :model="formTable" :rules="rulesTable" inline > <Table border :columns="columns" :data="data"></Table> </Form> </div></template><script> let Trim=function(str,isGlobal) { let result; result = str.replace(/(^\s+)|(\s+$)/g,""); if(isGlobal === true) result = result.replace(/\s/g,""); return result; }; import FormItemInput from './form-item-input.vue'; export default { name:'test', components: {FormItemInput}, data() { return { //+++ form 表单 start formTable:{}, rulesTable:{}, //+++ form 表单 end // +++++++ table start data:[ { "id":1, "name":"张三", "score":"" }, { "id":2, "name":"李四", "score":"" } ], columns:[ { title: '序号', type: 'index', width: 60, align: 'center' }, { title: '姓名', key: 'name', width: 200, ellipsis:true }, { title: '成绩', key: 'score', width: 150, className:'custom-table-form', ellipsis:true, renderHeader:(h, params) => { // 2019年1月4日11:01:36 // 必填 需要展示 * return h('div', [ h('i', { style: { color: 'red', marginRight: '5px' } }, '*'), h('span', {}, params.column.title) ]); }, render:(h, params)=>{ console.log('行数据',params.index, params); /** * 2019年5月14日17:08:32 添加校验 */ let _formKey=params.column.key+'_'+params.row.id; this.rulesTable[_formKey]=[]; // 2019年6月6日11:09:32 这步会导致,数据渲染两遍 this.$set(this.$data.formTable, _formKey, params.row[params.column.key]===undefined || params.row[params.column.key]===null ? '' : params.row[params.column.key]); this.rulesTable[_formKey].push( { /** * 清掉input输入的空格,如果为空,不能通过 */ validator(rule, value, callback, source, options) { let errors = []; if (!Trim(value) ){ errors.push( new Error( "该项是必填项")); } callback(errors); }, trigger: 'blur' }, { validator(rule, value, callback, source, options) { let errors = []; // 正则控制范围,比较大小,同时存在才为true let _tmpValue= value && Number(value); // 可以出现一项:100分,或者n项 几分~几十分 let reg= /^([1-9]?[0-9]{1,2})?\0?(\.\d{1,2})?$/; if(!(reg.test(_tmpValue)&&0<=_tmpValue&&_tmpValue<=100)) { errors.push( new Error("请输入0-100之间的数字,小数点后最多允许保留2位小数") ); } callback(errors); },type:'string', trigger: 'blur' } ); return h(FormItemInput, { props:{ formTable:this.formTable, formKey:_formKey, }, on:{ 'on-form-blur':(value)=>{ // 更新提交数据 this.$refs.formTable.validateField(_formKey, (message)=>{ //2019年5月16日10:12:08 校验通过以后,保留2位小数 if(message.length==0) { this.formTable[_formKey]=Number(value).toFixed(2); console.log('formTable',this.formTable); } }); } } }); } }, ], // +++++++ table end }; }, mounted() { }, methods: {} }</script>form-item-input.vue<template> <Form-item :prop="formKey" @on-form-blur="onFormBlur" :required="required" :label="label" :setLengthNumber="setLengthNumber" :lastFormItem="lastFormItem"> <Input v-model="formTable[formKey]" ></Input> </Form-item></template><script> export default { props:{ required:{ type:Boolean, default:false }, label:{ type:String, default:'' }, formTable:Object, formKey:String, }, methods: { onFormBlur(value){ this.$emit('on-form-blur', value); } } }</script>图示 ...

June 6, 2019 · 2 min · jiezi

Vue 中父传子组件传值,开发项目中总会遇到这样或那样的坑,作为前端小白,每天都在填坑中度过。。。

第一种情况: 简单传值,子组件只负责显示;父组件向子组件传递一个hello word; 子组件接收并显示出来;父组件Father.vue<template> <div id=“app”> <Child :msg = “message”/> </div></template><script>import Child from ‘./Child’export default { name: ‘App’, data() { return { message: "" } }, components: { Child }, mounted() { this.timer = setTimeout(() => { this.message = “hello world”; }, 3000) }}</script>子组件Child.vue<template> <div> {{msg}} </div></template><script>export default { name: ‘App’, props: [ “msg” ]}</script><style></style>第二种情况:父组件向子组件传值,子组件接收传值并修改传值并显示到页面上;我们可以使用watch 监听值的改变,当然我们也可以使用计算属性;我们先来说第一种使用watch 监听:父组件不变;子组件Child.vue<template> <div> {{childMsg}} </div></template><script>export default { name: ‘App’, props: [ “msg” ], data() { return { childMsg: "" } }, watch: { msg(newVal) { if(newVal !== “”) { this.childMsg = newVal + “—-child”; } } }}</script><style></style>说完了监听的,我们再来说一说计算属性的;父组件不变子组件Child.vue<template> <div> {{childMsg}} </div></template><script>export default { name: ‘App’, props: [ “msg” ], computed: { childMsg() { return this.msg + “—-child”; } }}</script><style></style>当然我们可以给计算属性设置get,和set 方法,我在项目中就用到了这种方法,父组件传递给子组件一个对象,在子组件里接收这个对象,并修改这个对象;父组件Father.vue<template> <div id=“app”> <Child :value = “data”/> </div></template><script>import Child from ‘./Child’export default { name: ‘App’, data() { return { data: null } }, components: { Child }, mounted() { this.timer = setTimeout(() => { this.data = { “name”: “south Joe”, “age”: 16, } }, 3000) }}</script>子组件Child.vue<template> <div> {{message}} <button @click=“handleModify”>修改年龄,姓名</button> </div> </template><script>export default { name: ‘App’, props: [ “value” ], computed: { message: { get: function() { if(this.value) { return [this.value.name,this.value.age] } }, set: function(newValue) { this.value.name = newValue[0]; this.value.age = newValue[1]; } } }, methods: { handleModify() { this.message = [“yangyang”,17]; } }}</script><style></style>职场小白south Joe,望各位大神批评指正,祝大家学习愉快! ...

April 4, 2019 · 1 min · jiezi

数据双向绑定的探究和实现

data-binding简单版vue的双向绑定实现成果图vue实现上诉成果用vue来实现其实很简单,我们先创建一个文件夹v0,在v0文件夹内见一个index.html:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>data-binding</title></head><style> #app { text-align: center; }</style><body> <div id=“app”> <h2>{{title}}</h2> <input v-model=“name”> <h1>{{name}}</h1> <button v-on:click=“clickMe”>click me!</button> </div></body><!– vue源码cdn地址 https://cn.vuejs.org/v2/guide/ –><script src=“https://cdn.jsdelivr.net/npm/vue"></script><script type=“text/javascript”> new Vue({ el: ‘#app’, data: { title: ‘hello world’, name: ‘yujiyang’ }, methods: { clickMe: function () { this.title = ‘hello world’; } }, mounted: function () { window.setTimeout(() => { this.title = ‘你好’; }, 1000); } });</script></html>那么vue是怎么实现的呢?数据双向绑定{{ }}双括号变量解析v-model和事件指令解析现在从简到难一步一步来实现这个’vue’。ps:由于本文只是为了学习和分享,所以只是简单实现下原理,并没有考虑太多情况和设计,如果大家有什么建议,欢迎提出来。vue数据双向绑定原理看过vue的源码的同学都知道,vue是通过数据劫持结合发布者-订阅者模式的方式来实现的~首先来看下vue是如何进行数据劫持的?new Vue({ el: ‘#app’, data: { message: { name:‘yjy’, } }, created: function () { console.log(this.message); }});结果:我们可以看到属性name有两个相对应的get和set方法,为什么会多出这两个方法呢?因为vue是通过Object.defineProperty()来实现数据劫持的。Object.defineProperty( )是用来做什么的?它可以来控制一个对象属性的一些特有操作,比如读写权、是否可以枚举,这里我们主要先来研究下它对应的两个描述属性get和set,如果还不熟悉其用法,请点击这里阅读更多用法。举个例子:var People = { name: ‘jiyang’};console.log(People.name); // yjy如果我们想要在执行console.log(People.name)的同时,直接给名字加个姓,那要怎么处理呢?或者说要通过什么监听对象People的属性值。这时候Object.defineProperty( )就派上用场了,代码如下:var People = {}var name = ‘’;Object.defineProperty(People, ’name’, { set: function (value) { name = value; console.log(‘你取了一个名叫做’ + value); }, get: function () { return ‘yu’ + name }}) People.name = ‘jiyang’; // 你取了一个名叫做jiyangconsole.log(People.name); // yujiyang我们通过Object.defineProperty( )设置了对象People的name属性,对其get和set进行重写操作,顾名思义,get就是在读取name属性这个值触发的函数,set就是在设置name属性这个值触发的函数,所以当执行 People.name = ‘jiyang’ 这个语句时,控制台会打印出 “你取了一个名叫做jiyang”,紧接着,当读取这个属性时,就会输出 “yujiyang”,因为我们在get函数里面对该值做了加工了。如果这个时候我们执行下下面的语句,控制台会输出什么?console.log(People)结果:是不是跟我们在上面打印vue数据长得有点类似,这也进一步验证vue确实是通过这种方法来进行数据劫持的。接下来我们通过其原理来实现一个简单版的mvvm双向绑定代码。mvvm思路分析实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据:view更新data其实可以通过事件监听即可,比如input标签监听 ‘input’ 事件就可以实现了。所以我们着重来分析下,当数据改变,如何更新视图的?其实上文我们已经给出答案了,就是通过Object.defineProperty()对属性设置一个set函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面就可以实现data更新view了。现在思路有了,接下去就是实现过程了。实现过程监听器Observer首先我们需要实现一个数据监听器Observer,用来监听所有属性,如果属性发上变化了,就执行相应方法。function defineReactive(data, key, val) { observe(val); // 递归遍历所有子属性 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { return val; }, set: function(newVal) { val = newVal; console.log(‘属性’ + key + ‘已经被监听了,现在值为:“’ + newVal.toString() + ‘”’); } });} function observe(data) { if (!data || typeof data !== ‘object’) { return; } Object.keys(data).forEach(function(key) { defineReactive(data, key, data[key]); });}; var people = { person1: { name: ’’ }, person2: ‘’};observe(people);people.person1.name = ‘yjy’; // 属性name已经被监听了,现在值为:“yjy”people.person2 = ‘没有此人’; // 属性person2已经被监听了,现在值为:“没有此人”订阅器Dep与订阅者Watcher由于一个属性的getter可能在多处触发(也就是被多个dom使用),所以属性setter的时候需要执行多个改变view的方法,我们把通知执行一个改变view的方法抽象成一个订阅者Watcher。有一个容器来专门收集Watcher,叫做订阅器Dep。Watcher初始化的时候,添加一个Watcher到Dep。属性每次setter的时候,执行Dep中所有Watcher。有了这个思路我们结合observer,来实现一下Dep和Watcher// observer.js// observer+depfunction Observer(data) { this.data = data; this.walk(data);}Observer.prototype = { walk: function(data) { var self = this; Object.keys(data).forEach(function(key) { self.defineReactive(data, key, data[key]); }); }, defineReactive: function(data, key, val) { var dep = new Dep(); var childObj = observe(val); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function() { if (Dep.target) { dep.addSub(Dep.target); } return val; }, set: function(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); } }); }};function observe(value, vm) { if (!value || typeof value !== ‘object’) { return; } return new Observer(value);};function Dep () { this.subs = [];}Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); }};Dep.target = null;// watcher.jsfunction Watcher(vm, exp, cb) { this.cb = cb; this.vm = vm; this.exp = exp; this.value = this.get(); // 将自己添加到订阅器的操作}Watcher.prototype = { update: function() { this.run(); }, run: function() { var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } }, get: function() { Dep.target = this; // 缓存自己 var value = this.vm.data[this.exp] // 强制执行监听器里的get函数 Dep.target = null; // 释放自己 return value; }};‘vue’的v1版本了解了Observer、Dep、Watcher后,我们只要将Observer和Watcher关联起来,就可以实现一个简单的数据双向绑定了。因为这里没有还没有设计解析器Compile,所以对于模板绑定的属性数据,我们都进行写死处理,假设模板上有一个节点,且id号为’name’,并且双向绑定的绑定的变量也为’name’,且是通过两个大双括号包起来(这里只是为了掩饰,暂时没什么用处)。先建立一个文件夹v1,目录结构如下:observer和watcher我们已经实现,还需要实现一个关联Observer和Watcher的index.js和模板index.html<!–index.html–><!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>self-vue</title></head><style> #name { text-align: center; }</style><body> <h1 id=“name”></h1></body><script src=“js/observer.js”></script><script src=“js/watcher.js”></script><script src=“js/index.js”></script><script type=“text/javascript”> var ele = document.querySelector(’#name’); var vue = new Vue({ name: ‘hello world’ }, ele, ’name’); window.setTimeout(function () { console.log(’name值改变了’); vue.name = ‘yjy’; }, 2000);</script></html>// index.jsfunction Vue (data, el, exp) { var self = this; this.data = data; // 知道为什么还要这个操作??? Object.keys(data).forEach(function(key) { self.proxyKeys(key); }); observe(data); el.innerHTML = this.data[exp]; // 初始化模板数据的值 new Watcher(this, exp, function (value) { el.innerHTML = value; }); return this;}Vue.prototype = { proxyKeys: function (key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; } }); }}解析器Compile虽然上面已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析dom节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器Compile来做解析和绑定工作。解析器Compile实现步骤:解析模板指令,并替换模板数据,初始化视图将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器为了解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,因此这个环节需要对dom操作比较频繁,所有可以先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理:function nodeToFragment (el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild } return fragment;}接下来需要遍历各个节点,对含有相关指定的节点进行特殊处理,这里咱们先处理最简单的情况,只对带有 ‘{{变量}}’ 这种形式的指令进行处理,后面再考虑更多指令情况:function compileElement (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /{{(.)}}/; var text = node.textContent; //获取该节点以及其子节点所包含文本内容 if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令且节点类型是文本类型 self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); // 继续递归遍历子节点 } });},function compileText (node, exp) { var self = this; var initText = this.vm[exp]; this.updateText(node, initText); // 将初始化的数据初始化到视图中 new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数 self.updateText(node, value); });},function updateText (node, value) { node.textContent = typeof value == ‘undefined’ ? ’’ : value;},function isTextNode (node) { return node.nodeType == 3;}获取到最外层节点后,调用compileElement函数,对所有子节点进行判断,如果节点是文本节点且匹配{{}}这种形式指令的节点就开始进行编译处理,编译处理首先需要初始化视图数据,对应上面所说的步骤1,接下去需要生成一个并绑定更新函数的订阅器,对应上面所说的步骤2。这样就完成指令的解析、初始化、编译三个过程,一个解析器Compile也就可以正常的工作了。为了将解析器Compile与监听器Observer和订阅者Watcher关联起来,我们需要再修改一下‘Vue类’:// index.jsfunction Vue (options) { var self = this; this.vm = this; this.data = options.data; Object.keys(this.data).forEach(function(key) { self.proxyKeys(key); }); observe(this.data); new Compile(options.el, this.vm); // 初始化视图和watcher初始化都封装在compile里面 return this;}Vue.prototype = { proxyKeys: function (key) { var self = this; Object.defineProperty(this, key, { enumerable: false, configurable: true, get: function proxyGetter() { return self.data[key]; }, set: function proxySetter(newVal) { self.data[key] = newVal; } }); }}更改后,我们就不要像之前通过传入固定的元素值进行双向绑定了,只要传入一个根节点就可以对所有子节点进行双向绑定了:<body> <div id=“app”> <h2>{{title}}</h2> <h1>{{name}}</h1> </div></body><script src=“js/observer.js”></script><script src=“js/watcher.js”></script><script src=“js/compile.js”></script><script src=“js/index.js”></script><script type=“text/javascript”> var Vue = new Vue({ el: ‘#app’, data: { title: ‘hello world’, name: ‘ddvdd’ } }); window.setTimeout(function () { Vue.title = ‘你好’; }, 2000); window.setTimeout(function () { Vue.name = ‘yjy’; }, 2500); </script>完整的Compile.js代码如下:function Compile(el, vm) { this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init();}Compile.prototype = { init: function () { if (this.el) { this.fragment = this.nodeToFragment(this.el); this.compileElement(this.fragment); this.el.appendChild(this.fragment); } else { console.log(‘Dom元素不存在’); } }, nodeToFragment: function (el) { var fragment = document.createDocumentFragment(); var child = el.firstChild; while (child) { // 将Dom元素移入fragment中 fragment.appendChild(child); child = el.firstChild } return fragment; }, compileElement: function (el) { var childNodes = el.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) { var reg = /{{(.)}}/; var text = node.textContent; if (self.isTextNode(node) && reg.test(text)) { // 判断是否是符合这种形式{{}}的指令 self.compileText(node, reg.exec(text)[1]); } if (node.childNodes && node.childNodes.length) { self.compileElement(node); // 继续递归遍历子节点 } }); }, compileText: function(node, exp) { var self = this; var initText = this.vm[exp]; this.updateText(node, initText); // 将初始化的数据初始化到视图中 new Watcher(this.vm, exp, function (value) { // 生成订阅器并绑定更新函数 self.updateText(node, value); }); }, updateText: function (node, value) { node.textContent = typeof value == ‘undefined’ ? ’’ : value; }, isTextNode: function(node) { return node.nodeType == 3; }}思考到这里,一个数据双向绑定功能已经基本完成了,接下去就是需要完善更多指令的解析编译,在哪里进行更多指令的处理呢?如何添加一个v-model指令和事件指令v-on:click的解析编译?所有例子和最终思考答案源码地址如果对你有帮助,麻烦star下~ ...

October 31, 2018 · 5 min · jiezi

揭密 Vue 的双向绑定

Vue 中需要输入什么内容的时候,自然会想到使用 <input v-model=“xxx” /> 的方式来实现双向绑定。下面是一个最简单的示例<div id=“app”> <h2>What’s your name:</h2> <input v-model=“name” /> <div>Hello {{ name }}</div></div>new Vue({ el: “#app”, data: { name: "" }});JsFiddle 演示https://jsfiddle.net/0okxhc6f/在这个示例的输入框中输入的内容,会随后呈现出来。这是 Vue 原生对 <input> 的良好支持,也是一个父组件和子组件之间进行双向数据传递的典型示例。不过 v-model 是 Vue 2.2.0 才加入的一个新功能,在此之前,Vue 只支持单向数据流。Vue 的单向数据流Vue 的单向数据流和 React 相似,父组件可以通过设置子组件的属性(Props)来向子组件传递数据,而父组件想获得子组件的数据,得向子组件注册事件,在子组件高兴的时候触发这个事件把数据传递出来。一句话总结起来就是,Props 向下传递数据,事件向上传递数据。上面那个例子,如果不使用 v-model,它应该是这样的<input :value=“name” @input=“name = $event.target.value” />由于事件处理写成了内联模式,所以脚本部分不需要修改。但是多数情况下,事件一般都会定义成一个方法,代码就会复杂得多<input :value=“name” @input=“updateName” />new Vue({ // …. methods: { updateName(e) { this.name = e.target.value; } }})从上面的示例来看 v-model 节约了不少代码,最重要的是可以少定义一个事件处理函数。所以 v-model 实际干的事件包括使用 v-bind(即 :)单向绑定一个属性(示例::value=“name”)绑定 input 事件(即 @input)到一个默认实现的事件处理函数(示例:@input=updateName这个默认的事件处理函数会根据事件对象带入的值来修改被绑定的数据(示例:this.name = e.target.value)自定义组件的 v-modelVue 对原生组件进行了封装,所以 <input> 在输入的时候会触发 input 事件。但是自定义组件应该怎么呢?这里不妨借助 JsFiddle Vue 样板的 Todo List 示例。JsFiddle 的 Vue 样板点击 JsFilddle 的 Logo,在上面弹出面板中选择 Vue 样板即可样板代码包含 HTML 和 Vue(js) 两个部分,代码如下:<div id=“app”> <h2>Todos:</h2> <ol> <li v-for=“todo in todos”> <label> <input type=“checkbox” v-on:change=“toggle(todo)” v-bind:checked=“todo.done”> <del v-if=“todo.done”> {{ todo.text }} </del> <span v-else> {{ todo.text }} </span> </label> </li> </ol></div>new Vue({ el: “#app”, data: { todos: [ { text: “Learn JavaScript”, done: false }, { text: “Learn Vue”, done: false }, { text: “Play around in JSFiddle”, done: true }, { text: “Build something awesome”, done: true } ] }, methods: { toggle: function(todo){ todo.done = !todo.done } }})定义 Todo 组件JsFiddle 的 Vue 模板默认实现一个 Todo 列表的展示,数据是固定的,所有内容在一个模板中完成。我们首先要做事情是把单个 Todo 改成一个子组件。因为在 JsFiddle 中不能写成多文件的形式,所以组件使用 Vue.component() 在脚本中定义,主要是把 <li> 内容中的那部分拎出来:Vue.component(“todo”, { template: &lt;label&gt; &lt;input type="checkbox" @change="toggle" :checked="isDone"&gt; &lt;del v-if="isDone"&gt; {{ text }} &lt;/del&gt; &lt;span v-else&gt; {{ text }} &lt;/span&gt;&lt;/label&gt;, props: [“text”, “done”], data() { return { isDone: this.done }; }, methods: { toggle() { this.isDone = !this.isDone; } }});原来定义在 App 中的 toggle() 方法也稍作改动,定义在组件内了。toggle() 调用的时候会修改表示是否完成的 done 的值。但由于 done 是定义在 props 中的属性,不能直接赋值,所以采用了官方推荐的第一种方法,定义一个数据 isDone,初始化为 this.done,并在组件内使用 isDone 来控制是否完成这一状态。相应的 App 部分的模板和代码精减了不少:<div id=“app”> <h2>Todos:</h2> <ol> <li v-for=“todo in todos”> <todo :text=“todo.text” :done=“todo.done”></todo> </li> </ol></div>new Vue({ el: “#app”, data: { todos: [ { text: “Learn JavaScript”, done: false }, { text: “Learn Vue”, done: false }, { text: “Play around in JSFiddle”, done: true }, { text: “Build something awesome”, done: true } ] }});JsFiddle 演示https://jsfiddle.net/0okxhc6f/1/不过到此为止,数据仍然是单向的。从效果上来看,点击复选框可以反馈出删除线线效果,但这些动态变化都是在 todo 组件内部完成的,不存在数据绑定的问题。为 Todo List 添加计数为了让 todo 组件内部的状态变化能在 Todo List 中呈现出来,我们在 Todo List 中添加计数,展示已经完成的 Todo 数量。因为这个数量受 todo 组件内部状态(数据)的影响,这就需要将 todo 内部数据变化反应到其父组件中,这才有 v-model 的用武之地。这个数量我们在标题中以 n/m 的形式呈现,比如 2/4 表示一共 4 条 Todo,已经完成 2 条。这需要对 Todo List 的模板和代码部分进行修改,添加 countDone 和 count 两个计算属性:<div id=“app”> <h2>Todos ({{ countDone }}/{{ count }}):</h2> <!– … –></div>new Vue({ // … computed: { count() { return this.todos.length; }, countDone() { return this.todos.filter(todo => todo.done).length; } }});现在计数呈现出来了,但是现在改变任务状态并不会对这个计数产生影响。我们要让子组件的变动对父组件的数据产生影响。v-model 待会儿再说,先用最常见的方法,事件:子组件 todo 在 toggle() 中触发 toggle 事件并将 isDone 作为事件参数父组件为子组件的 toggle 事件定义事件处理函数Vue.component(“todo”, { //… methods: { toggle(e) { this.isDone = !this.isDone; this.$emit(“toggle”, this.isDone); } }});<!– #app 中其它代码略 –><todo :text=“todo.text” :done=“todo.done” @toggle=“todo.done = $event”></todo>这里为 @toggle 绑定的是一个表达式。因为这里的 todo 是一个临时变量,如果在 methods 中定义专门的事件处理函数很难将这个临时变量绑定过去(当然定义普通方法通过调用的形式是可以实现的)。事件处理函数,一般直接对应于要处理的事情,比如定义 onToggle(e),绑定为 @toggle=“onToggle”。这种情况下不能传入 todo 作为参数。普通方法,可以定义成 toggle(todo, e),在事件定义中以函数调用表达式的形式调用:@toggle=“toggle(todo, $event)"。它和 todo.done = $event同属表达式。注意二者的区别,前者是绑定的处理函数(引用),后者是绑定的表达式(调用)现在通过事件方式已经达到了预期效果 Js Fiddle 演示 https://jsfiddle.net/0okxhc6f/2/改造成 v-model之前我们说了要用 v-model 实现的,现在来改造一下。注意实现 v-model 的几个要素子组件通过 value 属性(Prop)接受输入子组件通过触发 input 事件输出,带数组参数父组件中用 v-model 绑定Vue.component("todo", { // ... props: ["text", "value"], // &lt;-- 注意 done 改成了 value data() { return { isDone: this.value // &lt;-- 注意 this.done 改成了 this.value }; }, methods: { toggle(e) { this.isDone = !this.isDone; this.$emit("input", this.isDone); // &lt;-- 注意事件名称变了 } }});&lt;!-- #app 中其它代码略 --&gt;&lt;todo :text="todo.text" v-model="todo.done"&gt;&lt;/todo&gt;.sync 实现其它数据绑定前面讲到了 Vue 2.2.0 引入 v-model 特性。由于某些原因,它的输入属性是 value,但输出事件叫 input。v-model、value、input 这三个名称从字面上看不到半点关系。虽然这看起来有点奇葩,但这不是重点,重点是一个控件只能双向绑定一个属性吗?Vue 2.3.0 引入了 .sync 修饰语用于修饰 v-bind(即 :),使之成为双向绑定。这同样是语法糖,添加了 .sync 修饰的数据绑定会像 v-model 一样自动注册事件处理函数来对被绑定的数据进行赋值。这种方式同样要求子组件触发特定的事件。不过这个事件的名称好歹和绑定属性名有点关系,是在绑定属性名前添加 update: 前缀。比如 &lt;sub :some.sync="any" /&gt; 将子组件的 some 属性与父组件的 any 数据绑定起来,子组件中需要通过 $emit("update:some", value) 来触发变更。上面的示例中,使用 v-model 绑定始终感觉有点别扭,因为 v-model 的字面意义是双向绑定一个数值,而表示是否未完成的 done 其实是一个状态,而不是一个数值。所以我们再次对其进行修改,仍然使用 done 这个属性名称(而不是 value),通过 .sync 来实现双向绑定。Vue.component("todo", { // ... props: ["text", "done"], // &lt;-- 恢复成 done data() { return { isDone: this.done // &lt;-- 恢复成 done }; }, methods: { toggle(e) { this.isDone = !this.isDone; this.$emit("update:done", this.isDone); // &lt;-- 事件名称:update:done } }});&lt;!-- #app 中其它代码略 --&gt;&lt;!-- 注意 v-model 变成了 :done.sync,别忘了冒号哟 --&gt;&lt;todo :text="todo.text" :done.sync="todo.done"&gt;&lt;/todo&gt; Js Fiddle 演示 https://jsfiddle.net/0okxhc6f/3/揭密 Vue 双向绑定通过上面的讲述,我想大家应该已经明白了 Vue 的双向绑定其实就是普通单向绑定和事件组合来完成的,只不过通过 v-model 和 .sync 注册了默认的处理函数来更新数据。Vue 源码中有这么一段// @file: src/compiler/parser/index.jsif (modifiers.sync) { addHandler( el, update:${camelize(name)}, genAssignmentCode(value, $event`) )}从这段代码可以看出来,.sync 双向绑定的时候,编译器会添加一个 update:${camelize(name)} 的事件处理函数来对数据进行赋值(genAssignmentCode 的字面意思是生成赋值的代码)。展望目前 Vue 的双向绑定还需要通过触发事件来实现数据回传。这和很多所的期望的赋值回传还是有一定的差距。造成这一差距的主要原因有两个需要通过事件回传数据属性(prop)不可赋值在现在的 Vue 版本中,可以通过定义计算属性来实现简化,比如computed: { isDone: { get() { return this.done; }, set(value) { this.$emit(“update:done”, value); } }}说实在的,要多定义一个意义相同名称不同的变量名也是挺费脑筋的。希望 Vue 在将来的版本中可以通过一定的技术手段减化这一过程,比如为属性(Prop)声明添加 sync 选项,只要声明 sync: true 的都可以直接赋值并自动触发 update:xxx 事件。当然作为一个框架,在解决一个问题的时候,还要考虑对其它特性的影响,以及框架的扩展性等问题,所以最终双向绑定会演进成什么样子,我们对 Vue 3.0 拭目以待。 ...

October 5, 2018 · 3 min · jiezi