React
这是我参与第五届青训营伴学笔记创作活动的第2天,学习了React的一些思路。
一、React的历史与应用
应用场景
-
前端应用开发
-
移动原生应用开发
-
结合Electron,进行桌面应用开发
-
React Three Fiber可以进行3D开发
-
WebGL
关键历史因素:组合式组件思想的产生
二、React的设计思路
1. 原生JS写UI编程痛点
-
状态更新,UI不会自动更新,需要手动地调用DOM进行更新
-
欠缺基本的代码层面的封装和隔离,代码层面没有组件化。
- UI一般很明显分为几块,也就是应当分为几个组件
-
UI之间的数据依赖关系,需要手动维护,但一般这种数据依赖关系较多,因为UI编程的特点是每一步可能影响多个部分。如果依赖链路长,则会遇到“Callback Hell”(回调地狱)。
2. 响应式与转换式
-
转换式系统:给定输入求解输出
- 典型例子:编译器,数值计算
-
响应式系统:具有及时响应性(Responsive)、恢复性(Resilient)、有弹性(Elastic)以及消息驱动(Message Driven)的系统称为响应式系统
-
一般特征:监听事件、消息驱动、异步
-
典型例子:监控系统、UI界面
-
响应式系统的一般过程:
具体到前端代码
我们希望的React
-
状态更新,UI自动更新
-
前端代码组件化,可复用,可封装
-
状态之间的互相依赖关系,只需声明即可
3. 组件化及状态归属问题
例:我们可以把手机购物界面抽象为左边的树形结构
这个树形结构并不是DOM树,而且DOM并不是JS内部的对象,是浏览器内部的,我们只能通过调用JavaScript的API去修改DOM。
组件:
-
组件是组件的组合/原子组件
-
组件内拥有状态,外部不可见,比如颜色内容
-
父组件可将状态传入组件内部,组件间可以通信,增加复用性
-
例如,上面的图片中,手机的当前价格会受到各种状态改变的影响,由于父组件可将状态传入子组件,因此,当前价格应当放在Root节点。
-
状态归属于两个节点向上寻找到最近的祖宗节点
-
但如果这样的数据很多,全都放在Root节点,好像组件化有没有那么的合理,我们会在后面解决这问题。
-
现在有了新的问题,当前价格在Root节点,可是改变当前价格的事件却不在Root节点,而是在型号选择和颜色选择中,那要怎么变呢?
-
首先,JS中函数是一等公民,可以传来传去
-
这样,先在Root节点中写一个改变当前价格的函数onChangeValue(),然后将这个函数向下传给能改变当前价格的节点。
-
每当型号或颜色发生改变的时候,通过onclick调用这个Root传下来的改变当前价格的函数onChangeValue(),这样就可以改变Root节点的当前价格了
那么,这是一个双向传递的过程吗
并不是,onclick只是执行了onChangeValue()这个函数,这个函数只是改变了Root节点的当前价格,并没有反向传状态。而onChangeValue()才是真的将Root节点的状态传到了iPhone 13 pro
思考:
-
React是单向数据流,还是双向数据流?
React是单向数据流,永远是父组件给子组件传东西,子组件不能向父组件传东西。但是子组件可以通过执行父组件传来的函数来改变父组件的状态。
-
如何解决状态不合理上升的问题?
使用React状态管理库(下面会讲)
-
组件的状态改变后,如何更新DOM?
通过更改前后虚拟DOM找出diff,然后进行重新渲染影响真实DOM(下面会讲)
组件设计
-
组件声明了状态和UI的映射
- 输入几个状态,返回UI
-
组件有Props/State两种状态
-
内部私有状态:State
-
从外部传入的状态:Props
-
-
“组件”可由其他组件拼装而成
那么组件代码会是什么样子呢?
-
组件内部拥有私有状态State
-
组件接受外部的Props状态提供复用性
-
根据当前的State/Props,返回一个UI
4. React生命周期
重点:挂载,重新渲染,结束挂载
三、React(hooks)的写法
-
useState()
:-
在React中,声明一个新状态需要调用useState函数,这个函数传入一个状态的初始值,返回一个数组,数组的第一个值是状态本身,第二个值是这个状态的setter
-
虽然可能绕了一点,但是原理也很简单,React封装了这个函数,这样React本身就可以内置一些状态刷新的方法。
-
import React, { useState } from 'react';
function Example(){
// 声明一个新状态count
const [const, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onclick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
-
副作用
:组件对外部内容产生影响,本质就是非纯函数。除了单纯的计算之外,还要做其他的一些事情。比如网络请求,更新DOM,localStorage存储数据等 -
useEffect()
:将副作用代码写在函数中,在组件mount的时候和依赖项被set的时候会执行。下面例子即为改变title
import React, { useState, useEffect } from 'react';
function Example(){
// 声明一个新状态count
const [const, setCount] = useState(0);
// 更新title就是一种副作用,因为title并不是组件内部内容
useEffect(() => {
// 通过调用浏览器API更新title
document.title = 'You clicked ${count} times';
});
return (
<div>
<p>You clicked {count} times</p>
<button onclick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect的第二个参数表示副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。
若没有第二个参数,useEffect只会在组件挂载的时候执行一次
function Title(argument) {
useEffect(() => {
document.title = ${argument.name};
}, [argument.name])
return <h1>{argument.name}</h1>
}
Hook
-
可以挂载到React生命周期上执行的函数。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
-
上面的useState()和useEffect()都是Hook
-
其他的Hook大多是useState和useEffect的封装
不要在循环、条件或嵌套函数中调用Hook
四、React的实现
React代码有一些问题
-
JSX(React中的JS语法扩展)不符合JS标准语法
-
返回的JSX发生改变时,如何更新DOM
-
State/Props更新时,要重新出发render函数(render函数就是组件本身)
我们来一个一个解决问题
- JSX(React中的JS语法扩展)不符合JS标准语法
由于HTML是一个树状结构,理所当然可以使用创建节点方式将React语法变为标准JS语法,底层就是这么转换的
- 返回的JSX发生改变时,如何更新DOM
想法:JSX更改后,内部算法找到更改前后的不同,并针对性更改,这样可以很快速
-
Virtual DOM(虚拟DOM)
-
Virtual DOM 是一种用于和真实DOM同步,而在JS内存中维护的一个对象,它具有和DOM类似的树状结构,并和DOM可以建立一一对应的关系。
-
它赋予了React声明式的API:您告诉 React希望让UI是什么状态,React 就确保DOM匹配该状态。这使您可以从属性操作、事件处理和手动DOM更新这些在构建应用程序时必要的操作中解放出来。 真实dom不是js中的对象,是浏览器内部维护的状态,只能dom接口修改。js可以设置与dom对应的对象。
-
-
指令式编程
- 手动告诉程序一步一步怎么做,定义处理事情的逻辑和方法
-
声明式编程(大部分前端框架)
- 定义处理事情的逻辑框架,不必列出明确步骤,程序自行摸索
-
响应式编程
- 声明式编程的一类,通过异步和数据流来处理问题
那么既然声明式这么好,为什么声明式不直接移植进浏览器呢?
- 浏览器只是应用平台,不能提供更高层的东西。如果声明式移植进浏览器,浏览器的自由度就降低了。它只能提供底层平台,不应限制开发的方式。
更新DOM的过程
-
首先在虚拟DOM中,每次组件状态更新,开始计算Diff不同点
-
计算Diff的过程实质上是递归的过程,因为父组件可以嵌套子组件,是一个树,树找不同就是要进行递归查询
- 当父组件状态改变时,所有的子组件包括子组件的子组件会递归发生重新render,这也是影响React性能的大问题,因为位置很高的父组件一旦变化,重新render的组件太多,影响渲染
-
Diff求出来之后,进行re-render Virtual DOM,这样就真的改变了DOM树,完成了UI动态渲染
怎么高效计算Diff呢?
render函数本身执行速度太慢,所以我们要让Diff计算足够快;其次我们要让DOM更新足够少,Diff足够少;因此这两种需求变成了一种矛盾,出现了一种权衡。
客观事实:
-
完美的最小Diff算法,需要O(n^3)的复杂度(太高了)
-
牺牲理论最小Diff,换取时间,得到了O(n)复杂度的算法:
- Heuristic(启发式) O(n) Algorithm
启发式算法
:(百度百科)一个基于直观或经验构造的算法,在可接受的花费(指计算时间和空间)下给出待解决组合优化问题每一个实例的一个可行解,该可行解与最优解的偏离程度一般不能被预计。
那么这个O(n)的算法具体怎么工作呢?
-
不同类型的元素 —— 替换
-
同类型的DOM元素 —— 更新
-
同类型的组件元素 —— 递归
仍存在的弊病:父组件变化,所有子组件及子组件的子组件都会重新渲染
五、React状态管理库
核心思想:将状态抽离到UI外部进行统一管理
既然放在UI组件内部容易影响性能,那我拿到外面去共享不就可以了
问题:降低了组件的复用性,因为这个组件会与外面的数据Store强耦合。如果开发组件库,不可能要依赖外部数据,一定是要封装好的。因此,一般用在业务代码如app,lib中少用
推荐的React状态管理库:
-
redux
-
xstate —— 基于状态机思想
-
mobx
-
recoil
状态机
-
具有当前状态,在收到外部事件后,会迁移到下一个状态
-
状态机的全称是有限状态自动机,自动两个字也是包含重要含义的。给定一个状态机,同时给定它的当前状态以及输入,那么输出状态时可以明确的运算出来的。
-
四大概念:
-
状态State:一个状态机至少要包含两个状态
-
事件Event:事件就是执行某个操作的触发条件或者口令
-
动作Action:事件发生以后要执行动作
-
变化Transition:从一个状态变化为另一个状态
-
哪些状态应该放在状态管理库里面?
- 这些状态被整个app拥有,例如用户的信息可能随处都会被用到,全局拥有的话只需要发起一次请求即可,省时省力
六、应用级框架科普
-
NEXT.js:硅谷明星创业公司Vercel(很厉害)的React开发框架,稳定,开发体验好,支持Unbundled Dev,SWC等,其同样有Serverless一键部署平台帮助开发者快速完成部署。口号是“Let's Make Web Faster”
-
MODERN.JS:字节跳动Web Infra团队研发的全栈开发框架,内置了很多开箱即用的能力与最佳实践,可以减少很多调研选择工具的时间。
-
Blitz:无API思想的全栈开发框架,开发过程中无需写API调用与CRUD逻辑,适合前后端紧密结合小团队项目。
七、引用
- 字节录播课 - React 的历史与应用
- 字节录播课 - React 的设计思路
- 字节录播课 - React (hooks)的写法与 React 实现
- 字节录播课 - React 状态管理库与应用级框架科普
- 知乎 - 什么是状态机?