後jQuery時代的前端革命是由AngularJs發起的,它最初的一個想法是將後台的技術架構複製到前台來。後端的一個核心技術是所謂的模板技術(template)。它可以用一個公式來描述

html = template(vars)

這是一個特別直觀的想法:模板就是一個普通函數,它根據傳入的變數信息(無特殊要求)拼接得到字元串(無特殊結構)。這一模型完全不需要考慮面向對象傳統的狀態分散管理的問題,基本上是一種函數式的解決方案。

React的模式相當於是對模板渲染模型的一個面向領域結構的改進

vdom = render(viewModel)

vdom是面向瀏覽器的領域模型,而viewModel是基於業務領域概念所建立的頁面顯示模型,render相當於是兩個模型世界之間的傳送門。

傳統上我們編程時是必須要知道界面模型的。我們所依賴的基礎設施是瀏覽器內置的DOM結構和事件bubble機制,總是監聽在DOM節點上,總是拿到具體的控制項,然後從控制項上拉取我們所需要的數據。而在React的模式下,我們首先在JS中建立模型,這個模型包含具體的領域知識,在領域內部的操作是更加直接的,而且可以利用程序語言所提供的各種抽象手段。典型的,在jQuery時代我們需要頻繁的使用$el.find(".title")這種形式去動態查找到所需的元素,而在js模型中我們一般通過this.title屬性即可直接定位到所需要的數據。實際上我們對於弱耦合的事件機制的依賴是大大下降了的,特別是我們一般不再需要業務含義不明確的事件bubble處理。redux和vuex從某種意義上可以看作是面向領域的消息匯流排,它們一般都是直接派發到具體的監聽器,而且這些監聽器的入口函數不再是某種通用的、與業務無關的Event對象,而是具體的領域狀態對象state和業務參數param。

在新的範式下,viewModel的構造和管理成為一個獨立的問題。而界面組件之間也不再直接交互,它們之間的關聯通過共同依賴的js對象來得到隱式的表達。

control <--> js <--> control

如果我們改寫一下形式,可以把React的本質看得更清楚一些:

viewModel => vdom

render函數可以看作是從viewModel上拉取領域數據,傳送到vdom世界的一種信息管道。

但是我們知道,前端與後端有一個本質性的不同:前端是講究交互性的,而後端強調的只是單向執行。因此,我們需要一個新的概念reactive,利用這個概念可以把上面的公式改寫為

(props, @reactive state) => vdom

render函數是一種好不容易建立起來的信息管道,如果使用一次就隨手丟棄,那實在是太浪費了,何不反覆利用?通過引入具備響應性的狀態變數,規定一個全局的響應式規則:「無論什麼原因導致state變化,自動觸發局部的render函數重新執行」,就可以使得render函數得到成功的升華,完美的將微觀的交互性嵌入到了宏觀的信息流場景中。

React兜兜轉轉很多年,一直沒有能夠找到最契合以上公式的技術表達形式,其本質原因還是在於受到了面向對象思想的束縛,總是意圖帶著面向對象的尾巴。直到Hooks機制橫空出世,徹底和歷史決裂,我們才看到了React本來就應該具有的面目:

import React, { useState, useEffect } from react;

function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);

useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}

ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);

// 返回一個函數來進行額外的清理工作:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

if (isOnline === null) {
return Loading...;
}
return isOnline ? Online : Offline;
}

為什麼Hooks需要限制只能在代碼的第一層調用 Hooks,不能在循環、條件分支或者嵌套函數中調用 Hooks?因為本來它應該寫在參數區的,只是因為語法的限制導致它沒有專有的位置而已。

現代框架技術的發展仔細回顧起來,其實可以看作是對傳統面向對象封裝概念的反叛史。面向對象強調先有對象,再有屬性和方法,做事之前先拿到this。而現代框架強調的是全局規則,直接表達,為什麼無論幹什麼事都要找個this指針繞一下呢?對比一下React此前的類組件

class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}

componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}

render() {
if (this.state.isOnline === null) {
return Loading...;
}
return this.state.isOnline ? Online : Offline;
}
}

真正的核心函數是render, 其他的都是外圍支持性函數,這些函數之間通過this指針間接進行交互。仔細琢磨一下,我們不禁會有個疑問,所謂的生命周期函數為什麼要從屬於組件對象,它是局限於某個對象的知識嗎?難道它的觸發時刻不是一種全局知識嗎?useEffect函數深刻理解了這一點,它成為一個靜態函數,直接鉤掛到全局執行引擎中,通過函數閉包直接實現多個生命周期回調函數之間的信息傳遞,而不是必須要造出某個this指針來隨身攜帶。

長期以來,面向對象語言中存在三種標準的信息傳遞方式,參數(param)、全局變數(global)和成員變數(this),但是當面對複雜的領域模型時,我們經常需要表達某個局部範圍內的隱含的背景知識,這是一種自定義的、與領域緊密相關的上下文變數(context),不應該顯式傳遞。因此,數據驅動的核心公式可以被改進為

(props, @reactive state, @implicit context) => vdom

React Hooks中為implicit context這個概念也找到了一個對應的技術形式,把上下文定位方式確定為根據類型進行查找,相當於是某種import implicit機制。

const user = useContext(CurrentUser);
const notifications = useContext(Notifications);

React Hooks機制的出現意味著面向對象組件會衰落下去嗎?我想也不盡然。傳統的力量是強大的,而有生命力的文化總是具有包容性的。我們在Hooks概念之前在Vue技術體系中就已經通過元編程大法解決了相應問題:在編譯期聲明在一起的代碼塊,可以通過元編程機制拆分後掛接到組件對象上的各種插槽上。作為一種運行時,面向對象完全沒有任何問題。

推薦閱讀:

相关文章