精华内容
下载资源
问答
  • React使用useEffect实践项目 该入门工具包括一个简单的项目基础,该项目带有可为孩子配置火鸡绘画作品的控件。 它包括图像( src / components / PictureDisplay )和消息( src / components / Message )的功能...
  • 5:组件生命周期和状态(第5节) 研究组件的生命周期-安装,更新和卸载 项目:使用您的生命周期方法知识来构建时钟。 您需要在组件开始渲染时挂载它,更新它,然后使用不同的useEffect钩子将其卸载。
  • 使用useEffect与React中的异步功能的便捷功能。 为什么 方便,使代码更整洁。 允许以下 import { useAsyncEffect } from "@jeswr/use-async-effect" ; function MyComponent ( ) { useAsyncEffect ( async ( ) => ...
  • 小部件应用 本申请的目的 这个应用程序用来学习React提供的一些基本的... 制作这些小部件时使用的是useState,useEffect和useRef。 该项目与Stephon Grider的“ 一起作为代码完成。 如果您想了解更多有关该主题的信息
  • 有两个效果挂钩, useOnce和useEffect ,它们只能在第一次渲染时或在特定事物发生更改时才触发某些事物。 安装 npm install lit-element-effect 用法 @ property ( ) prop: string | undefined ; render ( ) { ...
  • useAction 与 useEffect 几乎相同,但不延迟。 为什么要使用Action? 与 componentDidMount 和 componentDidUpdate 不同,传递给 useEffect 的函数在布局和绘制之后,在延迟事件期间触发。 这使它适用于许多常见的副...
  • 问:让我们实现理解react的useEffect函数。 功能要求 它存储执行效果的功能和相关性数据。 允许您在渲染html元素后运行效果。 它比较依赖性数据并决定是否执行效果。 通过效果功能更改文档标题。 功能操作图 如何...
  • Work_With_useEffect 使用 CodeSandbox 创建
  • 用于探索和调试 useEffect 和 useState 的功能 nextjs 应用程序 当前状态:在文本框中输入查询,单击按钮设置搜索状态启动对 openlibrary api 的异步调用。 填充列表,填充列表框。 不完整:清理搜索表单,将额外的...
  • :whale:这是React的useEffect钩子,除了对输入使用深度比较,而不是引用相等use-deep-compare-effect:whale:这是React的useEffect钩子,除了对输入使用深度比较,而不是引用相等,警告:请仅在确实使用找不到使用...
  • useEffect使用指南

    2021-07-15 17:55:19
    本文是阅读A Complete Guide to useEffect之后的个人总结,建议拜读原文 理解hooks工作机制 可以这样说,在使用了useState或是useEffect这样的hooks之后,每次组件在render的时候都生成了一份本次render的state、...

    来源:XJBT的https://www.jianshu.com/p/fd17ce2d7e46

    本文是阅读A Complete Guide to useEffect之后的个人总结,建议拜读原文

    理解hooks工作机制

    可以这样说,在使用了useState或是useEffect这样的hooks之后,每次组件在render的时候都生成了一份本次render的state、function、effects,这些与之前或是之后的render里面的内容都是没有关系的。而对于class component来说,state是一种引用的形式。这就造成了二者在一些表现上的不同。

    来看下面这样一段代码:

        function Counter() {
            const [count, setCount] = useState(0);
    
            function handleAlertClick() {
                setTimeout(() => {
                alert('You clicked on: ' + count);
                }, 3000);
            }
            // 多次点击click me按钮,然后点击一下show alert按钮,然后又快速点击多次click me按钮,alert出来的count是点击该按钮时的count还是最新的count??
            // 实验表明,显示的是点击时的按钮,这就意味着handleAlertClick这个函数capture了被点击时的那个count,这也就是说每一轮的count都是不一样的
            return (
                <div>
                    <p>You clicked {count} times</p>
                    <button onClick={() => setCount(count + 1)}>
                        Click me
                    </button>
                    <button onClick={handleAlertClick}>
                        Show alert
                    </button>
                </div>
            );
        }
    

    再看这样一段代码:

        function Counter() {
            const [count, setCount] = useState(0);
    
            useEffect(() => {
                setTimeout(() => {
                    console.log(count)
                }, 3000)
            })
            // 在3秒内快速点击5次按钮,控制台打出的结果是什么样的?
            // 0 1 2 3 4 5
            return (
                <div>
                    <p>You clicked {count} times</p>
                    <button onClick={() => setCount(count + 1)}>
                        Click me
                    </button>
                </div>
            );
        }
    

    把上述代码改成class component的形式:

        class Example extends React.Component{
    
            constructor(props) {
                super(props);
                    this.state = {
                    count: 0,
                }
            }
    
            componentDidUpdate() {
                setTimeout(() => {
                    console.log(this.state.count)
                }, 3000)
            }
    
            add = () => {
                const {count} = this.state;
                this.setState({count: count + 1})
            }
    
            // 同样的操作,打印出的结果是 5 5 5 5 5
    
            render() {
                return (
                    <div>
                        <button onClick={this.add}>click me</button>
                    </div>
                )
            }
        }
    

    对于class component里面的表现,我们可以通过闭包来改变,之所以如此是因为class component里面的state随着render是发生变化的,而useEffect里面即使使用props.count也不会有问题,因为useEffect里面的所有东西都是每次render独立的

        componentDidUpdate() {
            // 在class component中必须每次把count取出来
            const { count } = this.state;
            setTimeout(() => {
                console.log(count)
            }, 3000)
        }
    

        function Example(props) {
            useEffect(() => {
                setTimeout(() => {
                console.log(props.counter);
                }, 1000);
            });
            // 在useEffect中不需要先把count从props里面取出来,每次依然是独立的
        }
    

    可以发现,尽管useEffect里面的函数延迟执行了,但是打出的count依然是当时render里面的count,这也说明了其实每次render都是独立的,里面有独立的state、effects、function

        // During first render
        function Counter() {
            const count = 0; // Returned by useState()
            // ...
            <p>You clicked {count} times</p>
            // ...
        }
    
        // After a click, our function is called again
        function Counter() {
            const count = 1; // Returned by useState()
            // ...
            <p>You clicked {count} times</p>
            // ...
        }
    
        // After another click, our function is called again
        function Counter() {
            const count = 2; // Returned by useState()
            // ...
            <p>You clicked {count} times</p>
            // ...
        }
    

    下面这段话是精髓:

    Inside any particular render, props and state forever stay the same. But if props and state are isolated between renders, so are any values using them (including the event handlers). They also “belong” to a particular render. So even async functions inside an event handler will “see” the same count value.

    useEffect的一些注意点

    来看官方文档里面关于useEffect清除工作的示例:

        function Example(props) {
            useEffect(() => {
                ChatAPI.subscribeToFriendStatus(props.id,       handleStatusChange);
                return () => {
                    ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
                };
            });
        }
    

    如果props从{id: 10}变化为{id: 20}那么react是怎么样来渲染组件、怎么样做清除工作的呢?

    按照惯性思维,我们可能觉得应该是先清理之前一次render注册的事件,然后render组件,然后再注册本次render的事件

        React cleans up the effect for {id: 10}.
        React renders UI for {id: 20}.
        React runs the effect for {id: 20}.
    

    但实际上react并不是这样工作的,而是像下面这样,因为react总是在浏览器paint之后再去做effects相关的事情,无论是useEffect还是他返回的函数,而且清理函数也和其他函数一样能够capture当前的props和state,尽管在他执行时已经是新的组件render好了

        React renders UI for {id: 20}.
        The browser paints. We see the UI for {id: 20} on the screen.
        React cleans up the effect for {id: 10}.
        React runs the effect for {id: 20}.
    

    清理函数就像闭包一样直接把他所属的render的props和state消费,然后在需要执行的时候使用这些值

        // First render, props are {id: 10}
        function Example() {
            // ...
            useEffect(
                // Effect from first render
                () => {
                    ChatAPI.subscribeToFriendStatus(10, handleStatusChange);
                    // Cleanup for effect from first render
                    return () => {
                        ChatAPI.unsubscribeFromFriendStatus(10, handleStatusChange);
                    };
                }
            );
            // ...
            }
    
        // Next render, props are {id: 20}
        function Example() {
            // ...
            useEffect(
                // Effect from second render
                () => {
                    ChatAPI.subscribeToFriendStatus(20, handleStatusChange);
                    // Cleanup for effect from second render
                    return () => {
                        ChatAPI.unsubscribeFromFriendStatus(20, handleStatusChange);
                    };
                }
            );
            // ...
        }
    

    忘记lifecycle的观念,拥抱synchronization

    在class component里面,lifecycle是我们做一切的基础,但是在使用react-hooks的时候,请忘记lifecycle,尽管useEffect函数很多时候达到了相似的效果

    但从根本上来讲,react-hooks的作用是一种同步的作用,同步函数hooks函数内的内容与外部的props以及state,所以才会在每次render之后执行useEffect里面的函数,这时可以获取到当前render结束后的props和state,来保持一种同步

    但正是由于useEffect里面的内容在每次render结束后都会执行,可能有时候内部的内容并没有发生变化,这时就会产生冗余的render,这时候就需要引入依赖,由写程序的人来告诉react我这个useEffect依赖了外部的那些参数,只有这些参数发生变化的时候才去执行我里面的函数。

    因为react自己不知道什么时候useEffect里面的函数其实没有发生变化。

         useEffect(() => {
            document.title = 'Hello, ' + name;
        }, [name]); // Our deps
    

    上面这段代码相当于告诉react,我这个effect的依赖项是name这个变量,只有当name发生变化的时候才去执行里面的函数

    而且这个比较是浅比较,如果state是一个对象,那么对象只要指向不发生变化,那么就不会执行effect里面的函数

    譬如:

        function Example() {
            const [count, setCount] = useState({a: 12});
    
            useEffect(() => {
                console.log('effect');
                return () => {
                    console.log('clean')
                }
            }, [count])
    
            function handleClick() {
                count.a++;
                setCount(count)
            }
    
            // 点击按钮时发现屏幕显示的值不发生变化,而且effect里面的函数也没有执行,所以进行的是浅比较,这点类似于pureComponent
    
            return (
                <div>
                    <p>You clicked {count.a} times</p>
                    <button onClick={handleClick}>
                        Click me
                    </button>
                </div>
            );
        }
    

    关于dependency数组

    如果强行欺骗react来达到跳过某些渲染之后的effect函数的话,那么可能会出现一些意想不到的后果:

    如下代码,我们想模拟一个定时器,在第一次渲染之后挂载,在组件卸载的时候取消这个定时器,那么这好像和把dependency数组设为[]的功能很像,但是如果这样做的话,结果是定时器只加一次。

        function Counter() {
            const [count, setCount] = useState(0);
    
            useEffect(() => {
                const id = setInterval(() => {
                    setCount(count + 1);
                }, 1000);
                // 定时器只加一次的原因在于虽然setInterval函数里面的函数每秒都会执行一次,但是count值始终是初始的0,因为这个函数绑定了第一轮render之后的count值,
                return () => clearInterval(id);
            }, []);
    
            return <h1>{count}</h1>;
        }
    

    如果写成下面这样的形式的话:

        function Counter() {
            const [count, setCount] = useState(0);
    
            setInterval(() => {
                setCount(count + 1);
            }, 1000);
            // 造成的后果就是能一直更新count,但是每一轮循环都会执行上面这行代码,定时器越来越多,然后,就卡死啦,而且每个定时器都会执行一遍,那么屏幕上的数字每秒都会在跳,可以试试看
            return <h1>{count}</h1>;
        }
    
    

    所以通过设置dependency数组来欺骗react达到自己不可告人的目的的话,很容易出现bug,而且还不容易发现,所以还是老老实实的不要骗人

    要让计时器正确工作的话,第一种方法是把dependency数组正确设置[count],但这显然不是最好的方法,因为每一轮都会设置计时器,清除计时器。但至少定时器work了。

    还有一种方法是利用functional updater,这时候你也可以不用设置dependency

        useEffect(() => {
            const id = setInterval(() => {
                setCount(preCount => preCount + 1);
                // 此时setCount里面的函数的入参是前一次render之后的count值,所以这样的情况下计时器可以work
            }, 1000);
            return () => clearInterval(id);
        }, []);
    

    其他hooks

    useContext

    使用方法:

        const value = useContext(myContext);
    

    当最近的一个myContext.Provider更新的时候,这个hook就会导致当前组件发生更新

    useReducer

        
        function reducer(state, action) {
            switch (action.type) {
                case 'increment':
                    return {count: state.count + 1};
                case 'decrement':
                    return {count: state.count - 1};
                default:
                    throw new Error();
            }
        }
    
        function Counter() {
            const [state, dispatch] = useReducer(reducer, {count: 100});
    
            // 如果此处不传入一个initialState: {count: 100}的话,那么默认initialState就是undefined,那么后面的取值就会报错
            return (
                <>
                    Count: {state.count}
                    <button onClick={() => dispatch({type: 'increment'})}>+</button>
                    <button onClick={() => dispatch({type: 'decrement'})}>-</button>
                </>
            );
        }
    

    使用dispatch以后,判断是否重新render是通过Object.is来判断的,每次render之后返回的dispatch其实都是不变的,所以之前定时器的例子最好的解决方案就是利用useReducer来实现:

        function Counter() {
            const [state, dispatch] = useReducer(reducer, initialState);
            const { count, step } = state;
    
            useEffect(() => {
                const id = setInterval(() => {
                dispatch({ type: 'tick' });
                }, 1000);
                return () => clearInterval(id);
            }, [dispatch]);
            // 现在useEffect不依赖count,依赖的是dispatch,而dispatch在每次render之后都是不变的,所以就不会每次render之后都清除计时器再重新设置计时器
            // 其实这里把dependency数组设为[]也是完全一样的
    
            return (
                <>
                <h1>{count}</h1>
                <input value={step} onChange={e => {
                    dispatch({
                        type: 'step',
                        step: Number(e.target.value)
                    });
                }} />
                </>
            );
        }
    
        const initialState = {
            count: 0,
            step: 1,
        };
    
        function reducer(state, action) {
            const { count, step } = state;
            if (action.type === 'tick') {
                return { count: count + step, step };
            } else if (action.type === 'step') {
                return { count, step: action.step };
            } else {
                throw new Error();
            }
        }
    
    
    

    useCallback

        const memoizedCallback = useCallback(
            () => {
                doSomething(a, b);
            },
            [a, b],
        );
        // 返回的memoizedCallback只有当a、b发生变化时才会变化,可以把这样一个memoizedCallback作为dependency数组的内容给useEffect
    

    我们来看一个useEffect的dependency数组含有函数的情况:

        function Counter() {
            const [count, setCount] = useState(0);
            const [a, setA] = useState(100);
    
            const fn = useCallback(() => {
                console.log('callback', a)
            }, [a])
            // 可知fn是依赖于a的,只有当a发生变化的时候fn才会变化,否则每轮render的fn都是同一个
    
            const f1 = () => {
                console.log('f1')
            }
            // 对于f1,每轮循环都有独自的f1,所以相当于一直在变化,如果useEffect依赖于f1的话,每次render之后都会执行
    
            useEffect(() => {
                console.log('this is effect')
            }, [f1])
            // 当dependency数组里面是f1时,不管更新count还是a,都会执行里面的函数,打印出this is effect
            // 当dependency数组里面是fn时,只有更新a时才会执行该函数
            return (
                <>
                    Count: {count}
                    <button onClick={() => setCount(count + 1)}>+</button>
                    <button onClick={() => setCount(count - 1)}>-</button>
                    <br />
                    <button onClick={() => setA(a + 1)}>+</button>
                    <button onClick={() => setA(a - 1)}>-</button>
                </>
            );
        }
    
    

    useMemo

        const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
    

    useRef

        const refContainer = useRef(initialValue);
    

    注意:useRef返回相当于一个{current: ...}的plain object,但是和正常这样每轮render之后直接显式创建的区别在于,每轮render之后的useRef返回的plain object都是同一个,只是里面的current发生变化

    而且,当里面的current发生变化的时候并不会引起render

    补充

    dependency数组里面写函数作为dependency的情景:

        function SearchResults() {
            const [query, setQuery] = useState('react');
    
            // Imagine this function is also long
            function getFetchUrl() {
                return 'https://hn.algolia.com/api/v1/search?query=' + query;
            }
            // 对于这样一个组件,如果我们改变了query,按理来说应该要重新拉取数据,但是这种写法里面就无法实现,除非在useEffect的dependency数组里面添加一个query,但是这样是很不明显的,因为useEffect里面的函数只写了一个fetchData,并没有看到query的身影,所以query很容易被忽略,而一旦忽略就会带来bug,所以简单的解决方法就是把fetchData这个函数作为dependency写进useEffect的dependency数组,但是这样也会带来问题,就是每次render之后,无论这次render是否改变了query,都会导致fetchData这个函数发生变化(因为每次render之后函数都是不同的),都会重新拉取数据,这是我们不想要的结果
    
            // Imagine this function is also long
            async function fetchData() {
                const result = await axios(getFetchUrl());
                setData(result.data);
            }
    
            useEffect(() => {
                fetchData();
            }, []);
    
            // ...
        }
    

    第一次改进,把函数直接写进dependency数组里面:

        function SearchResults() {
            // 🔴 Re-triggers all effects on every render
            function getFetchUrl(query) {
                return 'https://hn.algolia.com/api/v1/search?query=' + query;
            }
    
            useEffect(() => {
                const url = getFetchUrl('react');
                // ... Fetch data and do something ...
            }, [getFetchUrl]); // 🚧 Deps are correct but they change too often
    
            useEffect(() => {
                const url = getFetchUrl('redux');
                // ... Fetch data and do something ...
            }, [getFetchUrl]); // 🚧 Deps are correct but they change too often
    
            // ...
        }
    

    上面这种写法的问题就是useEffect里面的函数调用过于频繁,再次利用useCallback进行改造:

        function SearchResults() {
            const [query, setQuery] = useState('react');
    
            // ✅ Preserves identity until query changes
            const getFetchUrl = useCallback(() => {
                return 'https://hn.algolia.com/api/v1/search?query=' + query;
            }, [query]);  // ✅ Callback deps are OK
            // 只有当query发生变化的时候getFetchUrl才会变化
            useEffect(() => {
                const url = getFetchUrl();
                // ... Fetch data and do something ...
            }, [getFetchUrl]); // ✅ Effect deps are OK
    
            // ...
        }
    

    useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖

    实际上,函数在effect里面也是一种数据流,而在class component中则不是

    关于竞态

        function Article({ id }) {
            const [article, setArticle] = useState(null);
    
            useEffect(() => {
                let didCancel = false;
                // 利用didCancel这个变量来解决竞态问题,如果本次render之后的请求到下次render之后才返回,那么这次render之后的didCancel以及在清理函数里面被设置为true了,就不会继续执行
                async function fetchData() {
                    const article = await API.fetchArticle(id);
                    if (!didCancel) {
                        setArticle(article);
                    }
                }
    
                fetchData();
    
                return () => {
                    didCancel = true;
                };
            }, [id]);
    
            // ...
        }
    

    展开全文
  • useEffect的解读

    2021-08-05 19:05:03
    理解useEffect,就必须先深入理解 Function Component 的渲染机制。 Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。 .

    每天对自己多问几个为什么,总是有着想象不到的收获。 一个菜鸟小白的成长之路(copyer)

    前面: 这篇完全是根据阅读大佬博客写下来的笔记(摘抄笔记)。

    博客原文:精读《useEffect 完全指南》 (qq.com)

    理解useEffect,就必须先深入理解 Function Component 的渲染机制。

    Function Component 是更彻底的状态驱动抽象,甚至没有 Class Component 生命周期的概念,只有一个状态,而 React 负责同步到 DOM。

    理解render函数

    每次 Render 都有自己的 Props 与 State

    每个render函数都是自己的state和props,根据状态的不同的变化,而造成不停的render。那么可以这样理解, 每次的render的内容都会形成一个快照并保留下来,因此状态变化N次,就形成了N个render的状态,每个render的状态都有着自己固定的不变的props和state

    function Counter() {
      const [count, setCount] = useState(0);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
      );
    }
    

    在每次点击时,count 只是一个不会变的常量,而且也不存在利用 Proxy 的双向绑定,只是一个常量存在于每次 Render 中。

    初始状态下 count 值为 0,而随着按钮被点击,在每次 Render 过程中,count 的值都会被固化为 123

    // During first render
    function Counter() {
      const count = 0; // Returned by useState()
      // ...
      <p>You clicked {count} times</p>;
      // ...
    }
    
    // After a click, our function is called again
    function Counter() {
      const count = 1; // Returned by useState()
      // ...
      <p>You clicked {count} times</p>;
      // ...
    }
    
    // After another click, our function is called again
    function Counter() {
      const count = 2; // Returned by useState()
      // ...
      <p>You clicked {count} times</p>;
      // ...
    }
    

    其实不仅是对象,函数在每次渲染时也是独立的。这就是 Capture Value 特性,后面遇到这种情况就不会一一展开,只描述为 “此处拥有 Capture Value 特性”。

    每次render函数都有着自己的逻辑处理

    解释了为什么下面的代码会输出 5 而不是 10

    const Main: React.FC = () => {
        const [temp, setTemp] = React.useState(5);
        const log = () => {
          setTimeout(() => {
            console.log("3 秒前 temp = 5,现在 temp =", temp);
          }, 3000);
        };
        const btn = () => {
            setTemp(10)
            console.log(temp)    // 5
            log()                // 3 秒前 temp = 5,现在 temp = 5
        }
      
        return (
          <button onClick={btn}>
            点击
          </button>
        );
    }
    
    export default Main
    

    当触发btn点击是事件后,首先就会执行setTemp这个函数(改变temp的值后,就会生成一个新的render函数),但是接下来的代码 console.log(temp)和log()函数依旧还是本次的render函数, temp的状态为5,所以就是 打印出来 temp 为5

    原因就是 templog 都拥有 Capture Value 特性。(捕获本次的render)

    每次 Render 都有自己的 Effects

    useEffect 也一样具有 Capture Value 的特性。

    useEffect 在实际 DOM 渲染完毕后执行,那 useEffect 拿到的值也遵循 Capture Value 的特性:

    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        document.title = `You clicked ${count} times`;
      });
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
      );
    }
    

    上面的 useEffect 在每次 Render 过程中,拿到的 count 都是固化下来的常量。

    如何绕过 Capture Value

    利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。

    const Main: React.FC = () => {
        const [count, setCount] = React.useState(0);
        const latestCount = useRef(count);
    
        React.useEffect(() => {
            // Set the mutable latest value
            latestCount.current = count;
            setTimeout(() => {
            // Read the mutable latest value
            console.log(`You clicked ${latestCount.current} times`);
            }, 3000);
        });
        return (
            <button onClick={() => setCount(count+1)}>点击</button>
        )
    }
    
    export default Main
    

    也可以简洁的认为,ref 是 Mutable 的,而 state 是 Immutable 的。

    回收机制

    在组件被销毁时,通过 useEffect 注册的监听需要被销毁,这一点可以通过 useEffect 的返回值做到:

    useEffect(() => {
      ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
      return () => {
        ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
      };
    });
    

    在组件被销毁时,会执行返回值函数内回调函数。同样,由于 Capture Value 特性,每次 “注册” “回收” 拿到的都是成对的固定值。

    用同步取代 “生命周期”

    Function Component 不存在生命周期,所以不要把 Class Component 的生命周期概念搬过来试图对号入座。Function Component 仅描述 UI 状态,React 会将其同步到 DOM,仅此而已。

    既然是状态同步,那么每次渲染的状态都会固化下来,这包括 state props useEffect 以及写在 Function Component 中的所有函数。

    然而舍弃了生命周期的同步会带来一些性能问题,所以我们需要告诉 React 如何比对 Effect。

    告诉 React 如何对比 Effects

    虽然 React 在 DOM 渲染时会 diff 内容,只对改变部分进行修改,而不是整体替换,但却做不到对 Effect 的增量修改识别。因此需要开发者通过 useEffect 的第二个参数告诉 React 用到了哪些外部变量:

    useEffect(() => {
      document.title = "Hello, " + name;
    }, [name]); // Our deps
    

    直到 name 改变时的 Rerender,useEffect 才会再次执行。

    不要对 Dependencies 撒谎

    如果你明明使用了某个变量,却没有申明在依赖中,你等于向 React 撒了谎,后果就是,当依赖的变量改变时,useEffect 也不会再次执行:

    useEffect(() => {
      document.title = "Hello, " + name;
    }, []); // Wrong: name is missing in dep
    
    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const id = setInterval(() => {
          setCount(count + 1);
        }, 1000);
        return () => clearInterval(id);
      }, []);
    
      return <h1>{count}</h1>;
    }
    

    setInterval 我们只想执行一次,所以我们自以为聪明的向 React 撒了谎,将依赖写成 []

    “组件初始化执行一次 setInterval,销毁时执行一次 clearInterval,这样的代码符合预期。” 你心里可能这么想。

    但是你错了,由于 useEffect 符合 Capture Value 的特性,拿到的 count 值永远是初始化的 0相当于 setInterval 永远在 count0 的 Scope 中执行,你后续的 setCount 操作并不会产生任何作用。

    诚实的代价

    useEffect(() => {
      const id = setInterval(() => {
        setCount(count + 1);
      }, 1000);
      return () => clearInterval(id);
    }, [count]);
    

    你老实告诉 React “嘿,等 count 变化后再执行吧”,那么你会得到一个好消息和两个坏消息。

    好消息是,代码可以正常运行了,拿到了最新的 count

    坏消息有:

    1. 计时器不准了,因为每次 count 变化时都会销毁并重新计时。
    2. 频繁 生成/销毁 定时器带来了一定性能负担。

    怎么既诚实又高效呢?

    上述例子使用了 count,然而这样的代码很别扭,因为你在一个只想执行一次的 Effect 里依赖了外部变量。

    既然要诚实,那只好 想办法不依赖外部变量

    useEffect(() => {
      const id = setInterval(() => {
        setCount(c => c + 1);
      }, 1000);
      return () => clearInterval(id);
    }, []);
    

    setCount 还有一种函数回调模式,你不需要关心当前值是什么,只要对 “旧的值” 进行修改即可。这样虽然代码永远运行在第一次 Render 中,但总是可以访问到最新的 state

    将更新与动作解耦

    你可能发现了,上面投机取巧的方式并没有彻底解决所有场景的问题,比如同时依赖了两个 state 的情况:

    useEffect(() => {
      const id = setInterval(() => {
        setCount(c => c + step);
      }, 1000);
      return () => clearInterval(id);
    }, [step]);
    

    你会发现不得不依赖 step 这个变量,我们又回到了 “诚实的代价” 那一章。当然 Dan 一定会给我们解法的。

    利用 useEffect 的兄弟 useReducer 函数,将更新与动作解耦就可以了:

    const [state, dispatch] = useReducer(reducer, initialState);
    const { count, step } = state;
    
    useEffect(() => {
      const id = setInterval(() => {
        dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
      }, 1000);
      return () => clearInterval(id);
    }, [dispatch])
    

    这就是一个局部 “Redux”,由于更新变成了 dispatch({ type: "tick" }) 所以不管更新时需要依赖多少变量,在调用更新的动作里都不需要依赖任何变量。 具体更新操作在 reducer 函数里写就可以了。

    展开全文
  • 引言 Hooks 是 React 16.8 的新增特性,至今经历两年的时间,它可以让你在不编写 class 组件的情况下使用 state 以及其他 React 特性。useEffect 是...

    引言

    Hooks 是 React 16.8 的新增特性,至今经历两年的时间,它可以让你在不编写 class 组件的情况下使用 state 以及其他 React 特性。useEffect 是基础 Hooks 之一,我在项目中使用较为频繁,但总有些疑惑 ,比如:

    • 如何正确使用 useEffect

    • useEffect 的执行时机 ?

    • useEffect 和生命周期的区别 ?

    本文主要从以上几个方面分析 useEffect ,以及与另外一个看起来和 useEffect 很像的 Hook useLayoutEffect 的使用和它们之间的区别。

    useEffect 简介

    首先介绍两个概念,纯函数和副作用函数。纯函数( Pure Function ):对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,这样的函数被称为纯函数。副作用函数( Side effect Function ):如果一个函数在运行的过程中,除了返回函数值,还对主调用函数产生附加的影响,这样的函数被称为副作用函数。useEffect 就是在 React 更新 DOM 之后运行一些额外的代码,也就是执行副作用操作,比如请求数据,设置订阅以及手动更改 React 组件中的 DOM 等。

    正确使用 useEffect

    基本使用方法:useEffect(effect)根据传参个数和传参类型,useEffect(effect) 的执行次数和执行结果是不同的,下面一一介绍。

    • 默认情况下,effect 会在每次渲染之后执行。示例如下:

    useEffect(() => {
      const subscription = props.source.subscribe();
      return () => {
        // 清除订阅
        subscription.unsubscribe();
      };
    });
    
    • 也可以通过设置第二个参数,依赖项组成的数组  useEffect(effect,[]) ,让它在数组中的值发生变化的时候执行,数组中可以设置多个依赖项,其中的任意一项发生变化,effect 都会重新执行。示例如下:

    useEffect(
      () => {
        const subscription = props.source.subscribe();
        return () => {
          subscription.unsubscribe();
        };
      },
      [props.source],
    );
    

    需要注意的是:当依赖项是引用类型时,React 会对比当前渲染下的依赖项和上次渲染下的依赖项的内存地址是否一致,如果一致,effect 不会执行,只有当对比结果不一致时,effect 才会执行。示例如下:

    function Child(props) {
      
      useEffect(() => {
        console.log("useEffect");
      }, [props.data]);
      
      return <div>{props.data.x}</div>;
    }
    
    let b = { x: 1 };
    
    function Parent() {
      const [count, setCount] = useState(0);
      console.log("render");
      return (
        <div>
          <button
            onClick={() => {
              b.x = b.x + 1;
              setCount(count + 1);
            }}
          >
            Click me
          </button>
          <Child data={b} />
        </div>
      );
    }
    

    结果如下:


    上面实例中,组件 <Child/> 中的 useEffect 函数中的依赖项是一个对象,当点击按钮对象中的值发生变化,但是传入 <Child/>  组件的内存地址没有变化,所以 console.log("useEffect") 不会执行,useEffect 不会被打印。为了解决这个问题,我们可以使用对象中的属性作为依赖,而不是整个对象。把上面示例中组件 <Child/> 修改如下:

    function Child(props) {
      
      useEffect(() => {
        console.log("useEffect");
      }, [props.data.x]);
      
      return <div>{props.data.x}</div>;
    }
    

    修改后结果如下:


    可见 useEffect 函数中的 console.log("useEffect") 被执行,打印出 useEffect。

    • 当依赖项是一个空数组 [] 时 , effect 只在第一次渲染的时候执行。

    useEffect 的执行时机

    默认情况下,effect 在第一次渲染之后和每次更新之后都会执行,也可以是只有某些值发生变化之后执行,重点在于是每轮渲染结束后延迟调用( 异步执行 ),这是 useEffect 的好处,保证执行 effect 的时候,DOM 都已经更新完毕,不会阻碍 DOM 渲染,造成视觉阻塞。

    useEffect 和 useLayoutEffect 的区别

    useLayoutEffect 的使用方法和 useEffect 相同,区别是他们的执行时机。

    如上面所说,effect 的内容是会在渲染 DOM 之后执行,然而并非所有的操作都能被放在 effect 都延迟执行的,例如,在浏览器执行下一次绘制前,需要操作 DOM 改变页面样式,如果放在 useEffect 中执行,会出现闪屏问题。而 useLayoutEffect 是在浏览器执行绘制之前被同步执行,放在 useLayoutEffect 中就会避免这个问题。

    这篇文章中可以清楚的看到上述例子的具体实现:useEffect 和 useLayoutEffect 的区别

    对比 useEffect 和生命周期

    如果你熟悉生命周期函数,你可能会用生命周期的思路去类比思考 useEffect 的执行过程,但其实并不建议这么做,因为 useEffect 的心智模型和 componentDidMount 等其他生命周期是不同的。

    Function 组件中不存在生命周期,React 会根据我们当前的 props 和 state 同步 DOM ,每次渲染都会被固化,包括 state、props、side effects 以及写在 Function 组件中的所有函数。

    另外,大多数 useEffect 函数不需要同步执行,不会像 componentDidMountcomponentDidUpdate 那样阻塞浏览器更新屏幕。

    所以 useEffect 可以被看作是每一次渲染之后的一个独立的函数 ,可以接收 props 和 state ,并且接收的 props 和 state 是当次 render 的数据,是独立的 。相对于生命周期 componentDidMount 中的 this.state 始终指向最新数据, useEffect 中不一定是最新的数据,更像是渲染结果的一部分 —— 每个 useEffect 属于一次特定的渲染。对比示例如下:

    • 在 Function 组件中使用  useEffect  代码示例 (点击在线测试):

    function Counter() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        setTimeout(() => {
          console.log(`You clicked ${count} times`);
        }, 3000);
      });
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
            Click me
          </button>
        </div>
      );
    }
    

    结果如下:


    • 在 Class 组件中的使用生命周期,代码示例:

      componentDidUpdate() {
        setTimeout(() => {
          console.log(`You clicked ${this.state.count} times`);
        }, 3000);
      }
    

    结果如下:


    但是每次渲染之后都去执行 effect 并不高效。所以怎么解决呢 ?这就需要我们告诉 React 对比依赖来决定是否执行 effect

    如何准确绑定依赖

    effect 中用到了哪些外部变量,都需要如实告诉 React ,那如果没有正确设置依赖项会怎么样呢 ?示例如下 :


    上面例子中, useEffect 中用到的依赖项 count,却没有声明在卸载依赖项数组中,useEffect 不会再重新运行(只打印了一次 useEffect ), effectsetInterVal 拿的 count 始终是初始化的 0 ,它后面每一秒都会调用 setCount(0 + 1) ,得到的结果始终是 1 。下面有两种可以正确解决依赖的方法:

    1.在依赖项数组中包含所有在 effect 中用到的值

    effect 中用到的外部变量 count 如实添加到依赖项数组中,结果如下:


    可以看到依赖项数组是正确的,并且解决了上面的问题,但是也可以发现,随之带来的问题是:定时器会在每一次 count 改变后清除和重新设定,重复创建/销毁,这不是我们想要的结果。

    2.第二种方法是修改 effect 中的代码来减少依赖项

    即修改 effect 内部的代码让 useEffect 使得依赖更少,需要一些移除依赖常用的技巧,如:setCount 还有一种函数回调模式,你不需要关心当前值是什么,只要对 “旧的值” 进行修改即可,这样就不需要通过把 count 写到依赖项数组这种方式来告诉 React 了,因为 React 已经知道了。


    是否需要清除副作用

    若只是在 React 更新 DOM 之后运行一些额外的代码,比如发送网络请求,手动变更 DOM,记录日志,无需清除操作,因为执行之后就可以被忽略。

    需要清除的是指那些执行之后还有后续的操作,比如说监听鼠标的点击事件,为防止内存泄漏清除函数将在组件卸载之前调用,可以通过 useEffect 的返回值销毁通过 useEffect 注册的监听。

    清除函数执行时机是在新的渲染之后进行的,示例如下(点击在线测试):

    const Example = () => {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        console.log("useEffect");
        return () => {
          console.log("return");
        };
      }, [count]);
    
      return (
        <div>
          <p>You Click {count} times </p>
          {console.log("dom")}
          <button
            onClick={() => {
              setCount(count + 1);
            }}
          >
            Click me
          </button>
        </div>
      );
    };
    
    

    结果如下:


    需要注意的是useEffect 的清除函数在每次重新渲染时都会执行,而不是只在卸载组件的时候执行 。

    参考文档

    React Core Team 成员、Readux 作者 Dan 对 useEffect 的完全解读  ---  A Complete Guide to useEffect

    1. JavaScript 重温系列(22篇全)

    2. ECMAScript 重温系列(10篇全)

    3. JavaScript设计模式 重温系列(9篇全)

    4. 正则 / 框架 / 算法等 重温系列(16篇全)

    5. Webpack4 入门(上)|| Webpack4 入门(下)

    6. MobX 入门(上) ||  MobX 入门(下)

    7. 120+篇原创系列汇总

    回复“加群”与大佬们一起交流学习~

    点击“阅读原文”查看 120+ 篇原创文章

    展开全文
  • 浅谈useEffect

    2021-12-31 18:28:13
    useEffect接收一个函数,可以让用户在函数组件中执行副作用操作,如: 设置订阅和事件处理 ajax请求等异步操作 更改DOM对象及其他会对外部产生影响的操作等 使用方式 useEffect(create[, deps]); 第一个参数是要...

    作用

    useEffect接收一个函数,可以让用户在函数组件中执行副作用操作,如:

    1. 设置订阅和事件处理
    2. ajax请求等异步操作
    3. 更改DOM对象及其他会对外部产生影响的操作等

    使用方式

    useEffect(create[, deps]);
    

    第一个参数是要执行的 effect,而第二个参数是依赖项,依赖项是选填的。

    例如

    function App() {
      useEffect(() => {
        document.title = 'example'; // 副作用操作
      });
      return <div />;
    }
    

    执行时机

    传递给useEffect的函数(effect)会在浏览器完成布局与绘制之后延迟(异步)执行,这里的异步实现优先级如下:setImmediate > MessageChannel > setTimeout,并且 React 会保证每次运行 effect 的时候 DOM 都已经更新完毕。虽然 useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行 ,这是官网上的一句描述,很不起眼的一句话,甚至不知道怎么理解这句话,我一开始也这样,直到后面看到这样一个例子

    import "./styles.css";
    import { useState, useEffect } from "react";
    
    export default function App() {
      const [a, setA] = useState("a");
      const [b, setB] = useState("b");
    
      console.log("[render]", a, b);
    
      useEffect(() => {
        console.log("[useEffect]", a, b);
      });
    
      function handleClickWithPromise() {
        Promise.resolve().then(() => {
          console.log("async handler1", a, b);
          setA("aa");
          console.log("async handler2", a, b);
          setB("bb");
          console.log("async handler3", a, b);
        });
      }
    
      function handleClick() {
        console.log("sync handler1", a, b);
        setA("aaa");
        console.log("sync handler2", a, b);
        setB("bbb");
        console.log("sync handler3", a, b);
      }
    
      return (
        <div className="App">
          <button onClick={handleClickWithPromise}>
            {a} - {b} with Promise
          </button>
          <button onClick={handleClick}>
            {a} - {b} without Promise
          </button>
        </div>
      );
    }
    

    这个例子不仅关乎到 useEffect 的执行时机,还涉及到 setState 的执行方式。简单的说 setState 的执行会触发组件的重新渲染,即函数的重新执行。setState 本身是同步执行的,但是在 由 React 控制的 事件处理函数,以及生命周期函数(类组件)调用 setState 时会将多个 setState 进行合并然后延迟执行,在 React控制之外的 如 setTimeout/setInterval、Promise等里面执行 setState 则不会合并处理,表现为同步执行。所以上述例子当我们点击 {a} - {b} with Promise 在 Promise 中调用 setState 时,会立即同步执行重渲染,再来看官网这句话,便明白为什么会看到这样的打印结果。

    有条件的执行

    默认情况下,effect 会在每轮组件渲染完成后执行,但有些时候我们不想要这样,可能只是想挂载完后设置订阅,或者某个数据改变后才执行effect,以此来做一些优化或者避免 bug 的发生。此时我们可以给 useEffect 传递第二个参数,它是 effect 所依赖的值的数组,当设置了第二个参数 deps 后,effect只会在所依赖的值发生变化时(使用 Object.is 进行比较)才运行。

    需要清除的effect

    有一些副作用是需要清除的,比如我们绑定的事件在组件卸载的时候需要解绑等,不然可能会导致一些意料之外的错误或者内存溢出,在类组件中通常会在 comonponentWillUnmount(vue则为beforeDestory )中清除副作用,而在useEffect中,我们可以使 effect 返回一个函数,在该函数中清除副作用,React将会在执行清除操作(组件卸载的时候)时调用该函数,我们称之为清理函数。例如:

    function App() {
      useEffect(() => {
        const handleScroll = () => {};
        window.addEventListener('scroll', handleScroll);
        return () => window.removeEventListener('scroll', handleScroll);
      });
     	return <div />
    }
    
    • 首次渲染组件清理函数不会运行
    • 清理函数的运行时间点是每次运行副作用函数之前
    • 组件被销毁时一定会运行清理函数

    在React v17.0之前 useEffect 的清理函数是同步运行的,在React v17.0中清理函数更新为异步运行 —— React v 17.0

    看如下代码,首次进入和点击 increase 1 分别打印什么?顺序是怎么样的

    export default function App() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        console.log(`count is ${count} effect`);
        return () => console.log(`clear count ${count} effect`);
     	}, [count]);
      console.log(`count is ${count} render`);
      return (
        <div>
         	<div>{count}</div>
          <button onClick={() => setCount(count + 1)}>increate 1</button>
        </div>
     	);
    }
    

    类比生命周期

    如果你熟悉 React Class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。 —— 使用 Effect Hook。如下面的类组件例子

    class App extends React.Component {
    	componentDidMount() {
        const { id } = this.props;
        fetchData(id);
        subsribe(id);
        window.addEventListener(...);
        ...
      }
      componentDidUpdate() {
        const { id } = this.props;
        fetchData(id);
        subscribe(id);
        ...
      }
      componentWillUnmount() {
        removeSubscribe(this.props.id);
        window.removeEventListener(...);
        ...         
      }
      ...
    }
    

    可以看到,我们在 componentDidMount 和 componentDidUpdate 中书写了相同的代码,这在我们日常开发中是非常常见的,因为我们希望在组件挂载和更新的时候做一些同样的操作,比如重新获取数据,亦或是在组件卸载的时候清除副作用,当我们有很多类似的操作的时候,不仅会书写很多重复的代码,而且相关联的代码分散在不同的生命周期函数中,当代码量多且复杂的时候就会变得不好管理。而改用 useEffect Hooks 的话会变成怎么样呢?

    function App(props) {
      const { id } = props;
      useEffect(() => {
        fetchData(id);
      }, [id]);
      
      useEffect(() => {
        subscribe(id);
        return () => removeSubscribe(id);
      }, [id]);
      
      useEffect(() => {
        window.addEventListener(...);
        return () => window.removeEventListener(...);
      }, [...]);
          
      ...
    }
    

    基于 useEffect 的这种设计,我们不用再去考虑当前的 effect 是“挂载”还是“更新”,可以很好的实现 关注点分离 ,还可以在 effect 中返回一个函数,函数里面清除该 effect 中存在的副作用影响,相关代码都汇聚到了一块。当代码量和复杂度提高的时候甚至可以提取成自定义Hooks进行使用。

    模拟componentDidMount

    useEffect(() => {
      console.log('模拟componentDidMount')
    }, [])
    

    模拟componentDidUpdate

    const isUpdated = useRef(false);
    useEffect(() => {
      if (!isUpdated.current) {
        isUpdated.current = true;
     } else {
        // 这里编写componentDidUpdate相关代码
        console.log('模拟componentDidUpdate')
     }
    })
    

    事实上,useEffect 并不完全等价于 componentDidMount 和 componentDidUpdate(如运行时机),前者是在渲染器执行完当前任务后(即在浏览器将所有变化渲染到屏幕后) 才会被 异步执行,而后者是 渲染器执行当前渲染界面任务同步执行。这样做的好处是什么呢?让我们对比下面两个程序,相信你会找到答案

    export default class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0,
       	};
     	}
    
      componentDidMount() {
        console.log('componentDidMount start', new Date());
        new Array(500000000).fill(true).forEach(() => {});
        console.log('componentDidMount end', new Date());
     	}
    
      componentDidUpdate() {
        console.log('componentDidUpdate start', new Date());
        new Array(500000000).fill(true).forEach(() => {});
        console.log('componentDidUpdate end', new Date());
     	}
    
      render() {
        console.log('render', new Date());
        const { count } = this.state;
        return <div>
          <h4>synchronize {Math.random()}</h4>
          <button onClick={() => this.setState({ count: count + 1 })}>increate</button>
          <br />
         {count}
          <br />
          <button onClick={() => this.setState({ count: count - 1 })}>decrease</button>
        </div>;
     	}
    }
    

    可以看到上述例子,在点击按钮后很长时间都处于卡顿状态,因为 componentDidMoun 和 componentDidUpdate 都是同步执行到底,当里面有一些高消耗的操作时,长时间的运行会让页面渲染器一直处在等待中,页面迟迟得不到更新,而换成useEffect呢?

    export default function App() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        console.log('useEffect start', new Date());
        new Array(50000000).fill(true).forEach(() => {});
        console.log('useEffect end', new Date());
     	});
    
      return (
        <div>
          <h4>synchronize {Math.random()}</h4>
          <button onClick={() => setCount(count + 1)}>increate</button>
          <br />
         {count}
          <br />
          <button onClick={() => setCount(count - 1)}>decrease</button>
        </div>
     	);
    }
    

    通过这个例子,能很好的解释为什么 useEffect 是在将变化都渲染到屏幕后才异步运行,很重要的一个原因就是避免 effect 的执行阻塞UI渲染,让页面看起来响应更快。除此之外也是为了保证每次运行 effect 的同时,DOM 都已经更新完毕。

    useEffect 的兄弟

    useEffect 有一个兄弟叫 useLayoutEffect ,他和 useEffect 几乎完全相同,不同的就是 effect 的执行时机,useLayoutEffect 的 effect 和清理函数均是同步调用,并且与 componentDidMount 和 componentDidUpdate 的调用阶段一致,使其可以等价于componentDidMount 和 componentDidUpdate 。这在一些用户可见DOM变更的场景下会比较适用,React 保证其会在浏览器执行下一次渲染之前被同步执行,保证用户视觉上的一致 —— useLayoutEffect

    "监听"数据变化?

    这是很多 vue 转 React 的同学在刚接触 useEffect 时常有的一种理解方式,useEffect 的表现就像是 vue 中的监听属性 watch ,可以用来 “监听” 数据的变化。例如这个模拟百度实时搜索的例子,在输入框的值发生变化之后重新请求数据:

    vue watch

    <template>
      <div>
        <input type="text" :value="inputVal" @input="handleOnInput" />
        <p>result: {{ result }}</p>
      </div>
    </template>
    <script>
    let timer = null;
    function _debounce(fn, delay) {
      clearTimeout(timer);
      timer = setTimeout(fn, delay);
    }
    export default {
      data() {
        return {
          inputVal: "",
          result: "",
        };
      },
      watch: {
        inputVal: function (cur) {
          _debounce(
            () => this.fetchData(cur).then((res) => (this.result = res)),
            1000
          );
        },
      },
      methods: {
        handleOnInput(e) {
          this.inputVal = e.target.value;
        },
        fetchData(val) {
          return new Promise((resolve) => {
            setTimeout(() => {
              resolve(val.repeat(3));
            }, 1000);
          });
        },
      },
    };
    </script>
    

    useEffect Hooks

    const fetchData = val => new Promise(resolve => {
      setTimeout(() => {
        resolve(val.repeat(3));
     }, 1000);
    });
    
    export default function App() {
      const [inputVal, setInputVal] = useState('');
      const [result, setResult] = useState('');
    
      const handleOnChange = e => setInputVal(e.target.value);
    
      useEffect(() => {
        const timer = inputVal && setTimeout(() => 
          fetchData(inputVal).then(res => setResult(res)),
        1000);
        return () => clearTimeout(timer);
     }, [inputVal]);
    
      return (
        <div>
          <input
            type="text"
            value={inputVal}
            onChange={handleOnChange}
          />
          <p>result: {result}</p>
        </div>
     );
    }
    

    这里的“监听”并不是像 vue 中那样做了数据劫持或者代理结合发布订阅(观察者)模式,事实上 React 也并没有做任何的监听操作,上面讲有条件的执行的时候其实已经提到了,React 做的只是把所依赖的数据的旧值和新值进行比较,发生了变化便重新运行 effect。

    那依赖项究竟有什么作用?我能不能忽略它?

    有一天产品说要在页面上加一个计时器,在进入页面后从0开始每秒自增1,先让我们看看类组件是如何实现的

    import React from "react";
    export default class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    
      componentDidMount() {
        // 在组件挂载后开启一个定时器
        this.timer = setInterval(() => {
          this.setState({
            count: this.state.count + 1
          });
        }, 1000);
      }
    
      componentWillUnmount() {
        // 组件卸载前清除定时器
        clearInterval(this.timer);
      }
      render() {
        return <div>{this.state.count}</div>;
      }
    }
    

    但是你刚学了 Hooks,想要尝试用 useEffect 去实现,结合上面讲到的 useEffect 模拟 componentDidMount ,很容易的就把把这个定时器例子用 React Hooks 重构了出来,如下:

    export default function App() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        const timer = setInterval(() => {
          console.log(count);
          setCount(count + 1);
       }, 1000);
        return () => clearInterval(timer);
     }, []);
      
      return <div>{count}</div>;
    }
    

    在这个定时器例子中,一部分同学的想法是在组件挂载完后开启一个定时器,那么就意味着我这个 effect 只需执行一次就行,那么我的依赖就是一个空数组。如果是使用类组件实现,乍一想这个逻辑似乎并没有什么问题,但是 useEffect 这里存在一个陷阱,而如果你的心智模型是“只有当我想重新触发 effect 的时候才需要去设置依赖”,所以该程序我不需要设置依赖,那么就落入了这个陷阱,让我们运行这个程序看看最后结果如何

    可以看到,该程序并没有像我们预想的那样执行,而是卡在了数字1,并且每次打印的 count 都是 0,这是为什么呢?我不是更新了 count 了吗?为什么我会在我的函数中看到旧的 state ? 这些都是因为你欺骗 React,不告诉 React 依赖或者告诉了错误的依赖。在组件首次渲染的时候,我们定义一个 常量count 以及设置了一个 effect 只在首次渲染完成后执行。这里的 effect 本质就是函数,是函数组件 App(函数)运行的产物,它抓住了 App 首次运行的作用域链不释放,尽管 effect 里面重新赋值了 count 使得 App 重新执行,尽管产生了新的 state 和 effect,但是因为依赖项是空的,React 并不会重新执行 effect,所以 setInterval 里面访问的 count 始终是第一次渲染时的 count 值 0。有没有觉得这现象似曾相识?没错,是闭包在作祟。那既然是因为我们没有告诉React正确的依赖,那如果我们诚实的告诉 React 我们在 effect 中用到了哪些值呢?

    export default function App() {
      const [count, setCount] = useState(0);
    
      // useEffect(() => {
      //   const timerId = setInterval(() => {
      //     setCount(count + 1);
      //   }, 1000);
      //   return () => clearInterval(timerId);
      // }, [count]);
    
      useEffect(() => {
        const timerId = setTimeout(() => {
          setCount(count + 1);
        }, 1000);
        return () => clearTimeout(timerId);
      }, [count]);
    
      return <div>{count}</div>;
    }
    

    一切都正常了,现在,每次 count 的修改都会重新运行 effect ,并且定时器中的 setCount(count + 1) 会获取到最新的 count 值。这可以解决问题,但是并不完美,并且只是诚实的告诉 React 依赖有可能并不会使得程序如你所想的运行,这又是怎么回事?别着急,让我们继续看下面的例子加深对 useEffect 中闭包的印象。在进入页面后5秒内连续点击5次 Click me ,分别打印什么?

    function Comp1() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        setTimeout(() => {
          console.log(count);
       }, 5000);
     }, []);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
           Click me
          </button>
        </div>
     );
    }
    
    
    function Comp2() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        setTimeout(() => {
          console.log(count);
       }, 5000);
     }, [count]);
    
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>
           Click me
          </button>
        </div>
     );
    }
    

    Comp1 打印了一个 0 ,Comp2 打印 0 1 2 3 4 5

    没有答对的同学不要失望,虽然闭包的确可以访问外围的变量,但是,每次渲染的 effect 都是不同的,组件的每一次渲染都是相互独立的(函数组件的每一次渲染本质就是函数的执行),每次渲染都有固定不变的 props、state、事件处理函数以及 effects。它们会"捕获"定义它们的那次渲染中的 props 和 state 。这并不难理解,传递给 useEffect 的函数是一个局部变量,每次 App 被执行时,函数都会重新创建,再根据依赖是否发生了变化决定是否需要执行新的effect,所以 Comp1 它首先只会执行一次 effect,其次因为依赖为空数组,所以捕获的是首次渲染时的 count 0,故只会打印一个0;而 Comp2 的依赖是 count,意味着 count 的每一次改变都会重新运行 effect,即会重新开启一个定时器,首先可以确认必然会打印 6 个数字,其次React并不会保存 effect 函数,每次运行的 effect 都是该次渲染新生成的,这可以确保 effect 中可以获取到最新的状态,故会打印 0 1 2 3 4 5。某种意义上讲,effect 就是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。

    从上述的两个例子可以得知:依赖项的作用是告诉 React 在 effect 中用到了哪些值,决定本次渲染是否需要执行 effect,并且不能忽略他们,忽略依赖项可能会导致一些意想不到的 bug。

    移除依赖

    但有些时候依赖会频繁发生变化,变得让人头疼,比如上一个倒计时的例子,我们的定时器会在每一次 count 改变后清除和重新设置,这可能不是我们想要的结果;又比如将对象或函数作为依赖项,这时不将这些数据写进依赖项里可能会导致错误,eslint 也会警告,写进依赖项里又会频繁发生变化,导致 effect 多次执行,那在日常开发中有哪些常见的减少依赖或移除依赖的技巧呢?

    使用函数更新状态,如刚才的计时器例子

    export default function App() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const timer = setInterval(() => {
          console.log('setInterval');
          setCount(prevCount => prevCount + 1);
       }, 1000);
        return () => {
          console.log('clearInterval');
          clearInterval(timer);
       };
     }, []);
    
      return <div>{count}</div>;
    }
    

    这时我们已经掌握了第一个移除依赖的技巧,但是隔天产品说计时器自增的数是随机的,需要通过接口获取,如下:

    export default function App() {
      const [count, setCount] = useState(0);
    
      const fetchStep = () => Promise.resolve(Math.ceil(Math.random() * 10));
    
      useEffect(() => {
        let timerId;
        (async () => {
         	const step = await fetchStep();
          timerId = setInterval(() => {
            setCount((prevCount) => prevCount + step);
          }, 1000);
         })();
        return () => clearInterval(timerId);
      }, []);
    
      return <div>{count}</div>;
    }
    

    上面的代码可以正常运行,但是会有 eslint 警告,我们必须把 fetchStep 作为 useEffect 的依赖,当我们将 fetchStep 添加进 useEffect 的依赖后,eslint 又会提示我们,为了避免依赖频繁发生变化,导致 effect 重复执行/无限执行,你也可以把它包装成 useCallback Hook (向子组件传递的函数必须要用 useCallback Hook 包装)这就确保了它不会随渲染而改变,除非它自身的依赖发生了改变。或者如果某些函数仅在 effect 中调用,可以把它们的定义移到 effect 中。经过观察我们可以发现,fetchStep 仅在 useEffect 中使用到了,所以我们完全可以把 fetchStep 函数的定义移到 useEffect 内。

    export default function App() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const fetchStep = () => Promise.resolve(Math.ceil(Math.random() * 10));
        let timerId;
        (async () => {
         	const step = await fetchStep();
          timerId = setInterval(() => {
            setCount((prevCount) => prevCount + step);
          }, 1000);
         })();
        return () => clearInterval(timerId);
      }, []);
    
      return <div>{count}</div>;
    }
    

    这样便消除了依赖。有时候你可能不想把函数移入 effect 里,又或许是组件内有其他地方使用了相同的函数,你不想在每个 effect 里复制黏贴一遍这个逻辑。这时我们可以仔细看看这个函数的实现,如果一个函数没有使用组件内的任何值,你可以尝试把那个函数移动到你的组件之外,那样一来,这个函数就肯定不会依赖任何 props 或 state,并且也不用出现在依赖列表中了。刚好我们这个例子就满足这个场景,所以可以改造为:

    const fetchStep = () => Promise.resolve(Math.ceil(Math.random() * 10));
    
    export default function App() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        let timerId;
        (async () => {
         	const step = await fetchStep();
          timerId = setInterval(() => {
            setCount((prevCount) => prevCount + step);
          }, 1000);
         })();
        return () => clearInterval(timerId);
      }, []);
    
      return <div>{count}</div>;
    }
    

    所以一般建议把不依赖 props 和 state 的函数提到你的组件外面,并且把那些仅被 effect 使用的函数放到 effect 里面,万不得已的情况下才将函数包装成 useCallback Hook。隔天产品又说希望限制随机 step 的最大值,并且用户可以修改这个最大值,虽然你很不情愿,但是你还是咬着牙对程序进行了修改,如下:

    function App() {
      const [count, setCount] = useState(0);
      const [max, setMax] = useState(10);
      
    	const fetchStep = useCallback(() => {
        return Promise.resolve(Math.ceil(Math.random() * max));
     	}, [max]);
      
      useEffect(() => {
        let timerId;
        (async () => {
         	const step = await fetchStep();
          timerId = setInterval(() => {
            setCount((prevCount) => prevCount + step);
          }, 1000);
         })();
        return () => clearInterval(timerId);
     	}, [fetchStep]);
    
      return (
      	<div>
          <div>
            max:
            <input type="number" value={max} onChange={e => setMax(e.target.value)} />
          </div>
        	<div>count: {count}</div>
        </div>
      );
    }
    

    此时我们就无法将 fetchStep 函数移除到组件外部了,但是我们还是可以将函数移入 useEffect 内,这时可能有同学会有疑问?为什么呢?移入 useEffect 内好像也无法消除依赖啊!没错,有时候这种方法做不到完全消除依赖,但是这样做的好处是我们不再需要去考虑这些“间接依赖”;可以看到,useEffect 依赖了 fetchStep,fetchStep 依赖了max,当代码复杂到一定程度的时候,我们经常会因为是哪个依赖的改变导致的 effect 重复运行而苦恼,程序变得难以维护。当我们将 fetchStep 移入 useEffect 后可以很清晰的看到依赖的数据,只有max一个。

    function App() {
      const [count, setCount] = useState(0);
      const [max, setMax] = useState(10);
      useEffect(() => {
        let timerId;
        const fetchStep = () => Promise.resolve(Math.ceil(Math.random() * max));
        (async () => {
         	const step = await fetchStep();
          timerId = setInterval(() => {
            setCount((prevCount) => prevCount + step);
          }, 1000);
         })();
        return () => clearInterval(timerId);
     	}, [max]);
      return (
      	<div>
          <div>
            max:
            <input type="number" value={max} onChange={e => setMax(e.target.value)} />
          </div>
        	<div>count: {count}</div>
        </div>
      );
    }
    

    到这里似乎就结束了,你愉快的提交了代码,过了一阵子产品过来找你说程序有问题,计时器在 max 修改的时候并没有按1秒钟的时间增长,你很疑惑,我写的程序怎么会有问题呢?然后你又仔细看了下实现,发现了端倪,max 的改变会导致定时器重新开启,从而导致定时器没有按1秒钟的时间增长,比如过了0.9秒后,max 改变,旧的定时器被清除,新的定时器开启,重新计时,到下一次 count 改变时,历时最少1.9秒,而且理论上我可以重复这样,致使 count 长时间不会变化。你心想这不是很正常吗?你开始跟产品 battle,说其中的原因,但是产品不听并给你提了个缺陷,你很困惑,既然是 max 依赖搞的鬼,那我怎么消除这个依赖呢?

    本质上我们只需要将count的更新逻辑移到effect外部即可,其实官网已经给出了答案 ,useState 有一个强大的替代方案 useReducer Hook(事实上 useState 是一个简易版的 useReducer ) ,useReducer 接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。需要注意的是React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。单看这个定义好像不太能理解它是如何解决依赖频繁变化的问题,让我们直接往下看

    import React, { useReducer, useEffect } from "react";
    
    const initialState = {
      count: 0,
      max: 10,
      step: 1
    };
    
    const ACTION_TYPES = {
      UPDATE_MAX: "updateMax",
      UPDATE_COUNT: "updateCount",
      UPDATE_STEP: "updateStep"
    };
    
    const fetchStep = (max) => Promise.resolve(Math.ceil(Math.random() * max));
    
    function reducer(state, action) {
      const { type, payload } = action;
      const { count, step } = state;
      switch (type) {
        case ACTION_TYPES.UPDATE_COUNT:
          return {
            ...state,
            count: count + step
          };
        case ACTION_TYPES.UPDATE_MAX:
          return {
            ...state,
            max: payload
          };
        case ACTION_TYPES.UPDATE_STEP:
          return {
            ...state,
            step: payload
          };
        default:
          throw new Error("type is required");
      }
    }
    
    export default function App() {
      const [state, dispatch] = useReducer(reducer, initialState);
      const { count, max, step } = state;
    
      const handleInputChange = (e) => {
        const value = e.target.value;
        dispatch({
          type: ACTION_TYPES.UPDATE_MAX,
          payload: value
        });
        fetchStep(max).then((res) => {
          dispatch({
            type: ACTION_TYPES.UPDATE_STEP,
            payload: res
          });
        });
      };
    
      useEffect(() => {
        const timerId = setInterval(() => {
          dispatch({ type: ACTION_TYPES.UPDATE_COUNT });
        }, 1000);
        return () => clearInterval(timerId);
      }, []);
    
      return (
        <div>
          <div>
            max:
            <input type="number" value={max} onChange={handleInputChange} />
          </div>
          <div>step: {step}</div>
          <div>count: {count}</div>
        </div>
      );
    }
    

    故在一些更加复杂的场景中(比如一个 state 依赖于另一个 state 或 props),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外,它可以把更新逻辑和描述发生了什么分开。

    最后,在万不得已的情况下,如果你想要类似 class 中的 this 的功能,你可以 使用一个 ref 来保存一个可变的变量。然后你就可以对它进行读写了。但是要注意的是给 ref 重新赋值不会导致组件重新渲染。

    结论

    1. 使用函数更新状态
    2. 如果某些函数仅在 effect 中调用,把它们的定义移到 effect 中,虽然有时候做不到完全消除依赖,但是这样做的好处是我们不再需要去考虑这些“间接依赖”
    3. 有时候你可能不想把函数移入 effect 里。比如,组件内有几个 effect 使用了相同的函数,你不想在每个 effect 里复制黏贴一遍这个逻辑,也或许这个函数是一个 prop。
      1. 如果一个函数没有使用组件内的任何值,你可以尝试把那个函数移动到你的组件之外
      2. 或者, 将函数作为 useEffect 依赖,为了避免依赖频繁发生变化,导致 effect 重复执行/无限执行,把它包装成 useCallback Hook (向子组件传递的函数必须要用 useCallback Hook包装)
    4. 在一些更加复杂的场景中(比如一个 state 依赖于另一个 state),尝试用 useReducer Hook 把 state 更新逻辑移到 effect 之外
    5. 万不得已的情况下,可以 使用一个 ref 来保存一个可变的变量

    扩展

    其实最后我们用 useReducer 完成的程序其实并不完美,其中一个是 change 事件的每次触发都会发出请求,这个可以通过防抖或节流来进行优化。再一个是在真实的网络环境当中step的更新可能存在问题,在 max 的连续变化中,例如上一次的请求耗时3秒,最新一次的请求耗时2秒,那么最新的数据就会先于旧数据到达,就会导致 step 其实是根据上一个 max 得到的,这种现象我们称之为竟态。

    通常在类组件中我们可以在请求到达的时候通过比对现在的值和发起请求时的值是否一致来解决,如下

    class App extends React.Component {
      ...
      getData() {
    	const max = this.state.max;
    	fetchStep(max).then((res) => {
    	  if (max === this.state.max) {
    		this.setState({
    		  step: res,
    		});
    	  }
    	});
      }
      ...
    }
    

    我们回看最后完成的程序会发现怎么都不好解决,写起来很麻烦,因为必包的关系,我们不能像类组件那样在 handleInputChange 中获取到最新的 max ,要解决这个问题,我们必须使用一个 Ref 来保存最新的 max

    const lastestMax = useRef(max);
    
    const handleInputChange = (e) => {
      const value = e.target.value;
      dispatch({
        type: ACTION_TYPES.UPDATE_MAX,
        payload: value
      });
      lastestMax.current = value;
      fetchStep(value).then((res) => {
        if (value === lastestMax.current) {
          dispatch({
            type: ACTION_TYPES.UPDATE_STEP,
            payload: res
          });
        }
      });
    };
    

    除此之外我们可以将 handleInputChange 中的数据请求部分放在 effect 中请求数据,在 useEffect 中我们可以轻松的解决竞态问题

    useEffect(() => {
      let didCancel = false;
      fetchStep(max).then((res) => {
        if (!didCancel) {
          dispatch({
            type: ACTION_TYPES.UPDATE_STEP,
            payload: res
          });
        }
      });
      return () => {
        didCancel = true;
      };
    }, [max]);
    

    参考:

    React官方文档

    useEffect完整指南

    How to fetch data with React Hooks

    细烤useEffect

    被提前执行的useEffect

    React的useEffect和useLayoutEffect执行机制剖析

    展开全文
  • 生命周期与useEffect

    2021-04-23 14:48:31
    Hooks提供了一套新的阐述组件生命周期的方式,原先许多生命周期的API都有了对应的Hooks解决方案----使用强大的useEffect这一 Hooks函数。 二、组件生命周期过程及API详解 首先我先放一张图,详细列述了 React 组件...
  • useEffect使用规范

    千次阅读 2020-09-07 14:10:48
    使用 Effect Hook Hook 是 React 16.8 的新增特性。...import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // Similar to component
  • react-demo:上下文和useEffect
  • useEffect 原理

    2021-10-21 19:58:08
    0 ReactDOM.render(, document.getElementById('root')) } let prevDepsAry = [] //因为useEffect可以被调用多次,所以使用数组 prevDepsAry = [[name], [count]] let effectIndex = 0 // 每次组件重新渲染...
  • 最近在公司搬砖的过程中遇到了一个bug,页面加载的时候会闪现一下,找了很久才发现是useeffect的依赖项的问题,所以打算写篇文章总结一下,希望对看到文章的你也有所帮助。1.什么是use...
  • 练习基本的React JS概念,例如useState,useRef和useEffect。 直播: : 挑战性 我对诸如useRef和useEffect之类的概念的掌握程度正在提高。 在待办事项清单上进行小规模练习有助于巩固核心原则。 这可能是更复杂的...
  • React useEffect的cleanup 可以避免应用程序出现内存泄漏等问题 可以优化应用程序的性能 开始这篇文章前,您应该对什么useEffect是有一个基本的了解,包括可以使用它来获取数据。 本文将解释useEffectHook...
  • 如何解决 React.useEffect() 的无限循环

    千次阅读 2021-04-02 08:12:22
    无限循环的另一种常见方法是使用对象作为useEffect()的依赖项,并在副作用中更新该对象(有效地创建一个新对象) useEffect(() => { // Infinite loop! setObject({ ...object, prop: 'newValue' }) }, [object]); ...
  • 自定义钩子来调试useEffect
  • 引言 Hooks 是 React 16.8 的新增特性,至今经历两年的时间,它可以让你在不编写 class 组件的情况下使用 state 以及其他 React 特性。useEffect 是...
  • (1)useEffect 和 useLayoutEffect的区别 useEffect 在全部渲染完毕后才会执行 useLayoutEffect 会在浏览器 layout 之后,painting 之前执行 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 ...
  • 点击上方前端瓶子君,关注公众号回复算法,加入前端编程面试算法每日一题群最近在公司搬砖的过程中遇到了一个bug,页面加载的时候会闪现一下,找了很久才发现是useeffect的依赖项的问题,...
  • useEffect 完整指南

    千次阅读 2019-11-27 10:03:07
    你用Hooks写了一些组件,甚或写了一个小型应用。你可能很满意,使用它的API很...但有时候当你使用useEffect你总觉得哪儿有点不对劲。你会嘀咕你可能遗漏了什么。它看起来像class的生命周期…但真的是这样吗?你发觉...
  • React Hooks阐述useEffect 解绑副作用ReactHooksDemo\demo01\src\Example.jsuseEffect的第二个参数总结 阐述 在写React应用的时候,在组件中经常用到 componentWillUnmount 生命周期函数(组件将要被卸载时执行)。...
  • useEffect 依赖

    2021-11-26 19:19:49
    第二个参数是空,挂载和更新都渲染。当是[],挂载渲染。 当[数据] 当依赖是基础数据类型时,挂载和更新渲染 当依赖是引用类型,数组和对象时,会一直渲染 ...import React, {useState, useRef,useEffect}

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,129
精华内容 5,251
关键字:

useeffect