React Hooks 是 React 16.8 版本引入的一种新的特性,它允许在不编写 class 的情况下使用 state 和其他 React 特性。我的理解主要集中在以下几个方面:
函数组件的状态管理:Hooks 提供了
useState
这样的钩子函数,使得我们可以在函数组件中使用状态(state),而不需要将组件转换为 class 组件。这简化了代码,并使组件的逻辑更易于理解和维护。副作用的处理:使用
useEffect
可以替代类组件中的生命周期方法(如componentDidMount
、componentDidUpdate
、componentWillUnmount
),用于处理组件副作用,如数据获取、订阅管理等。自定义 Hooks:Hooks 提供了编写自定义钩子函数的能力,使得我们可以将组件之间共享的逻辑提取到可复用的函数中。这样可以有效地提高代码的重用性和可维护性。
更加函数式和声明式:使用 Hooks 后,组件中的逻辑更加集中和声明式,而不是被分散在生命周期方法中。这使得 React 组件的编写更加自然和直观。
性能优化:Hooks 在内部实现上有助于 React 的性能优化,可以避免 class 组件中因为生命周期方法调用顺序的问题导致的 bug,并且更容易优化渲染性能。
总体来说,React Hooks 是一个强大且灵活的工具,使得 React 组件的编写变得更加简洁、清晰和可维护,同时也推动了函数式编程风格在 React 生态中的普及和应用。
单向数据绑定
优点:
- 简单可控: 单向数据流通常更易于理解和调试,因为数据的流向是单一的,从数据源到UI展示,降低了复杂性。
- 性能优化: 由于数据流向明确,框架可以更容易地进行性能优化,例如实现虚拟DOM等技术。
- 预测性: 因为数据流向是单向的,使得状态变化更加可预测,减少了意外的副作用。
缺点:
- 繁琐的手动更新: 当数据发生变化时,UI无法自动更新,需要手动编写更新UI的逻辑。
- 输入交互复杂: 对于需要用户输入的表单等场景,需要手动处理用户输入并更新数据状态。
双向数据绑定
优点:
- 便捷的表单处理: 在处理表单等需要用户输入的场景中,双向绑定能够使得数据和UI之间的同步更加便捷。
- 简化代码: 可以减少手动编写的数据更新逻辑,减少重复代码。
缺点:
- 复杂性增加: 双向数据绑定引入了数据的双向流动,增加了系统的复杂性,使得数据流向不再那么明确,可能导致难以追踪的bug。
- 潜在的性能问题: 数据频繁变化时,双向绑定可能导致大量不必要的UI更新,影响性能。
- 组件耦合:使用 Context 可以使得很深的组件树中的任何组件都可以访问到 Context 中的数据,这可能会增加组件间的耦合。当你需要修改数据或者行为时,可能需要修改很多不相关的组件。
性能问题:当一个 Context 值改变时,所有消费这个 Context 的组件都会重新渲染。如果使用 Context 的组件很多,那么可能会导致性能问题。
复用性降低:如果一个组件依赖于 Context,那么这个组件就不能在没有这个 Context 的环境中运行,这降低了组件的复用性。
旨在创建不可变(Immutable)的数据结构。其原理是通过在数据变化时不修改原始数据,而是创建一个新的数据副本来实现。这意味着一旦数据被创建,就无法再被修改。Immutable 数据结构保证了数据的不可变性,从而简化了数据处理和状态管理。
可以通过给每个组件添加一个唯一的标识符,然后在事件处理函数中通过 event.target 获取点击的元素,进而判断点击的元素属于哪个组件。
React 测试中一种只渲染组件本身而不渲染其子组件的技术。在浅层渲染中,React 将组件渲染成虚拟 DOM,但不会递归渲染其子组件,而是用占位符代替子组件。这使得测试更加专注于被测试组件本身的行为,而不必关心其子组件的状态和行为。
使用回调函数的方式可以确保在 state 更新完成并且组件重新渲染后执行相应的逻辑,从而避免了因为获取到旧的 state 而导致的 bug。
在 React 中,
Component
和PureComponent
都是用来创建组件的基类,但它们在更新机制上有一些区别。
Component:
Component
是 React 中所有组件的基类。- 当组件的
state
或props
发生变化时,无论是否实际发生了变化,React 都会重新渲染组件及其所有子组件。- 这意味着,即使组件的
state
或props
没有变化,也会触发不必要的重新渲染,可能导致性能损失。PureComponent:
PureComponent
是Component
的一个子类,它在Component
的基础上实现了一种浅比较(shallow comparison)的机制。- 当组件的
props
或state
发生变化时,PureComponent
会先对新旧props
和state
进行浅比较。只有当新旧props
或state
不相等时,React 才会触发重新渲染;否则,不会重新渲染组件及其子组件。- 这种机制可以减少不必要的重新渲染,提高性能。
总的来说,
PureComponent
在更新机制上通过浅比较来避免不必要的重新渲染,从而提高性能。但需要注意的是,由于浅比较只能比较对象的引用值,如果props
或state
中包含复杂的数据结构(如数组或对象),可能会导致浅比较失效,从而影响性能。因此,在使用PureComponent
时,需要确保props
和state
中的数据结构是不可变的(immutable)或者通过深比较来确保比较的准确性。
React声明组件的方法包括函数式组件、类组件、React Hooks、React.PureComponent和React.memo。
Reconciliation算法的核心思想是尽量复用已有的DOM节点,减少创建新节点和删除旧节点的操作。React会根据节点的类型、属性等信息来判断节点是否可以复用,从而实现高效的更新。这种机制使得React在处理大规模数据变化时仍能保持较高的性能表现。
挂载阶段:
constructor()
:组件的构造函数,在组件被创建时调用,用于初始化状态和绑定事件处理函数。static getDerivedStateFromProps(props, state)
:在组件接收到新的 props 或者在组件初始化时调用,返回一个对象来更新状态,用于替代旧的 componentWillReceiveProps
。render()
:根据组件的状态和属性渲染组件的内容。componentDidMount()
:组件挂载后调用,可以进行 DOM 操作或数据请求等副作用。更新阶段:
static getDerivedStateFromProps(props, state)
:在组件接收到新的 props 时调用,返回一个对象来更新状态。shouldComponentUpdate(nextProps, nextState)
:在组件接收到新的 props 或 state 时调用,用于控制组件是否重新渲染。render()
:根据新的状态和属性重新渲染组件。getSnapshotBeforeUpdate(prevProps, prevState)
:在最近一次渲染输出(提交到 DOM 上)之前调用,可以在此保存当前 DOM 的一些信息。componentDidUpdate(prevProps, prevState, snapshot)
:组件更新完成后调用,可以进行 DOM 操作或处理更新前的信息。卸载阶段:
componentWillUnmount()
:组件卸载前调用,用于清理定时器、取消订阅等操作。
Promise.all
是一个用于并行执行多个 Promise 的方法,它接收一个 Promise 数组作为参数,并返回一个新的 Promise,该 Promise 在数组中的所有 Promise 都成功解析(resolve)时才会解析,如果其中任何一个 Promise 被拒绝(reject)了,则整个Promise.all
也会被拒绝。
render
函数里面可以编写JSX
,转化成createElement
这种形式,用于生成虚拟DOM
,最终转化成真实DOM
在 React
中,类组件只要执行了 setState
方法,就一定会触发 render
函数执行,函数组件使用useState
更改状态不一定导致重新render
组件的 props
改变了,不一定触发 render
函数的执行,但是如果 props
的值来自于父组件或者祖先组件的 state
在这种情况下,父组件或者祖先组件的 state
发生了改变,就会导致子组件的重新渲染
所以,一旦执行了setState
就会执行render
方法,useState
会判断当前值有无发生改变确定是否执行render
方法,一旦父组件发生渲染,子组件也会渲染
React
提供统一的事件对象,抹平了浏览器的兼容性差异React
通过顶层监听的形式,通过事件委托的方式来统一管理所有的事件,可以在事件上区分事件优先级,优化用户体验React
在合成事件上对于16
版本和17
版本的合成事件有很大不同,我们也会简单聊聊区别。
16
版本先执行原生事件,当冒泡到document
时,统一执行合成事件,17
版本在原生事件执行前先执行合成事件捕获阶段,原生事件执行完毕执行冒泡阶段的合成事件,通过根节点来管理所有的事件原生的阻止事件流会阻断合成事件的执行,合成事件阻止后也会影响到后续的原生执行
Immutable 是指数据不可变,一旦创建之后就不能被修改。在 JavaScript 中,原始类型(如字符串、数字)是不可变的,而对象和数组是可变的。
性能优化:使用 Immutable 数据结构可以帮助 React 进行浅比较,提高组件更新时的性能。通过比较引用而不是深层次比较对象内容,可以减少不必要的重新渲染。
简化状态管理:不可变数据结构可以减少状态管理的复杂性。在 Redux 或者其他状态管理工具中,配合 Immutable 数据结构可以更容易地追踪状态的变化,并且可以避免直接修改状态而导致的副作用。
防止意外修改:不可变数据结构可以减少由于在多处修改同一数据而引发的 bug。在 React 组件中,通过使用 Immutable 数据结构,可以更容易地追踪数据的变化,从而减少意外的状态修改。
在 React 项目中,可以通过以下方式应用 Immutable 数据结构:
JSX 编写阶段:
JSX 转换阶段:
React.createElement 函数调用:
虚拟 DOM 构建阶段:
虚拟 DOM 渲染阶段:
真实 DOM 更新阶段:
在 React 项目中,采用错误边界、全局错误处理、组件生命周期方法、try...catch 等方式结合起来,可以有效地捕获和处理错误,提高应用的稳定性和用户体验。同时,及时记录和监控错误信息也是保障项目质量的重要手段。
错误边界(Error Boundaries):
服务器端渲染阶段:
HTML 响应生成阶段:
客户端激活阶段:
diff 算法的三级策略
dif算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:
策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)
这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常"暴力”的,即两课树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。
策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)在组件比对的过程中:
如果组件是同一类型则进行树比对;
如果不是则直接放入补丁中。
只要父组件类型不同,就会被重新渲染,这也就是为什么 shoudcomponentupdate、Purecomponent及 Reactmemo 可以提高性能的原因。
策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)
元素比对主要发生在同层级中,通过标记节点操作生成补丁,节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。
可中断性:React Fiber 将整个组件更新过程分解为多个可中断的小任务单元,称为 fiber。这些 fiber 可以根据优先级和时间片来进行调度,使得 React 能够在更新过程中灵活地中断、暂停和恢复任务,从而提高用户界面的响应性。
优先级调度:每个 fiber 都有自己的优先级,React Fiber 根据优先级动态调整任务的执行顺序,确保高优先级任务能够优先执行,从而实现对更新过程的控制。
它通过将一个大的更新过程分割成多个小步骤,每个小步骤执行一部分工作,然后暂停并继续执行其他工作,从而实现了更新过程的可控。
react15把页面渲染分为了两个阶段:1.协调/调和阶段(reconclie):在这个阶段 React 会更新数据生成新的 虚拟Dom树,然后通过Diff算法,从该节点/组件(使用useState声明该state的节点,而非调用setXXX的节点)开始往下递归(该节点&所有子节点)虚拟Dom树找出需要更新的节点,放到更新队列中去,得到新的更新队列。该过程不可中断。
2.渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到Dom上。
简单来说,一个负责找,另一个负责改。
react16
react16引入了fiber的概念,此时的页面渲染可以分为三个阶段:1.调度阶段(schedule):调度任务的优先级,高优先级的先进入协调阶段。
2.协调阶段(reconclie):找出变化的节点,从不可中断的递归变成了可中断的循环过程,内部采用了fiber架构,react16就是把之前的stack reconlie(直到执行栈被执行空才会停止)重构成了fiber reconclie(可中断)。
3.渲染阶段(commit):将变更一次性更新到真实Dom上。
简单来说,我们的Vnode(虚拟节点)中只存有子节点(childrenNode)的信息,如果我们打断了,是找不到它的父亲和兄弟节点的,所以就又得重头开始找。
上面我们将react15和react16页面渲染的大致流程进行了讲解,我们会发现,最直观的差异就是:寻找变化节点的过程由不可中断变成了可以中断。
为什么要这样做呢?
浏览器js引擎和页面渲染引擎是在同一个渲染线程之内,两者是互斥关系。
当前有新的数据更新时候,我们需要递归虚拟Dom树,找出变动节点,如果其中dom节点过多,那么这个过程时间消耗的会很长,并且无法中断,所以会导致其他事件影响滞后,造成卡顿。
react15及之前版本:当有节点发生变动,会递归对比虚拟dom,找出变动节点,之后同步更新他们。这种遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。
————————————————
fiber:直译 纤维,意在指比线程Thread更细小的执行粒度。
从整体上看,fiber就是把原本不可中断的协调过程碎片化,化整为零,每次执行完一个小任务,都会让出线程,给其他任务执行的机会。
从本质上看,fiber实际上是一种数据结构:特殊的链表。每个节点都是一个 fiberNode。一个 fiberNode包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性(解决了react15Vnode中只有子节点信息,信息不足导致无法中断的问题)。
react 会先把 vdom 转换成 fiber,再去进行 reconcile,这样就是可打断的了。
{
type: any, // 对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
key: null | string, // 唯一标识符
stateNode: any, // 保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
child: Fiber | null, // 大儿子
sibling: Fiber | null, // 下一个兄弟
return: Fiber | null, // 父节点
tag: WorkTag, // 定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
nextEffect: Fiber | null, // 指向下一个节点的指针
alternate: Fiber | null,
updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列
memoizedState: any, // 用于创建输出的fiber状态,记录内部state对象的属性
pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
memoizedProps: any, // 在前一次渲染期间用于创建输出的props
// ……}
setState
方法既可以是同步的,也可以是异步的,具体取决于使用 setState
的地方和上下文。
同步更新:当在生命周期方法(如 componentDidMount
、componentDidUpdate
)和原生事件处理函数中调用 setState
时,通常会同步更新状态。这是因为 React 可以立即执行状态更新并重新渲染组件。
批量异步更新:在大多数情况下,setState
是异步的,即在事件处理函数、异步回调或 setTimeout
等代码块中调用 setState
时,React 会将多个 setState
调用合并成一个批量更新操作,以提高性能。这意味着连续的 setState
调用不会立即触发组件的重新渲染,而是在当前代码块执行完毕后,React 根据一定策略进行更新。
React 使用了一种称为事件代理)的机制来处理组件中的事件。事件代理是一种将事件处理函数绑定到父元素而不是每个子元素的技术,它利用事件冒泡机制将事件从子元素传递到父元素进行处理。
事件绑定:在 React 组件中,可以通过在 JSX 中添加事件监听器来绑定事件处理函数
事件冒泡:当用户在子元素上触发事件时,该事件会按照从子元素到父元素的顺序依次向上冒泡。也就是说,事件会首先在触发事件的子元素上被触发,然后沿着组件层级向上冒泡到更高层的父元素。
事件委派:React 在组件树的根节点上绑定事件监听器,这个根节点可以是最外层的 DOM 元素,也可以是某个组件的容器元素。当事件冒泡至根节点时,React 会根据事件类型找到对应的事件处理函数,并执行相应的处理逻辑。这种方式避免了为每个子元素都绑定事件监听器,提高了性能和内存利用率。
react用单链表来严格保证hooks的顺序。Hooks 依赖于 React 内部的调用顺序来确定每个 Hook 的对应关系,如果在循环、条件语句或嵌套函数中调用 Hooks,可能会导致 Hooks 调用顺序发生变化,从而破坏 React 内部的依赖关系。这种情况下,React 可能无法正确地管理状态,造成组件的不稳定行为。
违反规则一致性:
状态管理混乱:
潜在的问题与错误:
代码可读性和可维护性下降:
常见的 React Hook 闭包陷阱包括:
解决 React Hook 的闭包陷阩可以采取以下方案:
Concurrent Mode(并发模式):React 18 引入了 Concurrent Mode,这是一个可选的特性,可以帮助优化应用程序的性能和用户体验。通过 Concurrent Mode,React 可以更好地处理大型应用程序中的渲染优先级,并在不阻塞用户界面的情况下提高性能。
新的渲染器 Renderer:React 18 引入了新的渲染器,允许开发人员更好地控制渲染过程,并进行更细粒度的优化。
Automatic Batching(自动批处理):React 18 改进了更新批处理机制,使得在某些情况下不再需要手动进行批处理操作,从而提高了性能。
新的树形结构 Reconciler:React 18 中引入了新的树形结构 Reconciler,可以更好地处理组件树的更新和渲染,提高了整体的性能和效率。
新的事件系统:React 18 带来了全新的事件系统,使得事件处理更加灵活和高效。开发人员可以更容易地管理和优化事件处理逻辑。
新的Hooks:React 19引入了多个新的Hooks,如
use
、useOptimistic
、useFormState
和useFormStatus
。这些Hooks简化了数据获取和表单处理的复杂性。例如,useHook
允许开发者从Promise或Context中读取值,提供了一种新的“挂起”API,使得在等待异步操作完成时,UI可以更加友好地展示加载状态。而useOptimisticHook
使得开发者可以在等待服务器响应时,对UI进行乐观更新,提高了用户体验。编译器优化:React编译器的引入标志着React在性能优化方面迈出了重要一步。编译器能够通过对JS规则和React规则的建模,安全地编译代码,从而减少不必要的渲染。
数据获取和表单处理的改进:React 19通过新的Hooks和API,如
use
和form action
,提供了更直观和高效的方式来处理数据获取和表单提交。服务器组件(RSC):经过多年的开发,React引入了服务器组件,这是一个重大改进,使得开发者不再需要借助Next.js等其他工具来实现服务器端渲染。
动作(Action):动作也将彻底改变我们与DOM元素的交互方式。
文档元数据:这是一个备受期待的改进,让我们能够用更少的代码实现更多功能。
资源加载:这将使资源能够在后台加载,从而改善应用程序的加载时间和用户体验。React 19中,图像和其他文件将在用户浏览当前页面时在后台加载,这个改进应该有助于改善页面加载时间并减少等待时间。
Web Components:React代码现在可以让我们集成Web Components,这是一个特别令人着迷的特性。
ref
是 React 提供的一个属性,用于在组件中访问子组件或 DOM 元素。每次渲染都会被重新创建。useRef
是一个 Hook 函数,用于在函数组件中创建可变的 ref 对象。会因为重新渲染而改变。forwardRef
是一个高阶组件,用于在函数组件中向子组件传递 ref。语法和模板:
数据绑定:
v-model
指令可以实现父子组件之间的数据双向绑定,简化了数据传递和状态管理。状态管理:
组件通信:
生命周期:
beforeCreate
、created
、beforeMount
、mounted
等。虚拟 DOM:
Taro 利用 Babel、React、Webpack 等技术,通过封装原生 API 和提供不同的 Polyfill 实现了多端适配,同时也支持复杂的样式表达和自动化导入组件等特性。Taro 的实现原理主要是通过源码转换、组件映射、API 封装和样式处理等方式,实现了多端统一开发的目标,
iconfont
等字体文件来代替。性能优化:
路由管理:
样式处理:
react-router
等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面
路由的本质就是页面的URL
发生改变时,页面的显示结果可以根据URL
的变化而变化,但是页面不会刷新
主要是提供了一些组件:
Hash 模式:
#
符号,例如 http://www.example.com/#/about
。#
符号可能会被认为不够美观。History 模式:
#
符号,例如 http://www.example.com/about
。pushState
和 replaceState
方法,通过改变 URL 而不引起页面刷新来实现页面路由的切换。们的共同思想包括:
集中化的状态管理:Redux 和 Vuex 都提倡将应用程序的状态集中管理,以便于统一管理和跟踪状态的变化。
单向数据流:两者都采用了单向数据流的思想,即数据的流动是单向的,便于状态的追踪和调试。
纯函数:Redux 和 Vuex 都鼓励使用纯函数来处理状态的变化,使得状态的变化更加可预测和可控。
开发者工具:两者都提供了开发者工具,便于开发者监控状态变化、调试和时间旅行等功能。
它们的区别主要在于以下几点:
框架依赖:Redux 是一个独立的状态管理库,可以与任何框架结合使用,而 Vuex 是专门为 Vue.js 设计的状态管理库。
概念和API:Redux 使用了 action、reducer、store 等概念和 API,而 Vuex 使用了 mutation、action、state、getter 等不同的概念和 API。
语法差异:Redux 的语法比较简洁,但在某些情况下需要编写较多的模板代码;而 Vuex 在 Vue.js 中能够更好地利用框架的特性,提供了更加简洁的语法。
生态和社区:Redux 有着庞大的生态和社区支持,而 Vuex 则更加贴近 Vue.js 生态,能够更好地与 Vue.js 整合。
使用 Context API 或 Redux:
使用组件缓存技术:
使用 React Router 中的路由状态:
利用 LocalStorage 或 SessionStorage:
利用第三方库:
react-keep-alive
这样的第三方库来实现状态自动保存的功能。这些库一般提供了简单易用的方式来实现组件状态的缓存和自动保存。useEffect:
useEffect
是 React 提供的标准 Hook,它会在浏览器渲染完成后异步执行副作用操作。这意味着它不会阻塞浏览器渲染,并且会在浏览器布局和绘制更新之后执行。useLayoutEffect:
useLayoutEffect
也是一个 React 提供的 Hook,与 useEffect
类似,但它会在所有 DOM 变更之后同步执行副作用操作,但在浏览器布局和绘制之前执行。useLayoutEffect
是在 DOM 更新之后、页面重新布局之前同步执行的,因此如果在其中执行大量计算或操作,可能会导致性能问题,甚至造成页面卡顿。因此,主要区别在于执行时机和对页面布局的影响。一般情况下,推荐优先使用 useEffect
来处理副作用操作,只有在确实需要在布局更新之前立即执行代码时才考虑使用 useLayoutEffect
。
动态导入:
import()
函数,可以在需要时动态加载模块,而不是在应用初始化时一次性加载所有模块。const LazyComponent = React.lazy(() => import('./LazyComponent'))
,这样在需要时才会加载 LazyComponent 组件。React.lazy 和 Suspense:
React.lazy
和 Suspense
这两个懒加载相关的 API。React.lazy
函数接受一个函数,该函数应该返回一个动态 import 的 Promise,以实现组件的懒加载。Suspense
组件可以在等待懒加载组件加载完成时显示 loading 界面,以提升用户体验。在最外层包裹 Suspense
组件,并设置 fallback
属性为加载中时显示的组件,即可实现懒加载时的 loading 效果。代码分割:
React 渲染性能优化的三个方向,其实也适用于其他软件开发领域,这三个方向分别是:
通过 JavaScript 对象来模拟真实 DOM 的层次结构和状态,从而实现了对 DOM 的抽象和操作。虚拟 DOM 的实现可以简单概括为以下几个步骤:
创建虚拟 DOM 对象:
渲染虚拟 DOM:
对比更新:
应用更新:
通过虚拟 DOM,React 实现了一种高效的页面更新机制。在组件状态发生变化时,React 首先更新虚拟 DOM,然后通过 diff 算法找出实际需要更新的部分,最终只更新实际 DOM 中发生变化的部分,避免了不必要的 DOM 操作,提高了页面渲染的效率。
setState通过一个队列机制来实现 state 更新。当执行 setState 的时候,会将需要更新的 state 合并后放入状态队列,而不会立刻更新 this.state。队列机制可以高效的批量更新 state,如果不通过 setState 而直接修改 this.state,那么该 state 将不会被放入状态队列中,当下次调用 setState 并对状态队列进行合并时,将会忽略之前被直接修改的 state,而造成无法预知的错误。