2021-09-29

1、组件通信

(1)父子通信

① props-$emit

② $refs

短信验证码、图形验证码组件我经常用$refs

this.$refs.captcha= 'xxx'

③ 子组件$children[0]
并不保证顺序,所以从来不用这个方法,除非只有一个子组件。

// parent 
this.$children[0].xx = 'xxx'

(2)兄弟通信

通过共同的祖辈组件搭桥,$parent$root
$root$parent 都能够实现访问父组件的属性和方法,两者的区别在于,如果存在多级子组件,通过$parent访问得到的是它最近的一级的父组件,通过$root得到的是它的根父组件。

brother1:
this.$parent.$emit('foo');
brother2:
this.$parent.$on('foo', handle) 

(3)祖先与后代通信

用于组件库的开发,只能祖先给后代传值.。
这时用props属性就会嵌套太多props,不是很合适。

祖先: provide() {
        return {hi: 'hello 后代'}
      }
后代:inject:['hi'] 
-----------------------------------
可直接返回this,子组件直接拿祖先的数据:
祖先: 
  provide() {
      return {hi: this}
  },
  data() {
    return {
        grandfa:'dxl'
    }
  }
后代:
  <p>{{hi.grandfa}}</p>
  inject:['hi'] 

(4)任意两个组件之间通信:事件总线 或 vue

事件总线:创建一个Bus类负责事件派发、监听和回调管理

// Bus:事件派发、监听和回调管理 
class Bus{ 
     constructor(){    
    // {    
    //   eventName1:[fn1,fn2],   
    //   eventName2:[fn3,fn4],   
    // }    
    this.callbacks = {}  
    } 
   $on(name, fn){    
      this.callbacks[name] = this.callbacks[name] || []    
      this.callbacks[name].push(fn) 
   } 
   $emit(name, args){   
     if(this.callbacks[name]){     
     this.callbacks[name].forEach(cb => cb(args))    
    }  
  } 
}
 
// main.js 
Vue.prototype.$bus = new Bus()
以上自定义bus类实现观察者模式
或者直接用vue实例即可。
Vue.prototype.$bus = new Vue()
// child1
this.$bus.$on('foo', handle) 
// child2
this.$bus.$emit('foo')

(5)$attrs$listeners(基本被Vuex替代了)

  • $listeners
    包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件——在创建更高层次的组件时非常有用。
  • $attrs
    包含了父作用域中不被认为 (且不预期为) props 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建更高层次的组件时非常有用。

举例一:
想象一下,你打算封装一个自定义input组件——MyInput,需要从父组件传入type,placeholder,title等多个html元素的原生属性。此时你的MyInput组件props如下:

props:['type','placeholder','title',...]

很繁琐不是吗?$attrs专门为了解决这种问题而诞生,这个属性允许你在使用自定义组件时更像是使用原生html元素。比如:

// 父组件
<my-input placeholder="请输入你的姓名" type="text" title="姓名" v-model="name"/>

// 子组件
<template>
  <div>
    <label>姓名:</label>
    <input v-bind="$attrs" :value="value" @input="$emit('input',$event.target.value)"/>
  </div>
</template>
<script>
export default {
  inheritAttrs:false,
  props:['value']
}
</script>

让MyInput组件实现focus事件:

// 父组件
<my-input @focus="focus" placeholder="请输入你的姓名" type="text" title="姓名" v-model="name"/>

// 子组件
<template>
  <div>
     <input v-bind="$attrsAll" v-on="$listenserAll"/>
  </div>
</template>
<script>
export default {
  inheritAttrs:false,
  props:['value'],
  computed:{
     $attrsAll() {
      return {
        value: this.value,
        ...this.$attrs
      }
    },
    $listenserAll(){
      return Object.assign({},
        this.$listeners,
        {input:(event) => 
           this.$emit('input',event.target.value)
        }
      )
    }
  }
}
</script>


举例二:
三个组件:Grandfa、Father、Son
Grandfa => Son: (爷爷给孙子传值)
通过$attrs传值

// Grandfa  传了一个静态placeholder值: 请输入
<div id="app">
  {{value}}
  <wrapper v-on:focus="onFocus" v-bind:value="value" v-on:input="onFocus" placeholder="请输入">
  </wrapper>
</div>

// Father
Vue.component("Wrapper",{
   template:`
    <div>
        <son v-bind="$attrs" v-on="$listeners"></son>
    </div>
   `
});

// Son
Vue.component("son",{
   template:`
    <div>
       <button @click="handleClick">sonbutton</button>
       <input type="text" v-bind="$attrs" v-on="rewriteListener">
    </div>
   `,
});

Son => Grandfa: (孙子通知爷爷)


 computed: {
      rewriteListener() {
          const vm = this;
          return Object.assign({},
              this.$listeners,
              {
                input: (event) =>
                vm.$emit("input", event.target.value)
              }
          )
      }
  }

2、内容分发slot插槽

插槽语法是Vue实现的内容分发API,用于复合组件开发,在通用组件库开发中大量应用。
(注:Vue 2.6.0之后采用全新v-slot语法取代之前的slotslot-scope

(1)匿名插槽

// comp1
 <div>   
   <slot></slot>
 </div>

// parent
 <Comp1>hello</Comp1>

(2)具名插槽

// comp2 
<div>    
    <slot></slot>   
    <slot name="content"></slot> 
</div>
 
// parent 
<Comp2>    
    <!-- 默认插槽用default做参数 -->    
    <template v-slot:default>匿名插槽</template>    
    <!-- 具名插槽用插槽名做参数 -->    
    <template v-slot:content>具名插槽的内容...</template> 
</Comp2>

(3)作用域插槽

以上两个子组件的插槽值只能由父组件决定安排,但是我们的实际业务中,往往是儿子安排老子...这时就要用到作用域插槽。

// comp3 
 // 子组件决定值是'your name is XXX'
<div>    
    <slot :foo="your name is defaultFoo"></slot> 
    <slot name="content" :foo="your name is contentFoo"></slot> 
</div>
 
// parent 
<Comp3>    
    <!-- 把v-slot的值指定为作用域上下文对象 -->    
   <template v-slot:default="slotProps">        
      来自子组件数据:{{slotProps.foo}}   
   </template> 
  // slotProps这个命名可以随便起,或者直接解构:
   <template v-slot:content="{foo}">        
      来自子组件数据:{{foo}}   
   </template> 

  
</Comp3>

3、sync修饰符

sync修饰符添加于v2.4,类似于v-model,它能⽤于修改传递到⼦组件的属性,可以简化子组件通知父元素更新传入参数这个动作的代码逻辑。
场景:⽗组件传递的属性⼦组件想修改
所以sync修饰符的控制能⼒都在⽗级,事件名称也相对固定update:xx

// 父组件将value传给子组件并使用.sync修饰符。
<Input :value.sync="model.username">
<!-- 等效于下⾯这⾏,那么和v-model的区别只有事件名称的变化 -->
<Input :value="username" @update:value="username=$event">
<!-- 这⾥绑定属性名称更改,相应的属性名也会变化 -->
<Input :foo="username" @update:foo="username=$event">

 <!-- 绑定对象 -->
<my-com v-bind.sync="obj1"></my-com>

// 子组件触发事件:
this.$emit('update:obj1', "it is new key by my-com");
 

4、实战1:自定义表单组件

做几个自定义组件来更好的巩固知识。

index.vue:

<template>
  <div>
    <KForm :model="model" :rules="rules" ref="loginForm">
      <KFormItem label="用户名" prop="username">
        <KInput v-model="model.username"></KInput>
      </KFormItem>
      <KFormItem label="密码" prop="password">
        <KInput v-model="model.password" type="password"></KInput>
      </KFormItem>
      <KFormItem label="记住密码" prop="password">
        <KCheckBox v-model="model.remember"></KCheckBox>
        <KCheckBox :checked="model.remember" @change="model.remember = $event"></KCheckBox>
      </KFormItem>
      <KFormItem>
        <button @click="onLogin">登录</button>
      </KFormItem>
    </KForm>
    {{model}}
  </div>
</template>

<script>
import KInput from "./KInput.vue";
import KCheckBox from "./KCheckBox.vue";
import KFormItem from "./KFormItem.vue";
import KForm from "./KForm.vue";
import Notice from "../Notice";
import create from "@/utils/create";

export default {
  components: {
    KInput,
    KFormItem,
    KForm,
    KCheckBox
  },
  data() {
    return {
      model: {
        username: "tom",
        password: "",
        remember: false
      },
      rules: {
        username: [{ required: true, message: "用户名必填" }],
        password: [{ required: true, message: "密码必填" }]
      }
    };
  },
  methods: {
    onLogin() {
      // 创建弹窗实例
      let notice;
      this.$refs.loginForm.validate(isValid => {
       <!--弹窗组件在第5点讲解 -->
        notice = create(Notice, {
          title: "xxx",
          message: isValid ? "登录成功!" : "有错!!",
          duration: 3000
        });

        notice.show();
      });
    }
  }
};
</script>

KInput.vue:

  • 重点:v-bind="$attrs
    把父组件的 type="password"传了过来,但是此时会影响到div,这时就要用到 inheritAttrs,将其设为false避免顶层容器继承属性。
  • 实现:
    双向绑定::value@input
    派发校验事件
  • <template>
        <div>
            <!-- 自定义组件要实现v-model必须实现:value, @input -->
            <!-- $attrs存储的是props之外的部分:------{type:'password'} -->
            <input :value="value" @input="onInput" v-bind="$attrs">
        </div>
    </template>
    
    <script>
        export default {
            inheritAttrs: false, // 避免顶层容器继承属性
            props: {
                value: {
                    type: String,
                    default: ''
                }
            },
            methods: {
                onInput(e) {
                    // 通知父组件数值变化
                    this.$emit('input', e.target.value);
    
                    // 通知FormItem校验
                    // 此处用$parent派发事件不够健壮,因为如果在嵌套一层标签就派发不到了。
                    // 可看下elementUI form表单的源码,在下面
                
                    this.$parent.$emit('validate');
                }
            },
        }
    </script>
    
    <style lang="scss" scoped>
    
    </style>

elementUI form表单input部分源码:

// 派发,就是子组件向父组件派发事件
    dispatch (componentName, eventName, params) {
      // 获取当前组件的父组件
      var parent = this.$parent || this.$root
      // 拿到父组件名称
      var name = parent.$options.componentName
      // 通过循环的方式不断向父组件查找目标组件
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent
        if (parent) {
          name = parent.$options.componentName
        }
      }
      // 当循环结束,证明目标父组件已找到(如果存在),就通知父组件触发相应事件
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params))
      }
    },

KCheckBox.vue:

还可以通过设置model选项修改默认行为:

<template>
  <div>
    <input type="checkbox" :checked="checked" @change="onChange"/>
  </div>
</template>

<script>
export default {
  props: {
    checked: {
      type: Boolean,
      default: false
    }
  },
  model: {
    prop: "checked",
    event: "change"
  },
  methods: {
      onChange(e) {          
          this.$emit('change', e.target.checked)
      }
  },
};
</script>

KFormItem.vue:

  • 实现:
    给Input预留插槽 - slot
    能够展示label和校验信息
    能够进行校验
  • <template>
      <div>
        <label v-if="label">{{label}}</label>
        <slot></slot>
        <!-- 校验信息 -->
        <p v-if="errorMessage">{{errorMessage}}</p>
      </div>
    </template>
    
    <script>
    import Schema from "async-validator";
    
    export default {
      data() {
        return {
          errorMessage: ""
        };
      },
      inject: ["form"],
      props: {
        label: {
          type: String,
          default: ""
        },
        prop: String
      },
      mounted() {
        // 监听校验事件、并执行监听
        this.$on("validate", () => {
          this.validate();
        });
      },
      methods: {
        validate() {
          // 执行组件校验
          // 1.获取校验规则
          const rules = this.form.rules[this.prop];
    
          // 2.获取数据
          const value = this.form.model[this.prop];
    
          // 3.执行校验
          const desc = {
            [this.prop]: rules
          };
          const schema = new Schema(desc);
          //   参数1是值,参数2是校验错误对象数组
        //   返回的Promise<boolean>
          return schema.validate({ [this.prop]: value }, errors => {
            if (errors) {
              // 有错
              this.errorMessage = errors[0].message;
            } else {
              // 没错,清除错误信息
              this.errorMessage = "";
            }
          });
        }
      }
    };
    </script>
上一篇:fatal: Not a git repository (or any of the parent directories): .git


下一篇:最小生成树问题-kruskal算法