一、React 的理解
React 是一个用于构建用户界面的JS库。在代码开发过程中,我们将 UI 拆分成独立的小块,每块就是一个组件,通过组件之间的组合嵌套来构建页面。
React不是MVVM,最多算是Model -> View。官方给的公式就是UI = render(data),即数据驱动视图。
1. 设计思想: From 核心开发者
- 组件化开发:将
UI拆分成组件,通过组件之间的组合、复用来构建复杂UI。 - 单向数据流:组件间数据流动方向单一,所有状态改变都是可追溯的。
- 推崇函数式编程:React 设计时认为 UI 只是把数据通过映射关系变换成另一种形式的数据,相同的输入应该得到相同的输出
UI = f(data)。 - 数据的不可变性:数据一旦被创建就不能再修改了。
props不能修改,state要通过this.setState更新。 - 虚拟dom和diff算法:将真实 DOM 抽象为 js 对象,然后结合
diff算法来最小化 DOM 的更新。
2. react 工作原理?
- 虚拟
dom。 - 更新渲染 / 数据驱动视图。
3. react 解决了什么问题?相比原生JS的优势
- 性能提升:引入虚拟 DOM 的概念,结合
diff算法来最小化 DOM 的更新,批量更新机制减少页面重排重绘的次数,有利于提升性能。 - 数据驱动视图的设计让我们不用再直接写一大堆DOM操作了,只需要关注数据,这提高了工作效率。
- 可维护性:单向数据流使得所有状态的改变都是可追溯的,排查问题比较容易。
- 组件化开发有利于代码复用、提升开发效率。
React 有哪些问题吗?
- JSX 语法将 js、html 混搭,很难看;
- Diff 算法的改进点。
二、函数式编程
React 推崇的是函数式编程。函数式编程是一种以使用函数为主的开发风格,最重要的概念是纯函数,纯函数需要满足三个特征:
-
无副作用:副作用指的是函数执行时产生了外部可观察的变化。例如发送网络请求、操作DOM、打印数据等,副作用会影响代码可读性、可能导致一些难以排查的错误。
-
引用透明:指的是一个函数对于相同输入始终会得到相同的输出,不依赖外部环境。
-
数据不可变
immutable:数据一旦被创建就不能再更改。如果想要修改只能创建一个新对象,在新对象上修改。
1. react 为什么推崇函数式编程/纯函数的好处
- 更容易进行测试,因为结果只依赖输入。
- 结果可以缓存,因为相同输入会得到相同输出。
- 更容易调用,因为不用担心副作用的问题。
- 更容易维护和重构。
2. 函数式编程/纯函数的应用
- 函数式组件和 hooks
- 高阶函数:函数做参数或返回值是函数,像数组的 map、reduce、filter 等方法。
- redux 中的 reducer(纯函数)
- 柯里化:将一个多元函数转换成一个依次调用的单元函数。
三、生命周期(16.3)
每个组件都包含 “生命周期方法”,组件在进入和离开DOM时要经历一系列的生命周期方法。
1. 新旧版本对比
-
删除了三个生命周期
componentWillMount、componentWillUpdate、componentWillReceiveProps- v16 引入 Fiber 后协调过程会多次执行
willxxx周期,失去了原有的意义; - 这些生命周期也经常被误用,在里面使用
setState会触发多次重绘,影响性能。
- v16 引入 Fiber 后协调过程会多次执行
-
新增两个生命周期
getDerivedStateFromProps、getSnapshotBeforeUpdate。
2. 生命周期执行顺序
- 父子组件初始化:父子组件第一次渲染加载时:
- parent:
constructor=> ... =>render。 - Child:
constructor=> ... =>componentDidMount。相比上面多了一个生命周期。 - Parent:
componentDidMount。
- parent:
- 修改父组件传入子组件的
props- Parent:
getDerivedStateFromProps=> ... =>render。 - Child:
getDerivedStateFromProps=> ... =>getSnapshotBeforeUpdate。相比上面多了一个生命周期。 - Parent:
getSnapshotBeforeUpdate。 - Child:
componentDidUpdate。 - Parent:
componentDidUpdate。
- Parent:
- 子组件修改自身的state,此时只有子组件执行更新阶段的5个生命周期。
- 卸载子组件:点击父组件的卸载子组件按钮,
- Parent:
getDerivedStateFromProps=> ... =>getSnapshotBeforeUpdate。 - Child:
componentWillUnmount(如果是子组件自己卸载时就只执行这一个)。 - Parent:
componentDidUpdate。
- Parent:
3. 触发render的时机
- setState
- useState
- 父组件渲染
四、其他特性
- Fragment 特性允许组件返回多个根元素,并且不用额外添加 DOM 节点。
<>
<Child1 />
<Child2 />
</>
- lazy 用于组件的懒加载,并且要在 Suspense 组件中渲染该组件。
const Avatar = lazy(() => import('./Avatar.js'));
- memo 是高阶组件,它接收一个组件作为参数并返回一个组件。如果组件在相同的 props 情况下会渲染相同的结果就可以用 memo 进行包装,当 props 相同时会直接复用上次渲染的结果。
const Greet = memo(function Greet({ name }) {
return <h1>Hello, {name}</h1>;
});
默认只会做浅比较,可用作性能优化。如果想深层比较,可以传入第二个参数。
和 useMemo 的区别:
- memo 是一个高阶组件,useMemo 是一个 hook。
- memo 用来缓存组件的渲染,useMemo 用来缓存值。
- Context:当很多不同层级的组件都需要访问同样的数据时,可以借助
Context来共享这个数据,而不需要给每个组件添加 props,例如 UI 主题。
import { createContext } from 'react';
// 1. 使用 createContext 创建一个上下文
const ThemeContext = createContext('dark'); // 参数是低优先级的默认值
// 2. 用 Provider 包裹组件,为里面的组件指定context值
function App() {
const [theme, setTheme] = useState('dark')
//...
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 3. 在包裹的组件内部可以调用 useContext 读取context值
function Toolbar() {
const theme = useContext(ThemeContext)
}
useContext 会获取距离它最近的上层组件的 Provider 提供的 value。
五、单向数据流、数据绑定
- 数据流指的是组件之间的数据流动,和数据绑定没有关系。
- 数据绑定指的是
View层和Model层的映射关系。单双向数据绑定的区别在于 View 改变会不会触发 Model 更新。
React(Vue)是单向数据流,只能由父组件向子组件传递props,并且子组件不能修改父组件传递来的props,只能通过事件通知父组件进行数据修改。单向数据流使得所有的状态改变是可追溯的,排查问题更方便。
Vue 是双向数据绑定,React 不支持双向数据绑定、是单向数据流。
六、React 性能优化
1. 虚拟化长列表(不能分页)
当要渲染成千上万个列表项时,因为浏览器需要处理大量的DOM节点,所以速度会很慢。
列表虚拟化是一种按需加载的优化技术,它只会渲染可见区域中的数据,对不可见区域不渲染或部分渲染。react、vue都有一些虚拟列表库。它的原理是:
- 根据滚动距离和列表项高度,计算出当前需要展示列表的
起始index和可视区列表的偏移距离; - 根据
起始index和可视区高度,计算出...的末尾index; - 根据
起始index和末尾index截取相应的列表数据,然后赋值给可视区列表、进行渲染; - 将之前计算的偏移距离设置在列表上。(可视区是动态变化的)
滚动距离?scrollTop属性的值是元素内容的顶部到视窗可见内容的顶部之间的距离。
如果列表项不固定呢?以预估高度先渲染,然后获取真实高度缓存。
2. 避免不必要的 re-render
通常当父组件的state或props更新时,子组件的数据不论是否发生变化都会触发重新渲染。为了避免无效 render,可以采取:
(1) memo、useCallback
- memo 可以缓存子组件,如果 props 没有改变就不需要重新渲染。
- useCallback 可以缓存子组件的参数函数。
另外 useMemo 一般用于密集型、计算量大的缓存。
(2) shouldComponentUpdate通过比较新旧 props 和新旧 state 来控制组件的渲染行为。设置成“只有组件的 state 或 props 改变时才会 return true”、组件才会渲染。
shouldComponentUpdate (nextProps, nextState) {}
(了解忽略)引用类型需要深比较才能判断是否修改,非常影响效率,推荐结合 immutable 数据进行浅比较,只要被修改就会返回一个全新的对象。
(3) PureComponent(过时) 通过浅比较的方式,只有 props、state改变时才会重新渲染。
3. 其他优化
- diff算法能够最小化DOM的更新,批量更新机制可以减少重排重绘次数,提升渲染速度。
- 引入 key 来识别列表元素,以尽量复用 DOM 节点、减少开销。
- lazy 和 Suspense 组件搭配实现懒加载组件。
七、React 与 Vue
1. 相同点
- 组件化开发。
- 都有虚拟DOM,通过 diff 算法最大化地复用 DOM。
- 单向数据流,数据只能由父组件流向子组件。
- 都是数据驱动视图,不直接操作DOM。但Vue是响应式的、会自动改变,而React不是响应式的,需要手动setState。
2. 不同点
- 语法:vue 是模板语法,react 是 jsx 语法。都是 render 的一种形式。
- 双向数据绑定:vue 是双向数据绑定,react 不支持。
- 监听数据变化的原理不同:react 强调数据的不可变,它是通过比较对象引用的方式。vue 中的数据是可变的,它的响应式原理是基于追踪对象属性的读写来实现的。
- vue 内置多个指令,封装程度较高;react更灵活、扩展性高。
3. react 相比 vue 的优势
- 生态圈更大,有更多的扩展工具和相关技术支持。
- 函数式编程会有更好的测试性。
- 版本过渡比较平滑,项目更新升级时成本较低。
4. vue 相比 react 的优势
- 封装程度高、中文文档丰富,初学者容易上手。
- 模板语法会更容易阅读和维护。
八、新特性
- React16 新特性
- Fiber 架构、Hooks、Context
- 生命周期更新
- Fragment组件、memo缓存组件、lazy懒加载
- React17 新特性
- v17 最大的特点就是无新特性,旨在方便后续版本的升级。
- 去除事件池。事件池指的是合成事件对象放入池中统一管理。
- 事件委托到根节点。事件不再委托到
document上,而是委托到根节点。
React18
v18 不再支持 IE 浏览器,如需兼容应回退到 v17。
1. Concurrent Mode(并发模式)
Fiber 为状态更新提供了可中断的能力。虽然 v16、v17 已经应用了 Fiber 新架构,但并没有开启并发更新,所以实际表现和老架构一致。而 v17 和 v18 的区别在于从同步不可中断更新变成了异步可中断更新。
并发更新参考为什么要设计 Fiber。
(1)并发模式和并发更新:
- 使用
createRoot会开启并发模式,但开启了并发模式并不一定会开启并发更新,只是基本前提。 - v18 中以是否使用并发特性作为是否开启并发更新的依据。
(2)并发特性:useDeferredValue、useTransition,开启并发模式后才能使用。
两者都是用于管理并发渲染的过渡效果,旨在让应用更加平滑地响应用户操作,提高用户体验。
useTransition: 生成的startTransitionAPI 会将内部回调函数所触发的渲染标记为非紧急渲染,可以被其他紧急渲染抢占。
// isPending 指处于过渡状态,正在加载中
// startTransition 通过回调将状态更新包装为一个过渡任务,是一个低优先级的更新
const [isPending, startTransition] = useTransition(); // 返回数组
startTransition(() => {
setList(new Array(10000).fill(null));
})
useDeferredValue:返回一个延迟更新的值。只有当前没有紧急更新时,该值才会变为最新值。
const deferredList = useDeferredValue(value);
两者异同点
- 两者都是将任务标记成了非紧急更新。
- 但
useTransition是将任务包装成了延迟更新任务,useDeferredValue是生成一个表示延时状态的值。(一个用来包装方法,一个用来包装值)
2. 引入新的 root API
调用createRoot可以开启并发模式。
// React 17
import ReactDOM from 'react-dom';
const root = document.getElementById('root');
ReactDOM.render(<App />, root)
// React 18
import ReactDOM from 'react-dom/client';
const root = document.getElementById('root');
ReactDOM.createRoot(root).render(<App />); // 区别
3. 自动批处理(包括 setState)
v18 默认将多个状态更新合并成一次更新,以提高应用的性能。退出批量更新可以使用flushSync。
4. 新增 API
useId:支持同一个组件在客户端和服务端生成相同的 ID,避免hydration的不兼容。
const id = useId();
useSyncExternalStore(忽略):一般是三方状态管理库使用,日常业务中不需要关注。useInsertionEffect(忽略):只建议css-in-js库使用。
九、设计模式和架构模式
1. 前端工程化
能够提高软件质量、开发效率和方便协同开发的事情都属于工程化,比如提高应用的性能、稳定性、可维护性等。前端工程化可以分为以下四个方面:
-
模块化是从业务功能层面对代码和资源进行拆分,一个模块就是一个实现特定功能的文件,比如支付功能。JS 模块化方案有
AMD/CMD/module等,CSS 模块化方案有less/sass等。 -
组件化是从UI层面进行拆分,UI是由多个组件进行嵌套、组合构成的,比如输入框、提交按钮。
-
规范化包括编码规范(eslint、文件命名规范)、开发流程规范(codeReview)等。
-
自动化包括自动化测试、构建、部署等,将这些简单重复的工作交给机器来做。
- 开发者提交一个代码合入 Master 的请求后就会去跑自动化测试(单元测试、集成测试、e2e测试)。
- 测试通过后代码合入Master,然后开始 Build,将源码转换为可运行的实际代码,需要安装依赖、配置各种资源等。
- 然后当前代码就是一个可以直接部署的release版本。
2. 如何封装一个组件
组件封装是不会直接暴露内部结构,而是提供 props 去控制组件。
- 为什么要封装组件
- 避免重复的工作量,能够提高开发效率。
- 使代码逻辑更加清晰,方便项目的后期维护。
- 将业务功能收敛到组件里开发,不会影响其他人的业务,有利于协同开发。
- 如何封装一个组件
- 单一职责原则:一个组件应该只负责一件事情,不要耦合一些不相关逻辑。
- 复用性:Button等基础组件需要考虑通用性,业务组件需要考虑当前业务的一些特殊场景。
- 扩展性:有新的业务需要时是否容易扩展。
- 可维护性:如果组件过度追求复用性会导致内部的代码臃肿,维护成本较高。
- 组件的粒度:拆分并不是越细越好,遵循高内聚、低耦合的理念。
3. 架构模式:MVC/MVP/MVVM
MVX架构是为了分离 View 和 Model,让应用有清晰合理的架构,方便模块化开发和后期维护。三者的区别在于解耦的方案不同。
三个架构的公共部分有:Model 模型层负责存取、管理数据,View 视图层就是用户界面。
MVC:Model-View-Controller
Controller控制器连接 View 和 Model,它负责处理用户的输入请求,然后调用 Model 处理数据并将结果传给 View。
所有通信都是单向的。
- view 传送指令到 controller
- controller 完成业务逻辑后,要求 model 改变状态
- model 将新的数据发送给 view,用户得到反馈。
MVVM(Vue实践):Model-View-ViewModel
ViewModel视图模型层负责把 Model 的数据同步到 View 显示出来,把 View 的修改同步回 Model。
ViewModel 和 View 层是双向数据绑定,和 Model 层通过接口进行数据交互。
MVVM 和 MVC 区别
mvvm 通过双向数据绑定实现了 View和Model的自动同步,view 变动会自动改变 model。
- MVC 用于服务端,像Spring MVC。
- MVP(Model-View-Presenter)用于安卓开发。
- MVVM 用于前端。
参考