推荐 最新
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
浏览量1834
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
浏览量1616
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
浏览量2082
前端码农

手把手搭建基于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
浏览量2032
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
浏览量2048
前端码农

手把手搭建基于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
浏览量2031
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
浏览量2023
lucky0-0

一文解读 React 17 与 React 18 的更新变化

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 执行') }, [])

0
0
0
浏览量1543
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
浏览量2006
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
浏览量2006