vue源码学习系列之十一:组件化原理探索(父子组件通信)
youngwind opened this issue · 7 comments
前言
继 #93 之后,我们来探索如何实现Vue的父子组件通信。
在问如何实现之前,我习惯性地思考为什么。
为什么Vue、React这些MVVM框架会出现组件通信这个概念?犹记得我去年初刚刚学前端的时候,学的还是jquery,那时候还没接触组件的概念,所有的DOM都写到一块,A处的DOM和B处的DOM并没有明确的边界。如果A处的DOM想修改B处的DOM,那么一定是A处的DOM触发某个js方法,然后该js方法直接修改B处的DOM。就这样,非常原始,也非常符合js这门脚本语言的风格。
然而,前端早已非“当年吴下阿蒙”。在不断吸收传统软件开发优良**的过程中,前端已经越来越系统化,js也逐渐成为一门正规的语言。组件化的概念其实就很类似传统软件开发中的模块化(各模块之间有明确的边界),既然划分了模块,那么模块之间的通信自然就成为了要解决的问题。这就是为什么这些MVVM框架会出现组件通信这个概念。
目标
本文只解决父子组件通信,不解决兄弟组件通信。预期做到的效果如下所示。
注意:图中展示的效果总共有三层实例/组件,层层嵌套。他们之间传递“姓名”和“年龄”两个字段,又分为”向上冒泡“和”向下广播“两个方向,所以总共有2*2=4种事件。其中,在冒泡(dispatch)和广播(broadcast)的过程中,”姓名“事件的传递不会停止,而”年龄“事件在触发第一次的回调函数的时候,就会停止冒泡或广播。如果希望在首次触发回调之后继续冒泡或广播,那么须在events事件中指定return true
。这与vue的实现保持一致。
PS: 此处示例对应的代码比较长,就不在文中展示了,请直接阅读源码,或者运行项目直接debug。
思路
我们考察child组件,发现它跟一般的组件有以下两点不同:
- 多了一个events字段,里面定义了许多事件及其对应的回调方法。
- methods方法中调用了$dispatch和$broadcast,用来触发及传播事件。
先来看第一点。
事件初始化
我们需要将事件及其回调函数注册到child实例上,这样当其他组件(无论是父组件还是子组件)传来消息的时候,程序才能知道该触发哪个事件,该执行哪个回调函数。
// src/instance/events.js
/**
* 初始化事件events
* @private
*/
exports._initEvents = function () {
let options = this.$options;
registerCallbacks(this, '$on', options.events);
};
/**
* 遍历实例的所有事件
* @param vm {Bue} bue实例
* @param action {String} 动作类型,此处为'$on',代表绑定事件
* @param events {Object} 事件对象,可能包含多个事件, 所以需要遍历
*/
function registerCallbacks(vm, action, events) {
if (!events) return;
for (let key in events) {
let event = events[key];
register(vm, action, key, event);
}
}
/**
* 注册单个事件
* @param vm {Bue} bue实例
* @param action {String} 动作类型,此处为'$on',代表绑定事件
* @param key {String} 事件名称, 比如: 'parent-name',代表从父组件那里传递了名称过来
* @param event {Function} 触发key事件的时候, 对应的回调函数
*/
function register(vm, action, key, event) {
if (typeof event !== 'function') return;
vm[action](key, event);
}
// src/instance/api/events.js
/**
* 注册事件及其回调函数到实例上
* @param event {String} 事件名称
* @param fn {Function} 事件对应的回调函数
* @returns {Bue} 实例本身
*/
exports.$on = function (event, fn) {
(this._events[event] || (this._events[event] = [])).push(fn);
return this;
};
触发及传播事件
无论是$dispatch还是$broadcast,他们都有类似的步骤。
如果是向上冒泡事件
- 在当前组件实例触发($emit)事件,执行对应的回调函数。如果事件
return true
,那么代表事件可传播,执行第2步。如果事件不可传播,结束。 - 找出当前组件的父组件。没有父组件?结束。有父组件?把父组件当成当前组件,重新执行第1步。
如果是向下广播事件
- 找出当前组件的所有子组件。没有子组件?结束。有子组件?执行第2步。
- 深度遍历所有子组件,把子组件当成当前组件,触发($emit)事件,执行对应的回调函数。如果事件
return true
,那么代表事件可传播,重新执行第1步。如果事件不可传播,结束。
相关代码如下:
/**
* 在当前实例中触发指定的事件, 执行对应的回调函数
* @param event {String} 事件名称
* @param val {*} 事件所携带的参数
* @returns {boolean} true代表事件可以继续传播, false代表事件不可继续传播
*/
exports.$emit = function (event, val) {
let cbs = this._events[event];
let shouldPropagate = true;
if (cbs) {
shouldPropagate = false;
// 遍历执行事件
let args = new Array(Array.from(arguments)[1]);
cbs.forEach((cb) => {
let res = cb.apply(this, args);
// 就是这里, 决定了"只有当events事件返回true的时候, 事件才能在触发之后依然继续传播"
if (res === true) {
shouldPropagate = true;
}
});
}
return shouldPropagate;
};
/**
* 向上冒泡事件, 沿父链传播
* @param event {String} 事件的名称
* @param val {*} 事件所携带的参数
* @returns {Bue} 实例
*/
exports.$dispatch = function (event, val) {
// 在当前实例中触发该事件
let shouldPropagate = this.$emit.apply(this, arguments);
if (!shouldPropagate) return this;
let parent = this.$parent;
// 遍历父链
while (parent) {
shouldPropagate = parent.$emit.apply(parent, arguments);
parent = shouldPropagate ? parent.$parent : null;
}
return this;
};
/**
* 向下广播事件, 沿子链传播
* @param event {String} 事件的名称
* @param val {*} 事件所携带的参数
* @returns {Bue} 实例
*/
exports.$broadcast = function (event, val) {
let children = this.$children;
let shouldPropagate = true;
let args = new Array(Array.from(arguments)[0]);
children.forEach((child) => {
shouldPropagate = child.$emit.apply(child, arguments);
if (shouldPropagate) {
child.$broadcast.apply(child, arguments);
}
});
return this;
};
注意:Vue在广播事件的时候,是不会在发起广播事件的组件触发该事件的,只会从它的下一个子组件开始触发。这与冒泡事件稍有不同。
That's all。这样我们就实现了Vue父子组件之间的通信了。参考的Vue源码依然是1.0.26版本,实现的完整代码在这儿。
后话
虽然我们实现了父子组件通信,但是,兄弟节点A、B间怎么通信呢?一个比较粗糙的思路就是:兄弟A将消息发到兄弟A、B共同的父节点C,然后再经由C转发给兄弟B。但是这样做有一个弊端,当节点很多的时候,这种传递方式就会显得杂乱无章。那么,传统的计算机是如何解决众多兄弟节点(比如,CPU、内存、硬盘等等)之间的通信的呢?答案是总线机制。这么良好的**,我们前端怎么能不借鉴呢?所以就有了redux和vuex这些状态管理器。
学习了~👍
希望楼主多出些这类文章,太受益了~
看了你的文章受益良多,老铁可不可以写篇关于javascript的eventloop的博文。或者老铁可不可以帮我解惑一下关于eventloop的问题https://segmentfault.com/q/1010000008960948?_ea=1787342
嗯嗯,看了知乎上讨论的那个,明白了不少,thx @youngwind
厉害~ 关注你的博客半年了, vue的源码系列断断续续看完了,也自己实现了一套。 非常感谢!! 以后会持续关注你的博客
vue父子组件通信是基于props属性来实现的 不是基于发布订阅吧