Vue 中一些常常令人忽视的重要知识点

  1. inheritAttrs
  2. .sync修饰符
  3. $attrs/$listeners
  4. provide/inject
  5. $slots & $scopedSlots
  6. Vue.delete删除数组
  7. $forceUpdate
  8. @hook:[lifecycle]

这篇文章主要是总结一下自己在使用和学习Vue的过程中容易忽视的重要知识点

inheritAttrs

inheritAttrs的作用是组件根元素上是否继承来自父组件定义给该组件的属性,这里的属性不包括已经在子组件中使用props接收到的属性和classstyle属性;这些属性可以使用$attrs获取到;

默认值是true

例如以下父子组件:
父组件 ParentComp.vue

<template>
  <div>
    <p>ParentComp</p>
    <child-comp aaa="123"/>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
}
</script>
<template>
  <div>
    <p>ChildComp</p>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  // inheritAttrs: false,
  data() {
      return {
        childData: 'child data'
      }
  },
}
</script>

父组件中,在子组件定义了一个属性aaa123inheritAttrs默认值是true,因此,在子组件的根元素div上,会跟着有aaa123的属性;

如图:

如果此时把子组件的inheritAttrs设置为false,此时子组件的根元素上就不会有该属性了;

<template>
  <div>
    <p>ChildComp</p>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  inheritAttrs: false,
  data() {
      return {
        childData: 'child data'
      }
  },
}
</script>

那我要怎么拿到这个属性呢?

可以使用Vue提供的$attrs API来获取组件的属性;

<template>
  <div>
    <p>ChildComp</p>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  inheritAttrs: false,
  data() {
      return {
        childData: 'child data'
      }
  },
  created() {
    console.log('$attrs', this.$attrs)
  }
}
</script>

有了这个api就好办了,我们可以把该属性放到自己想要放到的任意元素上,而不是默认的根元素上;

<template>
  <div>
    <p v-bind="$attrs">ChildComp</p>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  inheritAttrs: false,
  data() {
      return {
        childData: 'child data'
      }
  },
  created() {
    console.log('$attrs', this.$attrs)
  }
}
</script>

使用v-bind="$attrs",可以直接把$attrs里面的属性绑定到元素上,有多少个就绑定多少个;

最后再强调一下,这里的$attrs是排除了组件props已接收到的属性和classstyle属性的;
通过一个例子综合看下效果:

<template>
  <div>
    <p>ParentComp</p>
    <child-comp aaa="123" bbb="456" ccc="789" class="class1" style="color: red" />
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
}
</script>
<template>
  <div>
    <p v-bind="$attrs">ChildComp</p>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  inheritAttrs: false,
  props: ['ccc'],
  data() {
      return {
        childData: 'child data'
      }
  },
  created() {
    console.log('$attrs', this.$attrs)
  }
}
</script>

可以看到,class属性和style属性被设置到了根元素上,而此时ccc属性被子组件props接收了,因此只有aaabbb属性了;

.sync修饰符

.sync修饰符有什么作用呢,很简单,我们都知道,父组件向子组件传值可以使用props,子组件是不能直接修改传过来props的,此时常用的方法是子组件通过$emit一个自定义事件,父组件中监听这个时间,然后去修改props的值,从而在子组件中修改props

这样并没有任何问题,但是如果每次修改都需要在父组件中定义一个自定义事件,这样十分冗余,那有没有什么方式能够实现props的“双向绑定”呢,这就是.sync的作用,.sync实际上就是上述实现方法的语法糖,通过下面的例子我们可以看到;

通过自定义方法修改props

<template>
  <div>
    <p>ParentComp</p>
    <child-comp @change-msg="msg = $event"  :msg="msg"/>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  data() {
    return {
      msg: '我是传奇'
    }
  },
}
</script>
<template>
  <div>
    <p>ChildComp</p>
    <div>{{ msg }}</div>
    <button @click="$emit('change-msg', '我不是传奇')">change msg</button>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  props: ['msg'],
  data() {
    return {}
  }
}
</script>

修改一下自定义事件名,修改为update:msg,也是可以正常工作的。为什么要修改成这样?后面再看。

<template>
  <div>
    <p>ParentComp</p>
    <child-comp @update:msg="msg = $event" :msg="msg"/>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  data() {
    return {
      msg: '我是传奇'
    }
  },
}
</script>
<template>
  <div>
    <p>ChildComp</p>
    <div>{{ msg }}</div>
    <button @click="$emit('update:msg', '我不是传奇')">change msg</button>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  props: ['msg'],
  data() {
    return {}
  }
}
</script>

修改为.sync修饰符的方式,可以看到,我们把父组件的@update:msg="msg = $event" :msg="msg" 这直接替换成了:msg.sync="msg",仍然能正常工作,实际上.sync就是上述写法的语法糖,不用自己定义事件了,方便了许多;

<template>
  <div>
    <p>ParentComp</p>
    <child-comp :msg.sync="msg"/>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  data() {
    return {
      msg: '我是传奇'
    }
  },
}
</script>
<template>
  <div>
    <p>ChildComp</p>
    <div>{{ msg }}</div>
    <button @click="$emit('update:msg', '我不是传奇')">change msg</button>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  props: ['msg'],
  data() {
    return {}
  }
}
</script>

$attrs/$listeners

$attrs可以拿到绑定到该组件的属性,可以使用v-bind="$attrs"进行透传,类似于react{...restProps}但是注意,这里的$attrs是排除组件props已接收到的属性和classstyle属性

$listeners可以拿到绑定到该组件的绑定的事件,可以使用v-on="$listeners"进行透传;

这两个api常用于多个属性,或多个事件的父子组件(或祖孙组件)通信,例如自己封装组件库的时候,一般一个组件层级不会太深,引入vuex进行通信又不合适,使用这两个属性就非常合适了

我们先看看两个东西具体长啥样,

<template>
  <div>
    <p>ParentComp</p>
    <child-comp props1="props1" props2="props2" @event1="handleEvent1" @event2="handleEvent2"></child-comp>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  data() {
    return {}
  },
  methods: {
      handleEvent1(val){
        console.log('handleEvent1', val)
      },
      handleEvent2(val){
        console.log('handleEvent2', val)
      },
    }
}
</script>
<template>
  <div>
    <p>ChildComp</p>
  </div>
</template>

<script>

export default {
  name: 'ChildComp',
  data() {
    return {}
  },
  created() {
    console.log(this.$attrs);
    console.log(this.$listeners);
  }
}
</script>

我们可以拿到父组件传递的属性和监听的事件了,如果这些属性和事件不是给子组件用的,而是给子组件的子组件用的,此时我们就可以进行透传,将其原封不动的传递给孙组件

<template>
  <div>
    <p>ChildComp</p>
    <!-- 这里进行透传 -->
    <grand-son-comp v-bind="$attrs" v-on="$listeners"></grand-son-comp>
  </div>
</template>

<script>
import GrandSonComp from './GrandSonComp.vue'
export default {
  name: 'ChildComp',
  components: {
    GrandSonComp
  },
  data() {
    return {}
  },
  created() {
    console.log(this.$attrs);
    console.log(this.$listeners);
  }
}
</script>
<template>
  <div>
    <p>GrandSonComp</p>
    {{props1}}
    {{props2}}
    <button @click="$emit('event1', '触发事件event1')">触发事件event1</button>
    <button @click="$emit('event2', '触发事件event2')">触发事件event2</button>
  </div>
</template>

<script>

export default {
  name: 'GrandSonComp',
  props: ['props1', 'props2'],
  data() {
    return {}
  }
}
</script>

这样我们就把属性和事件透传给孙组件了,在孙组件里面进行接收和使用;

provide/inject

provide/inject提供了一个方法,使你能够跨层级进行数据传递,但是注意,传递过去的属性并不是响应式的;

provide可以是一个对象也可以是一个返回对象的函数;

后代元素可以直接inject注入数据,可以使用字符串数组接收,也可以使用对象,给定一个default,使其变成可选项(后文有例子)

<template>
  <div>
    <p>ParentComp</p>
    <button @click="changeMsg">change msg</button>
    <child-comp></child-comp>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  provide() {
    return {
      dataFromParent: 'data from parent',
      msg: this.msg,
    }
  },
  data() {
    return {
      msg: '我是传奇'
    }
  },
  methods: {
    changeMsg() {
      this.msg = '我不是传奇'
    }
  }
}
</script>
<template>
  <div>
    <p>ChildComp</p>
    <grand-son-comp></grand-son-comp>
  </div>
</template>

<script>
import GrandSonComp from './GrandSonComp.vue'
export default {
  name: 'ChildComp',
  components: {
    GrandSonComp
  },
  data() {
    return {}
  }
}
</script>
<template>
  <div>
    <p>GrandSonComp</p>
    {{ dataFromParent }}
    {{ msg }}
  </div>
</template>

<script>

export default {
  name: 'GrandSonComp',
  inject: ['dataFromParent', 'msg'],
  data() {
    return {}
  }
}
</script>

上述代码,我们在ParentCompprovide了两个数据,一个是dataFromParent,一个是msg,子组件中并没有对着两个属性进行接收传递,而在孙组件GrandSonComp中使用inject可以直接拿到那两个数据,这就是provide/inject的作用,同时,我们可以看到,虽然msg在父组件中是响应式的,但是孙组件inject进来的msg并不具备响应式能力,这是为了避免props混乱而刻意为之的,但是如果provide提供的本身是一个响应式对象,则可以进行响应式;

如果我们在父组件中直接把父组件的实例传递provide,则后代可以拿到这个父实例,并且拿到父实例绑定的data,这些data都是响应式的,例如:

<template>
  <div>
    <p>ParentComp</p>
    {{ msg }}
    <button @click="changeMsg">change msg</button>
    <child-comp></child-comp>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  provide() {
    return {
      dataFromParent: 'data from parent',
      msg: this.msg,
      // 直接把父组件实例给到provide
      parentInstance: this
    }
  },
  data() {
    return {
      msg: '我是传奇'
    }
  },
  methods: {
    changeMsg() {
      this.msg = '我不是传奇'
    }
  }
}
</script>
<template>
  <div>
    <p>ChildComp</p>
    <grand-son-comp></grand-son-comp>
  </div>
</template>

<script>
import GrandSonComp from './GrandSonComp.vue'
export default {
  name: 'ChildComp',
  components: {
    GrandSonComp
  },
  data() {
    return {}
  }
}
</script>
<template>
  <div>
    <p>GrandSonComp</p>
    {{ dataFromParent }}
    {{ msg }}
    {{ parentInstance.msg }}
    <button @click="changeMsg">change msg</button>
  </div>
</template>

<script>

export default {
  name: 'GrandSonComp',
  inject: ['dataFromParent', 'msg', 'parentInstance'],
  data() {
    return {}
  },
  methods: {
      changeMsg() {
          this.parentInstance.msg = '我也不是传奇 - from GrandSonComp'
      }
  }
}
</script>

可以看到,我们直接把父组件实例给到provide,后代组件可以直接拿到,然后我们队父组件实例的数据修改,后代组件都能响应式处理,并且,也可以在后代组件中对父组件实例进行操作,比如修改属性等,十分灵活,但是这种用法不建议在应用程序中使用,而是更多的应用于基础组件的封装;

拓展一下,inject不仅可以使用字符串数组的方式,还可以使用对象的方式,还可以修改属性名,就以上的例子,下面的写法是相同的;

由于msg已经被重命名为parentMsg了,因此模板里的msg也需要随之修改

<template>
  <div>
    <p>GrandSonComp</p>
    {{ dataFromParent }}
    {{ parentMsg }}
    {{ parentInstance.msg }}
    <button @click="changeMsg">change msg</button>
  </div>
</template>

<script>

export default {
  name: 'GrandSonComp',
  // inject: ['dataFromParent', 'msg', 'parentInstance'],
  inject: {
      dataFromParent: { default: 'default value' }, // 给定default,如果provide中没有就使用默认值
      parentMsg: {  // msg是父组件provide过来的,如果想要在后代中修改属性名,可以使用这种方式,from是provide里的key
          from: 'msg',
          default: 'default msg'
      },
      parentInstance: 'parentInstance' // 这种也可以,什么都不指定
  },
  data() {
    return {}
  },
  methods: {
      changeMsg() {
          this.parentInstance.msg = '我也不是传奇 - from GrandSonComp'
      }
  }
}
</script>

$slots & $scopedSlots

$slots只能拿到具名插槽分发的内容,不能拿到作用域插槽的内容,默认插槽实际上也是具名插槽,只不过可以简写;

$scopedSlots用来获取作用域插槽的内容;

在如下组件中,子组件ChildComp定义了一个默认插槽,一个具名插槽,一个作用域插槽,而在我们获取$slots的时候,只能获取到defaultslot1的插槽内容

<template>
  <div>
    <p>ParentComp</p>
    <child-comp>
      <template #slot1>
        slot1 content
      </template>
      <template #slot2="{compName}">
        {{compName}}
        123
      </template>
      default slot content
    </child-comp>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  data() {
    return {}
  }
}
</script>
<template>
  <div>
    <p>ChildComp</p>
    <slot></slot>
    <slot name="slot1"></slot>
    <slot name="slot2" :comp-name="compName"></slot>
  </div>
</template>

<script>
export default {
  name: 'ChildComp',
  data() {
    return {
      compName: "ChildComp"
    }
  },
  created() {
    console.log(this.$slots)
  }
}
</script>

Vue.delete删除数组

使用js 的delete删除数组的时候,只是将该索引位的值变为了empty,实际长度是没变的,而使用Vue.delete删除数组的时候,会将该索引位的值直接删除,索引会前进一位

$forceUpdate

当视图层无法进行数据更新时,用this.$forceUpdate()进行视图层重新渲染。例如我们知道,对一个数组类型的数据,直接使用索引去修改数据,数据是不会响应式变化的,此时我们就可以使用$forceUpdate强制更新组件并重新渲染

例如下面的代码,我们点击了change按钮之后,data里面的值是发生了变化的,但是页面没有相应变化,就是没有重新渲染,当我们点击forceUpdate后,页面变化了;

<template>
  <div>
    <p>ParentComp</p>
    <ul>
      <li v-for="(item, index) in list" :key="index">{{item}}</li>
    </ul>
    <button @click="change">change</button>
    <button @click="forceUpdate">forceUpdate</button>
  </div>
</template>

<script>
export default {
  name: 'ParentComp',
  data() {
    return {
      list: [1, 2, 3, 5]
    }
  },
  methods: {
    change() {
      this.list[1] = 999
    },
    forceUpdate() {
      this.$forceUpdate()
    }
  }
}
</script>

@hook:[lifecycle]

试想一下,父子组件的挂载顺序是beforeCreate-> created -> beforeMount -> mounted,那我们有没有什么方法能够在父组件中监听到子组件是否挂载了呢?通常我们的做法是在子组件的mounted钩子中$emit一个自定义事件,在父组件中监听,这样确实可以,但是如果说子组件是一个第三方的组件,我们无法直接修改子组件里的代码,此时该怎么办?

此时我们就可以使用下面介绍的方法,在Vue中,当每个生命周期执行的时候,都会emit一个自定义事件,例如在created生命周期中,会$emit('hook:created'),这是Vue内部自己做的事情,我们不需要手动做,有了这一个东西,我们就可以在引用这个组件的任意组件中进行这个自定义事件的监听,例如@hook:created

通过代码能更直观的感受到这个方式的好处

<template>
  <div>
    <p>ParentComp</p>
    <child-comp @hook:mounted="handleChildCompMounted" @hook:created="handleChildCompCreated"></child-comp>
  </div>
</template>

<script>
import ChildComp from './ChildComp.vue'
export default {
  components: {
    ChildComp
  },
  name: 'ParentComp',
  data() {
    return {}
  },
  methods: {
    handleChildCompMounted() {
      console.log('child-comp mounted')
    },
    handleChildCompCreated() {
      console.log('child-comp created')
    }
  }
}
</script>
<template>
  <div>
    <p>ChildComp</p>
  </div>
</template>

<script>
export default {
  name: 'ChildComp',
  data() {
    return {
      compName: "ChildComp"
    }
  },
  created(){
    console.log('ChildComp created --- from ChildComp')
  },
  mounted() {
    console.log('ChildComp mounted --- from ChildComp')
  }
}
</script>

查看结果:


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com