Vue 核心基础 官网传送门
Vue 是动态构建用户界面的渐进式 JavaScript 框架
Vue 借鉴 Angular 的模板和数据绑定技术,React 的组件化和虚拟 DOM 技术
数据驱动视图 MVVM模型
M:模型Model ,data
中的数据
V:视图View ,模板代码
VM:视图模型ViewModel ,Vue实例
观察发现
data
中所有的属性,最后都出现在了vm
身上
vm
身上所有的属性及Vue原型
身上所有的属性,在Vue模板
中都可以直接使用
MVVM:数据驱动视图,即通过修改视图就能改变数据,反过来也可以。
本质:事件 + 方法 + 改变数据 = ViewModel
1 <p @click ="changeName" > {{name}}</p > // 事件
1 2 3 4 5 6 7 8 9 10 11 data ( ){ return { name :'vue' , list : ['a' ,'b' ,'c' ] } } methods : { changeName ( ){ this .name = "双越" } }
数据代理 Object.defineProperty() 的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 let person = { name : 'Vue' , sex : 'none' , } let number = 19 Object .defineProperty (person, 'age' , { value : 21 , writable : true , enumerable : true , configurable : true , }) Object .keys (person)Object .defineProperty (person, 'age' , { enumberable : true , configurable : true , get ( ) { console .log ('age 属性被读取' ) return number } set (value ) { console .log ('age 属性被修改' , value) number = value } })
何为数据代理 数据代理:通过一个对象对另一个对象的属性进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let obj = { a : 21 }let obj2 = { b : 10 }Object .defineProperty (obj2, 'a' , { get ( ) { return obj.a } set (value ) { obj.a = value } }) obj2.a obj2.a = 1000 obj.a
Vue 中的数据代理
Vue 中通过 vm 实例对象代理对 data 对象属性的操作,让我们更方便操作 data 中的数据。
data 中的数据实际上被存在 vm._data
属性上,如果不进行代理,使用起来很不方便。
通过 Object.defineProperty()
给 vm 添加属性,并且指定 getter 和 setter,通过 getter 和 setter 访问和修改 data 对应是属性。
Vue 监测数据的原理
监测数据,即 Vue 是如何监听数据发生变化,从而重新解析模板渲染页面的。Vue 会监测 data 中所有层级的数据。
Vue 监测对象数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 let person = { name : 'Vue' , age : 99 } function Observer (obj ) { const keys = Object .keys (obj) keys.forEach (key => { Object .defineProperty (this , key, { get ( ) { return obj[key] } set (value ) { console .log ('数据被修改,重新解析模板...' ) obj[key] = value } }) }) } let vm = {}let observer = new Observer (person)vm._data = observer
Vue 监测数组
原理:通过重写数组的 API 实现拦截:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
7 个 API 之所以是响应式的,是因为 Vue 对这些方法进行了包裹 ,即二次封装。做了两件事:调用对应的原生方法更新数组 & 重新解析模板更新页面
存在问题:
解决办法:
使用 7 个 API 修改数组
Vue.set()
、vm.$set()
Vue.delete()
、vm.$delete()
思否文章
插值语法 1 2 3 4 5 <p > 用户名:{{ username }}</p > <p > 密码{{ password }}</p > <p > {{ flag ? 111 : 000}}</p >
1 2 3 4 5 6 7 data ( ) { return { username : 'docsify' , password : 55520 , flag : true } }
属性绑定指令 v-bind 1 2 3 4 5 6 7 <input type ="text" v-bind:placeholder ="desc" /> <img :src ="url" alt ="这是一张图片" /> <div :id ="'hello' + 1" > </div >
1 2 3 4 5 6 7 data ( ) { return { desc : '请输入用户名' , url : 'www.baidu.com' , name : 'hello' } }
双向绑定指令 v-model v-model
用于表单元素如 input
,textarea
,select
。
v-model 基础用法 1 2 3 4 5 6 7 8 9 10 11 <p > {{ username }}</p > <input type ="text" v-model:value ="username" /> <input type ="text" v-model ="username" /> <p > {{ province }}</p > <select v-model ="province" > <option value ="" > 请选择</option > <option value ="1" > 北京</option > <option value ="2" > 上海</option > <option value ="3" > 广州</option > </select >
v-model 指令修饰符
修饰符
作用
示例
.number
将用户输入转为数值类型
<input v-model.number="age" />
.trim
删除输入的首尾空白字符
<input v-model.trim="msg">
.lazy
当失去焦点时,才更新数据,类似防抖
<input v-model.lazy="msg">
v-model 收集表单数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <div id ="root" > <form @submit.prevent ="demo" > 账号:<input type ="text" v-model.trim ="userInfo.account" /> 密码:<input type ="password" v-model ="userInfo.password" /> 年龄:<input type ="text" v-model.number ="userInfo.age" /> 性别: 男<input type ="radio" name ="sex" v-model ="userInfo.sex" value ="male" /> 女<input type ="radio" name ="sex" v-model ="userInfo.sex" value ="female" /> 爱好: 学习<input type ="checkbox" v-model ="userInfo.hobby" value ="study" /> 打游戏<input type ="checkbox" v-model ="userInfo.hobby" value ="game" /> 吃饭<input type ="checkbox" v-model ="userInfo.hobby" value ="eat" /> 所属校区 <select v-model ="userInfo.city" > <option value ="" > 请选择校区</option > <option value ="beijing" > 北京</option > <option value ="shanghai" > 上海</option > <option value ="shenzhen" > 深圳</option > <option value ="wuhan" > 武汉</option > </select > 其他信息: <textarea v-model.lazy ="userInfo.other" > </textarea > <input type ="checkbox" v-model ="userInfo.agree" /> 阅读接受协议 <button > 提交</button > </form > </div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 data ( ) { return { userInfo :{ account :'' , password :'' , age :18 , sex :'female' , hobby :[], city :'beijing' , other :'' , agree :'' } } }, methods : { demo ( ) { console .log (JSON .stringify (this .userInfo )) } }
事件绑定指令 v-on v-on 基础用法 1 2 3 4 5 <p > count的值:{{ count }}</p > <button v-on:click ="add" > +1</button > <button @click ="add" > +1</button >
1 2 3 4 5 6 7 8 9 10 data ( ) { return { count : 1 } }, methods : { add ( ) { this .count ++ } }
事件参数对象 如果事件处理函数没有传参,则默认会传一个时间参数对象 $event
,通过它可以获取触发事件的元素,并进行相关操作。
1 2 3 4 5 6 methods : { add (e ) { e.target .style .backgroundColor = 'red' this .count ++ } }
如果事件处理函数传递参数了,则默认的 $event
会被覆盖,需要手动进行传递。
1 <button @click ="add(2, $event)" > +1</button >
1 2 3 4 5 6 methods : { add (step, e ) { e.target .style .backgroundColor = 'red' this .count += step } }
事件修饰符
事件修饰符
说明
.prevent
阻止默认行为,如 a 链接跳转、表单提交
.stop
阻止事件冒泡
.once
绑定的事件只触发 1 次
.capture
以捕获模式触发事件处理函数
.self
只有在 event.target
是当前元素自身时触发事件处理函数
.passive
事件的默认行为立即执行,无需等待事件回调执行完毕
1 2 3 4 5 6 7 <a href ="www.baidu.com" @click.prevent ="fn" > 阻止链接跳转</a > <div @click.stop ="handleClick" > 阻止事件冒泡</div >
按键修饰符
Vue 中常用的按键别名:
回车 => enter
删除 => delete (捕获“删除”和“退格”键)
退出 => esc
空格 => space
换行 => tab (特殊,必须配合 keydown 去使用)
上 => up
下 => down
左 => left
右 => right
Vue 未提供别名的按键,可以使用按键原始的 key
值去绑定,但注意要转为 kebab-case(短横线命名)
系统修饰键(用法特殊):ctrl、alt、shift、meta(即 win 键)
配合 keyup 使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
配合 keydown 使用:正常触发事件。
可使用 keyCode 去指定具体的按键,此法不推荐,因为 keyCode 以后可能废除
Vue.config.keyCodes.自定义键名 = 键码
,可以去定制按键别名
1 2 3 4 5 6 7 8 9 10 <input type ="text" @keyup.enter ="submit" /> <input type ="text" @keyup.esc ="back" /> <input type ="text" @keydown.tab ="showInfo" /> <input type ="text" @keyup.caps-lock ="showInfo" /> <input type ="text" @keyup.huiche ="showInfo" /> <input type ="text" @keyup.13 ="showInfo" /> <script > Vue.config.keyCodes.huiche = 13 </script >
条件渲染指令 基础用法 1 2 3 4 5 6 7 8 9 10 11 12 <p v-if ="status === 200" > success</p > <p v-else-if ="status === 201" > xxx</p > <p v-else > yyy</p > <p v-show ="status === 404" > error</p > <template v-if ="status === 200" > <p > 111</p > <p > 222</p > <p > 333</p > </template >
v-if 和 v-show 的区别 实现原理不同:
v-if
通过创建或删除 DOM 元素来控制元素的显示与隐藏
v-show
通过添加或删除元素的 style="display: none"
样式来控制元素的显示与隐藏
性能消耗不同:
v-if
切换开销更高,如果运行时条件很少改变,使用 v-if
更好
v-show
初始渲染开销更高,如果切换频繁,使用 v-show
更好
列表渲染指令 v-for 基本用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <ul > <li v-for ="(item, index) in list" :key ="item.id" > {{ item.name }}</li > </ul > <ul > <li v-for ="(value, key) in obj" :key ="key" > {{ key }} - {{ value }}</li > </ul > <ul > <li v-for ="(char, index) in str" :key ="index" > {{ index }} - {{ char }}</li > </ul > <ul > <li v-for ="(number, index) in 5" :key ="index" > {{ index }} - {{ number }}</li > </ul >
1 2 3 4 5 6 7 8 9 10 11 data ( ) { return { list : [...], obj : { name : 'Bruce' , age : 88 , sex : 'unknown' }, str : 'hello vue' } }
key 的作用 key
的作用:
当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新。
为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲染的性能。此时,需要为每项提供一个唯一的 key 属性。
key
是虚拟 DOM 对象的标识,可提高页面更新渲染的效率。当数据变化时,Vue 会根据新数据生成新的虚拟 DOM,随后进行新旧虚拟 DOM 的差异比较
比较规则
旧虚拟 DOM 找到和新虚拟 DOM 相同的 key:
若内容没变,直接复用真实 DOM
若内容改变,生成新的真实 DOM,替换旧的真实 DOM
旧虚拟 DOM 未找到和新虚拟 DOM 相同的 key:创建新的真实 DOM,渲染到页面
key
的注意事项:
key 的值只能是字符串 或数字 类型
key 的值必须具有唯一性(即:key 的值不能重复)
建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
使用 index 的值当作 key 的值没有意义(因为 index 的值不具有唯一性)
建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)
其它内置指令 v-text v-text
指令会覆盖元素默认值。
1 <p v-text ="username" > 这段内容会被覆盖</p >
1 data ( ) { return { username : "Bruce" } }
v-html
v-html 存在安全问题,容易导致 XSS 攻击
1 <p v-html ="desc" > 原本内容被覆盖</p >
1 2 3 4 5 6 data ( ) { return { desc : '<h1 style="color: red">红色标题</h1>' , str : '<a href="http://www.baidu.com?"+document.cookie>兄弟我找到你想要的资源了,快来!</a>' } }
v-cloak
本质是一个特殊属性,Vue 实例创建完毕并接管容器后,会删除 v-cloak
属性
使用 CSS 配合 v-cloak
可解决网速慢时页面展示 的问题
1 2 3 [v-cloak] { display : none; }
1 <h2 v-cloak > {{ username }}</h2 >
v-once
v-once
所在节点初次渲染后就成为静态内容
即数据变化不会引起 v-once
所在节点内容的更新,可用于优化性能
1 2 <h2 v-once > 初次的内容:{{ content }}</h2 > <h2 > 最新的内容:{{ content }}</h2 >
v-pre
跳过所在节点的编译过程
没有使用插值语法等特殊语法的节点,可用其跳过编译过程,加快编译
1 2 <h2 v-pre > Vue 内置指令</h2 > <p > 用户名:{{ username }}</p >
过滤器
过滤器常用于文本的格式化,可用在插值表达式和 v-bind
属性绑定。
过滤器只在 vue 2.x
和 vue 1.x
中支持,vue 3.x
废弃了过滤器,官方建议使用计算属性或方法代替过滤器。
基本使用 1 2 3 4 <p > {{ message | capitalize }}</p > <div :id ="rawId | formatId" > </div >
1 2 3 4 5 6 filters : { capitalize (str ) { return str.charAt (0 ).toUpperCase () + str.slice (1 ) } }
1 2 3 4 Vue .filter ('capitalize' , (str ) => { return str.charAt (0 ).toUpperCase () + str.slice (1 ) })
如果私有过滤器和全局过滤器冲突,按照就近原则调用私有过滤器。
连续调用多个过滤器 过滤器从左往右调用,前一个过滤器的结果交给下一个过滤器继续处理。
1 <p > {{ text | capitalize | maxLength }}</p >
过滤器传参 1 <p > {{ message | myFilter(arg1, arg2) }}</p >
1 2 3 4 Vue .filter ('myFilter' , (value, arg1, arg2 ) => { ... })
computed 计算属性
定义:使用的属性不存在,要通过已有属性计算得到
原理:底层使用了 Object.defineProperty()
提供的 getter 和 setter
getter 何时执行:
优点:与 methods
相比,有缓存机制,效率更高
若计算属性要修改,必须声明 setter 响应修改,且 setter 中要引起依赖的数据发生改变
1 <span > {{ fullName }}</span >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 computed : { fullName : { get ( ) { return this .firstName + '-' + this .lastName } set (value ) { const arr = value.split ('-' ) this .firstName = arr[0 ] this .lastName = arr[1 ] } } } computed : { fullName ( ) { return this .firstName + this .lastName } }
watch 侦听器 watch
侦听器允许开发者监视数据的变化,针对数据的变化做特定的操作。
侦听器可以监听普通属性和计算属性
computed
能完成的功能,watch
也能
watch
能完成的功能,computed
不一定,如异步操作
Vue 管理的函数写成普通函数,使其 this
指向 vue 实例对象
不被 Vue 管理的函数写成箭头函数(定时器回调、ajax
回调、Promise
回调),这样其 this
才是 vue 实例对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export default { data ( ) { return { username : '' , } }, watch : { username (newVal, oldVal ) { console .log ('新值: ' , newVal) console .log ('旧值: ' , oldVal) }, }, }
默认情况下,组件在初次加载完毕后不会调用 watch
侦听器。如果想让 watch
侦听器立即被调用,需要使用 immediate
选项:
1 2 3 4 5 6 7 8 9 10 watch : { username : { handler (newVal, oldVal ) { ... }, immediate : true } }
当 watch
侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep
选项进行深度监听 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default { data ( ) { return { info : { username : 'admin' }, } }, watch : { info : { handler (newVal ) { console .log (newVal) }, deep : true , }, }, }
若只想监听对象里单个属性的变化,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default { data ( ) { return { info : { username : 'admin' }, } }, watch : { 'info.username' : { handler (newVal ) { console .log (newVal) }, }, }, }
通过 Vue 实例的 $watch
监听:
1 2 3 4 5 6 7 8 9 10 11 12 13 const vm = new Vue ({...})vm.$watch('isHot' ,{ immediate : true , deep : true , handler (newValue,oldValue ) { console .log (newValue, oldValue) } }) vm.$watch('isHot' ,function (newValue,oldValue ) { console .log (newValue, oldValue) })
动态绑定 class 和 style 通过动态绑定 class
属性和行内 style
样式,可动态操作元素样式。
动态绑定 class 类名
字符串写法:类名不确定,要动态获取
对象写法:要绑定多个样式,个数不确定,名字也不确定
数组写法:要绑定多个样式,个数确定,名字也确定,但不确定用不用
1 2 3 4 5 6 7 8 <style> .happy { ...; } .sad { ...; } </style>
1 2 3 <div class ="basic" :class ="mood" @click ="changeMood" > 字符串写法</div > <div class ="basic" :class ="arr" > 数组写法</div > <div class ="basic" :class ="classObj" > 对象写法</div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export default { data ( ) { return { mood : 'happy' , arr : ['happy' , 'sad' ], classObj : { happy : true , sad : false , }, } }, methods : { changeMood ( ) { this .mood = 'sad' }, }, }
动态绑定 style 样式 css
属性名既可以用驼峰形式,也能用短横线形式(需要使用引号括起来)。
1 2 3 4 <div :style ="{color: active, fontSize: fsize + 'px', 'background-color': bgcolor}" > 对象写法</div > <div :style ="styleObj" > 对象写法</div > <div :style ="[styleObj, styleObj2]" > 数组写法(用得少)</div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 data ( ) { return { active : 'red' , fsize : 30 , bgcolor : 'yellow' , styleObj : { color : 'active' , 'font-size' : '20px' }, styleObj2 : { backgroundColor : 'yellow' } } }
vue 生命周期 vue 生命周期是指一个组件从创建、运行、销毁的整个过程。每个阶段对应着不同的生命周期钩子。
生命周期钩子也可理解为:Vue 在特定的时刻调用特定的函数。
除了图中 8 个钩子,还有 nextTick
,activated
,deactivated
关于销毁过程:
销毁后借助 Vue 开发者工具看不到任何信息。
销毁后自定义事件会失效,但原生 DOM 事件依然有效。
一般不在 beforeDestroy
操作数据,因为即便操作数据,也不会再触发更新流程
Vue 组件化编程
组件:实现应用局部功能代码和资源的集合
非单文件组件 非单文件组件即所有组件写在同一个文件里。
基本使用 定义组件:
使用Vue.extend(options)
创建,和new Vue(options)
的区别
el
不写,最终所有的组件都要经过 vm 的管理,由 vm 的 el
决定服务哪个容器
data
必须写成函数,避免组件被复用时,数据存在引用关系
使用 template
节点可配置组件结构
注册组件;
局部注册:components
选项
全局注册:Vue.component('组件名',组件)
使用组件:<school></school>
1 2 3 4 <div id ="root" > <hello > </hello > <school > </school > </div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const student = Vue .extend ({ template : ` <div> <h2>学生姓名:{{studentName}}</h2> </div> ` , data ( ) { return { studentName : '张三' , } }, }) const hello = Vue .extend ({ template : ` <div> <h2>{{name}}</h2> </div> ` , data ( ) { return { name : 'Tom' , } }, }) const school = Vue .extend ({ name : 'school' , template : ` <div> <h2>学校名称:{{name}}</h2> <student></student> </div> ` , data ( ) { return { name : '北京大学' , } }, components : { student, }, }) Vue .component ('hello' , hello)new Vue ({ el : '#root' , components : { school, }, })
注意事项:
组件名
一个单词:school, School
多个单词:my-school, MySchool(需要 vue-cli 支持)
使用组件
<school></school>
<school />
(需要 vue-cli 支持)
const school = Vue.extend(options)
可简写为 const school = options
。这是脚手架里 <script>
代码的简写来源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import HelloWorld from './components/HelloWorld.vue' export default { name : 'App' , components : { HelloWorld } } const vc = Vue .extend ({ name : 'App' , components : { HelloWorld } }) export default vc
关于 VueComponent 构造函数
组件本质是一个名为 VueComponent
的构造函数,不是程序员定义的,是 Vue.extend
生成的
1 2 3 const school = Vue .extend ({...})console .dir (school)
使用组件时,Vue 自动创建组件实例对象,即 new VueComponent(options)
是 Vue 做的
每次调用 Vue.extend
,返回的都是一个全新的 VueComponent
构造函数
1 2 3 4 const school = Vue .extend ({...})const student = Vue .extend ({...})console .log (school === student)
组件的 this
指向 VueComponent
实例对象,而非 Vue 实例对象
重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
,这个改动使得组件实例对象得以访问 Vue 原型上的属性方法
单文件组件 单文件组件即 .vue
文件
scoped 解决样式冲突
原理:为当前组件所有 DOM 元素分配唯一的自定义属性,写样式时使用属性选择器防止样式冲突问题
scoped
只给子组件最外层的 div 添加了自定义属性 [data-v-xxx]
,子组件内部的标签并没有添加。因此父组件只能修改子组件最外层的 div 样式,修改子组件内层元素的样式是不可行的
若想让某些样式对子组件生效,需使用 /deep/
深度选择器
1 2 3 4 5 6 7 8 9 10 11 12 <style lang="less" scoped> .title { color : blue; } /deep/ .title { color : blue; } </style>
组件通信 自定义属性 props
父传子、子传父
props
验证:
基础类型检查:String, Number, Boolean, Array, Object, Date, Function, Symbol
多个可能的类型
必填项检查
默认值
自定义验证函数 validator
props
是只读的,若是对象,对象内部的修改不报错,但不推荐。若需修改,则把 props
内容拷贝一份到 data
进行修改
父传子:
1 2 3 <Son :num ="count" :msg ="message" :pub-time ="time" > </Son >
1 2 3 <p > 父组件传过来的值:{{ num }}</p > <p > 父组件传过来的值:{{ msg }}</p >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 props : ['num' , 'msg' , 'pubTime' ]props : { num : Number , msg : String } props : { num : { type : Number , default : 0 }, msg : { type : [String , Number ], required : true , validator (value ) { return value === 'hello' || value === 1 }, default : 1 } }
子传父:
父组件通过 props
给子组件传递函数,子组件调用该函数即可修改父组件的数据
组件 methods
里函数的 this 始终指向该组件实例,可理解为 Vue 底层对这些函数做了bind
处理
通过bind
修改 this 指向后的新函数,其 this 指向不能再次修改。官网说明
思否文章
不推荐该方式进行子传父,推荐使用自定义事件
1 <Son :addCount ="addCount" > </Son >
1 2 3 4 5 6 7 8 9 10 11 12 export default { data ( ) { return { count : 1 , } }, methods : { addCount ( ) { this .count ++ }, }, }
1 2 3 4 5 6 7 8 export default { props : ['addCount' ], methods : { add ( ) { this .addCount () }, }, }
自定义事件
自定义事件可用于实现子传父
子组件触发自定义事件,并传递数据:
1 2 3 4 5 6 7 8 9 10 11 12 data ( ) { return { count : 1 } }, methods : { add ( ) { this .count += 1 this .$emit('count-change' , this .count ) } }
父组件监听子组件的自定义事件,并调用回调函数处理数据:
父组件通过 this.$refs.xxx.$on('事件名称',回调)
监听子组件自定义事件时,回调函数要么配置在 methods
中,要么用箭头函数,否则 this 指向会出问题
组件上也可以绑定原生 DOM 事件,需要使用 native
修饰符
若想让自定义事件只触发一次,可以使用 once
修饰符,或 $once
方法
1 2 3 4 5 6 7 8 9 <Son @count-change ="getNewCount" > </Son > <Son @count-change.once ="getNewCount" > </Son > <Son ref ="sonRef" > </Son > <Son @click.native ="handleClick" > </Son >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export default { data ( ) { return { father : 1 , } }, methods : { getNewCount (val ) { this .father = val }, }, mounted ( ) { this .$refs .sonRef .$on('count-change' , this .getNewCount ) this .$refs .sonRef .$once('count-change' , this .getNewCount ) this .$refs .sonRef .$on('count-change' , (val ) => (this .father = val)) }, }
解绑自定义事件this.$off()
:
1 2 3 4 5 6 this .$off('count-change' )this .$off(['count-change' , 'add' ])this .$off()
EventBus 全局事件总线
思想:弄一个所有组件实例都能访问到的 Vue 实例对象,Vue 原型上包含事件处理的相关方法,包括 $on, $emit, $off, $once
方式一
安装全局事件总线:
1 2 3 4 5 6 7 8 new Vue ({ ... beforeCreate ( ) { Vue .prototype .$bus = this } ... })
数据接收方为自定义事件绑定回调函数:
1 2 3 4 5 6 7 8 9 10 11 12 export default { methods : { handleData ( ) {...} }, created ( ) { this .$bus .$on('share' , this .handleData ) }, beforeDestroy ( ) { this .$bus .$off('share' ) } }
数据发送方触发自定义事件:
1 2 3 4 5 6 7 export default { methods : { sendData ( ) { this .$bus .$emit('share' , 666 ) }, }, }
方式二
创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象。
1 2 3 4 import Vue from 'vue' export default new Vue ()
在数据发送方,调用 bus.$emit('事件名称', 要发送的数据)
方法触发自定义事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import bus from './eventBus.js' export default { data ( ) { return { message : 'hello' , } }, methods : { sendData ( ) { bus.$emit('share' , this .message ) }, }, }
在数据接收方,通过 bus.$on('事件名称', 事件处理函数)
为自定义事件注册事件处理函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import bus from './eventBus.js' export default { data ( ) { return { msg : '' , } }, created ( ) { bus.$on('share' , (val ) => { this .msg = val }) }, }
消息订阅与发布
与全局事件总线很相似,因此一般用事件总线,不用这个
安装第三方库 PubSubJS
:npm install -S pubsub.js
订阅消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import pubsub from 'pubsub-js' export default { methods : { handleData (messageName, data ) {...} }, created ( ) { this .pubId = pubsub.subscribe ('share' , this .handleData ) this .pubId = pubsub.subscribe ('share' , (messageName, data ) => { console .log (data) }) }, beforeDestroy ( ) { pubsub.unsubscribe (this .pubId ) } }
发布消息:
1 2 3 4 5 6 7 8 9 import pubsub from 'pubsub-js' export default { methods : { sendData ( ) { pubsub.publish ('share' , 666 ) }, }, }
ref / $refs ref
用于给 DOM 元素或子组件注册引用信息。每个 vue 实例都有 $refs
对象,里面存储着 DOM 元素或子组件的引用。通过该方式可以获取到 DOM 元素或子组件实例。
可以父传子,也能子传父。子传父要和自定义事件搭配使用。
1 2 3 4 5 6 7 <p ref ="pp" > 这是段落</p > <button @click ="getRef" > 获取 DOM 元素</button > <son ref ="sonRef" > </son > <button @click ="getComponent" > 获取子组件实例引用</button >
1 2 3 4 5 6 7 8 9 10 11 12 13 methods : { getRef ( ) { console .log (this .$refs .pp ) this .$refs .pp .style .color = 'red' }, getComponent ( ) { console .log (this .$refs .sonRef ) this .$refs .sonRef .count = 1 this .$refs .sonRef .add () } }
组件的 $nextTick(cb)
方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行,即在 DOM 更新完成后再执行回调,从而保证 cb 回调可以获取最新的 DOM 元素。
1 2 3 4 5 6 7 8 9 methods : { showInput ( ) { this .inputVisible = true this .$nextTick(() => { this .$refs .input .focus () }) } }
Vue 组件进阶 动态组件 vue 提供了内置的 <component>
组件用于动态切换组件。
1 2 3 4 5 <component :is ="comName" > </component > <button @click ="comName = 'Left'" > 展示Left组件</button > <button @click ="comName = 'Right'" > 展示Right组件</button >
keep-alive 默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的 <keep-alive>
组件保持动态组件的状态,对被包裹的组件进行状态缓存。
被 <keep-alive>
包裹的组件会多出两个生命周期函数:当组件被激活时,触发 activated
钩子;当组件被缓存时,触发 deactivated
钩子。
1 2 3 <keep-alive > <component :is ="comName" > </component > </keep-alive >
<keep-alive>
的 include
和 exclude
属性,分别用于指明哪些组件要缓存、哪些组件不要缓存。
1 2 3 4 5 6 7 <keep-alive include ="Left, Right" > <component :is ="comName" > </component > </keep-alive > <keep-alive :include ="['News', 'Message']" > <router-view > </router-view > </keep-alive >
插槽 何为插槽 插槽可以理解为组件封装期间,为用户预留的内容占位符 。它是 vue 为组件封装者提供的能力,允许开发者在封装组件时,把不确定的、希望由用户指定的部分 定义为插槽。
插槽基本用法 基础使用:
1 2 3 4 5 6 7 8 9 10 11 12 <template > <div class ="contianer" > <h1 > 这是子组件</h1 > <slot > </slot > </div > </template > <child-comp > <p > 填充到插槽的内容</p > </child-comp >
如果子组件没有预留插槽,那么父组件填充给子组件的自定义内容会被丢弃:
1 2 3 4 5 6 7 8 9 10 11 <template > <div class ="contianer" > <h1 > 这是子组件</h1 > </div > </template > <child-comp > <p > 这段自定义内容会被丢弃</p > </child-comp >
子组件可以为插槽提供后备内容 ,当父组件没有提供自定义内容时,后备内容就会生效。
1 2 3 4 5 6 7 8 9 10 <template > <div class ="contianer" > <h1 > 这是子组件</h1 > <slot > 这是后备内容,父组件没有提供自定义内容就会生效</slot > </div > </template > <child-comp > </child-comp >
具名插槽 组件在预留插槽时可以设置 name
属性,为插槽指定名称,这种有具体名称的插槽就叫具名插槽。 没有设置 name
名称的插槽默认名称为 default
。
1 2 3 4 5 6 7 8 9 10 11 12 13 <template > <div class ="contianer" > <h1 > 这是子组件</h1 > <slot name ="title" > title 具名插槽</slot > <hr /> <slot name ="content" > content 具名插槽</slot > > <hr /> <slot > 没有设置 name 名称则默认为 default</slot > <slot name ="default" > </slot > </div > </template >
父组件向具名插槽提供自定义内容
新的写法:包裹一个 <template>
标签,同时在 <template>
中通过 v-slot:名称
指明插槽的名称。简写形式为 #名称
,且 v-slot
只能使用在 <template>
和组件标签上,普通 HTML 标签不行
旧的写法:slot="名称"
指明插槽名称
如果不指定插槽名称,那么自定义内容会被填充到所有的 default
插槽当中
同一插槽填充多个内容,是追加不是覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <child-comp > <h1 slot ="title" > 《赠汪伦》</h1 > <template v-slot:title > <h1 > 《静夜思》</h1 > </template > <template #content > <p > 床前明月光,疑是地上霜。</p > <p > 举头望明月,低头思故乡。</p > </template > <template > <p > 这段内容没有指定名称,会被填充到所有 default 插槽中。</p > </template > </child-comp >
作用域插槽
组件可以为插槽绑定自定义属性 props
,这种插槽叫作用域插槽
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定
1 2 3 4 <template > <slot v-for ="item in list" :user ="item" > </slot > </template >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 export default { data ( ) { return { list : [ { id : 1 , name : 'Lily' , state : true , }, { id : 2 , name : 'Ben' , state : false , }, { id : 3 , name : 'Water' , state : true , }, ], } }, }
父组件向插槽提供自定义内容时,可以接收作用域插槽提供的数据:
旧写法:scope="scope"
、slot-scope="scope"
新写法:v-slot:default="scope"
1 2 3 4 5 6 7 8 9 <child-comp > <template #default ="scope" > <p > 作用域插槽提供的数据:{{ scope }}</p > </template > <template slot-scope ="scope" slot ="default" > <p > {{ scope }}</p > </template > </child-comp >
其中接收到的数据 scope
是一个对象。
1 2 3 4 5 6 7 8 { 'user' : { 'id' : 1 , 'name' : 'Lily' , 'state' : true } }
在接收作用域插槽提供的数据时可以使用解构赋值。
1 2 3 4 5 6 7 <child-comp > <template #default ="{user}" > <p > id:{{ user.id }}</p > <p > name:{{ user.name }}</p > <p > state:{{ user.state }}</p > </template > </child-comp >
自定义指令 分类
私有自定义指令:在组件的 directives
节点声明
全局自定义指令:在 main.js
文件中声明
完整写法 1 <input type ="text" v-focus ="content" />
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 data ( ) { return { content : 666 } }, directives : { focus : { bind (el, binding ) { el.value = binding.value } inserted (el, binding ) { el.focus () } update (el, binding ) { el.value = binding.value } } } Vue .directive ('focus' , { bind (el, binding ) { el.value = binding.value } inserted (el, binding ) { el.focus () } update (el, binding ) { el.value = binding.value } })
简写形式
当 bind
函数和 update
函数里的逻辑完全相同时,可以简写
不需要定义 inserted
函数才使用简写形式
因此简写形式的调用时机:初次绑定和 DOM 更新(指令所在模板被重新解析)
1 <h2 v-color ="'red'" > 简写形式</h2 >
1 2 3 4 5 6 7 8 9 10 directives : { color (el, binding ) { el.style .color = binding.value } } Vue .directive ('color' , (el, binding ) => { el.style .color = binding.value }))
注意事项
自定义指令使用时需要添加 v-
前缀
指令名如果是多个单词,要使用 kebab-case
短横线命名方式,不要用 camelCase
驼峰命名
自定义指令三个函数里的 this
指向 window
1 <span v-big-number ="n" > </span >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 data ( ) { return { n : 1 } }, directives : { 'big-number' : { bind (el, binding ) { console .log (this ) el.innerText = binding.value * 10 } } }
Mixin 混入
Mixin 可以把多个组件共用的配置提取成一个混入对象
混入和组件自身的配置会合并
data
、methods
若冲突以自身为准
对于生命周期钩子,执行动作会合并,且先执行 Mixin 里的动作
定义混入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export const mixin = { methods : { showName ( ) { alert (this .name ) }, }, mounted ( ) { console .log ('hello mixin' ) }, } export const mixin2 = { data ( ) { return { x : 100 , y : 200 , } }, }
使用局部混入:
1 2 3 4 5 6 7 8 9 10 11 import { mixin, mixin2 } from '../mixin.js' export default { name : 'School' , data ( ) { return { schoolName : '北大' , } }, mixins : [mixin, mixin2], }
使用全局混入:
1 2 3 4 5 import { mixin, mixin2 } from './mixin.js' Vue .mixin (mixin)Vue .mixin (mixin2)
插件
用于增强 Vue
本质是包含 install
方法的一对象,install
第一个参数是 Vue 构造函数,第二个以后的参数是插件使用者传递的数据
定义插件:
1 2 3 4 5 6 7 8 9 10 11 12 13 export default { install (Vue, ...rest ) { console .log (rest) Vue .filter (...) Vue .directive (...) Vue .mixin (...) Vue .prototype .myProperty = 'plugins' Vue .prototype .myMethod = function ( ) {} } }
使用插件:
1 2 3 4 import plugins from './plugins.js' Vue .use (plugins, 1 , 2 )
$nextTick
语法:this.$nextTick(回调函数)
作用:在下一次 DOM 更新结束后执行其指定的回调
什么时候用:当改变数据后,要基于更新后的 DOM 进行操作时,要在 nextTick
指定的回调函数中执行
组件的 $nextTick(cb)
方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行,即在 DOM 更新完成后再执行回调,从而保证 cb 回调可以获取最新的 DOM 元素
1 2 3 4 5 6 7 8 9 methods : { showInput ( ) { this .inputVisible = true this .$nextTick(() => { this .$refs .input .focus () }) } }
Vue 脚手架 官网传送门
创建 Vue 项目
全局安装 vue 脚手架:npm i -g @vue/cli
创建项目:vue create project-name
运行项目:npm run serve
暴露 webpack 配置:vue inspect > output.js
Vue 脚手架项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ├── node_modules ├── public │ ├── favicon.ico: 页签图标 │ └── index.html: 主页面 ├── src │ ├── assets: 存放静态资源 │ │ └── logo.png │ │── component: 存放组件 │ │ └── HelloWorld.vue │ │── App.vue: 汇总所有组件 │ │── main.js: 入口文件 ├── .gitignore: git 版本管制忽略的配置 ├── babel.config.js: babel 的配置文件 ├── package.json: 应用包配置文件 ├── README.md: 应用描述文件 ├── package-lock.json:包版本控制文件
index.html
代码分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <!DOCTYPE html > <html lang ="" > <head > <meta charset ="utf-8" /> <meta http-equiv ="X-UA-Compatible" content ="IE=edge" /> <meta name ="viewport" content ="width=device-width,initial-scale=1.0" /> <link rel ="icon" href ="<%= BASE_URL %>favicon.ico" /> <title > <%= htmlWebpackPlugin.options.title %></title > </head > <body > <noscript > <strong > We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong > </noscript > <div id ="app" > </div > </body > </html >
不同版本的 Vue 与 render
函数
vue.js
与 vue.runtime.xxx.js
的区别:
import Vue from 'vue'
默认导入 vue.runtime.esm.js
vue.js
是完整版的 Vue,包含:核心功能 + 模板解析器
vue.runtime.xxx.js
是运行版的 Vue,只包含:核心功能;没有模板解析器
vue.runtime.xxx.js
没有模板解析器,故不能使用 template
配置项,需使用 render
函数接收到的 createElement
函数去指定具体内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import Vue from 'vue' import App from './App.js' Vue .config .productionTip = false new Vue ({ render : h => h (App ), }).$mount('#app' ) render : function (createElement ) { return createElement ('h1' , 'Hello Vue' ) } render : createElement => createElement (App )
vue.config.js 配置文件
使用 vue inspect > output.js
可以查看到 Vue 脚手架的默认配置。
使用 vue.config.js
可以对脚手架进行个性化定制,详情
TodoList 案例总结
组件化编码流程:
拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突。
实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
一个组件在用:放在组件自身即可
一些组件在用:放在他们共同的父组件上(状态提升)
实现交互:从绑定事件开始。
props
适用于:
父 ==> 子
子 ==> 父(要求父先给子一个函数)
v-model
绑定的值不能是 props
传过来的值,因为 props
是不可以修改的!
props
传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐
一些第三方包
1 2 3 4 5 npm install nanoid import { nanoid } from nanoid model.id = nanoid()
Vue 网络请求 Vue 脚手架配置代理 配置单个代理 在 vue.config.js
中添加如下配置:
1 2 3 devServer : { proxy : 'http://localhost:5000' }
优点:配置简单,请求资源时直接发给前端(8080)即可。
缺点:不能配置多个代理,不能灵活的控制请求是否走代理。
工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)
配置多个代理 编写 vue.config.js
配置具体代理规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 module .exports = { devServer : { proxy : { '/api1' : { target : 'http://localhost:5000' , changeOrigin : true , pathRewrite : { '^/api1' : '' }, }, '/api2' : { target : 'http://localhost:5001' , changeOrigin : true , pathRewrite : { '^/api2' : '' }, }, }, }, }
优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
缺点:配置略微繁琐,请求资源时必须加前缀
vue 2.x 全局配置 axios 实际项目开发中,几乎每个组件中都会使用 axios
发起数据请求。此时会遇到如下两个问题:
每个组件中都需要导入 axios
(代码臃肿)
每次发请求都需要填写完整的请求路径(不利于后期的维护)
在 main.js
文件中进行配置:
1 2 3 4 5 axios.defaults .baseURL = 'http://api.com' Vue .prototype .$http = aixos
优点 :每个组件可以通过 this.$http.get
直接发起请求,无需再导入 axios
;若根路径发生改变,只需修改 axios.defaults.baseURL
,有利于代码维护。
缺点 :无法实现 API
的复用。即多个组件需要对同一个接口发起请求,那么每个组件都需要重复书写 this.$http.get('/users')
类似的代码,造成冗余。(视频上的说法,个人认为真正的缺点是如果存在多个根路径,这种方式无法解决,所以才会有下面的改进方式。)
改进 :对于每一个根路径,独立封装一个 request.js
模块,组件导入所需根路径对应的 axios
进行使用。
1 2 3 4 5 6 7 8 import axios from 'axios' const request = axios.create ({ baseURL : 'http://api.taobao.com' , }) export default request
Vuex 官网传送门
概述 何为 Vuex ?
Vuex 专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式的管理,也是一种组件间通信的方式,适用于任意组件间通信
何时用 Vuex ?
多个组件依赖于同一状态
来自不同组件的行为需要变更同一状态
Vuex 工作原理图:
官方 Vuex 项目结构示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ├── index.html ├── main.js ├── api │ └── ... # 抽取出API请求 ├── components │ ├── App.vue │ └── ... └── store ├── index.js # 组装模块并导出 store 的地方 ├── actions.js # 根级别的 action ├── mutations.js # 根级别的 mutation └── modules ├── cart.js # 购物车模块 └── products.js # 产品模块
Vuex 核心概念 state
actions
值为一个对象,包含多个响应用户动作的回调函数
通过 commit()
触发 mutation 中函数的调用,间接更新 state
可包含异步代码
mutations
值为一个对象,包含多个直接更新 state 的方法
不能写异步代码,只能单纯地操作 state
getters
值为一个对象,包含多个用于返回数据的函数
类似于计算属性,getters 返回的数据依赖于 state 的数据
modules
一个 module 是一个 store 的配置对象,与一个组件对应
搭建 Vuex 环境 安装 Vuex:npm install vuex@3 --save
注意:Vue2 安装 Vuex3,Vue3 安装 Vuex4,版本需对应。
创建文件 src/store/index.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 import Vue from 'vue' import Vuex from 'vuex' Vue .use (Vuex )const actions = {}const mutations = {}const state = {}export default new Vuex .Store ({ actions, mutations, state, })
main.js
配置 store
:
1 2 3 4 5 6 7 import store from './store' new Vue ({ el : '#app' , store, render : (h ) => h (App ), })
基本使用 组件实例与 Actions
和 Mutations
对话:
若没有网络请求或其他业务逻辑,组件中也可以越过 actions
,即不写dispatch
,直接编写commit
:
1 2 3 4 5 6 7 8 9 10 11 methods : { increment ( ) { this .$store .commit ('ADD' , this .number ) }, incrementOdd ( ) { this .$store .dispatch ('addOdd' , this .number ) }, incrementAsync ( ) { this .$store .dispatch ('addAsync' , this .number ) } }
定义 Actions
和 Mutations
:
context
是一个迷你版的 store
,可访问 dispatch
, commit
方法和 state
mutations
的动作类型一般用大写,与 actions
区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import Vue from 'vue' import Vuex from 'vuex' Vue .use (Vuex )const actions = { addOdd (context, value ) { if (context.state .sum % 2 !== 0 ) { context.commit ('ADD' , value) } }, addAsync (context, value ) { setTimeout (() => { context.commit ('ADD' , value) }) }, } const mutations = { ADD (state, value ) { state.sum += value }, } const state = { sum : 0 , } export default new Vuex .Store ({ actions, mutations, state, })
组件访问 Vuex 的数据:
1 <p > {{ $store.state.sum }}</p >
getters 的使用
当 state
中的数据需要经过加工后再使用时,可以使用 getters
加工
它不是必须的,当加工逻辑复杂且需要复用时,可以考虑使用
state
与 getters
的关系有点像 data
和 computed
的关系
组件读取:$store.getters.bigSum
1 2 3 4 5 6 7 8 9 10 11 12 13 ... const getters = { bigSum (state ) { return state.sum * 10 } } export default new Vuex .Store ({ actions, mutations, state, getters })
四个 mapXxx 方法 mapState()
将 state
状态映射为计算属性
对象写法:键为自取的计算属性名,值为对应的状态(必须为字符串)
数组写法:当键值同名,可直接写状态名(字符串)
函数返回一个对象:{sum: f, price: f}
注意对象的 ...{}
展开写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { mapState } from 'vuex' computed : { sum ( ) { return this .$store .state .sum }, price ( ) { return this .$store .state .price }, ...mapState ({sum : 'sum' , price : 'price' }), ...mapState (['sum' , 'price' ]) }
mapGetters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { mapGetters } from 'vuex' computed : { bigSum ( ) { return this .$store .getters .bigSum }, double ( ) { return this .$store .getters .double }, ...mapGetters ({bigSum : 'bigSum' , double : 'double' }), ...mapGetters (['bigSum' , 'double' ]), }
mapActions
生成与 actions
对话的函数,即包含 $store.dispatch()
mapActions
生成的函数不会传入参数,需要在调用时手动传入数据,不传参默认传入 $event
数组写法要注意函数名和 actions
动作类型同名,调用时勿写错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { mapActions } from 'vuex' methods : { incrementOdd ( ) { this .$store .dispatch ('addOdd' , this .number ) }, incrementAsync ( ) { this .$store .dispatch ('addAsync' , this .number ) }, ...mapActions ({incrementOdd : 'addOdd' , incrementAsync : 'addAsync' }), ...mapActions (['addOdd' , 'addAsync' ]), }
1 <button @click ="incrementOdd(number)" > 奇数+1</button >
mapMutations
生成与 mutations
对话的函数,即包含 $store.commit()
同样注意传递参数,以及数组形式函数名的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { mapMutations } from 'vuex' methods : { increment ( ) { this .$store .commit ('ADD' , this .number ) }, decrement ( ) { this .$store .commit ('SUB' , this .number ) }, ...mapMutations ({increment : 'ADD' , decrement : 'SUB' }), ...mapMutations (['ADD' , 'SUB' ]), }
Vuex 模块化&命名空间 让代码更好维护,让多种数据分类更加明确,每一类数据及其相关操作对应一个 store
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const countAbout = { namespaced : true , state : { sum : 0 }, actions : {...}, mutations : {...}, getters : { bigSum (state ) { return state.sum * 10 } } } const personAbout = { namespaced : true , state : { personList : [] }, actions : {...}, mutations : {...}, getters : {...} } export default new Vuex .Store ({ modules : { countAbout, personAbout } })
开启命名空间后,组件中读取 state
数据:
1 2 3 4 5 this .$store .state .personAbout .personList ...mapState ('countAbout' ,['sum' ,'school' ]),
开启命名空间后,组件中读取 getters
数据:
1 2 3 4 5 this .$store .getters ['countAbout/bigSum' ]...mapGetters ('countAbout' ,['bigSum' ])
开启命名空间后,组件中调用 dispatch
1 2 3 4 this .$store .dispatch ('countAbout/addODdd' , this .number )...mapActions ('countAbout' , {incrementOdd :'addOdd' , incrementWait :'addAsync' })
开启命名空间后,组件中调用 commit
1 2 3 4 this .$store .commit ('personAbout/ADD_PERSON' , person)...mapMutations ('countAbout' ,{increment :'ADD' ,decrement :'SUB' }),
Vue Router 官网传送门
路由 何为路由
一组路由即一组映射关系(key-value)
key 为路径,value 可能是 function 或 component
前端路由 前端路由即地址和组件之间的对应关系(以下已哈希模式为例)。
前端路由简易工作方式:
用户点击了页面上的路由链接
URL 地址的 Hash 值发生变化
前端路由监听到 Hash 值的变化
前端路由渲染 Hash 地址对应的组件
实现简易的前端路由:
1 2 3 4 5 6 7 <a href ="#/home" > Home</a > <a href ="#/movie" > Movie</a > <a href ="#/about" > About</a > <component :is ="compName" > </component >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 export default { name : 'App' , data ( ) { return { compName : 'Home' } }, created ( ) { window .onhashchange = () => { switch (location.hash ) { case : '#/home' : this .compName = 'Home' break case : '#/movie' : this .compName = 'Movie' break case : '#/about' : this .compName = 'About' break } } } }
后端路由
后端路由是指请求方式、请求地址与 function
处理函数之间的对应关系
服务器收到一个请求,根据请求方式、路径匹配对应的函数处理请求,返回响应数据
1 2 3 4 5 6 const express = require ('express' )const router = express.Router ()router.get ('/userlist' , function (req, res ) {...}) module .exports = router
单页面应用程序 SPA 单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web 页面初始化时加载相应的资源( HTML、JavaScript 和 CSS)。 一旦页面加载完成了,SPA 不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态地变换 HTML 的内容,从而实现页面与用户的交互。
SPA 的优点:
良好的交互体验
内容的改变不需要重新加载整个页面
数据通过 Ajax
异步获取
没有页面跳转,不会出现白屏现象
良好的前后盾工作分离模式
后端专注于提供 API 接口,更易实现接口复用
前端专注页面渲染,更利于前端工程化发展
减轻服务器压力
服务器只提供数据,不负责页面的合成与逻辑处理,吞吐能力会提高
SPA 的缺点:
首屏加载慢:可使用路由懒加载、代码压缩、CDN 加速、网络传输压缩
不利于 SEO :SSR 服务器端渲染
vue-router 初体验 安装 vue-router
:
1 npm install vue-router@3.5.2 -S
创建路由模块,在 src
源代码目录下,新建 router/index.js
路由模块,初始化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import Vue from 'vue' import VueRouter from 'vue-router' import Home from '@/component/Home.vue' import About from '@/component/About.vue' import Movie from '@/component/Movie.vue' Vue .use (VueRouter )const routes = [ { path : '/home' , component : Home }, { path : '/about' , component : About }, { path : '/movie' , component : Movie }, ] const router = new VueRouter ({ routes, }) export default router
在 main.js
文件中导入并挂载路由模块:
1 2 3 4 5 6 7 8 import Vue from 'vue' import App from './App.vue' import router from './router/index.js' new Vue ({ router, render : (h ) => h (App ), }).$mount('#app' )
在组件中声明路由链接和占位符:
1 2 3 4 5 6 7 8 9 10 11 <template > <div class ="app-container" > <router-link to ="/home" > 首页</router-link > <router-link to ="/about" > 关于</router-link > <router-link to ="/movie" > 电影</router-link > <router-view > </router-view > </div > </template >
注意事项:
组件分为路由组件和一般组件,前者放在 pages(或views)
文件夹,后者放在 components
文件夹
每个组件都有 $route
属性,存储着组件的路由规则信息
$router
是路由器对象,整个 SPA 只有一个
声明式导航 <router-link>
4 个常用属性:
to
属性
1 <router-link to ="/about" > </router-link >
tag
属性
指明 <router-link>
最终被渲染为何种标签,默认是 a 标签
渲染为其他标签也会监听点击,触发导航
1 2 3 <router-link to ="/about" tag ="li" > tag</router-link > <li > tag</li >
replace
属性
路由跳转不会增加新的历史记录,而是替换当前历史记录
1 <router-link to ="/about" replace > About</router-link >
active-class
属性
指明路由被激活时添加的类名,默认为 router-link-active
详见路由高亮
1 <router-link to ="/about" active-class ="active" > About</router-link >
路由高亮 被激活的路由链接,默认会添加 router-link-active
的类名。可据此为激活的路由链接设置高亮的样式:
1 2 3 4 .router-link-active { color : white; background-color : pink; }
定义路由模块时可以自定义路由链接被激活时添加的类名:
1 2 3 4 5 const router = new VueRouter ({ linkActiveClass : 'active-hello' , routes, })
声明路由链接时也可用 active-class
属性自定义激活类名:
1 2 <router-link active-class ="active" to ="/about" > About</router-link >
路由重定向 1 2 3 4 5 6 7 8 9 10 11 12 13 const routes = [ { path : '/' , redirect : '/home' }, { path : '/home' , component : 'Home' }, { path : '/about' , component : 'About' }, { path : '/movie' , component : 'Movie' }, ] const router = new VueRouter ({ routes, }) export default router
嵌套路由 About
组件中声明子路由链接和子路由占位符:
1 2 3 4 5 6 7 8 9 <template > <div class ="about-container" > <router-link to ="/about/tab1" > tab1</router-link > <router-link to ="/about/tab2" > tab2</router-link > <router-view > </router-view > </div > </template >
通过 children
属性声明子路由规则:
1 2 3 4 5 6 7 8 9 10 11 const routes = [ { path : '/about' , component : 'About' , children : [ { path : 'tab1' , component : Tab1 }, { path : 'tab2' , component : Tab2 }, ], }, ]
编程式导航 声明式导航:
通过点击链接实现导航
如普通网页点击 a
链接,vue
点击 <router-link>
编程式导航:
通过调用 API 实现导航
普通网页通过 location.href
的方式跳转页面也是编程式导航
vue-router
中实现编程式导航的 API :
this.$router.push('hash地址')
:跳转到指定页面,并增加一条历史记录
this.$router.replace('hash地址')
:跳转页面,但不会新增历史记录,而是替换当前的历史记录
this.$router.go(数值)
:历史记录前进或后退,相当于点击浏览器前进后退箭头
this.$router.forward()
:前进一步
this.$router.back()
:后退一步
命名路由 给路由命名,某些情况可简化路由跳转写法
1 2 3 4 5 6 7 const routes = [ { name : 'about' , path : '/about' , component : About , }, ]
1 2 3 <router-link :to ="{ name: 'about'} " > </router-link > <router-link :to ="{ name: 'about', query: { id: 1, title: 'hello' }}" > </router-link >
路由传参 query 参数 传递参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <router-link :to ="`/home/detail?id=${id}&title=${title}`" > 字符串写法</router-link > <router-link :to ="{ path: '/home/detail', query: { id: 1, title: 'hello', } }" > 对象写法 </router-link >
1 2 this .$router .push (`/home/detail?id=${id} &title=${title} ` )this .$router .push ({ path : '/home/detail' , query : { id : 1 , title : 'query' } })
接收参数:
1 2 this .$route .query .id this .$route .query .title
params 参数(动态路由) 动态路由是把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。
声明 params 参数:
1 2 3 4 5 6 7 { path : '/movie/:id/:age' , component : Movie } { path : '/movie/1/21' , component : Movie } { path : '/movie/2/22' , component : Movie } { path : '/movie/3/23' , component : Movie }
传递 params 参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <router-link :to ="/movie/1/21" > 字符串写法</router-link > <router-link :to ="{ name: 'movie', params: { id: 1, age: 21 } }" > 对象写法 </router-link > <router-link :to ="`/movie/1/21?id=${id}`" > 字符串写法</router-link > <router-link :to ="{ name:'movie', params: {id:1, age:21}, query: {school: 'love'} }" > 对象写法 </router-link >
1 2 this .$router .push (`/movie/1/21?id=${id} ` )this .$router .push ({ name : 'movie' , params : { id : 1 , age : 21 }, query : { school : 'love' } })
接收 params 参数:
1 2 3 4 5 <template > <div class ="movie-container" > <h1 > Movie组件,参数值:{{ this.$route.params.id }}</h1 > </div > </template >
路由的 props 配置 简化路由组件接收参数。
在路由规则中开启 props
传参,组件可以使用 props
接收路由参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { path : '/movie/:id/:title' , component : Movie , props : {id : 666 , title : 'hello' } props : true props ($route ) { return { id : $route.query .id , title : $route.params .title , age : 21 } } }
组件接收参数:
1 2 3 4 export default { props : ['id' , 'title' ], }
1 2 3 <template > <h1 > Movie组件,参数值:{{ id }},题目:{{ title }}</h1 > </template >
路由传参注意事项
path 不能和 params 一起使用。path+query、name+query/params 都行
如何指定 params 参数可传可不传?
若声明了 params 参数 path: '/movie/:title'
,默认是必须要传递 params 参数的,否则 URL 会出现问题
指定 params 参数可不传:path: '/movie/:title?'
已指明 params 参数可传可不传,但如果传递空串,如何解决?
传递空串,URL 也会出现问题
方法:使用 undefined
1 2 this .$router .push ({ name : 'search' , params : { keyword : '' }, query : { key : this .key } })this .$router .push ({ name : 'search' , params : { keyword : '' || undefined }, query : { key : this .key } })
在 meta
中可以为路由添加自定义信息:
1 2 3 4 5 6 7 8 const routes = [ { name : 'about' , path : '/about' , component : About , meta : { title : 'hello' , isAuth : true }, }, ]
路由守卫
作用:对路由进行权限控制。
分类:全局守卫、独享守卫、组件内守卫
全局守卫
全局前置守卫:beforeEach()
全局后置守卫:afterEach()
守卫回调函数 3 个形参:
to
:将要访问的路由的信息对象,即 $route
from
:将要离开的路由的信息对象
next
:放行函数(后置守卫没有)
next
函数 3 种调用方式:
直接放行:next()
强制跳转到其他路由:next(/login)
阻止本次跳转:next(false)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const router = new VueRouter ({...})router.beforeEach ((to, from , next ) => { if (to.path === '/main' ) { const token = localStorage .getItem ('token' ) if (token) { next () } else { next ('/login' ) } } else { next () } }) router.afterEach ((to,from ) => { if (to.meta .title ) { document .title = to.meta .title } else { document .title = 'vue_test' } })
独享路由守卫
1 2 3 4 5 6 7 { path : 'about' , component : About , beforeEnter (to, from ,next ) { ... } }
组件内路由守卫 1 2 3 4 5 6 7 8 9 10 11 12 13 export default { name : 'About' , beforeRouteEnter (to, from , next ) { ... } beforeRouteLeave (to, from , next) { ... } }
各个守卫执行顺序 从 About
组件通过路由规则进入 Home
组件:
1 2 3 4 5 6 About-beforeRouteLeave beforeEach Home-beforeEnter Home-beforeRouteEnter afterEach Home组件生命周期开始
vue-router 4.x 目前 vue-router
有 3.x
和 4.x
两个版本,前者只能在 vue2.x
中使用,后者只能在 vue3.x
中使用。
下面是 vue-router 4.x
初体验:
安装 vue-router 4.x
:
1 npm install vue-router@next -S
创建路由模块:
1 2 3 4 5 6 7 8 9 10 11 import { createRouter, createWebHashHistory } from 'vue-router' const router = createRouter ({ history : createWebHashHistory (), routes : [{ path : '/home' , component : Home }], }) export default router
main.js
导入并挂载路由模块:
1 2 3 4 5 6 7 8 import { createApp } from 'vue' import App from './App.vue' import './index.css' const app = createApp (App )app.use (router) app.mount ('#app' )
Vue3 官网传送门 Vue3 的变化
性能的提升
打包大小减少 41%
初次渲染快 55%, 更新渲染快 133%
内存减少 54%
源码的升级
使用 Proxy 代替 defineProperty 实现响应式
重写虚拟 DOM 的实现和 Tree-Shaking
拥抱 TypeScript
新的特性
Composition API(组合 API)
setup 配置
ref 与 reactive
watch 与 watchEffect
provide 与 inject
……
新的内置组件
Fragment
Teleport
Suspense
其他改变
新的生命周期钩子
data 选项应始终被声明为一个函数
移除 keyCode 支持作为 v-on 的修饰符
……
创建 Vue3 工程 vite 和 vue-cli 对比
vite
vue-cli
支持的 vue 版本
仅支持 vue3.x
支持 2.x 和 3.x
是否基于 webpack
否
是
运行速度
快
较慢
功能完整度
小而巧
大而全
是否建议企业级开发使用
暂不建议
建议
使用 vue-cli 创建 1 2 3 4 5 6 7 8 9 vue --version npm install -g @vue/cli vue create vue_test cd vue_testnpm run serve
使用 vite 创建
vite :新一代前端构建工具
优势:
开发环境中,无需打包操作,可快速冷启动(webpack 每次运行项目都要打包)
轻量快速的热重载 HMR(更改代码局部刷新,webpack 也行,但 vite 更轻量)
真正的按需编译,无需等待整个应用编译完成
传统构建 与 vite 构建对比(vite 现用现分析,按需导入,因此项目启动更快)
1 2 3 4 5 npm init vite-app 项目名称 cd 项目名称npm install npm run dev
Vue3 项目结构 Vue3 中 main.js
代码有所改变:
1 2 3 4 5 6 7 8 import { createApp } from 'vue' import App from './App.vue' const app = createApp (App )app.mount ('#app' )
Vue3 支持定义多个根节点,组件的 <template>
支持定义多个根节点:
1 2 3 4 <template > <h1 > 根节点</h1 > <h1 > 根节点</h1 > </template >
常用 Composition API setup
setup 是 Vue3 中一个新的配置项,值为函数
组件中使用的数据、方法等都要配置在 setup 中
setup 函数两种返回值:
返回一个对象,对象中的属性、方法可在模板中直接使用
返回一个渲染函数,可自定义渲染内容
setup 函数的参数:
props:值为对象,包含了组件外部传进来,且组件内部声明接收的属性
context:上下文对象
attrs
:值为对象,包含了组件外部传进来,且组件内部没有声明接收的属性,相当于 this.$attrs
slots
:收到的插槽内容,相当于 this.$slots
emit
:触发自定义事件的函数,相当于 this.$emit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import { h } from 'vue' export default { name : 'App' , props : ['title' ], emits : ['changeCount' ], setup (props, context ) { let name = 'Vue3' function sayHello ( ) {} function test ( ) { context.emit ('changeCount' , 888 ) } return { name, sayHello, test, } }, }
注意:
setup 在 beforeCreate
钩子之前执行,this
为 undefined
setup 不要和 Vue2 配置混用。Vue2 的配置可以访问到 setup 的属性方法,反过来不行;如有重名,setup 优先
setup 不能是 async 函数,因为 async 函数返回的是 promise 不是对象,会导致模板无法访问属性方法
若要返回 promise 实例,需要 Suspense
和异步组件的配合
ref 函数 作用:定义响应式数据
语法:const name = ref(initValue)
ref
函数返回一个 RefImpl
(reference implement) 实例对象,全称引用实现的实例对象
它包含响应式数据,简称引用对象、reference 对象、ref 对象
JS 访问数据:name.value
模板访问数据:<div></div>
注意事项:
ref
函数可以接收基本数据类型和引用数据类型
基本类型数据的响应式还是靠 Object.defineProperty()
完成
对象类型数据使用 ES6 的 Proxy 实现响应式,Vue3 把相关操作封装在 reactive
函数中
按照之前的办法,对于对象数据,应该遍历每一层的属性添加 getter
、setter
,但 Vue3 使用 Proxy 把内部数据一口气监测了
1 2 <h2 > {{ name }}</h2 > <p > {{ jobInfo.type }}</p >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { ref } from 'vue' export default { setup ( ) { let name = ref ('Vue3' ) let jobInfo = ref ({ type : 'frontend' , salary : '40w' , }) function changeInfo ( ) { name.value = '鱿鱼丝' jobInfo.value .salary = '50w' } return { name, jobInfo, changeInfo, } }, }
reactive 函数
定义引用类型的响应式数据,不可用于 jibenleixingshuju
const 代理对象 = reactive(源对象)
接收对象或数组,返回代理对象(Proxy 的实例对象)
reactive
的响应式是深度的
基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { reactive } from 'vue' export default { setup ( ) { let person = reactive ({ name : 'Vue3' , sex : 'unknown' , info : { school : 'Oxford' , major : 'computer' , }, }) let color = reactive (['red' , 'green' , 'blue' ]) function changeInfo ( ) { person.info .major = 'art' color[0 ] = 'yellow' } return { person, color, changeInfo, } }, }
ref VS reactive 定义数据:
ref 用于定义基本类型数据
reactive 用于定义对象或数组类型数据
ref 也可定义对象或数组类型数据,内部通过 reactive 转为代理对象
一般使用 reactive 函数,可以把所有数据封装为一个对象
原理:
ref 通过 Object.defineProperty()
实现响应式
reactive 通过 Proxy 实现响应式,Reflect 操作源对象数据
使用角度:
ref 定义数据,访问数据需要 .value
,模板中不需要
reactive 定义的数据,都不需要
Vue3 响应式原理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 let originPerson = { name : 'Lily' , age : 22 , } let person = new Proxy (originPerson, { get (target, prop ) { return Reflect .get (originPerson, prop) }, set (target, prop, value ) { return Reflect .set (target, prop, value) }, deleteProperty (target, prop ) { return Reflect .deleteProperty (target, prop) }, }) console .log (person.name )person.age = 33 person.sex = 'unknown' delete person.age
computed 函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import { reactive, computed } from 'vue' export default { setup ( ) { let person = reactive ({ firstName : 'Cai' , lastName : 'QP' , }) person.fullName = computed (() => { return person.firstName + '-' + person.lastName }) person.fullName = computed ({ get ( ) { return person.firstName + '-' + person.lastName }, set (value ) { const arr = value.split ('-' ) person.firstName = arr[0 ] person.lastName = arr[1 ] }, }) return { person, } }, }
watch 函数 Vue3 watch
能侦听的东西:
A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { ref, reactive, watch } from 'vue' ... let sum = ref (0 )let msg = ref ('hello' )let person = reactive ({ name : 'Vue3' , age : 18 , info : { job : { salary : 40 , }, }, })
侦听 ref 定义的响应式数据 :
1 2 3 4 5 6 7 8 watch ( sum, (newVal, oldVal ) => { console .log (newVal, oldVal) }, { immediate : true } )
侦听多个 ref 定义的响应式数据 :
1 2 3 4 watch ([sum, msg], (newVal, oldVal ) => { console .log (newVal, oldVal) })
侦听 ref 定义的对象类型数据 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 let person = ref ({ name : 'Vue3' , age : 18 , info : { job : { salary : 40 , }, }, }) watch (person, (newVal, oldVal ) => { ... }, { deep :true })watch (person.value , (newVal, oldVal ) => {...})
侦听 reactive 函数直接返回的那一整坨响应式数据 :
oldVal 是错误的!和 newVal 的值一样
强制开启了深度侦听,deep
配置不生效!
1 2 3 4 5 6 7 watch ( person, (newVal, oldVal ) => { console .log (newVal, oldVal) }, { immediate : true , deep : false } )
侦听 reactive 定义的响应式数据某个属性 :
如果是 () => person.info
oldVal 也是错误的!
() => person.name
oldVal 是正确的,何时对何时错自己琢磨吧!
此处没有强制开启深度监听
1 2 3 4 5 6 7 8 watch ( () => person.info , (newVal, oldVal ) => { console .log (newVal, oldVal) }, { deep : true } )
侦听 reactive 定义的响应式数据多个属性 :
1 2 3 4 5 6 7 watch ( [() => person.name , () => person.info ], (newVal, oldVal ) => { console .log (newVal, oldVal) }, { deep : true } )
watchEffect 函数
watchEffect
不需要指明监听哪个属性,回调里用到哪个属性,就自动监听哪个属性
computed
注重计算的值,即回调函数的返回值,因此必须有返回值
watchEffect
更注重过程,即回调函数的函数体,因此可没有返回值
watchEffect
没有开启深度监听,也不能开启深度监听!
watchEffect
内部自行修改数据,不会重新调用回调,因此不会出现递归调用
1 2 3 4 5 6 watchEffect (() => { let total = sum.value let p = person console .log ('watchEffect...' ) })
生命周期 注意和 vue2.x
的生命周期图作对比,beforeDestroy
和 destroyed
变为 beforeUnmount
和 unmounted
。
Vue3 也提供了 Composition API 形式的生命周期钩子,与 Vue2 中钩子对应关系如下:
beforeCreate
===>setup()
created
=======>setup()
beforeMount
===>onBeforeMount
mounted
=======>onMounted
beforeUpdate
===>onBeforeUpdate
updated
=======>onUpdated
beforeUnmount
==>onBeforeUnmount
unmounted
=====>onUnmounted
若和配置项生命钩子一起使用,则组合式会比配置项的先执行,如 onBeforeMount
先于 beforeMount
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue' setup ( ){ console .log ('---setup---' ) let sum = ref (0 ) onBeforeMount (()=> { console .log ('---onBeforeMount---' ) }) onMounted (()=> { console .log ('---onMounted---' ) }) onBeforeUpdate (()=> { console .log ('---onBeforeUpdate---' ) }) onUpdated (()=> { console .log ('---onUpdated---' ) }) onBeforeUnmount (()=> { console .log ('---onBeforeUnmount---' ) }) onUnmounted (()=> { console .log ('---onUnmounted---' ) }) return {sum} },
hook 函数
hook 是一个函数,把 setup 函数的 Composition API 进行了封装
类似 Vue2 的 Mixin,能复用代码,让 setup 里的逻辑更清晰
hook 放在 hooks 文件夹中,一个文件对应一个功能模块,以 useXxx
命名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { reactive, onMounted, onBeforeUnmount } from 'vue' export default function ( ) { let point = reactive ({ x : 0 , y : 0 , }) function savePoint (event ) { point.x = event.pageX point.y = event.pageY } onMounted (() => { window .addEventListener ('click' , savePoint) }) onBeforeUnmount (() => { window .removeEventListener ('click' , savePoint) }) return point }
1 2 3 4 5 6 7 8 9 10 import usePoint from '../hooks/usePoint.js' export default { setup ( ) { let point = usePoint () return { point } }, }
toRef 函数
创建一个 RefImpl 实例对象,其 value 值指向另一个对象的某个属性,修改 value 值会修改源对象对应的属性
应用:需要把响应式对象的某个属性单独提供给外部使用
批量创建:toRefs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import {reactive, toRef, toRefs} from 'vue' ... setup ( ) { let person = reactive ({ name : 'Vue3' , age : 18 , info : { job : { salary : 40 , }, }, }) return { name : toRef (person, 'name' ), salary : toRef (person.info .job , 'salary' ) ...toRefs (person) } }
其它 Composition API shallowReactive & shallowRef
shallowReactive
:只处理对象最外层属性的响应式,即浅响应式
shallowRef
:基本数据类型和 ref
相同,对象数据不再会调用 reactive
,因此只有对象引用改变了才是响应式的
若一个对象数据,结构很深,但只有最外层属性变化,可用 shallowReactive
若一个对象数据,属性不会改变,而是使用新对象替换,可用 shallowRef
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { shallowReactive, shallowRef } from 'vue' setup ( ) { let person = shallowReactive ({ name : 'Vue3' , age : 21 , info : { job : { salary : 22 } } }) let x = shallowRef ({ y : 0 }) return { person, x } }
readonly & shallowReadonly
readonly
: 让一个响应式数据变为只读的(深只读)
shallowReadonly
:让一个响应式数据变为只读的(浅只读)
应用场景: 不希望数据被修改时,如你用了别人的响应式数据,但是别人不希望你修改时
1 2 3 4 5 6 7 8 9 10 11 12 setup ( ) { let sum = ref (0 ) let person = reactive ({...}) sum = readonly (sum) person = shallowReadonly (person) return { sum, person } }
toRaw & markRaw toRaw
:
将一个由 reactive
生成的响应式对象转为普通对象
用于读取响应式对象对应的普通对象,对该普通对象的操作不会引起页面更新
markRaw
:
标记一个对象,让其不成为响应式对象
有些值不应设置为响应式,比如复杂的第三方库
当渲染复杂且不变的数据时,跳过响应式转换可提高性能
注意:仅仅让数据变为非响应式的,数据变的依旧变,只是没引起页面更新
1 2 3 4 5 6 7 8 9 10 11 12 13 setup ( ) { function showRawPerson ( ) { const p = toRaw (person); p.age ++; console .log (p); console .log (person); } function addCar ( ) { let car = { name : "奔驰" , price : 40 }; person.car = markRaw (car); } }
customRef 创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制
1 2 <input type ="text" v-model ="keyword" /> <h3 > {{ keyword }}</h3 >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { ref, customRef } from 'vue' export default { name : 'Demo' , setup ( ) { function myRef (value, delay ) { let timer return customRef ((track, trigger ) => { return { get ( ) { track () return value }, set (newValue ) { clearTimeout (timer) timer = setTimeout (() => { value = newValue trigger () }, delay) }, } }) } let keyword = myRef ('hello' , 500 ) return { keyword, } }, }
provide / inject 实现祖先组件与后代组件之间通信。
1 2 3 4 5 6 7 8 9 10 import { provide, reactive, ref } from 'vue' setup ( ) { let car = reactive ({...}) let sum = ref (0 ) provide ('sum' , sum) provide ('car' , car) }
1 2 3 4 5 6 7 8 import { inject } from 'vue' setup ( ) { const car = inject ('car' ) const sum = inject ('sum' ) return { car, sum } }
响应式数据的判断
isRef
: 检查一个值是否为一个 ref
对象
isReactive
: 检查一个对象是否是由 reactive
创建的响应式代理
isReadonly
: 检查一个对象是否是由 readonly
创建的只读代理
isProxy
: 检查一个对象是否是由 reactive
或者 readonly
方法创建的代理
Compositon API 的优势 Options API 存在的问题
使用传统 Options API 中,新增或者修改一个需求,就需要分别在 data,methods,computed 等地方修改。
Composition API 的优势
可以更加优雅地组织代码、函数,让相关功能的代码更加有序的组织在一起。说白了就是让同一个功能的代码整合到一起,日后修改代码直接找对应的功能模块。
新的组件 Fragment
在 Vue2 中: 组件必须有一个根标签
在 Vue3 中: 组件可以没有根标签, 内部会将多个标签包含在一个 Fragment
虚拟元素中
好处: 减少标签层级, 减小内存占用
Teleport
将包裹的 HTML 结构移动到指定元素的末尾
to
属性为 CSS 选择器
简易的模态框效果:
1 2 3 4 5 6 7 8 <teleport to ="#root" > <div v-if ="isShow" class ="mask" > <div class ="dialog" > <h3 > 我是一个弹窗</h3 > <button @click ="isShow = false" > 关闭弹窗</button > </div > </div > </teleport >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .mask { position : absolute; top : 0 ; bottom : 0 ; left : 0 ; right : 0 ; background-color : rgba (0 , 0 , 0 , 0.5 ); } .dialog { position : absolute; top : 50% ; left : 50% ; transform : translate (-50% , -50% ); text-align : center; width : 300px ; height : 300px ; background-color : green; }
Suspense 等待异步组件时渲染额外内容,让用户体验更好
异步引入组件:
1 2 import { defineAsyncComponent } from 'vue' const Child = defineAsyncComponent (() => import ('./components/Child.vue' ))
使用 Suspense
包裹组件,实际上是往插槽填充内容,default
插槽填充组件内容,fallback
插槽填充组件加载时显示的内容:
1 2 3 4 5 6 7 8 <Suspense > <template v-slot:default > <Child /> </template > <template v-slot:fallback > <h3 > 加载中,请稍等...</h3 > </template > </Suspense >
另外,若 Child
组件的 setup
函数返回一个 Promise 对象,也能渲染 fallback
里的内容:
1 2 3 4 5 6 7 8 async setup ( ) { let sum = ref (0 ) return await new Promise ((resolve, reject ) => { setTimeout (() => { resolve ({sum}) }, 3000 ) }) }
其他改变
Vue3 将全局的 API,即:Vue.xxx
调整到应用实例 app
上:
Vue2 全局 API
Vue3 实例 API
Vue.config.xxx
app.config.xxx
Vue.config.productionTip
移除
Vue.component
app.component
Vue.directive
app.directive
Vue.mixin
app.mixin
Vue.use
app.use
Vue.prototype
app.config.globalProperties
data
选项应始终被声明为一个函数
过渡类名的更改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .v-enter ,.v-leave-to { opacity : 0 ; } .v-leave ,.v-enter-to { opacity : 1 ; } .v-enter-from ,.v-leave-to { opacity : 0 ; } .v-leave-from ,.v-enter-to { opacity : 1 ; }
移除 keyCode
作为 v-on
的修饰符,同时也不再支持 config.keyCodes
移除 v-on.native
修饰符,子组件没有在 emits: ['close']
声明的自定义事件作为原生事件处理
移除过滤器 filter
…
组件上的 v-model 当需要维护组件内外数据的同步时,可以在组件上使用 v-model
指令。
父组件传值:
1 2 <my-counter v-model:number ="count" > </my-counter >
子组件在 emits
节点声明自定义事件,格式为 update:xxx
,调用 $emit
触发自定义事件:
1 2 3 4 5 6 7 8 9 export default { props : ['number' ], emits : ['update:number' ], methods : { add ( ) { this .$emit('update:number' , this .number ++) }, }, }
注意,在 vue3
中 props
属性同样是只读的,上面 this.number++
并没有修改 number
的值。
其实通过 v-bind
传值和监听自定义事件的方式能实现和 v-model
相同的效果。
EventBus 借助于第三方的包 mitt
来创建 eventBus
对象,从而实现兄弟组件之间的数据共享。
安装 mitt
依赖包:
创建公共的 eventBus
模块:
1 2 3 4 5 6 import mitt from 'mitt' const bus = mitt ()export default bus
数据接收方调用 bus.on()
监听自定义事件:
1 2 3 4 5 6 7 8 9 10 11 12 import bus from './eventBus.js' export default { data ( ) { return { count : 0 } }, created ( ) { bus.on ('countChange' , (count ) => { this .count = count }) }, }
数据接发送方调用 bus.emit()
触发事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 import bus from './eventBus.js' export default { data ( ) { return { cout : 0 } }, methods : { addCount ( ) { this .count ++ bus.emit ('countChange' , this .count ) }, }, }
vue 3.x 全局配置 axios 实际项目开发中,几乎每个组件中都会使用 axios
发起数据请求。此时会遇到如下两个问题:
每个组件中都需要导入 axios
(代码臃肿)
每次发请求都需要填写完整的请求路径(不利于后期的维护)
在 main.js
文件中进行配置:
1 2 3 4 5 6 axios.defaults .baseURL = 'http://api.com' app.config .globalProperties .$http = axios
组件调用:
1 this .$http .get ('/users' )
视频笔记内容来自B站尚硅谷的张天禹老师