vue的双向绑定原理:Object.defineProperty()
vue实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()
来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty()
将它们转为 getter/setter
。用户看不到 getter/setter
,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
vue的数据双向绑定 将MVVM作为数据绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听自己的model的数据变化,通过Compile来解析编译模板指令(vue中是用来解析 {{}}
),最终利用watcher搭起observer和Compile之间的通信桥梁,达到数据变化 —>视图更新;视图交互变化(input)—>数据model变更双向绑定效果。
Vue简单使用:(差值表达式)
<div id="app"> <h1>{{msg}} {{num+1}}</h1> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var app = new Vue({ el:"#app", data:{ msg:"Hello World", num:1 } }) </script>
也可以写做:
<div id="app"> <h1 v-text="msg"></h1> </div> <script> var vm = new Vue({ el:'#app', data() { return { msg: 'hello world' } }, }); </script>
还可以写做:
<div id="app"> <div v-html="msg"></div> </div> <script> var vm = new Vue({ el:'#app', data() { return { msg: '<h1>hello world</h1>' } }, }); </script>
还可以添加js表达式:
<div id="app"> {{msg + ' Joe'}} <div v-text="msg + ' Joe'"></div> <div v-html="msg + ' Joe'"></div> </div> <script> var vm = new Vue({ el:'#app', data() { return { msg: 'hello world' } }, }); </script>
v-bind 简写 :
动态地绑定一个或多个特性,或一个组件 prop 到表达式。
<img v-bind:src="imageSrc"> <!-- 缩写 --> <img :src="imageSrc">
<a v-bind:href="href">百度</a> <a :href="href">百度</a> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var app = new Vue({ el:"#app", data:{ href:"https://www.baidu.com", } }) </script>
<!-- 通过 prop 修饰符绑定 DOM 属性 --> <div v-bind:text-content.prop="text"></div> <!-- prop 绑定。“prop”必须在 my-component 中声明。--> <my-component :prop="someThing"></my-component> <!-- 通过 $props 将父组件的 props 一起传给子组件 --> <child-component v-bind="$props"></child-component>
动态class 可以绑定对象、数组:
对象绑定例子:点击文字变为红色,再点击恢复原来颜色
<style> .activated{color:red} </style> <div id="app"> <div :class="{activated: isActivated}" @click="handleClick">Hello World</div> </div> </script> <script> var app = new Vue({ el:'#app', data() { return { isActivated:false } }, methods: { handleClick() { this.isActivated = !this.isActivated } }, }); </script>
数组绑定例子:
<style> .activated{color:red} </style> <div id="app"> <div :class="[isActivated]" @click="handleClick">Hello World</div> </div> <script> var app = new Vue({ el:'#app', data() { return { isActivated: '' } }, methods: { handleClick() { this.isActivated = this.isActivated === 'activated' ? '' : 'activated' } }, }); </script>
当然也可以直接绑定style,对象例子:
<div id="app"> <div :style="styleObj" @click="handleClick">Hello World</div> </div> <script> var app = new Vue({ el:'#app', data() { return { styleObj: {color:'black'} } }, methods: { handleClick() { this.styleObj.color = this.styleObj.color === 'black' ? 'red' : 'black' } }, }); </script>
数组例子:数组里的对象来实现
<div id="app"> <div :style="[styleObj, {fontSize: '20px'}]" @click="handleClick">Hello World</div> </div> <script> var vm = new Vue({ el:'#app', data() { return { styleObj: {color:'black'} } }, methods: { handleClick() { this.styleObj.color = this.styleObj.color === 'black' ? 'red' : 'black' } }, }); </script>
v-if v-else v-show条件渲染
<div v-if="see">if true</div> <div v-else>if false</div> <div v-show="see">show true</div> <script> var app = new Vue({ el:"#app", data:{ see:false, } }) </script>
v-show为false时只是是隐藏了,相当于display:none。v-show的性能比v-if好。
for循环: ( in )也可以用( of )
<ul> <li v-for="item in person">我是{{item.name}},我{{item.age}}岁了</li> </ul> <ul> <li v-for="item in obj">{{item}}</li> </ul> <script> var app = new Vue({ el:"#app", data:{ person:[ {name:'111',age:'20'}, {name:'222',age:'22'},{name:'333',age:'26'}, ], obj:{name:'444',age:"30"}, //对象循环的是value值 } }) </script>
如果要循环key的话:第二参
<ul> <li v-for="(item, index) in person">{{index+1}}-我是{{item.name}},我{{item.age}}岁了</li> </ul> <ul> <li v-for="(item, name) in obj">{{name}}:{{item}}</li> </ul>
key值:标识,禁止复用。当给元素添加key后,就不会在出现Dom复用。
例子:上面的例子如果添加一个按钮,点击按钮删除第一条数据,如果不写key的话,实际删除的是Dom里的第三条li,因为Dom复用的原因,加上key,就不会出现Dom复用,删除的就是第一个li
<button @click="person.splice(0,1)">删除</button> <!--点击删除第一条数据--> <ul> <li v-for="(item, index) in person" :key="item.name"> {{index+1}}-我是{{item.name}},我{{item.age}}岁了 </li> </ul>
input例子:
//input也要加key避免复用 <div v-if="see"> 登录:<input type="text" placeholder="登录" key="login" /> </div> <div v-else> 注册:<input type="text" placeholder="注册" key="register" /> </div> //不过一般input都会绑定v-model监听 <div v-if="see"> 登录:<input type="text" placeholder="登录" v-model="login" /> </div> <div v-else> 注册:<input type="text" placeholder="注册" v-model="register" /> </div>
控制v-for循环次数的多种方法:
1、通过html的标签里面v-if对超出范围的进行隐藏
<div v-for="(item,index) in courselist" v-if="index < 6" :key='index'></div>
2、通过sclie截取数组的长度控制循环的次数
<div v-for="(item,index) in courselist.slice(0,6)" :key="index"></div>
template模板占位符:
可以帮助我们去包裹一些元素,但是在循环过程中,并不会被真正的渲染到页面上.
例如:
<template v-for="item of list" :key="item.id"> <div>{{item.title}}</div> <span>{{item.time}}</span> </template>
页面查看元素会发现:div下面是span,然后还是div和span
对象循环:
<div id="app"> <div v-for="(item, key, index) of userInfo">{{item}} -- {{key}} -- {{index}}</div> </div> <script> var vm = new Vue({ el:'#app', data() { return { userInfo:{ name: 'Joe', age: 28, gender: 'male', salary: 'secret' } } }, }); </script>
set方法
通过Vue.set全局方法给对象里添加值:
控制台输入:Vue.set(vm.userInfo, "address", "beijing") 回车,然后页面会变为5项内容.
也可以用实例方法来实现:vm.$set(vm.userInfo, "address", "beijing")
数组上的set方法使用:
<div id="app"> <div v-for="item of userInfo">{{item}}</div> </div> <script> var vm = new Vue({ el:'#app', data() { return { userInfo:[1, 2, 3, 4] } }, }); </script>
在控制台输入:Vue.set(vm.userInfo, 1, 5),这时页面会变为1 5 3 4
同样也可以调用实例方法,vm.$set(vm.userInfo, 2, 6),页面变为 1 5 6 4
v-on 简写@ 用到监听
//点击see的值取反,上面条件区域是否显示 <div> <h1>条件渲染</h1> <div v-if="see">if true</div> <div v-else>if false</div> <div v-show="see">show true</div> </div> <div> <h1>v-on</h1> <button v-on:click="see = !see">click me</button> </div>
复杂的功能可以写个方法:
<div> <h1>v-on</h1> <button @click="click">click me</button> </div> <script> var app = new Vue({ el:"#app", data:{ see:false, }, methods: { click:function(){ this.see = !this.see; //this指向当前app } }, }) </script>
传参:
<div> <h1>v-on</h1> <button @click="click('hello')">click me</button> </div> <script> var app = new Vue({ el:"#app", data:{ see:false, }, methods: { click:function(val){ this.see = !this.see; //this指向当前app alert(val); } }, }) </script>
监听input事件:
<input v-on:input="click" />
<div id="app"> <form action="/abc"> <input type="submit" /> </form> </div>
例子:点击提交,不希望页面进行跳转(阻止from表单的默认行为)
<div id="app"> <form action="/abc" @click="handleClick"> <input type="submit" /> </form> </div> <script> var vm = new Vue({ el:'#app', methods: { handleClick (e) { e.preventDefault(); // 阻止默认行为 } }, }); </script>
现在怎么点击提交,也不会进行跳转了。当然还可以简写为:@click.prevent
<div id="app"> <!-- <form action="/abc" @click="handleClick"> --> <form action="/abc" @click.prevent> <input type="submit" /> </form> </div> <script> var vm = new Vue({ el:'#app', }); </script>
<div id="app"> <div @click="handleClick"> <div>hello world</div> </div> </div> <script> var vm = new Vue({ el:'#app', methods: { handleClick () { console.log(123) } }, }); </script>
点击hello world打印出123,但是实际上我们在点击hello world的时候,经过了一次冒泡,实际上我点击的元素是div,通过冒泡,外层的div上触发了 handleClick 事件。
下面我们希望只有当点击了外层的这个div,才触发 handleClick 事件;如果是通过冒泡行为冒泡上来的事件,并不执行。就需要用到 @click.self
<div id="app"> <div @click.self="handleClick"> nick <div>hello world</div> </div> </div>
这时候点击 hello world 控制台不打印出123;当点击nick时,打印出123.
@click.once :指的是只在div上绑定一次handleClick,当事件触发过一次后,事件就会自动解绑。
@click.capture :capture 遵循的就不是冒泡,而是捕获的规则
例子:
<div @click="handleClick"> <div @click="handleClickInner">hello world</div> </div> <script> var vm = new Vue({ el:'#app', methods: { handleClick () { console.log(123) }, handleClickInner () { console.log('inner') } }, }); </script>
点击hello world后的结果是:先打印出inner,再通过冒泡到外层,再打印出123。
<div @click.capture="handleClick"> <div @click.capture="handleClickInner">hello world3</div> </div>
现在点击hello world后的结果是:先打印出123,再打印出inner,这时候遵循的是捕获规则。
按键修饰符:
例子:
<div id="app"> <input @keydown="handleInput" /> </div> <script> var vm = new Vue({ el:'#app', methods: { handleInput (e) { console.log(e.target.value) } }, }); </script>
这样在input框输入内容就会在控制台打印出来。当我们不希望一输入就打印,而是当我们输入完成后按回车的时候在打印到控制台:
<input @keydown.enter="handleInput" />
刷新后,当我们输入内容时并不会打印,而是输入完按回车后,才在控制台打印出结果。
当然按键修饰符还有很多,比如:tab 就是当tab键被点击的时候对应的事件才会被执行;还有del、esc等等的。
系统修饰符:ctrl alt shift meta
<input @keydown.ctrl="handleInput" /> <!-- .ctrl:输入完成后,点击ctrl键,输入的内容才会被打印出来 -->
鼠标按键的修饰符:left、right、middle
例子:
<div id="app"> <div @click.right="handleClick">click</div> </div> <script> var vm = new Vue({ el:'#app', methods: { handleClick () { console.log(123) } }, }); </script>
v-model 双向绑定:
<div><input type="text" v-model="textVal" />{{textVal}}</div> <script> var app = new Vue({ el:"#app", data:{ textVal:"", //初始化 }, methods: { //事件监听 }, }) </script>
单选:
<div> <input type="radio" value="0" id="a" v-model="radioVal" /><label for="a">A</label> <input type="radio" value="1" id="b" v-model="radioVal" /><label for="b">B</label> <input type="radio" value="2" id="c" v-model="radioVal" /><label for="c">C</label> {{radioVal}} </div> <script> var app = new Vue({ el:"#app", data:{ radioVal:"", }, }) </script>
复选:
<div> <input type="checkbox" value="0" id="aa" v-model="checkboxVal" /><label for="aa">A</label> <input type="checkbox" value="1" id="bb" v-model="checkboxVal" /><label for="bb">B</label> <input type="checkbox" value="2" id="cc" v-model="checkboxVal" /><label for="cc">C</label> {{checkboxVal}} </div> <script> var app = new Vue({ el:"#app", data:{ checkboxVal:[], //初始化数组 } })
下拉选择框:
<div> <select v-model="selectVal"> <option value="0">A</option> <option value="1">B</option> <option value="2">C</option> <option value="3">D</option> </select> {{selectVal}} </div> <script> var app = new Vue({ el:"#app", data:{ selectVal:0, //初始为0默认选第一 } }) </script>
上面的都可以设置初始值:
data:{ textVal:"111", radioVal:"0", checkboxVal:[0], selectVal:0, }
Vue表单绑定之中的修饰符:lazy、number、trim
例子:lazy
<div id="app"> <input type="text" v-model.lazy="value"/> {{value}} </div> <script> var vm = new Vue({ el:'#app', data () { return { value: '' } } }); </script>
我们输入内容时,没有任何变化,当input框失去焦点的时候,输入的内容才一次性的显示出来。
例子:number
<div id="app"> <input type="text" v-model="value"/> {{value}} </div> <script> var vm = new Vue({ el:'#app', data () { return { value: '' } }, watch: { value () { console.log(typeof this.value) // 打印出输入内容的类型 } } }); </script>
我们在输入框里输入字母和汉字时,控制台打印出String类型,但是我们输入123数字时,也打印出String类型。我们想输入数字类型时怎么办呢?
<input type="text" v-model.number="value"/>
刷新后,我们输入abc,输出的还是String类型,因为它转化不了numberleix。当输入123后,打印出来的就是number类型了。
例子:trim 首、尾空格的去除
<div id="app"> <input type="text" v-model.trim="value"/> {{value}} </div>
当我们在输入框里输入“ adfa ”时,发现结果还是 adfa ,.trim 把首部和尾部的空格都去除掉了。
计算属性:computed
计算属性有缓存机制:当计算属性所依赖的值没有改变时,就不会去重新计算。
简单实例:
<div id="app"> {{fullName}} </div> <script> var vm = new Vue({ el:'#app', data() { return { firstName: 'Joe', lastName: 'Lee', } }, // 计算属性 computed: { fullName() { return this.firstName + ' ' + this.lastName } }, }); </script>
实例:
<h1>{{msg.split('').reverse().join('')}}</h1> //dlroW olleH 截取反转拼接 //computed实现 <h1>{{msg}}</h1> <h2>{{reversedMsg}}</h2> <script> var app = new Vue({ el:"#app", data:{ msg:"Hello World", }, computed: { //计算属性 reversedMsg:function(){ return this.msg.split('').reverse().join(''); } }, }) </script> //也可以通过方法实现 <h1>{{msg}}</h1> <h2>{{reversedMsg()}}</h2> <script> var app = new Vue({ el:"#app", data:{ msg:"Hello World", }, methods: { //事件监听 reversedMsg:function(){ return this.msg.split('').reverse().join(''); } }, }) </script>
不建议用方法的形式,推荐使用计算属性。
结果:
例子:展示年龄大于30岁的数据
<ul> <h1>v-for</h1> <li v-for="(item, index) in person" v-if="item.age > 30"> {{index+1}}-我是{{item.name}},我{{item.age}}岁了 </li> </ul> <script> var app = new Vue({ el:"#app", data:{ person:[ {name:'111',age:'20'}, {name:'222',age:'28'},{name:'333',age:'36'}, ], }, }) </script> //官方推荐用计算属性写法 <ul> <li v-for="(item, index) in newPerson"> {{index+1}}-我是{{item.name}},我{{item.age}}岁了 </li> </ul> <script> var app = new Vue({ el:"#app", data:{ person:[ {name:'111',age:'20'}, {name:'222',age:'28'},{name:'333',age:'36'}, ], }, computed: { //计算属性 newPerson:function(){ return this.person.filter(function(item){return item.age > 30}); } }, }) </script>
计算属性的setter和getter
例子:
<div id="app"> {{fullName}} </div> <script> var vm = new Vue({ el:'#app', data() { return { firstName: 'Joe', lastName: 'Lee' } }, // 计算属性 computed: { fullName: { get: function(){ return this.firstName + ' ' + this.lastName }, set: function(value){ // console.log(value); var arr = value.split(' '); this.firstName = arr[0]; this.lastName = arr[1]; }, } }, }); </script>
然后在控制台输入:vm.fullName = 'Tom Wang'
回车后可以看到页面变为Tom Wang。这个就是计算属性的set和get。
监听属性:watch
//输入问题时答案显示waiting,1秒钟后显示404 <div> question:<input type="text" placeholder="enter" v-model="question" /><br> answer:{{answer}} </div> <script> var app = new Vue({ el:"#app", data:{ question:"", answer:"no answer", }, watch: { //监听属性 question:function(){ this.answer = "waiting"; var _this = this; setTimeout(function(){ _this.answer = "404"; },1000); } }, }) </script>
vue提供的7种数组变异方法:
push、pop、shift删除第一项、unshift往第一项添加、splice截取操作、sort排序、reverse取反
vue检测数组变动:
有两种情况变动的数组,是VUE不能检测到的,也不会触发视图的更新。
- 通过索引直接设置项 例如: vm.items[indexOfItem] = newValue
- 修改数组的长度 vm.items.length = newLength
举个例子:
var vm = new Vue({ data: { items: ['a', 'b', 'c'] } }) vm.items[1] = 'x' // 不是响应性的 vm.items.length = 2 // 不是响应性的
为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也将触发状态更新:
// Vue.set Vue.set(vm.items, indexOfItem, newValue) // Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue)
为了解决第二类问题,可以使用 splice
:
vm.items.splice(newLength)
生命周期函数:
生命周期函数:就是vue实例在某一个时间点自动执行的函数。
vue有8种生命周期函数:
钩子函数 | 触发的行为 | 在此阶段可以做的事情 |
---|---|---|
beforeCreadted | vue实例的挂载元素$el和数据对象data都为undefined,还未初始化。 | 加loading事件 |
created | vue实例的数据对象data有了,$el还没有 | 结束loading、请求数据为mounted渲染做准备 |
beforeMount | vue实例的$el和data都初始化了,但还是虚拟的dom节点,具体的data.filter还未替换。 | .. |
mounted | vue实例挂载完成,data.filter成功渲染 | 配合路由钩子使用 |
beforeUpdate | data更新时触发 | |
updated | data更新时触发 | 数据更新时,做一些处理(此处也可以用watch进行观测) |
beforeDestroy | 组件销毁时触发 | |
destroyed | 组件销毁时触发,vue实例解除了事件监听以及和dom的绑定(无响应了),但DOM节点依旧存在 | 组件销毁时进行提示 |
当vm实例中有template的,就用模板渲染,没有的话就把el元素,对应的html作为模板被渲染。
<div id="app"></div> <script> var vm = new Vue({ el:'#app', template: '<div>hello world</div>', beforeCreate: function() { console.log('beforeCreate'); }, created() { console.log('created'); }, }); </script> <!--相当于--> <div id="app">hello world</div> <script> var vm = new Vue({ el:'#app', beforeCreate: function() { console.log('beforeCreate'); }, created() { console.log('created'); }, }); </script>
-
在beforeCreate和created钩子函数之间的生命周期
在这个生命周期之间,进行初始化事件,进行数据的观测,可以看到在created的时候数据已经和data属性进行绑定(放在data中的属性当值发生改变的同时,视图也会改变)。
注意看下:此时还是没有el选项 -
created钩子函数和beforeMount间的生命周期
首先会判断对象是否有el选项。如果有的话就继续向下编译,如果没有el选项,则停止编译,也就意味着停止了生命周期,直到在该vue实例上调用vm.$mount(el)。
这之后,观察到template参数选项对生命周期的影响
(1)如果vue实例对象中有template参数选项,则将其作为模板编译成render函数。
(2)如果没有template选项,则将外部HTML作为模板编译。
(3)template中的模板优先级要高于outer HTML的优先级。
(4)render函数选项的优先级最高。 -
beforeMount和mounted 钩子函数间的生命周期
给vue实例对象添加$el成员,并且替换掉挂在的DOM元素。 -
mounted
el已经渲染完成并挂载到实例上 -
beforeUpdate钩子函数和updated钩子函数间的生命周期
当vue发现data中的数据发生了改变,会触发对应组件的重新渲染,先后调用beforeUpdate和updated钩子函数。 -
beforeDestroy和destroyed钩子函数间的生命周期
beforeDestroy钩子函数在实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
<div id="app"></div> <script> var vm = new Vue({ el:'#app', template: '<div>{{test}}</div>', data() { return { test: 'hello world' } }, beforeCreate: function() { console.log('beforeCreate'); }, created() { console.log('created'); }, beforeMount() { console.log(this.$el); console.log('beforeMount'); }, mounted() { console.log(this.$el); console.log('mounted'); }, beforeDestroy() { console.log('beforeDestroy'); }, destroyed() { console.log('destroyed'); }, beforeUpdate() { console.log('beforeUpdate'); }, updated() { console.log('updated'); }, }); </script>
控制台打印:
组件注册:
在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:
Vue.component('my-component-name', { /* ... */ })
该组件名就是 Vue.component
的第一个参数。
定义组件名的方式有两种:
使用 kebab-case
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>
。
Vue.component('my-component-name', { /* ... */ })
使用 PascalCase
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name>
和 <MyComponentName>
都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
Vue.component('MyComponentName', { /* ... */ })
全局注册:
到目前为止,我们只用过 Vue.component
来创建组件:
Vue.component('my-component-name', { // ... 选项 ... })
这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue
) 的模板中。比如:
Vue.component('component-a', { /* ... */ }) Vue.component('component-b', { /* ... */ }) Vue.component('component-c', { /* ... */ }) new Vue({ el: '#app' }) <div id="app"> <component-a></component-a> <component-b></component-b> <component-c></component-c> </div>
局部注册:
可以通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ } var ComponentB = { /* ... */ } var ComponentC = { /* ... */ }
然后在 components
选项中定义你想要使用的组件:
new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB } })
对于 components
对象中的每个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。
注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA
在 ComponentB
中可用,则你需要这样写:
var ComponentA = { /* ... */ } var ComponentB = { components: { 'component-a': ComponentA }, // ... }
或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue' export default { components: { ComponentA }, // ... }
用is来解决小bug:
例子:
<div id="app"> <table> <tbody> <!-- 这里直接写<row></row>会出错,tr会跑到table外面 --> <tr is="row"></tr> <tr is="row"></tr> <tr is="row"></tr> </tbody> </table> </div> <script> Vue.component('row', { template: '<tr><td>This is</td></tr>' }) var vm = new Vue({ el:'#app', }); </script>
同理,ul、ol、select这些标签也是一样用is
在子组件里,定义data的时候,data必须是个函数,而不能是对象
例子:
Vue.component('row', { data(){ return { content: 'This is content' } }, template: '<tr><td>{{content}}</td></tr>' })
ref引用:可以用ref来获取DOM节点
例子:
<!-- 点击div后控制台输出div的内容 --> <div id="app"> <div ref="hello" @click="handleClick">hello world</div> </div> <script> var vm = new Vue({ el:'#app', methods: { handleClick() { console.log(this.$refs.hello.innerHTML) } }, }); </script>
组件中的ref引用:获取子组件的引用
例子:计数器
<div id="app"> <counter ref="one" @change="handleChange"></counter> <counter ref="two" @change="handleChange"></counter> <div>求和:{{total}}</div> </div> <script> Vue.component('counter', { data() { return { number: 0 } }, template: '<div @click="handleClick">{{number}}</div>', methods: { handleClick() { this.number ++ // 当子组件数目发生变化时,向外触发一个 change 事件 this.$emit('change') } }, }) var vm = new Vue({ el:'#app', data() { return { total: 0 } }, methods: { handleChange() { //console.log(this.$refs.one) this.total = this.$refs.one.number + this.$refs.two.number } }, }); </script>
组件通信:
1、props
/ $emit
父组件 A 通过 props 的方式向子组件 B 传递,B to A 通过在 B 组件中 $emit, A 组件中 v-on 的方式实现。
(1)父组件给子组件传值:父组件通过属性的形式向下传递数据给子组件,子组件通过props接收。
例子:还是计数器
<div id="app"> <counter :count="0"></counter> <counter :count="1"></counter> </div> <script> var counter = { props: ['count'], template: '<div @click="handleClick">{{count}}</div>', methods: { handleClick() { this.count ++ } } } var vm = new Vue({ el:'#app', components:{ counter: counter }, }); </script>
这时点击0或者1都可以实现+1的功能,但是控制台会报错:“vue.js:634 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "count"”
子组件不能修改父组件传递过来的参数。
单项数据流:父组件可以通过属性的形式向子组件传递参数,也可以随便修改,但是子组件绝对不能反过来修改父组件传递过来的这个参数。
修改为:
var counter = { props: ['count'], template: '<div @click="handleClick">{{number}}</div>', data() { return { number: this.count } }, methods: { handleClick() { this.number ++ } } }
这时点击可以实现+1的功能,控制台也不会报错了。
例子1:购物车地址
//父Home.vue <Nav :NavActiveIndex="activeIndex"></Nav> <script> import Nav from "./Nav.vue"; export default { name: "home", components: { Nav }, data() { return { activeIndex: '/home/buycar', activeIndex2: '1' }; }, </script> //子Nav.vue {{NavActiveIndex}} //home/buycar 实现默认绑定购物车 <el-menu :default-active="NavActiveIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect" background-color="#545c64" text-color="#fff" router active-text-color="#ffd04b"> <script> export default { name: "nav", props:{ NavActiveIndex:String }, }, </script>
(2)子组件向父组件传值:
通过触发组件自定义事件的方式来实现给父组件传值:
例子:继续上面的计数器功能
<div id="app"> <counter :count="0" @change="handleChange"></counter> <counter :count="1" @change="handleChange"></counter> <div>求和:{{total}}</div> </div> <script> var counter = { props: ['count'], template: '<div @click="handleClick">{{number}}</div>', data() { return { number: this.count // 副本 } }, methods: { handleClick() { // this.number ++; // this.$emit('change', 1) this.number = this.number + 2; this.$emit('change', 2) } } } var vm = new Vue({ el:'#app', components:{ counter: counter }, data() { return { total: 1 } }, methods: { handleChange(step) { this.total += step } }, }); </script>
例子1:购物车地址
//父组件 Home.vue //绑定自定义事件fromNavVal <Nav v-if="NavOpen" :NavActiveIndex="activeIndex" @fromNavVal="fromNavVal"></Nav> <div v-else>折叠{{activeIndex2}}</div> <script> import Nav from "./Nav.vue"; export default { name: "home", components: { Nav }, data() { return { activeIndex: '/home/buycar', activeIndex2: '1', NavOpen: true, //Nav打开折叠 }; }, methods: { fromNavVal(val){ //自定义方法fromNavVal this.activeIndex2 = val; this.NavOpen = false; } } }; </script> //子组件 Nav.vue <button @click="click">折叠Nav</button> <script> export default { name: "nav", props:{ NavActiveIndex:String }, data() { return { activeIndex: '1', activeIndex2: '123' }; }, methods: { handleSelect(key, keyPath) { console.log(key, keyPath); }, click(){ //$emit(事件名,传出的参数) this.$emit('fromNavVal',this.activeIndex2) } } }; </script>
2、组件参数校验与非props特性
<div id="app"> <child content="hello world"></child> </div> <script> Vue.component('child', { props:{ content: String, // content类型必须为字符串类型 }, template: '<div>{{content}}</div>', }) var vm = new Vue({ el:'#app', }); </script>
例子:传递的参数必须为数字类型
<div id="app"> <child :content="123"></child> </div> <script> Vue.component('child', { props:{ content: Number // content类型必须为数字类型 }, template: '<div>{{content}}</div>', }) var vm = new Vue({ el:'#app', }); </script>
例子:传递的参数类型可以为字符串也可以为数字
props:{ content: [String, Number], // content类型可以为字符串类型或者数字类型 },
例子:更复杂的校验
<div id="app"> <child content="aaaaaa"></child> </div> <script> Vue.component('child', { props:{ content: { type: String, // 类型为字符串 required: false, // true表示:content 这个属性是必传的;false 为非必传 default: 'hello', // 默认值,有传值的时候不生效 validator: function(value) { return (value.length > 5 ) // content 的内容长度必须大于5 } } }, template: '<div>{{content}}</div>', }) var vm = new Vue({ el:'#app', }); </script>
非Props特性:
是指父组件向子组件传递了一个属性,但是子组件并没有声明props接收父组件传递过来的内容。并且非props特性的属性会显示在DOM中。
例子:
<div id="app"> <child content="aaaaaa"></child> </div> <script> Vue.component('child', { template: '<div>hello</div>', }) var vm = new Vue({ el:'#app', }); </script>
图:
给组件绑定原生事件: 事件修饰符native 监听原生的点击事件
例子:
<div id="app"> <child @click="handleClick"></child> </div> <script> Vue.component('child', { template: '<div @click="handleChild">Child</div>', methods: { handleChild(){ console.log('child chick'); // 触发原生事件 this.$emit('click'); // 触发自定义事件 } }, }) var vm = new Vue({ el:'#app', methods: { handleClick() { console.log('click'); } }, }); </script> <!--可以通过事件修饰符native来实现--> <div id="app"> <child @click.native="handleClick"></child> </div> <script> Vue.component('child', { template: '<div>Child</div>', }) var vm = new Vue({ el:'#app', methods: { handleClick() { console.log('click'); } }, }); </script>
3、event bus 事件处理中心(非父子组件间的传值)
具体实现方式:
var Event = new Vue(); Event.$emit(事件名,数据); Event.$on(事件名,data => {});
例子:点击上面文字,下面文字变为上面文字的内容;点击下面文字,上面文字变为下面文字的内容。
<div id="app"> <child content="Joe"></child> <child content="Lee"></child> </div> <script> Vue.prototype.bus = new Vue(); Vue.component('child', { data(){ return { selfContent : this.content // 单向数据流拷贝副本 } }, props: { content: String, }, template: '<div @click="handleClick">{{selfContent}}</div>', methods: { handleClick() { // console.log(this.content); this.bus.$emit('change', this.selfContent); // 向外触发事件 } }, mounted() { //生命周期钩子 var that = this; this.bus.$on('change', msg => { // 监听事件 // console.log(msg); that.selfContent = msg; }) }, }) var vm = new Vue({ el:'#app', }); </script>
例子:实现购物车数量。
先建立一个事件处理中心js文件,bus.js:
import Vue from "vue"; const EventBus = new Vue(); export default EventBus;
在Nav.vue里引入bus.js
<el-menu-item index="/home/buycar">购物车{{buycarCount}}</el-menu-item> <!--4展示购物车数量--> //1首先在Nav.vue里引入bus.js <script> import bus from '@/assets/bus.js'; export default { name: "nav", props:{ NavActiveIndex:String }, data() { return { activeIndex: '1', activeIndex2: '123', buycarCount: 0, //3购物车数量 }; }, created() { //2注册组件 bus.$on("buycarCountChang",(num) =>{ //监听事件buycarCountChang this.buycarCount = num; }) }, }
在buycar.vue里引入bus.js
<script> //1.引入bus.js import bus from '@/assets/bus.js'; export default { 。。。 watch: { //2.监听list改变 list:{ //list监听:当key是list,值是函数的时候,是不深度遍历的,也就是对象改变的时候不监听,所以用这种写法 handler:function(){ //handler处理函数 let count = 0; this.list.forEach( item => { count += parseInt(item.count); //item.count累加 }) bus.$emit("buycarCountChang",count); //购物车数量改变的时候触发buycarCountChang事件 }, deep:true //true的时候深度监听 } } }
4、Vue中使用插槽 slot 和作用域插槽 slot-scope
例子:
<div id="app"> <child> <h1>Joe</h1> </child> </div> <script> Vue.component('child', { template: '<div><p>Hello</p><slot></slot></div>', }) var vm = new Vue({ el:'#app', }); </script>
结果:
例子:默认值
<div id="app"> <child> </child> </div> <script> Vue.component('child', { template: '<div><p>Hello</p><slot>默认内容</slot></div>', }) var vm = new Vue({ el:'#app', }); </script>
结果:
例子:具名插槽
<div id="app"> <child> <div class="header" slot="header">header</div> <div class="footer" slot="footer">footer</div> </child> </div> <script> Vue.component('child', { template: `<div> <slot name="header"></slot> <div class="content">content</div> <slot name="footer"></slot> </div>`, }) var vm = new Vue({ el:'#app', }); </script>
结果:
当然也可以有默认值:
<div id="app"> <child> <div class="footer" slot="footer">footer</div> </child> </div> <script> Vue.component('child', { template: `<div> <slot name="header">default header</slot> <div class="content">content</div> <slot name="footer"></slot> </div>`, }) var vm = new Vue({ el:'#app', }); </script>
作用域插槽:
当子组件循环或某一部分的dom结构应该由外部传递进来的时候,我们要用作用域插槽。使用作用域插槽,子组件可以向父组件的作用域插槽里传递数据,父组件如果想接收这个数据,必须在外层使用template模版占位符,同时通过slot-scope对应的属性名字,来接收你传递过来的数据。
例子:
<div id="app"> <child> <template slot-scope="scope"> <p>{{scope.item}}</p> </template> </child> </div> <script> Vue.component('child', { data() { return { list:[1, 2, 3, 4] } }, template: `<div> <slot v-for="item in list" :item=item>{{item}}</slot> </div>`, }) var vm = new Vue({ el:'#app', }); </script>
上面代码,传递一个item过来,在父组件的作用域插槽里面,就可以接收到这个item,就可以使用它了。
动态组件与v-once指令
动态组件:就是会根据is里面数据的变化,自动的加载不同的组件
例子:交替显示
<div id="app"> <child-one v-if="type === 'child-one'"></child-one> <child-two v-else></child-two> <button @click="changeBtn">change</button> </div> <script> Vue.component('child-one', { template: '<div>child-one</div>' }) Vue.component('child-two', { template: '<div>child-two</div>' }) var vm = new Vue({ el:'#app', data() { return { type: 'child-one' } }, methods: { changeBtn() { this.type = this.type === 'child-one' ? 'child-two' : 'child-one'; } }, }); </script> <!--使用动态组件实现一样的效果--> <div id="app"> <component :is="type"></component> <button @click="changeBtn">change</button> </div>
v-once指令:
当组件的内容每次都一样时,可以使用v-once指令,当第1个组件第一次被渲染的时候,因为组件上有个v-once指令,所以直接就放在内容里了,当切换后,第2个组件被渲染的时候,也会被放在内存里,当我们再点击切换的时候,这时候并不需要重新创建第1个组件,而是从内存里直接拿出以前的第1个组件。这样会节省性能。
例子:
<div id="app"> <child-one v-if="type === 'child-one'"></child-one> <child-two v-else></child-two> <button @click="changeBtn">change</button> </div> <script> Vue.component('child-one', { template: '<div v-once>child-one</div>' }) Vue.component('child-two', { template: '<div v-once>child-two</div>' }) var vm = new Vue({ el:'#app', data() { return { type: 'child-one' } }, methods: { changeBtn() { this.type = this.type === 'child-one' ? 'child-two' : 'child-one'; } }, }); </script>
5、VueX:状态管理
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
核心模块:State、Getters、Mutations、Actions、Module
(1)、State:
(2)、Getters:
Getter相当于vue中的computed计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算,这里我们可以通过定义vuex的Getter来获取,Getters 可以用于监听、state中的值的变化,返回计算后的结果。
(3)、Mutations:
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。但是,mutation只允许同步函数。
(4)、Actions:
官方并不建议我们直接去修改store里面的值,而是让我们去提交一个actions,在actions中提交mutation再去修改状态值。可以异步操作
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
(5)、Module
Vuex 允许我们将store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。
简要介绍各模块在流程中的功能:
- Vue Components:Vue 组件。HTML 页面上,负责接收用户操作等交互行为,执行 dispatch 方法触发对应 action 进行回应。
- dispatch:操作行为触发方法,是唯一能执行 action 的方法。
- actions:操作行为处理模块,由组件中的
$store.dispatch('action 名称', data1)
来触发。然后由 commit()来触发 mutation 的调用 , 间接更新 state。负责处理 Vue Components 接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台 API 请求的操作就在这个模块中进行,包括触发其他 action 以及提交 mutation 的操作。该模块提供了 Promise 的封装,以支持 action 的链式触发。 - commit:状态改变提交操作方法。对 mutation 进行提交,是唯一能执行 mutation 的方法。
- mutations:状态改变操作方法,由 actions 中的
commit('mutation 名称')
来触发。是 Vuex 修改 state 的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些 hook 暴露出来,以进行 state 的监控等。 - state:页面状态管理容器对象。集中存储 Vue components 中 data 对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用 Vue 的细粒度数据响应机制来进行高效的状态更新。
- getters:state 对象读取方法。图中没有单独列出该模块,应该被包含在了 render 中,Vue Components 通过该方法读取全局 state 对象。
例子:购物车数量改为用VueX管理
在store/index.js里
export default new Vuex.Store({ state: { buycarCount: 0 //1初始化buycarCount数量0 }, mutations: { //2定义同步方法改变count changbuycarCount(state, num) { state.buycarCount = num; } }, actions: {}, modules: {} });
Nav.vue:
<el-menu-item index="/home/buycar">购物车{{this.$store.state.buycarCount}}</el-menu-item> <!--2直接获取值--> <script> //import bus from '@/assets/bus.js'; 不用了注释掉 export default { 。。。 1.list不需要监听事件了,注释掉 created() { //生命周期 // bus.$on("buycarCountChang",(num) =>{ //监听事件buycarCountChang 购物车数量改变的时候触发事件 // this.buycarCount = num; // }) }, }; </script>
buycar.vue:
<script> //import bus from '@/assets/bus.js'; 同样注释掉 export default { 。。。 watch: { list:{ //监听list handler:function(){ //handler处理函数 let count = 0; this.list.forEach( item => { count += parseInt(item.count); //item.count累加 }) //bus.$emit("buycarCountChang",count); //购物车数量改变时触发buycarCountChang事件 //1注释上面代码改为下面代码 this.$store.commit("changbuycarCount", count); //购物车数量改变时触发changbuycarCount方法 }, deep:true //true的时候深度监听 } } }; </script>
OK,这样就实现了VueX改变接收购物车数量。
当然如果觉得{{this.$store.state.buycarCount}}这种获取数据太繁琐,可以用映射函数来实现。
Nav.vue:
<el-menu-item index="/home/buycar">购物车{{buycarCount}}</el-menu-item> <!--3直接使用{{buycarCount}}--> <script> //import bus from '@/assets/bus.js'; import {mapState} from "vuex"; //1映射函数 结构赋值 映射到计算属性 export default { name: "nav", props:{ NavActiveIndex:String }, data() { return { activeIndex: '1', activeIndex2: '123', //buycarCount: 0, //4注释掉上面定义的购物车数量 重名冲突 }; }, computed: { //计算属性 ...mapState(['buycarCount']), //2用...展开运算符把buycarCount展开在资源属性里 }, }; </script>
好了,用映射函数同样实现VueX改变接收购物车数量的功能。
那么我们的mutations也是有映射函数的,在buycar.vue里也用映射函数的方式来实现:
<script> //import bus from '@/assets/bus.js'; import {mapMutations} from 'vuex' //1映射函数 映射到方法 export default { 。。。 methods: { //2...展开运算符,传一个数组映射成同名的方法changbuycarCount ...mapMutations([ "changbuycarCount" ]), 。。。 }, watch: { //watch是vue的监听,一旦监听对象有变化就会执行相应操作 list:{ //监听list 当key是list,值是函数的时候,是不深度遍历的,也就是说对象改变的时候不监听,所以用这种写法 handler:function(){ //handler处理函数 let count = 0; this.list.forEach( item => { count += parseInt(item.count); //item.count累加 }) //bus.$emit("buycarCountChang",count); //购物车数量改变时触发buycarCountChang事件 //this.$store.commit("changbuycarCount", count); //购物车数量改变时触发changbuycarCount方法 //3注释上面代码改为下面代码 this.changbuycarCount(count); //this实例changbuycarCount方法,把count传过来 }, deep:true //true的时候深度监听 } } }; </script>
OK,功能同样实现。
下面action来实现异步操作:
在store/index.js里定义方法:
import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); export default new Vuex.Store({ state: { buycarCount: 0 //初始化0 }, mutations: { //定义同步方法改变count changbuycarCount(state, num) { state.buycarCount = num; } }, actions: { //异步改变 第1个参数是上下文 第2个接收调用传参 asyncchangbuycarCount(content, num) { //一般实现ajax,这里就不调用接口,实现一个定时器功能 setTimeout(() => { content.commit("changbuycarCount", num); //content对象的commit方法,来触发mutations }, 1000); } }, modules: {} });
修改buycar.vue:
<script> //import bus from '@/assets/bus.js'; import {mapMutations} from 'vuex' //映射函数 映射到方法 export default { 。。。 watch: { //watch是vue的监听,一旦监听对象有变化就会执行相应操作 list:{ //监听list 当key是list,值是函数的时候,是不深度遍历的,也就是说对象改变的时候不监听,所以用这种写法 handler:function(){ //handler处理函数 let count = 0; this.list.forEach( item => { count += parseInt(item.count); //item.count累加 }) //bus.$emit("buycarCountChang",count); //购物车数量改变时触发buycarCountChang事件 //this.$store.commit("changbuycarCount", count); //购物车数量改变时触发changbuycarCount方法 //this.changbuycarCount(count); //this实例changbuycarCount方法,把count传过来 //1注释上面代码改为下面代码 this.$store.dispatch("asyncchangbuycarCount", count); }, deep:true //true的时候深度监听 } } }; </script>
现在实现操作,1秒钟后改变购物车数量。
action同样有映射函数,直接修改buycar.vue:
<script> //import bus from '@/assets/bus.js'; import {mapMutations, mapActions} from 'vuex' //1映射函数 添加mapActions export default { 。。。 methods: { ...mapMutations(["changbuycarCount"]), //...展开运算符,传一个数组映射成同名的方法changbuycarCount ...mapActions(["asyncchangbuycarCount"]), //2展开 asyncchangbuycarCount }, watch: { //watch是vue的监听,一旦监听对象有变化就会执行相应操作 list:{ //监听list 当key是list,值是函数的时候,是不深度遍历的,也就是说对象改变的时候不监听,所以用这种写法 handler:function(){ //handler处理函数 let count = 0; this.list.forEach( item => { count += parseInt(item.count); //item.count累加 }) //bus.$emit("buycarCountChang",count); //购物车数量改变时触发buycarCountChang事件 //this.$store.commit("changbuycarCount", count); //购物车数量改变时触发changbuycarCount方法 //this.changbuycarCount(count); //this实例changbuycarCount方法,把count传过来 //this.$store.dispatch("asyncchangbuycarCount", count); //异步的改变asyncchangbuycarCount //3注释掉上面代码改为下面代码 this.asyncchangbuycarCount(count); }, deep:true //true的时候深度监听 } } }; </script>
同样实现1秒钟后改变购物车数量。
下面用getters来实现在导航Nav上显示名字的功能。
在store/index.js里定义:
import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); export default new Vuex.Store({ state: { buycarCount: 0, //初始化0 //1建立用户信息 userinfo: { name: "latte", age: 20 } }, mutations: { //定义同步方法改变count changbuycarCount(state, num) { state.buycarCount = num; } }, actions: { //异步改变 第1个参数是上下文 第2个接收调用传参 asyncchangbuycarCount(content, num) { //一般实现ajax,这里就不调用接口,实现一个定时器功能 setTimeout(() => { content.commit("changbuycarCount", num); //content对象的commit方法,来触发mutations }, 1000); } }, //2.getters返回计算属性 getters: { userName(state) { return state.userinfo.name; //返回用户name } }, modules: {} });
修改Nav.vue:
<div>{{userName}}</div> <!--3展示在nav--> <script> //import bus from '@/assets/bus.js'; import {mapState, mapGetters} from "vuex"; //1直接用映射函数 添加mapGetters export default { 。。。 computed: { //计算属性 ...mapState(['buycarCount']), //用...展开运算符把buycarCount展开在资源属性里 ...mapGetters(['userName']), //2...展开 }, 。。。 }; </script>
效果实现了。
注意: 官方建议如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。当然,还是需要您自己来决定。
Vuex 与 localStorage:
let defaultCity = "上海" try { // 用户关闭了本地存储功能,此时在外层加个try...catch if (!defaultCity){ defaultCity = JSON.parse(window.localStorage.getItem('defaultCity')) } }catch(e){} export default new Vuex.Store({ state: { city: defaultCity }, mutations: { changeCity(state, city) { state.city = city try { window.localStorage.setItem('defaultCity', JSON.stringify(state.city)); // 数据改变的时候把数据拷贝一份保存到localStorage里面 } catch (e) { } } } })
这里需要注意的是:由于 vuex 里,我们保存的状态,都是数组,而 localStorage 只支持字符串,所以需要用 JSON 转换:
JSON.stringify(state.subscribeList); // array -> string JSON.parse(window.localStorage.getItem("subscribeList")); // string -> array
6、$attrs
/$listeners
简介:多级组件嵌套需要传递数据时,通常使用的方法是通过 vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此 Vue2.4 版本提供了另一种方法----$attrs
/$listeners
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
// index.vue <template> <div> <h2>浪里行舟</h2> <child-com1 :foo="foo" :boo="boo" :coo="coo" :doo="doo" title="前端工匠" ></child-com1> </div> </template> <script> const childCom1 = () => import("./childCom1.vue"); export default { components: { childCom1 }, data() { return { foo: "Javascript", boo: "Html", coo: "CSS", doo: "Vue" }; } }; </script>
// childCom1.vue <template class="border"> <div> <p>foo: {{ foo }}</p> <p>childCom1的$attrs: {{ $attrs }}</p> <child-com2 v-bind="$attrs"></child-com2> </div> </template> <script> const childCom2 = () => import("./childCom2.vue"); export default { components: { childCom2 }, inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性 props: { foo: String // foo作为props属性绑定 }, created() { console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script>
// childCom2.vue <template> <div class="border"> <p>boo: {{ boo }}</p> <p>childCom2: {{ $attrs }}</p> <child-com3 v-bind="$attrs"></child-com3> </div> </template> <script> const childCom3 = () => import("./childCom3.vue"); export default { components: { childCom3 }, inheritAttrs: false, props: { boo: String }, created() { console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script>
// childCom3.vue <template> <div class="border"> <p>childCom3: {{ $attrs }}</p> </div> </template> <script> export default { props: { coo: String, title: String } }; </script>
效果:
如上图所示$attrs
表示没有继承数据的对象,格式为{属性名:属性值}。Vue2.4 提供了$attrs
, $listeners
来传递数据与事件,跨级组件之间的通讯变得更简单。
简单来说:$attrs
与$listeners
是两个对象,$attrs
里存放的是父组件中绑定的非 Props 属性,$listeners
里存放的是父组件中绑定的非原生事件。
7、provide/inject
简介:Vue2.2.0 新增 API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。// A.vue export default { provide: { name: '浪里行舟' } }
// B.vue export default { inject: ['name'], mounted () { console.log(this.name); // 浪里行舟 } }
可以看到,在 A.vue 里,我们设置了一个 provide: name,值为 浪里行舟,它的作用就是将 name 这个变量提供给它的所有子组件。而在 B.vue 中,通过 inject
注入了从 A 组件中提供的 name 变量,那么在组件 B 中,就可以直接通过 this.name 访问这个变量了,它的值也是 浪里行舟。这就是 provide / inject API 最核心的用法。
需要注意的是:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的----vue 官方文档
所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是 浪里行舟。
provide 与 inject 怎么实现数据响应式:
一般来说,有两种办法:
- provide 祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如 props,methods
- 使用 2.6 最新 API Vue.observable 优化响应式 provide(推荐)
// A 组件 <div> <h1>A 组件</h1> <button @click="() => changeColor()">改变color</button> <ChildrenB /> <ChildrenC /> </div> ...... data() { return { color: "blue" }; }, // provide() { // return { // theme: { // color: this.color //这种方式绑定的数据并不是可响应的 // } // 即A组件的color变化后,组件D、E、F不会跟着变 // }; // }, provide() { return { theme: this//方法一:提供祖先组件的实例 }; }, methods: { changeColor(color) { if (color) { this.color = color; } else { this.color = this.color === "blue" ? "red" : "blue"; } } } // 方法二:使用2.6最新API Vue.observable 优化响应式 provide // provide() { // this.theme = Vue.observable({ // color: "blue" // }); // return { // theme: this.theme // }; // }, // methods: { // changeColor(color) { // if (color) { // this.theme.color = color; // } else { // this.theme.color = this.theme.color === "blue" ? "red" : "blue"; // } // } // }
// F 组件 <template functional> <div class="border2"> <h3 :style="{ color: injections.theme.color }">F 组件</h3> </div> </template> <script> export default { inject: { theme: { //函数式组件取值不一样 default: () => ({}) } } }; </script>
虽说 provide 和 inject 主要为高阶插件/组件库提供用例,但如果你能在业务中熟练运用,可以达到事半功倍的效果!
8、$parent
/ $children
与 ref
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent
/$children
:访问父 / 子实例
需要注意的是:这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。我们先来看个用 ref
来访问组件的例子:
// component-a 子组件 export default { data () { return { title: 'Vue.js' } }, methods: { sayHello () { window.alert('Hello'); } } }
// 父组件 <template> <component-a ref="comA"></component-a> </template> <script> export default { mounted () { const comA = this.$refs.comA; console.log(comA.title); // Vue.js comA.sayHello(); // 弹窗 } } </script>
不过,这两种方法的弊端是,无法在跨级或兄弟间通信。
// parent.vue <component-a></component-a> <component-b></component-b> <component-b></component-b>
总结
常见使用场景可以分为三类:
- 父子通信:
父向子传递数据是通过 props,子向父是通过 events($emit
);通过父链 / 子链也可以通信($parent
/$children
);ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
- 兄弟通信:
Bus;Vuex - 跨级通信:
Bus;Vuex;provide / inject API、$attrs/$listeners
购物车
事件修饰符native
Vue 中的动画特效
transition 标签的使用方法:
主要用于 v-show, v-if 或 router-view 的进出场动画
语法:
<transition name="name" > <div v-show="show" ></div> <div v-if="show" ></div> <router-view/> </transition> <style> // 定义进入前与离开后状态 .name-enter, .name-leave-to { ... } // 定义离开前与进入后状态 .name-leave, .name-enter-to { ... } // 定义进出过程 .name-enter-active, .name-leave-active { transition: all .5s } </style>
1. fade 淡化进出
例子:
<style> /* .fade-enter { opacity: 0; } .fade-enter-active { transition: opacity 3s } */ .v-enter, .v-leave-to { opacity: 0; } .v-enter-active, .v-leave-active { transition: opacity 2s } </style> <div id="app"> <!-- <transition name="fade"> <div v-if="show">Hello World</div> </transition> --> <!-- 如果不写name,那么样式默认的前缀是v- --> <transition> <div v-if="show">Hello World</div> </transition> <button @click="handleClick">切换</button> </div>
2. scale 缩放进出
.scale-enter, .scale-leave-to { transform: scale(0) } .scale-leave, .scale-enter-to { transform: scale(1) } .scale-enter-active, .scale-leave-active { transition: all .2s }
3. left 左侧进出 (通常用于左侧边栏)
.left-enter, .left-leave-to { transform: translate3d(-100%, 0, 0) } .left-leave, .left-enter-to { transform: translate3d(0, 0, 0) } .left-enter-active, .left-leave-active { transition: all .2s }
4. right 右侧进出 (通常用于右侧边栏)
.right-enter, .right-leave-to { transform: translate3d(100%, 0, 0) } .right-leave, .right-enter-to { transform: translate3d(0, 0, 0) } .right-enter-active, .right-leave-active { transition: all .2s }
5. top 顶部进出 (通常用于提示弹窗)
.top-enter, .top-leave-to { transform: translate3d(0, -100%, 0) } .top-leave, .top-enter-to { transform: translate3d(0, 0, 0) } .top-enter-active, .top-leave-active { transition: all .2s }
Vue中使用animate.css库
例子:
<style> /* css3动画 */ @keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.5); } 100% { transform: scale(1); } } .fade-enter-active { transform-origin: left center; animation: bounce-in 1s; } .fade-leave-active { transform-origin: left center; animation: bounce-in 1s reverse; /* 离开的时候反向执行 */ } </style> <div id="app"> <transition name="fade" > <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
点击按钮后会有一个放大缩小的效果。
也可以自定义名字:还是上面的例子
<style> /* css3动画 */ @keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.5); } 100% { transform: scale(1); } } .active { transform-origin: left center; animation: bounce-in 1s; } .leave { transform-origin: left center; animation: bounce-in 1s reverse; /* 离开的时候反向执行 */ } </style> <div id="app"> <transition name="fade" enter-active-class="active" leave-active-class="leave"> <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
效果完全是一样的。
通过上面例子就可以使用animate.css库了 https://daneden.github.io/animate.css/
首先需要把animate.css库下载下来,当然也可以直接引用cdn的连接。
然后必须使用自定义class这种形式,还有class里必须有一个animated这个类,同时在添加你需要的动画效果的类名。
例子:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css"> <div id="app"> <transition name="fade" enter-active-class="animated swing" leave-active-class="animated shake"> <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
这样复杂的动画就不需要写了,直接引用animate库就行了。
继续上面那个例子来说,点击toggle按钮时会有动画,但是我们刷新页面的时候没有,下面我们把第一次显示时也加上动画。
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css"> <div id="app"> <transition name="fade" appear enter-active-class="animated swing" leave-active-class="animated shake" appear-active-class="animated swing" > <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
Vue中同时使用过渡和动画
例子:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css"> <style> .fade-enter, .fade-leave-to { opacity: 0; } .fade-enter-active, .fade-leave-active { transition: opacity 3s; } </style> <div id="app"> <transition name="fade" appear enter-active-class="animated swing fade-enter-active" leave-active-class="animated shake fade-leave-active" appear-active-class="animated swing" > <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
效果及既有过渡也有动画效果了。
我们在过渡设置的时间是3s,查看animate库里的样式animated设置动画时间的却是1s。这种情况下,我们可以进行手动设置:
<transition type="transition" name="fade" appear enter-active-class="animated swing fade-enter-active" leave-active-class="animated shake fade-leave-active" appear-active-class="animated swing" > <div v-show="show">Hello World</div> </transition>
还可以自定义动画时长:
例子:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css"> <style> .fade-enter, .fade-leave-to { opacity: 0; } .fade-enter-active, .fade-leave-active { transition: opacity 3s; } </style> <div id="app"> <transition :duration="5000" name="fade" appear enter-active-class="animated swing fade-enter-active" leave-active-class="animated shake fade-leave-active" appear-active-class="animated swing" > <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
打开控制台发现:动画3s结束后样式依然存在,5s后样式才会隐藏。
动画还可以设置的复杂一点,入场、出场分别设置,出场3秒,入场5秒 :duration="{enter: 5000,leave: 3000}"
例子:
<div id="app"> <transition :duration="{enter: 5000,leave: 3000}" name="fade" appear enter-active-class="animated swing fade-enter-active" leave-active-class="animated shake fade-leave-active" appear-active-class="animated swing" > <div v-show="show">Hello World</div> </transition>
Vue中Js的动画钩子
例子:
<div id="app"> <transition name="fade" @before-enter="handleBeforeEnter" @enter="handleEnter" @after-enter='handleAfterEnter'> <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show }, // 当元素从隐藏到显示的时候会触发 handleBeforeEnter(el) { el.style.color = 'red' }, // 当before-enter被触发结束后,下步运行动画效果的时候,动画效果写在enter这个钩子对应的回调函数里handleEnter handleEnter(el, done) { setTimeout(() => { el.style.color = 'green' // done() }, 2000) setTimeout(() => { done() // 动画结束 }, 4000) }, handleAfterEnter(el) { el.style.color = 'black' } }, }); </script>
当然还有出场动画对应的Js钩子:before-leave、leave、after-leave。
Js常用的动画库Velocity.js
http://www.velocityjs.org/
例子:
<div id="app"> <transition name="fade" @before-enter="handleBeforeEnter" @enter="handleEnter" @after-enter='handleAfterEnter'> <div v-show="show">Hello World</div> </transition> <button @click="handleClick">toggle</button> </div> <script src="https://cdn.bootcss.com/velocity/2.0.5/velocity.min.js"></script> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show }, // 当元素从隐藏到显示的时候会触发 handleBeforeEnter(el) { el.style.opacity = 0; }, // 当before-enter被触发结束后,下步运行动画效果的时候,动画效果写在enter这个钩子对应的回调函数里handleEnter handleEnter(el, done) { Velocity(el, { opacity: 1 }, { duration: 1000, complete: done }) // 动画从opacity:0 到 opacity:1 耗时1秒,当Velocity执行完这个动画后,complete这个属性对应的内容会被自动执行,也就是说done这个回调函数会被执行 }, handleAfterEnter(el) { console.log('动画结束'); } }, }); </script>
可以看到效果一样。
Vue中多个元素或组件的过渡动画
例子:多个元素的过渡动画
<style> .v-enter, .v-leave-to { opacity: 0; } .v-enter-active, .v-leavev-active { transition: opacity 2s; } </style> <div id="app"> <transition mode="in-out"> <!-- in-out 表示先进入后隐藏;out-in 表示先隐藏后显示--> <!--dom不进行复用,加key值--> <div v-if="show" key="hello">Hello World</div> <div v-else key="bye">Bye World</div> </transition> <button @click="handleClick">toggle</button> </div> <script> var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show }, }, }); </script>
效果是bye world先显示,然后hello world隐藏。
例子:多个组件的过渡动画
<style> .v-enter, .v-leave-to { opacity: 0; } .v-enter-active, .v-leavev-active { transition: opacity 2s; } </style> <div id="app"> <transition mode="in-out"> <!-- in-out 表示先进入后隐藏;out-in 表示先隐藏后显示--> <child v-if="show"></child> <child-one v-else></child-one> </transition> <button @click="handleClick">toggle</button> </div> <script> Vue.component('child', { template: '<div>child</div>' }) Vue.component('child-one', { template: '<div>child-one</div>' }) var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show }, }, }); </script>
效果和上面是一样的。
例子:通过动态组件,实现多个组件的过渡动画
<style> .v-enter, .v-leave-to { opacity: 0; } .v-enter-active, .v-leavev-active { transition: opacity 2s; } </style> <div id="app"> <transition mode="in-out"> <!-- in-out 表示先进入后隐藏;out-in 表示先隐藏后显示--> <component :is="type"></component> </transition> <button @click="handleClick">toggle</button> </div> <script> Vue.component('child', { template: '<div>child</div>' }) Vue.component('child-one', { template: '<div>child-one</div>' }) var vm = new Vue({ el:'#app', data() { return { type: 'child' } }, methods: { handleClick() { this.type = this.type === 'child' ? 'child-one' : 'child' }, }, }); </script>
当然效果还是一样的。
Vue中的列表过渡动画
例子:
<style> .v-enter, .v-leave-to { opacity: 0; } .v-enter-active, .v-leave-active { transition: opacity 2s; } </style> <div id="app"> <transition-group> <div v-for="item of list" :key="item.id">{{item.id}}--{{item.title}}</div> </transition-group> <button @click="handleBtn">Add</button> </div> <script> var count = 0; var vm = new Vue({ el:'#app', data() { return { list: [] } }, methods: { handleBtn() { this.list.push({ id: count++, title: 'hello world' }) } }, }); </script>
通过transition-group实现列表过渡动画。
Vue中的动画封装
通过动画的封装来实现复用。
例子:
<style> .v-enter, .v-leave-to { opacity: 0; } .v-enter-active, .v-leave-active { transition: opacity 2s; } </style> <div id="app"> <fade :show="show"> <div>Hello World</div> </fade> <fade :show="show"> <h1>Hello World</h1> </fade> <button @click="handleClick">toggle</button> </div> <script> Vue.component('fade', { props: ['show'], template: ` <transition> <slot v-if="show"></slot> </transition> ` }) var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
这就实现了一个动画的封装,每次使用fade组件就可以了
下面把样式也一起封装,不用css动画,使用js动画:
<div id="app"> <fade :show="show"> <div>Hello World</div> </fade> <fade :show="show"> <h1>Hello World</h1> </fade> <button @click="handleClick">toggle</button> </div> <script> Vue.component('fade', { props: ['show'], template: ` <transition @before-enter="handleBeforeEnter" @enter="handleEnter"> <slot v-if="show"></slot> </transition> `, methods: { handleBeforeEnter(el) { el.style.color = 'red' }, handleEnter(el, done) { setTimeout(() => { el.style.color = 'green' done() },2000) } } }) var vm = new Vue({ el:'#app', data() { return { show: true } }, methods: { handleClick() { this.show = !this.show } }, }); </script>
效果:进入的时候先是红色,2秒后变为绿色。