$listeners
问题场景1
子组件给父组件传递一些事情,然后父组件去做一些事情,等父组件做完后,子组件再做一些事情,这个问题应该怎么处理?
第一种 emit 和回调函数
<template>
<div>
<button @click="handleClick" :disabled="isLoading">
{{ isLoading ? "loading" : "submit" }}
</button>
<div class="err">{{ error }}</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0, // 点击的次数
isLoading: false,
error: "",
};
},
methods: {
handleClick() {
/*
* 点击次数 +1
* 错误消息清空
* 为了防止重复点击,需要先将 isLoading 设置为 true
* 通知父组件:「我被点击了」,并传递当前的点击次数
* 等待父组件处理(有可能是异步的),将父组件处理的结果设置到 error
*/
this.count++;
this.error = "";
this.isLoading = true;
this.$emit("click", this.count, (err) => {
// 该函数传给父组件,让父组件决定什么时候执行
this.isLoading = false;
this.error = err;
});
},
},
};
</script>
<style>
.err {
color: #f40;
font-size: 12px;
}
</style>
<template>
<LoadingButton @click="handleClick" />
</template>
<script>
import LoadingButton from "./LoadingButton";
export default {
components: {
LoadingButton,
},
methods: {
handleClick(count, callback) {
console.log("父组件", count);
setTimeout(() => {
// 处理完成
callback("请填写账号");
}, 3000);
},
},
};
</script>
<style>
body {
text-align: center;
}
</style>
第二种 使用listeners,因为他可以在子组件拿到父组件传递过来的事件函数
<template>
<div>
<button @click="handleClick" :disabled="isLoading">
{{ isLoading ? "loading" : "submit" }}
</button>
<div class="err">{{ error }}</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0, // 点击的次数
isLoading: false,
error: "",
};
},
methods: {
async handleClick() {
/*
* 点击次数 +1
* 错误消息清空
* 为了防止重复点击,需要先将 isLoading 设置为 true
* 通知父组件:「我被点击了」,并传递当前的点击次数
* 等待父组件处理(有可能是异步的),将父组件处理的结果设置到 error
*/
this.count++;
this.error = "";
this.isLoading = true;
if (this.$listeners.click) {
// 判断父组件是否传递了事件处理函数 click
const err = await this.$listeners.click(this.count);
this.isLoading = false;
this.error = err;
}
},
},
};
</script>
<style>
.err {
color: #f40;
font-size: 12px;
}
</style>
<template>
<LoadingButton @click="handleClick" />
</template>
<script>
import LoadingButton from "./LoadingButton";
export default {
components: {
LoadingButton,
},
methods: {
async handleClick(count) {
console.log("父组件", count);
return new Promise((resolve) => {
setTimeout(() => {
resolve("有一个未知错误");
}, 3000);
});
},
},
};
</script>
<style>
body {
text-align: center;
}
</style>
第三种 使用 props 将父组件的事件传递过来,然后在子组件里面调用。类似于listeners。
<template>
<div>
<button @click="handleClick" :disabled="isLoading">
{{ isLoading ? "loading" : "submit" }}
</button>
<div class="err">{{ error }}</div>
</div>
</template>
<script>
export default {
props: {
click: Function,
},
data() {
return {
count: 0, // 点击的次数
isLoading: false,
error: "",
};
},
methods: {
async handleClick() {
/*
* 点击次数 +1
* 错误消息清空
* 为了防止重复点击,需要先将 isLoading 设置为 true
* 通知父组件:「我被点击了」,并传递当前的点击次数
* 等待父组件处理(有可能是异步的),将父组件处理的结果设置到 error
*/
this.count++;
this.error = "";
this.isLoading = true;
if (this.click) {
const err = await this.click(this.count);
this.isLoading = false;
this.error = err;
}
},
},
};
</script>
<style>
.err {
color: #f40;
font-size: 12px;
}
</style>
<template>
<LoadingButton :click="handleClick" />
</template>
<script>
import LoadingButton from "./LoadingButton";
export default {
components: {
LoadingButton,
},
methods: {
async handleClick(count) {
console.log("父组件", count);
return new Promise((resolve) => {
setTimeout(() => {
resolve("有一个未知错误");
}, 3000);
});
},
},
};
</script>
<style>
body {
text-align: center;
}
</style>
问题场景2
如果有个组件,他们里面还有两层结构的组件,分别叫它儿子组件,孙子组件,加入孙子组件想传递一个参数给他爷爷,他的父亲中间没啥特殊处理,只是把事件传递给最上层的组件,应该怎么处理?
方案1
父组件注册子组件的事件,然后在自己的处理函数里面再把事件抛出到最上层组件,这种方法是如果父组件如果需要一些额外的处理,也可以这么这么做,确定是如果组件层级不止这三层,且层级结构更深的话, 写法比较繁琐
方案二
如果中间层级没啥别的处理,只是抛出组件,可以使用 v-on="$listeners" ,就可以代替中间层的注册事件,在method写处理方法,同时把事件往上抛的过程。
下面解释一下上面的v-on,其实事件的最通用的写法是 v-on="{事件名1:事件处理函数,事件名2:事件处理函数}",v-on是可以同时传递很多事件的。而$listeners就是父组件各种处理函数,结构也是{事件名1:事件处理函数,事件名2:事件处理函数},而我们写的最多的是处理一个事件,比方说 v-on:submit="handleSubmit",也可以简写成 @submit="handleSubmit"
$listeners是vue的一个实例属性,它用于获取父组件传过来的所有事件函数
<!-- 父组件 -->
<Child @event1="handleEvent1" @event2="handleEvent2" />
// 子组件
this.$listeners // { event1: handleEvent1, event2: handleEvent2 }
$emit和$listeners通信的异同相同点:均可实现子组件向父组件传递消息
差异点:
$emit更加符合单向数据流,子组件仅发出通知,由父组件监听做出改变;而$listeners则是在子组件中直接使用了父组件的方法。- 调试工具可以监听到子组件
$emit的事件,但无法监听到$listeners中的方法调用。(想想为什么)- 由于
$listeners中可以获得传递过来的方法,因此调用方法可以得到其返回值。但$emit仅仅是向父组件发出通知,无法知晓父组件处理的结果
对于上述中的第三点,可以在
$emit中传递回调函数来解决父组件:
<template> <Child @click="handleClick" /> </template> <script> import Child from "./Child" export default { components:{ Child }, methods:{ handleClick(data, callback){ console.log(data); // 得到子组件事件中的数据 setTimeout(()=>{ callback(1); // 一段时间后,调用子组件传递的回调函数 }, 3000) } } } </script>子组件:
<template> <button @click="handleClick"> click </button> </template> <script> export default { methods:{ handleClick(){ this.$emit("click", 123, (data)=>{ console.log(data); // data为父组件处理完成后得到的数据 }) } } } </script>
事件修饰符
针对dom节点的原生事件,vue支持多种修饰符以简化代码
v-model
v-model指令实质是一个语法糖,它是value属性和input事件的结合体
<input :value="data" @input="data=$event.target.value" />
<!-- 等同于 -->
<input v-model="data" />
因为在js那里学的dom事件,事件处理函数都有一个事件参数e,e.target 获取事件处理源,这里其实input,然后再获取当前的value.input 元素和 data参数绑定,不论哪个改变,另外一个就跟着改变,其实就是一个语法糖。
function click(e){
}
详见:表单输入绑定
v-model修饰符
示例
<template>
<div class="container">
<form class="left">
<div class="form-item">
<label>账号</label>
<input type="text" v-model.trim="formData.loginId" />
</div>
<div class="form-item">
<label>密码</label>
<input
type="password"
v-model="formData.loginPwd"
autocomplete="new-password"
/>
</div>
<div class="form-item">
<label>爱好</label>
<label>
<input type="checkbox" value="sports" v-model="formData.loves" />
运动
</label>
<label>
<input type="checkbox" value="movie" v-model="formData.loves" />
电影
</label>
<label>
<input type="checkbox" value="music" v-model="formData.loves" />
音乐
</label>
<label>
<input type="checkbox" value="other" v-model="formData.loves" />
其他
</label>
</div>
<div class="form-item">
性别:
<label>
<input type="radio" v-model="formData.sex" value="male" />
男
</label>
<label>
<input type="radio" v-model="formData.sex" value="female" />
女
</label>
</div>
<div class="form-item">
<label>年龄</label>
<input type="number" v-model.number="formData.age" />
</div>
<div class="form-item">
<label>个人简介</label>
<textarea v-model.lazy="formData.introduce"></textarea>
</div>
<div class="form-item">
<label>职位</label>
<select v-model="formData.job">
<option value="-1">请选择</option>
<option value="1">前端开发</option>
<option value="2">后端开发</option>
<option value="3">全栈开发</option>
<option value="4">项目经理</option>
</select>
</div>
<div class="form-item">
<label>
<input type="checkbox" v-model="formData.remember" />
记住我的选择
</label>
</div>
</form>
<div class="right">
<pre
>{{ formData }}
</pre>
</div>
</div>
</template>
<script>
export default {
data() {
return {
formData: {
loginId: "abc",
loginPwd: "",
loves: ["sports", "movie", "music", "other"],
sex: "male",
age: 18,
introduce: "我是一只小鸭子呀咿呀咿呀哟\n嘎嘎",
job: "1",
remember: false,
},
};
},
};
</script>
<style lang="less">
.container {
width: 1000px;
height: 600px;
border: 1px solid #ccc;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.left,
.right {
width: 50%;
box-sizing: border-box;
padding: 25px;
}
.right {
border-left: 1px solid #ccc;
font-size: 16px;
}
.form-item {
margin-bottom: 15px;
label {
margin-right: 5px;
}
textarea {
display: block;
margin-top: 10px;
resize: none;
width: 100%;
height: 100px;
}
}
</style>
v-model 不是只能用于表单元素,其他任何元素都能使用。
面试题:请阐述一下
v-model的原理
v-model即可以作用于表单元素,又可作用于自定义组件,无论是哪一种情况,它都是一个语法糖,最终会生成一个属性和一个事件
当其作用于表单元素时,vue会根据作用的表单元素类型而生成合适的属性和事件。例如,作用于普通文本框的时候,它会生成value属性和input事件,而当其作用于单选框或多选框时,它会生成checked属性和change事件。
v-model也可作用于自定义组件,当其作用于自定义组件时,默认情况下,当前组件(一般都是子组件)它会生成一个value属性和input事件。
子组件拿到这个value的prop后可以通过v-bind 绑定当前组件的任何位置,然后当这个值改变的时候,可以在子组件里面通过input事件 this.$emit("input", res.data);将值传给父组件,这种情况本质还是prop,emit来处理,只是他替你声明一个value 属性和input 事件。
何为父子组件双向绑定,其实就是父子组件都有一个值,然后父组件改变子组件里面的这个值能改变。子组件改变,父组件的值也会改变。其实就是父子组件之间有一值会在父子组件之间同步。
<Comp v-model="data" />
<!-- 等效于 -->
<Comp :value="data" @input="data=$event" />
开发者可以通过组件的model配置来改变生成的属性和事件
// Comp
const Comp = {
model: {
prop: "number", // 默认为 value
event: "change" // 默认为 input
}
// ...
}
<template>
<div>
// 1.双向绑定了父组件的一个值
<child-component v-model="parentValue"></child-component>
<p>Parent Value: {{ parentValue }}</p>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
parentValue: 'Hello from Parent'
};
}
};
</script>
<template>
<div>
// 1.绑定值
<input type="text" :value="value" @input="updateValue">
</div>
</template>
<script>
export default {
// 2.接受父组件的值
props: ['value'],
methods: {
updateValue(event) {
//3.传递事件
this.$emit('input', event.target.value); // 通知父组件更新数据
}
}
};
</script>
如果子组件不想使用默认的传递过来的input事件和value值,应该怎么做?
只需要调整子组件里面的代码
<template>
<div>
// 1.绑定值
<input type="text" :value="number" @input="updateValue">
</div>
</template>
<script>
export default {
// 定义属性
props: ['number'],
// 你想传递用什么属性,事件你想用哪个用model 定义
model: {
prop: "number", // 默认为 value
event: "change" // 默认为 input
},
methods: {
updateValue(event) {
// 用什么事件
this.$emit('change', event.target.value);
}
}
};
</script>
v-model 的简单例子
sync
在Vue中,props属性是单向数据传输的,父级的prop的更新会向下流动到子组件中,但是反过来不行。可是有些情况,我们需要对prop进行“双向绑定”。上文中,我们提到了使用v-model实现双向绑定。但有时候我们希望一个组件可以实现多个数据的“双向绑定”,而v-model一个组件只能有一个(Vue3.0可以有多个),这时候就需要使用到.sync。
.sync与v-model的异同
相同点:
- 两者的本质都是语法糖,目的都是实现组件与外部数据的双向绑定
- 两个都是通过属性+事件来实现的
不同点:
- 一个组件只能定义一个v-model,但可以定义多个.sync
- v-model与.sync对于的事件名称不同,v-model默认事件为input,可以通过配置model来修改,.sync事件名称固定为update:属性名
在开发业务时,有时候需要使用一个遮罩层来阻止用户的行为(更多会使用遮罩层+loading动画),下面通过自定义.sync来实现一个遮罩层
<template>
<!-- 遮罩层 -->
<div class="my-loading" v-show="visible" @click="handleChange">
</div>
</template>
<script>
export default {
props: {
// 定义一个名为checked的属性
visible: {
type: Boolean,
default: false
}
},
methods: {
handleChange () {
// 通过 (`update:xxxx(属性名)`,传递的值) 事件修改外部传入的visible
this.$emit('update:visible', false)
}
}
}
</script>
<!--调用方式-->
<template>
<my-loading :visible.sync="visible"></my-loading>
</template>
<script>
export default {
data() {
return {
visible: false
}
}
}
</script>
表单基础知识
在表单元素里面除了多行文本,单行文本点回车就是提交。 默认表单元素里面的button是 type:submit,默认是提交按钮。除非你把type:改成button。也就是说你点击表单里面的按钮,会触发表单的 submit事件。
<div class="form-item">
性别:
<label>
<input type="radio" v-model="formData.sex" value="male" />
男
</label>
<label>
<input type="radio" v-model="formData.sex" value="female" />
女
</label>
</div>
默认情况下上面的input元素都要加上name 属性才行,但是这里是js控制的,他们的选中只有一个bool值,不加name也可以实现。
修饰符,下面v-model.number是事件修饰符。
<div class="form-item">
<label>年龄</label>
<input type="number" v-model.number="formData.age" />
</div>
文章数据逻辑
组件逻辑
BlogDetail
该组件没有任何难度,根据「属性 - 文章对象」显示出文章信息即可
由于文章的内容属于原始html,因此需要使用v-html指令来设置
另外,文章的内容是不带样式的,因此需要选择一款markdown的css样式(见附件markdown.css)
对于文章中脚本部分的样式,可以使用第三方库highlight.js中提供的样式
import "highlight.js/styles/github.css";