问题自检
  • 1、组件名和props用驼峰法还是中划线命名,js和html是如何对应的?
  • 2、全局注册组件和局部注册组件方式?
  • 3、props是单向数据流,如果子组件用data和计算属性处理后,props变化子组件还渲染吗?
  • 4、动态组件如何设置的?

在典型的 Vue 应用程序中,我们常使用单个文件组件而不是字符串模板。因此本节主要按照单文件形式讲解。

# 组件名规范

  • 1、全部小写
  • 2、中划线连字符

template中和js中统一用中划线方法:

<template>
  <my-component-name></my-component-name>
</template>
<script>
import MyComponentName from './components/MyComponentName.vue'
export default {
  components: {
    'my-component-name':MyComponentName
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
  • 3、使用驼峰法

template中可以用驼峰法或者中华线方法:

<template>
  <MyComponentName></MyComponentName>
  <my-component-name></my-component-name>
</template>
<script>
import MyComponentName from './components/MyComponentName.vue'
export default {
  components: {
    MyComponentName
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

综上所述,为了兼容html写法,建议统一使用中华线的方法

注意⚠️

对比组件名 、prop名、事件名: 不同于组件和 prop可以在js中写驼峰法,在html中写中划线形式,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。

# 注册组件

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。

# 全局注册组件

TIP

在入口文件中引入全局组件,在其他文件夹中开发全局组件的方式:

import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import DemoBlock from './components/demo-block';//在其他地方用单个文件组件方式定义组件

createApp(App)
  .component('demo-block', DemoBlock)
  .use(router)
  .mount('#doc');
1
2
3
4
5
6
7
8
9

# 局部注册组件

也就是常规的子组件形式,只能在引入组件的地方使用;

<template>
  <Child myProps="hello"></Child>
</template>
<script>
import Child from './components/child.vue'
export default {
  components: {
    Child
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11

# 组件传入props

  • 1、字符串形式: <blog-post title="My journey with Vue"></blog-post>
  • 2、变量形式:<blog-post :title="data.title"></blog-post>
  • 3、默认boolean为true:<blog-post is-checked></blog-post>
  • 4、传入一个对象,相当于把对象所有的属性传入 <Child v-bind="post"></Child>

对应数据 post: { id: 1, title: 'My Journey with Vue' }

相当于 <blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>

# 单向数据流

每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。不能在一个子组件内部改变 prop

1、如果prop 用来传递一个初始值,则定义一个 data 属性并将这个 prop 作为其初始值。

2、以一种原始的值传入且需要进行转换,用这个 prop 的值来定义一个计算属性。

方式一会导致,props发生变化时,子组件不会渲染。

父组件:

<template>
  <Child v-bind="post"></Child>
  <button @click="changeData">改变props数据</button>
</template>
<script>
import Child from './components/child.vue'
export default {
  components: {
    Child
  },
  data(){
    return {
      post: {
        id: 1,
        title: 'parent title'
      }
    }
  },
  methods:{
    changeData(){
      this.post = {
        id:2,
        title:'change world'
      }
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

对应子组件:

<template>
  <div>
    <div>用props处理后的data初始化:{{childId}}</div><!--不会变化-->
    <div>计算属性处理过的props:{{computedChildId}}</div><!--发生变化-->
    <div>计算属性处理过的props:{{computedChildTitle}}</div><!--发生变化-->
  </div>
</template>
<script>
export default {
  props:{
    id:Number,
    title:String
  },
  data(){
    return{
      childId:this.id
    }
  },
  computed:{
    computedChildId:function(){
      return this.id;
    },
    computedChildTitle:function(){
      return this.title+'  传入的父组件的title'
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# props 默认类型

TIP

注意那些 props 会在一个组件实例创建之前进行验证,所以实例的属性 (如 data、computed 等) 不能使用在props的 default 或 validator 函数中。

app.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function() {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function(value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    },
    // 具有默认值的函数
    propG: {
      type: Function,
      // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数
      default: function() {
        return 'Default function'
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 类型检查

String、Number、Boolean、Array、Object、Date、Function、Symbol

TIP

HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,驼峰命名法的 prop 名需要使用其等价的 短横线分隔命名命名

综合组件命名方法,建议统一按照中华线方法命名:组件和props

# 监听子组件事件

注意⚠️

对比组件名 、prop名、事件名: 不同于组件和 prop可以在js中写驼峰法,在html中写中划线形式,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。

注意向父组件传递事件名称和父组件监听名称的不同

<template>
  <Child @child-data="changeData"></Child>
</template>
<script>
import Child from './components/child.vue'
export default {
  components: {
    Child
  },
  methods:{
    changeData(e){
      console.log(e);
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

对应子组件:建议定义所有发出的事件,以便更好地记录组件应该如何工作。

<template>
  <button @click="$emit('child-data',$event)">改变props数据</button>
</template>
<script>
export default {
  emits: ['child-data']
}
</script>
1
2
3
4
5
6
7
8

# 非props的属性

TIP

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。常见的示例包括 class、style 和 id 属性。

当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中,同样的规则适用于事件监听器。

禁用 attribute 继承的常见情况是需要将 attribute 应用于根节点之外的其他元素。

通过将 inheritAttrs 选项设置为 false,你可以访问组件的 $attrs property,该 property 包括组件 props 和 emits property 中未包含的所有属性 (例如,class、style、v-on 监听器等)。

相关示例可以参考:基础知识-clas与style绑定-组件上使用

# 动态组件

<keep-alive> 会缓存动态组件,避免重复渲染。

<template>
  <!-- 失活的组件将会被缓存!-->
  <keep-alive>
    <component :is="currentTabComponent"></component>
  </keep-alive>
  <button @click="changeCom">点击切换组件</button>
</template>
<script>
import Child from './components/child.vue'
import Child2 from './components/child2.vue'
export default {
  components: {
    Child,
    Child2
  },
  data(){
    return {
      currentTabComponent:"Child"
    }
  },
  methods:{
    changeCom(){
      this.currentTabComponent = "Child2"
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 异步组件

异步组件要求使用 defineAsyncComponent 方法创建

由于 vue3 中函数式组件必须定义为纯函数,异步组件定义时有如下变化:

新的 defineAsyncComponent 助手方法,用于显式地定义异步组件

component 选项重命名为 loader

Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise

父组件 App.vue

<template>
  <div>
      <button @click="clickMe">点击我</button>
      <HelloWorld v-if="isShow"></HelloWorld>
  </div>
</template>
<script>
import { ref, defineAsyncComponent } from "vue";
export default {
  name: 'App',
  components: {
    HelloWorld: defineAsyncComponent(()=>import('./components/HelloWorld.vue')),
  },
  setup(){
    const isShow = ref(false);
    function clickMe(){
      isShow.value = true
    }
    return {
      isShow,
      clickMe
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

父组件:App.vue 在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件的定义需要通过将其包装在新的 defineAsyncComponent 助手方法中来显式地定义

TIP

注意在 3G 弱网下进行模拟

<template>
  <div>
      <button @click="clickMe">点击我</button>
      <HelloWorld v-if="isShow"></HelloWorld>
  </div>
</template>
<script>
import { ref, defineAsyncComponent } from "vue";
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 待配置的异步组件
const asyncPageWithOptions = defineAsyncComponent({
  loader:()=>import('./components/HelloWorld.vue'),
  delay: 2000, //展示加载时组件,也就是loading组件的延时时间。默认值是 200 (毫秒)
  timeout: 7000, //如果提供了超时时间且组件加载也超时了,则展示error 组件
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

export default {
  name: 'App',
  components: {
    HelloWorld: asyncPageWithOptions,
  },
  setup(){
    const isShow = ref(false);
    function clickMe(){
      isShow.value = true
    }
    return {
      isShow,
      clickMe
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

子组件 HelloWorld.vue:

<template>
  <div>
    <div class="my-content">我是子组件的内容</div>
  </div>
</template>
<script>
import {ref,onMounted,nextTick} from 'vue' 
export default {

}
</script>
1
2
3
4
5
6
7
8
9
10
11

子组件:LoadingComponent.vue

<template>
  <div>loading...</div>
</template>
<script>
export default {
    setup(props,cxt){
        console.log('我是loading组件');
    }
}
</script>
1
2
3
4
5
6
7
8
9
10

# 异步请求

在Vue3中,如果data响应式数据是通过异步请求返回的,组件标签的外部必须使用Suspense标签进行嵌套以等待异步方法的结束 => 可以进行页面的加载中的展示。同时也可以进行错误信息的页面处理。

父组件

<template>
  <div id="app">
    <div v-if="error">{{ error }}</div>
    <Suspense v-else>
      <template #default>
        <Child />
      </template>
      <template #fallback>
        Loading...
      </template>
    </Suspense>
  </div>
</template>

<script>
import Child from "./components/child.vue";
import { onErrorCaptured, ref } from "vue";
export default {
  components: {
    Child,
  },
  setup() {
    const error = ref(null);
    onErrorCaptured(e =>{
      error.value = e;
      // 不对错误进行拦截
      return true;
    });
    return { error };
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

对应子组件

<template>
  <div>
    <h1>{{ user.name }}</h1>
    <h1>{{ user.age }}</h1>
  </div>
</template>
<script>
export default {
  async setup() {
    const fetchUser = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve({ name: "Carlo", age: "20" });
        }, 3000);
      });
    };
    const user = await fetchUser();
    return { user };//响应式数据都是通过异步获取的
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 提供/注入

类似于react的context,父组件提供数据,子孙组件可以直接获取数据。

父组件

<template>
  <Child></Child>
  <button @click="changeData">改变data</button>
</template>
<script>
import Child from './components/child.vue'
export default {
  components: {
    Child
  },
  data(){
    return {
      name:'初始值'
    }
  },
  provide(){
    return {
      initname:"静态文本",//静态文本不可改变
      myName:this.name,//取值变量,无响应式
      user:()=>this.name//有响应式
    }
  },
  methods:{
    changeData(e){
      this.name = '改变后的data'
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

孙子组件:

<template>
    <div>{{initname}}:{{myName}}--{{parentUser}}</div>
</template>
<script>
export default {
    inject:['user','myName','initname'],
    computed:{
        parentUser(){
          return this.user()
        }
    }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

# ref获取元素

方式一:常规方式(类似于vue2的写法,使用this)

父组件:

<template>
  <div id="app">
    <Child ref="usernameInput"></Child>
  </div>
</template>
<script>
import Child from "./components/child.vue";
export default {
  components:{
    Child
  },
  mounted() {
    console.log(this.$refs.usernameInput.childData);//获取子组件中的属性
    this.$refs.usernameInput.focusInput()//执行子组件中的方法
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

对应子组件

<template>
  <div>
    <input type="text" name="myinput"/>
  </div>
</template>

<script>
export default {
  data(){
    return {
      childData:123
    }
  },
  methods: {
    focusInput() {
      console.log(11);
    }
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

方式二:在组合式 API 中使用 template refs

<template>
  <input type="text" ref="myInput" data-value="input-number"/>
</template>
<script>
import {ref, onMounted} from 'vue'
export default {
  setup(){
    const myInput = ref(null);
    onMounted(()=>{
      console.log(myInput.value);
      myInput.value.focus();
    })
    return {
      myInput
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

对于数组循环的ref,需要手动创建数组

<template>
  <ul>
    <li 
      v-for="item in list" 
      :ref="setItemRef" 
      :key="item" 
      :class="item===1?'list-li':''"
    >{{item}}</li>
  </ul>
</template>
<script>
import {ref,onMounted} from 'vue'
export default {
  setup(){
    const list = ref([1,2,3,4]);
    let itemRefs = ref([])
    const setItemRef = el => {
      itemRefs.value.push(el)
    }
    onMounted(() => {
      console.log(itemRefs.value)
    })
    return {
      list,
      setItemRef,
    }
  }
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29