03-面试之前端 Vue 组件传值

Vue 组件间通信方式

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握得越熟练。Vue 组件间通信主要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。

  1. props/$emit 适用父子组件通信

    这种方法是 Vue 组件的基础,父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过 $emit 触发事件来做到的

  2. ref$parent/$children 适用父子组件通信

    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $parent/$children:访问父/子实例
  3. EventBus($emit/$on) 适用于 父子、隔代、兄弟组件通信

    这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),然后通过 bus.on 监听触发的事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。

  4. $attrs/$listeners 适用于 隔代组件通信。Vue 2.4 出现。

    • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
  5. provide/inject 适用于 隔代组件通信

    祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 不论子组件有多深,只要调用了 inject 那么就可以注入 provider 中的数据。而不是局限于只能从当前父组件的 prop 属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。

    provide/inject 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

  6. Vuex 适用于 父子、隔代、兄弟组件通信

    Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。

    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
  7. v-model

    父组件通过 v-model 传递值给子组件时,会自动传递一个 value 的 prop 属性,在子组件中通过 this.$emit('input', val) 自动修改 v-model 绑定的值

  8. boradcast 和 dispatch

    vue1.0 中提供了这种方式,但 vue2.0 中没有,但很多开源软件都自己封装了这种方式,比如 min ui、element ui 和 iview 等。一般都作为一个 mixins 去使用, broadcast 是向特定的父组件,触发事件,dispatch 是向特定的子组件触发事件,本质上这种方式还是 on 和 emit 的封装,但在一些基础组件中却很实用

组件间传值,attrs 和 listeners

listeners 的作用:解决多层嵌套情况下,父组件A 下面有 子组件B,组件B 下面有 组件C,组件A 传递数据给 组件B 的问题,这个方法是在 Vue 2.4 提出的。

listeners 解决问题的过程:

C组件

1
2
3
4
5
6
7
8
9
10
11
12
13
Vue.component('C',{ 
template:`
<div>
<input type="text" v-model="$attrs.messageC" @input="passCData($attrs.messageC)">
</div>
`,
methods:{
passCData(val){
//触发父组件A中的事件
this.$emit('getCData',val)
}
}
})

B组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Vue.component('B',{ 
data(){
return {
myMessage:this.message
}
},
template:`
<div>
<input type="text" v-model="myMessage" @input="passData(myMessage)">
<C v-bind="$attrs" v-on="$listeners"></C>
</div>
`,
//得到父组件传递过来的数据
props:['message'],
methods:{
passData(val){
//触发父组件中的事件
this.$emit('getChildData',val)
}
}
})

A组件

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
Vue.component('A',{ 
template:`
<div>
<p>this is parent compoent!</p>
<B
:messageC="messageC"
:message="message"
v-on:getCData="getCData"
v-on:getChildData="getChildData(message)">
</B>
</div>
`,
data(){
return {
message:'Hello',
messageC:'Hello c'
}
},
methods:{
getChildData(val){
console.log('这是来自B组件的数据')
},
//执行C子组件触发的事件
getCData(val){
console.log("这是来自C组件的数据:"+val)
}
}
})
var app=new Vue({
el:'#app',
template:`
<div>
<A></A>
</div>
`
})

解析:

  • C组件 中能直接触发 getCData 的原因在于 B组件 调用 C组件 时使用 v-on 绑定了 $listeners 属性
  • 通过 v-bind 绑定 $attrs 属性,C组件 可以直接获取到 A组件 中传递下来的 props(除了B组件中props声明的)

组件间传值,中央事件总线 是怎么用的

中央事件总线主要用来解决兄弟组件通信的问题。

实现方式:新建一个 Vue 事件 bus 对象,然后通过 bus.on 监听触发的事件。

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
42
43
44
45
46
47
48
49
50
Vue.component('brother1',{
data(){
return {
myMessage:'Hello brother1'
}
},
template:`
<div>
<p>this is brother1 compoent!</p>
<input type="text" v-model="myMessage" @input="passData(myMessage)">
</div>
`,
methods:{
passData(val){
//触发全局事件globalEvent
bus.$emit('globalEvent',val)
}
}
})
Vue.component('brother2',{
template:`
<div>
<p>this is brother2 compoent!</p>
<p>brother1传递过来的数据:{{brothermessage}}</p>
</div>
`,
data(){
return {
myMessage:'Hello brother2',
brothermessage:''
}
},
mounted(){
//绑定全局事件globalEvent
bus.$on('globalEvent',(val)=>{
this.brothermessage=val;
})
}
})
//中央事件总线
var bus=new Vue();
var app=new Vue({
el:'#app',
template:`
<div>
<brother1></brother1>
<brother2></brother2>
</div>
`
})

refs、$parent 的使用

$root: 可以用来获取 Vue 的根实例,比如在简单的项目中将公共数据放再 Vue 根实例上(可以理解为一个全局 store ),因此可以代替 vuex 实现状态管理;

$refs: 在子组件上使用 ref 特性后,this.属性 可以直接访问该子组件。可以代替事件 emitrefs.testId 获取指定元素。

$parent: $parent 属性可以用来从一个子组件访问父组件的实例,可以替代将数据以 prop 的方式传入子组件的方式;当变更父级组件的数据的时候,容易造成调试和理解难度增加;

怎样理解 Vue 的单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个 单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

有两种常见的试图改变一个 prop 的情形 :

  • prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
        props: ['initialCounter'],
    data: function () {
    return {
    counter: this.initialCounter
    }
    }
    ```

    * **prop 以一种原始的值传入且需要进行转换。** 在这种情况下,最好使用这个 prop 的值来定义一个计算属性

    props: [‘size’],
    computed: {
    normalizedSize: function () {
    return this.size.trim().toLowerCase()
    }
    }

    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
    42
    43
    44
    45
    46
    47
    48
    49


    # Vuex

    ## Vuex 是什么,每个属性是干嘛的,如何使用

    Vuex 是一个专为 Vue 开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。store 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。

    * Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    * 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

    ![](https://uploadfiles.nowcoder.com/images/20220301/4107856_1646128565972/EB5115B586566907B3B642BA58A4482A)

    主要包括以下几个模块:

    * `State`:定义了应用状态的数据结构,可以在这里设置默认的初始状态。

    * `Getter`:getters 是 store 的计算属性,对 state 的加工,是派生出来的数据。就像 computed 计算属性一样,getter 返回的值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变才会被重新计算。
    mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。

    * `Mutation`:mutations 提交更改数据,使用 `store.commit` 方法更改 state 存储的状态。是唯一更改 store 中状态的方法,且必须是同步函数。

    * `Action`:actions 像一个装饰器,提交 mutation,而不是直接变更状态。(actions 可以包含任何异步操作)。

    * `Module`:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。Module 是 store 分割的模块,每个模块拥有自己的 state、getters、mutations、actions。

    ```js
    const moduleA = {
    state: { ... },
    mutations: { ... },
    actions: { ... },
    getters: { ... }
    };

    const moduleB = {
    state: { ... },
    mutations: { ... },
    actions: { ... }
    };

    const store = new Vuex.Store({
    modules: {
    a: moduleA,
    b: moduleB
    }
    });

    store.state.a; // -> moduleA 的状态
    store.state.b; // -> moduleB 的状态
  • 辅助函数。Vuex 提供了 mapState、mapGetters、mapActions、mapMutations 等辅助函数给开发在 vm 中处理 store。

Vuex 的使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vuex from 'vuex';
Vue.use(Vuex); // 1. vue的插件机制,安装vuex
let store = new Vuex.Store({ // 2.实例化store,调用install方法
state,
getters,
modules,
mutations,
actions,
plugins
});
new Vue({ // 3.注入store, 挂载vue实例
store,
render: h=>h(app)
}).$mount('#app');

Vuex 实现原理

可以通过以下三个方面来阐述 vuex 的实现原理:

  • store 是怎么注册的?
  • mutation,commit 是怎么实现的?
  • 辅助函数是怎么实现的?
  1. store 是怎么注册的?

    Vuex 在 vue 的生命周期中的初始化钩子前插入一段 Vuex 初始化代码。给 Vue 的实例注入一个 $store 的属性,这也就是为什么我们在 Vue 的组件中可以通过 this.$store.xxx, 访问到 Vuex 的各种数据和状态

    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
    export default function (Vue) {
    // 获取当前 Vue 的版本
    const version = Number(Vue.version.split('.')[0]);

    if (version >= 2) {
    // 2.x 通过 hook 的方式注入
    Vue.mixin({ beforeCreate: vuexInit })
    } else {
    // 兼容 1.x
    // 使用自定义的 _init 方法并替换 Vue 对象原型的_init方法,实现注入
    const _init = Vue.prototype._init;
    Vue.prototype._init = function (options = {}) {
    options.init = options.init
    ? [vuexInit].concat(options.init)
    : vuexInit;
    _init.call(this, options)
    }
    }

    /**
    * Vuex init hook, injected into each instances init hooks list.
    */

    function vuexInit () {
    const options = this.$options;
    // store 注入
    if (options.store) {
    this.$store = typeof options.store === 'function'
    ? options.store()
    : options.store
    } else if (options.parent && options.parent.$store) {
    // 子组件从其父组件引用 $store 属性
    this.$store = options.parent.$store
    }
    }
    }
  2. mutations,commit 是怎么实现的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function registerMutation (store, type, handler, local) {
    // 获取 type(module.mutations 的 key) 对应的 mutations, 没有就创建一个空数组
    const entry = store._mutations[type] || (store._mutations[type] = []);
    // push 处理过的 mutation handler
    entry.push(function wrappedMutationHandler (payload) {
    // 调用用户定义的 hanler, 并传入 state 和 payload 参数
    handler.call(store, local.state, payload)
    })
    }

    registerMutation 是对 store 的 mutation 的初始化,它接受 4 个参数,store 为当前 Store 实例,type 为 mutation 的 key,handler 为 mutation 执行的回调函数,path 为当前模块的路径。

    mutation 的作用就是同步修改当前模块的 state,函数首先通过 type 拿到对应的 mutation 对象数组,然后把一个 mutation 的包装函数 push 到这个数组中,这个函数接收一个参数 payload,这个就是我们在定义 mutation 的时候接收的额外参数。这个函数执行的时候会调用 mutation 的回调函数,并通过 getNestedState(store.state, path) 方法得到当前模块的 state,和 playload 一起作为回调函数的参数。

    我们知道 mutation 是通过 commit 来触发的,这里我们也来看一下 commit 的定义

    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
    commit (_type, _payload, _options) {
    // 解析参数
    const {
    type,
    payload,
    options
    } = unifyObjectStyle(_type, _payload, _options)

    // 根据 type 获取所有对应的处理过的 mutation 函数集合
    const mutation = { type, payload }
    const entry = this._mutations[type]
    if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
    console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
    }
    // 执行 mutation 函数
    this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
    handler(payload)
    })
    })

    // 执行所有的订阅者函数
    this._subscribers.forEach(sub => sub(mutation, this.state))

    if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
    ) {
    console.warn(
    `[vuex] mutation type: ${type}. Silent option has been removed. ` +
    'Use the filter functionality in the vue-devtools'
    )
    }
    }

    commit 支持 3 个参数,type 表示 mutation 的类型,payload 表示额外的参数,根据 type 去查找对应的 mutation,如果找不到,则输出一条错误信息,否则遍历这个 type 对应的 mutation 对象数组,执行 handler(payload) 方法,这个方法就是之前定义的 wrappedMutationHandler(handler),执行它就相当于执行了 registerMutation 注册的回调函数。

  3. 辅助函数

    辅助函数的实现都差不太多,在这里了解一下 mapState

    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
    export const mapGetters = normalizeNamespace((namespace, getters) => {
    // 返回结果
    const res = {}

    // 遍历规范化参数后的对象
    // getters 就是传递给 mapGetters 的 map 对象或者数组
    normalizeMap(getters).forEach(({ key, val }) => {
    val = namespace + val
    res[key] = function mappedGetter () {
    // 一般不会传入 namespace 参数
    if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
    return
    }
    // 如果 getter 不存在则报错
    if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
    console.error(`[vuex] unknown getter: ${val}`)
    return
    }
    // 返回 getter 值, store.getters 可见上文 resetStoreVM 的分析
    return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
    })
    return res
    })

    mapState 在调用了 normalizeMap 函数后,把传入的 states 转换成由 {key, val} 对象构成的数组,接着调用 forEach 方法遍历这个数组,构造一个新的对象,这个新对象每个元素都返回一个新的函数 mappedState,函数对 val 的类型判断,如果 val 是一个函数,则直接调用这个 val 函数,把当前 store 上的 state 和 getters 作为参数,返回值作为 mappedState 的返回值;否则直接把 this.$store.state[val] 作为 mappedState 的返回值。为了更直观的理解,我们看下最终 mapState 的效果

    1
    2
    3
    4
    5
    6
    7
    computed: mapState({
    name: state => state.name,
    })
    // 等同于
    computed: {
    name: this.$store.state.name
    }

Vuex 和单纯的全局对象有什么区别

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

mutation 和 action 有什么区别?

mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数

1
2
3
4
5
6
7
8
9
10
11
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})

不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:

1
store.commit('increment')

Action: Action 类似于 mutation,不同在于:

  1. Action 提交的是 mutation,而不是直接变更状态。
  2. Action 可以包含任意异步操作。

让我们来注册一个简单的 action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})

扩展: 事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutation 就行。异步怎么处理那是用户自己的事情。

vuex 真正限制你的只有 mutation 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)。同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。


03-面试之前端 Vue 组件传值
https://flepeng.github.io/interview-20-开发语言类-21-frontend-03-Vue-03-面试之前端-Vue-组件传值/
作者
Lepeng
发布于
2023年8月8日
许可协议