20.$listeners 和 v-model 的使用

102 阅读5分钟

$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"

$listenersvue的一个实例属性,它用于获取父组件传过来的所有事件函数

<!-- 父组件 -->
<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支持多种修饰符以简化代码

详见:事件修饰符、按键修饰符、系统修饰符

Pasted Graphic 5.tiff

Pasted Graphic 6.tiff

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修饰符

image.png

示例

<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的异同

相同点:

  1. 两者的本质都是语法糖,目的都是实现组件与外部数据的双向绑定
  2. 两个都是通过属性+事件来实现的

不同点:

  1. 一个组件只能定义一个v-model,但可以定义多个.sync
  2. 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>

文章数据逻辑

image-20210111142558879

组件逻辑

文章详情页组件结构.png

BlogDetail

该组件没有任何难度,根据「属性 - 文章对象」显示出文章信息即可

由于文章的内容属于原始html,因此需要使用v-html指令来设置

另外,文章的内容是不带样式的,因此需要选择一款markdowncss样式(见附件markdown.css

对于文章中脚本部分的样式,可以使用第三方库highlight.js中提供的样式

import "highlight.js/styles/github.css";

BlogTOC

BlogComment