精华内容
参与话题
问答
  • 本文是学习了2018年新鲜出炉的React Hooks提案之后,针对异步请求数据...你使用Redux实现过异步Action(非必需,只是本文不涉及该部分知识而直接使用)3.你听说过axios或者fetch(如果没有,那么想象一下原生js的pr...
    本文是学习了2018年新鲜出炉的React Hooks提案之后,针对异步请求数据写的一个案例。注意,本文假设了:
    1.你已经初步了解hooks的含义了,如果不了解还请移步官方文档。(其实有过翻译的想法,不过印记中文一直在翻译,就是比较慢啦)
    2.你使用Redux实现过异步Action(非必需,只是本文不涉及该部分知识而直接使用)
    3.你听说过axios或者fetch(如果没有,那么想象一下原生js的promise实现异步请求,或者去学习下这俩库)
    全部代码参见仓库: github | Marckon选择hooks-onlineShop分支以及master分支查看

    本文并非最佳实践,如有更好的方法或发现文中纰漏,欢迎指正!

    前序方案(不想看可以直接跳过)

    • 不考虑引入Redux

    通过学习React生命周期,我们知道适合进行异步请求的地方是componentDidMount钩子函数内。因此,当你不需要考虑状态管理时,以往的方法很简单:

    
    class App extends React.Component{
        componentDidMount(){
            axios.get('/your/api')
                .then(res=>/*...*/)
        }
    }
    
    • 引入Redux进行状态管理

    当你决定使用Redux进行状态管理时,比如将异步获取到的数据储存在store中,事情就开始复杂起来了。根据Redux的官方文档案例来看,为了实现异步action,你还得需要一个类似于redux-thunk的第三方库来解析你的异步action

    requestAction.js: 定义异步请求action的地方

    
    //这是一个异步action,分发了两个同步action,redux-thunk能够理解它
    const fetchGoodsList = url => dispatch => {
        dispatch(requestGoodsList());
        axios.get(url)
            .then(res=>{
                dispatch(receiveGoodsList(res.data))
            })
    };
    
    

    requestReducer.js: 处理同步action

    
    const requestReducer=(state=initialState,action)=>{
        switch (action.type) {
            case REQUEST_GOODSLIST:
                return Object.assign({},state,{
                    isFetching: true
                });
            case RECEIVE_GOODSLIST:
                return Object.assign({},state,{
                    isFetching:false,
                    goodsList:action.goodsList
                });
            default:
                return state;
        }
    };
    

    App Component :你引入redux store和redux-thunk中间件的地方

    
    import {Provider} from 'react-redux';
    import thunkMiddleWare from 'redux-thunk';
    import {createStore,applyMiddleware} from 'redux';
    //other imports
    
    let store=createStore(
        rootReducer,
        //这里要使用中间件,才能够完成异步请求
        applyMiddleware(
            thunkMiddleWare,
            myMiddleWare,
    
        )
    );
    class App extends React.Component{
        render(){
            return (
                <Provider store={store}>
                    <RootComponent/>
                </Provider>
            )
        }
    }
    

    GoodsList Component :需要进行异步请求的组件

    
    class GoodsList extends React.Component{
        //...
        componentDidMount(){
            this.props.fetchGoodsList('your/url');
        }
        //...
    }
    const mapDispatchToProps={
        fetchGoodsList
    }
    export default connect(
        mapStateToProps,
        mapDispatchToProps
    )(GoodsList);
    

    完整代码:branch:master-onlineShop

    使用Hooks-useReducer()useContext()

    总之使用Redux很累,当然,你可以不使用Redux,直接通过props层层传递,或者使用context都可以。只不过本文我们学过了useReducer,使用到了Redux的思想,总要试着用一下。

    这里你不需要引入别的任何第三方库了,简简单单地使用React@16.7.0-alpha.2版本就好啦

    很重要的一点就是——函数式组件,现在React推荐我们这么做,可以基本上代替class写法。

    函数签名

    1. useReducer(reducer,initialState)
    2. useContext(ctxObj)
    3. useEffect(effectFunction,[dependencyValues])

    概览-你需要编写什么

    1. action.js:

      • 我们还使用redux的思想,编写action
    2. reducer.js:

      • 处理action,不同于reduxreducer,这里我们可以不用提供初始状态
    3. 根组件:

      • Provider提供给子组件context
      • useReducer定义的位置,引入一个reducer并且提供初始状态initialState
    4. 子组件:

      • useContext定义的位置,获取祖先组件提供的context
      • useEffect用于进行异步请求

    实现

    1.action.js:我们使用action创建函数

    
    const REQUEST_GOODSLIST = "REQUEST_GOODSLIST";
    const RECEIVE_GOODSLIST = "RECEIVE_GOODSLIST";
    
    //开始请求
    const requestGoodsList = () => ({
        type: REQUEST_GOODSLIST
    });
    
    //接收到数据
    const receiveGoodsList = json => ({
        type: RECEIVE_GOODSLIST,
        goodsList: json.goodsList,
        receivedAt: Date.now()
    });
    
    export {
        RECEIVE_GOODSLIST,
        REQUEST_GOODSLIST,
        receiveGoodsList,
        requestGoodsList,
    }
    

    2.reducer.js:判断action的类型并进行相应处理,更新state

    
    import {
        RECEIVE_GOODSLIST,
        REQUEST_GOODSLIST,
    } from "../..";
    
    
    export const fetchReducer=(state,action)=>{
        switch (action.type) {
            case REQUEST_GOODSLIST:
                return Object.assign({},state,{
                    isFetching: true
                });
            case RECEIVE_GOODSLIST:
                return Object.assign({},state,{
                    isFetching:false,
                    goodsList:state.goodsList.concat(action.goodsList)
                });
            default:
                return state;
        }
    };
    
    
    

    3.根组件:引入reducer.js

    
    import React,{useReducer} from 'react';
    import {fetchReducer} from '..';
    
    //创建并export上下文
    export const FetchesContext = React.createContext(null);
    
    function RootComponent() {
        //第二个参数为state的初始状态
        const [fetchesState, fetchDispatch] = useReducer(fetchReducer, {
                isFetching: false,
                goodsList: []
            });
        return (
            //将dispatch方法和状态都作为context传递给子组件
             <FetchesContext.Provider value={{fetchesState,dispatch:fetchDispatch}}>
                 //...
                 //用到context的一个子组件
                 <ComponentToUseContext/>
             </FetchesContext.Provider>
        )
    }
    

    4.子组件:引入FetchesContext

    
    import {FetchesContext} from "../RootComponent";
    import React, {useContext, useEffect,useState} from 'react';
    import axios from 'axios';
    
    function GoodsList() {
    
        //获取上下文
        const ctx = useContext(FetchesContext);
        
        //一个判断是否重新获取的state变量
        const [reFetch,setReFetch]=useState(false);
    
        //具有异步调用副作用的useEffect
        useEffect(() => {
            //首先分发一个开始异步获取数据的action
            ctx.dispatch(requestGoodsList());
                axios.get(proxyGoodsListAPI())
                    .then(res=>{
                        //获取到数据后分发一个action,通知reducer更新状态
                        ctx.dispatch(receiveGoodsList(res.data))
                    })
          //第二个参数reFetch指的是只有当reFetch变量值改变才重新渲染
        },[reFetch]);
    
        return (
            <div onScroll={handleScroll}>
                {
                    //children
                }
            </div>
        )
    }
    
    

    完整代码参见:branch:hooks-onlineShop

    目录结构

    我的目录结构大概这样:

    
    src
      |- actions
         |- fetchAction.js
      |- components
         |-...
      |- reducers
         |- fetchReducer.js
      |- index.js
    

    注意点

    1. 使用useContext()时候我们不需要使用Consumer了。但不要忘记exportimport上下文对象
    2. useEffect()可以看做是class写法的componentDidMountcomponentDidUpdate以及componentWillUnMount三个钩子函数的组合。

      • 当返回了一个函数的时候,这个函数就在compnentWillUnMount生命周期调用
      • 默认地,传给useEffect的第一个参数会在每次(包含第一次)数据更新时重新调用
      • 当给useEffect()传入了第二个参数(数组类型)的时候,effect函数会在第一次渲染时调用,其余仅当数组中的任一元素发生改变时才会调用。这相当于我们控制了组件的update生命周期
      • useEffect()第二个数组为空则意味着仅在componentDidMount周期执行一次
    3. 代码仓库里使用了Mock.js拦截api请求以及ant-design第三UI方库。目前代码比较简陋。
    来源:https://segmentfault.com/a/1190000017209855
    展开全文
  • redux异步In react-application we encounter many scenarios where we want to switch to another route based on API response. For example when user submits a form, based on success failure we want our ...

    redux异步

    In react-application we encounter many scenarios where we want to switch to another route based on API response. For example when user submits a form, based on success failure we want our application to redirect him to certain specific page. Generally we accomplish this by creating aflag variable. Which keeps track of our decision whether to redirect or not. We make one state variable and update it, whether we want a redirection or not. Here is the code sample for the same.

    react-application我们遇到了许多想根据API响应切换到另一条路由的场景。 例如,当用户提交表单时,基于成功失败,我们希望我们的应用程序将其重定向到特定的页面。 通常,我们通过创建一个flag变量来完成此任务。 这将跟踪我们是否重定向的决定。 无论是否需要重定向,我们都使一个state变量并对其进行更新。 这是相同的代码示例。

    Class Version

    类版本

    Image for post

    Functional Version

    功能版本

    Image for post

    There is another version where we can leverage the createBrowserHistory object of history package which is more cleaner way of achieving this. By using this we can change our Route from async actions. To do so we have to through following process.

    还有另一个版本,我们可以利用history包的createBrowserHistory对象,这是实现此目的的更createBrowserHistory的方法。 通过使用此方法,我们可以从异步操作更改路由。 为此,我们必须通过以下过程。

    Step 1: Create a file and name it history.js or history.ts if you are using typescript. You can give any name you wanted but history name is suitable for this file.

    第1步:创建一个文件,并将其命名为history.jshistory.ts如果使用打字稿)。 您可以提供任何想要的名称,但history名称适用于此文件。

    Step 2: Place the following content inside it:

    步骤2:将以下内容放入其中:

    Image for post

    Step 3: Navigate to your root component where you’ve added ‘Router’ . Here you have to import the history file and pass it to the history prop of Router.

    第3步:导航到添加了“ Router”的根组件。 在这里,您必须导入history文件并将其传递给Router的history属性。

    Image for post

    Make sure you are importing Router from react-router-dom not the BrowserRouter. If you have an import like this import {BrowserRouter as Router} from 'react-router-dom'; change it to import {Router} from 'react-router-dom'; .

    确保您是从react-router-dom而非BrowserRouter.导入RouterBrowserRouter. 如果您有类似的导入,例如import {BrowserRouter as Router} from 'react-router-dom'; 将其更改为import {Router} from 'react-router-dom';

    Issue if you don’t do this: Your address bar URL will change but content won’t be rendered. 😒

    如果您不这样做,则会出现问题:地址栏URL会更改,但不会呈现内容。 😒

    WHY ?🤔 Short Answer Because BrowserRouter ignores the history prop as it handles the history automatically for you. If you need access to the history outside of a react component, then using Router should be fine👌🏻 😃

    为什么?🤔简短答案,因为BrowserRouter忽略历史记录道具,因为它会自动为您处理历史记录。 如果您需要访问react组件之外的历史记录,则可以使用Router

    Step 4: Use history in your action file to push routes.

    步骤4:使用动作文件中的history来推送路线。

    Image for post

    You’ve successfully changed the Route from Async Redux action.

    您已成功更改了“来自异步Redux的路由”操作。

    翻译自: https://medium.com/@rahuulmiishra/change-route-on-successful-async-redux-action-or-after-35e7aa5e70d4

    redux异步

    展开全文
  • 不吹不黑,redux蛮好用。只是有时略显繁琐,叫我定义每一个action、action type、使用时还要在组件上绑定一遍,臣妾...基于redux、async/await、无侵入、兼容性良好的异步状态管理器。 install npm i -S redux-effec...

    不吹不黑,redux蛮好用。只是有时略显繁琐,叫我定义每一个action、action type、使用时还要在组件上绑定一遍,臣妾做不到呀!下面分享一种个人比较倾向的极简写法,仍有待完善,望讨论。

    github: github.com/liumin1128/…

    基于redux、async/await、无侵入、兼容性良好的异步状态管理器。

    install

    npm i -S redux-effect
    
    // or
    yarn add redux-effect
    复制代码

    use

    import { createStore, applyMiddleware, combineReducers } from 'redux';
    import { reduxReduers, reduxEffects } from 'redux-effect';
    
    const models = [ test1, test2, ...];
    
    const reducers = combineReducers(reduxReduers(models));
    const middlewares = [ reduxEffects(models) ];
    
    const store = createStore(
        reducers,
        initialState,
        middlewares
      );
    复制代码

    从代码可以看出,从reduxReduers, reduxEffects中得到的就是标准的reducer和middleware,完美兼容其他redux插件,也可以轻松整合进老项目中。

    完整例子:example

    model

    在redux-effect中,没有action的概念,也不需要定义action type。

    所有关于某个state的一切声明在一个model中,本质就是一个对象。

    export default {
      namespace: 'test',
      state: { text: 'hi!' },
      reducers: {
        save: (state, { payload }) => ({ ...state, ...payload }),
        clear: () => ({})
      },
      effects: {
        fetch: async ({ getState,dispatch }, { payload }) => {
          await sleep(3000);
          await dispatch({ type: 'test/clear' });
          await sleep(3000);
          await dispatch({ type: 'test/save', payload: { text: 'hello world' } });
        }
      }
    };
    
    复制代码

    namespace:

    model的命名空间,对应state的名字,必填,只接受一个字符串。

    state:

    state的初始值,非必填,默认为空对象

    reducers:

    必填,相当于同步执行的action方法,接受两个参数state和action,合并后返回新的state状态值。

    effects:

    非必填,相当于异步执行的action方法,接受两个参数store和action,store里包括redux自带的getState和dispatch方法,action为用户dispatch时带的参数。

    dispatch

    这里的dispatch就是redux中的dispatch,但有几个约定。

    1. 不传定义好的action,而是直接传一个普通对象。
    2. type的组织形式:namespace + '/' + reducer或effect方法名
    3. 参数的传递:需要合并的参数用payload包裹

    定义每一个action,并将其绑定到视图层过于繁琐,去action化则让事件的触发变的灵活。

    普通事件

    发送事件时,不区分同步还是异步,只管dispatch,一切都已在model中定义好。

    // 同步
    dispatch({ type: 'test/save', payload: { text: "hello world" } })
    // 异步
    dispatch({ type: 'test/fetch' })
    复制代码

    等待

    等待一个事件完成再执行逻辑,dispatch方法是可以被await的,十分轻松。

    async function test() {
      await dispatch({ type: 'test/fetch' })
      await console.log('hello world')
    }
    复制代码

    回调

    等待某个事件,再执行外部定义的某个回调函数,只需要在action字段里加上callback方法,在effect中调用即可。

    相比较await,回调可以拿到某些返回值,也可以在effect流程的中间部分执行。

    
    dispatch({ type: 'test/fetch', callback1, callback2 })
    
    {
      effects: {
        fetch: async ({ getState,dispatch }, { payload, callback, callback2 }) => {
          const state = await getState()
          await sleep(3000);
          await callback1(state)
          await sleep(3000);
          await callback2(state)
        }
      }
    }
    复制代码

    自定义reducer

    reducer其实就是redux中的reducer,用法完全一样。比如定义一个push方法,将后续数据,压入到原有数据后面,可以这样写。

    export default {
      namespace: 'test',
      state: { data: [] },
      reducers: {
        save: (state, { payload }) => ({ ...state, ...payload }),
        clear: () => ({}),
        push: (state, { payload = {} }) => {
          const { key = 'data', data } = payload;
          return { ...state, [key]: state[key].concat(data) };
        }
      },
    };
    复制代码

    自定义effect

    effect其实就是一个普通async函数,接受store和action两个参数,可以使用async/await,可以执行任意异步方法,可以随时拿到state的值,可以dispatch触发另一个effect或者reducer。

    loading

    也许你会想监听某个effect,拿到loading状态,以便在ui给用户一个反馈。一般情况下监听一个异步方法,只需要在effect的开头和结束,各自设定状态即可,与常规写法无异。

    但这里也提供一种model级别的loading状态,新增一个名为loading的model,再使用reduxEffectsWithLoading包裹需要监听的model即可。

    关于model-creator

    以上所做的事情,是将redux核心规范为model,得到了统一且可以复用的数据模型,这为自动生成model创造了可能性,如果能通过工厂模式,自动化创建具有类似功能,且可以随意装配的model,一切将变得更加美好。

    Coming Soon

    展开全文
  • React Hooks 异步操作踩坑记

    千次阅读 2019-10-21 20:29:09
    React Hooks 是 React 16.8 的新功能,可以在不编写 class 的情况下使用状态等功能,从而使得函数式组件从无状态的变化为有状态的。 React 的类型包 @types/react 中也同步把 React.SFC (Stateless Functional ...

    React Hooks 是 React 16.8 的新功能,可以在不编写 class 的情况下使用状态等功能,从而使得函数式组件从无状态的变化为有状态的。 React 的类型包 @types/react 中也同步把 React.SFC (Stateless Functional Component) 改为了 React.FC (Functional Component)。

    通过这一升级,原先 class 写法的组件也就完全可以被函数式组件替代。虽然是否要把老项目中所有类组件全部改为函数式组件因人而异,但新写的组件还是值得尝试的,因为代码量的确减少了很多,尤其是重复的代码(例如 componentDidMount + componentDidUpdate + componentWillUnmount = useEffect)。

    从 16.8 发布(今年2月)至今也有大半年了,但本人水平有限,尤其在 useEffect 和异步任务搭配使用的时候经常踩到一些坑。特作本文,权当记录,供遇到同样问题的同僚借鉴参考。我会讲到三个项目中非常常见的问题:

    1. 如何在组件加载时发起异步任务
    2. 如何在组件交互时发起异步任务
    3. 其他陷阱

    TL;DR

    1. 使用 useEffect 发起异步任务,第二个参数使用空数组可实现组件加载时执行方法体,返回值函数在组件卸载时执行一次,用来清理一些东西,例如计时器。
    2. 使用 AbortController 或者某些库自带的信号量 (axios.CancelToken) 来控制中止请求,更加优雅地退出。
    3. 当需要在其他地方(例如点击处理函数中)设定计时器,在 useEffect 返回值中清理时,使用局部变量或者 useRef 来记录这个 timer不要使用 useState
    4. 组件中出现 setTimeout 等闭包时,尽量在闭包内部引用 ref 而不是 state,否则容易出现读取到旧值的情况。
    5. useState 返回的更新状态方法是异步的,要在下次重绘才能获取新值。不要试图在更改状态之后立马获取状态。

    如何在组件加载时发起异步任务

    这类需求非常常见,典型的例子是在列表组件加载时发送请求到后端,获取列表后展现。

    发送请求也属于 React 定义的副作用之一,因此应当使用 useEffect 来编写。基本语法我就不再过多说明,代码如下:

    import React, { useState, useEffect } from 'react';
    
    const SOME_API = '/api/get/value';
    
    export const MyComponent: React.FC<{}> = () => {
        const [loading, setLoading] = useState(true);
        const [value, setValue] = useState(0);
    
        useEffect(() => {
            (async () => {
                const res = await fetch(SOME_API);
                const data = await res.json();
                setValue(data.value);
                setLoading(false);
            })();
        }, []);
    
        return (
            <>
            {loading ? (
                <h2>Loading...</h2>
            ) : (
                <h2>value is {value}</h2>
            )}
            </>
        );
    }
    复制代码

    如上是一个基础的带 Loading 功能的组件,会发送异步请求到后端获取一个值并显示到页面上。如果以示例的标准来说已经足够,但要实际运用到项目中,还不得不考虑几个问题。

    如果在响应回来之前组件被销毁了会怎样?

    React 会报一个 Warning

    Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.in Notification

    大意是说在一个组件卸载了之后不应该再修改它的状态。虽然不影响运行,但作为完美主义者代表的程序员群体是无法容忍这种情况发生的,那么如何解决呢?

    问题的核心在于,在组件卸载后依然调用了 setValue(data.value)setLoading(false) 来更改状态。因此一个简单的办法是标记一下组件有没有被卸载,可以利用 useEffect 的返回值。

    // 省略组件其他内容,只列出 diff
    useEffect(() => {
        let isUnmounted = false;
        (async () => {
            const res = await fetch(SOME_API);
            const data = await res.json();
            if (!isUnmounted) {
                setValue(data.value);
                setLoading(false);
            }
        })();
    
        return () => {
            isUnmounted = true;
        }
    }, []);
    复制代码

    这样可以顺利避免这个 Warning。

    有没有更加优雅的解法?

    上述做法是在收到响应时进行判断,即无论如何需要等响应完成,略显被动。一个更加主动的方式是探知到卸载时直接中断请求,自然也不必再等待响应了。这种主动方案需要用到 AbortController

    AbortController 是一个浏览器的实验接口,它可以返回一个信号量(singal),从而中止发送的请求。这个接口的兼容性不错,除了 IE 之外全都兼容(如 Chrome, Edge, FF 和绝大部分移动浏览器,包括 Safari)。

    useEffect(() => {
        let isUnmounted = false;
        const abortController = new AbortController(); // 创建
        (async () => {
            const res = await fetch(SOME_API, {
                singal: abortController.singal, // 当做信号量传入
            });
            const data = await res.json();
            if (!isUnmounted) {
                setValue(data.value);
                setLoading(false);
            }
        })();
    
        return () => {
            isUnmounted = true;
            abortController.abort(); // 在组件卸载时中断
        }
    }, []);
    复制代码

    singal 的实现依赖于实际发送请求使用的方法,如上述例子的 fetch 方法接受 singal 属性。如果使用的是 axios,它的内部已经包含了 axios.CancelToken,可以直接使用,例子在这里

    如何在组件交互时发起异步任务

    另一种常见的需求是要在组件交互(比如点击某个按钮)时发送请求或者开启计时器,待收到响应后修改数据进而影响页面。这里和上面一节(组件加载时)最大的差异在于 React Hooks 只能在组件级别编写,不能在方法(dealClick)或者控制逻辑(if, for 等)内部编写,所以不能在点击的响应函数中再去调用 useEffect。但我们依然要利用 useEffect 的返回函数来做清理工作。

    以计时器为例,假设我们想做一个组件,点击按钮后开启一个计时器(5s),计时器结束后修改状态。但如果在计时未到就销毁组件时,我们想停止这个计时器,避免内存泄露。用代码实现的话,会发现开启计时器和清理计时器会在不同的地方,因此就必须记录这个 timer。看如下的例子:

    import React, { useState, useEffect } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [value, setValue] = useState(0);
    
        let timer: number;
    
        useEffect(() => {
            // timer 需要在点击时建立,因此这里只做清理使用
            return () => {
                console.log('in useEffect return', timer); // <- 正确的值
                window.clearTimeout(timer);
            }
        }, []);
    
        function dealClick() {
            timer = window.setTimeout(() => {
                setValue(100);
            }, 5000);
        }
    
        return (
            <>
                <span>Value is {value}</span>
                <button onClick={dealClick}>Click Me!</button>
            </>
        );
    }
    复制代码

    既然要记录 timer,自然是用一个内部变量来存储即可(暂不考虑连续点击按钮导致多个 timer 出现,假设只点一次。因为实际情况下点了按钮还会触发其他状态变化,继而界面变化,也就点不到了)。

    这里需要注意的是,如果把 timer 升级为状态(state),则代码反而会出现问题。考虑如下代码:

    import React, { useState, useEffect } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [value, setValue] = useState(0);
        const [timer, setTimer] = useState(0); // 把 timer 升级为状态
    
        useEffect(() => {
            // timer 需要在点击时建立,因此这里只做清理使用
            return () => {
                console.log('in useEffect return', timer); // <- 0
                window.clearTimeout(timer);
            }
        }, []);
    
        function dealClick() {
            let tmp = window.setTimeout(() => {
                setValue(100);
            }, 5000);
            setTimer(tmp);
        }
    
        return (
            <>
                <span>Value is {value}</span>
                <button onClick={dealClick}>Click Me!</button>
            </>
        );
    }
    复制代码

    有关语义上 timer 到底算不算作组件的状态我们先抛开不谈,仅就代码层面来看。利用 useState 来记住 timer 状态,利用 setTimer 去更改状态,看似合理。但实际运行下来,在 useEffect 返回的清理函数中,得到的 timer 却是初始值,即 0

    为什么两种写法会有差异呢?

    其核心在于写入的变量和读取的变量是否是同一个变量。

    第一种写法代码是把 timer 作为组件内的局部变量使用。在初次渲染组件时,useEffect 返回的闭包函数中指向了这个局部变量 timer。在 dealClick 中设置计时器时返回值依旧写给了这个局部变量(即读和写都是同一个变量),因此在后续卸载时,虽然组件重新运行导致出现一个新的局部变量 timer,但这不影响闭包内老的 timer,所以结果是正确的。

    第二种写法,timer 是一个 useState 的返回值,并不是一个简单的变量。从 React Hooks 的源码来看,它返回的是 [hook.memorizedState, dispatch],对应我们接的值和变更方法。当调用 setTimersetValue 时,分别触发两次重绘,使得 hook.memorizedState 指向了 newState(注意:不是修改,而是重新指向)。但 useEffect 返回闭包中的 timer 依然指向旧的状态,从而得不到新的值。(即读的是旧值,但写的是新值,不是同一个)

    如果觉得阅读 Hooks 源码有困难,可以从另一个角度去理解:虽然 React 在 16.8 推出了 Hooks,但实际上只是加强了函数式组件的写法,使之拥有状态,用来作为类组件的一种替代,但 React 状态的内部机制没有变化。在 React 中 setState 内部是通过 merge 操作将新状态和老状态合并后,重新返回一个新的状态对象。不论 Hooks 写法如何,这条原理没有变化。现在闭包内指向了旧的状态对象,而 setTimersetValue 重新生成并指向了新的状态对象,并不影响闭包,导致了闭包读不到新的状态。

    我们注意到 React 还提供给我们一个 useRef, 它的定义是

    useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

    ref 对象可以确保在整个生命周期中值不变,且同步更新,是因为 ref 的返回值始终只有一个实例,所有读写都指向它自己。所以也可以用来解决这里的问题。

    import React, { useState, useEffect, useRef } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [value, setValue] = useState(0);
        const timer = useRef(0);
    
        useEffect(() => {
            // timer 需要在点击时建立,因此这里只做清理使用
            return () => {
                window.clearTimeout(timer.current);
            }
        }, []);
    
        function dealClick() {
            timer.current = window.setTimeout(() => {
                setValue(100);
            }, 5000);
        }
    
        return (
            <>
                <span>Value is {value}</span>
                <button onClick={dealClick}>Click Me!</button>
            </>
        );
    }
    复制代码

    事实上我们后面会看到,useRef 和异步任务配合更加安全稳妥。

    其他陷阱

    修改状态是异步的

    这个其实比较基础了。

    import React, { useState } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [value, setValue] = useState(0);
    
        function dealClick() {
            setValue(100);
            console.log(value); // <- 0
        }
    
        return (
            <span>Value is {value}, AnotherValue is {anotherValue}</span>
        );
    }
    复制代码

    useState 返回的修改函数是异步的,调用后并不会直接生效,因此立马读取 value 获取到的是旧值(0)。

    React 这样设计的目的是为了性能考虑,争取把所有状态改变后只重绘一次就能解决更新问题,而不是改一次重绘一次,也是很容易理解的。

    在 timeout 中读不到其他状态的新值

    import React, { useState, useEffect } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [value, setValue] = useState(0);
        const [anotherValue, setAnotherValue] = useState(0);
    
        useEffect(() => {
            window.setTimeout(() => {
                console.log('setAnotherValue', value) // <- 0
                setAnotherValue(value);
            }, 1000);
            setValue(100);
        }, []);
    
        return (
            <span>Value is {value}, AnotherValue is {anotherValue}</span>
        );
    }
    复制代码

    这个问题和上面使用 useState 去记录 timer 类似,在生成 timeout 闭包时,value 的值是 0。虽然之后通过 setValue 修改了状态,但 React 内部已经指向了新的变量,而旧的变量仍被闭包引用,所以闭包拿到的依然是旧的初始值,也就是 0。

    要修正这个问题,也依然是使用 useRef,如下:

    import React, { useState, useEffect, useRef } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [value, setValue] = useState(0);
        const [anotherValue, setAnotherValue] = useState(0);
        const valueRef = useRef(value);
        valueRef.current = value;
    
        useEffect(() => {
            window.setTimeout(() => {
                console.log('setAnotherValue', valueRef.current) // <- 100
                setAnotherValue(valueRef.current);
            }, 1000);
            setValue(100);
        }, []);
    
        return (
            <span>Value is {value}, AnotherValue is {anotherValue}</span>
        );
    }
    复制代码

    还是 timeout 的问题

    假设我们要实现一个按钮,默认显示 false。当点击后更改为 true,但两秒后变回 false( true 和 false 可以互换)。考虑如下代码:

    import React, { useState } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [flag, setFlag] = useState(false);
    
        function dealClick() {
            setFlag(!flag);
    
            setTimeout(() => {
                setFlag(!flag);
            }, 2000);
        }
    
        return (
            <button onClick={dealClick}>{flag ? "true" : "false"}</button>
        );
    }
    复制代码

    我们会发现点击时能够正常切换,但是两秒后并不会变回来。究其原因,依然在于 useState 的更新是重新指向新值,但 timeout 的闭包依然指向了旧值。所以在例子中,flag 一直是 false,虽然后续 setFlag(!flag),但依然没有影响到 timeout 里面的 flag

    解决方法有二。

    第一个还是利用 useRef

    import React, { useState, useRef } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [flag, setFlag] = useState(false);
        const flagRef = useRef(flag);
        flagRef.current = flag;
    
        function dealClick() {
            setFlag(!flagRef.current);
    
            setTimeout(() => {
                setFlag(!flagRef.current);
            }, 2000);
        }
    
        return (
            <button onClick={dealClick}>{flag ? "true" : "false"}</button>
        );
    }
    复制代码

    第二个是利用 setFlag 可以接收函数作为参数,并利用闭包和参数来实现

    import React, { useState } from 'react';
    
    export const MyComponent: React.FC<{}> = () => {
        const [flag, setFlag] = useState(false);
    
        function dealClick() {
            setFlag(!flag);
    
            setTimeout(() => {
                setFlag(flag => !flag);
            }, 2000);
        }
    
        return (
            <button onClick={dealClick}>{flag ? "true" : "false"}</button>
        );
    }
    复制代码

    setFlag 参数为函数类型时,这个函数的意义是告诉 React 如何从当前状态产生出新的状态(类似于 redux 的 reducer,不过是只针对一个状态的子 reducer)。既然是当前状态,因此返回值取反,就能够实现效果。

    总结

    在 Hook 中出现异步任务尤其是 timeout 的时候,我们要格外注意。useState 只能保证多次重绘之间的状态是一样的,但不保证它们就是同一个对象,因此出现闭包引用的时候,尽量使用 useRef 而不是直接使用 state 本身,否则就容易踩坑。反之如果的确碰到了设置了新值但读取到旧值的情况,也可以往这个方向想想,可能就是这个原因所致。

    参考文章


    作者:小蘑菇哥哥
    链接:https://juejin.im/post/5dad5020f265da5b9603e0ca
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    展开全文
  • usereducer 异步React’s useReducer brings us one step closer to replacing ReduxStore with basic react hooks. Now creating a store and dispatch function is as easy as calling useReducer with your ...
  • SpringBoot2.X系列(四):使用异步

    万次阅读 2019-03-14 10:33:44
    请移步链接:https://www.javaclub.club/blog/article.html?id=12
  • using System; using System.Threading.Tasks;... /// 异步执行帮助类 /// </summary> public class DelegateHelper { /// <summary> /// 异步执行方法 /// </summary> //
  • Simulink三相异步电机仿真

    万次阅读 多人点赞 2019-03-01 16:31:22
    MATLAB/Simulink三相异步电机直接启动仿真 小树不修不直溜,人不学习哏揪揪!隔了好长一段时间,本小学生终于又回归正常的学习与记录生活。前些时,利用CADe_SIMu V1.0仿真了一些常见常用的电机控制电路,但是CADe_...
  • 同步和异步的区别

    万次阅读 多人点赞 2018-10-25 19:10:42
    原文地址:同步和异步,区别 同步: 同步的思想是:所有的操作都做完,才返回给用户。这样用户在线等待的时间太长,给用户一种卡死了的感觉(就是系统迁移中,点击了迁移,界面就不动了,但是程序还在执行,卡死了的...
  • JS 异步编程六种方案

    万次阅读 多人点赞 2019-06-28 11:15:45
    前言 我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务。 这种模式虽然实现起来比较简单,执行环境相对单纯,但是...
  • 使用ES6新特性async await进行异步处理

    万次阅读 多人点赞 2018-07-12 15:56:30
    我们往往在项目中会遇到这样的业务需求,就是首先...那是相当恶心的,下面我就来讲一下如何使用ES6的新特性async await进行异步处理,使上述情况就好像写同步代码一样,首先我们先举个例子: 先写上json文件: cod...
  • Spring Boot---(4)SpringBoot异步处理任务

    万次阅读 多人点赞 2018-01-12 17:21:06
    欢迎关注公众号:java4all 场景:现在需要上传一个Excel表格,数据量几万条,而且,上传解析后还需要进行计算,然后插入数据库。...所以,这里需要做异步处理: 1.上传-->返回正在解析的标志; 2.解...
  • SpringBoot异步编程

    万次阅读 2020-02-01 16:14:47
    配置@Async import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation....
  • SpringBoot异步处理任务

    万次阅读 2020-06-28 10:06:19
    2)异步:直接返回给用户指定的状态,同时程序在后台继续运行,用户不用等待。 实现 同步实现 @Autowired private TaskAsync taskAsync; /** * 同步处理的方式 */ @GetMapping("/test2") public ...
  • Java多线程实现异步调用

    万次阅读 2018-07-20 14:00:52
    首先我们来一个实际的应用场景:用户请求一些报表数据,但是这些数据需要实时计算,那么用户要等待的时间就会很久,这时候我们就可以用异步的方式来处理,更通俗的场景就是生活中烧开水的时候,你可以去包饺子。...
  • C#异步编程基础入门总结

    万次阅读 2017-08-23 15:54:07
    异步这概念刚开始接触的时候,不是那么容易接受,但是需要用的地方还真的挺多的,刚学习的时候,也很懵逼走了不少弯路,所以这里有必要总结一下。
  • 是时候向 Redux 说再见了 先上代码和在线执行链接,下面的代码注释中有一些解释 CodeSandbox在线执行地址 import React, { createContext, useContext, useReducer } from "react"; import ReactDOM from "react-...
  • js中的同步和异步的个人理解

    万次阅读 多人点赞 2017-06-09 15:09:05
    你应该知道,javascript语言是一门“单线程”的语言,不像java语言,...那么这里说的同步和异步到底是什么呢?其实个人觉得js官方的文档在使用两个词的时候并不准确,包括很多其他词汇,都只是听起来高深,但实际应用好
  • Spring Boot启用异步线程

    千次阅读 2018-10-31 14:31:14
    现在实际开发中常用的方式是采用JMS消息队列方式,发送消息到其他的系统中进行导出,或者是在项目中开启异步线程来完成耗时的导出工作。本文将结合文本导出的场景,来讲解一些Spring Boot中如何开启...
  • 首先,我得解释一下为什么我的标题取消异步加载打引号,这是因为可能最后实现效果并不是你自己想象中的那样。大家看取消异步加载,这不是很简单吗?AsyncTask中不是有一个cancel方法吗?直接调用该方法不就行了吗?...
  • mybatis异步操作数据库

    千次阅读 2018-09-13 10:54:43
    很多时候我们需要用mybatis来做数据库orm,基于...所以我们需要封装一套提供异步查询回调机制。 异步操作。提到异步操作,我们就得提到回调接口。回调接口就是通过在主线程监听其他线程执行完的结果取得返回值。或...
  • [springboot] 异步开发之异步请求

    千次阅读 2019-01-08 18:15:19
    何为异步请求 在Servlet 3.0之前,Servlet采用Thread-Per-Request的方式处理请求,即每一次Http请求都由某一个线程从头到尾负责处理。如果一个请求需要进行IO操作,比如访问数据库、调用第三方服务接口等,那么其所...
  • springboot的@Async注解实现异步 要在springboot中使用异步调用方法,只要在被调用的方法上面加上@Async就可以了 1.准备工作 准备一个springboot工程,在Application类上加上EnableAsync注解开启异步 ...
  • 谈谈NodeJS异步

    千次阅读 2019-03-31 00:57:59
    同步与异步 一谈起Node,了解过的人会想起是利用异步IO实现高性能后台,那么什么是异步IO呢? 异步IO与同步IO的区别是什么呢?举个例子:在生活中,大家每天都会用到微信来进行聊天。那么某个时间有两个人来跟你聊天...
  • 1.什么是同步和异步 同步,也就是你在执行代码时,他会等待代码返回结果,不管这代码执行多久,只有代码返回结果了然后再代码才会继续往下执行。而异步指的是:我要执行一段代码A,我不等待他出结果,我会为他设置...
  • 1.什么是Saga? ... 最初这篇论文是为了解决分布式系统中的LLT(Long Lived Transaction),也就是长时运行事务的数据一致性问题的。这么说有点抽象,我们来举个具体的例子: 假如你在一个在线订票系统上订了一张...
  • python_异步

    千次阅读 2018-12-12 17:04:01
    深入理解 Python 异步编程   前言 很多朋友对异步编程都处于“听说很强大”的认知状态。鲜有在生产项目中使用它。而使用它的同学,则大多数都停留在知道如何使用 Tornado、Twisted、Gevent 这类异步框架上,出现...
  • 今天在我的React交流群里面与朋友们一起聊面试题,突然其中一个朋友问this.setState的一套东西,其他的东西我都了解过,但是当他问为什么this.setState是异步的时候我就哑口无言了。。。于是到github上就去搜答案(有...
  • C#异步编程总结

    万次阅读 2017-05-02 15:26:43
    C#中的异步编程进程与线程程序在启动时,系统会在内存中创建一个进程。进程是程序运行所需资源的集合,这些资源包括虚地址空间、文件句柄和其他程序运行所需的东西。在进程的内部,系统创建一个称为线程的内核对象,...
  • 之前翻看别的大佬的...观看了几篇之后还是没有怎么看懂,于是自己开始分析代码,并整理了此文章,我相信通过此文章朋友们能对异步同步还有,setTimeout,Promise,async这些内容了然于胸,接下来让我们走入正题: ...

空空如也

1 2 3 4 5 ... 20
收藏数 1,049,163
精华内容 419,665
关键字:

useredux 异步