推荐 最新
lucky0-0

升级 React Router v6 指南

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 参数

0
0
0
浏览量1845
JACKY

React-合成事件解析

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:监听组件支持的所有事件。

0
0
0
浏览量2093
lucky0-0

深入 React 的 setState 机制

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 原理的文章,敬请关注!

0
0
0
浏览量1625
前端码农

手把手搭建基于React的前端UI库 (一)-- 项目初始化

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可以看到跑起来了至此,项目就搭建起来了!本章到此结束,下一节会逐步记录各个组件的开发过程。

0
0
0
浏览量2041
JACKY

React状态管理工具介绍及redux简析

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。

0
0
0
浏览量2053
前端码农

手把手搭建基于React的前端UI库 (三)-- 基础组件Icon和Button

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的记录就到此结束了。下一期会记录布局组件的搭建,敬请期待~

0
0
0
浏览量2043
JACKY

React - 时间切片理解

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 来实现任务的优先级和任务的调度,来提高应用程序的性能和用户体验。

0
0
0
浏览量2030
JACKY

react-redux使用及简析

react-redux是一个用于在React应用中使用Redux的库。它提供了一些工具和API,帮助我们将Redux与React组件无缝集成在一起.1. react-redux的使用// Provider组件:React-Redux提供了一个名为Provider的组件,将Redux的store传递给React应用的所有组件。通过在应用的根组件中使用Provider组件,我们可以确保所有的组件都能够访问到Redux的store。 import { Provider } from 'react-redux'; import Counter1 from './components/Counter1' import Counter2 from './components/Counter2' import store from './store'; createRoot(document.getElementById('root')).render( <Provider store={store}> <Counter1 /> <hr /> <Counter2 /> </Provider>); // connect函数:React-Redux提供了一个名为connect的函数,将Redux的状态和操作映射到React组件的props上。通过使用connect函数,我们可以将Redux的状态和操作与React组件进行绑定,从而实现组件的响应式更新。 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, mapDispatchToProps //组件的输出,在组件里派发动作,修改仓库 )(Counter1); hooks:React-Redux还提供了一些hooks,比如useSelector和useDispatch。这些hooks可以帮助我们在函数组件中使用Redux的状态和操作,而不需要使用connect函数2. 使用useSelector useDispatch 替代connect;使用useSelector和useDispatch可以替代connect函数,用于在函数组件中使用Redux的状态和操作。import { useSelector } from 'react-redux'; const MyComponent = () => { const state = useSelector((state) => state.myReducer); // 使用state进行组件的渲染和逻辑处理 return ( // JSX代码 ); }; // 使用useSelector选择了Redux状态树中的myReducer状态,并将其赋值给state变量。然后,我们可以使用state变量进行组件的渲染和逻辑处理 // useDispatch是另一个React-Redux提供的hook,它可以用于获取Redux的dispatch函数。通过使用useDispatch,我们可以在函数组件中触发Redux的action,从而改变状态。 import { useDispatch } from 'react-redux'; const MyComponent = () => { const dispatch = useDispatch(); // 使用dispatch触发Redux的action const handleClick = () => { dispatch({ type: 'INCREMENT' }); }; return ( // JSX代码 <button onClick={handleClick}>+</button> ); }; // 使用useDispatch获取Redux的dispatch函数,并将其赋值给dispatch变量。然后,我们可以在组件中使用dispatch触发Redux的action,从而改变状态。 通过使用useSelector和useDispatch,我们可以在函数组件中更方便地使用Redux的状态和操作,而不需要使用connect函数。这样可以简化代码,提高开发效率,并且更符合React的函数式编程风格。需要注意的是:使用useSelector和useDispatch时,确保在组件的顶层使用Provider组件来提供Redux的store。这样才能让组件正确地访问到Redux的状态和操作。import React from 'react' import { useStore } from 'react-redux' export const EComponent = ({ value }) => { const store = useStore() return <div>{store.getState().todos.length}</div> } 3. react-redux中Provider和connect原理简析// Provider和connect使用React.createContext()来管理状态,在connect组件中订阅subscribe,订阅仓库中的的状态变化事件,当仓库中的状态发生改变后重新用新的映射状态进行setState,去更新组件。 const ReactReduxContext = React.createContext(null); function Provider(props) { return ( <ReactReduxContext.Provider value={{ store: props.store }}> {props.children} </ReactReduxContext.Provider> ) } //connect(mapStateToProps,mapDispatchToProps)(Counter1); function connect(mapStateToProps, mapDispatchToProps) { return function (OldComponent) { return class extends React.Component { static contextType = ReactReduxContext;//context获取的另外一种方法,可以不适用Consumer来包裹。 constructor(props, context) {//this.context super(props); const { store } = context; const { getState, subscribe, dispatch } = store; //先获取仓库中的总状态{counter1:{number:1},counter2:{number:2}} this.state = mapStateToProps(getState()); //订阅仓库中的的状态变化事件,当仓库中的状态发生改变后重新用新的映射状态进行setState this.unsubscribe = subscribe(() => { this.setState(mapStateToProps(getState())); }); let dispatchProps; //如果说mapDispatchToProps是一个函数的话,传入dispatch if (typeof mapDispatchToProps === 'function') { dispatchProps = mapDispatchToProps(dispatch); } else {//如果它是一个对象的话,使用bindActionCreators,并传入dispatch。 dispatchProps = bindActionCreators(mapDispatchToProps, dispatch); } this.dispatchProps = dispatchProps; } componentWillUnmount() { this.unsubscribe(); } render() { return ( <OldComponent {...this.props} {...this.state} {...this.dispatchProps} /> ) } } } }

0
0
0
浏览量2014
JACKY

React常见问题汇总

React作为流行的前端框架,我使用了3年左右的时间,从类组件到函数组件,ahook,antd,umi,next等在不同业务场景下使用了一些库。此篇记录React的基本使用和一些注意事项。 一些代码实现,我们可以在 codePen 中执行,测试。codepen1. 类组件, class 继承React.Component或者React.PureComponent来实现类组件类组件有生命周期,可通过 shouldComponentUpdate,PureComponent 来跳过更新。 状态: 可在 constructor 中定义状态,通过 setState 来修改状态,更新组件,不要直接修改 state。class ClassComponent extends React.Component { render() { const { name, children, color } = this.props; return ( <h1 className="title" style={{ color: color }}> {name}:{children} </h1> ); } } 2 函数组件:接受 props 属性,返回 react node。在没hook之前,函数组件没有状态,只获取 props。 现在可用使用 useState,useEffect等hook 来处理状态。function FunctionComponent(props) { return ( <h1 className="title" style={{ color: props.color }}> {props.name}:{props.children} </h1> ); } 函数组件和类组件的区别类组件有生命周期,可以通过 shouldComponentUpdate 和 PureComponent 来跳过更新; 函数组件没有生命周期,可通过 useEffect 来模拟类组件需要创建实例,是基于面向对象的方式编程;函数式组件不需要创建实例,接收输入,返回输出可测试性: 函数式组件更方便编写单元测试类组件有自己的实例,可以定义状态,而且可以修改状态更新组件; 函数式组件以前没有状态,可以使用 useState 使用状态3. setState 调用 setState 可以修改状态,并且让组件刷新。面试会问:setState 是同步还是异步的?class Counter extends React.Component { constructor(props) { super(props); //组件里可以定义状态对象 this.state = { number: 0 }; } handleClick = () => { //调用setState可以修改状态,并且让组件刷新 // this.setState({ number: this.state.number + 1 }); // console.log(event.currentTarget, event.target); //event.stopPropagation(); this.setState({ number: this.state.number + 1 }); console.log(this.state); //输出0 this.setState({ number: this.state.number + 1 }); console.log(this.state); //输出0 setTimeout(() => { this.setState({ number: this.state.number + 1 }); console.log(this.state); //输出2 this.setState({ number: this.state.number + 1 }); console.log(this.state); //输出3 }, 50); }; render() { return ( <div id="counter"> <p>number:{this.state.number}</p> <button onClick={this.handleClick}>+</button> </div> ); } } State 的更新会被合并 当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state.State 的更新可能是异步的;出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用 因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态 可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数.setTimeout,Promise 中 setState 为啥是实时更新呢, setTimeout 是浏览器事件机制控制,脱离了 react 的上下文。在 React18 后就都是批量执行了。4. 类组件生命周期类组件的生命周期分为两版,16.8 版本之前的和之后的。16.8 版本之后的新增两个生命周期方法,删除了 3 个旧的方法。新增静态方法 static getDerivedStateFromProps() getSnapshotBeforeUpdate() getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。//父子组件的挂载和更新,卸载 //父组件 class Counter extends React.Component { static defaultProps = { name: "zhufeng", //定义默认属性 }; constructor(props) { super(props); this.state = { number: 0 }; console.log("Counter 1.constructor"); } componentWillMount() { console.log("Counter 2.componentWillMount"); } shouldComponentUpdate(nextProps, nextState) { console.log("Counter 5.shouldComponentUpdate"); return nextState.number % 2 === 0; //偶数才更新 } componentWillUpdate() { console.log("Counter 6.componentWillUpdate"); } handleClick = () => { debugger; this.setState({ number: this.state.number + 1 }); }; render() { console.log("Counter 3.render"); return ( <div> <p>{this.state.number}</p> {this.state.number === 4 ? null : ( <ChildCounter count={this.state.number} /> )} <button onClick={this.handleClick}>+</button> {null} </div> ); } componentDidUpdate() { console.log("Counter 7.componentDidUpdate"); } componentDidMount() { console.log("Counter 4.componentDidMount"); } } //子组件 class ChildCounter extends React.Component { componentWillUnmount() { console.log("ChildCounter 6.componentWillUnmount"); } componentWillReceiveProps(newProps) { console.log("ChildCounter 4.componentWillReceiveProps"); } componentWillMount() { console.log("ChildCounter 1.componentWillMount"); } shouldComponentUpdate(nextProps, nextState) { console.log("ChildCounter 5.shouldComponentUpdate"); return nextProps.count % 3 === 0; } render() { console.log("ChildCounter 2.render"); return <div>{this.props.count}</div>; } componentDidMount() { console.log("ChildCounter 3.componentDidMount"); } } //第一次生父子命周期执行顺序 "Counter 1.constructor" "Counter 2.componentWillMount" "Counter 3.render" "ChildCounter 1.componentWillMount" "ChildCounter 2.render" "ChildCounter 3.componentDidMount" "Counter 4.componentDidMount" //点击 1 次 "Counter 5.shouldComponentUpdate" //点击第 2 次 "Counter 5.shouldComponentUpdate" "Counter 6.componentWillUpdate" "Counter 3.render" "ChildCounter 4.componentWillReceiveProps" "ChildCounter 5.shouldComponentUpdate" "Counter 7.componentDidUpdate" //点击 3 次 "Counter 5.shouldComponentUpdate" //点击第 4 次 "Counter 5.shouldComponentUpdate" "Counter 5.shouldComponentUpdate" "Counter 6.componentWillUpdate" "Counter 3.render" "ChildCounter 6.componentWillUnmount" "Counter 7.componentDidUpdate" 函数组件使用 useEffect 实现生命周期,后面会介绍到。5. react 中的事件流,分为原生事件流和合成事件流。事件捕获事件目标事件冒泡事件委托先绑定先执行React 17 以前, 把事件委托到 document 对象上;当真实 DOM 元素触发事件,先处理原生事件,然后会冒泡到 document 对象后,再处理 React 事件.React 事件绑定的时刻是在 reconciliation 阶段,会在原生事件的绑定前执行目的和优势进行浏览器兼容,React 采用的是顶层事件代理机制,能够保证冒泡一致性事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象(React17 中被废弃)React17 以前的执行顺序document 捕获 父元素原生捕获 子元素原生捕获 子元素原生冒泡 父元素原生冒泡 父元素 React 事件捕获 子元素 React 事件捕获 子元素 React 事件冒泡 父元素 React 事件冒泡 document 冒泡 React17 以后的执行顺序document 原生捕获 父元素 React 事件捕获 子元素 React 事件捕获 父元素原生捕获 子元素原生捕获 子元素原生冒泡 父元素原生冒泡 子元素 React 事件冒泡 父元素 React 事件冒泡 document 原生冒泡 6.react hook介绍Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。 react 16.8 版本以后,新增了 fiber 架构,提供了 hook api。 fiber 架构之前 react 更新是递归式的,中途是不能中断的,批量的更新会造成页面卡顿。有了 fiber 架构后,会分更新优先级,计算浏览器空余时间是否足够本次更新或者中断,下次去更新。react 的更新优先级分为三类:事件优先级:更新优先级:调度优先级:fiber 使用 MessageChannel 来模拟 requestIdleCallbackrequestIdleCallback 是浏览器的 api 方法,这个函数将在浏览器空闲时期被调用 var handle = window.requestIdleCallback(callback[, options])const channel = new MessageChannel(); channel.port1.onmessage = function (event) { console.log("来自port2的消息:", event.data); }; channel.port2.onmessage = function (event) { console.log("来自port1的消息:", event.data); channel.port2.postMessage("port2"); }; channel.port1.postMessage("port1"); hook 的使用规则只能在函数组件最外层使用,不可以在 if,循环等内部,或者子函数中使用只能在函数组件中使用,不能再类组件中使用。 也可以在自定义 hook 中使用(后面会有自定义 hook 的介绍)。确保 Hook 在每一次渲染中都按照同样的顺序被调用。 由于 fiber 的结构是一个链表,双缓存的设计需要保持 currentFiber 和 workInporgressFiber 结构保持一致。7. useState使用数组解构的语法让我们在调用 useState 时可以给 state 变量取不同的名字function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>+</button>; } 8. useEffect使用可以使用useEffect模拟类组件的生命周期,如挂载,更新,卸载。// 默认每次都调用的effect useEffect(() => { document.title = `You clicked ${count} times`; }); // 需要清除副作用的effect,return返回一个清除副作用的callback,模拟componentWillUnmount useEffect(() => { console.log("开启一个定时器"); const timer = setInterval(() => { setNumber((number) => number + 1); }, 1000); return () => { console.log("销毁老的定时器"); clearInterval(timer); }; }); // 模拟componentDidUpdate 生命周期 useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 仅在 count 更改时更新 // 如果想只执行依次,useEffect第二个参数可传一个空数组[],告诉react不依赖props和state中的值。effect内部会一直保留初始值。 useEffect(() => { document.title = `You clicked 1 times`; }, []); 9. useReducer的使用传入一个纯函数reducer和初始值,返回state和dispatch方法。熟悉redux的同学应该比较容易理解function reducer(state = { number: 0 }, action) { switch (action.type) { case 'ADD': return { number: state.number + 1 }; case 'MINUS': return { number: state.number - 1 }; default: return state; } } function App() { const [state, dispatch] = React.useReducer(reducer, { number: 0 }); return ( <div> <p> Counter:{state.number}</p> <button onClick={() => dispatch({ type: 'ADD' })}>+</button> <button onClick={() => dispatch({ type: 'MINUS' })}>-</button> </div> ) } 10. useLayoutEffect的使用与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。尽可能使用标准的 useEffect 以避免阻塞视觉更新。function Animation() { const ref = React.useRef();//React.createRef() = {current:null} React.useLayoutEffect(() => { ref.current.style.transition = `all 500ms`; ref.current.style.transform = `translate(500px)`; }); let style = { width: '100px', height: '100px', borderRadius: '50%', backgroundColor: 'red' } return ( <div style={style} ref={ref}></div> ) } 11. useContext的使用React.createContext()创建一个Context;useContext 接收一个 context 对象,返回传递的值,由Context.Provider 的value决定12.useRef的使用返回一个可变的 ref 对象,.current被赋值;useRef 会在每次渲染时返回同一个 ref 对象。主动改变.current不会引发重新渲染。 function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); // ref保存值 function Counter() { const valueRef = React.useRef(); const [state, setState] = React.useState(0); const handleClick = () => { let newValue = state + 1; valueRef.current = newValue; setState(newValue); getNewValue(); } const getNewValue = () => { console.log(valueRef.current); } return ( <div> <p>{state}</p> <button onClick={handleClick}>+</button> <button onClick={getNewValue}>获取最新的状态</button> </div> ) } } 13. useMemo返回一个memoized 的值,依赖项变化的时候才重新计算值。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。useMemo存储的是一个值。 let data = React.useMemo(() => ({ number }), [number]); 14. useCallback返回一个memoized 回调函数,依赖变化的时候才会更新,useCallback存储的是一个函数,当依赖变化时,生成新的。 let handleClick = React.useCallback(() => setNumber(number + 1), [number]) 15. 自定义 hook规定以名字use开头的函数,根据情况决定传参和返回function useSlefStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // ... return isOnline; } 本章节介绍了 react 类组件和函数组件的使用以及一些基本原理,另外还有高阶组件,react-router路由管理,redux状态管理会在后面的文章中介绍。

0
0
0
浏览量2015
JACKY

React diff 算法的流程

React diff 算法的流程如下:比较两棵树的根节点,如果不同,则认为整棵树需要更新。对于相同的节点,比较它们的属性和子节点。对于同级节点,可以通过唯一 key 标识来判断是否为同一个节点,从而避免不必要的更新。递归处理所有子节点。对于有 key 的子节点,React 会尝试在旧的子节点中查找是否存在与之对应的节点。如果找到了,则将新节点复用旧节点,并对其进行更新;如果没有找到,则将新节点插入到相应位置。对于新增的节点,直接插入到相应位置。对于旧节点中已经不存在的节点,直接删除。在比较过程中,React 会尽可能地复用已有的节点,以最小化 DOM 操作的次数。同时,React 还提供了一些优化手段,如 shouldComponentUpdate 和 React.memo,让开发者可以在需要时自定义组件更新的逻辑和条件。整个过程中,React 通过记录每个节点的状态和变化情况,最终生成一组变更操作(比如要插入、删除、更新哪些节点),并一次性批量地执行这些操作,以提高性能。在实际应用中,React diff 算法还会针对一些特殊情况进行优化。比如:对于同级节点的移动操作,React 会尽可能地减少移动的次数,以提高性能。对于同级列表的渲染,React 会使用key属性来判断是否需要更新,从而避免因为顺序变化导致的不必要更新。对于大型列表的渲染,建议采用 “虚拟滚动” 技术,只渲染屏幕内可见的部分,从而提高性能和用户体验。总的来说,React diff 算法是 React 中非常重要的一部分,它通过智能地判断哪些节点需要更新、哪些节点可以复用等方式,使得 React 应用在处理大量数据时依然能够保持良好的性能表现。React diff 算法中,单节点和多节点的区别在 React diff 算法中,单节点和多节点的区别主要体现在更新策略上。 /** * 比较子Fibers DOM-DIFF 就是用老的子fiber链表和新的虚拟DOM进行比较的过程 * @param {*} returnFiber 新的父Fiber * @param {*} currentFirstChild 老fiber第一个子fiber current一般来说指的是老 * @param {*} newChild 新的子虚拟DOM h1虚拟DOM */ function reconcileChildFibers(returnFiber, currentFirstChild, newChild) { //现在需要处理更新的逻辑了,处理dom diff //考虑新的节点只有一个的情况 if (typeof newChild === "object" && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild)); default: break; } } //newChild [hello文本节点,span虚拟DOM元素] if (isArray(newChild)) { return reconcileChildrenArray(returnFiber, currentFirstChild, newChild); } return null; } return reconcileChildFibers; } 对于单节点,React diff 算法会直接比较其内容是否有变化,如果有变化,则直接更新该节点的内容,而不会进行进一步的子节点比较。这是因为单节点没有子节点,所以不存在子节点在顺序、位置等方面的变化,因此可以省略掉一些比较和操作。 /** * * @param {*} returnFiber 根fiber div#root对应的fiber * @param {*} currentFirstChild 老的FunctionComponent对应的fiber * @param {*} element 新的虚拟DOM对象 * @returns 返回新的第一个子fiber */ function reconcileSingleElement(returnFiber, currentFirstChild, element) { //新的虚拟DOM的key,也就是唯一标准 const key = element.key; // null let child = currentFirstChild; //老的FunctionComponent对应的fiber while (child !== null) { //判断此老fiber对应的key和新的虚拟DOM对象的key是否一样 null===null if (child.key === key) { //判断老fiber对应的类型和新虚拟DOM元素对应的类型是否相同 if (child.type === element.type) {// p div deleteRemainingChildren(returnFiber, child.sibling); //如果key一样,类型也一样,则认为此节点可以复用 const existing = useFiber(child, element.props); existing.ref = element.ref; existing.return = returnFiber; return existing; } else { //如果找到一key一样老fiber,但是类型不一样,不能此老fiber,把剩下的全部删除 deleteRemainingChildren(returnFiber, child); } } else { deleteChild(returnFiber, child); } child = child.sibling; } //因为我们现实的初次挂载,老节点currentFirstChild肯定是没有的,所以可以直接根据虚拟DOM创建新的Fiber节点 const created = createFiberFromElement(element); created.ref = element.ref; created.return = returnFiber; return created; } //将删除的节点放入returnFiber.deletions function deleteChild(returnFiber, childToDelete) { if (!shouldTrackSideEffects) return; const deletions = returnFiber.deletions; if (deletions === null) { returnFiber.deletions = [childToDelete] returnFiber.flags |= ChildDeletion; } else { returnFiber.deletions.push(childToDelete); } } 对于多节点,React diff 算法需要逐层比较其子节点的内容和状态,并根据变化情况采取不同的更新策略。具体来说,React diff 算法会通过比较每个子节点的 “key” 属性来确定它们的唯一性和相对位置,然后尽可能地复用已有的节点,并最小化 DOM 操作的次数。如果出现了新增、删除、移动等操作,则会相应地执行插入、删除、移动等操作,从而实现整个容器节点的更新。 // 第一轮循环 删除没有复用的老节点 // 使用lastPlacedIndex 来判断是否移动节点 //如果找到的老fiber的索引比lastPlacedIndex要小,则老fiber对应的DOM节点需要移动 function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) { let resultingFirstChild = null; //返回的第一个新儿子 let previousNewFiber = null; //上一个的一个新的儿fiber let newIdx = 0;//用来遍历新的虚拟DOM的索引 let oldFiber = currentFirstChild;//第一个老fiber let nextOldFiber = null;//下一个第fiber let lastPlacedIndex = 0;//上一个不需要移动的老节点的索引 // 开始第一轮循环 如果老fiber有值,新的虚拟DOM也有值 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { //先暂下一个老fiber nextOldFiber = oldFiber.sibling; //试图更新或者试图复用老的fiber const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]); if (newFiber === null) { break; } if (shouldTrackSideEffects) { //如果有老fiber,但是新的fiber并没有成功复用老fiber和老的真实DOM,那就删除老fiber,在提交阶段会删除真实DOM if (oldFiber && newFiber.alternate === null) { deleteChild(returnFiber, oldFiber); } } //指定新fiber的位置, 确定节点是否可以复用,移动或者替换 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C) } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber } //新的虚拟DOM已经循环完毕,3=>2 if (newIdx === newChildren.length) { //删除剩下的老fiber deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } if (oldFiber === null) { // 处理完需要删除的节点 //如果老的 fiber已经没有了, 新的虚拟DOM还有,进入插入新节点的逻辑 for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild(returnFiber, newChildren[newIdx]); if (newFiber === null) continue; lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //如果previousNewFiber为null,说明这是第一个fiber if (previousNewFiber === null) { resultingFirstChild = newFiber; //这个newFiber就是大儿子 } else { //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面 previousNewFiber.sibling = newFiber; } //让newFiber成为最后一个或者说上一个子fiber previousNewFiber = newFiber; } } // 开始处理移动的情况 const existingChildren = mapRemainingChildren(returnFiber, oldFiber); //开始遍历剩下的虚拟DOM子节点 for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]); if (newFiber !== null) { if (shouldTrackSideEffects) { //如果要跟踪副作用,并且有老fiber if (newFiber.alternate !== null) { existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key); } } //指定新的fiber存放位置 ,并且给lastPlacedIndex赋值 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; //这个newFiber就是大儿子 } else { //否则说明不是大儿子,就把这个newFiber添加上一个子节点后面 previousNewFiber.sibling = newFiber; } //让newFiber成为最后一个或者说上一个子fiber previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { //等全部处理完后,删除map中所有剩下的老fiber existingChildren.forEach(child => deleteChild(returnFiber, child)); } return resultingFirstChild; } function placeChild(newFiber, lastPlacedIndex, newIdx) { //指定新的fiber在新的挂载索引 newFiber.index = newIdx; //如果不需要跟踪副作用 if (!shouldTrackSideEffects) { return lastPlacedIndex; } //获取它的老fiber const current = newFiber.alternate; //如果有,说明这是一个更新的节点,有老的真实DOM。 if (current !== null) { const oldIndex = current.index; //如果找到的老fiber的索引比lastPlacedIndex要小,则老fiber对应的DOM节点需要移动 if (oldIndex < lastPlacedIndex) { newFiber.flags |= Placement; return lastPlacedIndex; } else { return oldIndex; } } else {//如果没有,说明这是一个新的节点,需要插入 newFiber.flags |= Placement; return lastPlacedIndex; } } 总的来说,单节点和多节点虽然都遵循 React diff 算法的基本流程,但在具体的更新策略上存在一些差异,需要针对不同的场景进行优化处理。除了更新策略上的差异,单节点和多节点在渲染性能上也有一些区别。####对于单节点,由于它没有子节点,所以不需要递归调用 diff 算法,因此处理速度很快,渲染性能比较高。对于多节点,由于它需要逐层比较子节点的状态和位置,并可能涉及到插入、删除、移动等操作,所以需要进行递归调用 diff 算法,并且随着节点数量的增加,处理速度会逐渐变慢。因此,在大规模列表渲染等场景下,需要采取一定的优化措施,如使用虚拟滚动等技术,以提高渲染性能和用户体验。总的来说,单节点和多节点都是 React 应用中常见的节点类型,在实际应用中需要根据具体情况选择合适的节点类型,并针对不同的场景进行优化处理,以保证应用的渲染性能和用户体验。diff中lastPlaceIndex的作用在 React diff 算法中,lastPlaceIndex(也称为“上一个节点的位置索引”)是一个用于优化节点移动操作的重要参数,它记录了当前正在遍历的子节点列表中,上一个参与比较的节点在原先列表中的位置。具体来说,当 React diff 算法在比较两个相邻的子节点时,会将上一个子节点的 lastPlaceIndex 与当前子节点的位置进行比较。如果当前子节点的位置小于等于上一个子节点的位置,则认为这两个子节点之间的距离没有变化,不需要进行移动操作;否则,会将当前子节点插入到正确的位置,并将上一个子节点从原先位置删除,以实现节点在列表中的移动操作。通过使用 lastPlaceIndex 参数,React diff 算法可以快速判断节点的移动情况,并最小化 DOM 操作的次数,从而提高应用的渲染性能。同时,为了避免 lastPlaceIndex 参数过期或失效导致的错误比较结果,React diff 算法会在处理完每个子节点后更新 lastPlaceIndex 的值,以确保其准确性和有效性。

0
0
0
浏览量2006