團隊使用react hooks差不多有半年了,回顧這半年,看着團隊一點點的生產的一個個hook,讓筆者想起了那個react剛剛橫空出世的年代。
應該是在2016年的時候,筆者的團隊還在使用以backbone為核心的前端架構,每一個新的組件,前端都需要花費大量的精力在建立數據與視圖的關系以及用戶輸入與數據的關系上,那是一種類似無定向數據流的模型。但隨着業務場景的日益復雜,復雜的數據流向給前端應用的維護帶來了極大的挑戰,組件狀態依賴於紛繁復雜的數據流的計算結果。。
而正當筆者的團隊在糾結該如何解決一個又一個這樣的問題時。。大洋彼岸,一個以“單向數據流”為特點的庫開始悄然崛起,它就是——React。
相較於react的其他特性,單向數據流,無疑是最吸引筆者的特性。這種數據流思想引入之后,開發者再也無須考慮復雜的數據流向,只需要統一對來自組件上層或組件自身的數據做出處理即可,極大的提高了數據處理的效率,再加上jsx、virtual dom等特性的加持,react順應時代的潮流成為了最流行的前端庫。
不過,沒什么方案是完美的,react也一樣,但令筆者始料不及的是,它會以這樣一種方式展現在筆者眼前。。
import React, { Component } from "react";
export default class App extends Component{
constructor(props) {
super(props);
this.state = { a: 1 };
}
componentDidMount() {
this.setState({ a: 2 }, () => {
this.state.a = 3;
})
}
render() {
return <div>{this.state.a}</div>
}
}
其實,了解React機制的同學都知道,上段代碼中標紅的部分其實是React本身不推薦的寫法,如果項目中有使用eslint,還會提示【Do not mutate state directly】,是一種極不安全的寫法(聰明的你不妨也想想最后會渲染出多少),這是React基於js實現所做出的讓步,本身是一種無奈之舉。。但是實際操作時,卻會產生遠超我們想象的副作用:
componentDidMount() { this.setState({ a: 2 }, () => { // eslint-disable-next-line this.state.a = 3; setTimeout(() => { this.setState({a: this.state.a}); }, 1000); }) }
為了規避eslint的檢測,就有了直接禁用下一行eslint的操作,而為了讓state設置生效,則又刻意避開batchupdate的setState。。
由於近年來前端整個行業的迅速發展特性,很多從業者的團隊基本上都是被半捆綁式的硬上React的,他們有的精通jQuery,有的精通angular,有的精通backbone,但卻對React知之甚少。。於是,最終就有了上文那啼笑皆非的局面。
但是,如果拋開那些外因不說,單單從工程化的層面來看待這個問題,卻又有一個問題擺在筆者面前:React這樣的設計是不是不太合理?
而命運就是這樣么的湊巧,雖然React團隊可能並不是專門想解決這個問題,不過隨着hooks的到來,新的方式在書寫上就直接提示你要使用setter而不是value去進行賦值操作,如下代碼示意:
import React, { useState, useEffect } from "react"; export default function App() { const [state, setState] = useState(1); useEffect(() => { setState(2); }, [state]); return <div>{state}</div>; }
得益於新的寫法建議是state value而不是整個state object,同時顯式的setState暴露,也間接避免了這個問題。不過,筆者還沒高興多久,新的問題又產生了,而且,這次的現象,甚至有些讓人懷疑自己學習的js是否正確:
import React, { useState, useEffect } from "react"; export default function App() { const [state, setState] = useState(1); const add = () => setInterval(() => setState(state + 1), 1000); useEffect(add, []); return <div>{state}</div>; // state always be 2 }
如上述代碼所示,在"didmount"之后,會以1秒為周期,對state進行自增,但是實際情況是,除了第一次自增(為2)有效之外,這個setInterval仿佛變成了setTimeout一樣,只執行了一次?正常的理解是,每次執行的時候,add引用外部的state進行自增,然后進行復制。但是,在當前的React中,因為存在capture value的特性(官方給出的解釋是:This prevents bugs caused by the code assuming props and state don’t change)所以,變成了每次執行時,add會生成一個新的閉包,而每個閉包都引用自己生成時的變量,而它們都是在state為1時生成的,也就導致了state的值,永遠都在重復由1變到2的過程,所以,才有了視覺上,只進行了一次interval的行為。
另外,因為function compoent會產生大量的調用,add通常還會被寫作形如
const add = useCallback(() => setInterval(() => setState(state + 1), 1000),[count]);
這樣做的好處是避免重復生成新的add實例,其實就是閉包或者函數的復用,在這個場景下其實更容易觸發Capture Value的機制。
那么,應該如何解決這種問題呢?
官方給出了一套方案是使用ref,對,在hooks中,ref的場景得到了增強:
import React, { useState, useEffect, useCallback, useRef } from "react"; export default function App() { const [state, setState] = useState(1); const countRef = useRef(null); const add = useCallback( () => setInterval(() => { if (!countRef.current) { countRef.current = 1; } else { countRef.current += 1; } setState(countRef.current); }, 1000), [] ); useEffect(add, []); return <div>{state}</div>; }
其實,就是官方為ref建立了一個能夠實時獲取更新的引用關系,讓它能夠實時的拿到引用的值,而不受capture value的限制。不過,不管怎么說,在易用性上,react此次的進化依然顯得不是那么令人滿意,雖然hooks提高了對業務邏輯復用復用性,但是原子化的state帶來的大量模板代碼,循環中的hooks的各種限制,還有這個capture value。。不得不說,hooks的前方路漫漫其修遠兮。
扯得有點遠,其實筆者里例子有很簡單的實現方式,都不需要用到interval:
import React, { useState, useEffect } from "react"; export default function App() { const [state, setState] = useState(1); useEffect(() => { setTimeout(() => setState(state + 1), 1000); }, [state]); return <div>{state}</div>; }
但並不是所有的業務場景都可以這樣避免。。說了這么多不太理想的地方,還是再聊聊好的部分,hooks的出現,很多場景就都能夠進行抽象和二次復用了:
const searchParam = useSearchParam("qs"); const data = useAsync(async () => { const res = await ajax(searchParam); return res; }); return <div>{data.loading ? "loading" : data.value}</div>;
如上述代碼所示,其實這兩個hook的實現都很簡單,但是一個實現了指定url參數的獲取,一個實現了對ajax過程的抽象,不再需要書寫冗余url監聽與組件loading狀態,直接由hook提供,直接實現了view與viewmodel的解耦,用戶只需要在自己的場景中實現loading組件即可。而正是這些細粒度的抽象,進一步切實的簡化了開發者的重復工作。
這一邊是React苦心孤詣的打造了函數式組件生態,而另一邊,Vue也在最近迎來了一次大的更新,隨着新版的vue將內部子模塊拆得越來越細,特別是@vue/reactivity這個包的獨立,提供給了筆者一個很有意思的視角:是否可以用拆分出的vue的數據綁定能力來操作react進行dom渲染呢?於是說干就干:
import React, { createContext, useContext, useEffect, useReducer, useRef } from "react"; import { effect, stop, reactive } from "@vue/reactivity";
// hook for update const useEffection = (...effectArgs) => { const effectionRef = useRef(); if (!effectionRef.current) { effectionRef.current = effect(...effectArgs); } const stopEffect = () => { stop(effectionRef.current); }; useEffect(() => stopEffect, []); return effectionRef.current; }; const useStore = selector => { const [, forceUpdate] = useReducer(s => s + 1, 0); const store = useContext(StoreContext); const effection = useEffection(() => selector(store), { scheduler: forceUpdate, lazy: true }); return effection(); };
// create reactive state const state = reactive({ count: 0 }); const increment = () => state.count++; const store = { state };
// react context const StoreContext = createContext(null); const Provider = StoreContext.Provider;
// function component function Count() { const countState = useStore(store => { const { state } = store; const { count } = state; return { count }; }); const { count } = countState; return ( <> <span style={{ marginRight: 10 }}>{count}</span> <span onClick={increment}>add</span> </> ); } export default function ComponentA() { return ( <> <Provider value={store}> <Count /> </Provider> </> ); }
使用reactivity來進行數據關聯之后,配上點context,目前雖然繞了點彎路,還是實現了一個簡單的計數器,以后隨着vue的發展可能有更好的實現也說不定。
其實回過頭來看,Hooks解決了Class時代抽象能力不足的同時,一些新特性的引入也給整個開發體驗蒙上了一層陰影(誠然,本文本身切入點是有些問題的,不過筆者覺得,這依然是一個健康的生態需要考慮的問題)。當然,FB團隊也並沒有停止去思考更好的解決方案,(雖然並不是為了解決筆者所說的一些“缺陷”,而是着眼於整體方案的設計),Recoil就是他們給出的一個基於狀態管理器的方案,基於Recoil,筆者又一次實現了一個Counter:
import React from "react"; import { RecoilRoot, atom, useRecoilState, useRecoilValue } from "recoil"; const countState = atom({ key: "countState", default: 0 }); function Counter() { const count = useRecoilValue(countState); return <>Count: {count}</>; } function Add() { const [count, setCount] = useRecoilState(countState); return <p onClick={() => setCount(count + 1)}>add</p>; } export default function App() { return ( <RecoilRoot> <Counter /> <Add /> </RecoilRoot> ); }
其實光看代碼就能發現,和reactivity很像,並且省去了很多抹平react&vue差異的代碼(...其實是他自集成了) ,同時,也省去了redux那紛繁復雜的大量模板代碼,相比rematch也更為精簡,還有人戲稱是一個hooks時代的redux。不過在筆者看來,雖然Recoil解決了很多在redux時代頗為詬病的問題: 跨組件狀態共享、狀態互相依賴、異步狀態等等,但就目前看來比較原子化的api,很難支撐起一個工程化的多人協作場景的,(此刻可能會有很多同學已經迫不及待的喊出那句“mobx它不香嗎?”)需要有一些更多的規范和相關設施的建設,才能更有效的提高或者說將整個生態帶往更好的未來。
但這又何嘗不是這個時代給我們的希望呢?新的事物不斷涌現,也許它還存在的許許多多的問題,但是,正是因為我們對它們一次又一次的審視、思考,才能引領我們走向更光明的未來。