关于从 Hooks 开始学 React 的事

一个月前我开始做一些前端工作,我他喵直接速成前端框架 React (其实 Vue3 也有偷偷学一哈,下篇再写) 。我个人的习惯是直接奔最新推的特性开始学,所以直接上 React Hooks 了。

Hello world!?

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

所有 React 应用都有这么一句好像 server.listen(4000) 的渲染调用,实际上他是一种侵占式服务——一旦某个元素被 React 接管,就 不可以 用常规方式去修改他里面的元素了,除非你保证 React 不会重新渲染相关 DOM。

这行代码还引出了 React 的核心卖点:JSX ,以及 diffing 算法 。所谓的 <h1>Hello, world!</h1> 不过是另一种普通 js 对象的表达。如果传入的 jsx 里包含组件类或组件函数,React 会在其 setState 的时候触发重新渲染。

单向绑定和花活

前端搞模型与视图分离,但是模型的变化要及时反映到视图上。如何实现呢?React 掏出了 setState 说:这个函数被我下了魔法,你的组件一旦调用它我就重新渲染该组件。对于组件类来说,就是重新调用 render() 函数(暂且不提那些生命周期钩子);而对于 Hooks,就是直接把整个函数重新执行一遍。组件类可以使用同一个实例对象从而保证 this.state 保留状态,函数则依赖 useState() 这个 hook 维护自己的数据(如果你对具体的实现感兴趣,又不想阅读完整的 React 源码,可以看一下这篇 Build your own React)。

Hooks 笔记

一个基础的函数组件长这样:

const Demo: React.FC = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>
};

你可以绑定一个 <input> 的值,然而更推荐的方式是使用 非受控组件小声 bb:如果此时查看这个元素上绑定的 listener,你会发现一堆什么 input, change, blur 啥啥的都被监听了。:/

const Demo: React.FC = () => {
  const [name, setName] = useState("");
  return <div>
    Name: {name}<br />
    <input onChange={(e) => setName(e.target.value)} value={name} />
  </div>
};

useEffect 可以在每次渲染后同步地执行一点东西:

const Demo: React.FC = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);
  useEffect(() => { setTimeout(increment) });
  return <div>{count}</div>
};

注意,如果不使用 setTimeout ,这段代码会直接把页面卡死(同步函数递归)。

它还可以有第二个参数表示依赖,变了才执行。第一个参数可以返回一个回调函数用于清空副作用。这里我就不展开瞎 bb 了,建议直接看 useEffect 完整指南

上面两个就是最常用的锤子和钉子了,魔法的核心,其他几乎所有操作都基于他们。

useContext 可以用于传递全局状态:

const MyContext = React.createContext()

function Child() {
  const [state, setState] = React.useContext(MyContext);
  return <div onClick={() => setState("setState from Child")}>
    State: {JSON.stringify(state)}
  </div>
}

function App({ name = "Click me!" }) {
  const [state, setState] = React.useState(name);
  return <MyContext.Provider value={[state, setState]}>
    <Child />
  </MyContext.Provider>
}

TODO: 增加更多用例

性能瓶颈

因为 React 应用经常要重新渲染 JSX,可以看到他的渲染很容易被 render() 函数或者说函数组件本身执行的效率影响,可以想象一堆函数返回巨量对象时的场景。目前只能依赖 prepack 等工具在生产环境做少量的优化。相较之而言 Vue 和 Svelte 本身编译模板时会得到更多可以用于优化的信息,比如对于纯静态视图直接 innerHTML 就塞了(只是举个例子)。

比如说我们使用函数作为 useState()useCallback() 的参数:

const [get, set] = useState(() => /* long long code */);

没错,useState 只在第一次渲染的时候调用这个回调函数,以后就当没看见了。但是,站在 JS 的角度,这个回调函数每次渲染时都要真正地重新定义一遍(即使并没有被使用),表达式不要钱吗(。临时对象也是占空间和时间的,虽然 V8 相当聪明,大部分时候他快得一比。

功能缺陷

下次写写 Vue3(咕咕咕