从 CallBack 到 Promise,React 框架异步开发学习心得
2023年2月4日 22:00
<h2 id="引入"><a href="#引入" class="headerlink" title="引入"></a>引入</h2><p>说起 React,我印象最深刻的是,在 React 中,数据是<a href="https://zh-hans.reactjs.org/docs/state-and-lifecycle.html#the-data-flows-down">向下流动</a>的(<a href="https://www.php.cn/website-design-ask-493282.html">react 为什么是单向数据流</a>)——越高层级的组件,获得着越多的数据,而低层级组件数据的获取和更新,大多都通过<a href="https://zh-hans.reactjs.org/docs/components-and-props.html">组件属性传递</a>以及<a href="https://juejin.cn/post/7065555069889937415">回调函数</a>方式得到。这就意味着,高层组件刷新会同时刷新低层组件,而低层组件刷新往往不会带动高层组件刷新,于是更多的状态和逻辑会出现在比较高层级的组件里,在 React 中叫做<a href="https://zh-hans.reactjs.org/docs/lifting-state-up.html">状态提升</a> 。例如对话框的打开与关闭更应该是对话框组件的属性,而不是对话框组件的状态——对话框的操作往往与高层数据相关,如果把状态放在低层级,则很难把当前的状态和数据与高层级组件交互。</p><p>在这种数据流的模式下,为了使得基本组件“动起来”,高层级组件里总会有大大小小的许多状态,以便控制基本组件的开/关、显示/隐藏等等。此外,除了控制基本组件的状态以外,高层级组件本身可能还承担着数据通信的功能,例如我们本次提到的异步请求和发送数据。在 React 中,状态<code>state</code>的更新会使得组件重新进行渲染(见<a href="https://react.docschina.org/docs/state-and-lifecycle.html">State & 生命周期</a>),有的时候我们只希望重新渲染这个组件的一部分组件(例如刚才所说的对话框),而有的时候我们希望重新请求数据(数据同步、表格翻页)全部刷新,于是我们通常会使用 <a href="https://zh-hans.reactjs.org/docs/hooks-effect.html">useEffect 钩子</a>对一些刷新操作添加限定,仅仅在某些变量修改的时候,才会重新执行该部分代码逻辑(在 React 官方文档中叫做<a href="https://zh-hans.reactjs.org/docs/hooks-effect.html#tip-use-multiple-effects-to-separate-concerns">关注点分离</a>)。</p><h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>所以对于一个又需要刷新数据,又需要控制对话框,而且获取数据要请求两次 api 的组件,就会变成这个样子(CallBack 版本):</p><figure class="highlight jsx"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Component</span>(<span class="params">props</span>) {</span><br><span class="line"> <span class="keyword">let</span> [dialogState, setDialogState] = <span class="title function_">useState</span>(<span class="literal">false</span>); <span class="comment">// some states for dialogs</span></span><br><span class="line"> <span class="keyword">let</span> [renderData, setRenderData] = <span class="title function_">useState</span>(<span class="literal">null</span>); <span class="comment">// some states for rendering</span></span><br><span class="line"> <span class="keyword">let</span> [page, setPage] = <span class="title function_">useState</span>(<span class="number">1</span>); <span class="comment">// some states which force data refresh</span></span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="title function_">fetchSomeData</span>(<span class="string">"url"</span>, {<span class="attr">params</span>: <span class="string">"Some Params"</span>}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>}, <span class="function">(<span class="params">data</span>) =></span> {</span><br><span class="line"> <span class="comment">// callback for success</span></span><br><span class="line"> <span class="keyword">let</span> someProps = <span class="title function_">getSomeProps</span>(data);</span><br><span class="line"> <span class="title function_">fetchSomeData</span>(<span class="string">"url2"</span>, {<span class="attr">params</span>: someProps}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>}, <span class="function">(<span class="params">data</span>) =></span> {</span><br><span class="line"> <span class="comment">// funciton for process</span></span><br><span class="line"> <span class="title function_">setRenderData</span>(<span class="title function_">processing</span>(data));</span><br><span class="line"> }, <span class="function">() =></span> {});</span><br><span class="line"> }, <span class="function">(<span class="params">error</span>) =></span> {</span><br><span class="line"> <span class="comment">// callback for failure</span></span><br><span class="line"> });</span><br><span class="line"> }, [page]); <span class="comment">// fetch data only when page changes</span></span><br><span class="line"> <span class="keyword">if</span> (renderData === <span class="literal">null</span>) <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><></span> {/* equals to <span class="tag"><<span class="name">React.Fragment</span>></span> */}</span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Dialog</span> <span class="attr">someStates</span>=<span class="string">{dialogState}/</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">Others</span> <span class="attr">data</span>=<span class="string">{RenderData}/</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"></></span> {/* equals to <span class="tag"></<span class="name">React.Fragment</span>></span> */}</span></span><br><span class="line"><span class="language-xml"> );</span></span><br><span class="line"><span class="language-xml">}</span></span><br></pre></td></tr></tbody></table></figure><p>于是这个组件的执行流程是这样子的:</p><ul><li>组件第一次渲染:执行<code>useEffect</code>,开启异步数据请求,此时并没有任何有效数据用于渲染,于是返回<code>null</code>不加载模型;</li><li>组件收取到信息:执行回调函数,对数据进行处理,并更新组件状态,此时仍未执行组件刷新,数据不变;</li><li>组件状态得到更新:组件状态变化,组件刷新,但不再执行<code>useEffect</code>;</li><li>(中间可能的)对话框状态变化:组件状态变化,组件刷新,但不再执行<code>useEffect</code>。</li></ul><p>这样存在的问题在于,</p><ul><li>回调函数过于复杂——函数体量太大在合作时难以理解,包装起来可能涉及数据传递的问题</li><li>回调函数嵌套——有可能在收到某些信息,还要基于这些信息继续发请求,那么回调函数可能嵌套多层</li></ul><h2 id="Promise-的意义"><a href="#Promise-的意义" class="headerlink" title="Promise 的意义"></a>Promise 的意义</h2><p>我感觉<a href="https://zhuanlan.zhihu.com/p/26523836">理解 JavaScript Promise</a>这篇文章写的还是不错的,使用 <code>Promise</code> 构造一个函数,这个 <code>Promise</code> 就可以管理这个函数的状态,以便后续任务在这个函数执行完毕后使用。所以现代的 fetch 函数都尽可能返回一个 <code>Promise</code>,以便我们使用 <code>Promise.then()</code> 这个方法以便对数据进行处理。</p><p>所以上面的代码或许可以改成这个样子:</p><figure class="highlight jsx"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Component</span>(<span class="params">props</span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="title function_">fetchSomeData</span>(<span class="string">"url"</span>, {<span class="attr">params</span>: <span class="string">"Some Params"</span>}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>}).<span class="title function_">then</span>(<span class="function">(<span class="params">data</span>) =></span> {</span><br><span class="line"> <span class="comment">// funciton for process</span></span><br><span class="line"> <span class="keyword">let</span> someProps = <span class="title function_">getSomeProps</span>(data);</span><br><span class="line"> <span class="keyword">return</span> <span class="title function_">fetchSomeData</span>(<span class="string">"url2"</span>, {<span class="attr">params</span>: someProps}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>});</span><br><span class="line"> }).<span class="title function_">then</span>(<span class="function">(<span class="params">data</span>) =></span> {</span><br><span class="line"> <span class="comment">// funciton for process</span></span><br><span class="line"> <span class="title function_">setRenderData</span>(<span class="title function_">processing</span>(data));</span><br><span class="line"> }).<span class="title function_">catch</span>(<span class="function">(<span class="params">error</span>) =></span> {</span><br><span class="line"> <span class="comment">// function for failure</span></span><br><span class="line"> });</span><br><span class="line"> }, [page]); <span class="comment">// fetch data only when page changes</span></span><br><span class="line">...</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>那么这样有没有实质性的减少代码层数?减少了,原先存在的嵌套调用现在变成了连续使用 <code>.then()</code>函数,使得硕大的处理层变得轻松的多。</p><h2 id="Async-和-await"><a href="#Async-和-await" class="headerlink" title="Async 和 await"></a>Async 和 await</h2><p>与上一节一样,先挂出一个链接用于学习:<a href="https://www.cnblogs.com/youma/p/10475214.html">【学习笔记】深入理解 async/await</a>。</p><p><code>await</code> 的出现带来了什么呢?<code>await</code> 使得获取的结果直接提取了出来,不再需要额外套一层函数用于执行。这样函数嵌套会更加少,而且也可以像同步的函数一样处理数据了。于是我们的代码会变得更加清楚,不会再像原来一样晦涩难懂。</p><p>于是我们的代码可能变成这样子,如果想分开处理异常可以套两个 <code>try-catch</code>块:</p><figure class="highlight jsx"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Component</span>(<span class="params">props</span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="keyword">async</span> () => {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">let</span> data = <span class="keyword">await</span> <span class="title function_">fetchSomeData</span>(<span class="string">"url"</span>, {<span class="attr">params</span>: <span class="string">"Some Params"</span>}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>});</span><br><span class="line"> <span class="comment">// funciton for process</span></span><br><span class="line"> <span class="keyword">let</span> someProps = <span class="title function_">getSomeProps</span>(data);</span><br><span class="line"> <span class="keyword">let</span> anoData = <span class="keyword">await</span> <span class="title function_">fetchSomeData</span>(<span class="string">"url2"</span>, {<span class="attr">params</span>: someProps}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>});</span><br><span class="line"> <span class="comment">// funciton for process</span></span><br><span class="line"> <span class="title function_">setRenderData</span>(<span class="title function_">processing</span>(anoData));</span><br><span class="line"> } <span class="keyword">catch</span> (error) {</span><br><span class="line"> <span class="comment">// function for failure</span></span><br><span class="line"> } <span class="comment">// try-catch block is unnecessary if no error exist</span></span><br><span class="line"> }, [page]); <span class="comment">// fetch data only when page changes</span></span><br><span class="line">...</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>但是这样 <code>Eslint</code> 组件是会报警告的——</p><blockquote><p>ESLint: Effect callbacks are synchronous to prevent race conditions. Put the async function inside:</p></blockquote><p>如果版本早于 React 16,可能会直接报错误——</p><blockquote><p>An effect function must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => …) or returned a Promise. Instead, write the async function inside your effect and call it immediately</p></blockquote><p>这是因为 <code>useEffect</code>是需要返回值来解决组件销毁/重建时的副作用清除的,而我们加上 <code>async</code> 关键字则会让这个函数返回一个 <code>Promise</code>,所以应该建一个普通的函数,然后在函数里面创建带有<code>async</code>关键字的函数,并立即调用。详见 <a href="https://juejin.cn/post/7029117054233870349">hooks 学习之 useEffect</a>。</p><figure class="highlight jsx"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Component</span>(<span class="params">props</span>) {</span><br><span class="line"> ...</span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {(<span class="keyword">async</span> () => {</span><br><span class="line"> ...</span><br><span class="line"> })()}, [page]); <span class="comment">// fetch data only when page changes</span></span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="并行的数据请求"><a href="#并行的数据请求" class="headerlink" title="并行的数据请求"></a>并行的数据请求</h2><p>我们的请求可能没有前置要求,那么异步的数据获取我们怎么进行处理呢?一般来说,我们对于数据请求,难免存在请求失败的情况,所以常见的策略是哪部分到了先加载哪部分,报错的部分再进行重试或请求备用数据源,以免用户等待太着急。于是我们就可以建造多个<code>useEffect</code>函数,分别进行数据请求和处理,加上 <code>React</code> 的关注点分离策略,我们就可以实现部分数据的渲染。</p><figure class="highlight jsx"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Component</span>(<span class="params">props</span>) {</span><br><span class="line"> <span class="keyword">let</span> [dataA, setDataA] = <span class="title function_">useState</span>(<span class="literal">null</span>); <span class="comment">// data for Component A</span></span><br><span class="line"> <span class="keyword">let</span> [dataB, setDataB] = <span class="title function_">useState</span>(<span class="literal">null</span>); <span class="comment">// data for Component B</span></span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="title function_">fetchSomeData</span>(<span class="string">"urlA"</span>, {<span class="attr">params</span>: <span class="string">"Some Params"</span>}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>}).<span class="title function_">then</span>(<span class="function">(<span class="params">data</span>) =></span> <span class="title function_">setDataA</span>(data));</span><br><span class="line"> }, [page]); </span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="title function_">fetchSomeData</span>(<span class="string">"urlB"</span>, {<span class="attr">params</span>: <span class="string">"Some Params"</span>}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>}).<span class="title function_">then</span>(<span class="function">(<span class="params">data</span>) =></span> <span class="title function_">setDataB</span>(data));</span><br><span class="line"> }, [page]); </span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><></span> {/* equals to <span class="tag"><<span class="name">React.Fragment</span>></span> */}</span></span><br><span class="line"><span class="language-xml"> { dataA && <span class="tag"><<span class="name">A</span> <span class="attr">data</span>=<span class="string">{dataA}/</span>></span> }</span></span><br><span class="line"><span class="language-xml"> { dataB && <span class="tag"><<span class="name">B</span> <span class="attr">data</span>=<span class="string">{dataB}/</span>></span> }</span></span><br><span class="line"><span class="language-xml"> <span class="tag"></></span> {/* equals to <span class="tag"></<span class="name">React.Fragment</span>></span> */}</span></span><br><span class="line"><span class="language-xml"> );</span></span><br><span class="line"><span class="language-xml">}</span></span><br></pre></td></tr></tbody></table></figure><p>如果我们对数据正确性有非常高的要求,要求必须所有数据到齐才能渲染的话,可以使用<code>Promise.all()</code>函数。</p><figure class="highlight jsx"><table><tbody><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> <span class="keyword">function</span> <span class="title function_">Component</span>(<span class="params">props</span>) {</span><br><span class="line"> <span class="keyword">let</span> [dataA, setDataA] = <span class="title function_">useState</span>(<span class="literal">null</span>); <span class="comment">// data for Component A</span></span><br><span class="line"> <span class="keyword">let</span> [dataB, setDataB] = <span class="title function_">useState</span>(<span class="literal">null</span>); <span class="comment">// data for Component B</span></span><br><span class="line"> <span class="title function_">useEffect</span>(<span class="function">() =></span> {</span><br><span class="line"> <span class="keyword">let</span> remoteA = <span class="title function_">fetchSomeData</span>(<span class="string">"urlA"</span>, {<span class="attr">params</span>: <span class="string">"Some Params"</span>}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>});</span><br><span class="line"> <span class="keyword">let</span> remoteB = <span class="title function_">fetchSomeData</span>(<span class="string">"urlB"</span>, {<span class="attr">params</span>: <span class="string">"Some Params"</span>}, {<span class="attr">config</span>: <span class="string">"Some Configurations"</span>});</span><br><span class="line"> <span class="title class_">Promise</span>.<span class="title function_">all</span>([remoteA, remoteB]).<span class="title function_">then</span>(<span class="function">(<span class="params">dataArray</span>) =></span> {</span><br><span class="line"> <span class="keyword">let</span> [first, second] = dataArray;</span><br><span class="line"> <span class="title function_">setDataA</span>(first);</span><br><span class="line"> <span class="title function_">setDataB</span>(second);</span><br><span class="line"> }).<span class="title function_">catch</span>(<span class="function">(<span class="params">error</span>) =></span> <span class="title function_">someFunction</span>(error));</span><br><span class="line"> }, [page]); </span><br><span class="line"> <span class="keyword">return</span> (</span><br><span class="line"> <span class="language-xml"><span class="tag"><></span> {/* equals to <span class="tag"><<span class="name">React.Fragment</span>></span> */}</span></span><br><span class="line"><span class="language-xml"> { dataA && <span class="tag"><<span class="name">A</span> <span class="attr">data</span>=<span class="string">{dataA}/</span>></span> }</span></span><br><span class="line"><span class="language-xml"> { dataB && <span class="tag"><<span class="name">B</span> <span class="attr">data</span>=<span class="string">{dataB}/</span>></span> }</span></span><br><span class="line"><span class="language-xml"> <span class="tag"></></span> {/* equals to <span class="tag"></<span class="name">React.Fragment</span>></span> */}</span></span><br><span class="line"><span class="language-xml"> );</span></span><br><span class="line"><span class="language-xml">}</span></span><br></pre></td></tr></tbody></table></figure>