天天看点

React运行时——React as a UI Runtime

本篇是笔记,原文地址: https://overreacted.io/zh-hans/react-as-a-ui-runtime/

在本篇文章中,我会从最佳原则的角度尽可能地阐述 React 编程模型。我不会解释如何使用它 — 而是讲解它的原理。

宿主更新

React 元素并不是永远存在的 。它们总是在重建和删除之间不断循环着。

React 元素具有不可变性。例如,你不能改变 React 元素中的子元素或者属性。如果你想要在稍后渲染一些不同的东西,你需要从头创建新的 React 元素树来描述它。

条件渲染中不需要渲染时写成null以占位,让react能对比元素顺序以重用。

key 给予 React 判断子元素是否真正相同的能力,即使在渲染前后它在父元素中的位置不是相同的。

组件

React 组件中对于 props 应该是纯净的。

惰性初始化是被允许的即使它不是完全“纯净”的:

function ExpenseForm() {
  // 只要不影响其他组件这是被允许的:
  SuperCalculator.initializeIfNotReady();
  // 继续渲染......
}
           

只要调用组件多次是安全的,并且不会影响其他组件的渲染,React 并不关心你的代码是否像严格的函数式编程一样百分百纯净。在 React 中,幂等性比纯净性更加重要。

也就是说,在 React 组件中不允许有用户可以直接看到的副作用。换句话说,仅调用函数式组件时不应该在屏幕上产生任何变化。

调用组件而不是函数

组件属于函数,因此我们可以直接进行调用

let reactElement = Form({ showMessage: true });
ReactDOM.render(reactElement, domContainer);
           

组件函数名称按照规定需要大写。当 JSX 转换时看见 而不是 ,它让对象 type 本身成为标识符而不是字符串:

console.log(<form />.type); // 'form' 字符串
console.log(<Form />.type); // Form 函数
           

但是为什么我们要编写 而不是 Form() ?因为如果是前者,React知道你是在调用组件,它会:

  1. React 专门针对于那些渲染 UI 树并且能够响应交互的应用增强了组件特性——如果你直接调用了组件,你就只能自己来构建这些特性了。
  2. 有助于参与协调,比如React在diff时决定哪些元素更新或重建。
  3. 推迟协调,优化性能
  4. 在开发者工具中更容易调试
  5. 惰性求值——当我们在组件中使用条件渲染提前返回jsx时,那些没用到的组件就不会被执行;而调用函数的话还要执行。如下,条件成立时children所含的组件就不会被执行。
function Page({ currentUser, children }) {
  if (!currentUser.isLoggedIn) {
      return <h1>Please login</h1>;
  }
  return (
    <Layout>
      {children}
    </Layout>
  );
}
           

在 JSX 中<A><B /></A> 和 <A children={<B />} /> 相同。

一致性

React 将所有的工作分成了“渲染阶段”和“提交阶段”的原因。渲染阶段是当 React 调用你的组件然后进行协调的时段。在此阶段进行干涉是安全的且在未来这个阶段将会变成异步的。提交阶段 就是 React 操作宿主树的时候。而这个阶段永远是同步的。

缓存

可以通过 useMemo() Hook 获得单个表达式级别的细粒度缓存。该缓存与其相关的组件紧密联系在一起,并且将与局部状态一起被销毁。它只会保留最后一次计算的结果。

state

组件内调用 setState 并不会立即执行重渲染。相反,React 会先触发所有的事件处理器,然后再触发一次重渲染以进行所谓的批量更新。如下代码相当于三次 setCount(1) 调用

const [count, setCounter] = useState(0);
  function increment() {
    setCounter(count + 1);
  }
  function handleClick() {
    increment();
    increment();
    increment();
  }
           

如果希望3次都在前一次结果上累加,则可以将setState的参数写成一个函数

如果更新逻辑更复杂,可以考虑使用useReducer Hook,action 字段可以是任意值,尽管对象是常用的选择。

const [counter, dispatch] = useReducer((state, action) => {
    if (action === 'increment') {
      return state + 1;
    } else {
      return state;
    }
  }, 0);
  function handleClick() {
    dispatch('increment');
   dispatch('increment');
   dispatch('increment');
 }
           

Context

从顶层传入的参数,所有后代组件都可以访问到,值变化时重新渲染。适用于主题等变量。

const ThemeContext = React.createContext(
  'light' // 默认值作为后备
);
function DarkApp() {
  return (
    <ThemeContext.Provider value="dark">
      <MyComponents />
    </ThemeContext.Provider>
  );
}
function SomeDeeplyNestedChild() {
  // 取决于其子组件在哪里被渲染
  const theme = useContext(ThemeContext);
  // ...
}
           

当 SomeDeeplyNestedChild 渲染时, useContext(ThemeContext) 会寻找树中最近的 <ThemeContext.Provider> ,并且使用它的 value 。

(事实上,React 维护了一个上下文栈当其渲染时。)

如果没有 ThemeContext.Provider 存在,useContext(ThemeContext) 调用的结果就会被调用 createContext() 时传递的默认值所取代。在上面的例子中,这个值为 ‘light’ 。

useEffect

  1. 考虑使用函数作为依赖
  2. useLayoutEffect尽量少用
  3. 使用自定义hooks

use

  1. use开头的hooks不能写在条件判断中
  2. Hooks 的内部实现其实是链表 。当你调用 useState 的时候,我们将指针移到下一项。当我们退出组件的“调用树”帧时,会缓存该结果的列表直到下次渲染开始。
// 伪代码
let hooks, i;
function useState() {
  i++;
  if (hooks[i]) {
    // 再次渲染时
    return hooks[i];
  }
  // 第一次渲染
  hooks.push(...);
}

// 准备渲染
i = -1;
hooks = fiber.hooks || [];
// 调用组件
YourComponent();
// 缓存 Hooks 的状态
fiber.hooks = hooks;
           

继续阅读