react 注意事项

1
<CommentItem likeComment={() => this.likeComment(user.id)} />

这个问题会导致每次父组件render方法被调用时,一个新的函数被创建,已将其传入likeComment。这会有一个改变每个子组件props的副作用,它将会造成他们全部重新渲染,即使数据本身没有发生变化

子组件的likeComment属性将总是有相同的引用,这样就不会造成不必要的重新渲染

01
02
03
04
05
06
07
08
09
10
// 方法1
import { useCallback } from 'react';
 
const List = ()=> {
  const comment = (id) => { doing something}
  const likeComment = useCallback(()=> { comment(id) }, [id]);
  // 这样自组建就不会从新渲染
  return list.map (
    v =>  <CommentItem onClick={likeComment(v.id)} >)
}

然后再子组件中创建一个引用了传入属性的类方法:

01
02
03
04
05
06
07
08
09
10
//方法2
<CommentItem likeComment={this.likeComment} userID={user.id} />
 
class CommentItem extends PureComponent {
  ...
  handleLike() {
    this.props.likeComment(this.props.userID)
  }
  ...
}

不要在render方法里派生数据

每次组件重新渲染时topTen都将有一个新的引用,即使posts没有改变并且派生数据也是相同的。这将造成列表不必要的重新渲染。

1
2
3
4
5
render() {
  const { posts } = this.props
  const topTen = posts.sort((a, b) => b.likes - a.likes).slice(0, 9)
  return //...
}

使用派生方法解决

1
2
3
4
5
6
static getDerivedStateFromProps(nextProps, state) {
   if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    )
}

管理派生 state 本来就很复杂,而且这种复杂度是随着需要管理的属性变得越来越庞大。比如,如果我们想在组件 state 里添加第二个派生 state,那就需要写两份跟踪变化的逻辑。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import memoize from "memoize-one";
 
class Example extends Component {
  // state 只需要保存当前的 filter 值:
  state = { filterText: "" };
 
  // 在 list 或者 filter 变化时,重新运行 filter:
  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );
 
  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };
 
  render() {
    // 计算最新的过滤后的 list。
    // 如果和上次 render 参数一样,`memoize-one` 会重复使用上一次的值。
    const filteredList = this.filter(this.props.list, this.state.filterText);
 
    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

在使用 memoization 时,请记住这些约束:

  1. 大部分情况下, 每个组件内部都要引入 memoized 方法,已免实例之间相互影响。
  2. 一般情况下,我们会限制 memoization 帮助函数的缓存空间,以免内存泄漏。(上面的例子中,使用 memoize-one 只缓存最后一次的参数和结果)。
  3. 如果每次父组件都传入新的 props.list ,那本文提到的问题都不会遇到。在大多数情况下,这种方式是可取的。

如果你正在使用Redux,可以考虑使用reselect来创建”selectors”来组合和缓存派生数据。

UseInterval

所以在 class 里你要怎么用 setInterval 做到这一点呢?我会这么做:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Counter extends React.Component {
  state = {
    count: 0,
    delay: 1000,
  };
 
  componentDidMount() {
    this.interval = setInterval(this.tick, this.state.delay);
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevState.delay !== this.state.delay) {
      clearInterval(this.interval);
      this.interval = setInterval(this.tick, this.state.delay);
    }
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  tick = () => {
    this.setState({
      count: this.state.count + 1
    });
  }
 
  handleDelayChange = (e) => {
    this.setState({ delay: Number(e.target.value) });
  }
 
  render() {
    return (
      <>
        <h1>{this.state.count}</h1>
        <input value={this.state.delay} onChange={this.handleDelayChange} />
      </>
    );
  }
}

Hook 版本

因为需要许多订阅 API 可以随时顺手移除老的监听者和加个新的。但是,setInterval 和它们不一样。当我们执行 clearInterval 和 setInterval 时,它们会进入时间队列里,如果我们频繁重渲染和重执行 effects,interval 有可能没有机会被执行!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
function Counter() {
  let [count, setCount] = useState(0);
 
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  });
 
  return <h1>{count}</h1>;
}
 
const rootElement = document.getElementById("root");
 
// Second interval to demonstrate the issue.
// Fast updates from it cause the Counter's
// interval to constantly reset and never fire.
setInterval(() => {
  ReactDOM.render(<Counter />, rootElement);
}, 100);

当我们 只 想在 mount 时执行 effect 和 unmount 时清理它,我们可以传空 [] 的依赖数组。

01
02
03
04
05
06
07
08
09
10
11
function Counter() {
  let [count, setCount] = useState(0);
 
  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
 
  return <h1>{count}</h1>;

但是,现在我们的计时器更新到 1 就不动了

问题在于,useEffect 在第一次渲染时获取值为 0 的 count,我们不再重执行 effect,所以 setInterval 一直引用第一次渲染时的闭包 count,以至于 count + 1 一直是 1。哎呀呀!

  • 修复它的一种方法是用像 setCount(c => c + 1) 这样的 「updater」替换 setCount(count + 1),这样可以读到新 state 变量。但这个无法帮助你获取到新的 props。
  • 另一个方法是用 useReducer()。这种方法为你提供了更大的灵活性。在 reducer 中,你可以访问到当前 state 和新的 props。dispatch 方法本身永远不会改变,所以你可以从任何闭包中将数据放入其中。useReducer() 有个约束是你不可以用它执行副作用。(但是,你可以返回新状态 —— 触发一些 effect。)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
function Counter() {
  const [count, dispatch] = useReducer((state, action) => {
    if (action === 'inc') {
      return state + 1;
    }
  }, 0);
 
  useEffect(() => {
    let id = setInterval(() => {
      dispatch('inc');
    }, 1000);
    return () => clearInterval(id);
  }, []);
 
  return <h1>{count}</h1>;
}
 
const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

阻抗不匹配

我们的「阻抗匹配」不在数据库和对象之间,它在 React 编程模型和命令式 setInterval API 之间。

一个 React 组件可能在 mounted 之前流经许多不同的 state,但它的渲染结果将一次性全部描述出来。

Hooks 使我们把相同的声明方法用在 effects 上:

1
2
3
4
// 描述每个间隔状态
useInterval(() => {
  setCount(count + 1);
}, isRunning ? delay : null);

我们不设置 interval,但指定它是否设置延迟或延迟多少,我们的 Hooks 做到了,用离散术语描述连续过程

相反,setInterval 没有及时地描述过程 —— 一旦设定了 interval,除了清除它,你无法对它做任何改变。

这就是 React 模型和 setInterval API 之间的不匹配。

Refs 出马

  • 我们在第一次渲染时执行带 callback1setInterval(callback1, delay)
  • 我们在下一次渲染时得到携带新的 propsstatecallbaxk2
  • 我们无法在不重置时间的情况下替换掉已经存在的 interval

那么如果我们根本不替换 interval,而是引入一个指向新 interval 回调的可变 savedCallback 会怎么样? 现在我们来看看这个方案:

  • 我们调用 setInterval(fn, delay),其中 fn 调用 savedCallback
  • 第一次渲染后将 savedCallback 设为 callback1
  • 下一次渲染后将 savedCallback 设为 callback2

这个可变的 savedCallback 需要在重新渲染时「可持续(persist)」,所以不可以是一个常规变量,我们想要一个类似实例的字段。

1
2
const savedCallback = useRef();
 // { current: null }

useRef() 返回一个有带有 current 可变属性的普通对象在 renders 间共享,我们可以保存新的 interval 回掉给它:

1
2
3
4
5
6
7
8
9
function callback() {
  // 可以读到新 props,state等。
  setCount(count + 1);
}
 
// 每次渲染后,保存新的回调到我们的 ref 里。
useEffect(() => {
  savedCallback.current = callback;
});

之后我们便可以从我们的 interval 中读取和调用它:

1
2
3
4
5
6
7
8
useEffect(() => {
  function tick() {
    savedCallback.current();
  }
 
  let id = setInterval(tick, 1000);
  return () => clearInterval(id);
}, []);

感谢 [],不重执行我们的 effectinterval 就不会被重置。同时,感谢 savedCallback ref,让我们可以一直在新渲染之后读取到回调,并在 interval tick 里调用它。

提取一个 Hook

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
function useInterval(callback) {
  const savedCallback = useRef();
 
  useEffect(() => {
    savedCallback.current = callback;
  });
 
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
 
    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
}

当前,1000 delay 是写死的,我想把它变成一个参数:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function Counter() {
  const [count, setCount] = useState(0);
 
  useInterval(() => {
    setCount(count + 1);
  }, 1000);
 
  return <h1>{count}</h1>;
}
 
function useInterval(callback, delay) {
  const savedCallback = useRef();
 
  useEffect(() => {
    savedCallback.current = callback;
  });
 
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
 
    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

等等,我们不是要避免重置 interval effect,并专门通过 [] 来避免它吗?不完全是,我们只想在回调改变时避免重置它,但当 delay 改变时,我们想要重启 timer

有效!我们现在可以不用想太多 useInterval() 的实现过程,在任意组件中使用它。

暂停 Interval

假设我们希望能够通过传递 null 作为 delay 来暂停我们的 interval:

1
2
3
4
5
6
const [delay, setDelay] = useState(1000);
 const [isRunning, setIsRunning] = useState(true);
 
 useInterval(() => {
   setCount(count + 1);
 }, isRunning ? delay : null);

如何实现这个?答案时:不创建 interval

01
02
03
04
05
06
07
08
09
10
useEffect(() => {
  function tick() {
    savedCallback.current();
  }
 
  if (delay !== null) {
    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }
}, [delay]);

vue watch vs getDerivedStateFromProps + componentDidUpdate

vue 中的watch 监听 props, data, computed 的变化, 执行异步或者开销较大的操作

getDerivedStateFromProps

getDerivedStateFromProps 会在render 前调用,并且在初始挂载和后续更新中都会调用, 返回一个对象更新state, 返回null 不更新任何内容,

当state 的值 在任何时候都需要props 的时候 适用该方法

1
2
3
4
5
6
static getDerivedStateFromProps(nextprops, preState){
  if(nextProps.info !== preState.info){
   return {info: nextProps.info}
 
  }
}

componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate()方法在组件更新后被调用。首次渲染不会执行此方法。当组件更新后,可以在此处操作DOM、执行setState或者执行异步请求操作。

  • 1)componentDidUpdate()的第三个参数snapshot参数来源于getSnapshotBeforeUpdate()生命周期的返回值。若没有实现getSnapshotBeforeUpdate(),此参数值为undefined。
  • 2)可以在componentDidUpdate()中直接调用setState(),但是它必需被包裹在一个条件语句里,否则会导致死循环。
  • 3)可以在componentDidUpdate()对更新前后的props进行比较,执行异步操作。
  • 4)如果shouldComponentUpdate()返回值为false,则不会调用componentDidUpdate()。

useMemo useCallback useRef

useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。

这里创建了两个state,然后通过expensive函数,执行一次昂贵的计算,拿到count对应的某个值。我们可以看到:无论是修改count还是val,由于组件的重新渲染,都会触发expensive的执行(能够在控制台看到,即使修改val,也会打印);但是这里的昂贵计算只依赖于count的值,在val修改的时候,是没有必要再次计算的。在这种情况下,我们就可以使用useMemo,只在count的值修改时,执行expensive计算:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default function WithMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    const expensive = useMemo(() => {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count]);
  
    return (
      <div>
        <h4>{count}-{expensive}</h4>
          {val}
        <div>
          <button onClick={() => setCount(count +
              1)}> +c1 </button>
            <input value={val} onChange={event =>
                setValue(event.target.value)}/>
        </div>
    </div>);
}

useCallback

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React, { useState, useCallback } from 'react';
 
const set = new Set();
export default function Callback() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
 
    const callback = useCallback(() => {
        console.log(count);
    }, [count]);
 
    set.add(callback);
  
    return (
      <div>
        <h4>{count}</h4>
        <h4>{set.size}</h4>
        <div>
           <button onClick={() => setCount(count + 1)}>+
           </button>
           <input value={val}
            onChange={event => setVal(event.target.value)}/>
        </div>
      </div>);
}

我们可以看到,每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。

知道useCallback有什么样的特点,那有什么作用呢?

使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
  
    const callback = useCallback(() => {
        return count;
    }, [count]);
 
    return (
      <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
           <button onClick={() => setCount(count + 1)}>+
           </button>
           <input value={val} onChange={event =>
                setVal(event.target.value)}/>
        </div>
     </div>);
}
  
function Child({ callback }) {
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return (<div>  {count} </div>)
}

不仅是上面的例子,所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。

useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref来访问。

useRef

useRef hooks函数,除了传统的用法之外,它还可以“跨渲染周期”保存数据

它保存数据的用法

在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state在多次渲染之后依旧不变。但是的,state问题在于一旦修改了它就会造成组件的重新渲染。

那么这个时候就可以使useRef用来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { useState, useEffect, useMemo, useRef } from 'react';
 
export default function App(props){
  const [count, setCount] = useState(0);
 
  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);
 
  const timerID = useRef();
   
  useEffect(() => {
    timerID.current = setInterval(()=>{
        setCount(count => count + 1);
    }, 1000);
  }, []);
   
  useEffect(()=>{
      if(count > 10){
          clearInterval(timerID.current);
      }
  });
   
  return (
    <>
      <button ref={couterRef} onClick={() => {setCount(count
        + 1)}}>
         Count: {count}, double: {doubleCount}
     </button>
    </>
  );
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//  useRef 穿透闭包
function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();
 
 // useLayoutEffect: DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
  useLayoutEffect(() => {
    textRef.current = text; // 将 text 写入到 ref
  });
 
  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // 从 ref 中读取 text
    alert(currentText);
  }, [textRef]); // handleSubmit 只会依赖 textRef 的变化。不会在 text 改变时更新
 
  return (
    <>
      <input value={text} onChange={e =>
        updateText(e.target.value)}
      />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

上面 input 值改变了, handleSubmit 也获取到的值也会变, 但 handleSubmit 不会变,不会触发ExpensiveTree 更新,

React 不可变数据

  • 如果直接修改数据 可能 检测不到 数据变化, 导致 组件不会更新
  • immutable.js 生成新数据, 并共享没有改变的数据, 新的props 或state 新的部分触发组件更新, 老的共享结构自然不更新

pure components 会等 props 和 state 浅比较, components 则 父组件更新 它就更新

PureComponent (React.momo)

在使用PureComponent的时候,只能把react组件写成是class的形式,不能使用函数的形式;react v16.6.0之后,可以使用React.memo来实现函数式的组件,也有了PureComponent的功能。

1
2
3
const ListComponent = React.momo(() => (
    <div>{this.props.data || 'loading'}</div>
))

所谓浅比较(shallowEqual),即react源码中的一个函数,然后根据下面的方法进行是不是PureComponent的判断,帮我们做了本来应该我们在shouldComponentUpdate中做的事情。

  1. props 或 state 
1
2
3
if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}

可能用不到派生 state ,可控 组件,非可控组件

什么时候用到派生

  • 1,直接复制 props 到 state 上;
  • 2,如果 props 和 state 不一致就更新 state。

如果你只是为了缓存(memoize)基于当前 props 计算后的结果的话, 请看 https://www.xmetal.cc/?p=1315

问题 :针对输入 组件

  1. 父组件更新, props 改变, 会重置 state ,我们输入的所有东西都会丢失
  2. if (nextProps.email !== this.props.email) {}只要 props.email 改变,就改变 state, 这样不会丢掉
  3. 上面会导致父组件格式如果是[{email:xxx, email:xxx}] , 子组件的input 就不能更新

设计组件时,重要的是确定组件是受控组件还是非受控组件

不要直接复制(mirror) props 的值到 state 中,而是去实现一个受控的组件,然后在父组件里合并两个值。比如,不要在子组件里被动的接受 props.value 并跟踪一个临时的 state.value,而要在父组件里管理 state.draftValue 和 state.committedValue,直接控制子组件里的值。这样数据才更加明确可预测

建议:完全可控的组件

1
2
3
function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

建议:有 key 的非可控组件

在这密码管理器的例子中,为了在不同的页面切换不同的值,我们可以使用 key 这个特殊的 React 属性。当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。 Keys 一般用来渲染动态列表,但是这里也可以使用

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
class EmailInput extends Component {
  state = { email: this.props.defaultEmail };
 
  handleChange = event => {
    this.setState({ email: event.target.value });
  };
 
  render() {
    return <input onChange={this.handleChange} value=
     {this.state.email} />;
  }
}
//每次 ID 更改,都会重新创建 EmailInput ,并将其状态重置为最新的 defaultEmail 值。
<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

选项一:用 prop 的 ID 重置非受控组件

如果某些情况下 key 不起作用(可能是组件初始化的开销太大),一个麻烦但是可行的方案是在 getDerivedStateFromProps 观察 userID 的变化:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
    prevPropsUserID: this.props.userID
  };
//使用了 getDerivedStateFromProps,用 componentWillReceiveProps 也一样。
  static getDerivedStateFromProps(props, state) {
    // 只要当前 user 变化,
    // 重置所有跟 user 相关的状态。
    // 这个例子中,只有 email 和 user 相关。
    if (props.userID !== state.prevPropsUserID) {
      return {
        prevPropsUserID: props.userID,
        email: props.defaultEmail
      };
    }
    return null;
  }
 
  // ...
}

选项二:使用实例方法重置非受控组件

更少见的情况是,即使没有合适的 key,我们也想重新创建组件。一种解决方案是给一个随机值或者递增的值当作 key,另外一种是用实例方法强制重置内部状态:

refs 在某些情况下很有用,比如这个。但通常我们建议谨慎使用。即使是做一个演示,这个命令式的方法也是非理想的,因为这会导致两次而不是一次渲染。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
export default class AccountsList extends Component {
  inputRef = React.createRef();
 
  state = {
    selectedIndex: 0
  };
 
  handleChange = index => {
    this.setState({ selectedIndex: index }, () => {
      const selectedAccount = this.props.accounts[index];
    this.inputRef.current.resetEmailForNewUser(selectedAccount.email);
    });
  };
 
  render() {
    const { accounts } = this.props;
    const { selectedIndex } = this.state;
    const selectedAccount = accounts[selectedIndex];
    return (
      <Fragment>
        <UncontrolledEmailInput
          defaultEmail={selectedAccount.email}
          ref={this.inputRef}
        />
        <p>
          Accounts:
          {this.props.accounts.map((account, index) => (
            <label key={account.id}>
              <input
                type="radio"
                name="account"
                checked={selectedIndex === index}
                onChange={() => this.handleChange(index)}
              />{" "}
              {account.name}
            </label>
          ))}
        </p>
       
      </Fragment>
    );
  }
}
//子组件
class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail
  };
 
  resetEmailForNewUser(newEmail) {
    this.setState({ email: newEmail });
  }
 
  // ...
}