精读《Function Component 入门》

1. 弁言

如果你在运用 React 16,能够尝试 Function Component 作风,享用更大的灵活性。但在尝试之前,最好先浏览本文,对 Function Component 的头脑情势有一个开端熟习,防备因头脑情势差别步构成的搅扰。

2. 精读

什么是 Function Component?

Function Component 就是以 Function 的情势建立的 React 组件:

function App() {
  return (
    <div>
      <p>App</p>
    </div>
  );
}

也就是,一个返回了 JSX 或 createElement 的 Function 就可以够看成 React 组件,这类情势的组件就是 Function Component。

所以我已学会 Function Component 了吗?

别急,故事才刚刚开始。

什么是 Hooks?

Hooks 是辅佐 Function Component 的东西。比方 useState 就是一种 Hook,它能够用来治理状况:

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

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

useState 返回的结果是数组,数组的第一项是 ,第二项是 赋值函数useState 函数的第一个参数就是 默认值,也支撑回调函数。更细致的引见能够参考 Hooks 划定规矩解读

先赋值再 setTimeout 打印

我们再将 useStatesetTimeout 连系运用,看看有什么发明。

建立一个按钮,点击后让计数器自增,然则延时 3 秒后再打印出来

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

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

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

如果我们 在三秒内一连点击三次,那末 count 的值终究会变成 3,而随之而来的输出结果是。。?

0
1
2

嗯,彷佛对,但总以为有点怪?

运用 Class Component 体式格局完成一遍呢?

敲黑板了,回到我们熟习的 Class Component 情势,完成一遍上面的功用:

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

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

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

嗯,结果应当等价吧?3 秒内疾速点击三次按钮,此次的结果是:

3
3
3

如何和 Function Component 结果不一样?

这是用好 Function Component 必需迈过的第一道坎,请确认完全明白下面这段话:

起首对 Class Component 举行诠释:

  1. 起首 state 是 Immutable 的,setState 后一定会天生一个全新的 state 援用。
  2. 但 Class Component 经由历程 this.state 体式格局读取 state,这致使了每次代码实行都邑拿到最新的 state 援用,所以疾速点击三次的结果是 3 3 3

那末对 Function Component 而言:

  1. useState 发生的数据也是 Immutable 的,经由历程数组第二个参数 Set 一个新值后,本来的值会构成一个新的援用在下次衬着时。
  2. 但因为对 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

明白了这一点,我们就可以继承了。

如何让 Function Component 也打印 3 3 3

所以这是不是是代表 Function Component 没法掩盖 Class Component 的功用呢?完全不是,我愿望你读完本文后,不仅能处置惩罚这个题目,更能明白为何用 Function Component 完成的代码更佳合理、文雅

第一种计划是借助一个新 Hook – 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 建立值,那末很天然的题目就是,如何不转变原始值的写法,到达一样的结果呢?

如何不革新原始值也打印 3 3 3

一种最简朴的做法,就是新建一个 useRef 的值给 setTimeout 运用,而递次其余部分照样用原始的 count:

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 – useEffect,请务必深切明白这个函数。

useEffect 是处置惩罚副作用的,实在行机遇在 每次 Render 衬着终了后,换句话说就是每次衬着都邑实行,只是现实在实在 DOM 操纵终了后。

我们能够应用这个特征,在每次衬着终了后,将 count 此时最新的值赋给 currentCount.current,如许就使 currentCount 的值自动同步了 count 的最新值。

为了确保人人准确明白 useEffect,笔者再烦琐一下,将实在行周期拆解到每次衬着中。假定你在三秒内疾速点击了三次按钮,那末你须要在大脑中模仿出下面这三次衬着都发生了什么:

第一次点击,共衬着了 2 次,useEffect 见效在第 2 次衬着:

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

  useEffect(() => {
    currentCount.current = 1; // 第二次衬着终了后实行一次
  });

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

  return ...
}

第二次点击,共衬着了 3 次,useEffect 见效在第 3 次衬着:

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

  useEffect(() => {
    currentCount.current = 2; // 第三次衬着终了后实行一次
  });

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

  return ...
}

第三次点击,共衬着了 4 次,useEffect 见效在第 4 次衬着:

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

  useEffect(() => {
    currentCount.current = 3; // 第四次衬着终了后实行一次
  });

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

  return ...
}

注重对照与上面章节睁开的 setTimeout 衬着时有什么差别。

要注重的是,useEffect 也跟着每次衬着而差别的,同一个组件差别衬着之间,useEffect 内闭包环境完全自力。关于本次的例子,useEffect 共实行了 四次,阅历了以下四次赋值终究变成 3:

currentCount.current = 0; // 第 1 次衬着
currentCount.current = 1; // 第 2 次衬着
currentCount.current = 2; // 第 3 次衬着
currentCount.current = 3; // 第 4 次衬着

请确保明白了这句话再继承往下浏览:

  • setTimeout 的例子,三次点击触发了四次衬着,但 setTimeout 离别见效在第 1、2、3 次衬着中,因而值是 0 1 2
  • useEffect 的例子中,三次点击也触发了四次衬着,但 useEffect 离别见效在第 1、2、3、4 次衬着中,终究使 currentCount 的值变成 3

用自定义 Hook 包装 useRef

是不是是以为每次都写一堆 useEffect 同步数据到 useRef 很烦?是的,想要简化,就须要引出一个新的观点:自定义 Hooks

起首引见一下,自定义 Hooks 许可建立自定义 Hook,只需函数名遵照以 use 开首,且返回非 JSX 元素,就是 Hooks 啦!自定义 Hooks 内还能够挪用包括内置 Hooks 在内的一切自定义 Hooks

也就是我们能够将 useEffect 写到自定义 Hook 里:

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

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

  return ref;
}

这里又引出一个新的观点,就是 useEffect 的第二个参数,dependences。dependences 这个参数定义了 useEffect 的依靠,在新的衬着中,只需一切依靠项的援用都不发生变化,useEffect 就不会被实行,且当依靠项为 [] 时,useEffect 仅在初始化实行一次,后续的 Rerender 永久也不会被实行。

这个例子中,我们通知 React:仅当 value 的值变化了,再将其最新值同步给 ref.current

那末这个自定义 Hook 就可以够在任何 Function Component 挪用了:

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

封装今后代码清新了许多,而且最主要的是将逻辑封装起来,我们只需明白 useCurrentValue 这个 Hook 能够发生一个值,其最新值永久与入参同步。

看到这里,或许有的小伙伴已按捺不住迸发的灵感了:useEffect 第二个参数设置为空数组,这个自定义 Hook 就代表了 didMount 生命周期!

是的,但笔者发起人人 不要再想生命周期的事变,如许会障碍你更好的明白 Function Component。因为下一个话题,就是要通知你:永久要对 useEffect 的依靠老实,被依靠的参数一定要填上去,不然会发生异常难以发觉与修复的 BUG。

setTimeout 换成 setInterval 会如何

我们回到出发点,将第一个 setTimeout Demo 中换成 setInterval,看看会如何:

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

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

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

这个例子将激发进修 Function Component 的第二个拦路虎,明白了它,才深切明白了 Function Component 的衬着道理。

起首引见一下引入的新观点,useEffect 函数的返回值。它的返回值是一个函数,这个函数在 useEffect 行将从新实行时,会先实行上一次 Rerender useEffect 第一个回调的返回函数,再实行下一次衬着的 useEffect 第一个回调。

以两次一连衬着为例引见,睁开后的结果是如许的:

第一次衬着:

function Counter() {
  useEffect(() => {
    // 第一次衬着终了后实行
    // 终究实行递次:1
    return () => {
      // 因为没有填写依靠项,所以第二次衬着 useEffect 会再次实行,在实行前,第一次衬着中这个处所的回调函数会起首被挪用
      // 终究实行递次:2
    }
  });

  return ...
}

第二次衬着:

function Counter() {
  useEffect(() => {
    // 第二次衬着终了后实行
    // 终究实行递次:3
    return () => {
      // 依此类推
    }
  });

  return ...
}

但是本 Demo 将 useEffect 的第二个参数设置为了 [],那末其返回函数只会在这个组件被烧毁时实行

读懂了前面的例子,应当能想到,这个 Demo 愿望应用 [] 依靠,将 useEffect 看成 didMount 运用,再连系 setInterval 每次时 count 自增,如许希冀将 count 的值每秒自增 1。

但是结果是:

1
1
1
...

明白了 setTimeout 例子的读者应当能够自行推导出缘由:setInterval 永久在第一次 Render 的闭包中,count 的值永久是 0,也就是等价于:

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

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

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

但是罪魁祸首就是 没有对依靠老实 致使的。例子中 useEffect 明显依靠了 count,依靠项却非要写 [],所以发生了很难明白的毛病。

所以纠正的要领就是 对依靠老实

永久对依靠项老实

一旦我们对依靠老实了,就可以够获得准确的结果:

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

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

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

我们将 count 作为了 useEffect 的依靠项,就获得了准确的结果:

1
2
3
...

既然漏写依靠的风险这么大,天然也有保护措施,那就是 eslint-plugin-react-hooks 这个插件,会自动修订你的代码中的依靠,想不对依靠老实都不可!

但是对这个例子而言,代码依旧存在 BUG:每次计数器都邑从新实例化,如果换成其他省事操纵,机能本钱将不可接受。

如何不在每次衬着时从新实例化 setInterval?

最简朴的要领,就是应用 useState 的第二种赋值用法,不直接依靠 count,而是以函数回调体式格局举行赋值:

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

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

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

这这写法真正做到了:

  1. 不依靠 count,所以对依靠老实。
  2. 依靠项为 [],只需初始化会对 setInterval 举行实例化。

而之所以输出照样准确的 1 2 3 ...,缘由是 setCount 的回调函数中,c 值永久指向最新的 count 值,因而没有逻辑破绽。

然则智慧的同砚细致一想,就会发明一个新题目:如果存在两个以上变量须要运用时,这招就没有用武之地了。

同时运用两个以上变量时?

如果同时须要对 countstep 两个变量做累加,那 useEffect 的依靠必定要写上一种某一个值,频仍实例化的题目就又涌现了:

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

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

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

这个例子中,因为 setCount 只能拿到最新的 count 值,而为了每次都拿到最新的 step 值,就必需将 step 申明到 useEffect 依靠中,致使 setInterval 被频仍实例化。

这个题目天然也搅扰了 React 团队,所以他们拿出了一个新的 Hook 处置惩罚题目:useReducer

什么是 useReducer

先别遐想到 Redux。只斟酌上面的场景,看看为何 React 团队要将 useReducer 列为内置 Hooks 之一。

先引见一下 useReducer 的用法:

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

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

reducer 定义了如何对数据举行变更,比方一个简朴的 reducer 以下:

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return {
        ...state,
        count: state.count + 1
      };
    default:
      return state;
  }
}

如许就可以够经由历程挪用 dispatch({ type: 'increment' }) 的体式格局完成 count 自增了。

那末回到这个例子,我们只须要轻微改写一下用法即可:

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

能够看到,我们经由历程 reducertick 范例完成了对 count 的累加,而在 useEffect 的函数中,居然完全绕过了 countstep 这两个变量。所以 useReducer 也被称为处置惩罚此类题目的 “黑魔法”。

实在不论被如何称谓也好,其本质是让函数与数据解耦,函数尽管发出指令,而不须要体贴运用的数据被更新时,须要从新初始化本身。

细致的读者会发明这个例子照样有一个依靠的,那就是 dispatch,但是 dispatch 援用永久也不会变,因而能够疏忽它的影响。这也表现了不论如何都要对依靠坚持老实。

这也激发了另一个注重项:只管将函数写在 useEffect 内部

将函数写在 useEffect 内部

为了防止脱漏依靠,必需将函数写在 useEffect 内部,如许 eslint-plugin-react-hooks 才经由历程静态剖析补齐依靠项:

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

  useEffect(() => {
    function getFetchUrl() {
      return "https://v?query=" + count;
    }

    getFetchUrl();
  }, [count]);

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

getFetchUrl 这个函数依靠了 count,而如果将这个函数定义在 useEffect 外部,不论是机械照样人眼都难以看出 useEffect 的依靠项包括 count

但是这就激发了一个新题目:将一切函数都写在 useEffect 内部岂不是异常难以保护?

如何将函数抽到 useEffect 外部?

为了处置惩罚这个题目,我们要引入一个新的 Hook:useCallback,它就是处置惩罚将函数抽到 useEffect 外部的题目。

我们先看 useCallback 的用法:

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

  const getFetchUrl = useCallback(() => {
    return "https://v?query=" + count;
  }, [count]);

  useEffect(() => {
    getFetchUrl();
  }, [getFetchUrl]);

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

能够看到,useCallback 也有第二个参数 – 依靠项,我们将 getFetchUrl 函数的依靠项经由历程 useCallback 打包到新的 getFetchUrl 函数中,那末 useEffect 就只须要依靠 getFetchUrl 这个函数,就完成了对 count 的间接依靠。

换句话说,我们应用了 useCallbackgetFetchUrl 函数抽到了 useEffect 外部。

为何 useCallbackcomponentDidUpdate 更好用

回想一下 Class Component 的情势,我们是如安在函数参数变化时举行从新取数的:

class Parent extends Component {
  state = {
    count: 0,
    step: 0
  };
  fetchData = () => {
    const url =
      "https://v?query=" + this.state.count + "&step=" + this.state.step;
  };
  render() {
    return <Child fetchData={this.fetchData} count={count} step={step} />;
  }
}

class Child extends Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.props.fetchData();
  }
  componentDidUpdate(prevProps) {
    if (
      this.props.count !== prevProps.count &&
      this.props.step !== prevProps.step // 别漏了!
    ) {
      this.props.fetchData();
    }
  }
  render() {
    // ...
  }
}

上面的代码经经常使用 Class Component 的人应当很熟习,但是暴露的题目可不小。

我们须要明白 props.count props.stepprops.fetchData 函数运用了,因而在 componentDidUpdate 时,推断这两个参数发生了变化就触发从新取数。

但是题目是,这类明白本钱是不是是过高了?如果父级函数 fetchData 不是我写的,在不读源码的状况下,我如何晓得它依靠了 props.countprops.step 呢?更严峻的是,如果某一天 fetchData 多依靠了 params 这个参数,下流函数将须要悉数在 componentDidUpdate 掩盖到这个逻辑,不然 params 变化时将不会从新取数。能够设想,这类体式格局保护本钱庞大,以至能够说险些没法保护。

换成 Function Component 的头脑吧!试着用上适才提到的 useCallback 处置惩罚题目:

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

  const fetchData = useCallback(() => {
    const url = 'https://v/search?query=' + count + "&step=" + step;
  }, [count, step])

  return (
    <Child fetchData={fetchData} />
  )
}

function Child(props) {
  useEffect(() => {
    props.fetchData()
  }, [props.fetchData])

  return (
    // ...
  )
}

能够看出来,当 fetchData 的依靠变化后,按下保留键,eslint-plugin-react-hooks 会自动补上更新后的依靠,而下流的代码不须要做任何转变,下流只须要体贴依靠了 fetchData 这个函数即可,至于这个函数依靠了什么,已封装在 useCallback 后打包透传下来了。

不仅处置惩罚了保护性题目,而且关于 只需参数变化,就从新实行某逻辑,是迥殊适合用 useEffect 做的,运用这类头脑思索题目会让你的代码更 “智能”,而运用破裂的生命周期举行思索,会让你的代码支离破碎,而且轻易遗漏种种机遇。

useEffect 对营业的笼统异常轻易,笔者举几个例子:

  1. 依靠项是查询参数,那末 useEffect 内能够举行取数要求,那末只需查询参数变化了,列表就会自动取数革新。注重我们将取数机遇从触发端改成了吸收端。
  2. 当列表更新后,从新注册一遍拖拽相应事宜。也是同理,依靠参数是列表,只需列表变化,拖拽相应就会从新初始化,如许我们能够宁神的修正列表,而不必忧郁拖拽事宜失效。
  3. 只需数据流某个数据变化,页面题目就同步修正。同理,也不须要在每次数据变化时修正题目,而是经由历程 useEffect “监听” 数据的变化,这是一种 “掌握反转” 的头脑。

说了这么多,其本质照样应用了 useCallback 将函数自力抽离到 useEffect 外部。

那末进一步思索,能够将函数抽离到全部组件的外部吗?

这也是能够的,须要灵活运用自定义 Hooks 完成。

将函数抽到组件外部

以上面的 fetchData 函数为例,如果要抽到全部组件的外部,就不是应用 useCallback 做到了,而是应用自定义 Hooks 来做:

function useFetch(count, step) {
  return useCallback(() => {
    const url = "https://v/search?query=" + count + "&step=" + step;
  }, [count, step]);
}

能够看到,我们将 useCallback 打包搬到了自定义 Hook useFetch 中,那末函数中只须要一行代码就可以完成一样的结果了:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const [other, setOther] = useState(0);
  const fetch = useFetch(count, step); // 封装了 useFetch

  useEffect(() => {
    fetch();
  }, [fetch]);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>setCount {count}</button>
      <button onClick={() => setStep(c => c + 1)}>setStep {step}</button>
      <button onClick={() => setOther(c => c + 1)}>setOther {other}</button>
    </div>
  );
}

跟着运用愈来愈轻易,我们能够将精神放到机能上。视察能够发明,countstep 都邑频仍变化,每次变化就会致使 useFetchuseCallback 依靠的变化,进而致使从新天生函数。但是现实上这类函数是没必要每次都从新天生的,反复天生函数会构成大批机能消耗。

换一个例子就可以够看得更清晰:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const [other, setOther] = useState(0);
  const drag = useDraggable(count, step); // 封装了拖拽函数
}

假定我们运用 Sortablejs 对某个地区举行拖拽监听,这个函数每次都反复实行的机能消耗异常大,但是这个函数内部能够因为仅仅要上报一些日记,所以依靠了没有现实被运用的 count step 变量:

function useDraggable(count, step) {
  return useCallback(() => {
    // 上报日记
    report(count, step);

    // 对地区举行初始化,异常耗时
    // ... 省略耗时代码
  }, [count, step]);
}

这类状况,函数的依靠就迥殊不合理。虽然依靠变化应当触发函数从新实行,但如果函数从新实行的本钱异常高,而依靠只是无足轻重的装点,得不偿失。

应用 Ref 保证耗时函数依靠稳定

一种要领是经由历程将依靠转化为 Ref:

function useFetch(count, step) {
  const countRef = useRef(count);
  const stepRef = useRef(step);

  useEffect(() => {
    countRef.current = count;
    stepRef.current = step;
  });

  return useCallback(() => {
    const url =
      "https://v/search?query=" + countRef.current + "&step=" + stepRef.current;
  }, [countRef, stepRef]); // 依靠不会变,却能每次拿到最新的值
}

这类体式格局比较取巧,将须要更新的地区与耗时地区星散,再将需更新的内容经由历程 Ref 提供给耗时的地区,完成机能优化。

但是如许做对函数的修改本钱比较高,有一种更通用的做法处置惩罚此类题目。

通用的自定义 Hooks 处置惩罚函数从新实例化题目

我们能够应用 useRef 制造一个自定义 Hook 替代 useCallback使其依靠的值变化时,回调不会从新实行,却能拿到最新的值!

这个奇异的 Hook 写法以下:

function useEventCallback(fn, dependencies) {
  const ref = useRef(null);

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

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}

再次体会到自定义 Hook 的无所不能。

起首看这一段:

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

fn 回调函数变化时, ref.current 从新指向最新的 fn 这个逻辑中规中矩。重点是,当依靠 dependencies 变化时,也从新为 ref.current 赋值,此时 fn 内部的 dependencies 值是最新的,而下一段代码:

return useCallback(() => {
  const fn = ref.current;
  return fn();
}, [ref]);

又仅实行一次(ref 援用不会转变),所以每次都能够返回 dependencies 是最新的 fn,而且 fn 还不会从新实行。

假定我们对 useEventCallback 传入的回调函数称为 X,则这段代码的寄义,就是使每次衬着的闭包中,回调函数 X 老是拿到的老是最新 Rerender 闭包中的谁人,所以依靠的值永久是最新的,而且函数不会从新初始化。

React 官方不引荐运用此范式,因而关于这类场景,应用 useReducer,将函数经由历程 dispatch 中挪用。 还记得吗?
dispatch 是一种能够绕过依靠的黑魔法,我们在 “什么是 useReducer” 小节提到过。

跟着对 Function Component 的运用,你也逐渐体贴到函数的机能了,这很棒。那末下一个重点天然是关注 Render 的机能。

用 memo 做 PureRender

在 Fucntion Component 中,Class Component 的 PureComponent 等价的观点是 React.memo,我们引见一下 memo 的用法:

const Child = memo((props) => {
  useEffect(() => {
    props.fetchData()
  }, [props.fetchData])

  return (
    // ...
  )
})

运用 memo 包裹的组件,会在本身重衬着时,对每一个 props 项举行浅对照,如果援用没有变化,就不会触发重衬着。所以 memo 是一种很棒的机能优化东西。

下面就引见一个看似比 memo 难用,但真正明白后会发明,实在比 memo 更好用的衬着优化函数:useMemo

用 useMemo 做部分 PureRender

比拟 React.memo 这个异类,React.useMemo 但是正派的官方 Hook:

const Child = (props) => {
  useEffect(() => {
    props.fetchData()
  }, [props.fetchData])

  return useMemo(() => (
    // ...
  ), [props.fetchData])
}

能够看到,我们应用 useMemo 包裹衬着代码,如许即使函数 Child 因为 props 的变化从新实行了,只需衬着函数用到的 props.fetchData 没有变,就不会从新衬着。

这里发明了 useMemo 的第一个优点:更细粒度的优化衬着

所谓更细粒度的优化衬着,是指函数 Child 团体能够用到了 AB 两个 props,而衬着仅用到了 B,那末运用 memo 计划时,A 的变化会致使重衬着,而运用 useMemo 的计划则不会。

useMemo 的优点还不止这些,这里先留下伏笔。我们先看一个新题目:当参数愈来愈多时,运用 props 将函数、值在组件间通报异常冗杂:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const fetchData = useFetch(count, step);

  return <Child fetchData={fetchData} setCount={setCount} setStep={setStep} />;
}

虽然 Child 能够经由历程 memouseMemo 举行优化,但当递次庞杂时,能够存在多个函数在一切 Function Component 间同享的状况 ,此时就须要新 Hook: useContext 来拯救了。

运用 Context 做批量透传

在 Function Component 中,能够运用 React.createContext 建立一个 Context:

const Store = createContext(null);

个中 null 是初始值,平常置为 null 也没紧要。接下来另有两步,离别是在根节点运用 Store.Provider 注入,与在子节点运用官方 Hook useContext 拿到注入的数据:

在根节点运用 Store.Provider 注入:

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(0);
  const fetchData = useFetch(count, step);

  return (
    <Store.Provider value={{ setCount, setStep, fetchData }}>
      <Child />
    </Store.Provider>
  );
}

在子节点运用 useContext 拿到注入的数据(也就是拿到 Store.Providervalue):

const Child = memo((props) => {
  const { setCount } = useContext(Store)

  function onClick() {
    setCount(count => count + 1)
  }

  return (
    // ...
  )
})

如许就不须要在每一个函数间举行参数透传了,大众函数能够都放在 Context 里。

然则当函数多了,Providervalue 会变得很痴肥,我们能够连系之前讲到的 useReducer 处置惩罚这个题目。

运用 useReducer 为 Context 通报内容瘦身

运用 useReducer,一切回调函数都经由历程挪用 dispatch 完成,那末 Context 只需通报 dispatch 一个函数就好了:

const Store = createContext(null);

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

  return (
    <Store.Provider value={dispatch}>
      <Child />
    </Store.Provider>
  );
}

这下不论是根节点的 Provider,照样子元素挪用都清新许多:

const Child = useMemo((props) => {
  const dispatch = useContext(Store)

  function onClick() {
    dispatch({
      type: 'countInc'
    })
  }

  return (
    // ...
  )
})

你或许很快就想到,将 state 也经由历程 Provider 注入进去岂不更妙?是的,但此处请务必注重潜伏机能题目。

state 也放到 Context 中

稍稍革新下,将 state 也放到 Context 中,这下赋值与取值都异常轻易了!

const Store = createContext(null);

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

  return (
    <Store.Provider value={{ state, dispatch }}>
      <Count />
      <Step />
    </Store.Provider>
  );
}

Count Step 这两个子元素而言,可须要郑重一些,如果我们这么完成这两个子元素:

const Count = memo(() => {
  const { state, dispatch } = useContext(Store);
  return (
    <button onClick={() => dispatch("incCount")}>incCount {state.count}</button>
  );
});

const Step = memo(() => {
  const { state, dispatch } = useContext(Store);
  return (
    <button onClick={() => dispatch("incStep")}>incStep {state.step}</button>
  );
});

其结果是:不论点击 incCount 照样 incStep,都邑同时触发这两个组件的 Rerender。

其题目在于:memo 只能挡在最外层的,而经由历程 useContext 的数据注入发生在函数内部,会 绕过 memo

当触发 dispatch 致使 state 变化时,一切运用了 state 的组件内部都邑强迫从新革新,此时想要对衬着次数做优化,只需拿出 useMemo 了!

useMemo 合营 useContext

运用 useContext 的组件,如果本身不运用 props,就可以够完全运用 useMemo 替代 memo 做机能优化:

const Count = () => {
  const { state, dispatch } = useContext(Store);
  return useMemo(
    () => (
      <button onClick={() => dispatch("incCount")}>
        incCount {state.count}
      </button>
    ),
    [state.count, dispatch]
  );
};

const Step = () => {
  const { state, dispatch } = useContext(Store);
  return useMemo(
    () => (
      <button onClick={() => dispatch("incStep")}>incStep {state.step}</button>
    ),
    [state.step, dispatch]
  );
};

对这个例子来讲,点击对应的按钮,只需运用到的组件才会重衬着,结果相符预期。 连系 eslint-plugin-react-hooks 插件运用,连 useMemo 的第二个参数依靠都是自动补全的。

读到这里,不晓得你是不是遐想到了 ReduxConnect?

我们来对照一下 ConnectuseMemo,会发明惊人的相似之处。

一个一般的 Redux 组件:

const mapStateToProps = state => (count: state.count);

const mapDispatchToProps = dispatch => dispatch;

@Connect(mapStateToProps, mapDispatchToProps)
class Count extends React.PureComponent {
  render() {
    return (
      <button onClick={() => this.props.dispatch("incCount")}>
        incCount {this.props.count}
      </button>
    );
  }
}

一个一般的 Function Component 组件:

const Count = () => {
  const { state, dispatch } = useContext(Store);
  return useMemo(
    () => (
      <button onClick={() => dispatch("incCount")}>
        incCount {state.count}
      </button>
    ),
    [state.count, dispatch]
  );
};

这两段代码的结果完全一样,Function Component 除了更简约以外,另有一个更大的上风:全自动的依靠推导

Hooks 降生的一个缘由,就是为了便于静态剖析依靠,简化 Immutable 数据流的运用本钱。

我们看 Connect 的场景:

因为不晓得子组件运用了哪些数据,因而须要在 mapStateToProps 提早写好,而当须要运用数据流内新变量时,组件里是没法访问的,我们要回到 mapStateToProps 加上这个依靠,再回到组件中运用它。

useContext + useMemo 的场景:

因为注入的 state 是全量的,Render 函数中想用什么都可直接用,在按保留键时,eslint-plugin-react-hooks 会经由历程静态剖析,在 useMemo 第二个参数自动补上代码里运用到的外部变量,比方 state.countdispatch

别的能够发明,Context 很像 Redux,那末 Class Component 情势下的异步中间件完成的异步取数如何应用 useReducer 做呢?答案是:做不到。

固然不是说 Function Component 没法完成异步取数,而是用的东西错了。

运用自定义 Hook 处置惩罚副作用

比方上面抛出的异步取数场景,在 Function Component 的最好做法是封装成一个自定义 Hook:

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl);

  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData
  });

  useEffect(() => {
    let didCancel = false;

    const fetchData = async () => {
      dispatch({ type: "FETCH_INIT" });

      try {
        const result = await axios(url);
        if (!didCancel) {
          dispatch({ type: "FETCH_SUCCESS", payload: result.data });
        }
      } catch (error) {
        if (!didCancel) {
          dispatch({ type: "FETCH_FAILURE" });
        }
      }
    };

    fetchData();

    return () => {
      didCancel = true;
    };
  }, [url]);

  const doFetch = url => setUrl(url);

  return { ...state, doFetch };
};

能够看到,自定义 Hook 具有完全生命周期,我们能够将取数历程封装起来,只暴露状况 – 是不是在加载中:isLoading 是不是取数失利:isError 数据:data

在组件中运用起来异常轻易:

function App() {
  const { data, isLoading, isError } = useDataApi("https://v", {
    showLog: true
  });
}

如果这个值须要存储到数据流,在一切组件之间同享,我们能够连系 useEffectuseReducer

function App(props) {
  const { dispatch } = useContext(Store);

  const { data, isLoading, isError } = useDataApi("https://v", {
    showLog: true
  });

  useEffect(() => {
    dispatch({
      type: "updateLoading",
      data,
      isLoading,
      isError
    });
  }, [dispatch, data, isLoading, isError]);
}

到此,Function Component 的入门观点就讲完了,末了附带一个彩蛋:Function Component 的 DefaultProps 如何处置惩罚?

Function Component 的 DefaultProps 如何处置惩罚?

这个题目看似简朴,实则不然。我们至少有两种体式格局对 Function Component 的 DefaultProps 举行赋值,下面逐一申明。

起首关于 Class Component,DefaultProps 基本上只需一种人人都承认的写法:

class Button extends React.PureComponent {
  defaultProps = { type: "primary", onChange: () => {} };
}

但是在 Function Component 就八门五花了。

应用 ES6 特征在参数定义阶段赋值

function Button({ type = "primary", onChange = () => {} }) {}

这类要领看似很文雅,实在有一个严重隐患:没有掷中的 props 在每次衬着援用都差别。

看这类场景:

const Child = memo(({ type = { a: 1 } }) => {
  useEffect(() => {
    console.log("type", type);
  }, [type]);

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

只需 type 的援用稳定,useEffect 就不会频仍的实行。如今经由历程父元素革新致使 Child 跟着革新,我们发明,每次衬着都邑打印出日记,也就意味着每次衬着时,type 的援用是差别的。

有一种不太文雅的体式格局能够处置惩罚:

const defaultType = { a: 1 };

const Child = ({ type = defaultType }) => {
  useEffect(() => {
    console.log("type", type);
  }, [type]);

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

此时不停革新父元素,只会打印出一次日记,因为 type 的援用是雷同的。

我们运用 DefaultProps 的本意必定是愿望默认值的援用雷同, 如果不想零丁保护变量的援用,还能够借用 React 内置的 defaultProps 要领处置惩罚。

应用 React 内置计划

React 内置计划能较好的处置惩罚援用频仍更改的题目:

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

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

Child.defaultProps = {
  type: { a: 1 }
};

上面的例子中,不停革新父元素,只会打印出一次日记。

因而发起关于 Function Component 的参数默认值,发起运用 React 内置计划处置惩罚,因为纯函数的计划不利于坚持援用稳定。

末了补充一个父组件 “坑” 子组件的典范案例。

不要坑了子组件

我们做一个点击累加的按钮作为父组件,那末父组件每次点击后都邑革新:

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

别的我们将 schema = { b: 1 } 通报给子组件,这个就是埋的一个大坑。

子组件的代码以下:

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 在每一个衬着快照中都只需一个唯一的援用。

3. 总结

所以运用 Function Component 你入门了吗?

本次精读留下的思索题是:Function Component 开辟历程当中另有哪些轻易犯毛病的细节?

议论地点是:
精读《Function Component 入门》 · Issue #157 · dt-fe/weekly

如果你想介入议论,请 点击这里,每周都有新的主题,周末或周一宣布。前端精读 – 帮你挑选靠谱的内容。

关注
前端精读微信民众号

《精读《Function Component 入门》》

special Sponsors

版权声明:自在转载-非商用-非衍生-坚持签名(
创意同享 3.0 许可证

    原文作者:黄子毅
    原文地址: https://segmentfault.com/a/1190000019300727
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞