写了这么多年的 React,知道通过写 JSX 来创建 React Components,并渲染到页面上。
const App = () => {
return (
<div>
App Component
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
并且还有一些其他功能,如状态管理和生命周期
const [state, setState] = useState();
useEffect(() => {
// Do something
})
React 相对于 Vue 需要记的东西并不多,更多的是理解,新手现在写 React 基本上一周之内都能上手,但总是会出现一些奇奇怪怪的问题,这就需要理解 React 背后的原理了。
JSX (JavaScript XML
)是一种语法扩展,这些代码浏览器并不能识别,需要通过 Babel
等工具将其转换为普通的 JavaScript
代码。
const App = () => {
return (
<div>
App Component
</div>
)
}
对于上面这个函数组件,在 React17 版本之前 JSX 被转换为 React.createElement
调用,这意味着在 jsx 文件的头部需要引入 React。
在 babel 的 Try it out 中,可以将 React Runtime 设置为 Classic。
const App = () => {
return /*#__PURE__*/React.createElement("div", null, "App Component");
};
而在 React 17 及以上版本中,引入了 react/jsx-runtime
这个模块,它用于优化 JSX 的编译过程。目的在于简化 JSX 的运行时需求,特别是对自动导入 React 的需求,以及创建 React 元素时的性能。
可以将 React Runtime 设置为 Automatic。
import { jsx as _jsx } from "react/jsx-runtime";
const App = () => {
return /*#__PURE__*/_jsx("div", {
children: "App Component"
});
};
虽然新版本引入了新的模块,但是对于输出两者都是相同的,都会创建 React 元素,且生成的元素本质上是相同的。
console.log(App());
的输出为
type 对应一个 HTML 元素类型,在这里是 div
key 和 ref 属性都是 null,因为没有赋值,并且都是特殊的属性,没有被包含到 props 中
props 中包含所有的 children
如果对于稍微多了一些标签和属性的组件来说
export default function App() {
return (
<div className="123">
<h1>App Component</h1>
<span>hello</span>
</div>
);
}
输出的结果为
可以看出会将所有的标签进行转换。
上图转换后就可以称为 elements tree
。
在 React 项目依赖中,除了 React 这个库还会有 React-dom 这个库,并且在脚手架工具中都会有这段代码
ReactDOM.render(
<App />,
document.getElementById('root')
);
它的意思是将 App 这个组件渲染到 root 这个节点下。
因为 React 支持跨平台开发,所以 React 库会将 JSX 转换为普通的对象,而想将这部分用在网页还是移动端,就可以选择不同的库。网页就是 react-dom,而移动端则是 React Native 那一套,还有其他 VR 等。
那么 render 这个方法接受的第一个参数 <App />
是什么,我们知道是个组件。
它的结构看上去像一个 React 元素,但是 type 是一个 function,实际上就是 App 组件。
Reconciliation 是 React 非常重要的一部分。它的主要作用是将 JSX 代码中描述的内容、样式等与其宿主环境(web、移动端等)协调一致。
在 React16 版本之前使用的是 stack reconciler,使用是堆栈这种数据结构,也就是堆栈协调器。
这是一种后进先出的方式,它是顺序的、而且很难并行或者设置优先级。这对于 UI、用户 interface 是一个很大的问题,因为用户输入事件具有固有的优先级意识。比如用户在输入框的输入内容,这个是需要立刻响应的;而一些通知类的消息,延迟一秒响应并没有多大的问题。
stack reconciler 所做的一切都是实时的,就像它只是在屏幕上进行更新一样。
这就数据结构的设置就导致了 stack reconciler 是不可用的。
在 React16 开始,Fibre Reconciler 就应运而生了,它与 Stack Reconciler 完全是两种东西。
使用的是一种特定的树结构,而不是传统意义上的链表结构。
在 Fiber 中,每个节点代表一个组件或一个元素。Fiber 节点包含了组件的状态和相关信息
类型(element type,组件类型)
子节点(child,只想该节点的第一个子节点)
兄弟节点(sibling,指向同一父节点的下一个兄弟节点)
父节点(return,指向其父节点)
更新状态(用于追踪状态和更新信息)
上图就代表了 fiber 节点树
Fibers 通常都是从 React elements 创建的
它们会共享 key 和 type 这个属性
React elements 每一次都会被重新创建,但是 Fibers 尽可能的重复使用
大多数的 Fibers 会在初始挂载时被创建
Fiber 的工作原理分为两个阶段:render 阶段和 commit 阶段
它在 render 阶段进行更新,并在屏幕外的内存中进行更新,当渲染的工作完成后,然后会进行 commit 阶段,将其提交到屏幕上,并将其放在浏览器中。
为了实现这两个阶段的方法,Fiber 需要维护两棵树,一棵是上图中的 current tree,另外一棵为备用树,叫做 workInProgress tree。
它会维护这两颗树来渲染和提交。
在 render 阶段,React 在幕后执行一些用户看不到的异步操作,React 可以确认任务的优先级,暂停某些工作甚至丢弃它等等。
每一个 Fiber 都是一个工作单元,React 在这个阶段会处理所有的 Fiber
在 render 阶段,会创建一个 workloop
,那么什么东西是 work?
state 改变
生命周期方法的调用
当有更新导致 DOM 变化时
在这个阶段会调用 beginWork()
和 completeWork()
等内部函数。
beginWork
接受三个参数 beginWork(current, workInProgress, lanes)
,它就是将两棵树进行比较,从顶部到底部(这不是递归而是一种 while 循环)。比较的时候会设置一堆的 flag 来标记该组件是否需要更新。每一个节点都会如此的进行比较。到了最底部的元素后会向上返回,再去循环兄弟节点,但是在转到兄弟节点之前会调用completeWork()
函数
completeWork()
与 beginWork()
的参数一致。它的作用是构造一个 elements tree,不是可以看到的那种,是在内存中的。直到到达最顶部元素
可以视 beginWork
为向下设置 flag,completeWork
为向上,组成一个正在进行的元素树。
当它到达顶部完成 render 阶段后,就到达了 commit 阶段。
在这个阶段会调用 commitWork()
函数,该阶段是同步的,不能被中断的
在 commit 阶段,workInProgress Tree
就是最新的 tree,需要将这个 tree 提交给宿主环境。为了实现这个效果会有一个 FiberRootNode
,它是一个隐藏的但是真正存在的 Fiber tree 的根节点。当应用启动时,在任何更新之前,这个跟节点有一个指针会指向 current tree,在workInProgress Tree
完成工作后,这个指针会指向 workInProgress tree
,这时最新的状态会立即在浏览器中显示,此时workInProgress Tree
转换为 Current Tree
,Current Tree
就会转换为 workInProgress Tree
。
至此这个 workloop
完成。
这些更新通常都是在屏幕外进行的,当有一个优先级更高的任务中断它时,你不会在屏幕看到任何变化。这样就比之前的 Stack Reconciler 更强。
上面说到的 beginWork
进行比较时,React 是如何判断是否需要进行更新。
在同一位置有两种不同元素的类型:React 会从头开始创建一棵新树,对于组件来说就是卸载之前的组件,重新挂载新的组件
<div>
<h1>Title1</h1>
</div>
<div>
<h2>Title1</h2>
</div>
同一位置有两个具有相同类型但是属性不同的元素:React 只会更新这些属性,实例保持不变并且维持 state
<div
className='first'
/>
<div
className='second'
/>
在一个列表的尾部添加一个元素:React 识别到前面的部分相同,在后面 insert 一个新的元素即可
<ul>
<li>John</li>
<li>Martin</li>
</ul>
<ul>
<li>John</li>
<li>Martin</li>
<li>Thomas</li>
</ul>
但是如果在列表的头部添加一个元素:React 比较后发现 li 中同一位置没有相同的元素,相当于重新生成一个新树,这就不是我们所期望的,如果有一万个列表,重新生成一万个元素,这些都是需要避免的部分。
所以就需要提供 key 这个属性,我们就可以告诉 React 哪些元素是相同的,哪些元素是新的。这也就是不能使用 index 作为 key 的原因。
<ul>
<li key='john'>John</li>
<li key='martin'>Martin</li>
</ul>
<ul>
<li key='thomas'>Thomas</li>
<li key='john'>John</li>
<li key='martin'>Martin</li>
</ul>
React Hooks 最重要的两条规则
只在 React 函数组件或自定义 Hooks 中调用 Hooks,不应在常规的函数、条件或循环中使用
确保在组件的顶层调用 Hooks,保持调用顺序一致,以便 React 能正确管理状态和副作用
可以在函数组件中使用 Hooks 来代替类组件,可以使用 useState
、useEffect
等
但是也会有很多对 Hooks 的疑问
为什么不能在循环或条件语句中使用 hooks
why isn't there a hook for this?
why isn't there a hook for that?
换句话说就是为什么 hooks 依赖调用顺序,最重要的一点是 hooks 都是关于数组的。
我们会使用 useState 创建很多的 state 状态,这些状态都被 React 通过数组存储,useState 会使用索引访问该数组。当函数组件被调用时,React 会使用初始化的 index 从 0 开始未每一个 useState 和 useEffect 调用顺序分配一个索引,并存储在数组中。每个 Hooks 的调用结果(如状态和副作用)会被存储在 Fiber 节点的对应位置,
如果我们在循环或条件内调用 useState,则会导致可能有些 hooks 没有被执行,index 就不会增加,Hooks 调用的顺序就会被打乱,导致 React 不能将状态与特定的 Hooks 关联起来,导致错误。
useState 是从 React 这个库中引入的,但是我们知道渲染是和 react-dom 这个库相关的,这是为什么
原因就是当 useState()被调用时,调用将被转发到一个称为 currentDispatcher 的一个特殊字段。在 react-dom 中使用了这个字段。
只要能搞清楚了 React 背后的原理在面对项目或面试时,一般都不会有太大的问题。