解密傳統元件間通訊與React元件間通訊
在React中最小的邏輯單元是元件,元件之間如果有耦合關係就會進行通訊,本文將會介紹React中的元件通訊的不同方式
通過歸納範,可以將任意元件間的通訊歸類為四種類型的元件間通訊,分別是父子元件,爺孫元件,兄弟元件和任意元件,需要注意的是前三個也可以算作任意元件的範疇,所以最後一個是萬能方法
父子元件
父子元件間的通訊分為父元件向子元件通訊和子元件向父元件通訊兩種情況,下面先來介紹父元件向子元件通訊,傳統做法分為兩種情況,分別是初始化時的引數傳遞和例項階段的方法呼叫,例子如下
class Child { constructor(name) { // 獲取dom引用 this.$div = document.querySelector('#wp'); // 初始化時傳入name this.updateName(name); } updateName(name) { // 對外提供更新的api this.name = name; // 更新dom this.$div.innerHTML = name; } } class Parent { constructor() { // 初始化階段 this.child = new Child('yan'); setTimeout(() => { // 例項化階段 this.child.updateName('hou'); }, 2000); } }
在React中將兩個情況統一處理,全部通過屬性來完成,之所以能夠這樣是因為React在屬性更新時會自動重新渲染子元件,下面的例子中,2秒後子元件會自動重新渲染,並獲取新的屬性值
class Child extends Component { render() { return <div>{this.props.name}</div> } } class Parent extends Component { constructor() { // 初始化階段 this.state = {name: 'yan'}; setTimeout(() => { // 例項化階段 this.setState({name: 'hou'}) }, 2000); } render() { return <Child name={this.state.name} /> } }
下面來看一下子元件如何向父元件通訊,傳統做法有兩種,一種是回撥函式,另一種是為子元件部署訊息介面
先來看回調函式的例子,回撥函式的優點是非常簡單,缺點就是必須在初始化的時候傳入,並且不可撤回,並且只能傳入一個函式
class Child { constructor(cb) { // 呼叫父元件傳入的回撥函式,傳送訊息 setTimeout(() => { cb() }, 2000); } } class Parent { constructor() { // 初始化階段,傳入回撥函式 this.child = new Child(function () { console.log('child update') }); } }
下面來看看訊息介面方法,首先需要一個可以釋出和訂閱訊息的基類,比如下面實現了一個簡單的EventEimtter
,實際生產中可以直接使用別人寫好的類庫,比如@jsmini/event,子元件繼承訊息基類,就有了釋出訊息的能力,然後父元件訂閱子元件的訊息,即可實現子元件向父元件通訊的功能
訊息介面的優點就是可以隨處訂閱,並且可以多次訂閱,還可以取消訂閱,缺點是略顯麻煩,需要引入訊息基類
// 訊息介面,訂閱釋出模式,類似繫結事件,觸發事件
class EventEimtter {
constructor() {
this.eventMap = {};
}
sub(name, cb) {
const eventList = this.eventMap[name] = this.eventMap[name] || {};
eventList.push(cb);
}
pub(name, ...data) {
(this.eventMap[name] || []).forEach(cb => cb(...data));
}
}
class Child extends EventEimtter {
constructor() {
super();
// 通過訊息介面釋出訊息
setTimeout(() => { this.pub('update') }, 2000);
}
}
class Parent {
constructor() {
// 初始化階段,傳入回撥函式
this.child = new Child();
// 訂閱子元件的訊息
this.child.sub('update', function () {
console.log('child update')
});
}
}
Backbone.js就同時支援回撥函式和訊息介面方式,但React中選擇了比較簡單的回撥函式模式,下面來看一下React的例子
class Child extends Component {
constructor(props) {
setTimeout(() => { this.props.cb() }, 2000);
}
render() {
return <div></div>
}
}
class Parent extends Component {
render() {
return <Child cb={() => {console.log('update')}} />
}
}
爺孫元件
父子元件其實可以算是爺孫元件的一種特例,這裡的爺孫元件不光指爺爺和孫子,而是泛指祖先與後代元件通訊,可能隔著很多層級,我們已經解決了父子元件通訊的問題,根據化歸法,很容易得出爺孫元件的答案,那就是層層傳遞屬性麼,把爺孫元件通訊分解為多個父子元件通訊的問題
層層傳遞的優點是非常簡單,用已有知識就能解決,問題是會浪費很多程式碼,非常繁瑣,中間作為橋樑的元件會引入很多不屬於自己的屬性
在React中,通過context可以讓祖先元件直接把屬性傳遞到後代元件,有點類似星際旅行中的蟲洞一樣,通過context這個特殊的橋樑,可以跨越任意層次向後代元件傳遞訊息
怎麼在需要通訊的元件之間開啟這個蟲洞呢?需要雙向宣告,也就是在祖先元件宣告屬性,並在後代元件上再次宣告屬性,然後在祖先元件上放上屬性就可以了,就可以在後代元件讀取屬性了,下面看一個例子
import PropTypes from 'prop-types';
class Child extends Component {
// 後代元件宣告需要讀取context上的資料
static contextTypes = {
text: PropTypes.string
}
render() {
// 通過this.context 讀取context上的資料
return <div>{this.context.text}</div>
}
}
class Ancestor extends Component {
// 祖先元件宣告需要放入context上的資料
static childContextTypes = {
text: PropTypes.string
}
// 祖先元件往context放入資料
getChildContext() {
return {text: 'yanhaijing'}
}
}
context的優點是可以省去層層傳遞的麻煩,並且通過雙向宣告控制了資料的可見性,對於層數很多時,不失為一種方案;但缺點也很明顯,就像全域性變數一樣,如果不加節制很容易造成混亂,而且也容易出現重名覆蓋的問題
個人的建議是對一些所有元件共享的只讀資訊可以採用context來傳遞,比如登入的使用者資訊等
小貼士:React Router路由就是通過context來傳遞路由屬性的
兄弟元件
如果兩個元件是兄弟關係,可以通過父元件作為橋樑,來讓兩個元件之間通訊,這其實就是主模組模式
下面的例子中,兩個子元件通過父元件來實現顯示數字同步的功能
class Parent extends Component {
constructor() {
this.onChange = function (num) {
this.setState({num})
}.bind(this);
}
render() {
return (
<div>
<Child1 num={this.state.num} onChange={this.onChange}>
<Child2 num={this.state.num} onChange={this.onChange}>
</div>
);
}
}
主模組模式的優點就是解耦,把兩個子元件之間的耦合關係,解耦成子元件和父元件之間的耦合,把分散的東西收集在一起好處非常明顯,能帶來更好的可維護性和可擴充套件性
任意元件
任意元件包括上面的三種關係元件,上面三種關係應該優先使用上面介紹的方法,對於任意的兩個元件間通訊,總共有三種辦法,分別是共同祖先法,訊息中介軟體和狀態管理
基於我們上面介紹的爺孫元件和兄弟元件,只要找到兩個元件的共同祖先,就可以將任意元件之間的通訊,轉化為任意元件和共同祖先之間的通訊,這個方法的好處就是非常簡單,已知知識就能搞定,缺點就是上面兩種模式缺點的疊加,除了臨時方案,不建議使用這種方法
另一種比較常用的方法是訊息中介軟體,就是引入一個全域性訊息工具,兩個元件通過這個全域性工具進行通訊,這樣兩個元件間的通訊,就通過全域性訊息媒介完成了
還記得上面介紹的訊息基類嗎?下面的例子中,元件1和元件2通過全域性event進行通訊
class EventEimtter {
constructor() {
this.eventMap = {};
}
sub(name, cb) {
const eventList = this.eventMap[name] = this.eventMap[name] || {};
eventList.push(cb);
}
pub(name, ...data) {
(this.eventMap[name] || []).forEach(cb => cb(...data));
}
}
// 全域性訊息工具
const event = new EventEimtter;
// 一個元件
class Element1 extends Component {
constructor() {
// 訂閱訊息
event.sub('element2update', () => {console.log('element2 update')});
}
}
// 另一個元件。
class Element2 extends Component {
constructor() {
// 釋出訊息
setTimeout(function () { event.pub('element2update') }, 2000)
}
}
訊息中介軟體的模式非常簡單,利用了觀察者模式,將兩個元件之間的耦合解耦成了元件和訊息中心+訊息名稱的耦合,但為了解耦卻引入全域性訊息中心和訊息名稱,訊息中心對元件的侵入性很強,和第三方元件通訊不能使用這種方式
小型專案比較適合使用這種方式,但隨著專案規模的擴大,達到中等專案以後,訊息名字爆炸式增長,訊息名字的維護成了棘手的問題,重名概率極大,沒有人敢隨便刪除訊息資訊,訊息的釋出者找不到訊息訂閱者的資訊等
其實上面的問題也不是沒有解決辦法,重名的問題可以通過制定規範,訊息名稱空間等方式來極大降低衝突,其他問題可以通過把訊息名字統一維護到一個檔案,通過對訊息的中心化管理,可以讓很多問題都很容易解決
如果你的專案非常大,上面兩種方案都不合適,那你可能需要一個狀態管理工具,通過狀態管理工具把元件之間的關係,和關係的處理邏輯從組建中抽象出來,並集中化到統一的地方來處理,Redux就是一個非常不錯的狀態管理工具
除了Redux,還有Mobx,Rematch,reselect等工具,本文不展開介紹,有機會後面單獨成文,這些都是用來解決不同問題的,只要根據自己的場景選擇合適的工具就好了
總結
元件間的關係千變萬化,都可以用上面介紹的方法解決,對於不同規模的專案,應該選擇適合自己的技術方案,上面介紹的不同方式解耦的程度是不一樣的,關於不同耦合關係的好壞,可以看我之前的文章《圖解7種耦合關係》
本文節選自我的新書《React 狀態管理與同構實戰》,感興趣的同學可以繼續閱讀本書,這本書由我和前端自身技術侯策合力打磨,凝結了我們在學習、實踐 React 框架過程中的積累和心得。除了 React 框架使用介紹以外,著重剖析了狀態管理以及服務端渲染同構應用方面的內容。同時吸取了社群大量優秀思想,進行歸納比對。
本書受到百度公司副總裁沈抖、百度高階前端工程師董睿,以及知名JavaScript語言專家阮一峰、Node.js佈道者狼叔、Flarum中文社群創始人 justjavac、新浪移動前端技術專家小爝、知乎知名博主顧軼靈等前端圈眾多專家大咖的聯合力薦。
有興趣的讀者可以點選下面的連結購買,再次感謝各位的支援與鼓勵!懇請各位批評指正!