React理解

一.Class Component VS Function Component

1.Capture(捕获) 特性

对比下面两段代码
Function Component

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

  const log = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(count); // 0 1 2
    }, 3000);
  };

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

Class Component

class Counter extends Component {
  state = { count: 0 };

  log = () => {
    this.setState({
      count: this.state.count + 1
    });
    setTimeout(() => {
      console.log(this.state.count); // 3 3 3
    }, 3000);
  };

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.log}>Click me</button>
      </div>
    );
  }
}

在3s内连续点击button3次,Class和Function的结果完全不同

Class Component
首先 state 是 Immutable(不可变) 的,setState 后一定会生成一个全新的 state 引用。但 Class Component 通过 this.state 方式读取 state,这导致了每次代码执行都会拿到最新的 state 引用,所以快速点击三次的结果是 3 3 3。

Function Component
useState 产生的数据也是 Immutable(不可变) 的,通过数组第二个参数 Set 一个新值后,原来的值会形成一个新的引用在下次渲染时。但由于对 state 的读取没有通过 this. 的方式,使得每次 setTimeout 都读取了当时渲染闭包环境的数据,虽然最新的值跟着最新的渲染变了,但旧的渲染里,状态依然是旧值。

为了更容易理解,来模拟三次 Function Component 模式下点击按钮时的状态:

第一次点击,共渲染了 2 次,setTimeout 生效在第 1 次渲染,此时状态为:

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

  const log = () => {
    setCount(0 + 1);
    setTimeout(() => {
      console.log(0);
    }, 3000);
  };

  return ...
}

第二次点击,共渲染了 3 次,setTimeout 生效在第 2 次渲染,此时状态为:

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

  const log = () => {
    setCount(1 + 1);
    setTimeout(() => {
      console.log(1);
    }, 3000);
  };

  return ...
}

第三次点击,共渲染了 4 次,setTimeout 生效在第 3 次渲染,此时状态为:

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

  const log = () => {
    setCount(2 + 1);
    setTimeout(() => {
      console.log(2);
    }, 3000);
  };

  return ...
}

可以看到,每一个渲染都是一个独立的闭包,在独立的三次渲染中,count 在每次渲染中的值分别是 0 1 2,所以无论 setTimeout 延时多久,打印出来的结果永远是 0 1 2。

2.如何规避掉capture特性?

第一种方法是useRef

function Counter() {
  const count = useRef(0);

  const log = () => {
    count.current++;
    setTimeout(() => {
      console.log(count.current);
    }, 3000);
  };

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button onClick={log}>Click me</button>
    </div>
  );
}

这样打印结果就是 3 3 3。

useRef 的功能:通过 useRef 创建的对象,其值只有一份,而且在所有 Rerender 之间共享。
所以我们对 count.current 赋值或读取,读到的永远是其最新值,而与渲染闭包无关,因此如果快速点击三下,必定会返回 3 3 3 的结果。

但这种方案有个问题,就是使用 useRef 替代了 useState 创建值,那么很自然的问题就是,如何不改变原始值的写法,达到同样的效果呢?

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

  useEffect(() => {
    currentCount.current = count;
  });

  const log = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(currentCount.current);
    }, 3000);
  };

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

可以写成自定义hook

function useCurrentValue(value) {
  const ref = useRef(0);

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref;
}
function Counter() {
  const [count, setCount] = useState(0);
  const currentCount = useCurrentValue(count);

  const log = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log(currentCount.current);
    }, 3000);
  };

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

将 setTimeout 换成 setInterval 会怎样?

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

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

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

运行的结果是每次都输出1

把useEffect的依赖加上代码的功能就会正常

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

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

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

这样还是有一个问题,每次计数器都会重新实例化,性能成本会增加

如何解决这个问题?

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

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

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

利用 useState 的第二种赋值用法,不直接依赖 count,而是以函数回调方式进行赋值
这种写法的好处是
1.不依赖 count。
2.依赖项为 [],只有初始化会对 setInterval 进行实例化。
而之所以输出还是正确的 1 2 3 …,原因是 setCount 的回调函数中,c 值永远指向最新的 count 值,因此没有逻辑漏洞。

但是依旧还有一个需要考虑到的地方,如果同时用到了两个变量的话,这种方法将会失效,那么要怎么解决?

useReducer

const [state, dispatch] = useReducer(reducer, initialState);

useReducer 返回的结构与 useState 很像,只是数组第二项是 dispatch,而接收的参数也有两个,初始值放在第二位,第一位就是 reducer。

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

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

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

function reducer(state, action) {
  switch (action.type) {
    case "tick":
      return {
        ...state,
        count: state.count + state.step
      };
  }
}

可以看到,我们通过 reducer 的 tick 类型完成了对 count 的累加,而在 useEffect 的函数中,竟然完全绕过了 count、step 这两个变量。

这个例子还是有一个依赖的,那就是 dispatch,然而 dispatch 引用永远也不会变,因此可以忽略它的影响。

最后补充一个父组件 “坑” 子组件的经典案例。

function App() {
  const [count, forceUpdate] = useState(0);

  const schema = { b: 1 };

  return (
    <div>
      <Child schema={schema} />
      <div onClick={() => forceUpdate(count + 1)}>Count {count}</div>
    </div>
  );
}
子组件的代码如下:

const Child = memo(props => {
  useEffect(() => {
    console.log("schema", props.schema);
  }, [props.schema]);

  return <div>Child</div>;
});

只要父级 props.schema 变化就会打印日志。结果自然是,父组件每次刷新,子组件都会打印日志,也就是 子组件 [props.schema] 完全失效了,因为引用一直在变化。

其实 子组件关心的是值,而不是引用,所以一种解法是改写子组件的依赖:

const Child = memo(props => {
  useEffect(() => {
    console.log("schema", props.schema);
  }, [JSON.stringify(props.schema)]);

  return <div>Child</div>;
});

可是真正罪魁祸首是父组件,我们需要利用 Ref 优化一下父组件:

function App() {
  const [count, forceUpdate] = useState(0);
  const schema = useRef({ b: 1 });

  return (
    <div>
      <Child schema={schema.current} />
      <div onClick={() => forceUpdate(count + 1)}>Count {count}</div>
    </div>
  );
}

这样 schema 的引用能一直保持不变。如果你完整读完了本文,应该可以充分理解第一个例子的 schema 在每个渲染快照中都是一个新的引用,而 Ref 的例子中,schema 在每个渲染快照中都只有一个唯一的引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值