react前端知识点

React effect hooks各种使用场景

2020-09-20  本文已影响0人  初漾流影

react rendering原理

举个栗子

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

以上程序,react是这样渲染的。

  //first render
  function Counter() {
      const count = 0;
      
      function handleAlertClick() {
        setTimeout(() => {
          alert('You clicked on: ' + count);
        }, 3000);
      }
      // ...
      <p>You clicked {count} times</p>
      
  }
  
  //second render
  function Counter() {
      const count = 1;
      
      function handleAlertClick() {
        setTimeout(() => {
          alert('You clicked on: ' + count);
        }, 3000);
      }
      // ...
      <p>You clicked {count} times</p>
      
  }

每一次渲染中,count的值是固定的,在每一次更新状态引起的渲染中,count值独立于其他的渲染。

划重点:每一次渲染都有它自己的Props and State, 在单次渲染中,props和state始终不变。

如果点击counter增加到3,点击show alert,点击增加 counter到5并且在定时器回调触发前完成,alert弹出count的值是3.

因为在事件处理函数属于某一次特定的渲染,在任意一次渲染中,props和state是始终保持不变的,而且用到他们的任何值也是独立的(包括事件处理函数)。

每次渲染都有它自己的Effects

举个栗子

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>
  );
}

上面提到每次渲染都有独立的props,state和事件处理函数,同样,effect也是独立的,每次渲染都是一个不同的effect函数。

做一道题

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>
  );
}

如果连续点击2次button,输出结果是?
因为状态每次变更,都会有一次新的渲染,每次渲染都有一个独立的effect函数,props也是特定的。所以结果是

   You clicked 0 times
   You clicked 1 times
   You clicked 2 times

逆潮而动

什么是逆潮而动?
effect的回调函数里每次读取的是具体每次渲染时机,props或者state的值,如果我们想要在effect回调中读取最新的值,而不是捕获的值。这种做法就叫逆潮而动。
简单来说就是从过去渲染中的函数里读取未来的props和state。

可以通过useRef来实现

function Example() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    // Set the mutable latest value
    latestCount.current = count;
    setTimeout(() => {
      // Read the mutable latest value
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });

以上代码每次获取的都是最新值。
useRef hooks返回一个可变的 ref 对象,其 .current 属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。

需要注意的是,内容更改时useRef不会通知。.current属性更改也不会导致重新渲染。

以上代码,在页面重新渲染,effect函数执行时,latestCount.current中的始终是最新值。

Effect是如何清理副作用的?

举个例子

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

假设第一次渲染的时候props是{id: 10},第二次渲染的时候是{id: 20}
那第一次的订阅是在什么时候被清除的?
上一次的effect会在重新渲染后被清除。

过程如下:

React 渲染{id: 20}的UI。
浏览器绘制。我们在屏幕上看到{id: 20}的UI。
React 清除{id: 10}的effect。
React 运行{id: 20}的effect。

有个问题:如果清除上一次的effect发生在props变成{id: 20}之后,那它为什么还能“看到”旧的{id: 10}?

这里用到我们以上说的一个知识点:
组件内的每一个函数(包括事件处理函数,effects,定时器或者API调用等等)会捕获定义它们的那次渲染中的props和state。

React运行effects的机制
React只会在浏览器绘制后运行effects。这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。Effect的清除同样被延迟了。

从一个定时器例子开始

例子:

 function Counter() {
  const [count, setCount] = useState(0);

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

  return <h1>{count}</h1>;
}

上面的程序运行结果:定时器只会递增一次。
原因是设置了[ ]依赖,effect不会再重新运行,它后面每一秒都会调用setCount(0 + 1)

过程如下:

// First render, state is 0
function Counter() {
  // ...
  useEffect(
    // Effect from first render
    () => {
      const id = setInterval(() => {
        setCount(0 + 1); // Always setCount(1)
      }, 1000);
      return () => clearInterval(id);
    },
    [] // Never re-runs
  );
  // ...
}

// Every next render, state is 1
function Counter() {
  // ...
  useEffect(
    // This effect is always ignored because
    // we lied to React about empty deps.
    () => {
      const id = setInterval(() => {
        setCount(1 + 1);
      }, 1000);
      return () => clearInterval(id);
    },
    []
  );
  // ...
}

若要实现每秒数字加一,有以下两种方式:

第一种:通过以下方式可以实现定时器更新

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

这种方式能解决问题,但是我们的定时器会在每一次count改变后清除和重新设定。

第二种:通过以下去除依赖的方式也可以实现定时器更新

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

重点:当我们想要根据前一个状态更新状态的时候,我们可以使用setState的函数形式。

因为我们在effect中写了setCount(count + 1),所以count是一个必需的依赖。但是,我们真正想要的是把count转换为count+1,然后返回给React。可是React其实已经知道当前的count。我们需要告知React的仅仅是去递增状态,不管它现在具体是什么值。

这正是setCount(c => c + 1)做的事情。你可以认为它是在给React“发送指令”告知如何更新状态。

effect只运行了一次,第一次渲染中的定时器回调函数可以完美地在每次触发的时候给React发送c => c + 1更新指令。它不再需要知道当前的count值。因为React已经知道了。

扩展:定时器每次在count上增加一个step

第一种方案
在effect中添加step依赖

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

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

  return (
    <>
      <h1>{count}</h1>
      <input value={step} onChange={e => setStep(Number(e.target.value))} />
    </>
  );
}

这种方法问题是修改step会重启定时器,清除上一次的effect然后重新运行新的effect。

第二种方案
dispatch依赖去替换effect的step依赖

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]);
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();
  }
}

这种方案好处是:React会保证dispatch在组件的声明周期内保持不变。所以不再需要重新订阅定时器。

扩展:依赖属性的定时器

function Counter({ step }) {
  const [count, dispatch] = useReducer(reducer, 0);

  function reducer(state, action) {
    if (action.type === 'tick') {
      return state + step;
    } else {
      throw new Error();
    }
  }

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({ type: 'tick' });
    }, 1000);
    return () => clearInterval(id);
  }, [dispatch]);

  return <h1>{count}</h1>;
}

我们需要依赖props去计算下一个状态,可以把reducer函数放到组件内去读取props

在之前渲染中调用的reducer怎么“知道”新的props
答案是当你dispatch的时候,React只是记住了action - 它会在下一次渲染中再次调用reducer。在那个时候,新的props就可以被访问到,而且reducer调用也不是在effect里。

调用两次useEffect

以下场景:组件内有几个effect使用了相同的函数,若不想在每个effect里复制黏贴一遍这个逻辑,将该函数提取出来

function SearchResults() {
  function getFetchUrl(query) {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // 🔴 Missing dep: getFetchUrl

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // 🔴 Missing dep: getFetchUrl

  // ...
}

这个例子中:为了复用逻辑,不把getFetchUrl移到effects中,

如果我们按照上面的错误提示,将函数依赖添加到effect依赖数组中。因为函数每次渲染都会改变,我们的两个effects都依赖getFetchUrl,而它每次渲染都不同,所以我们的依赖数组会变得无用。

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

  // ...
}

有两种解决方案:
第一种:将函数提到组件外面去定义

// ✅ Not affected by the data flow
function getFetchUrl(query) {
  return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

function SearchResults() {
  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, []); // ✅ Deps are OK

  // ...
}

不需要该函数设为依赖,因为它不在渲染范围内,因此不会被数据流影响。它不可能突然意外地依赖于props或state。

第二种:把该函数包装成useCallBack hook

function SearchResults() {
  // ✅ Preserves identity when its own deps are the same
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);  // ✅ Callback deps are OK

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  useEffect(() => {
    const url = getFetchUrl('redux');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}

useCallback本质上是getFetchUrl添加了一层依赖检查,使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。

**继续扩展一下上面的例子 **
函数getFetchUrl的query不通过传参,而是从状态中获取。

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

  useEffect(() => {
    const url = getFetchUrl();
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}

以上代码中,如果query保持不变,getFetchUrl也会保持不变,我们的effect也不会重新运行。但是如果query修改了,getFetchUrl也会随之改变,因此会重新请求数据。

useReducer

使用场景: 当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时。
干的事情:reducer可以让你把组件内发生了什么(actions)和状态如何响应并更新分开表述。

总结

本文章非原创,是用来知识总结沉淀的。
参考:https://overreacted.io/zh-hans/a-complete-guide-to-useeffect/

上一篇下一篇

猜你喜欢

热点阅读