基于 React18。
React 是 Facebook 开发的一个用于构建用户界面的 JavaScript 库。
React 的特点:
- 声明式编程:React 采用声明式编码,指的是我们只需要维护自己的状态 state,当状态改变时,React 会自动执行 render 函数,根据最新的状态去渲染 UI 界面。
命令式编程:命令机器如何去做事情(how),这样不管想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉机器想要的是什么(what),让机器想出如何去做(how)。
例如:想让一个数组里的数值翻倍。// 命令式编程:需要先遍历整个数组,取出每个元素,乘以二,然后把翻倍后的值放入新数组,一步一步都要告诉程序 var numbers = [1,2,3,4,5] var doubledNumbers = [] for(var i = 0; i < numbers.length; i++) { var newNumber = numbers[i] * 2 doubledNumbers.push (newNumber) } // 声明式编程:只需要告诉程序想要数组里的数值翻倍 var numbers = [1,2,3,4,5] var doubledNumbers = numbers.map(n => n * 2)
- 组件化开发:React 采用组件化开发,指的是将复杂的页面拆分成一个个小的组件,然后再将这些组件组合、嵌套在一起,最终形成应用程序。
- 一次学习,跨平台编写:例如在 React Native 中就可以使用 React 语法进行移动端开发。
使用 React:
在现有网站中添加 React:
- 在 HTML 中准备一个容器。
- 添加依赖。
- 引入 react 核心库:react 包中包含了 react web 和 react native 所共同拥有的核心代码。引入之后就会有一个全局对象 React。
- 引入
react-dom
扩展库:react-dom
包针对 web 和 native 所做的事情不同。在 web 端,react-dom
包最终会将 JSX 渲染成真实 DOM,显示在浏览器中;在 native 端,react-dom
包最终会将 JSX 渲染成原生的空控件(例如: Android 中的 Button,IOS 中的 UIButton 等)。因此将react-dom
包单独拆分了出来。引入之后就会有一个全局对象 ReactDOM。 - 引入 babel:用于将 JSX 转为 JS,以便在浏览器中运行。
- 编写 React 代码。
<body>
<!-- 准备一个容器 -->
<div id="root"></div>
<!-- 添加依赖 -->
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<!-- 编写 React 代码:React18 之前 -->
<script type="text/babel" > // type="text/babel" 是为了让 babel 知道它需要解析来解析这段 JSX
ReactDOM.render(<h1>Hello,React</h1> , document.getElementById('root')) // 将要显示的内容渲染到根元素中。第一个参数是一个 HTML 元素或 React 组件,第二个参数是要渲染到的页面中的元素
</script>
<!-- 编写 React 代码:React18 之后 -->
<script type="text/babel" > // type="text/babel" 是为了让 babel 知道它需要解析来解析这段 JSX
const root = ReactDOM.createRoot(document.getElementById('root')) // 创建一个根元素。参数是要渲染到的页面中的元素
root.render(<h1>Hello,React</h1> ) // 将要显示的内容渲染到根元素中。参数是一个 HTML 元素或 React 组件
</script>
</body>
基于 React 脚手架 create-react-app
创建一个新的 React 项目:
传统的脚手架:指的是在搭建建筑物时临时搭建出来的一个框架。
编程中的脚手架:是一种可以帮助开发者快速生成项目的工程化结构的工具。脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷。
- 全局安装
create-react-app
脚手架(简称cra
):npm i create-react-app -g
。安装成功后,可以通过
create-react-app --version
可以查看create-react-app
的版本。 - 切换到想要创建项目的目录,创建基于 React 脚手架的项目:
create-react-app react-demo
。项目名称不能包含大写字母。
- 进入项目所在的目录:
cd react-demo
。 - 开启项目并且会自动打开浏览器:
npm start
。npm run start
:会打开一个本地的服务器,并且将项目托管在这个服务器中,就可以在本地浏览网站。
npm run build
:在 build 文件夹内生成应用的优化版本,build 文件夹中的文件可以被用于部署到生产环境。
npm run eject
:create-react-app
脚手架是基于 Webpack 开发的,但是在生成的项目目录中并没有看到 Webpack 相关的内容,这是因为create-react-app
脚手架将 Webpack 相关的配置隐藏了。通过npm run eject
可以将 Webpack 配置信息弹出。这个操作是不可逆的,要谨慎使用。
使用 create-react-app
架手架创建出来的项目默认目录结构:
node_modules
:项目依赖的第三方包。- public:网站的静态资源文件和 项目的 HTML 入口文件。
- src:网站的源代码,是基于 ES6 标准的 React 代码。
基于 ES6 标准的 React 代码是不能直接被浏览器执行的,因此 src 和 public 文件夹中所有的代码和资源都要转化成可以被浏览器识别的基于 ES5 标准的 JavaScript 代码,这些转化后的代码将会被打包统一放置在一个新生成的 build 文件夹中。
- build:可以被浏览器执行的 ES5 标准的 JavaScript 代码。
.gitignore
:git 的忽略文件。package.json
:记录项目的基础信息、依赖包版本信息等。package-lock.json
:记录node_modules
目录下所有包的名称、版本号、下载地址以及当前包又依赖了哪些包等。README.md
:项目的说明文档。
React 的渲染机制:
- 在
render()
方法中会返回 JSX,JSX 会被转换成React.createElement()
函数的调用。 React.createElement()
会创建出来一个 ReactElement(React 元素),ReactElement 是一个虚拟节点,其实就是一个 JS 对象。- React 利用 ReactElement 组成一个 JS 的对象树,也就是虚拟 Dom。
- 最终,React 根据虚拟 Dom 渲染出真实 Dom (
document.createElement()
),呈现在页面中。
// JSX
const element = (
<div className='header'>
<div>标题</div>
</div>
)
// JSX 本质上是 React.createElement()
const element = React.createElement(
"div",
{
className: "header"
},
React.createElement("div", null, "标题")
)
// 虚拟 DOM 节点
console.log(element)
// 真实 DOM 节点
<div class='header'>
<div>标题</div>
</div>
const dom = document.getElementsByClassName('header')[0]
console.log(dom)
React 的更新机制:
React 中的虚拟 Dom:
虚拟 Dom:ReactElement 组成的对象树,其实就是由一堆 JS 对象来模拟真实的 Dom 结构。
React 中虚拟 Dom 的作用:
- 通过虚拟 Dom 和 Diff 算法可以实现视图的高效更新:
- 在 React 中,所有的 Dom 构造都是通过虚拟 Dom 进行,而不总是直接操作页面真实 Dom,虚拟 Dom 是内存数据,性能极高。
- 每当数据变化时,React 都会重新构建整个完整的 Dom 树,然后 React 将当前整个 Dom 树和上一次的 Dom 树进行对比,得到 Dom 结构的区别,然后仅仅将需要变化的部分进行实际的浏览器 DOMDom更新,而且 React 能够批量处理虚拟 Dom 的刷新。
在 Web 开发中,总需要将变化的数据实时反应到 UI 上,这时就需要对 Dom 进行操作,而复杂或频繁的 Dom 操作通常是性能瓶颈产生的原因。
- 通过虚拟 Dom 可以实现跨平台:虚拟 Dom 本质上就是一堆 JS 对象,因此React 既可以将它渲染成 Web 端的 HTML 元素,也可以通过桥接的方式将它渲染成移动端(IOS、Android)的控件。
- 虚拟 Dom 帮助开发者从命令式编程转到了声明式编程的模式:UI 以一种虚拟化的方式存在于内存中,开发者只需要维护状态,然后告诉 React 希望 UI 是什么状态,React 就能够确保 Dom 和状态是匹配的。
React 中的 Diff 算法:
传统的 Diff 算法是通过循环递归的方式对 Dom 节点进行依次的比对,效率较低,其复杂度可达到 O(n^3),其中 n 代表的节点个数。那么如果有 1000 个节点,则一次 Diff 就将进行 10 亿次比较。
React 中使用三个层级策略对传统的 Diff 算法进行了优化,使复杂度从 O(n^3) 降到了 O(n)。
React 是基于组件构建的,首先可以将整个虚拟 DOM 树抽象为 React 组件树,每一个组件又是由一棵更小的组件树构成,依次类推。将 React diff 策略应用比较这棵组件树,若其中某个组件需要进行比较,将这个组件看成一棵更小的组件树,继续用 React diff 策略比较这棵更小的组件树,依次类推,直到层次遍历完所有的需要比较的组件。
tree diff:
树层级的比较:React 只对同层级的节点进行比较,不考虑节点的跨层级比较,如果发生跨级操作,React 不复用已有节点。
React 只会对相同颜色框内的节点进行比较,如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。
B 节点发生了跨层级的移动操作,React 并不会复用 B 节点及其子节点,而是会直接删除 A 节点下的 B 节点及其子节点,然后再在 C 节点下创建新的 B 节点及其子节点。
component diff:
组件的比较:如果是同类型的组件,就按照 React diff 策略进行比较;如果不是同类型的组件,React 不会进行比较,而是直接替换这个组件及其子孙节点。
对于同类型组件, React 通过让开发人员自定义
shouldComponentUpdate()
方法来进行比较优化,减少组件不必要的比较。如果没有自定义,shouldComponentUpdate()
方法默认返回 true,每次组件发生数据(state & props)变化时,都会进行比较。
虽然组件 C 和组件 H 结构相似,但类型不同,React 不会进行比较,会直接删除组件 C,创建组件 H。
element diff:
单个节点的比较:可以通过对同一层级的单个节点设置唯一的 key 来复用已有的节点。
对于同一层级的单个节点,有三种操作,分别是移动、创建和删除,针对是否使用 key 标识可分为两种情况:
- 对于不使用 key 标识的情况:
React 对同一层级的新旧单个节点进行对比,发现新集合中的 B 不等于旧集合中的 A,于是删除 A,创建 B,依此类推,直到删除 D,创建 C。这会使得相同的节点不能复用,出现频繁的删除和创建操作,从而影响性能。 - 对于使用 key 标识的情况:
React 会对新集合进行遍历,通过唯一的 key 来判断旧集合中是否存在相同的节点,如果没有则创建,如果有则判断是否需要进行移动操作。