一、Vue组件化 ---kkb

组件通信

1、父组件 => 子组件

  • 属性props
// child
props: { msg: String }
// parent
<HelloWorld msg="Welcome to Your Vue.js App"/>
  • 引用refs  ---  特别适用于想要操作某些DOM结构时,refs就是这些DOM节点
// parent
<HelloWorld ref="hw"/>
this.$refs.hw.xx = 'xxx'

this.$refs 需要在mounted 中 或之后调用,因为父组件先于子组件挂载,在父组件 created 的时候,子组件还没挂上去,所以访问不到

  • 子组件chidren
// parent
this.$children[0].xx = 'xxx'

子元素不保证顺序,需要小心使用

2、子组件 => 父组件  自定义事件

// child
this.$emit('add', good)
// parent
<Cart @add="cartAdd($event)"></Cart>

3、兄弟组件:通过共同祖辈组件

通过共同的祖辈组件搭桥,$parent  或  $root

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

4、祖先和后代之间

由于嵌套层数过多,传递props不切实际,vue提供了 provide/indect API完成该任务

  • provide / inject:能够实现祖先给后代传值
// ancestor
provide() {
  return {foo: 'foo'}
}
// descendant
inject: ['foo']

注意:provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序中。我们更多会在开源组件库中见到。

但是,反过来想要后代给组件传值,这种方案就不行了,子组件如果想改的话,需要组件在provide中注入 setFoo 的函数,子组件通过setFoo来修改 foo 的值

provide的时候,完全可以把当前组件(祖先组件)注入,直接传入 this 就行了

5、任意两个组件之间:事件总线 或 vuex

  • 事件总线:创建一个 Bus 类负责事件派发、监听 和 回调管理
// Bus:事件派发、监听和回调管理
class Bus {
  constructor() {
    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();

// 使用
// child1
this.$bus.$on("foo", handle);
// child2
this.$bus.$emit("foo");

简单点的写法: Vue.prototype.$bus = new Vue()

这里的 new Vue() 是一个新的、干净的vue实例,只在做监听、派发消息的事情。因为Vue已经实现上述 Bus 的接口了

  • vuex:创建唯一的全局数据管理者 store,通过它管理数据并通知组件状态变更

插槽

插槽语法是Vue实现的内容分发API,用于复合组件开发。该技术在通用组件库开发中有大量应用。

Vue 2.6.0 之后采用全新的 v-slot 语法取代之前的 slot、slot-scope

匿名插槽

// child
<div>
  <slot></slot>
</div>

// parent
<comp>hello</comp>

具名插槽

// child
<div>
  <slot></slot>
  <slot name="content"></slot>
</div>

// parent
<Comp2>
  // 默认插槽用default做参数
  <template v-slot:default>具名插槽</template>
  // 具名插槽用插槽名做参数 
  <template v-slot:content>内容...</template>
</Comp2>

作用域插槽

// comp3
<div>
  <slot :foo="foo"></slot>
</div>

// parent
<Comp3>
  // 把v-slot的值指定为作用域上下文对象
  <template v-slot:default="ctx">
    来自子组件的数据:{{ctx.foo}}
  </template>
</Comp3>    

相当于子组件存有一个值,父组件可以从 v-slot 获取后,进行相应的操作

另一种写法--对象的解构

// comp3
<div>
  <slot :foo="foo"></slot>
</div>

// parent
<Comp3>
  // 把v-slot的值指定为作用域上下文对象
  <template v-slot:default="{foo}">
    来自子组件的数据:{{foo}}
  </template>
</Comp3>  

 组件化实战

 实现 Form、FormItem、Input

  •  Input
    • 双向绑定:@input、:value
    • 派发校验事件
<template>
  <div>
    <!-- 自定义组件要实现 v-model 必须实现 :value @inpit -->
    <!-- $attrs存储的是props之外的部分,这里用v-bind把这些东西展开 -->
    <input :value="value" @input="onInput" v-bind="$attrs"/>
  </div>
</template>
<script>
export default {
  inheritAttrs: false, // 避免顶层容器继承属性---<div>上面就不会有 type=password 属性了
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  methods: {
    onInput(e) {
      // 通知父组件数值变化
      this.$emit('input', e.target.value)

// 通知FormItem校验
this.$parent.$emit('validate'); } }, } </script>
  • 实现KFormItem
    • 任务1:给Inoput预留插槽 - slot
    • 任务2:能够展示label 和校验信息
    • 任务3:能够进行校验

数据校验,思路:校验发生在FormItem,它需要知道何时校验(让Input通知它),还需要知道怎么校验(注入校验规则)

任务1:Input通知校验

onInput(e){
  // $parent 指 FormItem
  this.$parent.$emit('validate')
}

任务2:FormItem 监听校验通知,获取规则并执行校验

安装校验库:npm i async-validator -S      

<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 {
  inject: ['form'],
  props:{
    label: {
      type: String,
      default: ''
    },
    prop: String
  },
  data(){
    return{
      errorMessage: ''
    }
  },
  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 = ''
        }
      })
    }
  },
  mounted() {
    // 监听校验事件,并执行监听
    this.$on('validate', () => {
      this.validate()
    })
  }
}
</script>
  • Form
    • 给FormItem留插槽
    • 设置数据和校验规则
    • 全局校验
template>
  <div>
    <slot></slot>
  </div>
</template>
<script>
export default {
  provide() {
    return {
      form: this // 传递Form实例给后代,比如FormItem用来校验
    }
  },
  props: {
    model: {
      type: Object,
      required: true
    },
    rules: Object
  },
  methods: {
    validate(cb) {
      // map 的结果是若干 Promise数组
      const tasks = this.$children
        .filter(item => item.prop)
        .map(item => item.validate())
      // 所有任务必须全部成功才算校验成功
      Promise.all(tasks).then(() => {
        cb(true)
      }).catch(() => {
        cb(false)
      })
    }
  },
}
</script>

使用如下: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>
        <button @click="onLogin">登录</button>
      </KFormItem>
    </KForm>
    {{model}}
  </div>
</template>
<script>
import KInput from './KInput'
import KFormItem from './KFormItem'
import KForm from './KForm'
export default {
  data() {
    return {
      model: {
        username: 'tom',
        password: ''
      },
      rules: {
        username: [{required: true, message: '用户名必填'}],
        password: [{required: true, message: '密码必填'}]
      }
    }
  },
  methods: {
    onLogin() {
      this.$refs.loginForm.validate((isValid) => {
        if(isValid) {
          alert('登录!!!')
        }else{
          alert('有错!!!')
        }
      })
    }
  },
  components: {
    KInput,
    KFormItem,
    KForm
  }
}
</script>

inheritAttrs   2.4.0 新增

类型:boolean   默认:true

  默认情况下父作用域的不被认作props的特性绑定将会“回退”,且作为普通的 HTML 特性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 为 false,这些默认行为将会被丢掉。而通过实例属性 $arrts 可以让这些特性生效,且可以通过 v-bind 显式绑定到非根元素上。

注意:这个选项不影响 class 和 style 绑定

组件由上至下广播

可以解决 组件之间通信 父组件向子组件派发事件时,如果父子之间还有其他组件导致无法通信的问题 :$children  的派发事件

function boardcast(eventName, data) {
  this.$children.forEach(child => {
    // 子元素触发$emit
    child.$emit(eventName, data)
    if(child.$children.length) {
      // 递归调用,通过call修改this指向 child
      boardcast.call(child, eventName, data)
    }
  })
}

Vue.prototype.$boardcast = function(eventName, data) {
  boardcast.call(this, eventName, data)
}

elementUI的做法

// 事件广播
function broadcast(componentName, eventName, params){
  // 遍历所有的children
  this.$children.forEach(child => {
    var name = child.$options.componentName;

    if(name === componentName){
      // 如果找到了 componentName 组件,则让child自己给自己派发事件
      // 然后子组件里面就可以自己监听这个事件了
      child.$emit.apply(child, [eventName].concat(params))
    }else{
      // 如果没找到,接着找,继续调用broadcast给子组件
      broadcast.apply(child, [componentName, eventName].concat([params]))
    }
  })
}

由下而上广播

Vue.prototype.$dispatch = function(eventName, data) {
  let parent = this.$parent
  // 查找父元素
  while(parent){
    // 父元素用$emit触发
    parent.$emit(eventName, data)
    // 递归查找父元素
    parent = parent.$parent
  }
}

elementUI的做法

// 自下而上的事件派发
dispatch(componentName, eventName, params) {
  var parent = this.$parent || this.$root
  var name = parent.$options.componentName

  // 当parent存在,并且 name存在且name不等于componentName时进行循环
  while(parent && (!name || name !== componentName)){
    parent = parent.$parent
    if(parent){
      name = parent.$options.componentName
    }
  }
  if(parent){
    parent.$emit.apply(parent, [eventName].concat(params))
  }
}
broadcast(conponentName, eventName, params) {
  broadcast.call(this, componentName, eventName, params)
}

v-model 和 .sync 的异同

// v-model 是语法糖
<Input v-model="username">

// 默认等效于下面这行
<Input :value="username" @input="username=$event">

// 但是你也可以通过设置model选项修改默认行为, Checkbox.vue
{
  model: {
    prop: 'checked',
    event: 'change'
  }
}

// 上面这样设置会导致上级使用 v-model 时行为变化,相当于
<Checkbox :checked="model.remember" @change="model.remember = $event"></Checkbox>

场景:v-model 通常用于表单控件,它有默认行为,同时属性名和事件名均可在子组件定义

// sync 修饰符添加于 v2.4,类似于 v-model,它能用于修改传递到子组件的属性,如果像下面这样写
<Input :value.sync="username">

// 等效于下面这行,那么和v-model的区别只有事件名称的变化
<Input :value="username" @update:value="username=$event">

// 这里绑定属性名称更改,相应的属性名也会变化
<Input :foo="username" @update:foo="username=$event">

场景:父组件传递的属性子组件想修改

所以sync修饰符的控制能力都在父级,事件名称也相对固定 update:xx

习惯上表单元素用 v-model

实现弹窗组件

弹窗这类组件的特点是它们在当前vue实例之外独立存在,通常挂载于body;它们是通过JS动态创建的,不需要在任何组件中声明,常见使用姿势:

this.$create(Notice, {
  title: 'xx',
  message: 'xx',
  duration: 1000
}).show()

create

create函数用于动态创建指定组件实例并挂载至body

// 创建指定组件实例,并挂载到body上
import Vue from 'vue';
export default function create(Component, props) {
  // 0、先创建vue实例,这里先不挂载  $mount()
  const vm = new Vue({
    render(h) {
      // render方法提供给我们一个h函数(createElement函数的别名),它可以渲染VNode
      return h(Component, {props})
    }
  }).$mount()

  // 1、上面的vm帮我们创建组件实例
  // 2、通过$children获取该组件实例
  // console.log(vm.$root)
  const comp = vm.$children[0]

  // 3、追加至body
  document.body.appendChild(vm.$el)

  // 4、清理函数
  comp.remove = () => {
    document.body.removeChild(vm.$el)
    vm.$destroy()
  }

  // 5、返回组件实例
  return comp
}

创建通知组件 Notice.vue

<template>
  <div v-if="isShow">
    <h3>{{title}}</h3>
    <p>{{message}}</p>
  </div>
</template>
<script>
export default {
  props: {
    title:{
      type: String,
      default: ''
    },
    message:{
      type: String,
      default: ''
    },
    duration: {
      type: Number,
      default: 1000
    }
  },
  data() {
    return {
      isShow: false
    }
  },
  methods: {
    show(){
      this.isShow = true
      setTimeout(() => {
        this.hide()
      }, this.duration)
    },
    hide(){
      this.isShow = false
      this.remove()
    }
  },
}
</script>

使用方法:

const notice = create(Notice, {
  title: 'xxx',
  message: 'blabla~~',
  duration: 1000
})
notice.show()
原文地址:https://www.cnblogs.com/haishen/p/11742002.html