react学习日记 react深刻了解

宿主树

  React 程序通常会输出一棵会随时间变化的树,我们称它为“宿主树”。

宿主实例

  宿主树由节点组成,我们称之为“宿主实例”。

  在 DOM 环境中,宿主实例就是我们通常所说的 DOM 节点 — 就像当你调用 document.createElement('div') 时获得的对象。

渲染器

  渲染器教会 React 如何与特定的宿主环境通信以及如何管理它的宿主实例。React DOM、React Native 甚至 Ink 都可以称作 React 渲染器。

React 元素

  在宿主环境中,一个宿主实例(例如 DOM 节点)是最小的构建单元。而在 React 中,最小的构建单元是 React 元素。

  React 元素是一个普通的 JavaScript 对象。它用来描述一个宿主实例。

// JSX 是用来描述这些对象的语法糖。
// <button className="blue" />
{
  type: 'button',
  props: { className: 'blue' }
}

  React 元素是轻量级的因为没有宿主实例与它绑定在一起。同样的,它只是对你想要在屏幕上看到的内容的描述。

  就像宿主实例一样,React 元素也能形成一棵树: 

// JSX 是用来描述这些对象的语法糖。
// <dialog>
//   <button className="blue" />
//   <button className="red" />
// </dialog>
{
  type: 'dialog',
  props: {
    children: [{
      type: 'button',
      props: { className: 'blue' }
    }, {
      type: 'button',
      props: { className: 'red' }
    }]
  }
}

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

入口

  React DOM 的入口就是 ReactDOM.render

ReactDOM.render(
  // { type: 'button', props: { className: 'blue' } }
  <button className="blue" />,
  document.getElementById('container')
)

  React 会这么做:

let domNode = document.createElement('button');
domNode.className = 'blue';
domContainer.appendChild(domNode);

协调

  如果相同的元素类型在同一个地方先后出现两次,React 会重用已有的宿主实例。

// let domNode = document.createElement('button');
// domNode.className = 'blue';
// domContainer.appendChild(domNode);
ReactDOM.render(
  <button className="blue" />,
  document.getElementById('container')
);

// 能重用宿主实例吗?能!(button → button)
// domNode.className = 'red';
ReactDOM.render(
  <button className="red" />,
  document.getElementById('container')
);

// 能重用宿主实例吗?不能!(button → p)
// domContainer.removeChild(domNode);
// domNode = document.createElement('p');
// domNode.textContent = 'Hello';
// domContainer.appendChild(domNode);
ReactDOM.render(
  <p>Hello</p>,
  document.getElementById('container')
);

// 能重用宿主实例吗?能!(p → p)
// domNode.textContent = 'Goodbye';
ReactDOM.render(
  <p>Goodbye</p>,
  document.getElementById('container')
);

  同样的启发式方法也适用于子树。例如,当我们在 <dialog> 中新增两个 <button> ,React 会先决定是否要重用 <dialog> ,然后为每一个子元素重复这个决定步骤。

条件

  很少会直接调用 ReactDOM.render 。相反,在 React 应用中程序往往会被拆分成这样的函数:

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = <p>I was just added here!</p>;
  }
  return (
    <dialog>
      {message}
      <input />
    </dialog>
  );
}

不管 showMessage 是 true 还是 false ,在渲染的过程中 <input> 总是在第二个孩子的位置且不会改变,第一个孩子位置的 message 如果没有内容就会为空。

如果 showMessage 从 false 改变为 true ,React 会遍历整个元素树,并与之前的版本进行比较:

  • dialog → dialog :能够重用宿主实例吗?能 — 因为类型匹配。

    • (null) → p :需要插入一个新的 p 宿主实例。
    • input → input :能够重用宿主实例吗?能 — 因为类型匹配。

之后 React 大致会像这样执行代码:

let inputNode = dialogNode.firstChild;
let pNode = document.createElement('p');
pNode.textContent = 'I was just added here!';
dialogNode.insertBefore(pNode, inputNode);

列表

  当我们遇到动态列表时,React 只会对其中的每个元素进行更新,而不是将其重新排序,这就会造成性能上的问题。 比如说,当商品列表的顺序发生变化时,原本在第一个输入框内的内容仍然会存在于现在的第一个输入框中(尽管事实上在山沟内列表里他应该代表着其他商品)

   这就是为什么每次当输出中包含元素数组时,React 都会让你指定一个叫做 key 的属性:

function ShoppingList({ list }) {
  return (
    <form>
      {list.map(item => (
        <p key={item.productId}>
          You bought {item.name}
          <br />
          Enter how many do you want: <input />
        </p>
      ))}
    </form>
  )
}

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

  当 React 在 <form> 中发现 <p key="42"> ,它就会检查之前版本中的 <form> 是否同样含有 <p key="42"> 。即使 <form> 中的子元素们改变位置后,这个方法同样有效。在渲染前后当 key 仍然相同时,React 会重用先前的宿主实例,然后重新排序其兄弟元素。

  需要注意的是 key 只与特定的父亲 React 元素相关联,比如 <form> 。React 并不会去匹配父元素不同但 key 相同的子元素。(React 并没有惯用的支持对在不重新创建元素的情况下让宿主实例在不同的父元素之间移动。)

   给 key 赋予什么值最好呢?最好的答案就是:什么时候你会说一个元素不会改变即使它在父元素中的顺序被改变? 例如,在我们的商品列表中,商品本身的 ID 是区别于其他商品的唯一标识,那么它就最适合作为 key 。

组件

  函数会返回 React 元素

function Form({ showMessage }) {
  let message = null;
  if (showMessage) {
    message = <p>I was just added here!</p>;
  }
  return (
    <dialog>
      {message}
      <input />
    </dialog>
  );
}

  这些函数被叫做组件。它们让我们可以打造自己的“工具箱”,例如按钮、头像、评论框等等。组件就像 React 的面包和黄油。

  组件接受一个参数 — 对象哈希。它包含“props”(“属性”的简称)。在这里 showMessage 就是一个 prop 。它们就像是具名参数一样。

纯净

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

function Button(props) {
  // 🔴 没有作用
  props.isActive = true;
}

   突变在 React 中不是惯用的。不过,局部的突变是绝对允许的

function FriendList({ friends }) {
  let items = [];
  for (let i = 0; i < friends.length; i++) {
    let friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    );
  }
  return <section>{items}</section>;
}

  在函数组件内部创建 items 时不管怎样改变它都行,只要这些突变发生在将其作为最后的渲染结果之前。所以并不需要重写你的代码来避免局部突变。  

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

递归

   如何在组件中使用组件:组件属于函数因此我们可以直接进行调用。

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

  然而,在 React 运行时中这并不是惯用的使用组件的方式。

  相反,使用组件惯用的方式与我们已经了解的机制相同 — 即 React 元素。这意味着不需要你直接调用组件函数,React 会在之后为你做这件事情:

// { type: Form, props: { showMessage: true } }
let reactElement = <Form showMessage={true} />;
ReactDOM.render(reactElement, domContainer);

  然后在 React 内部,你的组件会这样被调用:

// React 内部的某个地方
let type = reactElement.type; // Form
let props = reactElement.props; // { showMessage: true }
let result = type(props); // 无论 Form 会返回什么

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

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

  React并没有全局的注册机制 — 字面上当我们输入 <Form> 时代表着 Form 。如果 Form 在局部作用域中并不存在,就会发现一个 JavaScript 错误,就像平常使用错误的变量名称一样。

  因此,当元素类型是一个函数的时候 React 会调用你的组件,然后询问组件想要渲染什么元素。

  这个步骤会递归式地执行下去,更详细的描述在这里 。总的来说,它会像这样执行:

  • 你: ReactDOM.render(<App />, domContainer)
  • React: App ,你想要渲染什么?

    • App :我要渲染包含 <Content> 的 <Layout> 。
  • React: <Layout> ,你要渲染什么?

    • Layout :我要在 <div> 中渲染我的子元素。我的子元素是 <Content> 所以我猜它应该渲染到 <div> 中去。
  • React: <Content> ,你要渲染什么?

    • <Content> :我要在 <article> 中渲染一些文本和 <Footer> 。
  • React: <Footer> ,你要渲染什么?

    • <Footer> :我要渲染含有文本的 <footer> 。
  • React: 好的,让我们开始吧:
// 最终的 DOM 结构
<div>
  <article>
    Some text
    <footer>some more text</footer>
  </article>
</div>

  这就是为什么我们说协调是递归式的。当 React 遍历整个元素树时,可能会遇到元素的 type 是一个组件。React 会调用它然后继续沿着返回的 React 元素下行。最终我们会调用完所有的组件,然后 React 就会知道该如何改变宿主树。

  在之前已经讨论过的相同的协调准则,在这一样适用。如果在同一位置的 type 改变了(由索引和可选的 key 决定),React 会删除其中的宿主实例并将其重建。

控制反转

   到这里你可能会疑惑,为什么要写<Form /> 而不是 Form()
  这是因为 React 可以做的更好,如果他知道你的组件的话,如下;
// 🔴 React 并不知道 Layout 和 Article 的存在。
// 因为你在调用它们。
ReactDOM.render(
  Layout({ children: Article() }),
  domContainer
)

// ✅ React知道 Layout 和 Article 的存在。
// React 来调用它们。
ReactDOM.render(
  <Layout><Article /></Layout>,
  domContainer
)

  通过让 React 调用我们的组件,我们可以知道

    1、组件不仅仅只是函数。react 能够用 在树中 与组件本身紧密相连的 局部状态等特性来增强组件功能。

      2、组件类型参与协调。通过react 来调用你的组件,能够让它了解更多关于元素树的结构。

      3、React能够推迟协调。如果React控制调用你的组件,它可以让浏览器在组件调用之间做一些工作,这样渲染组件树是就不会阻塞主线程,想要手动实现这个过程不依赖React的话会十分困难。

    4、更好的可调式性。 

    5、惰性求值。

惰性求值

  JS 在调用函数时,参数往往在函数调用之前被执行。但是 React 相对纯净,如果我们知道它的结果不会在屏幕上出现,就完全没有必要去执行。

               

  但是如果需要提前返回呢?

        

  让 React 来决定何时以及是否调用组件。如果我们的的 Page 组件忽略自身的 children prop 且相反地渲染了 <h1>Please login</h1> ,React 不会尝试去调用 Comments 函数。

状态

  组件拥有局部状态,在树中每个组件所绑定的局部状态就是 React 对构建 UI 的特性。这些特性就收我们说的 Hooks,比如useState。

一致性

  我们需要在同步循环过程中对真实的宿主进行操作。这样我们才能保证用户不会看见半更新状态的 UI,浏览器也不会对用户不因看到的中间装药进行不必要的布局和样式的重新计算。

  因此,React 将工作氛围了 渲染阶段 和 提交阶段。

    渲染阶段:当 React 调用你的组件然后进行协调的阶段(在此阶段进行干涉是安全的,而且这个阶段未来会变成异步的)

    提交阶段:React 操作宿主树的阶段(这个阶段永远是同步的)

缓存

  当父组件想要通过 setState 更新状态时,React 会默认协调整个子树,因为 React 不知道父组件的更新会不会影响到其他子树。

  当树的深度和广度达到一定程度时,可以让React去缓存子树并且重用之前的渲染结果。

function Row({ item }) {
  // ...
}

export default React.memo(Row);

  在父组件 <Table> 中调用 setState 时如果 <Row> 中的 item 与先前渲染的结果是相同的,React 就会直接跳过协调的过程。可以使用 useMemo 获取缓存。  

原始模型

  任何在顶层的更新只会触发协调而不是局部更新那些受影响的组件。

  React 的设计原则十一就是他可以处理原始数据。React 渲染是 O(视图大小) 而不是 O(模型大小) ,并且你可以通过 windowing 显著地减少视图大小。

  注意,即使细粒度订阅和“反应式”系统也无法解决一些常见的性能问题。比如:渲染一颗很深的树,而不阻塞浏览器。

批量更新

  下面我们来看一个🌰

function Parent() {
  let [count, setCount] = useState(0);
  return (
    <div onClick={() => setCount(count + 1)}>
      Parent clicked {count} times
      <Child />
    </div>
  );
}

function Child() {
  let [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Child clicked {count} times
    </button>
  );
}

  当子组件的 onClick 首先被触发(同时它的 setState也触发了)。然后父组件的 onClick 中调用setState。

  如果 React 立即渲染组件以此响应 setState 调用,最终我们对渲染子组件两次。

     

  第一次 Child 的渲染是浪费的。并且我们也不会让React跳过 Child 的第二次渲染,因为 Parent 可能会传递不同的数据。

  这就是React会在组件哪多有事件触发完成后再进行批量更新的原因。

     

  组件内调用 setState 并不会立即执行重渲染。相反,React 会先触发所有的事件处理器,然后再触发一次重渲染以进行所谓的批量更新。 

调用树

   编程语言的运行时往往有调用栈 。当函数 a() 调用 b() ,b() 又调用 c() 时,在 JavaScript 引擎中会有像 [a, b, c] 这样的数据结构来“跟踪”当前的位置以及接下来要执行的代码。一旦 c 函数执行完毕,它的调用栈帧就消失了!因为它不再被需要了。我们返回到函数 b 中。当我们结束函数 a 的执行时,调用栈就被清空。

  当然,React 以 JavaScript 运行当然也遵循 JavaScript 的规则。但是我们可以想象在 React 内部有自己的调用栈用来记忆我们当前正在渲染的组件,例如 [App, Page, Layout, Article /* 此刻的位置 */] 。

  React 与通常意义上的编程语言进行时不同,因为它针对于渲染 UI 树,这些树需要保持“活性”,这样才能使我们与其进行交互。在第一次 ReactDOM.render() 出现之前,DOM 操作并不会执行。

   

 

转载自:https://overreacted.io/zh-hans/react-as-a-ui-runtime/#%E5%88%97%E8%A1%A8

posted @ 2022-01-17 16:12  山海南坪  阅读(36)  评论(0编辑  收藏  举报