v5 升级 v6 指南<Switch>全部换成<Routes>v5<BrowserRouter> <Menu /> <Switch> <Route component={Home} path="/home"></Route> <Route component={List} path="/list"></Route> <Route component={Detail} path="/detail"></Route> <Route component={Category} path="/category"></Route> </Switch> </BrowserRouter> // Category.tsx <Switch> <Route component={CategoryA} path="/category/a"></Route> <Route component={CategoryB} path="/category/b"></Route> </Switch> Switch 组件作用:渲染第一个被 location 匹配到的并且作为子元素的 <Route> 或者 <Redirect>,它仅仅只会渲染一个路径v6<BrowserRouter> <Menu /> <Routes> <Route element={<Home />} path="/home"></Route> <Route element={<List />} path="/list"></Route> <Route element={<Detail />} path="/detail"></Route> <Route element={<Category />} path="/category"> {/* children 写法嵌套子路由,path是相对路径 */} <Route element={<CategoryA />} path="a"></Route> <Route element={<CategoryB />} path="b"></Route> </Route> </Routes> </BrowserRouter> 与 Switch 相比,Routes 的主要优点是:Routes 内的所有 <Route> 和 <Link>的 path 支持相对路由(如果以/开头的则是绝对路由)。这使得 <Route path> 和 <Link to> 中的代码更精简、更可预测路由基于最佳 path 匹配的,而不是按顺序遍历选择的路由可以嵌套在同一个地方而不必分散在不同的组件中注意:不能认为 Routes 作为 Switch 的替代品。Switch 功能是匹配唯一的 Route 组件但它本身是可选的,可使用Route组件而不使用Switch组件。但只要使用Route组件则 v6 的Routes组件是必选的, Routes 必须套在最外层才可以使用Route组件,否则会报错。Route 组件属性Route 的 render 或 component 改为 element// v5 <Route component={Home} path="/home"></Route> // v6 <Route element={<Home />} path="/home"></Route> 简化path格式,只支持两种动态占位符:id 动态参数* 通配符,只能在 path 的末尾使用,如 users/*v6 path的正确写法:/groups /groups/admin /users/:id /users/:id/messages /files/* /files/:id/* v6 path错误的写法/users/:id? // ? 不满足上面两种格式 /tweets/:id(\d+) // 有正则表达式,不满足 /files/*/cat.jpg /files-* 路由匹配的区分大小写开启 caseSensitivecaseSensitive,用于正则匹配 path 时是否开启 ignore 模式,即匹配时是否忽略大小写<Routes caseSensitive> 所有路径匹配都会忽略 URL 上的尾部斜杠新增 Outlet 组件作用:通常用于渲染子路由,类似插槽的作用,用于匹配子路由的 elementexport default function Category() { return ( <div> <div> <Link to="a">跳转 CategoryA</Link> </div> <div> <Link to="b">跳转 CategoryB</Link> </div> {/* 自动匹配子路由的渲染 */} <Outlet /> </div> ) } Link 组件属性to 属性有无 / 与当前 URL 的区别在 v5 中,如果 to 没有以 / 开头的话会充满不确定性,这取决于当前的 URL。比如当前 URL 是/category, <Link to="a"> 会渲染成 <a href="/a">; 而当前 URL 如果是 /category/,那么又会渲染成 <a href="/category/a">。在 v6 中,无论当前 URL 是 /category 还是 /category/, <Link to="a"> 都会渲染成 <a href='/category/a'>,即忽略 URL 上的尾部斜杠统一规则处理。to 属性支持相对位置与'..' 和'.'等写法<ul> <li>当前页面:CategoryA</li> <li>当前url:/category/a</li> <li> {/* /list */} <Link to="../../list">跳转到list页面</Link> </li> <li> {/* /category/b */} <Link to="../b">跳转到category/b页面</Link> </li> <li> {/* /category/a */} <Link to=".">跳转到当前路由</Link> </li> </ul> 直接传 state 属性// v5: <Link to={{ pathname: "/home", state: state }} /> // v6: <Link to="/home" state={state} /> 新增 target 属性type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {}) NavLink<NavLink exact> 属性名改为了 <NavLink end>移除activeStyle、activeClassName属性<NavLink to="/messages" - style={{ color: 'blue' }} - activeStyle={{ color: 'green' }} + style={({ isActive }) => ({ color: isActive ? 'green' : 'blue' })} > Messages </NavLink> 移除Redirect重定向组件移除的主要原因是不利于 SEO// v5 <Redirect from="/404" to="/home" /> // v6 使用 Navigate 组件替代 <Route path="/404" element={<Navigate to="/home" replace />} /> 新增 useNavigate 代替 useHistory函数组件可以使用useHistory获取history对象,用来做页面跳转导航// v5 import { useHistory } from 'react-router-dom' export default function Menu() { const history = useHistory() return ( <div> <div onClick={() => { history.push('/list') }} > 编程式路由跳转list页面 </div> </div> ) } // v6 import { useNavigate } from 'react-router-dom' export default function Menu() { const navigate = useNavigate() return ( <div> <div onClick={() => { navigate('/list') // 等价于 history.push }} > 编程式路由跳转list页面 </div> </div> ) } 下面再列举其它其它的区别用法//v5 history.replace('/list') // v6 navigate('/list', { replace: true }) // v5 history.go(1) history.go(-1) // v6 navigate(1) navigate(-1) 新增 useRoutes 代替 react-router-configuseRoutes 根据路由表生成对应的路由规则useRoutes使用必须在<Router>里面react-router-config:用于集中管理路由配置import { useRoutes } from 'react-router-dom' import Home from './components/Home' import List from './components/List' function App() { const element = useRoutes([ { path: '/home', element: <Home /> }, { path: '/list', element: <List /> }, ]) return element } export default App 新增 useSearchParamsv6 提供 useSearchParams 返回一个数组来获取和设置 url 参数import { useSearchParams } from 'react-router-dom' export default function Detail() { const [searchParams, setSearchParams] = useSearchParams() console.log('getParams', searchParams.get('name')) return ( <div onClick={() => { setSearchParams({ name: 'jacky' }) }} > 当前页面:Detail 点我设置url查询参数为name=jacky </div> ) } 不支持 <Prompt>在老版本中,Prompt组件可以实现页面关闭的拦截,但它在 v6 版本还暂不支持,如果想 v5 升级 v6 就要考虑清楚了。// v5 <Prompt when={formIsHalfFilledOut} message="Are you sure you want to leave?" /> 总结v5 和 v6 在使用层面的区别总结:<Switch> 全部换成 <Routes>Route 新特性变更新增 Outlet 组件用于渲染匹配到的子路由移除Redirect重定向组件,因为不利于 SEO新增 useNavigate 替代 useHistory新增 useRoutes 代替 react-router-config新增 useSearchParams 来获取和设置 url 参数
setState 经典问题setState(updater, [callback]) React 通过 this.setState() 来更新 state,当使用 this.setState()的时候 ,React 会调用 render 方法来重新渲染 UI。setState 的几种用法就不用我说了,来看看网上讨论 setState 比较多的问题:批量更新import React, { Component } from 'react' class App extends Component { state = { count: 1, } handleClick = () => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 } render() { return ( <> <button onClick={this.handleClick}>加1</button> <div>{this.state.count}</div> </> ) } } export default App 点击按钮触发事件,打印的都是 1,页面显示 count 的值为 2。这就是常常说的 setState 批量更新,对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行结果。所以每次 setState 之后立即打印值都是初始值 1,而最后页面显示的值则为最后一次的执行结果,也就是 2。setTimeoutimport React, { Component } from 'react' class App extends Component { state = { count: 1, } handleClick = () => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 setTimeout(() => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 3 }) } render() { return ( <> <button onClick={this.handleClick}>加1</button> <div>{this.state.count}</div> </> ) } } export default App 点击按钮触发事件,发现 setTimeout 里面的 count 值打印值为 3,页面显示 count 的值为 3。setTimeout 里面 setState 之后能马上能到最新值。在 setTimeout 里面,setState 是同步的;经过前面两次的 setState 批量更新,count 值已经更新为 2。在 setTimeout 里面的首先拿到新的 count 值 2,再一次 setState,然后能实时拿到 count 的值为 3。DOM 原生事件import React, { Component } from 'react' class App extends Component { state = { count: 1, } componentDidMount() { document.getElementById('btn').addEventListener('click', this.handleClick) } handleClick = () => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 2 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 3 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 4 } render() { return ( <> <button id='btn'>触发原生事件</button> <div>{this.state.count}</div> </> ) } } export default App 点击按钮,会发现每次 setState 打印出来的值都是实时拿到的,不会进行批量更新。在 DOM 原生事件里面,setState 也是同步的。setState 同步异步问题这里讨论的同步和异步并不是指 setState 是否异步执行,使用了什么异步代码,而是指调用 setState 之后 this.state 能否立即更新。React 中的事件都是合成事件,都是由 React 内部封装好的。React 本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,就是我们所说的"异步"了。由上面也可以得知 setState 在原生事件和 setTimeout 中都是同步的。setState 源码层面源码选择的 React 版本为15.6.2setState 函数源码里面,setState 函数的代码React 组件继承自React.Component,而 setState 是React.Component的方法ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState) if (callback) { this.updater.enqueueCallback(this, callback, 'setState') } } 可以看到它直接调用了 this.updater.enqueueSetState 这个方法。enqueueSetStateenqueueSetState: function(publicInstance, partialState) { // 拿到对应的组件实例 var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState', ); // queue 对应一个组件实例的 state 数组 var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); // 将 partialState 放入待更新 state 队列 // 处理当前的组件实例 enqueueUpdate(internalInstance); } _pendingStateQueue表示待更新队列enqueueSetState 做了两件事:将新的 state 放进组件的状态队列里;用 enqueueUpdate 来处理将要更新的实例对象。接下来看看 enqueueUpdate 做了什么:function enqueueUpdate(component) { ensureInjected() // isBatchingUpdates 标识着当前是否处于批量更新过程 if (!batchingStrategy.isBatchingUpdates) { // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件 batchingStrategy.batchedUpdates(enqueueUpdate, component) return } // 需要批量更新,则先把组件塞入 dirtyComponents 队列 dirtyComponents.push(component) if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1 } } batchingStrategy 表示批量更新策略,isBatchingUpdates表示当前是否处于批量更新过程,默认是 false。enqueueUpdate做的事情:判断组件是否处于批量更新模式,如果是,即isBatchingUpdates为 true 时,不进行 state 的更新操作,而是将需要更新的组件添加到dirtyComponents数组中;如果不是处于批量更新模式,则对所有队列中的更新执行batchedUpdates方法当中 batchingStrategy该对象的isBatchingUpdates属性直接决定了是马上要走更新流程,还是应该进入队列等待;所以大概可以得知batchingStrategy用于管控批量更新的对象。来看看它的源码:/** * batchingStrategy源码 **/ var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, // 初始值为 false 表示当前并未进行任何批量更新操作 // 发起更新动作的方法 batchedUpdates: function (callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates ReactDefaultBatchingStrategy.isBatchingUpdates = true if (alreadyBatchingUpdates) { return callback(a, b, c, d, e) } else { // 启动事务,将 callback 放进事务里执行 return transaction.perform(callback, null, a, b, c, d, e) } }, } 每当 React 调用 batchedUpdate 去执行更新动作时,会先把isBatchingUpdates置为 true,表明正处于批量更新过程中。看完批量更新整体的管理机制,发现还有一个操作是transaction.perform,这就引出 React 中的 Transaction(事务)机制。Transaction(事务)机制Transaction 是创建一个黑盒,该黑盒能够封装任何的方法。因此,那些需要在函数运行前、后运行的方法可以通过此方法封装(即使函数运行中有异常抛出,这些固定的方法仍可运行)。在 React 中源码有关于 Transaction 的注释如下: * <pre> * wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ * </pre> 根据以上注释,可以看出:一个 Transaction 就是将需要执行的 method 使用 wrapper(一组 initialize 及 close 方法称为一个 wrapper) 封装起来,再通过 Transaction 提供的 perform 方法执行。在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法,而且 Transaction 支持多个 wrapper 叠加。这就是 React 中的事务机制。batchingStrategy 批量更新策略再看回batchingStrategy批量更新策略,ReactDefaultBatchingStrategy 其实就是一个批量更新策略事务,它的 wrapper 有两个:FLUSH_BATCHED_UPDATES 和 RESET_BATCHED_UPDATES。isBatchingUpdates在 close 方法被复位为 false,如下代码:var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false }, } // flushBatchedUpdates 将所有的临时 state 合并并计算出最新的 props 及 state var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates), } var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES] React 钩子函数都说 React 钩子函数也是异步更新,则必须一开始 isBatchingUpdates 为 ture,但默认 isBatchingUpdates 为 false,它是在哪里被设置为 true 的呢?来看下面代码:// ReactMount.js _renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) { // 实例化组件 var componentInstance = instantiateReactComponent(nextElement); // 调用 batchedUpdates 方法 ReactUpdates.batchedUpdates( batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context ); } 这段代码是在首次渲染组件时会执行的一个方法,可以看到它内部调用了一次 batchedUpdates 方法(将 isBatchingUpdates 设为 true),这是因为在组件的渲染过程中,会按照顺序调用各个生命周期(钩子)函数。如果在函数里面调用 setState,则看下列代码:if (!batchingStrategy.isBatchingUpdates) { // 立即更新组件 batchingStrategy.batchedUpdates(enqueueUpdate, component) return } // 批量更新,则先把组件塞入 dirtyComponents 队列 dirtyComponents.push(component) 则所有的更新都能够进入 dirtyComponents 里去,即 setState 走的异步更新React 合成事件当我们在组件上绑定了事件之后,事件中也有可能会触发 setState。为了确保每一次 setState 都有效,React 同样会在此处手动开启批量更新。看下面代码:// ReactEventListener.js dispatchEvent: function (topLevelType, nativeEvent) { try { // 处理事件:batchedUpdates会将 isBatchingUpdates设为true ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } } isBatchingUpdates 这个变量,在 React 的生命周期函数以及合成事件执行前,已经被 React 改为 true,这时我们所做的 setState 操作自然不会立即生效。当函数执行完毕后,事务的 close 方法会再把 isBatchingUpdates 改为 false。就像最上面的例子,整个过程模拟大概是:handleClick = () => { // isBatchingUpdates = true this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 // isBatchingUpdates = false } 而如果有 setTimeout 介入后handleClick = () => { // isBatchingUpdates = true this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 setTimeout(() => { // setTimeout异步执行,此时 isBatchingUpdates 已经被重置为 false this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 3 }) // isBatchingUpdates = false } isBatchingUpdates是在同步代码中变化的,而 setTimeout 的逻辑是异步执行的。当 this.setState 调用真正发生的时候,isBatchingUpdates 早已经被重置为 false,这就使得 setTimeout 里面的 setState 具备了立刻发起同步更新的能力。batchedUpdates 方法看到这里大概就可以了解 setState 的同步异步机制了,接下来让我们进一步体会,可以把 React 的batchedUpdates拿来试试,在该版本中此方法名称被置为unstable_batchedUpdates 即不稳定的方法。import React, { Component } from 'react' import { unstable_batchedUpdates as batchedUpdates } from 'react-dom' class App extends Component { state = { count: 1, } handleClick = () => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 setTimeout(() => { batchedUpdates(() => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 2 }) }) } render() { return ( <> <button onClick={this.handleClick}>加1</button> <div>{this.state.count}</div> </> ) } } export default App 如果调用batchedUpdates方法,则 isBatchingUpdates变量会被设置为 true,由上述得为 true 走的是批量更新策略,则 setTimeout 里面的方法也变成异步更新了,所以最终打印值为 2,与本文第一道题结果一样。总结setState 同步异步的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout/setInterval 函数,DOM 原生事件中,它都表现为同步。这是由 React 事务机制和批量更新机制的工作方式来决定的。在 React16 中,由于引入了 Fiber 机制,源码多少有点不同,但大同小异,之后我也会写 React16 原理的文章,敬请关注!
React 17 更新首先,官方发布日志称react17最大的特点就是无新特性,这个版本主要目标是让React能渐进式升级,它允许多版本混用共存,可以说是为更远的未来版本做准备了。去除事件池在React17之前,如果使用异步的方式来获取事件e对象,会发现合成事件对象被销毁,如下:function App() { const handleClick = (e: React.MouseEvent) => { console.log('直接打印e', e.target) // <button>React事件池</button> // v17以下在异步方法拿不到事件e,必须先调用 e.persist() // e.persist() // 异步方式获取事件e setTimeout(() => { console.log('setTimeout打印e', e.target) // null }) } return ( <div className="App"> <button onClick={handleClick}>React事件池</button> </div> ) } 如果你需要在事件处理函数运行之后获取事件对象的属性,你需要调用 e.persist(),它会将当前的合成事件从事件池中删除,并允许保留对事件的引用。事件池:合成事件对象会被放入池中统一管理。这意味着合成事件对象可以被复用,当所有事件处理函数被调用之后,其所有属性都会被回收释放置空。事件池的好处是在较旧浏览器中重用了不同事件的事件对象以提高性能,但它对现代浏览器的性能优化微乎其微,反而给开发者带来困惑,因此去除了事件池,因此也没有了事件复用机制。function App() { // v17 去除了 React 事件池,异步方式使用e不再需要 e.persist() const handleClick = (e: React.MouseEvent) => { console.log('直接打印e', e.target) // <button>React事件池</button> setTimeout(() => { console.log('setTimeout打印e', e.target) // <button>React事件池</button> }) } return ( <div className="App"> <button onClick={handleClick}>React事件池</button> </div> ) } 事件委托到根节点reactv17前,React 将事件委托到 document 上,在react17中,则委托到根节点const rootNode = document.getElementById('root'); ReactDOM.render(<App />, rootNode); import { useState, useEffect } from 'react' function App() { const [isShowText, setIsShowText] = useState(false) const handleShowText = (e: React.MouseEvent) => { // e.stopPropagation() // v16无效 // e.nativeEvent.stopImmediatePropagation() // 阻止监听同一事件的其他事件监听器被调用 setIsShowText(true) } useEffect(() => { document.addEventListener('click', () => { setIsShowText(false) }) }, []) return ( <div className="App"> <button onClick={handleShowText}>事件委托变更</button> {isShowText && <div>展示文字</div>} </div> ) } 如上代码,在react16和v17版本,点击按钮时,都不会显示文字。这是因为react的合成事件是基于事件委托的,有事件冒泡,先执行React事件,再执行document上挂载的事件。v16:出于对冒泡的了解,我们直接在按钮事件上加e.stopPropagation(),这样就不会冒泡到document,isShowText 也不会被置为false了。但由于v16版本的事件委托是绑在document上的,它的事件源跟document就是同级了,而不是上下级,所以e.stopPropagation()并没有起作用。如果要阻止冒泡,可以使用原生的e.nativeEvent.stopImmediatePropagation()阻止同级冒泡,这样文字就可以显示了。v17:由于事件委托到根目录root节点,与document属于上下级关系,所以可以直接使用e.stopPropagation()阻止stopImmediatePropagation() 方法可以阻止监听同一事件的其他事件监听器被调用这种更新不仅方便了局部使用 React 的项目,还可以用于项目的渐进式升级,解决不同版本的 React 组件嵌套使用时,e.stopPropagation()无法正常工作的问题更贴近原生浏览器事件对事件系统进行了一些较小的更改:onScroll 事件不再冒泡,以防止出现常见的混淆React 的 onFocus 和 onBlur 事件已在底层切换为原生的 focusin 和 focusout 事件。它们更接近 React 现有行为,有时还会提供额外的信息。blur、focus 和 focusin、focusout 的区别:blur、focus 不支持冒泡,focusin、focusout 支持冒泡捕获事件(例如,onClickCapture)现在使用的是实际浏览器中的捕获监听器。这些更改会使 React 与浏览器行为更接近,并提高了互操作性。尽管 React 17 底层已将 onFocus 事件从 focus 切换为 focusin,但请注意,这并未影响冒泡行为。在 React 中,onFocus 事件总是冒泡的,在 React 17 中会继续保持,因为通常它是一个更有用的默认值全新的 JSX 转换器总结下来就是两点:用 jsx() 函数替换 React.createElement()运行时自动引入 jsx() 函数,无需手写引入react在v16中,我们写一个React组件,总要引入import React from 'react' 这是因为在浏览器中无法直接使用 jsx,所以要借助工具如@babel/preset-react将 jsx 语法转换为 React.createElement 的 js 代码,所以需要显式引入 React,才能正常调用 createElement。通过React.createElement() 创建元素是比较频繁的操作,本身也存在一些问题,无法做到性能优化,具体可见官方优化的 动机v17之后,React 与 Babel 官方进行合作,直接通过将 react/jsx-runtime 对 jsx 语法进行了新的转换而不依赖 React.createElement,因此v17使用 jsx 语法可以不引入 React,应用程序依然能正常运行。function App() { return <h1>Hello World</h1>; } // 新的 jsx 转换为 // 由编译器引入(禁止自己引入!) import { jsx as _jsx } from 'react/jsx-runtime'; function App() { return _jsx('h1', { children: 'Hello world' }); } 如何升级至新的 JSX 转换更新至支持新转换的 React 版本(v17)如果你还在使用v16,也可升级至 React v16.14.0 的版本,官方在该版本也支持这个特性。修改配置@babel/preset-react编译增加 runtime: 'automatic'配置// 如果你使用的是 @babel/preset-react { "presets": [ ["@babel/preset-react", { "runtime": "automatic" }] ] } // 如果你使用的是 @babel/plugin-transform-react-jsx { "plugins": [ ["@babel/plugin-transform-react-jsx", { "runtime": "automatic" }] ] } 修改 tsconfig.json 配置,具体配置可见TS官方文档{ "compilerOptions": { // "jsx": "react", "jsx": "react-jsx", }, } 从 Babel 8 开始,"automatic" 会将两个插件默认集成在 rumtime 中副作用清理时机useEffect(() => { // This is the effect itself. return () => { // This is its cleanup. } }) v17前,组件被卸载时,useEffect的清理函数都是同步运行的;对于大型应用程序来说,同步会减缓屏幕的过渡(如切换标签)v17后,useEffect 副作用清理函数是异步执行的,如果要卸载组件,则清理会在屏幕更新后运行此外,v17 将在运行任何新副作用之前执行所有副作用的清理函数(针对所有组件),v16 只对组件内的副作用保证这种顺序。不过需要注意useEffect(() => { someRef.current.someSetupMethod(); return () => { someRef.current.someCleanupMethod(); }; }); 问题在于 someRef.current 是可变的且因为异步的,在运行清除函数时,它可能已经设置为 null。// 用一个变量量在 ref 每次变化时,将 someRef.current 保存起来,放到副作用清理回调函数的闭包中,来保证不可变性。 useEffect(() => { const instance = someRef.current instance.someSetupMethod() return () => { instance.someCleanupMethod() } }) 或者用 useLayoutEffectuseLayoutEffect(() => { someRef.current.someSetupMethod(); return () => { someRef.current.someCleanupMethod(); }; }); useLayoutEffect 可以保证回调函数同步执行,这样就能确保 ref 此时还是最后的值。返回一致的 undefined 错误在v17以前,组件返回undefined始终是一个错误。但是有漏网之鱼,React 只对类组件和函数组件执行此操作,但并不会检查 forwardRef 和 memo 组件的返回值。function Button() { return; // Error: Nothing was returned from render } function Button() { // We forgot to write return, so this component returns undefined. // React surfaces this as an error instead of ignoring it. <button />; } 在 v17 中修复了这个问题,forwardRef 和 memo 组件的行为会与常规函数组件和类组件保持一致,在返回 undefined 时会报错let Button = forwardRef(() => { // We forgot to write return, so this component returns undefined. // React 17 surfaces this as an error instead of ignoring it. <button />; }); let Button = memo(() => { // We forgot to write return, so this component returns undefined. // React 17 surfaces this as an error instead of ignoring it. <button />; }); 原生组件栈v16中错误调用栈的缺点:缺少源码位置追溯,在控制台无法点击跳转到到出错的地方无法适用于生产环境整体来说不如原生的 JavaScript 调用栈,不同于常规压缩后的 JavaScript 调用栈,它们可以通过 sourcemap 的形式自动恢复到原始函数的位置,而使用 React 组件栈,在生产环境下必须在调用栈信息和 bundle 大小间进行选择。在v17使用了不同的机制生成组件调用栈,直接从 JavaScript 原生错误栈生成的,所以在生产环境也能按sourcemap 还原回来,且支持点击跳到源码位置。想详细了解的可见该 PR移除私有导出 APIv17 删除了一些私有 API,主要是 React Native for Web 使用的另外,还删除了ReactTestUtils.SimulateNative工具方法,因为其行为与语义不符,如果你想要一种简便的方式来触发测试中原生浏览器的事件,可直接使用 React Testing Library启发式更新算法更新引用 React17新特性:启发式更新算法React16的expirationTimes模型只能区分是否>=expirationTimes决定节点是否更新。React17的lanes模型可以选定一个更新区间,并且动态的向区间中增减优先级,可以处理更细粒度的更新。这个我目前也不是太清楚具体算法,先不展开了有兴趣的可去查阅相关资料React 18 更新并发模式v18的新特性是使用现代浏览器的特性构建的,彻底放弃对 IE 的支持。v17 和 v18 的区别就是:从同步不可中断更新变成了异步可中断更新,v17可以通过一些试验性的API开启并发模式,而v18则全面开启并发模式。并发模式可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整,该模式通过使渲染可中断来修复阻塞渲染限制。在 Concurrent 模式中,React 可以同时更新多个状态。这里参考下文区分几个概念:并发模式是实现并发更新的基本前提v18 中,以是否使用并发特性作为是否开启并发更新的依据。并发特性指开启并发模式后才能使用的特性,比如:useDeferredValue/useTransition可阅读参考 Concurrent Mode(并发模式)更新 render APIv18 使用 ReactDOM.createRoot() 创建一个新的根元素进行渲染,使用该 API,会自动启用并发模式。如果你升级到v18,但没有使用ReactDOM.createRoot()代替ReactDOM.render()时,控制台会打印错误日志要提醒你使用React18,该警告也意味此项变更没有造成breaking change,而可以并存,当然尽量是不建议。// v17 import ReactDOM from 'react-dom' import App from './App' ReactDOM.render(<App />, document.getElementById('root')) // v18 import ReactDOM from 'react-dom/client' import App from './App' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />) 自动批处理批处理是指 React 将多个状态更新,聚合到一次 render 中执行,以提升性能在v17的批处理只会在事件处理函数中实现,而在Promise链、异步代码、原生事件处理函数中失效。而v18则所有的更新都会自动进行批处理。// v17 const handleBatching = () => { // re-render 一次,这就是批处理的作用 setCount((c) => c + 1) setFlag((f) => !f) } // re-render两次 setTimeout(() => { setCount((c) => c + 1) setFlag((f) => !f) }, 0) // v18 const handleBatching = () => { // re-render 一次 setCount((c) => c + 1) setFlag((f) => !f) } // 自动批处理:re-render 一次 setTimeout(() => { setCount((c) => c + 1) setFlag((f) => !f) }, 0) 如果在某些场景不想使用批处理,可以使用 flushSync退出批处理,强制同步执行更新。flushSync 会以函数为作用域,函数内部的多个 setState 仍然是批量更新const handleAutoBatching = () => { // 退出批处理 flushSync(() => { setCount((c) => c + 1) }) flushSync(() => { setFlag((f) => !f) }) } Suspense 支持 SSRSSR 一次页面渲染的流程:服务器获取页面所需数据将组件渲染成 HTML 形式作为响应返回客户端加载资源(hydrate)执行 JS,并生成页面最终内容上述流程是串行执行的,v18前的 SSR 有一个问题就是它不允许组件"等待数据",必须收集好所有的数据,才能开始向客户端发送HTML。如果其中有一步比较慢,都会影响整体的渲染速度。v18 中使用并发渲染特性扩展了Suspense的功能,使其支持流式 SSR,将 React 组件分解成更小的块,允许服务端一点一点返回页面,尽早发送 HTML和选择性的 hydrate, 从而可以使SSR更快的加载页面<Suspense fallback={<Spinner />}> <Comments /> </Suspense> 具体可参考文章 React 18 中新的 Suspense SSR 架构startTransitionTransitions 是 React 18 引入的一个全新的并发特性。它允许你将标记更新作为一个 transitions(过渡),这会告诉 React 它们可以被中断执行,并避免回到已经可见内容的 Suspense 降级方案。本质上是用于一些不是很急迫的更新上,用来进行并发控制在v18之前,所有的更新任务都被视为急迫的任务,而Concurrent Mode 模式能将渲染中断,可以让高优先级的任务先更新渲染。React 的状态更新可以分为两类:紧急更新:比如点击按钮、搜索框打字是需要立即响应的行为,如果没有立即响应给用户的体验就是感觉卡顿延迟过渡/非紧急更新:将 UI 从一个视图过渡到另一个视图。一些延迟可以接受的更新操作,不需要立即响应startTransition API 允许将更新标记为非紧急事件处理,被startTransition包裹的会延迟更新的state,期间可能被其他紧急渲染所抢占。因为 React 会在高优先级更新渲染完成之后,才会渲染低优先级任务的更新React 无法自动识别哪些更新是优先级更高的。比如用户的键盘输入操作后,setInputValue会立即更新用户的输入到界面上,是紧急更新。而setSearchQuery是根据用户输入,查询相应的内容,是非紧急的。const [inputValue, setInputValue] = useState() const onChange = (e)=>{ setInputValue(e.target.value) // 更新用户输入值(用户打字交互的优先级应该要更高) setSearchQuery(e.target.value) // 更新搜索列表(可能有点延迟,影响) } return ( <input value={inputValue} onChange={onChange} /> ) React无法自动识别,所以它提供了 startTransition让我们手动指定哪些更新是紧急的,哪些是非紧急的,从而让我们改善用户交互体验。// 紧急的更新 setInputValue(e.target.value) // 开启并发更新 startTransition(() => { setSearchQuery(input) // 非紧急的更新 }) 这里有个比较好的在线例子,可以直接感受到 startTransition的优化useTransition当有过渡任务(非紧急更新)时,我们可能需要告诉用户什么时候当前处于 pending(过渡) 状态,因此v18提供了一个带有isPending标志的 Hook useTransition来跟踪 transition 状态,用于过渡期。useTransition 执行返回一个数组。数组有两个状态值:isPending: 指处于过渡状态,正在加载中startTransition: 通过回调函数将状态更新包装起来告诉 React这是一个过渡任务,是一个低优先级的更新function TransitionTest() { const [isPending, startTransition] = useTransition() const [count, setCount] = useState(0) function handleClick() { startTransition(() => { setCount((c) => c + 1) }) } return ( <div> {isPending && <div>spinner...</div>} <button onClick={handleClick}>{count}</button> </div> ) } 直观感觉这有点像 setTimeout,而防抖节流其实本质也是setTimeout,区别是防抖节流是控制了执行频率,让渲染次数减少了,而 v18的 transition 则没有减少渲染的次数。useDeferredValueuseDeferredValue 和 useTransition 一样,都是标记了一次非紧急更新。useTransition是处理一段逻辑,而useDeferredValue是产生一个新状态,它是延时状态,这个新的状态则叫 DeferredValue。所以使用useDeferredValue可以推迟状态的渲染useDeferredValue 接受一个值,并返回该值的新副本,该副本将推迟到紧急更新之后。如果当前渲染是一个紧急更新的结果,比如用户输入,React 将返回之前的值,然后在紧急渲染完成后渲染新的值。function Typeahead() { const query = useSearchQuery(''); const deferredQuery = useDeferredValue(query); // Memoizing 告诉 React 仅当 deferredQuery 改变, // 而不是 query 改变的时候才重新渲染 const suggestions = useMemo(() => <SearchSuggestions query={deferredQuery} />, [deferredQuery] ); return ( <> <SearchInput query={query} /> <Suspense fallback="Loading results..."> {suggestions} </Suspense> </> ); } 这样一看,useDeferredValue直观就是延迟显示状态,那用防抖节流有什么区别呢?如果使用防抖节流,比如延迟300ms显示则意味着所有用户都要延时,在渲染内容较少、用户CPU性能较好的情况下也是会延迟300ms,而且你要根据实际情况来调整延迟的合适值;但是useDeferredValue是否延迟取决于计算机的性能。感兴趣可以看下这篇文章:usedeferredvalue-in-react-18在线例子useIduseId支持同一个组件在客户端和服务端生成相同的唯一的 ID,避免 hydration 的不匹配,原理就是每个 id 代表该组件在组件树中的层级结构。function Checkbox() { const id = useId() return ( <> <label htmlFor={id}>Do you like React?</label> <input id={id} type="checkbox" name="react" /> </> ) } 这里涉及到 SSR 部分知识,这里不展开了,可以阅读该篇文章理解:为了生成唯一id,React18专门引入了新Hook:useId提供给第三方库的 Hook这两个 Hook 日常开发基本用不到,简单带过useSyncExternalStoreuseSyncExternalStore 一般是第三方状态管理库使用如 Redux。它通过强制的同步状态更新,使得外部 store 可以支持并发读取。它实现了对外部数据源订阅时不再需要 useEffectconst state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]); useInsertionEffectuseInsertionEffect 仅限于 css-in-js 库使用。它允许 css-in-js 库解决在渲染中注入样式的性能问题。 执行时机在 useLayoutEffect 之前,只是此时不能使用ref和调度更新,一般用于提前注入样式。useInsertionEffect(() => { console.log('useInsertionEffect 执行') }, [])
实现 createElement 方法这个方法平时开发我们并不会用到,因为它是经 babel 编译后的代码,我们新建一个 React 项目,index.js 最简单的代码结构如下:import React from 'react' import ReactDOM from 'react-dom' ReactDOM.render(<h1 className='title'>Hello React</h1>, document.getElementById('root')) 这里就 jsx 会变编译成真正的 DOM ,把 html 代码拿到 babel 官网编译于是我们就看到了 React.createElement() 方法,但这只是调用这个方法,它具体做了什么返回什么我们还不知道,我们可以打印这个函数运行的结果:console.log( React.createElement( 'h1', { className: 'title', }, 'Hello React' ) ) 返回的这个对象就是虚拟 DOM 了。我们来分析它返回的对象参数,首先第一个是?typeof: REACT_ELEMENT_TYPE这个是 React 元素对象的标识属性REACT_ELEMENT_TYPE 的值是一个 Symbol 类型,代表了一个独一无二的值。如果浏览器不支持 Symbol类型,值就是一个二进制值。为什么是 Symbol?主要防止 XSS 攻击伪造一个假的 React 组件。因为 JSON 中是不会存在 Symbol 类型的。key:这个比如循环中会用到这个key值props:传入的属性值,比如 id, className, style, children 等ref: DOM 的引用剩下的是私有属性(本篇不展开讨论)在本篇我们会用自己简单的方式实现这两个方法,而不是根据源码,所以实现上的方法只要能实现它的基本功能即可;有个基本概念在,以后再循序渐进学习源码。而 createElement 中有三个参数,更确切说是 n 个参数:type:表示要渲染的元素类型。这里可以传入一个元素 Tag 名称,也可以传入一个组件(如div span 等,也可以是是函数组件和类组件)props:创建React元素所需要的props。childrens(可选参数):要渲染元素的子元素,这里可以向后传入n个参数。可以为文本字符串,也可以为数组初步 createElement 方法:// 创建 JSX 对象 function createElement(type, props, ...childrens) { return { type, props: { ...props, children: childrens.length <= 1 ? childrens[0] || '' : childrens, }, } 参数中 props 和 childrens 是并列关系,然后返回的 props 对象,里面包含了 children,所以我们需要再 props 里面添加 children 参数,然后根据 children 参数为一个或多个的可能在进行取值处理。调用该方法:console.log( createElement( 'h1', { className: 'title', }, 'Hello React' ) ) 除去其它本篇我们不讨论的属性,目前算是实现了一半;我们观察原来 React 自身方法输出的结果有 key, ref, 同输出的 props 也是并列关系,于是我们进一步作出处理function createElement(type, props, ...childrens) { let ref, key if ('ref' in props) { ref = props['ref'] props['ref'] = undefined } if ('key' in props) { key = props['key'] props['key'] = undefined } return { type, props: { ...props, children: childrens.length <= 1 ? childrens[0] || '' : childrens, }, ref, key, } } 同样的方式调用结果如下:如果添加多一些属性,我们来看看结果console.log( createElement( 'div', { id: 'box', className: 'box', style: { color: 'red' }, key: '20' }, 'this is text', createElement('h2', { className: 'title' }, 'hello'), createElement('div', { className: 'content' }, 'Hi') ) ) 用了这种比较粗鲁的方式添加,设置为 undefined 在实现 render 方法的时候我们会根据这个忽略props内部的 key 和 props 属性,这里就实现了最基本的 createElement 方法了。实现 render 方法render 方法的第一个参数接收的是 createElement 返回的对象,也就是虚拟DOM;第二个参数则是挂载的目标DOM。同样的做法,我们用 babel 编译来看:执行后,就被挂在到页面了实现代码如下:/* * 功能:把创建的对象生成对应的DOM元素,最后插入到页面中 * objJSX: createElement 返回的 JSX 对象 * container:挂载的容器,如 document.getElementById('root') */ function render(objJSX, container) { let { type, props } = objJSX let newElement = document.createElement(type) for (let attr in props) { // 遍历传入的 props 属性 if (!props.hasOwnProperty(attr)) break // 不是私有的直接结束遍历 let value = props[attr] // >如果当前属性没有值,直接不处理即可 if (value == undefined) continue // NULL OR UNDEFINED // 对几个特殊属性单独设置 switch (attr.toUpperCase()) { case 'ID': newElement.setAttribute('id', value) break case 'CLASSNAME': newElement.setAttribute('class', value) break case 'STYLE': // 传入的行内样式 style 是个对象,故需遍历赋值 for (let styleAttr in value) { if (value.hasOwnProperty(styleAttr)) { newElement['style'][styleAttr] = value[styleAttr] } } break case 'CHILDREN': /* * 可能是一个值:可能是字符串也可能是一个JSX对象 * 可能是一个数组:数组中的每一项可能是字符串也可能是JSX对象 */ // 首先把一个值也变为数组,这样后期统一操作数组即可 !(value instanceof Array) ? (value = [value]) : null value.forEach((item, index) => { // 验证ITEM是什么类型的:如果是字符串就是创建文本节点,如果是对象,我们需要再次执行RENDER方法,把创建的元素放到最开始创建的大盒子中 if (typeof item === 'string') { let text = document.createTextNode(item) newElement.appendChild(text) } else { render(item, newElement) } }) break default: newElement.setAttribute(attr, value) } } container.appendChild(newElement) }
commit 阶段流程commit 阶段概览function commitRoot(root) { const renderPriorityLevel = getCurrentPriorityLevel() // 调度 commitRootImpl runWithPriority(ImmediateSchedulerPriority, commitRootImpl.bind(null, root, renderPriorityLevel)) return null } commit 阶段首先会处理上次还未被完成的 effect 异步函数,其次针对 root 上收集的 effectList 进行处理,其余工作可分为三个子阶段:Before Mutation 阶段(执行 DOM 操作前)Mutation 阶段(执行 DOM 操作)Layout 阶段(执行 DOM 操作后)function commitRootImpl(root, renderPriorityLevel) { do { // 执行未执行的 useEffect flushPassiveEffects(); // 底层会调用 `flushPassiveEffectsImpl` 遍历执行 useEffect 的回调和销毁函数 } while (rootWithPendingPassiveEffects !== null); // 判断有无未执行的 useEffect,有的话先执行 // ... // 获取 effectList let firstEffect; if (finishedWork.flags > PerformedWork) { // 把 rootFiber 节点 加入到链表中 if (finishedWork.lastEffect !== null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { // There is no effect on the root. firstEffect = finishedWork.firstEffect; } // ... 省略其它代码,下文说 // Before Mutation 阶段 commitBeforeMutationEffects(); // Mutation 阶段 commitMutationEffects(root, renderPriorityLevel); // Layout 阶段 commitLayoutEffects(root, lanes); } commitRootImpl 函数的代码很长,上面省略为大致流程,该函数最主要做的事情:针对 rootFiber 节点进行处理,将 rootFiber 节点加入到 effectList 中调用 commitBeforeMutationEffects函数(Before Mutation 阶段)调用 commitMutationEffects 函数(Mutation 阶段)调用 commitLayoutEffects 函数(Layout 阶段)Before Mutation 阶段Before Mutation 阶段的入口函数为 commitBeforeMutationEffects,主要作用是:如果是类组件,判断调用 getSnapshotBeforeUpdate,在 commit 前获取 DOM 相关信息如果是函数组件且使用了 useEffect,则异步调度 useEffect(安排回调)function commitBeforeMutationEffects() { while (nextEffect !== null) { const current = nextEffect.alternate // 处理 blur 与 focus 逻辑 if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) { // ... } const flags = nextEffect.flags if ((flags & Snapshot) !== NoFlags) { // 在该函数里会调用 getSnapshotBeforeUpdate commitBeforeMutationEffectOnFiber(current, nextEffect) } if ((flags & Passive) !== NoFlags) { // 异步调度 useEffect if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true scheduleCallback(NormalSchedulerPriority, () => { flushPassiveEffects() return null }) } } nextEffect = nextEffect.nextEffect } } commitBeforeMutationEffectOnFiber 函数:里面会根据不同的 finishedWork.tag 进行处理,如果是 ClassComponent,则会执行 getSnapshotBeforeUpdate 生命周期,并将返回值赋值给 fiber 对象的 __reactInternalSnapshotBeforeUpdate 属性function commitBeforeMutationEffectOnFiber(current, finishedWork) { switch (finishedWork.tag) { // ... case ClassComponent: { if (finishedWork.flags & Snapshot) { if (current !== null) { const prevProps = current.memoizedProps const prevState = current.memoizedState const instance = finishedWork.stateNode // 执行 getSnapshotBeforeUpdate 生命周期 const snapshot = instance.getSnapshotBeforeUpdate( finishedWork.elementType === finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState ) instance.__reactInternalSnapshotBeforeUpdate = snapshot } } return } } } Mutation 阶段Mutation 阶段的入口函数为 commitMutationEffects,主要作用是:遍历 effectList,根据 effectTag 分别处理操作 DOM 节点(如 Placement | Update | Deletion | Hydrating)function commitMutationEffects(root, renderPriorityLevel) { // 遍历 effectList while (nextEffect !== null) { const flags = nextEffect.flags // ... const primaryFlags = flags & (Placement | Update | Deletion | Hydrating) switch (primaryFlags) { // 插入 DOM case Placement: { commitPlacement(nextEffect) nextEffect.flags &= ~Placement break } // 更新组件和 DOM case PlacementAndUpdate: { // 将变化的 DOM 都插入到页面上 commitPlacement(nextEffect) nextEffect.flags &= ~Placement // 更新 const current = nextEffect.alternate commitWork(current, nextEffect) break } // ... // 更新组件 case Update: { const current = nextEffect.alternate commitWork(current, nextEffect) break } // 卸载 case Deletion: { commitDeletion(root, nextEffect, renderPriorityLevel) break } } nextEffect = nextEffect.nextEffect } } Placement(插入)调用了 commitPlacement 方法:获取当前 Fiber 节点的父 Fiber 对应的 DOM 节点和插入位置,根据父节点对应的 DOM 是否为 container,执行 insertOrAppendPlacementNodeIntoContainer 或 insertOrAppendPlacementNode 进行插入操作function commitPlacement(finishedWork) { // 找到最近的 Fiber 节点 const parentFiber = getHostParentFiber(finishedWork) // 获取父级 DOM 节点 const parentStateNode = parentFiber.stateNode // 获取兄弟 DOM 节点 const before = getHostSibling(finishedWork) // 是否 container if (isContainer) { insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent) } else { insertOrAppendPlacementNode(finishedWork, before, parent) } } Update(更新)调用 commitWork,处理由 useLayoutEffect 创建的 effectfunction commitWork(current, finishedWork) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case MemoComponent: case SimpleMemoComponent: case Block: { // 调用 useLayoutEffect 的销毁函数 commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork) return } } } function commitHookEffectListUnmount(tag, finishedWork) { const updateQueue = finishedWork.updateQueue const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null if (lastEffect !== null) { const firstEffect = lastEffect.next let effect = firstEffect do { if ((effect.tag & tag) === tag) { // 如果有定义 effect 的销毁函数,则执行 const destroy = effect.destroy effect.destroy = undefined if (destroy !== undefined) { destroy() } } effect = effect.next } while (effect !== firstEffect) } } HookLayout | HookHasEffect 是一个位运算表达式,表示同时具备两种标识,即表示是 useLayoutEffect 类型的 effectDeletion(删除)调用 commitDeletion 方法,当 fiber.tag 为 ClassComponent 时,执行 componentWillUnmount 生命周期钩子,从页面移除 Fiber 节对应 DOM 节点function commitDeletion(finishedRoot, current, renderPriorityLevel) { if (supportsMutation) { unmountHostComponents(finishedRoot, current, renderPriorityLevel) } else { // Detach refs and call componentWillUnmount() on the whole subtree. commitNestedUnmounts(finishedRoot, current, renderPriorityLevel) } const alternate = current.alternate detachFiberMutation(current) if (alternate !== null) { detachFiberMutation(alternate) } } Layout 阶段Layout 阶段的入口函数为 commitLayoutEffects,作用:针对类组件:调用生命周期 componentDidMount 和 componentDidUpdate,执行 setState 的第二个参数回调函数针对函数组件:执行 useLayoutEffect 的回调函数function commitLayoutEffects(root, committedLanes) { // 遍历 effectList while (nextEffect !== null) { const flags = nextEffect.flags // 调用生命周期钩子和 hook if (flags & (Update | Callback)) { const current = nextEffect.alternate commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes) } // 赋值 ref if (flags & Ref) { commitAttachRef(nextEffect) } nextEffect = nextEffect.nextEffect } } function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { // 循环 FunctionComponent 上的 effect 链 // 执行 useLayoutEffect 的回调 commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork) // 收集 useEffect 需要执行回调和销毁函数 schedulePassiveEffects(finishedWork) return } case ClassComponent: { const instance = finishedWork.stateNode if (finishedWork.flags & Update) { if (current === null) { // 节点首次挂载,执行 componentDidMount instance.componentDidMount() } else { const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps) const prevState = current.memoizedState // 更新 instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate) } } const updateQueue = finishedWork.updateQueue if (updateQueue !== null) { // 执行 setState 的第二个参数回调 commitUpdateQueue(finishedWork, updateQueue, instance) } return } // case ... } } 针对函数组件,会调用 commitHookEffectListMount 和 schedulePassiveEffects 方法// 只处理由 useLayoutEffect 创建的 effect function commitHookEffectListMount(tag, finishedWork) { const updateQueue = finishedWork.updateQueue const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null if (lastEffect !== null) { const firstEffect = lastEffect.next let effect = firstEffect do { if ((effect.tag & tag) === tag) { // 执行副作用 effect 创建函数,并将返回值(cleanup 函数)存储到 `destroy` 属性上 // 在 Mutation 阶段已经执行了 useLayoutEffect 的销毁函数,在此执行创建函数,确保每次 useLayoutEffect 的执行的卸载与创建顺序正确 const create = effect.create effect.destroy = create() } effect = effect.next } while (effect !== firstEffect) } } // 收集 useEffect 待执行的回调和销毁函数 function schedulePassiveEffects(finishedWork) { const updateQueue = finishedWork.updateQueue; const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null; if (lastEffect !== null) { const firstEffect = lastEffect.next; let effect = firstEffect; do { const { next, tag } = effect; if ( // HookPassive 标识 useEffect (tag & HookPassive) !== NoHookEffect && // 依赖数组没有发生变化 (tag & HookHasEffect) !== NoHookEffect ) { // 收集待执行的销毁函数到数组 enqueuePendingPassiveHookEffectUnmount(finishedWork, effect); // 收集待执行的回调函数到数组 enqueuePendingPassiveHookEffectMount(finishedWork, effect); } effect = next; } while (effect !== firstEffect); } } 总结commit 阶段可分为三个子阶段:Before Mutation 阶段:读取组件变更前的状态。其中针对类组件会调用 getSnapshotBeforeUpdate 生命周期函数;调度 useEffectMutation 阶段:更新 DOM 节点的阶段。如插入、更新以及删除操作。针对类组件会调用 componentWillUnmount 生命周期函数;针对函数组件,则会执行 useLayoutEffect 的销毁函数Layout 阶段:调用类组件的 componentDidMount、componentDidUpdate 生命周期函数,执行 setState 的第二个回调参数;针对函数组件会执行 useLayoutEffect 的 effect 回调,并处理 useLayoutEffect以上就是各个 commit 阶段的大致工作,当然描述的内容肯定不全,只是挑出一些重点,具体各位可以细看源码。
1、使用dumi创建 dumi是一个开源的负责组件开发及组件文档生成的工具,这里仅为了方便组件库文档展示使用。在最后打包发布时,dumi不参与打包,所以这里使用dumi是可以的。接下来就开始记录实操步骤。首先安装 node,并确保node >= 10.13 && node < 17.6.0。这里作者亲测,node 17.6.0是不兼容的,而作者本地使用的v16.14.2可以完全兼容。在空白的地方新建文件夹mkdir dux-ui && cd ./dux-ui然后执行安装命令,这里我选择站点式的创建方式$ npx @umijs/create-dumi-lib # 初始化一个文档模式的组件库开发脚手架 # or $ yarn create @umijs/dumi-lib $ npx @umijs/create-dumi-lib --site # 初始化一个站点模式的组件库开发脚手架 # or $ yarn create @umijs/dumi-lib --site在根目录下执行命令npm install npm run dev然后项目就跑起来了!(盗用官网的图,仅供参考)2、文件目录脚手架搭建起项目后,可以看到初始文件目录、里为了开发方便,我们把自定义的组件放在单独的components文件夹下:然后修改src下的index文件中组件的导入路径:export { default as Foo } from './components/Foo'; 修改dumi配置文件.umirc.ts,新增menu展示路径:import { defineConfig } from 'dumi'; export default defineConfig({ title: 'test-dumi', favicon: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png', logo: 'https://user-images.githubusercontent.com/9554297/83762004-a0761b00-a6a9-11ea-83b4-9c8ff721d4b8.png', outputPath: 'docs-dist', mode: 'site', // 新增 menus: { // 需要自定义侧边菜单的路径,没有配置的路径还是会使用自动生成的配置 '/components': [ { title: '组件', path: '/components', children: [ // 菜单子项(可选) 'components/Foo/index.md', ], }, ], }, });修改package.json然后执行npm run docs可以看到跑起来了至此,项目就搭建起来了!本章到此结束,下一节会逐步记录各个组件的开发过程。
React合成事件是React提供的一种事件处理机制,它抽象了浏览器原生事件,提供了一些额外的特性和优化。React合成事件的主要特点包括:与原生事件兼容:React合成事件模拟了大部分浏览器原生事件的接口和行为,因此可以直接替换掉原生事件处理函数。跨平台支持:React合成事件可以在不同平台上使用相同的代码,无需考虑浏览器兼容性问题。事件委托:React合成事件采用了事件委托的方式处理事件,减少了事件监听器的数量,提高了性能。自动化内存管理:React合成事件会在组件卸载时自动销毁相关的事件监听器,避免了内存泄漏问题。React合成事件的具体实现方式包括:将所有事件绑定到document上,通过事件冒泡的方式传递到目标元素。(件委托机制冒泡到 root(react 17) 或者 document(react 16))将原生事件封装成合成事件,添加一些额外的属性和方法(如stopPropagation()、preventDefault()等)。将合成事件传递给组件的事件处理函数中,在处理函数中可以直接访问到合成事件对象,而无需关心浏览器兼容性问题。事件执行流程react 事件冒泡捕捉1.事件的注册事件监听使用addEventListener来实现冒泡和捕捉import { registerTwoPhaseEvent } from "./EventRegistry"; export function createRoot( container: Element | Document | DocumentFragment, options?: CreateRootOptions ): RootType { const rootContainerElement: Document | Element | DocumentFragment = container.nodeType === COMMENT_NODE ? (container.parentNode: any) : container; // 在createRoot的时候监听事件,listenToAllSupportedEvents中调用EventListener.js中的监听方法来监听事件,如addEventBubbleListener,addEventBubbleListener listenToAllSupportedEvents(rootContainerElement); // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions return new ReactDOMRoot(root); } // const allNativeEvents: Set<DOMEventName> = new Set(); export function listenToAllSupportedEvents(rootContainerElement: EventTarget) { if (!(rootContainerElement: any)[listeningMarker]) { (rootContainerElement: any)[listeningMarker] = true; allNativeEvents.forEach(domEventName => { // We handle selectionchange separately because it // doesn't bubble and needs to be on the document. if (domEventName !== 'selectionchange') { if (!nonDelegatedEvents.has(domEventName)) { //注册冒泡阶段 listenToNativeEvent(domEventName, false, rootContainerElement); } //注册捕获阶段 listenToNativeEvent(domEventName, true, rootContainerElement); } }); } } export function listenToNativeEvent( domEventName: DOMEventName, isCapturePhaseListener: boolean, target: EventTarget, ): void { let eventSystemFlags = 0; if (isCapturePhaseListener) { eventSystemFlags |= IS_CAPTURE_PHASE; } //注册监听函数 addTrappedEventListener( target, domEventName, eventSystemFlags, isCapturePhaseListener, ); } function addTrappedEventListener( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, isCapturePhaseListener: boolean, isDeferredListenerForLegacyFBSupport?: boolean, ) { let listener = createEventListenerWrapperWithPriority( targetContainer, domEventName, eventSystemFlags, ); // 后面的方法中调用addEventCaptureListener,addEventBubbleListenerWithPassiveFlag,addEventBubbleListener等函数 unsubscribeListener = addEventCaptureListener( targetContainer, domEventName, listener, ); } // EventListener.js export function addEventBubbleListener( target: EventTarget, eventType: string, listener: Function ): Function { target.addEventListener(eventType, listener, false); return listener; } export function addEventCaptureListener( target: EventTarget, eventType: string, listener: Function ): Function { target.addEventListener(eventType, listener, true); return listener; } const topLevelEventsToReactNames: Map<DOMEventName, string | null> = new Map(); // DOMEventProperties.js function registerSimpleEvent(domEventName: DOMEventName, reactName: string) { topLevelEventsToReactNames.set(domEventName, reactName); registerTwoPhaseEvent(reactName, [domEventName]); } export function registerSimpleEvents() { for (let i = 0; i < simpleEventPluginEvents.length; i++) { const eventName = ((simpleEventPluginEvents[i]: any): string); const domEventName = ((eventName.toLowerCase(): any): DOMEventName); const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1); registerSimpleEvent(domEventName, "on" + capitalizedEvent); } // Special cases where event names don't match. registerSimpleEvent(ANIMATION_END, "onAnimationEnd"); registerSimpleEvent(ANIMATION_ITERATION, "onAnimationIteration"); registerSimpleEvent(ANIMATION_START, "onAnimationStart"); registerSimpleEvent("dblclick", "onDoubleClick"); registerSimpleEvent("focusin", "onFocus"); registerSimpleEvent("focusout", "onBlur"); registerSimpleEvent(TRANSITION_END, "onTransitionEnd"); } // EventRegistry.js const allNativeEvents: Set<DOMEventName> = new Set(); export function registerTwoPhaseEvent( registrationName: string, dependencies: Array<DOMEventName> ): void { registerDirectEvent(registrationName, dependencies); registerDirectEvent(registrationName + "Capture", dependencies); } export function registerDirectEvent( registrationName: string, dependencies: Array<DOMEventName> ) { registrationNameDependencies[registrationName] = dependencies; for (let i = 0; i < dependencies.length; i++) { allNativeEvents.add(dependencies[i]); } } // DOMPluginEventSystem.js function dispatchEventsForPlugins( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, nativeEvent: AnyNativeEvent, targetInst: null | Fiber, targetContainer: EventTarget ): void { const nativeEventTarget = getEventTarget(nativeEvent); const dispatchQueue: DispatchQueue = []; extractEvents( dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer ); processDispatchQueue(dispatchQueue, eventSystemFlags); } export function processDispatchQueue( dispatchQueue: DispatchQueue, eventSystemFlags: EventSystemFlags ): void { const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0; for (let i = 0; i < dispatchQueue.length; i++) { const { event, listeners } = dispatchQueue[i]; processDispatchQueueItemsInOrder(event, listeners, inCapturePhase); // event system doesn't use pooling. } // This would be a good time to rethrow if any of the event handlers threw. rethrowCaughtError(); } // ReactDOMEventListener.js 2.事件绑定// dispatchEventForPluginEventSystem、 // createEventListenerWrapperWithPriority方法中注册事件回调的函数, // 还记得事件的优先级么? //离散事件优先级 click onchange export const DiscreteEventPriority = SyncLane; //1 //连续事件的优先级 mousemove export const ContinuousEventPriority = InputContinuousLane; //4 //默认事件车道 export const DefaultEventPriority = DefaultLane; //16 //空闲事件优先级 export const IdleEventPriority = IdleLane; // export function createEventListenerWrapperWithPriority( targetContainer: EventTarget, domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, ): Function { const eventPriority = getEventPriority(domEventName); let listenerWrapper; switch (eventPriority) { case DiscreteEventPriority: listenerWrapper = dispatchDiscreteEvent; break; case ContinuousEventPriority: listenerWrapper = dispatchContinuousEvent; break; case DefaultEventPriority: default: listenerWrapper = dispatchEvent; break; } return listenerWrapper.bind( null, domEventName, eventSystemFlags, targetContainer, ); } function dispatchDiscreteEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, container: EventTarget, nativeEvent: AnyNativeEvent, ) { const previousPriority = getCurrentUpdatePriority(); const prevTransition = ReactCurrentBatchConfig.transition; ReactCurrentBatchConfig.transition = null; try { setCurrentUpdatePriority(DiscreteEventPriority); dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent); } finally { setCurrentUpdatePriority(previousPriority); ReactCurrentBatchConfig.transition = prevTransition; } } 3.事件触发 export function dispatchEvent( domEventName: DOMEventName, eventSystemFlags: EventSystemFlags, targetContainer: EventTarget, nativeEvent: AnyNativeEvent, ): void { if (!_enabled) { return; } dispatchEventOriginal( domEventName, eventSystemFlags, targetContainer, nativeEvent, ); } // 执行 if (blockedOn === null) { dispatchEventForPluginEventSystem( domEventName, eventSystemFlags, nativeEvent, return_targetInst, targetContainer, ); if (allowReplay) { clearIfContinuousEvent(domEventName, nativeEvent); } return; } // extractEvents中存储事件 processDispatchQueue(dispatchQueue, eventSystemFlags); /** + * 处理dispatch方法 + * @param {*} event 合成事件对象 + * @param {*} dispatchListeners 监听函数 + * @param {*} inCapturePhase 是否是获取阶段 + */ // processDispatchQueueItemsInOrder处理冒泡或者捕捉 function processDispatchQueueItemsInOrder( event: ReactSyntheticEvent, dispatchListeners: Array<DispatchListener>, inCapturePhase: boolean, ): void { let previousInstance; if (inCapturePhase) {//因为收集的时候是从内往外,所以捕获阶段是倒序执行 for (let i = dispatchListeners.length - 1; i >= 0; i--) { const {instance, currentTarget, listener} = dispatchListeners[i]; if (instance !== previousInstance && event.isPropagationStopped()) { return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; } } else { for (let i = 0; i < dispatchListeners.length; i++) { const {instance, currentTarget, listener} = dispatchListeners[i]; if (instance !== previousInstance && event.isPropagationStopped()) { return; } executeDispatch(event, listener, currentTarget); previousInstance = instance; } } } 另外在 React 事件系统中还有其他事件监听,大家感兴趣可以去了解源码,这些方法的作用如下:listenToNativeEvent:监听原生 DOM 事件,可以用于处理自定义组件中的原生事件。 listenToNonDelegatedEvent:监听非委托事件(即不冒泡的事件)。 listenToNativeEventForNonManagedEventTarget:为非受控事件目标添加原生事件监听器,例如在自定义组件内部使用的第三方库。 listenToAllSupportedEvents:监听组件支持的所有事件。
react 状态管理react 是一个用于用户界面构建的库,提供了 createContext 来进行状态管理,当复杂项目中 context 就捉襟见肘了,需要使用合理的工具来管理状态。1. react 中常用状态管理a. 父子组件通过 props 传递数据, 当属性跨层级传递时不是很方便。function Parent() { const a = 1; return ( <div> <Child a={a} /> </div> ); } function Child(props) { console.log(props); //{a: 1} return <div>{props.a}</div>; } b.createContext react提供了createContext,创建上下文context来实现属性的跨层级传递。React.createContext()方法是React提供的一个用于创建上下文的方法。通过创建上下文,可以在组件之间共享数据,避免了通过 props 一层层地传递数据的繁琐过程。使用上下文,可以更方便地在组件树中的任何地方访问共享的数据。import { createContext } from "react"; //使用React.createContext()方法创建上下文 const MyContext = createContext(); function Text() { const obj = { a: 1, b: false }; return ( // 需要注意的是,Consumer组件必须在Provider组件的子组件中使用,否则无法获取到上下文中的值。 //组件中使用Provider组件来提供共享的数据 <MyContext.Provider value={obj}> <Child /> </MyContext.Provider> ); } function Child() { console.log(MyContext); /** * $$typeof: Symbol(react.context) * Consumer:{$$typeof: Symbol(react.context), _context: {…}, …} * Provider: {$$typeof: Symbol(react.provider), _context: {…}} * _currentValue: {a: 1, b: false} */ //在Consumer组件中,使用一个函数作为子元素,并将上下文中的数据作为参数传递给这个函数。在这个函数中,可以使用上下文中的数据 return ( <div> <p>test context</p> <MyContext.Consumer> {(value) => { return <div>{value.a}</div>; }} </MyContext.Consumer> </div> ); } c. redux 中文官网。为了我们开发方便,可以使用redux-devtools来查看redux状态。 Redux中,处理异步操作需要使用中间件,如redux-thunk或redux-saga,redux-promise等中间件redux使用 <!-- 创建store --> let store = createStore(combinedReducer); //action export const ADD1 = "ADD1"; export const MINUS1 = "MINUS1"; import { ADD1, MINUS1 } from "../action-types"; function add1() { //actionCreator 它是一个创建action的函数 return { type: ADD1 }; } //reducers import { ADD1, MINUS1, DOUBLE } from "../action-types"; let initState = { number: 0 }; const reducer = (state = initState, action) => { switch (action.type) { case ADD1: return { number: state.number + 1 }; case MINUS1: return { number: state.number - 1 }; case DOUBLE: return { number: state.number * 2 }; default: return state; } }; // 使用combineReducers方法合并reducer let combinedReducer = combineReducers({ counter1, counter2, }); // 在组件中的使用 import actionCreators from "../store/actionCreators/counter1"; import { connect } from "react-redux"; class Counter1 extends React.Component { render() { return ( <div> <p>{this.props.number}</p> <button onClick={this.props.add1}>+</button> </div> ); } } //把仓库中的状态映射为组件的属性对象 仓库到组件的输出 const mapStateToProps = (state) => state.counter1; //const mapDispatchToProps = dispatch => bindActionCreators(actionCreators, dispatch) export default connect( mapStateToProps, actionCreators //组件的输出,在组件里派发动作,修改仓库 )(Counter1); d. redux-tookit 用于简化Redux开发的工具集。它提供了一组用于处理常见Redux任务的工具和API,使得开发者可以更快、更简单地编写Redux代码。英文官网:redux-toolkit.js.org/import { configureStore } from "@reduxjs/toolkit"; import rootReducer from "./reducers"; //configureStore函数,用于创建Redux store。这个函数封装了常见的Redux配置,包括合并reducer、应用中间件和DevTools等。使用configureStore函数可以减少配置代码的编写量,并提供了一些默认的配置选项 const store = configureStore({ reducer: rootReducer, }); const initialState = { value: 0 }; function counterReducer(state = initialState, action) { switch (action.type) { case "increment": return { ...state, value: state.value + 1 }; case "decrement": return { ...state, value: state.value - 1 }; case "incrementByAmount": return { ...state, value: state.value + action.payload }; default: return state; } } function increment(amount: number) { return { type: INCREMENT, payload: amount, }; } const action = increment(3); //createSlice:Redux Toolkit提供了一个createSlice函数,用于创建Redux的slice。slice是一个包含了reducer和action的对象,用于定义和处理Redux的一部分状态。使用createSlice函数可以更简洁地定义slice,并自动生成对应的reducer和action。 const counterSlice = createSlice({ name: "counter", initialState, reducers: { increment(state) { state.value++; }, decrement(state) { state.value--; }, incrementByAmount(state, action: PayloadAction<number>) { state.value += action.payload; }, }, }); e. dva dva // 创建应用 const app = dva(); // 注册 Model app.model({ namespace: "count", state: 0, reducers: { add(state) { return state + 1; }, }, effects: { //Effect是一个Generator函数,用于处理异步的副作用操作。在Dva中,每个Model可以定义一个或多个effect函数,用于处理异步的操作,如发送网络请求、访问浏览器缓存等。Effect函数通过使用Dva提供的一些内置的effect函数,如call、put、select等,来处理异步操作。 *addAfter1Second(action, { call, put }) { yield call(delay, 1000); yield put({ type: "add" }); }, }, }); // 注册视图 app.router(() => <ConnectedApp />); // 启动应用 app.start("#root"); f. rematch 基于Redux的轻量级框架,用于简化Redux应用程序的开发。它提供了一种简单的方式来定义和管理Redux的模型(Model),并提供了一些内置的功能和约定,使得开发者可以更快、更简单地编写Redux代码。Rematch使用了redux-saga来处理异步操作,使得处理异步操作更简单和直观。 Rematch提供了一个插件系统,可以通过插件来扩展和定制Rematch的功能。插件可以用于添加中间件、增强Model、添加额外的功能等。这使得开发者可以根据实际需求来选择和使用插件,从而更灵活地定制Rematch的功能。// 在Model中,可以定义state属性来表示状态的初始值。reducers属性用于定义同步的状态更新,effects属性用于定义异步的副作用操作。 // 过使用connect函数,可以将组件与Rematch store连接起来。mapState函数用于将状态映射到组件的props中,mapDispatch函数用于将action函数映射到组件的props中 export const countModel = { state: { counter: 0 }, // initial state reducers: { add: (state, payload) => { return { ...state, counter: state.counter + payload, }; }, }, effects: { async loadData(payload, rootState) { const response = await fetch(`http://xxx.com/${payload}`); const data = await response.json(); this.add(data); // dispatch action to a local reducer }, }, }; // 创建一个Rematch store: // 在init函数中,可以通过models属性来定义一个或多个Model。每个Model包含了reducers、effects和selectors等属性,用于定义和管理状态和副作用。 import { init } from "@rematch/core"; const store = init({ models: { // 定义Model }, }); 还有其他状态管理工具如 MobX,Zustand,React Query,感兴趣的可以去了解下。2. redux解析redux 核心思想数据向下流动Redux 是将整个应用状态存储到到一个地方,称为 store,里面保存一棵状态树 state tree组件可以派发 dispatch 行为 action 给 store,而不是直接通知其它组件其它组件可以通过订阅 store 中的状态(state)来刷新自己的视图redux 三大原则整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中State 是只读的,唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象 使用纯函数来执行修改,为了描述 action 如何改变 state tree ,你需要编写 reducers单一数据源的设计让 React 的组件之间的通信更加方便,同时也便于状态的统一管理版本redux@4.2.0export { createStore, legacy_createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes, }; // __DO_NOT_USE__ActionTypes // 这是redux内部的默认action,我们在设置自己的action的时候,不要和他们重复 const ActionTypes = { INIT: `@@redux/INIT${randomString()}`, REPLACE: `@@redux/REPLACE${randomString()}`, PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`, }; 2.1 createStoreexport function createStore(reducer, preloadedState, enhancer) { if ( (typeof preloadedState === "function" && typeof enhancer === "function") || (typeof enhancer === "function" && typeof arguments[3] === "function") ) { // 这里进行参数校验 } if (typeof preloadedState === "function" && typeof enhancer === "undefined") { enhancer = preloadedState; preloadedState = undefined; } if (typeof enhancer !== "undefined") { if (typeof enhancer !== "function") { throw new Error( `Expected the enhancer to be a function. Instead, received: '${kindOf( enhancer )}'` ); } // 如果有enhancer则在enhancer内部初始化store return enhancer(createStore)(reducer, preloadedState); } // reducer是纯函数,在函数内部修改state。 if (typeof reducer !== "function") { throw new Error( `Expected the root reducer to be a function. Instead, received: '${kindOf( reducer )}'` ); } let currentReducer = reducer; let currentState = preloadedState; let currentListeners = []; let nextListeners = currentListeners; let isDispatching = false; function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice(); } } function getState() { return currentState; } // subcribe是我们订阅事件的地方,subcribe返回一个卸载函数。 // 订阅状态变化事件,当状态发生改变后执行所有的监听函数、 // 创建Redux store时,可以通过调用store.subscribe()方法来订阅状态的变化。该方法接受一个回调函数作为参数,这个回调函数会在状态发生变化时被调用 function subscribe(listener) { let isSubscribed = true; ensureCanMutateNextListeners(); nextListeners.push(listener); // 如果不再需要监听状态的变化,应该及时取消订阅,以避免不必要的性能消耗。可以通过调用返回的取消订阅函数来取消订阅,例如:const unsubscribe = store.subscribe(callback),然后在不需要监听时调用unsubscribe()即可。 return function unsubscribe() { if (!isSubscribed) { return; } isSubscribed = false; ensureCanMutateNextListeners(); const index = nextListeners.indexOf(listener); nextListeners.splice(index, 1); currentListeners = null; }; } // dispatch是一个用于触发状态变化的方法。它是Redux中唯一能够改变状态的方式。 // 通过调用store.dispatch()方法来触发状态的变化。该方法接受一个action对象作为参数,这个action对象描述了状态变化的类型和相关的数据。 function dispatch(action) { if (!isPlainObject(action)) { // 判断action的类型 } if (typeof action.type === "undefined") { // 判断action的类型 } // 调用dispatch方法时,Redux会根据action对象的类型来执行相应的reducer函数。reducer函数是一个纯函数,它接受当前的状态和action对象作为参数,并返回一个新的状态。 try { isDispatching = true; currentState = currentReducer(currentState, action); } finally { isDispatching = false; } // 在这里执行subscribe订阅的方法,响应式更新 const listeners = (currentListeners = nextListeners); for (let i = 0; i < listeners.length; i++) { const listener = listeners[i]; listener(); } return action; } // replaceReducer是一个用于替换当前reducer的方法。它允许我们在运行时动态地替换Redux store中的reducer函数。 function replaceReducer(nextReducer) { currentReducer = nextReducer; dispatch({ type: ActionTypes.REPLACE }); } //当创建一个存储时,会调度一个“INIT”操作,以便reducer返回其初始状态。这有效地填充初始状态树。 dispatch({ type: ActionTypes.INIT }); return { dispatch, subscribe, getState, replaceReducer, }; } export const legacy_createStore = createStore; 2.2 bindActionCreatorsbindActionCreators 用于绑定action creators到dispatch的方法,简化在组件中使用action creators的过程. 返回一个与原始action creators具有相同键值对的对象,但是每个action creator都会被自动调用dispatch函数进行包装。function bindActionCreators(actionCreators, dispatch) { if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch) } const boundActionCreators = {} for (const key in actionCreators) { const actionCreator = actionCreators[key] if (typeof actionCreator === 'function') { boundActionCreators[key] = bindActionCreator(actionCreator, dispatch) } } return boundActionCreators } // 返回调用dispatch函数的回调 function bindActionCreator(actionCreator, dispatch) { return function () { return dispatch(actionCreator.apply(this, arguments)) } } 2.3 applyMiddlewareapplyMiddleware是一个用于redux使用中间件的方法。允许我们在Redux的数据流中插入自定义的逻辑,以处理异步操作、日志记录、错误处理等。 创建store时,可以通过调用applyMiddleware()方法来应用中间件。该方法接受一个或多个中间件函数作为参数,并返回一个增强后的store创建函数中间件函数是一个接受store的dispatch函数作为参数的函数。在dispatch函数被调用之前或之后执行一些额外的逻辑。中间件函数可以访问store的getState方法来获取当前的状态,也可以调用dispatch方法来触发下一个中间件或reducer函数。 我们常用的redux中间件有:redux-thunk中间件来处理异步操作,redux-logger中间件来记录日志,redux-promise中间件来处理Promise对象,redux-saga处理异步操作等。function applyMiddleware(...middlewares) { return (createStore) => (...args) => { const store = createStore(...args) let dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args), } const chain = middlewares.map((middleware) => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch, } } } // 中间件函数的顺序非常重要。它们会按照顺序依次执行,因此前一个中间件的输出会成为下一个中间件的输入。在编写中间件时,我们需要确保它们的顺序是正确的,以便实现预期的逻辑。 // compose是一个用于组合函数的方法。允许我们将多个函数按照从右到左的顺序依次执行,并将每个函数的输出作为下一个函数的输入。 // 调用compose(f, g, h)时,它会返回一个新的函数,这个新的函数等价于f(g(h(...args)))。也就是说,它会将参数args依次传递给h、g和f,并将每个函数的输出作为下一个函数的输入。 function compose(...funcs) { if (funcs.length === 0) { return (arg) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))) } 2.4 redux中间件//日志中间件 中间件的格式是固定的 function logger({ getState, dispatch }) { return function (next) { return function (action) {//此方法就是我们改造后的dispatch方法 console.log('老状态', getState()); next(action);//调用原始的dispatch方法,传入动作action,发给仓库,仓库里会调用reducer计算新的状态 console.log('新状态', getState()); return action; } } } function promise({ getState, dispatch }) { return function (next) { return function (action) {//此方法就是我们改造后的dispatch方法 if (action.then && typeof action.then === 'function') { action.then(dispatch) } else { next(action); } } } } function thunk({ getState, dispatch }) { return function (next) { return function (action) {//此方法就是我们改造后的dispatch方法 if (typeof action === 'function') { //把新的dispatch传递给了函数,这样就可以在函数里派发动作 return action(getState, dispatch); } return next(action); } } } 3. 总结主要分为两部分:第一部分简单介绍了react常用的状态管理工具,第二部分分析了redux的源码和中间件。后面有时间介绍下react和redux连接的react-redux。
React 时间切片是 React 通过将任务分割成小的时间片,然后分批次去处理任务以提高应用程序性能的一种技术。本文将介绍 React 时间切片并提供一个简单的教程,以便开发者学习相关知识。什么是时间切片?在 React 应用程序中,多个任务需要同时被执行,例如渲染组件、处理用户输入、更新状态等。如果所有的任务都在同一时间内执行,那么它们之间将会互相干扰,导致应用程序的性能下降和用户体验变差。时间切片是一种将任务分割成小的时间段的技术,这样一来,任务就可以独立运行并分批次处理。时间切片的一个重要方面是任务优先级。React 将任务分为四个优先级:Immediate,User-blocking,Normal 和 Low。这些优先级是确定任务完成顺序的关键。时间切片的主要优点:提高应用程序的响应性和流畅度。分批次运行任务可以避免长时间占用 CPU。更好地控制渲染过程,让用户可以快速看到应用程序的变化。如何实现时间切片?React 时间切片的实现依赖于新的 Scheduler API。这个 API 作为一组工具和算法来管理任务并将它们排入队列。由于 React 时间切片插件已经预先包含在 create-react-app 中,所以你不需要重新配置你的应用程序。下面是一个简单的应用程序,其中包含了一些使用时间切片的异步任务:import React, { useState } from 'react'; import { unstable_scheduleCallback } from 'scheduler'; // 设置任务的优先级 const PRIORITY = { IMMEDIATE: 1, USER_BLOCKING: 2, NORMAL: 3, LOW: 4, }; function App() { const [ count, setCount ] = useState(0); // 定义一个时间片任务 const sliceTask = ({ priority = PRIORITY.NORMAL, onStart, onEnd }) => { unstable_scheduleCallback(priority, () => { if (onStart) { onStart(); } const result = runAsyncTask(); if (onEnd) { onEnd(result); } }); } // 模拟一个异步任务 const runAsyncTask = async () => { console.log('start task'); await new Promise(resolve => setTimeout(resolve, 1000)); console.log('end task'); return 'done'; }; // 处理用户输入 const handleClick = () => { setCount(count + 1); // 启动时间切片任务 sliceTask({ priority: PRIORITY.IMMEDIATE, onStart: () => console.log('task started'), onEnd: result => console.log(`task finished with result ${result}`), }); }; return ( <div> <p>Count: {count}</p > <button onClick={handleClick}>Click Me</button> </div> ); } export default App; 在这个示例中,我们使用了 Scheduler API 中的 unstable_scheduleCallback 方法来实现时间切片,定义了一个运行异步任务的 sliceTask 函数。我们使用 useState hook 来保存状态,然后在 handleClick 函数中调用 sliceTask 函数,在按钮单击时启动一个优先级为 Immediate 的时间切片任务。总结React 时间切片是一种通过将任务分割成小的时间片然后分批次处理任务以提高应用程序性能的技术。除了优化应用程序性能,时间切片还可以更好地控制渲染过程,以便用户可以快速看到应用程序的变化。要实现时间切片,React 提供了 Scheduler API 作为一组工具和算法来管理任务并将它们排入队列。开发者可以使用 Scheduler API 来实现任务的优先级和任务的调度,来提高应用程序的性能和用户体验。
Icon 图标的设计是个技术活,需要设计出自己专属的风格,就像上一期一开始讲的那样,Style风格设计是三要素之首。基础组件设计,比如按钮是扁平还是立体,输入框是方角还是圆角,加不加阴影等这些,受项目经理、产品和交互的影响会多一点,但是大体来说,应与公司同类产品保持一致,至于如何设计,那就是另外的课题了,不在本文的讨论之列。 本文中的Icon图标使用字体库来完成,通过CSS无侵入式的在一个元素上加入before或者after伪类来实现图标显示,这里不是浏览器的字体,也不是客户电脑里安装的字体,也不是图片或其他方式,而且是以文字的方式显示,这样做相对比较简洁,方便修改,更重要的是利于SEO优化。浏览器兼容性比较好的字体库有WOFF、WOFF2等。字体库兼容性见官方解释。字体库有专门的自定义生成工具,例如fonteditor,可以试用30天;至于字体库,你也可以使用第三方开源的字体库,例如Font Awesome。这里作者就使用Font Awesome的woff字体库,我们打开图形创建工具fonteditor来查看这个文件: 可以看到,每一个Icon都有对应的Code-points,这样我们就能通过CSS来配置字体图标了:// 上图中玻璃杯 .glass:before { content: '\F000'; }设计思路有了,接下来就开始动工!Icon.tsx 在src/components文件夹下,新建文件夹Icon,在该文件夹下新建Icon.tsx:... // 定义接收参数 export interface IconProps { /** 图标类型 */ type: string; /** 是否旋转 */ spin?: boolean; /** 自定义 icon 类名前缀,使用自定义图标库时使用,默认为 icon\_\_ */ prefix?: string; } // 图标控件 const Icon = ({ type, spin, prefix, className, ...rest } => { const configContext = useContext(ConfigContext); const finalPrefix = useMemo(() => prefix || configContext.iconDefaultPrefix || 'icon__', [ configContext.iconDefaultPrefix, prefix ]); return ( <IconWrap className={classnames(prefixCls, `${finalPrefix}${type}`, spin && `${prefixCls}-spin`, className)} spin={spin} {...rest} /> ); }; export default React.memo(Icon); 这里可以看到,我在全局configContext定义了默认的图标类名前缀,默认为icon__,你也可以自定义,只要和CSS样式对应即可。最后返回的是一个IconWrap组件,我们在Icon文件夹下新建style文件夹来放置样式包裹类,style下设置font文件夹来安置我们下载的WOFF文件,并在style同目录下新建icon.css和index.ts:src/components/Icon/style/icon.css:/* 自定义专属的字体类型 */ @font-face { font-family: duiicon; /* src: url(./fonts/duiicon.eot?v=1552285261926); */ src: url(./fonts/fontawesome-webfont.woff) format('woff'); font-weight: 400; font-style: normal; } ... /* 设置字体对应的类 */ [class*=' icon__']:before, [class^='icon__']:before { display: inline-block; } .icon__glass:before { content: '\F000'; } ...src/components/Icon/style/index.ts:import styled from '@emotion/styled'; import { css } from '@emotion/core'; // spinMixin是公共的旋转样式,详见全部代码 const iconSpinMixin = css` ${spinMixin}; line-height: normal; `; export const IconWrap = styleWrap<{ spin?: boolean }>({})( styled('i')(props => { const { spin } = props; return css` vertical-align: baseline; &&& { ${spin && iconSpinMixin}; } `; }) ); 从代码中看到,虽然这里面没有用到主题样式的变量,但为了风格统一,这里用styleWrap包裹一下,不给输入参数即可,其返回的仍然是一个回调函数,接受一个函数式组件作为参数,这里传递一个i标签:styled('i'),参数props是组件IconWrap接受的参数,如果有spin旋转,那就加上旋转的样式。 关于前缀的拼接,我这里说一下:classnames(prefixCls, `${finalPrefix}${type}`, spin && `${prefixCls}-spin`, className)第一段是公共样式,这里为dux-ui-icon,可以理解为来自组件库的标识,也方便用户在使用时批量添加样式;第二段就是我们代码中拼接的icon__glass等,用于实际图标显示;第三段是旋转标识;第四段提供了用户自定义class。现在,代码目录结构如下:当然别忘了在src/index导出:export { default as Icon } from './components/Icon';Icon demo我们在同目录下的index.md中写上demo用例:import React from 'react'; // dumi-dux-ui要与你package.json中的name一致 import { Icon } from 'dumi-dux-ui'; import Copy from 'copy-to-clipboard'; // demo start const layout = { style: { marginRight: 10 } }; const Icons = [ 'glass', ]; const TypeDemo = () => ( <div style={{ display: "flex" }}> {Icons.map(item => ( <div key={item} style={{ width: '50px', height: '50px', cursor: 'pointer' }} onClick={() => Copy(item)}> <Icon type={item} {...layout} /> </div> ))} </div> );修改.umirc.ts:... menus: { // 需要自定义侧边菜单的路径,没有配置的路径还是会使用自动生成的配置 '/components': [ { title: '组件', path: '/components', children: [ // 菜单子项(可选) 'components/Icon/index.md', ], }, ], },然后在根目录下执行:npm run docs,打开浏览器,进入localhost:8000/components/icon即可看到:我们的玻璃杯图标加载出来了!F12查看元素,确实是我们想要的加载方式:关于 @emotion中的styled和css方法,可以避免使用外挂css文件,同时组件传递参数更加方便,当然也可以完全不用styled,如下面代码所示,其效果是等价的。详细使用可以查看emotion官网function Content(props: any) { // props的属性需要特殊处理,可传递className属性,通过外挂css实现相同的样式 ... return <i {...props}></i> } export const IconWrap = styleWrap<{ spin?: boolean }>({})(Content);Button 按钮一般会分类别,不同的类别有不同的颜色,我们分为实心,边框空心和禁用三种模式。下面列一个表格说明所有的button样式:类别名称种类名集合StyleTypes 种类['primary', 'warning', 'success', 'error', 'border', 'border-gray']Sizes 大小['sm', 'md', 'lg']Shapes 形状['circle', 'square']Shadowed 阴影['true', 'false']Loading 加载['true', 'false']Disabled 禁用['true', 'false']Block 块级显示['true', 'false']我们根据罗列的类型,开始搭建组件!搭建基础 我们在src/components文件夹下新建文件夹Button,在该目录下新建文件index.tsx:// 定义接受参数,为表格中罗列属性 export interface ButtonProps { /** 按钮类型 */ styleType?: 'primary' | 'warning' | 'success' | 'error' | 'border' | 'border-gray'; /** 按钮尺寸 */ size?: 'sm' | 'md' | 'lg'; /** 形状 */ shape?: 'circle' | 'square'; /** 阴影 */ shadowed?: boolean; /** 主题 */ // theme?: 'dark'; /** 是否加载中 */ loading?: boolean; /** 图标 */ icon?: string | ReactNode; /** 设置原生的 button 上 type 属性 */ type?: string; /** 展示设置为块元素 */ block?: boolean; }同样的,我们使用一个样式类包裹一下原生的button(还是在这个文件):... render() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { loading, icon, children, ...rest } = this.props; return ( <StyleButton loading={loading} {...rest}> // renderIcon为挂载按钮内图标的函数 {this.renderIcon()} {children} </StyleButton> ); } 接下来去创建StyleButton,与Icon创建一样地,我们在src/components/Button文件夹下新建style文件夹,在该文件夹下新建index.tsx,核心代码在这里:... const Button = ({ loading, styleType, disabled, onClick, block, shadowed, ...rest }) => ( <button disabled={disabled} onClick={!disabled ? onClick : undefined} {...rest} /> ); export const StyleButton = styleWrap<SButtonProps, HTMLButtonElement>({ className: classNameMixin, })(styled(Button)(buttonStyleMixin)); 定义一个叫Button的函数式组件,其返回的就是一个原生的button,所以rest参数里你可以传递原生的属性,最后用styleWrap包裹后导出。与Icon不同的是,Button的样式更多,而且还要适配主题。所以这里多了一个类名声明classNameMixin和一个样式函数buttonStyleMixin.classNameMixin:// classNameMixin负责添加各种类名,用于唯一识别和开发者进行定制 const classNameMixin = ({ size, styleType, shape, loading, disabled, checked, }) => classnames( prefixCls, `${prefixCls}-size-${size}`, `${prefixCls}-styletype-${styleType}`, shape && `${prefixCls}-${shape}`, loading && `${prefixCls}-loading`, disabled && `${prefixCls}-disabled`, checked && `${prefixCls}-checked`, );buttonStyleMixin是一个总开关,用于添加各种样式:// buttonStyleMixin const buttonStyleMixin = (props) => { const { theme, loading, shape, checked, block } = props; const { designTokens: DT } = theme; return css` margin: 0; box-sizing: border-box; border-radius: ${DT.T_CORNER_LG}; text-align: center; text-decoration: none; cursor: pointer; outline: none; font-size: ${DT.T_TYPO_FONT_SIZE_1}; white-space: nowrap; ${inlineBlockWithVerticalMixin}; // 块级 ${sizeMixin(props)}; // 大小 ${styleTypeMixin(props)}; // styleType ${shape && shapeMixin(props)}; // 形状 ${loading && loadingMixin(props)}; // 加载中 ${block && css` width: 100%; `}; `; };接下来分别记录上述各个样式变量:sizeMixin:// 通过getHeightBySize拿到主题配置文件中的各个大小 ... return css` height: ${getHeightBySize(DT, size)}; line-height: ${getHeightBySize(DT, size)}; padding: 0 ${getPaddingBySize(DT, size)}; `; ...shapeMixin:... // 目前支持圆形和方形 switch (shape) { case 'circle': return css` border-radius: 50% !important; padding: 0; overflow: hidden; width: ${getHeightBySize(DT, size)}; `; case 'square': return css` padding: 0; overflow: hidden; width: ${getHeightBySize(DT, size)}; `; default: return css``; }loadingMixin的设置也一样,使得鼠标不可点击,暗灰色显示即可。styleTypeMixin用来通过styleType的不同,对应的设置不同的主题配色, 以primary为例:const { // 接受ThemeProvider传递的theme参数 theme: { designTokens: DT }, styleType, disabled, size, shadowed, } = props; ... primary: { background: DT.T_BUTTON_PRIMARY_COLOR_BG_DEFAULT, ':hover,:active': { background: DT.T_BUTTON_PRIMARY_COLOR_BG_HOVER, boxShadow: shadowed ? DT.T_SHADOW_BUTTON_PRIMARY_HOVER : 'none', }, color: DT.T_BUTTON_PRIMARY_COLOR_TEXT_DEFAULT, fill: DT.T_BUTTON_PRIMARY_COLOR_TEXT_DEFAULT, border: 'none', boxShadow: shadowed ? DT.T_SHADOW_BUTTON_PRIMARY : 'none', transition: `${transitionProperty} ${transitionFlat}`, ':link,:visited': { color: DT.T_BUTTON_PRIMARY_COLOR_TEXT_DEFAULT, }, }, ...可以看到,当styleType='primary'时,设置了其背景色、文字颜色、填充色、渐变、过渡、激活时的样式以及被访问后的样式等。如此,一个简单的Button组件封装装好了,我们只是对样式进行了改动,其本质还是返回了一个原生按钮,原来的事件不影响使用。Button demo我们写个demo测试一下。在src/components/Button下新建index.md:import React from 'react'; import { Button } from 'dumi-dux-ui'; // demo start const { StyleTypes } = Button; const ColorDemo = () => { return ( <div> {StyleTypes.map((type) => ( <Button styleType={type} key={type} onClick={() => console.log('clicked')}> Button </Button> ))} </div> ); }; // demo end export default ColorDemo;在.umirc.ts中加入:... menus: { // 需要自定义侧边菜单的路径,没有配置的路径还是会使用自动生成的配置 '/components': [ { title: '组件', path: '/components', children: [ // 菜单子项(可选) 'components/Icon/index.md', 'components/Button/index.md', ], }, ], },根目录下执行npm run docs, 不出意外的话,在localhost:8000/components/button下可以看到:F12测试点击事件:至此,基础组件Icon和Button的记录就到此结束了。下一期会记录布局组件的搭建,敬请期待~