文章较长请先关注收藏如果一不小心解决了你在使用vue中的某个痛点记得点个赞哦
用vue
也有些年头了,不得不说vue
确实是一个了不起的框架(不接受任何反驳)但在工作中有太多的前端开发者还只是停留在会用的圈圈中,有很多人觉得我没有看完官方文档也不妨我们做vue
项目写vue
代码啊?确实,这点不可否认
但是大哥,你一个vue
文件写1000多行,是觉得自己的头发掉的不够快吗?
你们信不爱读文档的程序员能写出好代码吗?反正我是不信
我们知道prop
是接受父组件参数,假如现在要接收一个对象,可能你会这样用
<!--父组件传过来的值 父组件的数据是异步请求回来的-->
item:{
name:'刘小灰',
age:18
}
<!--子组件接收-->
Vue.component('my-component', {
props:['item']
}
<!--页面上使用-->
<span> {{item.name}} </span>
如果粗心的程序员没有传这个item
,控制台就会报错
<span v-if="item">{{ item.name }}</span>
页面又一切正常好像什么都没发生,这个时候你可能心里犯迷糊, 这个bug大家都是这样解决的吗?
如果你看过vue
的官方文档,了解prop
的所有用法,当你第一眼看到这个bug时就会立马反应过来,prop
应该这样写更为合理
Vue.component('my-component', {
props:{
item:{
type:Object,
defent:()=>{return:{}}
}
}
}
例子可能过于简单,主要想表达的思想就是 只有先了解框架具备的所有能力,才能写出更高质量的代码
既然是重学vue
说明不是第一次学了,这个时候建议大家从 风格指南 开始重学,如果是新手还是建议大家从 教程 一步一步开始学
以下示例均在
vue-cli3
中完成
在开发中你可能会遇到 不知道给组件怎么取名 的尴尬情况,遵从vue
规范,让你给组件起名即 顺畅 又 规范
组件名应该始终是多个单词的,根组件 App 以及 、 之类的
Vue
内置组件除外。这样做可以避免跟现有的以及未来的 HTML 元素相冲突,因为所有的 HTML 元素名称都是单个单词的。 -官方文档
用多个单词定义组件不仅可以避免和原有的HTML元素相冲突,在外观上看来也更加的好看
PascalCase
或kebab-case
命名规范或的意思是我们在命名时即可以采用驼峰命名
da也可以采用-
命名,但建议大家在项目中统一风格只用一种,我本人习惯使用PascalCase
格式
单词大写开头对于代码编辑器的自动补全最为友好,因为这使得我们在
JS(X)
和模板中引用组件的方式尽可能的一致。
然而,混用文件命名方式有的时候会导致大小写不敏感的文件系统的问题,这也是横线连接命名同样完全可取的原因 -官方文档
原因就是PascalCase
更有利于 代码自动补全 ,至于导致大小写不敏感的系统文件问题我暂时还没遇到
Base
| App
| V
开头推荐用Base开头,因为更加语义化如一个基础的按钮组件我们可以叫BaseBtn.vue
The
开头可能有的人不熟悉什么是单例组件,单例是一种设计模式不清楚这个概念的可以自己查阅资料(也可以关注公众号 码不停息 里面有介绍),比如我们常见的element-ui
中通过js
调用的弹窗组件就可以看做是一个单例组件
如果一个公用组件比较复杂我们可以抽离出几个子组件,同时从命名上区别出组件之间的关系,如:
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
1. 这里我把基础组件和单例组件单独拿出来放在了`common`文件夹中`components`文件里面放置项目公共组件
2. 每个组件建议放在一个单独的文件夹而不是用单独的文件,有利于后期的扩展及维护
在我们平常开发中一个组件会调用很多vue
实例,由于开发人员的习惯不同这些实例书写顺序也不同,这样无形之中增加了我们的维护成本,下面我们来看看vue
推荐的书写顺序
vue
文件里面js
,要按照vue
的生命周期来写,最开始是mixins->porps->data->computed->mounted->watch->methods->components
,用不到的可以忽略,统一顺序,养成习惯
1. name
2. components
4. directives
5. filters
6. extends
7. minins
8. props
9. data
10. computed
11. watch
12. beforeCreate
13. created
14. beforeMount
15. mounted
16. beforeUpdate
17. updated
18. activated`
19. deactivated
20. beforeDestroy
21. destroyed
22. methods
上面列的比较多,在我们实际开发中,没有用到的可以不写,保留这个顺序即可
应该优先通过
prop
和事件进行父子组件之间的通信,而不是this.$parent
或变更prop
。一个理想的
Vue
应用是prop
向下传递,事件向上传递的。遵循这一约定会让你的组件更易于理解。然而,在一些边界情况下prop
的变更或this.$parent
能够简化两个深度耦合的组件
记住这句话 一个理想的 Vue
应用是 prop
向下传递,事件向上传递的 可以让我们少写很多野路子代码
vue
官方的风格规范有很多,我这里只是抛砖引玉,捡了我认为比较有用的给大家回顾下,更加详细的内容可以去官方文档瞅一瞅
如:
<BaseBtn @click="btn-click"></BaseBtn>
写在里面的(组件的使用,事件)使用
kebab-case
命名规范,其他地方使用PascalCase
命名规范
可以在任何地方都使用PascalCase
吗?
不推荐因为有些时候可能会出现大小写不明白情况
可以再任何地方都使用kebab-case
吗?
原则上可以这个看个人爱好,需要注意的是kebab-case
对代码编辑器的自动补全不太友好
相信大家最初学vue
的时候都看过这个教程,下面我带着大家再回顾下比较重要且容易被遗忘的一些知识点
Vue
的安装目前使用vue
最常用的就是通过npm
引入或者直接script
标签引入,下面是官方给出的vue
构建的不同版本
UMD
UMD
版本可以通过
标签直接用在浏览器中CommonJS
CommonJS
版本用来配合老的打包工具比如 Browserify
或 webpack 1
。这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS
版本 (vue.runtime.common.js)
ESModule
从 2.6 开始 Vue
会提供两个 ES Modules (ESM)
构建文件:
webpack 2
或 Rollup
提供的现代打包工具。ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。为这些打包 工具提供的默认文件 (pkg.module)
是只有运行时的 ES Module 构建 (vue.runtime.esm.js)。ESM (2.6+)
:用于在现代浏览器中通过
直接导入。可以看出 vue
给出了很多种构建版本适用于UMD CommonJS ESModule
,对这些规范不理解的可以看 这篇文章,而 我们通常使用的通过webpack
构建出来的vue-cli
遵循的是ESModule
规范
不同构建版本中又分为 完整版 和 只包含运行时版本 ,为了便于理解我们可以把vue
代码大致分为负责运行时的代码
和负责编译的代码
,他们之间的关系是编译器 + 运行时 ≈ 完整版
而编译器只是在编译开发环境下使用,也就是说生产环境中我们只需要使用 只包含运行时版本 的vue
,而不是 完整版 的vue
,如果你是使用vue-cli
可以在vue.config.js
中配置生成环境下不打包vue
然后通过 CDN 的方式去引入 只包含运行时版本 的vue
,代码如下:
index.html
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>vue-apptitle>
<script
src="https://cdn.bootcss.com/vue/2.6.10/vue.runtime.min.js"
crossorigin="anonymous"
>script>
head>
module.exports = {
configureWebpack: {
externals: {
vue: 'Vue'
}
}
}
下面是通过 npm使用vue
和 通过cdn使用vue完成版
及通过cdn使用只包含运行时版
打包后的性能对比图
通过cdn
方式引入vue
打出来的包要小
我们再来看vueruntime.js (只包含运行时)
和vue.main.js (完整版)
大小的对比
这也验证了官方的数据
在看看你的项目vue
引入对了吗?
Vue
并不完全遵循MVVM
模型vue
是基于什么模型吗?MVVM
要说清楚这点,我们先来看看学习几个典型的架构模型
视图(View):用户界面。
控制器(Controller):业务逻辑
模型(Model):数据保存
他们之间的通讯方式为
可以看出MVC
模型数据都是单向的,流程可以简化为
用户行为改变(点击事件)Viwe -> View通知Contoller进行逻辑处理 -> 处理后Controller通知Model层数据改变
-> Model数据改变后交给View渲染(改变view层)
注:用户也可以直接改变Contoller
MVP
可以看做是MVC
的衍生物,在MVP
中Model
不能直接操作View
,且所有的通讯都是双向的
MVVM
模式将 Presenter
改名为 ViewModel
,基本上与 MVP
模式完全一致。
唯一的区别是,它采用双向绑定(data-binding)
:View
的变动,自动 反映在 ViewModel
,反之亦然
Vue
没有完全遵循MVVM
严格意义上在MVVM
中 View
和Model
之间是不能通讯的,但Vue
却提供了相应的Api $refs
我们可以在项目中这样使用
<template>
<div>
<input type="text" ref="dome" value="1" />
div>
template>
<script>
export default {
name: 'home',
components: {},
data() {
return {}
},
mounted() {
console.log(this.$refs.dome.value)
this.$refs.dome.value = 2
},
methods: {}
}
script>
可以看出我们可以直接通过Model
去操作View
vue
官方也对$refs
进行说明
所以说Vue
并不是正在意义上的MVVM
架构,但是思想是借鉴了MVVM
然后又进行了些本土化
,不过问题不大,现在根据MVVM本土化
出来的架构都统称MV*
架构
你还知道vue
的哪些设计没有遵循MVVM
规范呢? 欢迎留言
Vue
实例Object.freeze()
当一个
Vue
实例被创建时,它将data
对象中的所有的property
加入到Vue
的响应式系统中。当这些property
的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
但在项目开发中,有的信息我们不需要他是响应式的,这个时候我们可以用Object.freeze()
如:
<template>
<div>
<div>
{{ user }}
div>
div>
template>
<script>
export default {
name: 'index',
data() {
return {
number: 2,
price: 10,
user: Object.freeze({ age: 18 })
}
},
mounted() {
this.user.age = 20
}
}
script>
<style>
.totle {
padding-top: 20px;
}
style>
当我们给user
数据加上Object.freeze()
后,如果再更改user数据控制台就会报错
Object.freeze()只能用户对象|数组
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
上面是官方给出的完整的生命周期流程图,可以说是应用的灵魂,下面我们在代码中实际运行顺序为
beforeCreate: function() {
console.log(this)
console.log('创建vue实例前', this)
},
created: function() {
console.log('创建vue实例后', this)
},
beforeMount: function() {
console.log('挂载到dom前', this)
},
mounted: function() {
console.log('挂载到dom后', this)
},
beforeUpdate: function() {
console.log('数据变化更新前', this)
},
updated: function() {
console.log('数据变化更新后', this)
},
beforeDestroy: function() {
console.log('vue实例销毁前', this)
},
destroyed: function() {
console.log('vue实例销毁后', this)
}
beforeCreate
创建vue
前调用,这个过程中进行了初始化事件、生命周期created
vue
创建成功之后,dom
渲染之前调用,通常请求数据会写在这个函数里面mounted
dom
创建渲染完成时调用,这个时候页面已经渲染完毕,可以再这个函数里面进行dom
操作updated
数据更改且 时调用,他和watch
不同,watch
只有监听的数据变化就会触发,而 updated
要求这个变更的数据必须在页面上使用了,且 只要页面的数据发生变化都会触发这个函数beforeDestroy/destroyed
vue
实例或者说组件销毁前后调用,如果页面中需要销毁定时器和释放内存,可以写在这个函数里destroyed
和beforeRouteLeave
destroyed
需要和 vue-router
中 beforeRouteLeave
api区别开
通常意义下路由发生变化也就意味上个组件被销毁,所以这两个函数都会触发
destroyed
只是个监听功能,不能阻止页面要不要销毁
而beforeRouteLeave
可以通过next()
控制路由是否要变化
例如:当需要判断用户是否返回时使用beforeRouteLeave
而不是destroyed
双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html
,比如用v-html
渲染后端返回回来的富文本内容
值得注意的是:
站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。
vue
指令支持动态参数,比如:
<a v-on:[eventName]="doSomething">...a>
当eventName=click
时doSomething
就是点击事件当eventName=focus
时doSomething就是focus事件
同理,属性也支持动态形式,如:
<a v-bind:[attributeName]="url"> ... a>
两者最大的区别就是
代码说明:
<template>
<div>
<span>数量:span> <input type="number" ref="dome" v-model="number" />
<span>价格:span> <input type="number" v-model="price" />
<div class="totle">
<span> 总价: span>
<div>
{{ totle }}
div>
<div>
{{ totle }}
div>
div>
div>
template>
<script>
export default {
name: 'index',
data() {
return {
number: 2,
price: 10
}
},
computed: {
totle() {
console.log(1)
const totle = this.number * this.price
return totle > 0 ? totle : 0
}
}
}
script>
<style>
.totle {
padding-top: 20px;
}
style>
这是例子很简单,就是时时计算totle
值,我们在页面上故意写两个{{totle}}
但控制台中只输出了一个1,说明计算属性totle
只计算了一次,页面上第二个20
直接用了第一次计算的结果
我们把totle
改成方法的形式看一看
<template>
<div>
<span>数量:span> <input type="number" ref="dome" v-model="number" />
<span>价格:span> <input type="number" v-model="price" />
<div class="totle">
<span> 总价: span>
<div>
{{ totle() }}
div>
<div>
{{ totle() }}
div>
div>
div>
template>
<script>
export default {
name: 'index',
data() {
return {
number: 2,
price: 10
}
},
methods: {
totle() {
console.log(1)
const totle = this.number * this.price
return totle > 0 ? totle : 0
}
}
}
script>
<style>
.totle {
padding-top: 20px;
}
style>
可以看出totle
在页面上调用了两次而控制台就输出两次
显然如果有多个数据依赖totle
,方法的性能开销是计算属性的n倍,下面是官方的解释
我们使用监听着watch
实现上述功能
<div>
<span>数量:span> <input type="number" ref="dome" v-model="number" />
<span>价格:span> <input type="number" v-model="price" />
<div class="totle">
<span> 总价: span>
<div>
{{ totle }}
div>
<div>
{{ totle }}
div>
div>
div>
template>
<script>
export default {
name: 'index',
data() {
return {
number: 2,
price: 10,
totle: 20
}
},
watch: {
price() {
const totle = this.number * this.price
this.totle = totle > 0 ? totle : 0
},
number() {
const totle = this.number * this.price
this.totle = totle > 0 ? totle : 0
}
}
}
script>
显然没有计算属性来的优雅,所有项目中,让我们又动态计算需求时最应该使用计算属性
,而不是watch
那什么时候使用watch
呢?官方给出答案
也就是说,当我们处理函数中有异步请求(定时器,ajax)时应该使用watch
,因为计算属性里面不支持写异步
可以看出,编辑器直接提示computed
不支持异步的写法
watch
在Vue
中给元素添加事件可谓是最常见的操作,Vue
中也为我们提供了很多事件修饰符供我们使用
<a @click.stop="doThis">a>
<form @submit.prevent="onSubmit">form>
<a @click.stop.prevent="doThat">a>
<form @submit.prevent>form>
<div @click.capture="doThis">...div>
<div @click.self="doThat">...div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 @click.prevent.self 会阻止所有的点击,而 @click.self.prevent 只会阻止对元素自身的点击。
有两个修饰符值得我们注意:
once
点击事件将只会触发一次native
将原生事件绑定到组件<item-list @click="clickHandle"></item-list> // 将会触发item-list内部的clickHandle
<item-list @click.native="clickHandle"></item-list> // 将会触发父组件内部的clickHandle
官网上给出了很多像上面一样的小技巧
,大家可以自行查阅
这应该是最重要的一节,组件是vue
的灵魂 在实际开发中,好的组件可以让我们的开发效率及维护成本事倍功半,反之事倍功半
在做项目中有些组件是我们经常用的,这样的组件我们可以注册为全局组件,如:注册一个全局的BaseBtn
组件
BaseBtn
组件<template>
<div class="baseben">
这是一个按钮组件
div>
template>
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BaseBtn from "@/common/BaseBtn";
Vue.component("base-btn", BaseBtn);
Vue.config.productionTip = false;
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
<template>
<div class="about">
<h1>This is an about pageh1>
<base-btn>base-btn>
div>
template>
关键性方法require.context
主要流程是:读取要注册为全局组件的文件路径->循环进行动态注册
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import upperFirst from "lodash/upperFirst";
import camelCase from "lodash/camelCase";
Vue.config.productionTip = false;
const requireComponent = require.context(
// 其组件目录的相对路径
"./common",
// 是否查询其子目录
false,
// 匹配基础组件文件名的正则表达式
/Base[A-Z]\w+\.(vue|js)$/
);
requireComponent.keys().forEach(fileName => {
const componentConfig = requireComponent(fileName);
const componentName = upperFirst(
camelCase(
fileName
.split("/")
.pop()
.replace(/\.\w+$/, "")
)
);
Vue.component(componentName, componentConfig.default || componentConfig);
});
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
页面还是正常显示
上文说过,基础组件使用Base
开头进行命名,所以require.context
的筛选正则才可以这样写 Base[A-Z]\w+\.(vue|js)$/
这样以后只要我们在common
文件夹下以Base
开头的文件都会自动注册为全局组件
比如我们要封装一个input组件,使用的时候这样使用
<base-input v-model="name">base-input>
data(){
return{
name:'刘小灰'
}
}
那我们BaseInput
里面如何去接受name
参数呢? 我们可以使用model
选项,如:
<template>
<div class="inputWarp">
<input
type="text"
:value="value"
@input="$emit('change', $event.target.value)"
/>
{{ value }}
div>
template>
<script>
export default {
props: {
value: {
type: String || Number
}
},
model: {
prop: "value",
event: "change"
}
};
script>
子组件model
中需要定义prop
和event
prop
给参数重命名,新的变量名需要在props
中定义event
触发的事件名称默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event。
上面的代码还可以这样简化
<template>
<div class="inputWarp">
<input
type="text"
:value="value"
@input="$emit('input', $event.target.value)"
/>
{{ value }}
div>
template>
<script>
export default {
props: {
value: {
type: String || Number
}
}
};
script>
可能有的人不理解.sync
有什么用,其实它就是一种子组件改变父组件传过来的prop
并让父组件数据更新的一种语法糖
那什么时候使用呢?我们来封装一个自定义弹窗组件BaseAlert.vue
<template>
<div class="baseAlert">
<div v-if="show" class="alert">
<div>
我是一个弹框
div>
<button @click="close">关闭弹窗button>
div>
div>
template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false
}
},
data() {
return {};
},
methods: {
close() {
this.show = false;
}
}
};
script>
在父组件中使用
<template>
<div class="about">
<button @click="show = true">打开弹框button>
<base-alert :show="show">base-alert>
div>
template>
<script>
export default {
data() {
return {
show: false
};
}
};
script>
试验一下
为什么可以关闭,但页面为什么会报错?
因为我们在子组件中让show=false
但是show是父组件传过来的,我们直接改变它的值vue
会报错
再次点击打开弹窗时,为什么没有反应?
虽然在子组件中show
的状态是false
但是在父组件中show
的状态还是true
上面的情况可以解决吗? 肯定可以, 我们只需要点击子组件的关闭按钮时通知父组件,让父组件把show
的状态变为false
即可:如
methods: {
close() {
this.$emit('close',false)
}
}
<base-alert :show="show" @close="close">base-alert>
methods: {
close(status) {
this.show=status
}
}
问题是 父组件为了关闭弹窗这个简单的功能还需要用一个函数close
,实在是不太优雅,而.sync
就是来解决这样类似的场景
我们现在使用.sync
重构代码
<template>
<div class="about">
<button @click="show = true">打开弹框button>
<base-alert :show.sync="show">base-alert> // 在 :show 改为:show.sync
div>
template>
<script>
export default {
data() {
return {
show: false
};
},
methods: {
close(status) {
this.show = status;
}
}
};
script>
BaseAlert
组件改造
<template>
<div class="baseAlert">
<div v-if="show" class="alert">
<div>
我是一个弹框
div>
<button @click="close">关闭弹窗button>
div>
div>
template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false
}
},
data() {
return {};
},
methods: {
close() {
this.$emit("update:show", false); // 注意把show 改为 update:show
}
}
};
script>
公用逻辑的抽离和组合一直是项目中难题,同样也是框架设计者的难题,在Vue2.0
中也提供了抽离公共逻辑的方法Mixins
,但Mixins
有着明显的缺陷 无法清楚得知数据来源 特别是在一个页面有多个Mixins
时,页面维护起来简直是种灾难
上面是尤雨溪在VueConf演讲中提到的有关Mixins
问题
上面是尤雨溪在VueConf演讲中提到插槽有关的问题,因为插槽
是以组件为载体,所以有额外的组件实例性能消化,但也正是因为以组件为载体,所以也可以封装些样式相关的东西
可以看出在Vue2.0
中插槽
是逻辑复用的最优解决方案,当然在Vue3.0中
有更好的解决方案composition-api
,现在你应该了解到为什么Vue3.0
要出composition-api
了,主要解决的问题就是 逻辑的分封装与复用
下面我们再来改造下上面的BaseAlert.vue
组件来学习下各种插槽之间的使用
需求:
假设我们在父组件中这样使用
<base-alert :show.sync="show">
<template v-slot:title>
重大提示
template>
<template v-slot="{ time }">
这是一个弹窗
<div class="time">{{ time }}div>
template>
base-alert>
我们定义了一个具名插槽 v-slot:title
和作用域插槽接受子组件的time
BaseAlert.vue
封装
<template>
<div class="baseAlert">
<div v-if="show" class="alert">
<div class="header">
<slot name="title">slot>
div>
<div class="contents">
<slot :time="time"> slot>
div>
<button @click="close">关闭弹窗button>
div>
div>
template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false
}
},
computed: {
time() {
const d = new Date();
return d.getFullYear() + "-" + (d.getMonth() - 1) + "-" + d.getDate();
}
},
data() {
return {};
},
methods: {
close() {
this.$emit("update:show", false);
}
}
};
script>
作用域插槽的使用就是在slot
中可以添加自定义属性,在父组件用v-slot
接收即可
关于匿名插槽
和动态插槽
理解起来比较简单,就不举例子说明了
我们可以通过is
关键字动态加载组件,同时使用keep-alive
可对组件进行缓存,在tab切换场景中使用较多,如:
<keep-alive>
<component v-bind:is="currentTabComponent">component>
keep-alive>
keep-alive
也可和路由搭配使用可以把项目的整体体验提升一个段,后续写重学vue-Router
的时候会讲到
当一个组件比较大的时候,为了不影响整个页面的加载速度,我们需要使用异步去加载整个组件,和异步路由写法一样,异步组件使用如下:
new Vue({
components: {
'my-component': () => import('./my-async-component')
}
})
值得注意的是,官方还支持对异步组件加载状态进行配置
但是我在vue-cli测试的时候delay
属性一直无效,不知道你们如何配置异步组件加载状态的呢?欢迎留言谈论
组件之间的通讯一直是vue
的高频考点,也是在项目开发中比较重要的一个知识点,下面我就带领大家总结下,vue
组件通讯都有哪些,且分别适用于哪些场景
父组件给子组件传递数据
父组件调用子组件用 :(v-bind)
绑定数据
<item-list :item="data">item-list>
子组件用 props
接收
<!--子组件-->
export default {
props:{
item:{
type:Object
}
}
}
子组件给父组件传递数据
子组件通过触发事件的方式 $emit
给父组件传递数据
<template>
<div>
我是子组件
<button @click="btnClick">点击传递数据button>
div>
template>
<script>
export default {
methods: {
btnClick() {
this.$emit("childFn", "数据");
}
}
};
script>
父组件用对应的事件去接收
<button @click="btnClick" @childFn="childFn">点击传递数据button>
<script>
export default {
methods: {
childFn(val) {
console.log(val); //数据
}
}
};
script>
父组件触发子组件方法
父组件通过ref的方式调用子组件方法
<button @click="btnClick" ref="hello">点击传递数据button>
<script>
export default {
methods: {
btnClick(val) {
this.$refs['hellow'].子组件方法
}
}
};
script>
子组件触发父组件方法
通过$emit
触发父组件方法,和上面的 子组件给父组件传递数据 一样
在父组件里想拿到子组的实例很简单this.$children
就可以拿到全部子组件的实例,只要拿到组件的实例,那事情都变的简单了
this.$children['组件名称'].xxx //改变子组件数据
this.$children['组件名称'].xxx() // 调用子组件方法
子组件调用父组件的道理也一样,用this.$parent
即可
这种父子组件通讯的方式这么简单,为什么不推荐使用呢?刚开始学vue的时候我也有这样的疑问,但是通过做项目的时候发现,这样通讯最要命的弊端就是 数据状态改变不明了, 特别是一个父组件里面有很多子组件,当父组件数据改变时你并不知道是哪个子组件所为, 就和使用mixins所带来的尴尬一样
值得注意的是,官方并没给出父子组件隔代通讯及兄弟组件之间通讯的相关API,如果业务里面有这样的需求我们只能用vuex
这种第三方状态管理器,但如果我们是封装项目的基础组件,或者自己做个组件库,这个时候并不能依赖vuex
,那我们应该怎么样方便快捷的去实现呢?
下面所说的方式推荐在开发独立组件的时候使用,不推荐在项目组件中直接使用
provide / inject
主要用于子组件获取父组件的数据/方法,如:
<!--父组件-->
export default {
name: "Home",
provide: {
name: "公众号码不停息" // 传数据
},
}
<!--子组件-->
export default {
inject: ["name"], // 接受数据
mounted() {
console.log(this.name); //公众号码不停息
}
};
并且provide / inject
还支持隔代传递
官方不推荐provide/inject
用于普通应用程序代码中,但如果你充分的了解了它的特性,有时候provide/inject
在某种应用场景下可以代替vuex
需要满足什么场景呢?
provide/inject
只能用于vue
组件,不能用于js
文件中,而权限判断一般会写在路由拦截等js
文件里面,这时用provide/inject
就会显得比较乏力那我们怎么使用provide/inject
来取代vuex
呢?
vuex
主要的作用就是管理应用中的数据,那按道说只要我们把provide
写在一个应用 最大的父组件 里面,那应用里面所有的组件都可以使用provide
所暴露出来的数据/方法了,显然这个 最大的父组件 就是app.vue
组件
// app.vue
<script>
export default {
provide() {
return {
app: this //把整个app实例暴露出去
};
},
data() {
return {
userInfo: {
name: "码不停息",
age: 18
}
};
},
methods: {
getUserInfo() {
console.log("请求接口");
}
}
};
</script>
下面我们在任意一个组件中去拿app.vue
中userInfo
和调用getUserInfo
方法
<script>
import BaseLoading from "@/common/BaseLoading.vue";
export default {
inject: ["app"], // 把app实例导入
mounted() {
console.log(this.app.userInfo);
this.app.getUserInfo();
}
};
</script>
使用provide/inject
可以满足我们搭建小而美的应用
所谓的自定义,也就是自己封装一个通用的函数,来实现复杂情况下的数据传递,原理就是根据组件的name去遍历查找自己需要的组件,下面我们先弄清楚这个方法应给具备怎样的功能
向上找到最近的指定组件
向上找到所有的指定组件
向下找到最近的指定组件
向下找到所有指定的组件
找到指定组件的兄弟组件
代码如下:
// 由一个组件,向上找到最近的指定组件
function findComponentUpward(context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
// 由一个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
let parents = [];
const parent = context.$parent;
if (parent) {
if (parent.$options.name === componentName) parents.push(parent);
return parents.concat(findComponentsUpward(parent, componentName));
} else {
return [];
}
}
// 由一个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
const childrens = context.$children;
let children = null;
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name;
if (name === componentName) {
children = child;
break;
} else {
children = findComponentDownward(child, componentName);
if (children) break;
}
}
}
return children;
}
// 由一个组件,向下找到所有指定的组件
function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child);
const foundChilds = findComponentsDownward(child, componentName);
return components.concat(foundChilds);
}, []);
}
// 由一个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
let res = context.$parent.$children.filter(item => {
return item.$options.name === componentName;
});
let index = res.findIndex(item => item._uid === context._uid);
if (exceptMe) res.splice(index, 1);
return res;
}
export {
findComponentUpward,
findComponentsUpward,
findComponentDownward,
findComponentsDownward,
findBrothersComponents
};
此代码copy Aresn大神(iView 作者) 写的 Vue.js组件精讲小册,不是打广告,看完这个小册你的vue水平可以提升一个段
下面我们简单的使用下其中的一个方法findComponentUpward
向上查找最近的父组件
<script>
import { findComponentUpward } from "@/units/findComponents";
export default {
methods: {
btnClick() {
console.log(this.$parent);
this.$parent.aaaaa();
},
ceshi() {}
},
mounted() {
console.log(findComponentUpward(this, "Home"));
}
};
</script>
可以看出父组件实例已经打印出来,有了父组件实例就可以为所欲为啦
注意:使用该方法时组件必须有name属性
本文主要从官方文档出发,梳理了vue
比较实用且容易被忽略的知识点,不是大神,如有错误请多多指教,该篇是__从文档开始重学vue
__的上册,下册将带领大家回顾vue
一些比较重要的api
希望大家持续关注哦!
交个朋友吧