文章目录
- 三大框架
- 数据绑定和数据流
- vue的安装
- wepack初始化vue项目
- vue脚手架初始化项目(推荐)
- 应用实例和组件实例
- MVC
- Mustache{{}}
- v-bind
- v-on
- v-once、${}
- v-html
- v-if、v-else
- v-show
- data
- methods
- lodash
- 计算属性computed
- 计算属性
- 数组语法
- Style绑定
- 对象语法
- 数组语法
- Class和Style的实现
- template
- v-for
- 数组更新检测
- 替换数组
- 注意事项
- 数据劫持
- v-if与v-for联合使用
- v-for中的就地更新和key属性
- 事件绑定
- addEventListener改变this
- let、const
- 监听事件中执行JavaScript
- 事件处理方法
- 内联处理器中的方法 传入实参
- 访问原始的DOM事件$event
- 多事件处理的监听
- dom4标准的事件和滚屏优化
- 事件修饰符
- 按键修饰符
- 按键码
- 自定义按键修饰符
- 系统修饰键
- .exact 修饰符
- 鼠标按钮修饰符
- 为什么在 HTML 中监听事件?
- 表单输入的数据的双向绑定
- v-model 原理 和 input
- select
- textarea
- checkbox
- radio
- lazy
- .number
- .trim
- 组件化
- model
- Form、子组件上使用v-model
- vue filter 管道 格式化
- 组件注册
- 全局注册
- 局部注册
- 组件名
- 组件分离写法
- web compoents标准
- 插槽slot
- 后备内容-默认插槽内容
- 具名插槽
- 编译作用域-作用域插槽-插槽 prop
- 独占默认插槽的缩写语法
- 解构插槽 Prop
- 动态插槽名
- 树形结构组件和递归
- provide、inject
- 动态组件和异步组件
- 动态组件:is
- keep-alive
- 异步组件
- 父子组件通信
- props
- 1. $emit v-on
- 2. HTML元素上的ref
- $nextTick和异步渲染
- 兄弟组件传值
- 1. 父组件中转
- 2. bus中央事件总线
- 3. vuex
- vuex
- State
- Getter
- Mutation
- Action
- Module
- 数据持久化
- 混入mixin
- 基础
- 选项合并
- 全局混入
- 不足
- 路由
- 基础
- 重定向和别名
- 路由组件传参实战
- 嵌套路由
- 导航守卫
三大框架
- vue
https://cn.vuejs.org/v2/guide/ Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。
与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。
Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
它和react的区别就是官方做了vuex 和router,可以选择集成
Angular也是做了集成,但他不是可选的 - Angular
https://angular.cn/docs Angular 是一个应用设计框架与开发平台,用于创建高效、复杂、精致的单页面应用。
它的关注点是项目应用,自上而下的应用 - React
https://react.docschina.org/ 用于构建用户界面的 JavaScript 库
关注点只是视图层
官方没有提供存储和路由,需要依靠第三方的redux和router
数据绑定和数据流
- 数据绑定
- react 单向数据绑定->event->state更改->视图变更(只能通过事件改变状态,然后改变视图)
- vue 双向数据绑定-> event->state更改->视图变更
->v-model->视图变更->state更改
- 数据流
- 父子组件中数据按照什么方向流动->单向数据流
- react/vue->父组件->state->子组件->props(父组件可以改变子组件的内容,而子组件不能改变父组件的内容)
- props 是不可变的
vue的安装
cdn、npm 两种方式
推荐npm
# 最新稳定版
$ npm install vue
wepack初始化vue项目
- 初始化项目
npm init -y
- -y 的含义:yes的意思,在init的时候省去了敲回车的步骤,生成的默认的package.json
- version 版本号,一般从0.1.0开始
- description描述
- keywords 关键词用逗号隔开
- author名字加邮箱
- license UNLICENSE,一般不授权公用
- 安装webpack
- 安装vue-loader
https://vue-loader.vuejs.org/zh/
vue脚手架初始化项目(推荐)
应用实例和组件实例
const app = Vue.createApp({})
createApp 返回一个应用实例
应用实例上也暴露了很多方法
app.component 注册组件
app.directive 注册指令
app.filter 注册过滤器
app.use 使用插件
大多数这样的方法返回的是createApp创建出来的应用实例,所以允许链式操作
根组件本质就是一个对象{}
createApp执行的时候需要一个根组件
根组件是vue渲染的起点
根元素是一个html元素
createApp执行的时候也需要一个根元素
<div id="app"></div>
app.mount返回的是一个根组件的实例
const app = Vue.createApp({App})//
const vm = app.mount('#app') //返回的是一个根组件的实例 #app也可以用.app
// 根组件的实例挂载在App组件中的#app元素中
// vm --ViewModel-MVVM-VM
// vue 不是一个完整的mvvm模型,只是参考了这个模型
组件实例中的属性和方法都是以$开头
MVC
- M model
- V View(视图解析有两种方式)
- 后端渲染:后端直接把jsp解析成html返回,这是比较传统的开发
- 前端渲染:ajax请请求异步获取数据然后操作dom把数据渲染到页面
- C Controller
controller->model->view
view->controller->model
MVVM的雏形
Mustache{{}}
- 插值表达式 插入的是js表达式不是js语句
- 插值表达式{{}}->通过vue中的h函数来渲染
- 属性
- attribute:HTML的扩展 title src href->attr
- property:在对象内部存储数据,通常用来描述数据结构->prop
- 数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:
<span>Message: {{ msg }}</span>
- Mustache 标签将会被替代为对应数据对象上 msg property 的值。无论何时,绑定的数据对象上 msg property 发生了改变,插值处的内容都会更新。
- Mustache不能插入全局变量,只能插入实例上的数据或传递进来的属性
- Mustache 语法不能作用在 HTML attribute 上,遇到这种情况应该使用 v-bind 指令:
<div v-bind:id="dynamicId"></div>
v-bind
<!-- 完整语法 -->
<a v-bind:href="url">...</a>
<!-- 缩写 -->
<a :href="url">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a :[key]="url"> ... </a>
动态属性名参数key不能出现空格和引号,因为HTML的合法属性名不能出现空格引号
<a :['data-'+attr]="url"> ... </a>
<!-- 这种是错误的写法,应该把 'data-'+attr 放到data中去-->
null作为属性是无效的,可以用来清除指定属性
<h1 :[null]="title"> ... </h1>
<!-- 这种是错误的写法,应该把 'data-'+attr 放到data中去-->
对于布尔 attribute (它们只要存在就意味着值为 true),v-bind 工作起来略有不同,在这个例子中:
<button v-bind:disabled="isButtonDisabled">Button</button>
如果 isButtonDisabled 的值是 null、undefined 或 false,则 disabled attribute 甚至不会被包含在渲染出来的 button 元素中。
v-on
v-on 缩写
<!-- 完整语法 -->
<a v-on:click="doSomething">...</a>
<!-- 缩写 -->
<a @click="doSomething">...</a>
<!-- 动态参数的缩写 (2.6.0+) -->
<a @[event]="doSomething"> ... </a>
它们看起来可能与普通的 HTML 略有不同,但 : 与 @ 对于 attribute 名来说都是合法字符,在所有支持 Vue 的浏览器都能被正确地解析。而且,它们不会出现在最终渲染的标记中。缩写语法是完全可选的,但随着你更深入地了解它们的作用,你会庆幸拥有它们。
v-once、${}
:::tip v-once
不需要表达式
只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
:::
<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- 组件 -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` 指令-->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
- 通过 v-once 创建低开销的静态组件
渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来,就像这样:
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})
- 不推荐使用v-once
<h1 v-once>{{title}}-{{author}}</h1>
:::warning
假如title是固定不变的,author是动态变化的,加了v-once,影响的是h1里面的所有子元素只会渲染一次
推荐写法:
外界定义const title = 'this is my title'
使用${title}
不能使用{{title}}
视图上vue指定的插入方式的数据变量必须声明在实例上,所以用ES6${}
的用法
:::
v-html
:::tip v-html
更新元素的 innerHTML。
注意:内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译。
如果试图使用 v-html 组合模板,可以重新考虑是否通过使用组件来替代。
:::
:::danger
在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 XSS 攻击。只在可信内容上使用 v-html,永不用在用户提交的内容上。
:::
:::warning
在单文件组件里,scoped 的样式不会应用在 v-html 内部,因为那部分 HTML 没有被 Vue 的模板编译器处理。如果你希望针对 v-html 的内容设置带作用域的 CSS,你可以替换为 CSS Modules 或用一个额外的全局 style 元素手动设置类似 BEM 的作用域策略。
:::
示例:
<div v-html="html"></div>
- 插值不能解析html
- 注意,你不能使用 v-html 来复合局部模板,因为 Vue 不是基于字符串的模板引擎,无法对它进行管理。反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。
- v-html的原理是使用innerHTML,innerHTML容易导致XSS攻击
//var text = '<script>alert(123)'; 这种在HTML5中已经被禁止,但禁止的不彻底,如下:
var text = '<img src="123" οnerrοr="alert(123)">'
document.getElementById('app').innerHTML = text;
//所以安全的写法如下
document.getElementById('app').appendChild(text)
v-if、v-else
v-if
如果是false删除节点插入注释节点<!--v-if-->
占位
如果是true则用元素替换掉注释节点
实际上是两个节点。一个真实节点。一个是注释节点,他们来回切换,并不是删除
::: details v-if、v-show底层实现原理
而对于v-if内部的子组件都会进行销毁和替换,所以频繁切换开销很大
v-if只有为true才会渲染及其子组件,惰性渲染
v-show总是会渲染,用display来进行渲染和隐藏,初始化渲染有开销
如果切换频繁就用v-show,否则就用v-show
<!DOCTYPE html>
<html lang="en">
<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">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script>
/***
*
* showPool
* [
* dom: {
* dom
* type:if/show
* prop:data
* }
* ]
*
* eventPool
* [
* dom:{
* handle
* }
* ]
*/
var Vue = (function () {
function Vue(options) {
// 生命周期钩子
var recycles = {
beforeCreate: options.beforeCreate.bind(this),
created: options.created.bind(this),
beforeMount: options.beforeMount.bind(this),
mounted: options.mounted.bind(this)
}
recycles.beforeCreate()
this.$el = document.querySelector(options.el);
this.$data = options.data();
// this._methods = options.methods; 不需要暴露methods
this._init(this, options.template, options.methods, recycles)
}
Vue.prototype._init = function (vm, template, methods, recycles) {
recycles.created();//vue创建完成 开始初始化数据
var container = document.createElement('div')
container.innerHTML = template;
var showPool = new Map;
var eventPool = new Map;
initData(vm, showPool);
initPool(container, methods, showPool, eventPool);
bindEvent(vm, eventPool);
render(vm, showPool, container, recycles)
}
function initData(vm, showPool) {
for (var key in vm.$data) {
(function (k) {
Object.defineProperty(vm, k, {
get: function () {
return vm.$data[k]
},
set: function (newValue) {
vm.$data[k] = newValue;
update(vm, k, showPool);
},
})
})(key)
}
}
function initPool(container, methods, showPool, eventPool) {
// 获取所有节点
var allNodes = container.getElementsByTagName('*');
var dom = null;
for (var index = 0; index < allNodes.length; index++) {
dom = allNodes[index];
var vIfData = dom.getAttribute("v-if");
var vShowData = dom.getAttribute("v-show");
var vEvent = dom.getAttribute("@click")
if (vIfData) {
showPool.set(
dom,
{
type: 'if',
prop: vIfData
}
);
// 这些属性使用完后删除
dom.removeAttribute('v-if');
} else if (vShowData) {
showPool.set(
dom,
{
type: 'show',
prop: vShowData
}
)
dom.removeAttribute('v-show');
}
if (vEvent) {
eventPool.set(
dom,
methods[vEvent]
)
dom.removeAttribute('@click');
}
}
}
function bindEvent(vm, eventPool) {
console.log(eventPool)
for (var [dom, handle] of eventPool) {
vm[handle.name] = handle;
dom.addEventListener('click', vm[handle.name].bind(vm), false)
}
}
function render(vm, showPool, container, recycles) {
var _data = vm.$data;
var _el = vm.$el
for (var [dom, info] of showPool) {
switch (info.type) {
case 'if':
// 创建if注释节点
info.comment = document.createComment(['v-if']);
!_data[info.prop] && dom.parentNode.replaceChild(info.comment, dom)
break;
case 'show':
!_data[info.prop] && (dom.style.display = 'none')
break;
default:
break;
}
}
recycles.beforeMount()
_el.appendChild(container)
recycles.mounted()
}
function update(vm, key, showPool) {
var _data = vm.$data;
for (var [dom, info] of showPool) {
if (info.prop === key) {
switch (info.type) {
case 'if':
!_data[key] ? dom.parentNode.replaceChild(info.comment, dom)
: info.comment.parentNode.replaceChild(dom, info.comment)
break;
case 'show':
!_data[key] ? (dom.style.display = 'none')
: (dom.removeAttribute('style'));
break;
default:
break;
}
}
}
}
return Vue;
})()
var option = {
data() {
return {
isShowImg1: true,
isShowImg2: true
}
},
methods: {
showImg1() {
console.log(1)
this.isShowImg1 = !this.isShowImg1
},
showImg2() {
console.log(2)
this.isShowImg2 = !this.isShowImg2
}
},
template: `
<div>
<div>
<img v-if="isShowImg1" width="200" src="https://i.loli.net/2021/11/30/rwX96yEPo4mQ8nA.jpg">
<img v-show="isShowImg2" width="200" src="https://i.loli.net/2021/11/24/JErPvRZgG48Dhnc.jpg">
hello
</div>
<button @click="showImg1">显示图片1</button>
<button @click="showImg2">显示图片2</button>
</div>
`,
el: '#app',
beforeCreate() {
console.log('beforeCreate')
},
created() {
console.log('created')
},
beforeMount() {
console.log('beforeMount')
},
mounted() {
console.log('mouted')
}
}
var vm = new Vue(option)
console.log(vm)
</script>
</html>
:::
v-show
使用display:none
隐藏节点
data
vue在创建实例的过程中就会调用data函数
返回数据对象
通过响应式包装后存储在实例的data 是一个响应式对象)
并且实例可以越过$data访问属性 (所以在methods中可以通过this.data来访问数据属性)
- data为什么必须是一个函数?
vue中$data的实现
function Vue(options){
this.$data = options.data();
var _this = this;
for (const key in this.$data) {
(function(k){
//独立的作用域
// k->当前作用域的临时的局部变量
Object.defineProperty(_this,k,{
get:function(){
return _this.$data[k]
},
set:function(newValue){
_this.$data[k] = newValue;
},
})
})(key)
}
}
var vm = new Vue({
data(){
return {
a:1,b:2
}
}
})
console.log(vm.a)
假如vue中的data是一个对象
var data = {
a:1,
b:2,
}
var vm1 = new Vue({data});
var vm2 = new Vue({data});
vm1.a = 6; //vm2.a也会改变
console.log(vm1,vm2)
// 如果构造vue使用的是同一个对象,修改其中一个vue实例会影响另外一个
使用函数可以生成一个新的对象
var data = function(){
return {
a:1,
b:2
}
}
// this.$data = options.data(); 每次函数执行都会产生新的对象赋值给$data
确保vue实例中的data是独一无二的,不互相影响(也可以用深拷贝)
methods
向组件实例添加方法
- vue创建实例时,会自动为methods绑定当前vm实例this,保证事件监听时回调函数始终执行当前实例
- 方法要避免使用箭头函数
插值表达式里面可以执行方法->视图上可以调用方法,但最好不要有副作用
{{method}}
实例中直接挂载methods中的每一个方法
没有vue.methods的属性
我们可以直接通过this调用方法
- methods的实现
var Vue = (function () {
function Vue(options) {
this.$data = options.data();
this._methods = options.methods;
this._init(this)
}
Vue.prototype._init = function (vm) {
initData(vm);
initMethods(vm);
}
function initData(vm) {
for (var key in vm.$data) {
(function (k) {
Object.defineProperty(vm, k, {
get: function () {
return vm.$data[k]
},
set: function (newValue) {
vm.$data[k] = newValue;
},
})
})(key)
}
}
function initMethods(vm){
console.log(vm._methods)
for (var key in vm._methods) {
console.log(key)
vm[key] = vm._methods[key]
}
}
return Vue;
})()
var vm = new Vue({
data(){
return {
a:1,b:2
}
},
methods:{
increaseA(num){
this.a+=num
}
}
})
console.log(vm)
vm.increaseA(22)
console.log(vm.a)
lodash
vue官方推荐使用lodash这个js工具库,lodash里面都是一些无副作用的方法
https://www.lodashjs.com/docs/lodash.debounce
https://github.com/lodash/lodash/blob/master/isObject.js
lodash里面的实现源码,值得我们反复学习
- 使用lodash.cloneDeep实现深拷贝
import _ from 'lodash'
var obj = {id:1,name:{a:'xx'},fn:function(){}};
var obj2 = _.cloneDeep(obj);
obj2.name.a = 'obj2';
console.log(obj,obj2)
计算属性computed
- 解决模板中复杂的逻辑运算及复用的问题
- 计算属性只在内部逻辑依赖的属性变化的时候才会再次调用
- 计算属性会缓存其依赖的上一次计算出的数据结果
- 多次复用一个相同值的数据,只调用一次
计算属性缓存vs方法
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<div id="example">
<p>Original message: "{{ message }}"</p>
<p>Computed reversed message: "{{ reversedMessage }}"</p>
<p>Reversed message: "{{ reversedMessage() }}"</p>
</div>
组件
// 在组件中
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
// 这里我们声明了一个计算属性 reversedMessage。
// 我们提供的函数将用作 property vm.reversedMessage 的 getter 函数:
}
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
我们可以将同一函数定义为一个方法而不是一个计算属性。
两种方式的最终结果确实是完全相同的。
然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。
只在相关响应式依赖发生改变时它们才会重新求值。
这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
computed: {
now: function () {
return Date.now()
}
}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
我们为什么需要缓存?
假设我们有一个性能开销比较大的计算属性 A,
它需要遍历一个巨大的数组并做大量的计算。
然后我们可能有其他的计算属性依赖于 A。
如果没有缓存,我们将不可避免的多次执行 A 的 getter!
如果你不希望有缓存,请用方法来替代。
- 计算属性不用写括号,方法需要写括号
{{ reversedMessage }}
{{ reversedMessage() }}
- 计算属性会将计算出的结果缓存到到实例vm上
计算属性的简单实现
::: details 计算属性的简单实现
<!DOCTYPE html>
<html lang="en">
<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">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script>
var Vue = (function () {
var computedData = {};
var reg_var = /\{\{(.+?)\}\}/g
var dataPool = {}
/***
* total:{
* value:函数执行返回的结果
* get:get
* dep:[a,b]
* }
*/
function Vue(options) {
this.$data = options.data();
this.$el = document.querySelector(options.el);
this._init(this, options.computed, options.template)
}
Vue.prototype._init = function (vm,computed,template) {
initData(vm);
compuetedReactive(vm,computed);
render(vm,template);
}
// 响应式数据
function initData(vm) {
for (var key in vm.$data) {
(function (k) {
Object.defineProperty(vm, k, {
get: function () {
return vm.$data[k]
},
set: function (newValue) {
vm.$data[k] = newValue;
update(vm,k)
_updateComputedData(vm,k,function(k2){
update(vm,k2)
})
},
})
})(key)
}
}
function update(vm,key){
dataPool[key].textContent = vm[key];
}
function _updateComputedData(vm,key,update){
var _dep = null;
for(var _key in computedData){
_dep = computedData[_key].dep;
for (var i = 0; i < _dep.length; i++) {
if(_dep[i] === key){
vm['total'] = computedData[_key].get();
update(_key)
}
}
}
}
function render(vm, template) {
var container = document.createElement('div');
var _el = vm.$el;
// 渲染模板
container.innerHTML = template;
var domTree = _complieTemplate(vm, container);
_el.appendChild(domTree);
}
function _complieTemplate(vm, container) {
// 获取所有节点
var allNodes = container.getElementsByTagName('*');
var dom = null;
for (var i = 0; i < allNodes.length; i++) {
dom = allNodes[i];
var matched = dom.textContent.match(reg_var)
if(matched){
dom.textContent = dom.textContent.replace(reg_var,function(node,key){
dataPool[key.trim()] = dom;
return vm[key.trim()]
})
}
}
return container;
}
// 响应式计算属性
function compuetedReactive(vm, computed) {
_initComputedData(vm, computed);
for (var key in computedData) {
(function (key) {
Object.defineProperty(vm, key, {
get() {
return computedData[key].value;
},
// ===== todo =====
// =============
set(newVal){
computedData[key].value = newVal
}
})
})(key)
}
}
function _initComputedData(vm, computed) {
for (var key in computed) {
var descriptor = Object.getOwnPropertyDescriptor(computed, key);
console.log(descriptor);
// 计算属性中也可以写对象的get方法
var descriptorFn = descriptor.value.get ? descriptor.value.get : descriptor.value;
console.log(computedData)
computedData[key] = {}
computedData[key].value = descriptorFn.call(vm) //更改计算属性方法中的this为vm
computedData[key].get = descriptorFn.bind(vm);//更改计算属性方法中的this为vm
computedData[key].dep = _collectDep(descriptorFn);
}
}
// 获取计算属性中的依赖data
function _collectDep(fn) {
var _collecion = fn.toString().match(/this.(.+?)/g);
if (_collecion.length > 0) {
for (var i = 0; i < _collecion.length; i++) {
_collecion[i] = _collecion[i].split('.')[1];
}
// console.log(_collecion);
}
return _collecion;
}
return Vue;
})()
var vm = new Vue({
el: '#app',
template: `
<span>{{a}}</span>
<span>+</span>
<span>{{b}}</span>
<span>=</span>
<span>{{total}}</span>
`,
data() {
return {
a: 1,
b: 2
}
},
computed: {
total() {
console.log('computed total');
return this.a + this.b
}
// ,total:{
// get(){
// console.log('computed total');
// return this.a + this.b
// }
// }
}
})
</script>
</html>
:::
## watch侦听器
* watch VS computed
* computed:
关注点在模板上->抽离复用模板中复杂的计算逻辑
特点->计算属性中依赖的data改变后,重新调用
* watch:
关注点在数据更新:给数据添加侦听器,数据更新时,侦听器函数执行
特点:当数据更新时需要完成的逻辑
watch中可以获得 新数据和旧数据
watch中可以侦听computed和data中的数据
watch和computed中都是函数,他们的名字和data中都是一样的
案例:
页面点击事件触发,methods中进行了data的修改和下一页的请求
上述可以分成两个方法,一个修改data,一个发起请求,发起请求的调用放到对应data的侦听器中
::: wath的实现 todo (3)
todo
:::
## WebPack
style-loader
sass less -> sass-loader
postcss postcss-loader-> autoprefixer(配置浏览器兼容列表)
css-loader:模块化解析
vue-style-loader
yarn add sass sass-loader postcss postcss-loader autoprefixer css-loader vue-style-loader -D
module 配置rules
## Class绑定
>在将 v-bind 用于 class 和 style 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
### 对象语法
我们可以传给 v-bind:class 一个对象,以动态地切换 class:
```html
<div v-bind:class="{ active: isActive }"></div>
上面的语法表示 active 这个 class 存在与否将取决于数据 property isActive 的 truthiness。
你可以在对象中传入更多字段来动态切换多个 class。
此外,v-bind:class 指令也可以与普通的 class attribute 共存。当有如下模板:
<div
class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>
<!-- text-danger也可以不用引号包裹,因为对于对象的key本身也是一个属性 -->
和如下 data:
data: {
isActive: true,
hasError: false
}
结果渲染为:
<div class="static active"></div>
当 isActive 或者 hasError 变化时,class 列表将相应地更新。例如,如果 hasError 的值为 true,class 列表将变为 “static active text-danger”。
- 绑定的数据对象不必内联定义在模板里:
<div v-bind:class="classObject"></div>
data: {
classObject: {
active: true,
'text-danger': false
}
}
计算属性
- 渲染的结果和上面一样。我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:
<div v-bind:class="classObject"></div>
data: {
isActive: true,
error: null
},
computed: {
classObject: function () {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
数组语法
我们可以把一个数组传给 v-bind:class,以应用一个 class 列表:
<div v-bind:class="[activeClass, errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
渲染为:
<div class="active text-danger"></div>
如果你也想根据条件切换列表中的 class,可以用三元表达式:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
这样写将始终添加 errorClass,但是只有在 isActive 是 truthy[1] 时才添加 activeClass。
不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
- 当在一个自定义组件上使用 class property 时,这些 class 将被添加到该组件的根元素上面。这个元素上已经存在的 class 不会被覆盖。
Style绑定
对象语法
v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。
CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
activeColor: 'red',
fontSize: 30
}
直接绑定到一个样式对象通常更好,这会让模板更清晰:
<div v-bind:style="styleObject"></div>
data: {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
同样的,对象语法常常结合返回对象的计算属性使用。
数组语法
v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上:
<div v-bind:style="[baseStyles, overridingStyles]"></div>
- 自动添加前缀
vue在运行时,当 v-bind:style 使用需要添加浏览器引擎前缀的 CSS property 时,如 transform,Vue.js 会自动侦测并添加相应的前缀。 - 多重值
从 2.3.0 起你可以为 style 绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex。
- mouted中this.$attrs:父组件通过调用组件时传递的属性的集合集合
Class和Style的实现
todo
template
template标签不会渲染到页面上
所以通常在template上写vue-if等指令
v-for
::: details 详细使用
<template>
<div id="app">
v-for遍历数组
<ul>
<li v-for="item of list" :key="item.id">
<!-- key属性必须是唯一的,方便vue就地更新策略的实施 -->
<span>No.:{{ item.id }} </span>
<span>Name.:{{ item.name }} </span>
<span>Score.:{{ item.score }} </span>
</li>
</ul>
v-for遍历对象
<ul>
<li v-for="(item,key,index) in object" :key="index">
<!-- key属性必须是唯一的,方便vue就地更新策略的实施 -->
<span>order.:{{ index }} </span>
<span>key.:{{ key }} </span>
<span>item.:
<template v-if="key==='hobbies'">
<ul>
<li v-for="(item,index) of item" :key="index">
<span>{{index}}:{{item}}</span>
</li>
</ul>
</template>
<template v-else>
{{item}}
</template>
</span>
</li>
</ul>
计算属性与v-for
<ul>
<li v-for="item of computedList" :key="item.id">
{{item.name}}->
<span :style="{color:item.pass?'green':'red'}">
{{item.pass?'Pass':'Faild'}}
</span>
</li>
</ul>
methods与v-for
<ul v-for="(numbers,index) of myArray" :key="index">
<li v-for="item of even(numbers)" :key="item">
{{item}}
</li>
</ul>
值范围
<div>
<span v-for="s in 5" :key="s" :style="{color:s<=starNum ? 'orange':'grey'}"
@click="setStar(s)"
>
★
</span>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
list: [
{
id: 1,
name: "jack",
score: 66,
},
{
id: 2,
name: "tom",
score: 68,
},
{
id: 3,
name: "kapa",
score: 55,
},
],
object:{
name:'hello',
age:18,
height:169,
hobbies:['travel','piano','sleep']
},
myArray:[
[1,2,3],
[4,5,6,8]
],
starNum:3
};
},
methods: {
even(num){
return num.filter(number=>number%2===0)
},
setStar(num){
this.starNum = num
}
},
computed:{
computedList(){
return this.list.map(item=>{
item.pass =item.score>=60;
return item;
})
}
}
};
</script>
:::
- 用 v-for 把一个数组对应为一组元素
v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。
在 v-for 块中,我们可以访问所有父作用域的 property。v-for 还支持一个可选的第二个参数,即当前项的索引。
:::warning
你也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法,它两底层实现都是一样的
根据语义化的规则:of 一般用于遍历数组 、in一般用于遍历对象
:::
- 在 v-for 里使用对象
你也可以用 v-for 来遍历一个对象的 property。
你也可以提供第二个的参数为 property 名称 (也就是键名)
还可以用第三个参数作为索引
在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。
- key
:::warning
不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值
:::
数组更新检测
var obj= {test:1}
Object.defineProperty(obj,obj.test,{
// set(newVal){
// // obj['key'] = newVal 这样会循环调用 内存溢出
// test = newVal
// },
get(){
return obj.test
}
})
var vm = {
data:{
a:1,
b:2,
list:[1,2,3,4]
}
}
for(var key in vm.data){
(function(kuy){
Object.defineProperty(vm,key,{
get(){
console.log('数据获取');
return vm.data[key]
},
set(newValue){
console.log('数据设置');
vm.data[key] = newValue;
}
})
})(key)
}
console.log(vm.a);
vm.b = 3
console.log(vm.b)
vm.list = [2,3,4]//对数组重新赋值的话可以监听到
console.log(vm.list)
// 这个地方虽然设置了,但并没有监听到
vm.list.push(6) //push没有返回值
console.log(vm.list)
// 数据变化了,但set没有执行
当我们使用Object.defineProperty进行了监听了一个数组。
然后使用push添加了元素后,push方法没有返回新的数组,所以Object.defineProperty无法监听
vue对push等没有返回值的方法进行了封装,
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
vue3中对以上方法使用了proxy
替换数组
变更方法,顾名思义,会变更调用了这些方法的原始数组。
相比之下,也有非变更方法,例如 filter()、concat() 和 slice()。它们不会变更原始数组,而总是返回一个新数组。
当使用非变更方法时,可以用新数组替换旧数组:
example1.items = example1.items.filter(function (item) {
return item.message.match(/Foo/)
})
你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。
幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,
所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
注意事项
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。深入响应式原理中有相关的讨论。
- 对于对象
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。 - 对于数组
Vue 不能检测以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength
- 异步更新队列
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
数据劫持
todo
v-if与v-for联合使用
- vue3中、当 v-if 与 v-for 一起使用时,v-if 具有比 v-for 更高的优先级
在处理列表list之前,先进行了判断,在执行每一项
v-if 决定是否渲染的问题
v-for 怎么渲染的问题 - 混合使用的问题
渲染todoList
<li v-for="todo of todoList" :key="todo.id"
v-if="!todo.completed">
{{todo.content}}
</li>
The ‘todoList’ variable inside ‘v-for’ directive should be replaced with a computed property that returns filtered array instead. You should not mix ‘v-for’ with ‘v-if’.eslint-plugin-vue
- 在处理列表,先进行了if判断,此时却还没有拿到for中的变量:
- v-if的优先级高于v-for
- v-if没有获取到todo
- v-for时才获取到
在渲染期间,属性todo确实被访问了,
todo没有并没有定义在实例上
- 解决1: v-for 提到外层
````<template v-for>‘ cannot be keyed. Place the key on real elements instead```
<template v-for="todo of todoList">
<li v-if="!todo.completed" :key="todo.id">{{todo.content}} </li>
</template>
- 解决2: v-for 使用计算属性替代if
<li v-for="todo of notCompletedTodoList" :key="todo.id" >
{{todo.content}}
</li>
computed:{
notCompletedTodoList(){
return this.todoList.filter(item=>!item.completed)
}
}
- 特殊情况
if里面不访问for中的数据,可以写在一块,但仍然不建议这样使用
<li v-for="todo of todoList" :key="todo.id"
v-if="todoList.length>0">
{{todo.content}}
</li>
应该先进行todoList.length>0的判断
<template v-if="todoList.length>0">
</template>
todoList.length>0 也可以写在computed中
- 当 Vue2 处理指令时,v-for 比 v-if 具有更高的优先级,所以这个模板:
<ul>
<li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
>
{{ user.name }}
</li>
</ul>
将会经过如下运算:
this.users.map(function (user) {
if (user.isActive) {
return user.name
}
})
因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。
所以最好替换为计算属性
v-for中的就地更新和key属性
- 一个list列表中有3个元素,每一个元素都有一个根据索引来删除list中对应元素的事件
- 当我们点击删除第二个元素时,list中就会移除index=1的元素。
- 但渲染视图时,视图上先移除了第三个元素,然后第二个元素位置上的text替换为第三个元素。
- 这就是就地更新策略
https://cn.vuejs.org/v2/guide/list.html#%E7%BB%B4%E6%8A%A4%E7%8A%B6%E6%80%81
- 问题
如果视图上面list中有3个input,我们在input上分别输入1 2 3 ,然后删除第二个
虽然也是就地更新原则,但是视图上第二个input上的输入为还是2,但实际上第二个元素已经被删掉了。
原因,就地更新不跟踪临时状态的dom,子组件中的属性也是如此
- 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
- 为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key attribute
- 建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
- key 避免使用index,如果list变化的话,index也会发生变化
事件绑定
addEventListener改变this
document.addEventListener('click',test.call(obj))
// 它打印的document,绑定的时机并不是执行的时机,这句执行后他绑定的是test.call(obj)返回的结果
// 解决
document.addEventListener('click',test.bind(obj))
// test.bind返回的是一个函数,函数并没有执行
let、const
let->方便,随时可变(程序的扩展) 一派
const->不可变,(符合变量命名规范),但程序扩展的时候需要可变 一派
- 常量固定字段用const声明
可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。
监听事件中执行JavaScript
https://cn.vuejs.org/v2/guide/events.html
事件处理方法
然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on 指令中是不可行的。因此 v-on 还可以接收一个需要调用的方法名称。
<div id="example-2">
<!-- `greet` 是在下面定义的方法名 -->
<button v-on:click="greet">Greet</button>
</div>
var example2 = new Vue({
el: '#example-2',
data: {
name: 'Vue.js'
},
// 在 `methods` 对象中定义方法
methods: {
greet: function (event) {
// `this` 在方法里指向当前 Vue 实例
alert('Hello ' + this.name + '!')
// `event` 是原生 DOM 事件
if (event) {
alert(event.target.tagName)
}
}
}
})
// 也可以用 JavaScript 直接调用方法
example2.greet() // => 'Hello Vue.js!'
内联处理器中的方法 传入实参
除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:
<div id="example-3">
<button v-on:click="say('hi')">Say hi</button>
<button v-on:click="say('what')">Say what</button>
</div>
new Vue({
el: '#example-3',
methods: {
say: function (message) {
alert(message)
}
}
})
访问原始的DOM事件$event
- 有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event 把它传入方法:
传入$event的参数名字是不能变的,但顺序可以变
<button v-on:click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
// ...
methods: {
warn: function (message, event) {
// 现在我们可以访问原生事件对象
if (event) {
event.preventDefault()
}
alert(message)
}
}
多事件处理的监听
<div id="example-3">
<button v-on:click="say('hi'),say('what')">Say hi</button>
<button v-on:click="say('what')">Say what</button>
</div>
new Vue({
el: '#example-3',
methods: {
say: function (message) {
alert(message)
}
}
})
dom4标准的事件和滚屏优化
- 事件冒泡
- 事件捕获
- EventTarget.addEventListener()
https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
- 语法
- target.addEventListener(type, listener, options);
- target.addEventListener(type, listener, useCapture);
- 参数
- options 可选
- 一个指定有关 listener 属性的可选参数对象。可用的选项如下:
- capture: Boolean,表示 listener 会在该类型的事件捕获阶段传播到该 EventTarget 时触发。
- once: Boolean,表示 listener 在添加之后最多只调用一次。如果是 true, listener 会在其被调用之后自动移除。
- passive: Boolean,设置为true时,表示 listener 永远不会调用 preventDefault()。如果 listener 仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。查看 使用 passive 改善的滚屏性能 了解更多.
- signal:AbortSignal,该 AbortSignal 的 abort() 方法被调用时,监听器会被移除。
- useCapture 可选
- Boolean,在DOM树中,注册了listener的元素, 是否要先于它下面的EventTarget,调用该listener。 当useCapture(设为true) 时,沿着DOM树向上冒泡的事件,不会触发listener。当一个元素嵌套了另一个元素,并且两个元素都对同一事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。进一步的解释可以查看 事件流 及 JavaScript Event order 文档。 如果没有指定, useCapture 默认为 false 。
- 使用 passive 改善的滚屏性能
根据规范,passive 选项的默认值始终为false。但是,这引入了处理某些触摸事件(以及其他)的事件监听器在尝试处理滚动时阻止浏览器的主线程的可能性,从而导致滚动处理期间性能可能大大降低。
为防止出现此问题,某些浏览器(特别是Chrome和Firefox)已将文档级节点 Window,Document和Document.body的touchstart (en-US)和touchmove (en-US)事件的passive选项的默认值更改为true。这可以防止调用事件监听器,因此在用户滚动时无法阻止页面呈现。
var elem = document.getElementById('elem');
elem.addEventListener('touchmove', function listener() { /* do something */ }, { passive: true });
// touchmove默认的事件行为就是滚动
// window 阻止了默认行为
添加passive参数后,touchmove事件不会阻塞页面的滚动(同样适用于鼠标的滚轮事件)。
:::details 实战
<!DOCTYPE html>
<html lang="en">
<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">
<title>Document</title>
</head>
<body>
https://zhuanlan.zhihu.com/p/24555031
<div style="background-color: orange;height: 100px;" ></div>
<div style="background-color: grey;height: 100px;" ></div>
<div style="background-color: orange;height: 100px;" ></div>
<div style="background-color: grey;height: 100px;" ></div>
<div style="background-color: orange;height: 100px;" ></div>
<div style="background-color: grey;height: 100px;" ></div>
<div style="background-color: orange;height: 100px;" ></div>
<div style="background-color: grey;height: 100px;" ></div>
<div style="background-color: orange;height: 100px;" ></div>
<div style="background-color: grey;height: 100px;" ></div>
<div style="background-color: orange;height: 100px;" ></div>
</body>
<script>
// 当页面开始活动的时候 touchmove事件就会触发
// window.addEventListener("touchstart", function (e) {
// console.log(e.defaultPrevented); // will be false
// // 并没有阻止默认行为
// e.preventDefault(); // does nothing since the listener is passive
// console.log(e.defaultPrevented); // still false
// }
// )
window.addEventListener("touchstart", function (e) {
console.log(document,e.defaultPrevented); // will be false
// 有阻止默认行为
e.preventDefault(); // nothing since the listener is passive
/***
* 1. 处理器程序执行 console.log(e.defaultPrevented); 非常大的性能问题 scroll卡顿
* 因为此时上面执行完后浏览器要判断你有没有调用阻止默认行为的方法
* 2. 执行默认行为后 不去滚动
* 3. 默认为true,就不用判断,因为你不会调用preventDefault,直接响应滚动
*
*/
console.log(document,e.defaultPrevented); // still true
// 但此时页面并没有滚动
},
{
passive:false
}
)
// passive=true 阻止默认行为永远不会调用
// 页面可以直接执行滚动逻辑而不用判断你有没有调用阻止默认行为的方法
// 两个线程处理滚动的问题
// 1.处理器程序的执行
// 2. 执行默认行为
</script>
</html>
:::
事件修饰符
在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。
.prevent 阻止事件默认行为
.stop 阻止单击事件继续传播
.once 只调用一次事件处理然后移除监听器
.capture 事件捕获而不是冒泡
.self 当前元素自身时触发处理函数
.passive 不调用preventDefault
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
:::warning
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。
不要把 .passive 和 .prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive 会告诉浏览器你不想阻止事件的默认行为。
:::
按键修饰符
在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为 v-on 在监听键盘事件时添加按键修饰符:
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input v-on:keyup.enter="submit">
你可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符。
<input v-on:keyup.page-down="onPageDown">
在上述示例中,处理函数只会在 $event.key 等于 PageDown 时被调用。
按键码
为了在必要的情况下支持旧浏览器,Vue 提供了绝大多数常用的按键码的别名:
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
:::warning
有一些按键 (.esc 以及所有的方向键) 在 IE9 中有不同的 key 值, 如果你想支持 IE9,这些内置的别名应该是首选。
:::
自定义按键修饰符
你还可以通过全局 config.keyCodes 对象自定义按键修饰符别名:
// 可以使用 `v-on:keyup.f1`
Vue.config.keyCodes.f1 = 112
系统修饰键
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
.meta
<!-- Alt + C -->
<input v-on:keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div v-on:click.ctrl="doSomething">Do something</div>
.exact 修饰符
鼠标按钮修饰符
为什么在 HTML 中监听事件?
当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。
表单输入的数据的双向绑定
v-model 原理 和 input
v-model 会忽略掉value checked selected
{{inputText}}<br>
<input type="text" v-model="inputText"/>
<input type="text" :value="inputText" @input="setInputText"/>
data(){return {
inputText:'inputText'
}},
methods: {
setInputText(e){
this.inputText = e.target.value
}
},
- 区别
- v-model 是输入完一个字确认后改变
- @input 是输入的时候就改变
select
- @change-:value
- 单选
- 多选时 (绑定到一个数组):
- 用 v-for 渲染的动态选项:
textarea
checkbox
多选 @change-:checked
每个checkbox的value不一样,v-model为一个数组
radio
单选 @change-:checked
每个radio的value不一样,v-model为一个字符串
lazy
change->失去焦点
在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步 (除了上述输入法组合文字时)。你可以添加 lazy 修饰符,从而转为在 change 事件_之后_进行同步:
<!-- 在“change”时而非“input”时更新 -->
<input v-model.lazy="msg">
.number
如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 number 修饰符:
<input v-model.number="age" type="number">
这通常很有用,因为即使在 type=“number” 时,HTML 输入元素的值也总会返回字符串。
如果这个值无法被 parseFloat() 解析,则会返回原始的值。
只是非数字无法输入
.trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:
<input v-model.trim="msg">
组件化
父组件传递给子组件一个属性的初始值,子组件中这个初始值变化后传递给父组件
- 子组件中通过watch监听这个属性,当属性变化或通过emit自定义方法传递
- 子组件中通过计算属性,写属性的get、set方法,在set中通过emit自定义方法传递
- 问题:当子组件第一次获得这个初始值时没有调用上述方法
- 解决:在mounted中,判断这个属性值是否为空,如果不为空,调用相应方法
model
CSS绝对定位元素left设为50%实现水平居中
https://www.jb51.net/css/213725.html
- 先一个黑色透明的背景,v-show是否显示,z-index
- 绝对定位居中弹出框
- 这个弹出框分为上中下
- head定义
- content 插槽
- footer 按钮,确认取消,点击按钮控制模态框的关闭显示,并向上传递对应事件
Form、子组件上使用v-model
当用在组件上时,v-model 则会这样:
<custom-input
v-bind:value="searchText"
v-on:input="searchText = $event"
></custom-input>
为了让它正常工作,这个组件内的 input 必须:
- 将其 value attribute 绑定到一个名叫 value 的 prop 上
- 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出
写成代码之后是这样的:
Vue.component('custom-input', {
props: ['value'],
template: `
<input
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
>
`
})
现在 v-model 就应该可以在这个组件上完美地工作起来了:
<custom-input v-model="searchText"></custom-input>
- v-model 也可以写计算属性,这个计算属性依赖于父组件传递的属性
这个计算属性中写一个set方法,set方法中emit
<div class="input-box">
<input type="text" v-model="username" placeholder="username" />
</div>
import { computed } from "@vue/reactivity";
export default {
name: "Form",
props: {
myusername: {
type: "String",
},
},
setup(props ,ctx) {
const username = computed({
get() {
return props.username;
},
set(newValue) {
// 更改父组件的值
ctx.emit("update:username",newValue);
},
});
return {
username,
};
},
};
vue filter 管道 格式化
组件注册
const app = Vue.createApp({})
app.mount('#app')
createApp创建一个vue的应用
createApp(组件)->这个组件就是根组件
vue创建组件的本质就是一个对象
使用根组件创建一个vue对象挂载到视图上
全局注册
Vue.component('my-component-name', { /* ... */ })
公共组件库 可以 用全局注册
局部注册
通过一个普通的 JavaScript 对象来定义组件:
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
var ComponentC = { /* ... */ }
然后在 components 选项中定义你想要使用的组件:
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
对于 components 对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。
组件名
- 使用 kebab-case
Vue.component('my-component-name', { /* ... */ })
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如
<my-component-name>
。
- 使用 PascalCase
Vue.component('MyComponentName', { /* ... */ })
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name>
和<MyComponentName>
都是可接受的。
注意,尽管如此,直接在 DOM (即非字符串的模板(就是html文件中)) 中使用时只有 kebab-case 是有效的。
- 不幸的是,由于 HTML 是大小写不敏感的,在 DOM 模板中必须仍使用 kebab-case。
好例子
<!-- 在单文件组件和字符串模板中 -->
<MyComponent/>
<!-- 在 DOM 模板中 -->
<my-component></my-component>
或者
<!-- 在所有地方 -->
<my-component></my-component>
- vue文件的名称必须是PascalCase
- vue实例上的属性
组件分离写法
传统vue都是将template,js,style写在一个vue单文件中
使文件内容看起来很长
-MyTitle
---index.js
---MyTitle.scss
---MyTitle.tpl //视图
- tpl loder
web compoents标准
vue模板系统参考了web compoents规范来进行上层设计
自定义标签
定义属性
自定义组件—自定义标签—渲染
- 标签template solt
- 容器 shadowDOM
- customElements.define
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_shadow_DOM
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_custom_elements
https://developer.mozilla.org/zh-CN/docs/Web/Web_Components/Using_templates_and_slots
插槽slot
内容占位符
:::tip
在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope
:::
后备内容-默认插槽内容
具名插槽
base-layout 组件:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带 name 的 slot 出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 template 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
v-slot:xxx -> #xxx
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<!-- <template v-slot:default> -->
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<!-- </template> -->
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
:::warning
- 任何没有被包裹在带有 v-slot 的 template 中的内容都会被视为默认插槽的内容。
- vue3中,即使默认的插槽内容也要 显示声明 #default
:::
注意 v-slot 只能添加在 template 上
编译作用域-作用域插槽-插槽 prop
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
一个带有如下模板的<current-user>
组件:
为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 slot 元素的一个 attribute 绑定上去
<span>
<!-- <slot v-bind:user="user">
{{ user.lastName }}
</slot> -->
<slot :user="user"></slot>
</span>
绑定在 slot 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
还有其他简写
<current-user>
<template #default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
<!-- -->
<current-user>
<template #="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
- v-slot:default,default为插槽名称
- v-slot:default后面赋值slotProps,为
<slot :user="user"></slot>
绑定的属性集合{user}
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps,但你也可以使用任意你喜欢的名字。
独占默认插槽的缩写语法
当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</current-user>
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
</current-user>
:::warning
只要出现多个插槽,请始终为所有的插槽使用完整的基于 template的语法:
:::
解构插槽 Prop
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<current-user v-slot="{ user }">
{{ user.firstName }}
</current-user>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person:
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
动态插槽名
动态指令参数v-bind:也可以用在 v-slot 上,来定义动态的插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
This is Me
</template>
</base-layout>
export default {
data: {
dynamicslotname: "header"
},
};
相当于
<base-layout>
<template v-slot:header>
This is Me
</template>
</base-layout>
树形结构组件和递归
provide、inject
动态组件和异步组件
动态组件:is
https://v3.cn.vuejs.org/guide/component-dynamic-async.html#在动态组件上使用-keep-alive
- 在新闻内容页分为上下结构
- 上图片下文字
- 上文字下图片
- 我们分别有文字和图片组件
- 传统写法是判断:内容页有两个结构,通过if判断选择1结构还是2结构
- 使用动态组件,内容页只有一个结构,通过if判断返回对应的组件名就可以了
- keep-alive 可以给动态组件加上缓存
- 核心
<component :is="componentTag"></component>
<!-- componentTag 为自定义的变量,将需要加载的组件名赋值给它,即可在<component />标签出现的位置,渲染该组件。 -->
keep-alive
- keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 transition 相似,
- keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
- 当组件在 keep-alive 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
- activated 和 deactivated 将会在 keep-alive 树内的所有嵌套组件中触发。
- 主要用于保留组件状态或避免重新渲染。提高性能
- 场景
- v-if显示组件总是伴随着销毁和重建,使用keepalive+动态组件提高性能
- 假如动态组件上有表单元素,keepalive可以缓存动态组件上的表单值
<div id="dynamic-component-demo" class="demo">
<!-- 导航栏按钮 -->
<button v-for="tab in tabs" :key="tab" :class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab">
{{ tab }}
</button>
<!-- 激活的导航选项卡的内容 -->
<!-- Inactive components will be cached! -->
<keep-alive>
<component :is="currentTabComponent"> </component>
</keep-alive>
</div>
</body>
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
computed: {
currentTabComponent() {
// 计算属性动态返回当前显示的组件
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
// tab-home选项卡的内容
app.component('tab-home', {
template: `<div class="demo-tab">Home component</div>`
})
// tab-posts文章内容
app.component('tab-posts', {
template: `<div class="dynamic-component-demo-posts-tab">`
})
app.component('tab-archive', {
template: `<div class="demo-tab">Archive component</div>`
})
app.mount('#dynamic-component-demo')
异步组件
- 假如页面上有一个很重的组件,加载是比较慢的,影响第一次加载的性能.
- 为了提高性能可以不加载这个组件,只有当指定的页面用到这个组件的时候在加载它
- 实际业务:图表展示页面,首页只是展示全国数据,点击详细显示各省的图表数据
<template>
<comp v-if="show"></comp>
<button @click="show = true">显示详情组件</button>
</template>
<!--
export default{
components:{
Comp:Comp
}
}
-->
- test 打开浏览器network,首页comp组件相关资源就被加载了,首页展示比较慢,comp显示的时候没有再次加载
- 异步加载
export default{
components:{
Comp:()=>{
return import('../comp/index.vue')
};
// Comp:()=>import('../comp/index.vue')
}
}
- 官网demo
- 全局注册异步组件
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
app.component('async-component', AsyncComp)
- 局部注册异步组件时
import { createApp, defineAsyncComponent } from 'vue'
createApp({
// ...
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
- 聚焦
- 滚动加载列表
父子组件通信
props
- 父组件向子组件中传递数据->业务中通常使用props
:::warning 当尝试在子组件中修改props中的值时
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.
- 子组件不能直接修改父组件->单向数据流
- 解决:->通过data和computed包裹,然后再html中使用
:::
- 子组件向父组件中传递数据
1. $emit v-on
You can use $emit method for this purpose.
v-on directive captures the child components events that is emitted by $emit
- Child component triggers clicked event:
export default {
methods: {
onClickButton (event) {
this.$emit('clicked', 'someValue')
}
}
}
- Parent component receive clicked event:
<div>
<child @clicked="onClickChild"></child>
</div>
export default {
methods: {
onClickChild (value) {
console.log(value) // someValue
}
}
}
2. HTML元素上的ref
- 父元素可以通过this.$ref获取子组件上的值
https://v3.cn.vuejs.org/api/special-attributes.html#ref
- ref
- ref 被用来给元素或子组件注册引用信息。
- 引用信息将会被注册在父组件的 $refs 对象上。
- 如果在普通的 DOM 元素上使用,引用指向的就是那个 DOM 元素;
- 如果用在子组件上,引用就指向组件实
<!-- vm.$refs.p 会是 DOM 节点 -->
<p ref="p">hello</p>
<!-- vm.$refs.child 会是子组件实例 -->
<child-component ref="child"></child-component>
<!-- 当动态绑定时,我们可以将 ref 定义为回调函数,显式地传递元素或组件实例 -->
<child-component :ref="(el) => child = el"></child-component>
- 关于 ref 注册时机的重要说明:因为 ref 本身是作为渲染函数的结果而创建的,在初始渲染时你不能访问它们——它们还不存在!
- $refs 也是非响应式的,因此你不应该试图用它在模板中做数据绑定。也不要在计算属性中使用
- refs是只读的,不能修改
onMounted(){
const oLink = document.createElement('a');
this.$refs.myRef = oLink
}
https://v3.cn.vuejs.org/guide/component-template-refs.html
:::warning
$refs 只会在组件渲染完成之后(mounted)生效。这仅作为一个用于直接操作子元素的“逃生舱”——你应该避免在模板或计算属性中访问 $refs。
:::
$nextTick和异步渲染
- vue中data有一个list,页面上通过v-for显示这个list的元素
- 定义一个按钮,点击事件为:list.push一个元素并打印list.length
- 此时当push后list.length+1,但实际没有发生变化
- 因为vue中dom为异步渲染,push后的一瞬间并没有渲染完成
- 什么时候渲染完毕呢
- $nextTick
//list多次push
//nextTick批量异步更新而不是每一个push都异步更新
//1.
this.$nextTick(function () {
console.log(this.$refs.myRef.childNodes.length) // => '已更新'
})
//2. ES2017 async/await 语法
await this.$nextTick()
console.log(this.$refs.myRef.childNodes.length) // => '已更新'
- v-if 动态ref 的获取 $nextTick
兄弟组件传值
1. 父组件中转
A1要向A2传值 、 可以用$emit传给A、A在使用v-bind传给A2
2. bus中央事件总线
- 在项目中创建一个单独的eventBus.js文件
- 该js文件的内容很简单,就是暴露一个vue实例而已。
import Vue from 'vue'
export default new Vue()
- 有人喜欢在main.js全局引入该js文件,我一般在需要使用到组件中引入。
- 父组件中注册并使用子组件one和two
- 分别在子组件one和two中引入eventBus.js
- one组件向two组件传值:(传值使用$emit)
import eventBus from './event-bus'
export default{
methods:{
handle(){
eventBus.$emit('event-name','parm')
}
}
}
- two组件接收到one组件的值:(接收值使用$on)
import eventBus from './event-bus'
export default {
methods:{
handle(x){
console.log(x)
}
},
// mounted中监听事件
mounted() {
eventBus.$on('event-name', this.handle)
},
// 组件销毁的时候也要解绑监听器,否则会造成内存泄露
beforeDestroy(){
eventBus.$on('event-name', this.handle)
}
}
3. vuex
vuex
https://vuex.vuejs.org/zh/ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RsAShVTC-1654528100800)(https://vuex.vuejs.org/vuex.png “请深刻理解这张图的每个步骤”)]
- src/store/index.js
import { createStore } from 'vuex'
// state 数据状态定义
// mutations 同步修改state 不能做异步操作(定时器发请求等) 第一个参数时state 第二个参数时需要修改的值(可选)
// actions异步提交mutations 第一个参数时store,第二个参数时修改的值 store.commit 提交mutations
export default createStore({
state: {
name: 'jack',
list: [
{title: '吃饭',complete: false},
{title: '睡觉',complete: false},
{title: '敲代码',complete: true}
]
},
mutations: {
addTodo (state, payload) {
state.list.push(payload)
},
actions: {
asyncAddTodo (store, params) {
setTimeout(() => {
// commit 提交mutations
store.commit('addTodo', params)
}, 3000)
}
},
// 模块化
modules: {
}
})
- 组件中使用
setup () {
// import { useStore } from 'vuex'
const store = useStore()
// 通过计算属性拿到store.state
const list = computed(() => { return store.state.list })
const Add = (val) => {
store.commit('addTodo', {
title: val,
complete: false
})
}
return { Add, list }
}
State
- 存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则
- 在 Vue 组件中通过计算属性获得 Vuex 状态
- 那么我们如何在 Vue 组件中展示状态呢?
- 由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态
- Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex))
- 通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
// return store.state.count
return this.$store.state.count
}
}
}
- mapState 辅助函数
- 当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。
- 为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
- 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
- mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?->对象展开运算符
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
Getter
- Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。
- 就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- Getter 接受 state 作为其第一个参数:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
- 通过属性访问
- Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
- Getter 也可以接受其他 getter 作为第二个参数:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
- 我们可以很容易地在任何组件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
- 通过方法访问
- 你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
- mapGetters 辅助函数
- mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
- 如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
Mutation
- 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
- Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
- 提交载荷(Payload)
- 你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):
- 在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
- 对象风格的提交方式
- 提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
store.commit({
type: 'increment',
amount: 10
})
- 当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变
- Mutation 需遵守 Vue 的响应规则
- 最好提前在你的 store 中初始化好所有所需属性。
- 当需要在对象上添加新属性时,你应该
- 使用 Vue.set(obj, ‘newProp’, 123), 或者
- 以新对象替换老对象。例如,利用对象展开运算 (opens new window)我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
//state.obj.newProp = 123 不能这样写,它的引用没有发生变化,vue无法监听
- 使用常量替代 Mutation 事件类型
- 使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式
- 减少硬编码
- 大项目多人开发时,对mutations统一管理
- 可以根据模块来分类来给mutation type命名
- ES6计算属性名
- 从ECMAScript 2015开始,对象初始化语法开始支持计算属性名。其允许在[]中放入表达式,计算结果可以当做属性名。
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
export const ADD_TODO = 'ADD_TODO '
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION, ADD_TODO } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
//[methodName](){}===methodName(){}
[SOME_MUTATION] (state) {
// mutate state
},
// [ADD_TODO](state,params){
// state.list.push(params)
// },
[ADD_TODO](state,{id,name}){
state.list.push({id,name})
}
}
})
- Mutation必须是同步函数
- Mutation执行后,devtool开发者工具记录state的值
- 现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
- mutation的异步回调会被执行,只是不被开发者工具检测
- 在组件中提交 Mutation
- 你可以在组件中使用 this.$store.commit(‘xxx’) 提交 mutation
- 或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
Action
- 在 mutation 中混合异步调用会导致你的程序很难调试。
- mutation 都是同步事务:
- Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
- Action中也可以commit多个mutation
- Action经常用来请求后端接口
- 分发 Action
- Action 通过 store.dispatch 方法触发:
store.dispatch('increment')
- Actions 支持同样的载荷方式和对象方式进行分发:
- 在组件中分发 Action
- 你在组件中使用 this.$store.dispatch(‘xxx’) 分发 action
- 或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
- 组合 Action
- Action 通常是异步的,那么如何知道 action 什么时候结束呢?
- 首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:
store.dispatch('actionA').then(() => {
// ...
})
//在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
- 最后,如果我们利用 async / await,我们可以如下组合 action
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
Module
- 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
- 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
- 模块的局部状态
- 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象
- 同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:
- 对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:
- 命名空间
- 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
- 如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
- 在带命名空间的模块注册全局 action
- 若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
数据持久化
- vue中组件中的data和store中的state都存储与内存中,页面刷新就会丢失
- 使用H5数据存储localstorage和sessionStorage
- vuex-persistedstate和vuex-persist
https://github.com/robinvdvleuten/vuex-persistedstate
混入mixin
基础
- Mixin 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
- 一个 mixin 对象可以包含任意组件选项。当组件使用 mixin 对象时,所有 mixin 对象的选项将被“混合”进入该组件本身的选项。
// 定义一个 mixin 对象
const myMixin = {
created() {
this.hello()
},
methods: {
hello() {
console.log('hello from mixin!')
}
}
}
// 定义一个使用此 mixin 对象的应用
// 全局混入
const app = Vue.createApp({
mixins: [myMixin]
})
app.mount('#mixins-basic') // => "hello from mixin!"
选项合并
当组件和 mixin 对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
- data
- 比如,每个 mixin 可以拥有自己的 data 函数。每个 data 函数都会被调用,并将返回结果合并。在数据的 property 发生冲突时,会以组件自身的数据为优先。
- 同名钩子函数
- 同名钩子函数将合并为一个数组,因此都将被调用。另外,mixin 对象的钩子将在组件自身钩子之前调用
- 值为对象的选项
- 例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
全局混入
const app = Vue.createApp({
myOption: 'hello!'
})
// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
created() {
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
app.mount('#mixins-global') // => "hello!"
- Mixin 也可以进行全局注册。使用时格外小心!一旦使用全局 mixin,它将影响每一个之后创建的组件 (例如,每个子组件)
- 大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用 mixin。
不足
- 在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:
- Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。
- 可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。
路由
基础
- src\router\index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Start from '../views/Start.vue'
const routes = [
{
path: '/home', // 路由路径必须以/开头
name: 'Home', // 路由名称
component: Home // 路由组件
},
{
path: '/', // 路由路径必须以/开头
name: 'Start', // 路由名称
component: Start // 路由组件
},
]
const router = createRouter({
history: createWebHistory('vue3todolist'),
routes
})
export default router
在组件中使用
- 声明式
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</template>
<!-- 组件定义后后面需要空一行-->
- 编程式vue3
import { useRouter } from 'vue-router'
export default {
name: 'Start',
components: {},
setup () {
// 全局路由对象 进行路由跳转
const router = useRouter()
// 当前页面路由对象 接收路由参数
// const route = useRoute()
const start = () => {
router.push({
path: '/home',
query: {
name: 'hello',
age: 18,
obj: JSON.stringify({ school: 'mock' })
// 传递对象应该为json字符串,接收的时候再解析
// 参数会拼接到地址栏,优点是参数不会丢失
}
})
}
return { start }
}
}
========================================
- 首先简单来说明一下route的区别
- $router : 全局路由,任何页面都可以调用其 push(), replace(), go() 等方法。
- $route : 当前路由对象,可以获取对应的 name, path, params, query 等属性。
- vue-router传递参数分为两大类
- 编程式的导航 router.push,编程式导航传递参数有两种类型:字符串、对象。
- 声明式的导航 router-link
- router.push() vs router.replace()
- router.push() 描述:跳转到不同的url,但这个方法回向history栈添加一个记录,点击后退会返回到上一个页面。
- router.replace()同样是跳转到指定的url,但是这个方法不会向history里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。
- 两种导航的源码
- 编程式的导航params+name->命名路由
const routers = [
{
path:'/news/:userId',
//1. path中最好传入参数名userId,防止刷新页面后参数丢失
//2. path:'/news',
//如果path中不传入参数名,那么页面刷新后,本应出现的参数123就不见了,并且浏览器URL中的也不会有参数
//3. 也可以传递多个参数
//path:'/news/:userId/:name',
name:'news'
//1. 命名路由:通过一个名称来标识一个路由显得更方便一些,特别是在链接一个路由,或者是执行一些跳转的时候。你可以在创建 Router 实例的时候,在 routes 配置中给某个路由设置名称。
//2. 要链接到一个命名路由,可以给 router-link 的 to 属性传一个对象:
//3. 声明式导航:<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
//4. 编程式导航:router.push({ name: 'user', params: { userId: 123 } })
//5. 这两种方式都会把路由导航到 /user/123 路径。
component:News
}
]
// 组件中的使用 this.$router.push({ name: 'news', params: { userId: 123 }})
// 目标组件中接收 {{this.$route.params.userId}}
- 编程式的导航query+path
const routers = [
{
path:'/news',
//由于用的是query参数,name可以注释掉,只需要path就可以了
//name:'news'
//类式get请求,刷新不会丢失参数
component:News
}
]
// 组件中的使用 this.$router.push({ name: 'news', params: { userId: 123 }})
// 目标组件中接收 {{this.$route.params.userId}}
- 声明式的导航
<!-- 字符串 -->
<router-link to="home">Home</router-link>
<!-- 渲染结果 -->
<a href="home">Home</a>
<!-- 使用 v-bind 的 JS 表达式 -->
<router-link v-bind:to="'home'">Home</router-link>
<!-- 不写 v-bind 也可以,就像绑定别的属性一样 -->
<!-- 同上 -->
<router-link :to="{ path: 'home' }">Home</router-link>
- 声明式的导航to跳转和传参的两种方式
<!-- 1.name+params 命名的路由 -->
<!-- params在url中不显示参数 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
<!-- 2.path+query 带查询参数,下面的结果为 /register?plan=private -->
<!-- query在url中显示参数 -->
<router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>
- 动态路由匹配中的问题
- 响应路由参数的变化
- 提醒一下,当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。
- 复用组件时,想对路由参数的变化作出响应的话,你可以简单地 watch (监测变化) $route 对象
- 或者使用 beforeRouteUpdate 导航守卫:
- 捕获所有路由或 404 Not found 路由
- 常规参数只会匹配被 / 分隔的 URL 片段中的字符。如果想匹配任意路径,我们可以使用通配符 (*)
- 当使用通配符路由时,请确保路由的顺序是正确的,也就是说含有通配符的路由应该放在最后。路由 { path: ‘*’ } 通常用于客户端 404 错误。
routes: [
// 404组件的匹配应该放到routes数组的末尾
// 匹配规则类似于switch的default
{
path: '*', component: NotFound404
}
]
- 当使用一个通配符时,$route.params 内会自动添加一个名为 pathMatch 参数。它包含了 URL 通过通配符被匹配的部分
- 匹配优先级
有时候,同一个路径可以匹配多个路由,此时,匹配的优先级就按照路由的定义顺序:路由定义得越早,优先级就越高。
- 路由mode
- hash 默认
- history
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。 - vue2
const router = new VueRouter({
mode: 'history',
routes: [...]
})
- vue3
const router = createRouter({
history: createWebHistory('vue3todolist'),
routes
})
- 路由懒加载
const routes = [
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited. 路由懒加载
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')// 按需引入
}
]
重定向和别名
重定向也是通过 routes 配置来完成,下面例子是从 /a 重定向到 /b:
routes: [
{ path: '/a', redirect: '/b' }
]
重定向的目标也可以是一个命名的路由:
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
甚至是一个方法,动态返回重定向目标:
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
注意导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上。为 /a 路由添加一个 beforeEnter 守卫并不会有任何效果。
- “重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么“别名”又是什么呢?
- /a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
上面对应的路由配置为:
routes: [
{ path: '/a', component: A, alias: '/b' }
]
路由组件传参实战
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [{ path: '/user/:id', component: User }]
})
在我们实现页面接收参数的时候,通常会使用$route.query或者 $route.params去获取路由中传递的参数,使用路由解耦就不需要这样获取,直接用props接收参数。
- 布尔模式-在组件中用props接收
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
]
})
- 如果 props 被设置为 true,route.params 将会被设置为组件属性。
- 对象模式
- 如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用。
const router = new VueRouter({
routes: [
{
path: '/user',
component: User,
props: { id: false }
}
]
})
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
- 函数模式
- 你可以创建一个函数返回 props。这样你便可以将get参数转换成另一种类型,将静态值与基于路由的值结合等等。
const router = new VueRouter({
routes: [
{
path: '/user',
component: User,
props: route => ({ id: route.query.id })
}
]
})
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
- URL /user?id=123 会将 {id: ‘123’} 作为属性传递给 User 组件。
嵌套路由
实际应用界面,通常由多层嵌套的组件组合而成。
比如,我们 “首页”组件中,还嵌套着 “登录”和 “注册”组件,那么URL对应就是/home/login和/home/reg。
- home组件
<template id="home">
<!-- 注意:组件只能有一个根元素,所以我们包装到这个div中 -->
<div>
<h2>首页HOME</h2>
<router-link to="/home/login">登录</router-link>
<router-link to="/home/reg">注册</router-link>
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</template>
- 登录和注册2个组件
<template id="login">
<div>登录界面</div>
</template>
<!-- ------------ -->
<template id="reg">
<div>注册界面</div>
</template>
- 路由
const routes = [
{ path: '/', redirect: '/home' },
{
path: '/home',
component: Home,
//在home路由配置了它的children。这就是嵌套路由
children:[
{ path: '/home/login', component: Login},
{ path: '/home/reg', component: Reg}
]
},
]
:::details
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<script src="http://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
</head>
<body>
<div id="box">
<p>
<router-link to="/home">home</router-link>
<router-link to="/news">news</router-link>
</p>
<router-view></router-view>
</div>
<!-- 模板抽离出来 -->
<template id="home">
<!-- 注意:组件只能有一个根元素,所以我们包装到这个div中 -->
<div>
<h2>首页</h2>
<router-link to="/home/login">登录</router-link>
<router-link to="/home/reg">注册</router-link>
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
</template>
<template id="news">
<div>新闻</div>
</template>
<template id="login">
<div>登录界面</div>
</template>
<template id="reg">
<div>注册界面</div>
</template>
<script type="text/javascript">
// 1. 定义(路由)组件。
const Home = { template: '#home' };
const News = { template: '#news' };
const Login = { template: '#login' };
const Reg = { template: '#reg' };
// 2. 定义路由
const routes = [
{ path: '/', redirect: '/home' },
{
path: '/home',
component: Home,
children:[
{ path: '/home/login', component: Login},
{ path: '/home/reg', component: Reg}
]
},
{ path: '/news', component: News}
]
// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
routes // (缩写)相当于 routes: routes
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
router
}).$mount('#box')
// 现在,应用已经启动了!
</script>
</body>
</html>
:::
导航守卫
https://router.vuejs.org/zh/guide/advanced/navigation-guards.htm
- 全局前置守卫beforeEach
你可以使用 router.beforeEach 注册一个全局前置守卫:
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
- 当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
- 每个守卫方法接收三个参数:
- to: Route: 即将要进入的目标 路由对象
- from: Route: 当前导航正要离开的路由
- next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
- next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
- next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
- 假如当前是登录页面登录成功跳转的时候发现token不正确,调用next(false)禁止跳转
- next(‘/’) 或者 next({ path: ‘/’ }):
跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航
你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。 - next(error):如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。
- 确保 next 函数在任何给定的导航守卫中都被严格调用一次。它可以出现多于一次,但是只能在所有的逻辑路径都不重叠的情况下,否则钩子永远都不会被解析或报错。这里有一个在用户未能验证身份时重定向到 /login 的示例:
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// 如果用户未能验证身份,则 `next` 会被调用两次
next()
})
// GOOD
router.beforeEach((to, from, next) => {
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
- 关于vue-router的beforeEach无限循环的问题解决
//在通过判断token的值来确定用户是不是已经登录,如果没有登录的用户只能访问登录页
router.beforeEach((to, from, next) => {
let token = window.localStorage.getItem('token');
if (!token) { //token不存在 跳转到登录页
next({ path: '/login' }) //next('login')
//如果当前是登录页并且登录失败会无限循环
//所以只要确定当页面是登录页面的钩子就不让他循环下去
//当跳转路径不是登录页并且token值不存在时才让他跳登录页
//如果是登录就正常跳不要进入循环
} else {
next()
}
})
//next() 表示路由成功,直接进入to路由,不会再次调用router.beforeEach()
//next('login') 表示路由拦截成功,重定向至login,会再次调用router.beforeEach()
//也就是说beforeEach()必须调用next(),否则就会出现无限循环
//next() 和 next('xxx') 不一样,区别就是前者不会再调用router.beforeEach(),后者会!
//解决:
router.beforeEach((to, from, next) => {
let token = window.localStorage.getItem('token');
if(to.path!='/' && !token){
next({path: '/'})
} else {
next()
}
})
- 全局解析守卫beforeResolve
你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。 - 全局后置钩子afterEach
你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {
// ...
})
- 路由独享的守卫
你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
// 这些守卫与全局前置守卫的方法参数是一样的。
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
- 组件内的守卫
你可以在路由组件内直接定义以下路由导航守卫:
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
- beforeRouteEnter守卫不能访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
- 不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
- 注意 beforeRouteEnter是支持给next传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}
- 这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。
beforeRouteLeave (to, from, next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
- 完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的beforeResolve守卫 (2.5+)。
- 导航被确认。
- 调用全局的afterEach钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。