react 与 DOM


一、ReactElement

React 元素(ReactElement)是构成 React 应用的最小单元。组件是由元素构成的。他们最终构成了渲染在浏览器中的 DOM。

例如:

const element = <h1 id="hello">Hello, world</h1>
// 上面的代码经过编译后会生成下面的代码
React.createElement("h1", {
  id: "hello"
}, "Hello, world");

执行 React.createElement 函数,会返回类似于下面的一个 JavaScript 对象,这个对象就是我们所说的 React 元素:

const element = {
  type: 'h1',
  props: {
    id: 'hello',
    children: 'hello world'
  }
}

可见,React 元素其实就是一个普通的 JavaScript 对象。


二、React Fiber

React Fiber 是 React 16 中新的协调引擎,主要目的是使 Virtual DOM 可以进行增量式渲染。这是一个正在进行的项目,在完成之前可能会经历重大的重构。

React Fiber 架构

React Fiber 是对 React 核心算法的不断重新实现,能够将渲染工作分成块并将其分散到多个帧上来实现增量渲染,从而提高其在动画、布局和手势等领域的适用性。

React Fiber 增量渲染主要体现在——使 React 能够利用调度完成以下操作:

  • 暂停工作,稍后再回来。
  • 为不同类型的工作分配优先级。
  • 重用之前完成的工作。
  • 如果不再需要,则中止工作。

React Fiber 的实现背景:
计算机通常跟踪程序执行的方式是使用调用堆栈。当一个函数被执行时,一个新的栈帧被添加到栈中。问题是:在处理 UI 时,如果一次执行过多的工作,可能会导致动画掉帧并且看起来不连贯。更重要的是,如果这些工作被更新的更新所取代,其中一些工作可能是不必要的。

于是,较新的浏览器(和 React Native)专门对这个问题提共的 2 个 API,Fiber 的实现得益于此:

  • requestIdleCallback:安排在空闲期间调用低优先级函数。
  • requestAnimationFrame:安排在下一个动画帧调用高优先级函数。

问题是,为了使用这些 API,您需要一种将渲染工作分解为增量单元的方法。如果你只依赖调用堆栈,它会一直工作直到堆栈为空。也就是说,我们需要 “可以自定义调用堆栈的行为以优化渲染 UI” 的功能,需要 “可以随意中断调用堆栈并手动操作堆栈帧” 的功能,于是,Fiber 应运而生了,Fiber 是堆栈的重新实现,专门用于 React 组件。可以将单个 Fiber 视为一个虚拟堆栈帧。

React Fiber 是一个 JavaScript 对象,其中包含有关组件、其输入和输出的信息。下面是 Fiber 对象的部分属性:

  • type:节点类型(元素、文本、组件)(具体的类型)。
  • props:节点属性。
  • stateNode:节点 DOM 对象或者组件实例对象。它是 Fiber 对应的真实 DOM 节点。
  • tag:节点标记(对具体类型的分类 hostRoot || hostComponent || classComponent || functionComponent)。
  • effects:数组, 存储需要更改的 fiber 对象。
  • effectTag:当前 Fiber 要被执行的操作(新增, 删除, 修改)。
  • parent:当前 Fiber 的父级 Fiber。
  • child:当前 Fiber 的子级 Fiber。
  • sibling:当前 Fiber 的下一个兄弟 Fiber。
  • alternate:Fiber 备份 Fiber比对时使用。

三、React 与 HTML 之间的属性差异

  • React DOM 为非受控组件提供了 defaultValue 属性 和 defaultChecked 属性,HTML 中没有这些属性。
  • React DOM 用 className 属性代替 HTML 的 class 属性。
  • React DOM 用 dangerouslySetInnerHTML 属性代替 HTML 的 innerHTML 属性。
  • React DOM 用 htmlFor 属性代替 HTML 的 for 属性。

1、value 和 defaultValue

组件的默认值,按照是否受控 可分为以下两种:

  • 受控组件的默认值:React 支持 value 属性——你可以使用它为组件设置 value。此处的受控组件包括的 HTML 标签:<input>、<select> 和 <textarea> 。
  • 非受控组件的默认值:React 提供了 defaultValue 属性,HTML 中没有该属性——你可以使用它来设置组件第一次挂载时的 value。

【拓展】React 中,什么是受控组件?
在 HTML 中,表单元素(如<input>、<select> 和 <textarea> )通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。我们可以把两者结合起来,使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。

2、checked 和 defaultChecked

组件的选中态,按照是否受控 可分为以下两种:

  • 受控组件的选中态:React 支持 checked 属性——你可以使用它来设置组件是否被选中。此处的受控组件包括:对于类型为 checkbox 或 radio 的 <input> 的 HTML 标签。
  • 非受控组件的选中态:React 提供了 defaultChecked 属性,HTML 中没有该属性——你可以使用它来设置组件首次挂载时是否被选中。

3、用 className 代替 class

一般,在 react 中给 dom 元素添加 class 时,要用 className 属性。

【拓展】
在 react 中想要动态添加 className 时,通常会使用 classnames 这个库
在 React 中使用 Web Components 时(不常见,不推荐),可以使用 HTML 的 class 属性,而不必使用 React 的 className 属性。

4、dangerouslySetInnerHTML 代替 innerHTML

React 中,使用 dangerouslySetInnerHTML 属性替换 HTML 的 innerHTML 属性。以避免跨站脚本(XSS)的攻击。

dangerouslySetInnerHTML 的使用:

function createMarkup() {
  return {__html: 'First &middot; Second'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}

5、htmlFor 代替 for

React 元素中使用了 htmlFor 属性来代替 HTML 的 for 属性

【拓展】HTML 的 for 属性指的是标签 <label> 和 <output> 的属性。

  • 当用于 <label> 元素时,它表示此标签描述的表单元素。
  • 当用于 <output> 元素时,它允许元素之间的显式关系,这些元素表示输出中使用的值。

四、React 使用 ref 操作真实 DOM

React 内部会自动的通过虚拟 DOM 匹配好你想要的渲染输出,所以不希望开发者去直接操作真实 DOM。若不可避免的需要操作真实 DOM 的话,就用 Refs 来操作真实 DOM 吧。
Refs 允许我们访问真实的 DOM 节点,或在 render 方法中创建的 React 元素。一般会使用 Refs 来操作真实 DOM 元素。

通常,在第一次渲染期间,尚未创建 DOM 节点,所以现在阅读它们还为时过早。提交更新 DOM 后,React 立即将它们设置为相应的 DOM 节点。所以最好在事件处理程序中访问 refs。

1、refs 与 state 的对比

refsstate
useRef(initialValue)
返回{ current: initialValue }
useState(initialValue)
返回状态变量和状态设置函数的当前值 ( [value, setValue])
更改时不会触发重新渲染。更改时触发重新渲染。
可变的——您可以 current 在渲染过程之外修改和更新 的值。“不可变”——您必须使用状态设置功能来修改状态变量以排队重新渲染。
您不应该 current 在渲染期间读取(或写入)该值。您可以随时读取状态。但是,每个渲染都有自己的状态快照,不会改变。

在渲染期间,读取 ref 对象里的某个属性,会导致代码不可靠。此时建议您改用 state。

2、class 组件中使用 Ref

(1)、class 组件里通过 createRef API 来创建使用 ref

先在 constructor 构造函数里用 createRef API 来创建一个 ref 实例对象。然后,在 dom 中将这个 ref 实例对象作为 ref 属性的值传入。

例如:

class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }

  componentDidMount() {
    this.textInput.current.focusTextInput();
  }

  render() {
    return (
      <CustomTextInput ref={this.textInput} />
    );
  }
}

(2)、class 组件里的回调 Refs

回调 Refs 能助你更精细地控制何时 refs 被设置和解除。

不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保证 refs 一定是最新的。

例如:

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null;

    this.setTextInputRef = element => {
      this.textInput = element;
    };

    this.focusTextInput = () => {
      // 使用原生 DOM API 使 text 输入框获得焦点
      if (this.textInput) this.textInput.focus();
    };
  }

  componentDidMount() {
    // 组件挂载后,让文本框自动获得焦点
    this.focusTextInput();
  }

  render() {
    // 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
    // 实例上(比如 this.textInput)
    return (
      <div>
        <input type="text" ref={this.setTextInputRef} />
        <input type="button" value="Focus the text input" onClick={this.focusTextInput} />
      </div>
    );
  }
}

3、function 组件中使用 Ref

(1)、function 组件里通过 useRef hook 使用 ref

先用 useRef 创建一个 ref 实例对象,然后,在 dom 中将这个 ref 实例对象作为 ref 属性的值传入。之后就可以在 useEffect 中或函数中使用到该 ref 了。

例如:

import { useRef } from "react";

function CustomTextInput(props) {
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input type="text" ref={textInput} />
      <input type="button" value="Focus the text input" onClick={handleClick} />
    </div>
  );
}

(2)、function 组件里的回调 Refs

先定义并实现一个函数,该函数接收一个参数——使用这个函数的 ref 所对应的 dom 元素。然后,在 dom 中将这个函数作为 ref 属性的值传入。

例如:

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  render() {
    return (
      <CustomTextInput
        inputRef={el => this.inputElement = el}
      />
    );
  }
}

在上面的例子中,Parent 把它的 refs 回调函数当作 inputRef props 传递给了 CustomTextInput,而且 CustomTextInput 把相同的函数作为特殊的 ref 属性传递给了 <input>。结果是,在 Parent 中的 this.inputElement 会被设置为与 CustomTextInput 中的 input 元素相对应的 DOM 节点。

4、forwardRef(Ref 转发)

Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。

forwardRef:表示 Ref 转发,这是一个可选特性,其允许某些组件接收 ref,并将其向下传递给子组件。

例如:下面实现一个需求,点击按钮自动聚焦输入框,并改变输入框的默认值

①、使用 ref 实现

定义 MyCont 组件:

import React from "react";

const MyCont = ((props) => {
  const ref = React.useRef()
  React.useEffect(() => {
    props.setRef(ref)
  }, [])
  return (
    <div>
      <input defaultValue="111" ref={ref}></input>
    </div>
  )
})

export default MyCont;

在页面中引用:

import React from 'react'
import MyCont from '@/pages/components/myCont'

const Test = () => {
  let myRef = null
  const handleClick = () => {
    myRef.current.focus()
    myRef.current.value="2222"
  }
  return (
    <div>
      <button type="button" onClick={handleClick}>获取焦点</button>
      <MyCont setRef={(el) => myRef = el}/>
    </div>
  )
}

export default Test;

②、使用 forwardRef 实现

定义 MyInput 组件:

import React from "react";

const MyInput = React.forwardRef((props, ref)=>{
  return (
    <div>
      <input defaultValue="111" ref={ref}></input>
    </div>
  )
})

export default MyInput;

在页面中引用:

import React from 'react';
import MyInput from '@/pages/components/myInput';

const Test = () => {
  // 创建一个 ref 实例对象:const myRef = React.createRef() 或
  const myRef = React.useRef()
  return (
    <div>
      <button type="button" onClick={() => myRef.current.value="333"}>获取焦点</button>
      <MyInput ref={myRef}/>
    </div>
  )
}

export default Test;

这样,使用 MyInput 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样。

以下是对上述代码发生情况的逐步解释:

  • 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  • 我们通过指定 ref 为 JSX 属性,将其向下传递给 <MyInput ref={ref}/>
  • React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  • 我们向下转发该 ref 参数到 ,将其指定为 JSX 属性。
  • 当 ref 挂载完成,ref.current 将指向 DOM 节点。


五、样式——style 和 className

1、动态添加 style

在 JSX 语法的大花括号里传入或实现一个定义了 style 的对象。不同的 style 属性之间用逗号隔开。

style 语法:

<div style={{
  styleOne: 三目运算符(?:)动态添加不同的样式属性值,
  styleTwo: 和运算符(&&)动态添加某个样式属性值,
  styleThree: 样式属性值(静态的)
}}>...</div>

例如:

<div style={{
  display: index === this.state.currentIndex ? "block" : "none",
  background: id === this.state.currentId && "lightyellow",
  fontSize: "21px"
}}>...</div>

2、动态添加 className

在 JSX 语法的大花括号里传入字符串。不同的 className 之间用空格隔开。

className 语法:

<div className={`<样式类名(静态的)> <和运算符(&&)动态添加某个样式类名> <三目运算符(?:)动态添加不同的样式类名>`}>...</div>

例如:

<div className={`container ${id === this.state.currentId && "highlight"} ${index === this.state.currentIndex ? "active" : "normal"}`}>...</div>

六、React 事件

1、React 事件与 DOM 事件

React 事件
DOM 事件

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

例如:

// HTML 中
<button onclick="activateLasers()">
  Activate Lasers
</button>

// React 的 JSX 中
<button onClick={activateLasers}>
  Activate Lasers
</button>

2、React 在内置事件中传递额外的自定义参数

在内置事件除自带参数外额外传递自定义参数


七、操作 DOM 的核心 API

React 操作 DOM 主要用到的包是 react-dom。react-dom 包提供了用户操作 DOM 的方法。并且,react-dom 包还分别为客户端和服务器提供了 react-dom/clientreact-dom/server 模块。

react-dom 包的核心 API:

  • render() 方法:将一个组件挂载到一个真实 dom 节点上。
    • 已弃用。自 React 18 起,官方决定使用了 createRoot 函数代替 render 函数。
    • 接收 2 个参数: 一个组件 和 一个真实 dom 节点。
    • 第一次执行 render 函数时,将替换 root 节点内的所有dom。
  • createRoot() 方法:React 18 新增 API。代替了原来的 render 函数。与 render 函数的功能一样。
  • hydrate() 方法:与 render 相同,此方法在服务端渲染时调用。
    • 已弃用。自 React 18 起,官方决定使用了 hydrateRoot 函数代替 hydrate 函数。
    • 此方法将对 ReactDomServer 渲染的 html 进行事件绑定。
  • hydrateRoot () 方法:React 18 新增 API。代替了原来的 hydrate 函数。与 hydrate 函数的功能一样。
  • unmountComponentAtNode() 方法:从 DOM 中卸载组件,会将其 事件处理器 和 state 一并清除。
  • findDOMNode() 方法:传入一个组件实例,一般是 this, 用于获取组件对应的真实 DOM 节点。
    • 在大多数情况下,不推荐使用该方法,因为它会破坏组件的抽象结构。你可以直接用 ref 获取 DOM 节点。严格模式下该方法已弃用。
    • 建议在 ref 无法实现的情况下使用此方式获取 DOM 。
  • createPortal() 方法:可以将组件渲染到父组件之外的任何节点中。多用于 Modal 弹框, Message 等组件中。
    • 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。
    • 第二个参数(container)是一个 DOM 元素。
  • flushSync() 方法:强制更新 DOM。
    • 通常批量更新(下文的虚拟 DOM 中会讲)是安全的,但有时需要在状态更改后立即更新 DOM,比如,React 的 class 组件不再支持某些场景下的同步状态更新。此时就需要使用 flushSync() 函数来手动规避 “批量更新”,从而强制地实现 DOM 的立即更新。

1、createRoot() 方法

createRoot 函数

创建一个 React 根container并返回根。根可用于将 React 元素渲染到 DOM 中。

例如:

// 创建root节点
const root = ReactDOM.createRoot(container);

// 初始化:将组建渲染到root
root.render(<App tab="home" />);

// 更新组件时,无需再关注container的引用了,因为它已经提前绑定在root上
root.render(<App tab="profile" />);

// 卸载组件
root.unmount();

2、hydrateRoot () 方法

hydrateRoot 函数

hydrateRoot() 函数的第二个参数需要传入初始化 jsx。无需调用 root.render()。这是因为初始 client 端渲染是特殊的,需要与 server 端的节点树匹配。

例如:

const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// 注意这里不用调用 root.render()
// 首次 hydration 之后如果想更新 root,可以调用 render
root.render(<App tab="profile" />);

八、React 虚拟 DOM

Virtual DOM 的诞生背景:真实的 DOM 操作代价昂贵,操作频繁还会引起页面卡顿影响用户体验。很多 DOM API 的读写都涉及页面布局的 重绘(repaint)和 回流(reflow),这会更加的耗费性能。而虚拟 DOM 就是为了减少不必要的 DOM API 调用,从而提高浏览器的性能。

React 官网之 Virtual DOM 及内核

虚拟 DOM 是一个 JavaScript 对象,是对真实 DOM 的抽象。

React Virtual DOM 由两部分构成(上文均有介绍):ReactElementReact Fiber

1、React 虚拟 DOM 的核心

React Virtual DOM 的核心有 2 个步骤:

  • diff 算法(对比差异):状态变更时,记录新树和旧树的差异——React 的 render() 方法,会创建一棵由 React 元素组成的树,也就是 Virtual DOM Tree。在下一次 state 或 props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来进行 diff,得到一个 Patch,这样就可以找到变化了的 DOM 节点,只对变化的部分进行 DOM 更新,而不是重新渲染整个 DOM 树。这个比较的过程就是 diff 算法。diff 算法可以将时间复杂度降为 O(n)。
  • batching(批量更新):最后把差异更新到真正的 DOM 中——就是将多次比较的结果合并后一次性更新到页面,从而有效地减少页面渲染的次数,提高渲染效率。

2、React 虚拟 DOM 的工作流程

React Virtual DOM 的工作流程:

  • 初始化渲染,调用函数组件、或 class 组件的 render 方法,将 JSX 代码编译成 ReactELement 对象,它描述当前组件内容的数据结构。
  • 根据生产的 ReactELement 对象构建 Fiber tree,它包含了组件 schedule、reconciler、render 所需的相关信息。
  • 一旦有状态变化,触发更新,Scheduler 在接收到更新后,根据任务的优先级高低来进行调度,决定要执行的任务是什么。
  • 接下来的工作交给 Reconciler 处理,Reconciler 通过对比找出变化了的 Virtual DOM ,为其打上代表增/删/更新的标记,当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer。
  • Renderer 根据 Reconciler 为 Virtual DOM 打的标记,同步执行对应的 DOM 更新操作。

当 React 元素改变,React DOM 会将元素和它的子元素与它们之前的状态进行比较,只会更新变化的部分来使 DOM 达到预期的状态。

  • 如果节点名字变了,则会将旧的节点卸载,并在需要的位置创建一个全新的节点。
  • 如果节点的名字没变,只是内容(data)变了,此时会走对应的生命周期来更新数据,不会卸载旧的节点和创建新的节点。

例如:
在这里插入图片描述
将 A 节点从 R 节点下移动到 B 节点下,react virtual DOM 会将 A 节点直接 unMount 掉,然后在 B 节点下重新 mount 一个 A 节点,而不是直接迁移。

新增一个 X 节点,此时分两种情况:

  • 有 key,则直接在对应的位置 mount X 节点即可。
  • 无 key,则会先在对应的位置 mount X 节点,然后需要将 X 节点之后的元素依次先 unMount 掉原来的,再 mount 新的。

此时就体现了设置 key 的重要性:通过设置 key 可以对 element diff 进行优化——开发者在 map 一个 Array 时主动设置 key 的重要性。这个 key,尽量不要采用数组的下标,因为在增删数组里的元素后,原来的元素对应的下标是会动态变化的。key 一定要是一成不变的。

3、React 虚拟 DOM 的优势

Virtual DOM 的优势:

  • 为函数式的 UI 编程方式打开了大门,我们不需要再去考虑具体 DOM 的操作,框架已经替我们做了,我们就可以用更加声明式的方式书写代码。
  • 减少页面渲染的次数,提高渲染效率。
  • 提供了更好的跨平台的能力,因为 virtual DOM 是以 JavaScript 对象为基础而不依赖具体的平台环境,因此可以适用于其他的平台,如 node、weex、native 等。

【拓展】虚拟 DOM 与 真实 DOM 对比
框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化真实 DOM 操作更快,框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。


【参考文章】
你不知道的 React Virtual DOM
React 虚拟 DOM 实现原理






【参考文章】
React 官网

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值