最近觉得得开始找工作了,虽然现在好像都不咋招人了,但还是得复习一下react基本原理和相关技术栈,再着重看看部分源码,以前看的时候没有记录需要的时候找不着,这回得作个文档留存:
拉代码: https://github.com/facebook/react
demo: https://github.com/bajiu/blog_code/tree/master/react
项目结构
目录结构
react 采用的使monorepo的项目结构,这种结构下有先找ES-lint,因为一般为了代码的统一都会在最外层做统一的lint来保证代码一致性
这次先重点关注三个包:
react
: 包含核心API,如useState
、useEffect
等。react-dom
: 包含与浏览器相关的DOM操作代码。scheduler
: 负责调度更新的模块,使react
调度的核心。
然后是剩下的一些相关包:
shared
:包含一些公共的工具和类型定义,其他模块也会引用。react-reconciler
:这是 React 的核心调和器模块,管理 React 树的对比和更新。jest
和react-test-renderer
:包含测试框架 Jest 的配置和一些自定义匹配器。允许在不依赖 DOM 的情况下渲染和测试 React 组件。react-noop-renderer
: 主要用于测试和调试 Fiber 架构。eslint-plugin-react-hooks
:这是React Hooks
的ESLint
插件,用于检查Hooks
的使用规则,如依赖项数组和调用顺序。fixtures
:包含一些小型应用和示例代码,用于演示和测试不同的React
功能。scripts
:包含构建、测试和发布React
的脚本和配置。flow
:React
使用了Flow
类型检查,该文件夹包含Flow
配置和类型声明。
接下来的阅读顺序大概如下:
- 从
react/src/React.js
的API
出发,逐步分析createElement
和Component
等关键实现。 - 在
react-dom/src/ReactDOM.js
中,研究React
是如何将虚拟DOM
转换为真实 DOM。 - 通过
scheduler
理解任务调度,并在react-reconciler
了解Fiber
树的调度和更新机制。
在这之前,我们先了解一下JSX。
React核心API和hooks
先介绍一下核心api,这里就直接列出来了,之后分析源码时候再挨个来
React.createElement(type, [props], [...children])
:创建一个虚拟 DOM.React.Component
和React.PureComponent
:定义一个 React 组件的基类。React.PureComponent
是Component
的子类,自动实现shouldComponentUpdate
,通过浅比较来决定是否重新渲染。React.Fragment
:包装多个元素,避免创建额外的 DOM 节点。ReactDOM.render(element, container)
:将 React 元素渲染到实际的 DOM 中(一般用于项目的根节点)。ReactDOM.createPortal(child, container)
:将子元素渲染到指定的 DOM 节点,而不在父组件的 DOM 层级内。实现模态框等浮层。React.useState(initialState)
:在函数组件中定义一个状态变量。React.useEffect(callback, [dependencies])
:useEffect(callback, [dependencies])React.useMemo(callback, [dependencies])
:优化性能,避免不必要的计算。useMemo
会记住一个计算的值,只有当依赖项发生变化时才会重新计算,否则会返回上次的计算结果。React.useCallback(callback, [dependencies])
React.useRef(initialValue)
React.useLayoutEffect(callback, [dependencies])
:与useEffect
类似,useLayoutEffect
在 DOM 更新之后、浏览器绘制之前同步执行,这意味着它会阻塞浏览器的渲染。dan同时能保证布局更新发生后立即执行,而不会让浏览器在布局更新时渲染不一致的内容。React.createContext(defaultValue)
:创建上下文对象,允许组件通过上下文共享状态而不必手动传递 props。React.useContext(Context)
:在函数组件中订阅上下文变化,避免多层传递属性。React.useReducer(reducer, initialArg, init?)
:管理那些状态变更逻辑复杂、状态依赖其他状态值的场景。React.useImperativeHandle(ref, createHandle, [dependencies])
React.memo(Component, [areEqual])
:是用于组件的优化,通过缓存组件的渲染结果来避免不必要的重新渲染。React.forwardRef((props, ref) => <Component {...props} ref={ref} />)
:使函数组件支持ref
属性,便于父组件访问子组件的 DOM 节点或实例。React.lazy(() => import(‘./MyComponent’))
:实现组件的动态加载,按需加载组件以优化性能。React.Suspense
:与React.lazy
一起使用,设置异步加载组件的占位内容。React.StrictMode
: 在开发模式下启用严格模式(没用过)。React.useId
: 差点儿拉下了这个,生成唯一ID的
JSX详解
JSX(JavaScript XML)
是 React
提供的一种语法扩展,允许我们在 JavaScript
中直接编写类似 HTML
的标签,从而更加直观地描述 UI
结构。JSX
最终会被编译成纯 JavaScript
代码,并通过 React.createElement
函数生成虚拟 DOM
。
1.基本语法
jsx的基本语法类似于HTML,但实际上是在js中嵌入 XML 语法。
比如这段代码
const element = <h1>Hello, world!</h1>;
会被Babel编译成
const element = React.createElement('h1', null, 'Hello, world!');
也可以嵌入表达式:
const name = "summer889";
const element = <h1>Hello, {name}!</h1>;
// 编译后
const element = React.createElement('h1', null, `Hello, ${name}!`);
对于属性的处理与HTML类似,但是属性名需要使用驼峰命名
子元素嵌套、条件、数组、内联样式的渲染:
// 子元素嵌套
const element0 = (
<div>
<h1>Hello, React!</h1>
<p>This is a paragraph.</p>
</div>
);
// 编译结果
const element0 = React.createElement(
'div',
null,
React.createElement('h1', null, 'Hello, World!'),
React.createElement('p', null, 'run p tag')
);
// 条件渲染
const isLoggedIn = true;
const element1 = (
<div>
{isLoggedIn ? <h1>Welcome back!</h1> : <h1>Please sign in.</h1>}
</div>
);
// 编译结果
const element1 = React.createElement(
'div',
null,
isLoggedIn
? React.createElement('h1', null, 'Welcome back!')
: React.createElement('h1', null, 'Please sign in.')
);
// 数组渲染
const items = ['Apple', 'Banana', 'Cherry'];
const element2 = (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
// 内联样式渲染
const divStyle = { color: 'blue', fontSize: '20px' };
const element3 = <div style={divStyle}>Styled Text</div>;
// 编译结果
const element3 = React.createElement('div', { style: { color: 'blue', fontSize: '20px' } }, 'Styled Text');
使用 JSX
可以减少 React.createElement
的调用次数,使代码更接近 HTML
结构。
工作原理
要把大象装冰箱,总共分三步:
1.JSX 编译:React
使用 Babel
将 JSX
编译为 React.createElement
调用。
2.生成虚拟 DOM:React.createElement
会返回一个 JavaScript
对象,描述这个元素的类型、属性和子元素。这个对象就是虚拟 DOM(VNode)
。
3.渲染到真实 DOM:React
会根据虚拟 DOM
的变化,通过调和(reconciliation)算法
将更改反映到实际的 DOM
中。
Babel编译JSX主要也分为三个部分:
- 类型识别:判断
JSX
标签的类型,例如HTML
标签(h1、div)或 React 组件。 - 属性解析:将
JSX
属性转换为对象形式传递给React.createElement
。 - 子元素解析:嵌套的
JSX
标签会递归转换为React.createElement
的嵌套调用。
使用Babel插件 @babel/preset-react
Babel 使用 @babel/preset-react
这个预设(preset)来识别和转换 JSX。,先安装:
yarn add @babel/core @babel/cli @babel/preset-react -D
配置 .babelrc
{
"presets": ["@babel/preset-react"]
}
JSX 到 React.createElement
的转换
const element = (
<div className="container">
<h1>Hello, World!</h1>
<p>This is summmer889.</p>
</div>
);
添加编译: "jsx": "babel --presets @babel/preset-react src/jsx/example1.jsx --out-dir dist"
然后我们就可以在dist/example1.js
中看到编译后的代码:
const element = /*#__PURE__*/React.createElement("div", {
className: "container"
}, /*#__PURE__*/React.createElement("h1", null, "Hello, World!"), /*#__PURE__*/React.createElement("p", null, "JSX \u8F6C\u6362"));
console.log(element);
我们可以看到汉字被转成了unicode
编码,react
里面应该有在做CharCodeAt
方法。然后加载进界面就能看到了。
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSX with Babel</title>
<!-- 引入 React 和 ReactDOM -->
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
</head>
<body>
<div id="root"></div> <!-- React 渲染容器 -->
<script src="../../dist/example1.js"></script> <!-- 编译后的 JavaScript -->
</body>
</html>
然后就能水灵灵看到界面了:
并且在日志里可以看到如下信息:
JSX编译选项
Babel 在 @babel/preset-react
中提供了以下几个选项,可以用来控制 JSX 的编译行为:
pragma
:指定用于创建元素的函数,默认为React.createElement
。pragmaFrag
:指定用于创建 Fragment 的函数,默认为React.Fragment
。throwIfNamespace
:当使用XML
命名空间(例如 SVG)时,是否抛出错误。默认为true
。runtime
:决定运行时编译方式,有两个可选值:"classic"
:传统的编译方式,将JSX
转换为React.createElement
。"automatic"
:自动引入运行时,避免手动导入React
。
React 17 引入了自动化运行时,即可以使用
runtime: "automatic"
的新编译模式。在这个模式下,不再需要手动导入React
。
手动调用babel进行JSX转换
当然,我们也可以在项目中通过手动的方式进行对JSX的转换
const babel = require('@babel/core');
const jsxCode = `<h1>Hello, world!</h1>`;
const result = babel.transform(jsxCode, {
presets: ['@babel/preset-react']
});
console.log(result.code);
通过 Babel 插件扩展 JSX
Babel 插件可以帮助我们进一步扩展 JSX
的功能。例如,可以添加自定义属性、样式前缀或自动优化等。
在每个 JSX
元素上自动添加 data-summer
属性,首先我们要创建一个自定义 Babel 插件:
module.exports = function ({ types: t }) {
return {
visitor: {
JSXOpeningElement(path) {
path.node.attributes.push(
t.jsxAttribute(t.jsxIdentifier('data-summer'), t.stringLiteral('auto-id'))
);
}
}
};
};
- Babel 提供的
t
(即 Babel 的AST
工具)来生成JSX
属性。 - 在
JSXOpeningElement
访问器中,给每个JSX
元素添加data-summer="auto-id"
属性。 - 当 Babel 处理
JSX
时,每个JSX
元素都会自动添加data-summer
属性。
在.babelrc
文件中添加插件的配置
{
"presets": ["@babel/preset-react"],
"plugins": [
"./src/babelplugin"
]
}
然后继续刚才的操作,就有了
要是有前来学习的小伙伴儿迷茫的,具体babel
相关的看webpack
那部分去~