关于Vue中的组件知识


更新记录

  • 2023-09-14:对于“Mixins”作了补充。

非单文件组件

创建方法

  1. 定义组件

    使用 Vue.extend(options) 创建,其中 optionsnew Vue(options) 时传入的options几乎一样,但也有区别:

    • el不要写,因为最终所有的组件都要经过一个vm的管理,由vm中的el才决定服务哪个容器。

    • data必须写成函数,避免组件被复用时,数组存在单引用关系。

      直接将data写成对象,而不是一个函数,那么每个组件的data属性会共享同一个对象实例。这意味着当一个组件的data属性被修改时,其他组件的data属性也会受到影响,导致数据混乱和不可预料的行为。

      为了避免这个问题,Vue要求将data属性写成一个函数。这样一来,每个组件实例都会调用该函数来返回一个新的数据对象。这样每个组件都会独立拥有自己的数据对象,互不干扰。

  2. 注册组件

    • 局部注册:new Vue() 的时候 options 传入 components 选项。
    • 全局注册:Vue.component('组件名', 组件)
  3. 使用组件

    //定义school组件
    		const school = Vue.extend({
    			name:'school',
    			template:`
    				<div>
    					<h2>学校名称:{{name}}</h2>	
    					<h2>学校地址:{{address}}</h2>	
    					<button @click="showName">点我提示学校名</button>
    				</div>
    			`,
    			data(){
    				return {
    					name:'尚硅谷',
    					address:'北京'
    				}
    			},
    			methods: {
    				showName(){
    					console.log('showName',this)
    				}
    			},
    		})

VueComponent与Vue的关系

image.png

  1. 一个重要的内置关系:VueComponent.prototype. _proto === Vue.prototype
  2. 为什么要有这个关系:让组件实例对象VueComponent可以访问到Vue原型上的属性、方法。

创建组件的注意事项:

组件名:

  • 一个单词组成
    • 第一种写法(首字母小写):school
    • 第二种写法(首字母大写):School(只能在
  • 多个单词组成
    • 第一种写法(kebab-case命名):my-school
    • 第二种写法(CameCase命名):MySchool(需要Vue脚手架支持)
  • 备注
    • 可以使用name配置项指定组件在开发者工具中呈现的名字

组件标签:

  • 第一种写法:<school></school>
  • 第二种写法:<school/>(需要Vue脚手架支持)

注意:

在HTML模板中使用组件时,首字母大写的形式只能在template配置项中的HTML代码中使用,或者在单文件组件的HTML中使用。

如果在像index.html这种纯html文件中使用组件时,必须使用小写单词+短横线的形式。

定义名字的时候尽量使用两个或以上的单词,避免和原生的HTML标签名冲突。

组件数据的流向设计:属性向下,事件向上,即父组件只能向子组件传递数据,子组件不能修改父组件的属性,父组件也不能访问子组件中的属性,子组件可通过事件触发父组件中的事件。

props:

组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。也就是props子组件访问父组件数据的唯一接口

  • 单向数据流
  • 父组件属性变化,子组件自动刷新
  • 子组件不能直接修改父组件属性

你可以基于对象的语法使用以下选项:

  • type: 可以是下列原生构造函数中的一种:StringNumberBooleanArrayObjectDateFunctionSymbol、任何自定义构造函数、或上述内容组成的数组。会检查一个 prop 是否是给定的类型,否则抛出警告。Prop 类型的更多信息在此
  • default: any
    为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回。
  • required: Boolean
    定义该 prop 是否是必填项。在非生产环境中,如果这个值为 truthy 且该 prop 没有被传入的,则一个控制台警告将会被抛出。
  • validator: Function
    自定义验证函数会将该 prop 的值作为唯一的参数代入。在非生产环境下,如果该函数返回一个 falsy 的值 (也就是验证失败),一个控制台警告将会被抛出。你可以在这里查阅更多 prop 验证的相关信息。

示例:

props: ["sender", "msg", "time"]
props:{
     isStudent:{
         type:Boolean,
         default:true // 可声明默认值
     }
   }
// 不同类型的数据,注意默认值的返回类型
props: {
    user: {
      type: Object,
      default() {
        return { username: "abc" };
      },
    },
    followers: {
      type: Array,
      default() {
        return []; // 即使是空数组,也需要从函数中返回
      },
    },
    handleClick: {
      type: Function,
      default() {
        console.log("点击事件");
      },
    },
  },
props: {
   time: {
     type: Number,
     // 可以在props中校验数据
     validator(value) {
       return value > 0;
     },
   },
   sender: {
     type: String,
   },
   msg: {
     type: String,
   },
 },

在父组件中,首先需要引入组件:

<script>
import MessageItem from "./components/MessageItem.vue"
export default {
  components:{
    MessageItem
  }
}
</script>

向子组件传值:

 <!-- 使用v-bind才可以传送数据本身的属性值 -->
<MessageItem  :user={haha:222} :time=2022 sender="q"></MessageItem>

emit:

子组件可以使用 $emit 触发父组件的自定义事件

Vue虽然也支持通过props向父组件传递数据,但如此做子组件就会依赖父组件,即如果父组件没有传递参数,且子组件的事件处理函数也没有默认值,那么子组件调用事件处理函数就会报出异常;而使用emit事件传递则没有这个问题,如果父组件不处理子组件的事件也没有任何问题,因为子组件不依赖父组件的事件监听。

  • 参数

    • {string} eventName
    • [...args]

    触发当前实例上的事件。附加参数都会传给监听器回调。

  • 示例

    在子组件中,定义emits数组以及触发事件,可添加多个参数:

    <script>
    export default {
      emits: ["deletePosts"]
    };
    </script>
    <button @click="$emit('deletePosts', 5)"></button>

    也可自定义方法,例如:

    <button @click="selectPosts(5)"></button>
    <script>
    export default {
      methods:{
        selectPosts(id){
          this.$emit("deletePosts",id); // selectPosts事件触发后,自动触发deletePosts事件
        }
      }
    }
    </script>

    在父组件中,首先需要引入组件:

    <script>
    import MessageItem from "./components/MessageItem.vue"
    export default {
      components:{
        MessageItem
      }
    }
    </script>

    父组件触发方法:

    <MessageItem  @deletePosts="handleDeletePosts"></MessageItem>
    <script>
    import MessageItem from "./components/MessageItem.vue"
    export default {
      components:{
        MessageItem
      },
      methods:{
        handleDeletePosts(id){
          console.log(id);
        }
      }
    }
    </script>

provide和inject:

在实际开发中,可能需要传递数据到更深层次的组件,例如子组件的子组件,即使中间的组件没有用到这个属性也要层层传递。

Vue中提供了provide配置项来向所有下层组件传递数据,而用到数据的组件可以使用inject配置项来获取数据:

  • 类型
    • provideObject | () => Object
    • injectArray<string> | { [key: string]: string | Symbol | Object }

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 SymbolReflect.ownKeys 的环境下可工作。

inject 选项应该是:

  • 一个字符串数组,或
  • 一个对象,对象的 key 是本地的绑定名,value 是:
    • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
    • 一个对象,该对象的:
      • from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
      • default 属性是降级情况下使用的 value

注意:provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

  • 示例:

    MovieCard.vue文件:

    <template>
      <div class="card">
        <MovieItem
          :title="movie.title"
          :description="movie.description"
        />
      </div>
    </template>
    <script>
    import MovieItem from "./MovieItem.vue";
    export default {
      components: {
        MovieItem,
      },
      data() {
        return {
          movie: {
            title: "电影",
            description: "这是一段电影的描述",
          },
        };
      },
    };
    </script>
    <style scoped>
    .card {
      padding: 12px;
      border: 1px solid hsl(240deg, 100%, 80%);
      border-radius: 4px;
    }
    </style>
    

    MovieItem.vue文件:

    <template>
      <MovieTitle :title="title" />
      <div class="description">
        {{ description }}
      </div>
    </template>
    <script>
    import MovieTitle from "./MovieTitle.vue";
    export default {
      components: { MovieTitle },
      props: ["title", "description"],
    };
    </script>
    <style scoped>
    .description {
      margin-top: 12px;
      color: hsl(240deg, 10%, 88%);
    }
    </style>
    

    MovieTitle.vue文件:

    <template>
      <h2>{{ title }}</h2>
    </template>
    <script>
    export default {
      props: ["title"],
    };
    </script>
    <style scoped>
    h2 {
      color: hsl(240deg, 100%, 80%);
    }
    </style>
    

    可以看出组件的层级关系,中间组件尽管没有使用到title属性,但仍需要将其加入到props配置项中,向下传递到MovieTitle.vue文件中。若使用provideinject配置项,则简洁很多:

    <template>
      <div class="card">
        <!-- <MovieItem :title="movie.title" :description="movie.description" /> -->
        <MovieItem :description="movie.description" />
      </div>
    </template>
    <script>
    import MovieItem from "./MovieItem.vue";
    export default {
      components: {
        MovieItem,
      },
      data() {
        return {
          movie: {
            title: "电影",
            description: "这是一段电影的描述",
          },
        };
      },
      provide: {
        title: "测试电影",
      },
    };
    </script>
    <style scoped>
    .card {
      padding: 12px;
      border: 1px solid hsl(240deg, 100%, 80%);
      border-radius: 4px;
    }
    </style>
    
    <template>
      <!-- <MovieTitle :title="title" /> -->
      <MovieTitle />
      <div class="description">
        {{ description }}
      </div>
    </template>
    <script>
    import MovieTitle from "./MovieTitle.vue";
    export default {
      components: { MovieTitle },
      // props: ["title", "description"],
      props: ["description"],
    };
    </script>
    <style scoped>
    .description {
      margin-top: 12px;
      color: hsl(240deg, 10%, 88%);
    }
    </style>
    
    <template>
      <h2>{{ title }}</h2>
    </template>
    <script>
    export default {
      // props: ["title"],
      inject: ["title"],
    };
    </script>
    <style scoped>
    h2 {
      color: hsl(240deg, 100%, 80%);
    }
    </style>

    但如此做provide中的title属性没有与data中的title属性相对应,若修改为以下代码:

    provide: {
     title: this.movie.title
    },

    则会报错:

    image-20220819210345147

    原因是如果需要provide提供data中的属性,则需要将provide写为一个函数,类似data中的形式:

    provide() {
        return {
          title: this.movie.title,
        };
      }

如何修改子组件中的样式:

对于子组件中的根元素,可通过普通CSS样式书写格式进行修改,若想为子组件中除根元素以外的元素,则需要使用deep选择器:

.text :deep(a) {
  display: block;
  text-decoration: none;
  color: hsl(0deg, 80%, 70%);
}

ref:

Vue提供了一种机制,可以获取到组件的实例并访问其中的属性,即为ref。使用ref可以访问到原生的HTML的DOM实例,也可以获取Vue组件的实例,但会破坏数据的流向,所以万不得已的时候最好不要使用。

在子组件中:

<template>
  <input type="text" v-model="inputText" ref="inputControl" />
</template>

<script>
export default {
  data() {
    return {
      inputText: "",
    };
  },
  mounted() {
    this.$refs.inputControl.focus();
  },
  methods: {
    blur() {
      this.$refs.inputControl.blur();
    },
  },
};
</script>

<style scoped>
input {
  padding: 8px 14px;
  border: 1px solid hsl(280deg, 50%, 50%);
  border-radius: 4px;
  outline: none;
  background: hsl(280deg, 50%, 30%, 0.2);
  color: white;
}
</style>

在父组件中:

<template>
  <main>
    <div>
      <AutoFocus ref="autofocus" />
    </div>
  </main>
</template>

<script>
import AutoFocus from "./components/AutoFocus.vue";
export default {
  components: {
    AutoFocus,
  },
  mounted() {
    setTimeout(() => {
      console.log(this.$refs.autofocus.inputText);
      this.$refs.autofocus.blur();
    }, 5000);
  },
};
</script>

<style>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}

body {
  background-color: #0f141c;
  opacity: 1;
  background-image: radial-gradient(
    #212943 0.6000000000000001px,
    #0f141c 0.6000000000000001px
  );
  background-size: 12px 12px;
  color: white;
}

#app {
  width: 100vw;
  height: 100vh;
  max-width: 100%;
  display: grid;
  place-items: center;
}
</style>

让组件支持v-model指令:

示例:

子组件SearchInput.vue文件:

<template>
  <label>
    <span>搜索:</span>
    <input type="text" />
  </label>
</template>
<script>
export default {};
</script>
<style scoped>
input {
  padding: 8px 14px;
  border: 1px solid hsl(280deg, 50%, 50%);
  border-radius: 4px;
  outline: none;
  background: hsl(280deg, 50%, 30%, 0.2);
  color: white;
}
</style>

父组件App.vue文件:

<template>
  <main>
    <div>
      <SearchInput v-model="searchTerm"/>
        <p>{{searchTerm}}</p>
    </div>
  </main>
</template>

<script>
import SearchInput from "./components/SearchInput.vue";

export default {
  components: {
    SearchInput,
  },
  data(){
  	return{
  		SearchTerm: ""
  	}
  }
};
</script>

<style>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}

body {
  background-color: #0f141c;
  opacity: 1;
  background-image: radial-gradient(
    #212943 0.6000000000000001px,
    #0f141c 0.6000000000000001px
  );
  background-size: 12px 12px;
  color: white;
}

#app {
  width: 100vw;
  height: 100vh;
  max-width: 100%;
  display: grid;
  place-items: center;
}

p {
  margin-top: 12px;
}
</style>

此时v-model并不能生效,需要在子组件中添加属性:

<template>
  <label
    ><span>搜索:</span
    ><input
      type="text"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    />
  </label>
</template>
<script>
export default {
  props: ["modelValue"],
  emits: ["update:modelValue"],
};
</script>
<style scoped>
input {
  padding: 8px 14px;
  border: 1px solid hsl(280deg, 50%, 50%);
  border-radius: 4px;
  outline: none;
  background: hsl(280deg, 50%, 30%, 0.2);
  color: white;
}
</style>

若需要多个v-model,添加更多属性,并加以区分:

<template>
  <label
    ><span>搜索:</span
    ><input
      type="text"
      :value="searchTerm"
      @input="$emit('update:searchTerm', $event.target.value)"
    />
  </label>
  <label>
    <span>类别:</span>
    <select
      :value="category"
      @change="$emit('update:category', $event.target.value)"
    >
      <option value="default">默认</option>
      <option value="fontend">前端</option>
      <option value="backend">后端</option>
      <option value="fullstack">全栈</option>
    </select>
  </label>
</template>
<script>
export default {
  props: ["searchTerm", "category"],
  emits: ["update:searchTerm", "update:category"],
};
</script>
<style scoped>
label {
  display: block;
  margin-bottom: 20px;
}

input {
  padding: 8px 14px;
  border: 1px solid hsl(280deg, 50%, 50%);
  border-radius: 4px;
  outline: none;
  background: hsl(280deg, 50%, 30%, 0.2);
  color: white;
}

select {
  border: 1px solid hsl(280deg, 50%, 50%);
  background: none;
  color: white;
  padding: 6px;
  border-radius: 4px;
}
</style>
<template>
  <main>
    <div>
      <!-- v-model 后面的参数必须和子组件接收的属性名相同,例如 searchTerm -->
      <SearchInput
        v-model:searchTerm="searchTerm"
        v-model:category="category"
      />
      <div class="splitLine"></div>
      <p>搜索词:{{ searchTerm }}</p>
      <p>类别:{{ category }}</p>
    </div>
  </main>
</template>

<script>
import SearchInput from "./components/SearchInput.vue";

export default {
  components: {
    SearchInput,
  },
  data() {
    return {
      // 名字无需和 SearchInput 中的属性名相同
      // 例如这里可以叫 searchQuery,
      searchTerm: "",
      category: "default",
    };
  },
};
</script>

<style>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}

body {
  background-color: #0f141c;
  opacity: 1;
  background-image: radial-gradient(
    #212943 0.6000000000000001px,
    #0f141c 0.6000000000000001px
  );
  background-size: 12px 12px;
  color: white;
}

#app {
  width: 100vw;
  height: 100vh;
  max-width: 100%;
  display: grid;
  place-items: center;
}

.splitLine {
  width: 100%;
  height: 1px;
  border-bottom: 1px dashed hsl(280deg, 50%, 50%);
}

p {
  margin-top: 12px;
  color: hsl(50deg, 100%, 80%);
}
</style>

使用动态组件渲染不同的HTML标签:

使用:is+HTML标签(可能需要用到计算属性)

父组件App.vue文件:

<template>
  <main>
    <div>
      <!-- 动态 HTML 元素  -->
      <TextHeading level="1">一级标题</TextHeading>
      <TextHeading level="2">二级标题</TextHeading>
      <TextHeading level="3">三级标题</TextHeading>
      <TextHeading level="4">四级标题</TextHeading>
      <TextHeading level="5">五级标题</TextHeading>
      <TextHeading level="6">六级标题</TextHeading>
    </div>
  </main>
</template>

<script>
import TextHeading from "./components/TextHeading.vue";
export default {
  components: {
    TextHeading,
  },
};
</script>

子组件TextHeading.vue文件:

<template>
  <Component :is="heading"><slot></slot></Component>
</template>

<script>
export default {
  props: ["level"],
  computed: {
    heading() {
      return `h${this.level}`;
    },
  },
};
</script>

<style scoped>
h1 {
  font-size: 3em;
}
h2 {
  font-size: 2.4em;
}
h3 {
  font-size: 1.8em;
}
h4 {
  font-size: 1.4em;
}
h5 {
  font-size: 1.2em;
}
h6 {
  font-size: 1em;
}
</style>

使用动态组件渲染不同的Vue组件:

使用:is+组件名称

例如此功能实现两个表单组件的切换:

登录表单文件ProfileForm.vue:

<template>
  <form @submit.prevent>
    <label>昵称:<input type="text" /></label>
    <label>生日:<input type="date" /></label>
    <label>地址:<input type="text" /></label>
  </form>
</template>
<script>
export default {};
</script>
<style scoped>
::-webkit-calendar-picker-indicator {
  filter: invert(1);
}
</style>

注册表单文件RegisterForm.vue:

<template>
  <form @submit.prevent>
    <label>手机号:<input type="number" /></label>
    <label
      >验证码:<input type="number" /><button class="sendSMSCodeBtn">
        发送验证码
      </button></label
    >
  </form>
</template>
<script>
export default {};
</script>
<style scoped>
.sendSMSCodeBtn {
  margin-left: 24px;
}
</style>

父组件App.vue文件:

<template>
  <main>
    <div>
      <Component :is="currentForm" />
      <div class="buttons">
        <button
          v-if="currentForm === 'RegisterForm'"
          @click="currentForm = 'ProfileForm'"
        >
          下一步
        </button>
        <template v-else-if="currentForm === 'ProfileForm'">
          <button @click="currentForm = 'RegisterForm'">
            上一步
          </button>
          <button>完成</button>
        </template>
      </div>
    </div>
  </main>
</template>

<script>
import RegisterForm from "./components/RegisterForm.vue";
import ProfileForm from "./components/ProfileForm.vue";
export default {
  components: {
    RegisterForm,
    ProfileForm,
  },
  data() {
    return {
      currentForm: "RegisterForm",
    };
  },
};
</script>

注意,如果仅使用这种方法切换表单,会导致表单数据缺失,原因是因为使用动态组件每次切换组件时,都会创建新的组件实例。

改进:

Component标签外绑定KeepAlive标签

<!-- 动态组件默认每次渲染都会重新创建,数据会丢失,使用 <KeepAlive></KeepAlive> 组件可以缓存组件,避免数据丢失 -->
     <KeepAlive>
       <Component :is="currentForm" />
     </KeepAlive>

组件传送

在其他DOM元素挂载组件:

某些组件可能在逻辑上不属于任何父组件,例如页面边缘的提示框,它们会根据body元素进行绝对定位。如果我们将它们放在某个父组件中,那么它们的位置就会限制于父组件容器内。

Vue提供了teleport属性,可以让子组件挂载在其他页面元素上。

<template>
  <Teleport to="body">
    <div v-if="show" class="alertBox">
      <div class="closeIcon" @click="show = false">X</div>
      <div class="content">
        <slot>消息提示框组件</slot>
      </div>
    </div>
  </Teleport>
</template>

多次传送组件:

如果一个组件多次传送到相同的页面下,那么传送的顺序会追加到页面中,例如要弹出多个消息提示框:

在index.html文件中添加:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
      <!-- 添加messages -->
    <div id="messages"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

在父组件App.vue文件中:

<template>
  <main>
    <div class="container">
      <button @click="msgs.push(`这是一段消息${msgs.length + 1}`)">
        添加消息
      </button>
      <AlertBox v-for="msg in msgs">{{ msg }}</AlertBox>
    </div>
  </main>
</template>

<script>
import AlertBox from "./components/AlertBox.vue";
export default {
  components: {
    AlertBox,
  },
  data() {
    return {
      msgs: [],
    };
  },
};
</script>

<style>
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: Arial, "PingFang SC", "Microsoft Yahei", sans-serif;
}

body {
  background-color: #0f141c;
  opacity: 1;
  background-image: radial-gradient(
    #212943 0.6000000000000001px,
    #0f141c 0.6000000000000001px
  );
  background-size: 12px 12px;
  color: white;
}

#app {
  width: 100vw;
  height: 100vh;
  max-width: 100%;
  display: grid;
  place-items: center;
}

button {
  border: none;
  background: linear-gradient(
    90deg,
    hsl(240deg, 50%, 50%),
    hsl(280deg, 50%, 50%)
  );
  padding: 12px 18px;
  margin-top: 12px;
  border-radius: 4px;
  color: white;
}

.container {
  position: relative;
  /* border: 1px solid hsl(280deg, 100%, 50%); */
}
/*注意此处布局*/
#messages {
  position: absolute;
  right: 12px;
  bottom: 12px;
  display: flex;
  flex-direction: column-reverse;
  gap: 12px;
}
</style>
<template>
   <!-- 此处改为#messages -->
  <Teleport to="#messages">
    <div v-if="show" class="alertBox">
      <div class="closeIcon" @click="show = false">X</div>
      <div class="content">
        <slot>消息提示框组件</slot>
      </div>
    </div>
  </Teleport>
</template>
<script>
export default {
  data() {
    return {
      show: true,
    };
  },
  mounted() {
      //可添加计时器,3s后关闭提示框
    setTimeout(() => {
      this.show = false;
    }, 3000);
  },
};
</script>
<style scoped>
.alertBox {
  width: 350px;
  height: 80px;
  border: 1px solid hsl(280, 100%, 50%);
  border-radius: 8px;
  padding: 24px;

  /* position: absolute;
  right: 12px;
  bottom: 12px; */
  /*改为相对定位,因为关闭按钮在其中*/
  position: relative;
}
.content {
  height: 100%;
  display: flex;
  align-items: center;
}
.closeIcon {
  position: absolute;
  right: 12px;
  top: 12px;
  font-size: 12px;
  font-weight: 900;
  cursor: pointer;
}
</style>

Mixins:

概念

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。

本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如datacomponentsmethodscreatedcomputed等等

我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来

Vue中我们可以局部混入全局混入

局部混入

定义一个mixin对象,有组件optionsdatamethods属性

// 定义一个混入对象
var myMixin = {
  created: function () {
    this.hello()
  },
  methods: {
    hello: function () {
      console.log('hello from mixin!')
    }
  }
}

// 定义一个使用混入对象的组件
var Component = Vue.extend({
  mixins: [myMixin]
})

组件通过mixins属性调用mixin对象

var component = new Component() // => "hello from mixin!"

该组件在使用的时候,混合了mixin里面的方法,在自动执行created生命钩子,执行hello方法

全局混入

通过Vue.mixin()进行全局的混入

import { createApp } from "vue";
import App from "./App.vue";
import PaginationMixin from "./mixins/PaginationMixin";

const app = createApp(App);

app.mixin(PaginationMixin);

app.mixin({
  siteTitle: "我的 Vue 应用",
  computed: {
    siteTitle() {
      return this.$options.siteTitle;
    },
  },
});

app.mount("#app");
<template>
  <main>
    <div class="container">
      <h1>
        {{ siteTitle }}
      </h1>
      <PaginationComponent
        :totalPage="totalPage"
        :defaultCurrentPage="currentPage"
      />
      <PaginationComponent2
        :totalPage="totalPage"
        :defaultCurrentPage="currentPage"
      />
      <BaseButton :defaultCurrentPage="currentPage" />
    </div>
  </main>
</template>

<script>
import BaseButton from "./components/BaseButton.vue";
import PaginationComponent from "./components/PaginationComponent.vue";
import PaginationComponent2 from "./components/PaginationComponent2.vue";

export default {
  siteTitle: "Mixin 全局注册",
  components: {
    PaginationComponent,
    PaginationComponent2,
    BaseButton,
  },
  data() {
    return {
      totalPage: 6,
      currentPage: 4,
    };
  },
};
</script>

使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)

PS:全局混入常用于插件的编写

注意事项

  • 当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项

  • 但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子(而不是覆盖)

使用场景

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立

这时,可以通过Vuemixin功能将相同或者相似的代码提出来

定义一个modal弹窗组件,内部通过isShowing来控制显示

const Modal = {
  template: '#modal',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

定义一个tooltip提示框,内部通过isShowing来控制显示

const Tooltip = {
  template: '#tooltip',
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin就派上用场了

首先抽出共同代码,编写一个mixin

const toggle = {
  data() {
    return {
      isShowing: false
    }
  },
  methods: {
    toggleShow() {
      this.isShowing = !this.isShowing;
    }
  }
}

两个组件在使用上,只需要引入mixin

const Modal = {
  template: '#modal',
  mixins: [toggle]
};
 
const Tooltip = {
  template: '#tooltip',
  mixins: [toggle]
}

通过上面小小的例子,让我们知道了Mixin对于封装一些可复用的功能如此有趣、方便、实用

源码分析

首先从Vue.mixin入手

源码位置:/src/core/global-api/mixin.js

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

主要是调用merOptions方法

源码位置:/src/core/util/options.js

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {

if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并
    for (let i = 0, l = child.mixins.length; i < l; i++) {
    parent = mergeOptions(parent, child.mixins[i], vm)
    }
}

  const options = {} 
  let key
  for (key in parent) {
    mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并
  }
  for (key in child) {
    if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了
      mergeField(key) // 处理child中的key 也就parent中没有处理过的key
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并
  }
  return options
}

从上面的源码,我们得到以下几点:

  • 优先递归处理 mixins
  • 先遍历合并parent 中的key,调用mergeField方法进行合并,然后保存在变量options
  • 再遍历 child,合并补上 parent 中没有的key,调用mergeField方法进行合并,保存在变量options
  • 通过 mergeField 函数进行了合并

下面是关于Vue的几种类型的合并策略

  • 替换型
  • 合并型
  • 队列型
  • 叠加型

替换型

替换型合并有propsmethodsinjectcomputed

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal
  const ret = Object.create(null) // 创建一个第三方对象 ret
  extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中
  if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中
  return ret
}
strats.provide = mergeDataOrFn

同名的propsmethodsinjectcomputed会被后来者代替

合并型

和并型合并有:data

strats.data = function(parentVal, childVal, vm) {    
    return mergeDataOrFn(
        parentVal, childVal, vm
    )
};

function mergeDataOrFn(parentVal, childVal, vm) {    
    return function mergedInstanceDataFn() {        
        var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象
        var parentData = parentVal.call(vm, vm)        
        if (childData) {            
            return mergeData(childData, parentData) // 将2个对象进行合并                                 
        } else {            
            return parentData // 如果没有childData 直接返回parentData
        }
    }
}

function mergeData(to, from) {    
    if (!from) return to    
    var key, toVal, fromVal;    
    var keys = Object.keys(from);   
    for (var i = 0; i < keys.length; i++) {
        key = keys[i];
        toVal = to[key];
        fromVal = from[key];    
        // 如果不存在这个属性,就重新设置
        if (!to.hasOwnProperty(key)) {
            set(to, key, fromVal);
        }      
        // 存在相同属性,合并对象
        else if (typeof toVal =="object" && typeof fromVal =="object") {
            mergeData(toVal, fromVal);
        }
    }    
    return to
}

mergeData函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:

  • 当目标 data 对象不包含当前属性时,调用 set 方法进行合并(set方法其实就是一些合并重新赋值的方法)
  • 当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性

队列性

队列性合并有:全部生命周期和watch

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

// watch
strats.watch = function (
  parentVal,
  childVal,
  vm,
  key
) {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) { parentVal = undefined; }
  if (childVal === nativeWatch) { childVal = undefined; }
  /* istanbul ignore if */
  if (!childVal) { return Object.create(parentVal || null) }
  {
    assertObjectType(key, childVal, vm);
  }
  if (!parentVal) { return childVal }
  var ret = {};
  extend(ret, parentVal);
  for (var key$1 in childVal) {
    var parent = ret[key$1];
    var child = childVal[key$1];
    if (parent && !Array.isArray(parent)) {
      parent = [parent];
    }
    ret[key$1] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child];
  }
  return ret
};

生命周期钩子和watch被合并为一个数组,然后正序遍历一次执行

叠加型

叠加型合并有:componentdirectivesfilters

strats.components=
strats.directives=

strats.filters = function mergeAssets(
    parentVal, childVal, vm, key
) {    
    var res = Object.create(parentVal || null);    
    if (childVal) { 
        for (var key in childVal) {
            res[key] = childVal[key];
        }   
    } 
    return res
}

叠加型主要是通过原型链进行层层的叠加

小结

  • 替换型策略有propsmethodsinjectcomputed,就是将新的同名参数替代旧的参数
  • 合并型策略是data, 通过set方法进行合并和重新赋值
  • 队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
  • 叠加型有componentdirectivesfilters,通过原型链进行层层的叠加

异步组件加载:

对于大型项目,一次性加载所有组件会影响页面的首次打开速度。正确方法应该是根据用户的操作与导航,加载相应的组件。

defineAsyncComponent:接收一个回调函数作为参数,可用于异步加载组件。

import { defineAsyncComponent } from "vue";

const ProductPage = defineAsyncComponent(() =>
  import("./components/ProductPage.vue")
);

组件错误处理:

全局错误处理:

errorHandler

  • 类型Function

  • 默认值undefined

  • 用法

    Vue.config.errorHandler = function (err, vm, info) {
      // handle error
      // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
      // 只在 2.2.0+ 可用
    }

    指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例。

    从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是 undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩溃。

    从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了。

    从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理。

    错误追踪服务 SentryBugsnag 都通过此选项提供了官方支持。

示例:

import { createApp } from "vue";
import App from "./App.vue";

const app = createApp(App);

app.config.errorHandler = (err, vm, info) => {
  console.log(err);
  console.log(vm);
  console.log(info);
};

app.mount("#app");

错误边界:

如果某个Vue组件提供了错误处理能力,那么它就被称为错误边界。Vue组件会以事件冒泡方式向父组件传递错误信息,只有当父组件处理了错误并手动停止了错误的传播,错误才不会继续向上传递,这样这个子组件所有的子组件发生的错误都会交给他进行处理。

示例:

<template>
  <main>
    <div>
      <p v-if="error">啊哦,我是错误的顶级组件了!</p>
      <!-- <AppList v-else :data="data" /> -->
      <template v-else>
        <AppList :data="data" />
        <AppButton>测试按钮</AppButton>
      </template>
    </div>
  </main>
</template>

<script>
import AppList from "./components/AppList.vue";
import AppButton from "./components/AppButton.vue";

export default {
  components: {
    AppList,
    AppButton,
  },
  data() {
    return {
      data: [1, 2, 3],
      error: false,
    };
  },
  errorCaptured() {
    this.error = true;
    // 停止错误向上传播
    return false;
  },
};
</script>

文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录