帶你深入了解 useState
大廠技術(shù)??堅(jiān)持周更??精選好文
為什么 react 16 之前的函數(shù)組件沒有狀態(tài)?
眾所周知,函數(shù)組件在 react 16 之前是沒有狀態(tài)的,組件狀態(tài)只能通過(guò)?props?進(jìn)行傳遞。
寫兩個(gè)簡(jiǎn)單的組件,一個(gè)類組件和一個(gè)函數(shù)組件:
const App = () =><span>123span>;
class App1 extends React.Component {
?constructor(props) {
? ?super(props);
? ?this.state = {
? ? ?a: 1,
? ?}
?}
?render() {
? ?return (<p>312p>)
?}
}用 babel 編譯?App1,App1?編譯之后就是一個(gè)函數(shù)組件。
// 偽代碼
var App1 = /*#__PURE__*/function (_React$Component) {
?_inherits(App1, _React$Component);
?var _super = _createSuper(App1);
?function App1(props) {
? ?var _this;
? ?_classCallCheck(this, App1);
? ?_this = _super.call(this, props);
? ?_this.state = {
? ? ?a: 1
? ?};
? ?return _this;
?}
?_createClass(App1, [{
? ?key: "render",
? ?value: function render() {
? ? ?return/*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
? ? ? ?children: "312"
? ? ?});
? ?}
?}]);
?return App1;
}(React.Component);那為什么函數(shù)組件沒有狀態(tài)呢?函數(shù)組件和類組件的區(qū)別在于原型上是否有?render?這一方法。react 渲染時(shí),調(diào)用類組件的?render?方法。而函數(shù)組件的?render?就是函數(shù)本身,執(zhí)行完之后,內(nèi)部的變量就會(huì)被銷毀,當(dāng)組件重新渲染時(shí),無(wú)法獲取到之前的狀態(tài)。而類組件與函數(shù)組件不同,在第一次渲染時(shí),會(huì)生成一個(gè)類組件的實(shí)例,渲染調(diào)用的是 render 方法。重新渲染時(shí),會(huì)獲取到類組件的實(shí)例引用,在不同的生命周期調(diào)用類組件對(duì)應(yīng)的方法。
通過(guò)類組件和函數(shù)組件的渲染之后的數(shù)據(jù)結(jié)構(gòu)來(lái)看,兩者之間也沒有區(qū)別。
為什么 react 16 之后函數(shù)組件有狀態(tài)?
眾所周知,react 16 做的最大改動(dòng)就是 fiber。為了適配 fiber,節(jié)點(diǎn)(fiber node)的數(shù)據(jù)結(jié)構(gòu)做了很大的改動(dòng)。修改一下?App?這個(gè)組件,在頁(yè)面渲染,得到下圖的?fiber node?數(shù)據(jù)結(jié)構(gòu):
const App = () => {
?const [a, setA] = React.useState(0);
?const [b, setB] = React.useState(1);
?return<span>123span>
};
(左邊是函數(shù)組件,右邊是類組件)
react 如何知道當(dāng)前的狀態(tài)屬于哪個(gè)組件?
所有的函數(shù)組件狀態(tài)都是通過(guò) useState 進(jìn)行注入,是如何做到識(shí)別到對(duì)應(yīng)組件的呢?
在 react 的?render?流程中打個(gè)斷點(diǎn),可以看到函數(shù)組件有一個(gè)特殊的?render?方法?renderWithHooks。方法有 6 個(gè)參數(shù):current、workInProgress、component、?props、secondArg、nextRenderExpirationTime。
current: 當(dāng)前正在頁(yè)面渲染的node,如果是第一次渲染,則為空
workInProgress: 新的node,用于下一次頁(yè)面的渲染更新
component: node對(duì)應(yīng)的組件
props: 組件的props
secondArg: 不清楚...,不影響后續(xù)文章閱讀
nextRenderExpirationTime: fiber渲染的過(guò)期時(shí)間在執(zhí)行?renderWithHooks?的時(shí)候,會(huì)用變量?currentlyRenderingFiber$1?記錄當(dāng)前的?fiber node。于是在執(zhí)行函數(shù)組件的時(shí)候,useState?方法就能拿到到當(dāng)前?node?的狀態(tài)。將狀態(tài)插入到對(duì)應(yīng)?node?的?memoizedState?字段中。同時(shí)返回的觸發(fā)?state?改變的方法因?yàn)殚]包,在執(zhí)行變更時(shí),也知道是哪個(gè)?fiber node。相應(yīng)源碼:
function mountState(initialState) {
?// 獲取hook狀態(tài)
?var hook = mountWorkInProgressHook();
?if (typeof initialState === 'function') {
? ?// $FlowFixMe: Flow doesn't like mixed types
? ?initialState = initialState();
?}
?hook.memoizedState = hook.baseState = initialState;
?var queue = hook.queue = {
? ?pending: null,
? ?dispatch: null,
? ?lastRenderedReducer: basicStateReducer,
? ?lastRenderedState: initialState
?};
?// 綁定當(dāng)前node和更新隊(duì)列
?var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
?return [hook.memoizedState, dispatch];
}
renderWithHooks?只用于函數(shù)組件的渲染。
從?memoizeState?字段的值看出,函數(shù)組件和類組件的?state?存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)不一樣了。類組件是簡(jiǎn)單的數(shù)據(jù)對(duì)象,而函數(shù)組件是單向鏈表。
interface State {
? ?memoizedState: state數(shù)據(jù),和baseState值相同,
?baseState: state數(shù)據(jù),
?baseQueue: 本次更新之前沒執(zhí)行完的queue,
?next: 下一個(gè)state,
?queue: {
? ?pending: 更新state數(shù)據(jù)(這個(gè)數(shù)據(jù)是一個(gè)對(duì)象,里面有數(shù)據(jù),還有其他key用于做其他事情。),
? ?dispatch: setState方法本身,
? ?lastRenderedReducer: useReducer用得上,
? ?lastRenderedState: 上次渲染的State.memoizedState數(shù)據(jù),
?}
}調(diào)用 setA 方法,發(fā)生了什么?
在說(shuō)更新組件?state?之前,先看下組件掛載的流程。
調(diào)用?useState?的時(shí)候,會(huì)利用?currentlyRenderingFiber$1?拿到當(dāng)前組件的?fiber node,并掛載數(shù)據(jù)到節(jié)點(diǎn)上的?memoizedState?的字段上。這樣函數(shù)組件就有了狀態(tài)。
// react
function useState(initialState) {
?var dispatcher = resolveDispatcher();
?return dispatcher.useState(initialState);
}
function resolveDispatcher() {
?// ReactCurrentDispatcher 的值是react-dom注入的,后續(xù)會(huì)講。
?var dispatcher = ReactCurrentDispatcher.current;
?if (!(dispatcher !== null)) {
? ?{
? ? ?throwError( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
? ?}
?}
?return dispatcher;
}
// react-dom 會(huì)根據(jù)當(dāng)前組件的狀態(tài)注入不同的useState實(shí)現(xiàn)方法,這里可以先忽略。
useState: function (initialState) {
?currentHookNameInDev = 'useState';
?mountHookTypesDev();
?var prevDispatcher = ReactCurrentDispatcher.current;
?ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
?try {
?// 掛載state
? ?return mountState(initialState);
?} finally {
? ?ReactCurrentDispatcher.current = prevDispatcher;
?}
},
function mountState(initialState) {
?// 生成hook初始化數(shù)據(jù),掛到fiber node節(jié)點(diǎn)上
?var hook = mountWorkInProgressHook();
?if (typeof initialState === 'function') {
? ?// $FlowFixMe: Flow doesn't like mixed types
? ?initialState = initialState();
?}
?hook.memoizedState = hook.baseState = initialState;
?var queue = hook.queue = {
? ?pending: null,
? ?dispatch: null,
? ?lastRenderedReducer: basicStateReducer,
? ?lastRenderedState: initialState
?};
?var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
?return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook() {
?var hook = {
? ?memoizedState: null,
? ?baseState: null,
? ?baseQueue: null,
? ?queue: null,
? ?next: null
?};
?if (workInProgressHook === null) {
? ?// node節(jié)點(diǎn)的memoizedState指向第一個(gè)hooks
? ?currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
?} else {
? ?// 上一個(gè)hooks的next,等于當(dāng)前hooks,同時(shí)把當(dāng)前workInProgressHook,等于當(dāng)前hooks
? ?workInProgressHook = workInProgressHook.next = hook;
?}
?return workInProgressHook;
}useState?還會(huì)返回對(duì)應(yīng)的?state?和修改?state?的方法。修改?state?的方法?dispatchAction?綁定了當(dāng)前的?fiber node,同時(shí)還有當(dāng)前更新狀態(tài)的?action queue。
// 這里刪除了部分無(wú)關(guān)代碼
function dispatchAction(fiber, queue, action) {
?// 這些都是用于Fiber Reconciler,在這里不用太在意
?var currentTime = requestCurrentTimeForUpdate();
?var suspenseConfig = requestCurrentSuspenseConfig();
?var expirationTime = computeExpirationForFiber(currentTime, fiber, suspenseConfig);
?var update = {
? ?expirationTime: expirationTime,
? ?suspenseConfig: suspenseConfig,
? ?action: action,
? ?eagerReducer: null,
? ?eagerState: null,
? ?next: null
?};
?{
? ?update.priority = getCurrentPriorityLevel();
?}
?// pending 是當(dāng)前state是否有未更新的任務(wù)(比如多次調(diào)用更新state的方法)
?var pending = queue.pending;
?// queue是一個(gè)循環(huán)鏈表
?if (pending === null) {
? ?update.next = update;
?} else {
? ?update.next = pending.next;
? ?pending.next = update;
?}
?queue.pending = update;
?var alternate = fiber.alternate;
?if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
? ?// Reconciler 計(jì)算是否還有時(shí)間渲染,省略
?} else {
? ?// 此處省略很多代碼
? ?// 標(biāo)記當(dāng)前fiber node需要重新計(jì)算。
? ?scheduleWork(fiber, expirationTime);
?}
}從上面代碼可以看到,當(dāng)調(diào)用?setA?方法更新組件 state 的時(shí)候,會(huì)生成需要更新的數(shù)據(jù),包裝好數(shù)據(jù)結(jié)構(gòu)之后,推到?state?中的?queue?中。
scheduleWork?會(huì)觸發(fā) react 更新,這樣組件需要重新渲染。整體的流程和初次掛載的時(shí)候基本一致,但是從?mountState?方法體的實(shí)現(xiàn)來(lái)看,組件渲染是使用?initialState。這樣肯定是有問(wèn)題的。
function mountState(initialState) {
?// 掛載state
?var hook = mountWorkInProgressHook();
?if (typeof initialState === 'function') {
? ?initialState = initialState();
?}
?// state的初始值是initialState,也就是組件傳入的值
?hook.memoizedState = hook.baseState = initialState;
?var queue = hook.queue = {
? ?pending: null,
? ?dispatch: null,
? ?lastRenderedReducer: basicStateReducer,
? ?lastRenderedState: initialState
?};
?var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
?return [hook.memoizedState, dispatch];
}從此可以推斷,在前置步驟中,肯定有標(biāo)示當(dāng)前組件不是初次掛載,需要替換?useState?的實(shí)現(xiàn)方法。于是在?renderWithHooks?中找到了答案。
為了方便理解,簡(jiǎn)單說(shuō)一下,react 有兩個(gè)比較關(guān)鍵的數(shù)據(jù) current,workInProgress,分別代表當(dāng)前頁(yè)面渲染的 fiber node,觸發(fā)更新之后計(jì)算差別的 fiber node。全部計(jì)算完成之后,current 就會(huì)指向 workInProgress,用于渲染。
// 這里刪除部分無(wú)關(guān)代碼
// current 當(dāng)前頁(yè)面上組件對(duì)應(yīng)的fiber node
// workInProgress 當(dāng)前重新渲染對(duì)應(yīng)的fiber node
// Component 函數(shù)方法體
// ...
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderExpirationTime) {
?// currentlyRenderingFiber$1 是當(dāng)前正在渲染的組件,后續(xù)渲染流程會(huì)從改變量獲取state
?currentlyRenderingFiber$1 = workInProgress;
?workInProgress.memoizedState = null;
?workInProgress.updateQueue = null;
?workInProgress.expirationTime = NoWork; // The following should have already been reset
?// currentHook = null;
?// workInProgressHook = null;
?// didScheduleRenderPhaseUpdate = false;
?// TODO Warn if no hooks are used at all during mount, then some are used during update.
?// Currently we will identify the update render as a mount because memoizedState === null.
?// This is tricky because it's valid for certain types of components (e.g. React.lazy)
?// Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
?// Non-stateful hooks (e.g. context) don't get added to memoizedState,
?// so memoizedState would be null during updates and mounts.
?{
? ?// 如果當(dāng)前current不為null,且有state,說(shuō)明當(dāng)前組件是更新,需要執(zhí)行的更新state,否則就是初次掛載。
? ?if (current !== null && current.memoizedState !== null) {
? ? ?ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
? ?} elseif (hookTypesDev !== null) {
? ? ?// This dispatcher handles an edge case where a component is updating,
? ? ?// but no stateful hooks have been used.
? ? ?// We want to match the production code behavior (which will use HooksDispatcherOnMount),
? ? ?// but with the extra DEV validation to ensure hooks ordering hasn't changed.
? ? ?// This dispatcher does that.
? ? ?ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
? ?} else {
? ? ?ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
? ?}
?}
?// 往后省略
}在?renderWithHooks?方法中,會(huì)修改?ReactCurrentDispatcher,也就導(dǎo)致了?useState?對(duì)應(yīng)的方法體不一樣。HooksDispatcherOnUpdateInDEV?中的?useState?方法調(diào)用是?updateState。這個(gè)方法會(huì)忽略?initState,選擇從?fiber node?的?state?中去獲取當(dāng)前狀態(tài)。
useState: function (initialState) {
?currentHookNameInDev = 'useState';
?updateHookTypesDev();
?var prevDispatcher = ReactCurrentDispatcher.current;
?ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
?try {
? ?return updateState(initialState);
?} finally {
? ?ReactCurrentDispatcher.current = prevDispatcher;
?}
},
function updateState(initialState) {
?return updateReducer(basicStateReducer);
}
function updateReducer(reducer, initialArg, init) {
?// 根據(jù)之前的state初始化新的state結(jié)構(gòu),具體方法在下面
?var hook = updateWorkInProgressHook();
?// 當(dāng)前更新state的隊(duì)列
?var queue = hook.queue;
?queue.lastRenderedReducer = reducer;
?var current = currentHook; // The last rebase update that is NOT part of the base state.
?var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
?var pendingQueue = queue.pending;
?if (pendingQueue !== null) {
? ?// We have new updates that haven't been processed yet.
? ?// We'll add them to the base queue.
? ?if (baseQueue !== null) {
? ? ?// Merge the pending queue and the base queue.
? ? ?var baseFirst = baseQueue.next;
? ? ?var pendingFirst = pendingQueue.next;
? ? ?baseQueue.next = pendingFirst;
? ? ?pendingQueue.next = baseFirst;
? ?}
? ?current.baseQueue = baseQueue = pendingQueue;
? ?queue.pending = null;
?}
?if (baseQueue !== null) {
? ?// We have a queue to process.
? ?var first = baseQueue.next;
? ?var newState = current.baseState;
? ?var newBaseState = null;
? ?var newBaseQueueFirst = null;
? ?var newBaseQueueLast = null;
? ?var update = first;
? ?do {
? ? ?// fiber Reconciler 的內(nèi)容,省略
? ? ?} else {
? ? ? ?// This update does have sufficient priority.
? ? ? ?if (newBaseQueueLast !== null) {
? ? ? ? ?var _clone = {
? ? ? ? ? ?expirationTime: Sync,
? ? ? ? ? ?// This update is going to be committed so we never want uncommit it.
? ? ? ? ? ?suspenseConfig: update.suspenseConfig,
? ? ? ? ? ?action: update.action,
? ? ? ? ? ?eagerReducer: update.eagerReducer,
? ? ? ? ? ?eagerState: update.eagerState,
? ? ? ? ? ?next: null
? ? ? ? ?};
? ? ? ? ?newBaseQueueLast = newBaseQueueLast.next = _clone;
? ? ? ?} // Mark the event time of this update as relevant to this render pass.
? ? ? ?// TODO: This should ideally use the true event time of this update rather than
? ? ? ?// its priority which is a derived and not reverseable value.
? ? ? ?// TODO: We should skip this update if it was already committed but currently
? ? ? ?// we have no way of detecting the difference between a committed and suspended
? ? ? ?// update here.
? ? ? ?markRenderEventTimeAndConfig(updateExpirationTime, update.suspenseConfig); // Process this update.
? ? ? ?if (update.eagerReducer === reducer) {
? ? ? ? ?// If this update was processed eagerly, and its reducer matches the
? ? ? ? ?// current reducer, we can use the eagerly computed state.
? ? ? ? ?newState = update.eagerState;
? ? ? ?} else {
? ? ? ? ? ? ? ? ?// 執(zhí)行狀態(tài)更新,reducer是個(gè)包裝函數(shù):typeof action === 'function' ? action(state) : action;
? ? ? ? ?var action = update.action;
? ? ? ? ?newState = reducer(newState, action);
? ? ? ?}
? ? ?}
? ? ?update = update.next;
? ?} while (update !== null && update !== first);
? ?if (newBaseQueueLast === null) {
? ? ?newBaseState = newState;
? ?} else {
? ? ?newBaseQueueLast.next = newBaseQueueFirst;
? ?} // Mark that the fiber performed work, but only if the new state is
? ?// different from the current state.
? ?if (!objectIs(newState, hook.memoizedState)) {
? ? ?markWorkInProgressReceivedUpdate();
? ?}
? ?hook.memoizedState = newState;
? ?hook.baseState = newBaseState;
? ?hook.baseQueue = newBaseQueueLast;
? ?queue.lastRenderedState = newState;
?}
?var dispatch = queue.dispatch;
?return [hook.memoizedState, dispatch];
}
function updateWorkInProgressHook() {
?var nextCurrentHook;
?// 當(dāng)前
?if (currentHook === null) {
? ?// alternate 指向的是當(dāng)前頁(yè)面渲染組件對(duì)應(yīng)fiber node
? ?var current = currentlyRenderingFiber$1.alternate;
? ?if (current !== null) {
? ? ?nextCurrentHook = current.memoizedState;
? ?} else {
? ? ?nextCurrentHook = null;
? ?}
?} else {
? ?nextCurrentHook = currentHook.next;
?}
?var nextWorkInProgressHook;
?if (workInProgressHook === null) {
? ?nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
?} else {
? ?nextWorkInProgressHook = workInProgressHook.next;
?}
?if (nextWorkInProgressHook !== null) {
? ?// There's already a work-in-progress. Reuse it.
? ?workInProgressHook = nextWorkInProgressHook;
? ?nextWorkInProgressHook = workInProgressHook.next;
? ?currentHook = nextCurrentHook;
?} else {
? ?// Clone from the current hook.
? ?if (!(nextCurrentHook !== null)) {
? ? ?{
? ? ? ?throwError( "Rendered more hooks than during the previous render." );
? ? ?}
? ?}
? ?currentHook = nextCurrentHook;
? ?var newHook = {
? ? ?memoizedState: currentHook.memoizedState,
? ? ?baseState: currentHook.baseState,
? ? ?baseQueue: currentHook.baseQueue,
? ? ?queue: currentHook.queue,
? ? ?next: null
? ?};
? ?if (workInProgressHook === null) {
? ? ?
? ? // 第一個(gè)hook currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
? ?} else {
? ? ?// 下一個(gè)hooks,關(guān)聯(lián)前一個(gè)hooks
? ? ?workInProgressHook = workInProgressHook.next = newHook;
? ?}
?}
?return workInProgressHook;
}至此,調(diào)用?setA?方法,react 內(nèi)部做了什么就比較清晰了。setA?會(huì)在當(dāng)前?state?的?queue?里面插入一個(gè)?update action,并通知 react,當(dāng)前有組件狀態(tài)需要更新。在更新的時(shí)候,useState?的方法體和初始掛載的方法體不一樣,更新的時(shí)候時(shí)候會(huì)忽略?useState?傳遞的?initState,從節(jié)點(diǎn)數(shù)據(jù)的?baseState?中獲取初始數(shù)據(jù),并一步步執(zhí)行?queue?里的?update action,直至 queue 隊(duì)列為空,或者 queue 執(zhí)行完。
為什么有時(shí)候函數(shù)組件獲取的狀態(tài)不是實(shí)時(shí)的?
const App3 = () => {
?const [num, setNum] = React.useState(0);
?const add = () => {
? ?setTimeout(() => {
? ? ?setNum(num + 1);
? ?}, 1000);
?};
?return (
? ?<>
? ? ?<div>{num}div>
? ? ?<button onClick={add}>addbutton>
? ?>
?);
}在一秒內(nèi)點(diǎn)擊按鈕,無(wú)論點(diǎn)擊多少次,最終頁(yè)面返回都會(huì)是?1。原因:setTimeout 閉包了當(dāng)前狀態(tài)?num,在執(zhí)行?update state?的時(shí)候,對(duì)應(yīng)的 baseState 其實(shí)一直沒有更新,仍然是舊的,也就是?0,所以多次點(diǎn)擊,仍然是?0 + 1 = 1。修改的方式就是傳入的參數(shù)變?yōu)楹瘮?shù),這樣 react 在執(zhí)行?queue?的時(shí)候,會(huì)傳遞上一步的?state?值到當(dāng)前函數(shù)中。
setNum((state) => state + 1);為什么 useState 不能在判斷語(yǔ)句中聲明?
react 官網(wǎng)有這么一段話:
參考我們上面說(shuō)的,多個(gè)?state?之間通過(guò)?next?進(jìn)行關(guān)聯(lián),假設(shè)有 3 個(gè)?state,A、B、C。如果 B 在判斷語(yǔ)句中,那么就會(huì)就會(huì)出現(xiàn) A,B 的狀態(tài)能夠及時(shí)更新,但是 C 不會(huì)更新。因?yàn)檎{(diào)用 2 次?useState,只會(huì)更新兩次 state,在 state 的鏈表中,A.next->B,B.next->C,那么就只會(huì)更新了 A、B,C 不會(huì)更新,導(dǎo)致一些不可預(yù)知的問(wèn)題。
為什么 state 要用鏈表關(guān)聯(lián)起來(lái)?
這個(gè)問(wèn)題我也沒有想到答案,能解析的通的,感覺只有:是為了萬(wàn)物皆(純)函數(shù)吧。
因?yàn)榘凑瘴业睦斫?,其?shí)是可以保持和類組件一樣的狀態(tài)管理。state 還是一個(gè)對(duì)象,都通過(guò)調(diào)用一個(gè)方法來(lái)進(jìn)行更新。這樣和類組件反倒保持了統(tǒng)一,更好理解。
結(jié)語(yǔ)
通過(guò)解讀源碼的形式去理解?useState?執(zhí)行過(guò)程,能夠加深對(duì) react 函數(shù)組件狀態(tài)更新的理解。不足或者有錯(cuò)的地方,歡迎指出。
上文的解析,都是建立在 react@16,reac-dom@16 的基礎(chǔ)上。
???H5-Dooring,讓H5制作更簡(jiǎn)單
