React
如何创建一个react的项目(使用脚手架)
- 安装cr脚手架:npm install -g create-react-app
- 进入文件夹:create-react-app 项目名称
- 进入项目:cd 项目名称
- 运行项目:npm start
如何不使用脚手架创建一个项目
之前面试官问过是否有不使用脚手架创建项目的经历。
详细可以查看这篇文章
我理解这个问题说出来大概就可以,比如:
-
yarn init 初始化package.json文件
-
安装react和react-dom
-
配置webpack
- 配置babel支持ES6
- 配置@babel/preset-react支持react
- 支持ts:ts-loader @types/react @types/react-dom
- 支持antd
- 支持less:less-loader,css-loader,style-loader
- 配置plugins,常用的有html-webpack-plugin(当使用 webpack 打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中。)和 clean-webpack-plugin(是一个清除文件的插件。 在每次打包后,磁盘空间会存有打包后的资源,在再次打包的时候,我们需要先把本地已有的打包后的资源清空,来减少它们对磁盘空间的占用。 插件clean-webpack-plugin就可以帮我们做这个事情)
-
安装router
-
安装redux
对于React 框架的理解(React的特性有哪些)
React是一个用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案。
它有以下特性:
-
组件化:将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面,提高代码的复用率和开发效率。
-
数据驱动视图:
React
通过setState
实现数据驱动视图,通过setState
来引发一次组件的更新过程从而实现页面的重新渲染。- 数据驱动视图是我们只需要关注数据的变化,不用再去操作dom。同时也提升了性能。
-
JSX 语法:用于声明组件结构,是一个 JavaScript 的语法扩展。
-
单向数据绑定:从高阶组件到低阶组件的单向数据流,单向响应的数据流会比双向绑定的更安全,速度更快
-
虚拟 DOM:使用虚拟
DOM
来有效地操作DOM
-
声明式编程:
如实现一个标记的地图: 通过命令式创建地图、创建标记、以及在地图上添加的标记的步骤如下:
// 创建地图 const map = new Map.map(document.getElementById("map"), { zoom: 4, center: { lat, lng }, }); // 创建标记 const marker = new Map.marker({ position: { lat, lng }, title: "Hello Marker", }); // 地图上添加标记 marker.setMap(map);
而用 React 实现上述功能则如下:
<Map zoom={4} center={(lat, lng)}> <Marker position={(lat, lng)} title={"Hello Marker"} /> </Map>
声明式编程方式使得 React 组件很容易使用,最终的代码简单易于维护
jsx语法是必须的吗
以下是经过babel转译之后的jsx:
// jsx
const element = <h1>Hello, world!</h1>;
const container = document.getElementById(
'root'
);
ReactDOM.render(element, container);
// babel 处理后
const element = /*#__PURE__*/React.createElement("h1", null, "Hello, world!");
const container = document.getElementById('root');
ReactDOM.render(element, container);
注:React.createElement(标签名,属性对象,子元素)
所以不使用jsx语法也可以使用React:
import React from "react";
// 本文件用于测试jsx 语法是否是必须的
// 不使用jsx语法创建的元素
const ReactCreateElement = React.createElement("h1", null, "Hello, createElement!");
// 使用jsx语法创建的元素
const JsxElement = <h1>Hello, JSX!</h1>;
export {
JsxElement,ReactCreateElement
}
// 使用
import {ReactCreateElement,JsxElement} from './components/JsxNecessary'; // 验证jsx是否是必须的
function App() {
return (
<div className="App">
{ReactCreateElement}
{JsxElement}
</div>
);
}
export default App;
两者均可正常显示,但是两者的优劣显而易见,使用createElement方法会使代码更加的冗余,而jsx更加简洁。
为什么提出jsx
JSX是JS的语法扩展,主要用于声明元素,可以理解为React.createElement()的语法糖,React并不强制使用JSX,即使使用了JSX最后也会被babel编译成createElement。
React认为视图和逻辑内在耦合,比如,在 UI 中需要绑定处理事件、在某些时刻状态发生变化时需要通知到 UI,以及需要在 UI 中展示准备好的数据。
React并没有采用将视图与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为“组件”的松散耦合单元之中,来实现关注点分离。 为了实现其组件化的目的,而不引入更多的概念(比如Vue引入了模板语法,这就是新的概念,学习成本会比较高),使用人们熟悉的js语法的扩展更加适用。
并且相比于createElement,JSX更加的简洁。
关注点分离是日常生活和生产中广泛使用的解决复杂问题的一种系统思维方法。大体思路是,先将复杂问题做合理的分解,再分别仔细研究问题的不同侧面(关注点),最后综合各方面的结果,合成整体的解决方案。
Babel 插件是如何实现 JSX 到 JS 的编译 ?
需要的依赖:
- @babel/cli
- @babel/core
- @babel/preset-react
babel.rc文件添加配置:
{
"presets": ["@babel/preset-react"]
}
Babel插件实现JSX到JS的编译过程主要基于抽象语法树(AST)的转换。以下是该过程的详细解析:
一、Babel简介
Babel是一个JavaScript编译器,它可以将现代JavaScript代码转换为向后兼容的代码,以便在老版本的浏览器中正确运行。Babel支持多种JavaScript新特性的转换,包括箭头函数、async/await、模板字符串以及JSX等。
二、JSX到JS的编译过程
-
解析(Parsing) :
- Babel首先使用解析器(如@babel/parser)将JSX代码转换为AST。AST是一种树状的数据结构,它表示了代码的结构和语法信息。
- 在解析过程中,Babel会识别JSX中的标签、属性、子元素等,并将它们转换为AST中的节点。
-
转换(Transforming) :
- 接下来,Babel会对AST进行遍历和转换。在转换过程中,Babel会识别出JSX节点,并将其转换为对应的
React.createElement
函数调用。 - 例如,JSX中的
<div><h1>Hello, world!</h1></div>
会被转换为React.createElement('div', null, React.createElement('h1', null, 'Hello, world!'))
。 - 为了实现这一过程,Babel通常会使用@babel/plugin-transform-react-jsx等插件。这些插件会提供特定的转换规则,告诉Babel如何将JSX节点转换为
React.createElement
调用。
- 接下来,Babel会对AST进行遍历和转换。在转换过程中,Babel会识别出JSX节点,并将其转换为对应的
-
生成(Generating) :
- 最后,Babel会将转换后的AST生成为目标JavaScript代码。这个过程称为代码生成(code generation)。
- 在代码生成阶段,Babel会遍历AST,并根据节点的类型和属性生成相应的JavaScript代码。
- 最终,Babel会输出转换后的JavaScript代码,该代码可以在老版本的浏览器中正确运行,并且实现了与原始JSX代码相同的UI结构和行为。
三、Babel插件的作用
在Babel的编译过程中,插件起到了至关重要的作用。插件可以扩展Babel的功能,使其能够处理更多的JavaScript新特性。对于JSX到JS的编译过程来说,@babel/plugin-transform-react-jsx等插件是必不可少的。
综上所述,Babel插件通过解析JSX代码为AST、遍历并转换AST中的JSX节点为React.createElement
调用、最后生成目标JavaScript代码的过程,实现了JSX到JS的编译。在使用Babel进行JSX编译时,需要注意配置Babel、关注版本兼容性以及进行性能优化等措施。
Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement 的函数。
React.createElement源码:
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// props 变量用于储存元素属性的键值对集合
const props = {};
// key、ref、self、source 均为 React 元素的属性,此处不必深究
let key = null;
let ref = null;
let self = null;
let source = null;
// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
for (propName in config) {
if (
// 筛选出可以提进 props 对象里的属性
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
const childrenLength = arguments.length - 2;
// 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
if (childrenLength === 1) {
// 直接把这个参数的值赋给props.children
props.children = children;
// 处理嵌套多个子元素的情况
} else if (childrenLength > 1) {
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组里
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 最后把这个数组赋值给props.children
props.children = childArray;
}
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
createElement并没有十分复杂的操作,整个过程看起来更像是一个格式化的过程:将我们输入的相对简单清晰的结构转化为ReactElement函数需要的格式。
ReactElement函数源码:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 内置属性赋值
type: type,
key: key,
ref: ref,
props: props,
// 记录创造该元素的组件
_owner: owner,
};
//
if (__DEV__) {
// 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
}
return element;
};
ReactElement 其实只做了一件事情,那就是“创建”,说得更精确一点,是“组装”:ReactElement 把传入的参数按照一定的规范,“组装”进了 element 对象里,并把它返回给了 React.createElement,最终 React.createElement 又把它交回到了开发者手中。
ReactElement返回的element 其实就是虚拟DOM中的一个节点:一个JS对象,这个对象包含了对真实节点的描述。
对于React虚拟DOM的理解
- js对象,保存在内存中
- 是对真实DOM结构的映射
虚拟 DOM 的工作流程:
挂载阶段:React 将结合 JSX 的描述,构建出虚拟 DOM 树,然后通过 ReactDOM.render 实现虚拟 DOM 到真实 DOM 的映射(触发渲染流水线);
更新阶段:页面的变化先作用于虚拟 DOM,虚拟 DOM 将在 JS 层借助算法先对比出具体有哪些真实 DOM 需要被改变,然后再将这些改变作用于真实 DOM。
虚拟 DOM 解决的关键问题有以下三个:
- 减少 DOM 操作:虚拟 DOM 可以将多次 DOM 操作合并为一次操作
- 研发体验/研发效率的问题:虚拟 DOM 的出现,为数据驱动视图这一思想提供了高度可用的载体,使得前端开发能够基于函数式 UI 的编程方式实现高效的声明式编程。
- 跨平台的问题:虚拟 DOM 是对真实渲染内容的一层抽象。同一套虚拟 DOM,可以对接不同平台的渲染逻辑,从而实现“一次编码,多端运行”
既然是虚拟 DOM,那就意味着它和渲染到页面上的真实 DOM 之间还有一定的距离,这个距离通过 ReactDOM.render 方法填充:
ReactDOM.render(
// 需要渲染的元素(ReactElement)
element,
// 元素挂载的目标容器(一个真实DOM)
container,
// 回调函数,可选参数,可以用来处理渲染结束后的逻辑
[callback]
)
VDOM 和 DOM 的区别
- 真实DOM存在重排和重绘,虚拟DOM不存在;
- 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘(可能比直接操作真实DOM要少)”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘”
传统的原生 api 或 jQuery 去操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。
当你在一次操作时,需要更新 10 个 DOM 节点,浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行 10 次流程。
而通过 VNode,同样更新 10 个 DOM 节点,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地的一个 js 对象中,最终将这个 js 对象一次性 attach 到 DOM 树上,避免大量的无谓计算。
VDOM 和 DOM 优缺点
真实 DOM 的优势:
- 易用
真实 DOM 的缺点:
- 效率低,解析速度慢,内存占用量过高
- 性能差:频繁操作真实 DOM,易于导致重绘与回流
虚拟 DOM 的优势:
- 简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
- 性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能
- 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行
虚拟 DOM 的缺点:
- 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化,首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢
react 的生命周期
挂载
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
getDerivedStateFromProps
该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例。
执行时机:组件创建和更新阶段,不论是props变化还是state变化,都会调用。
在每次render方法前调用,第一个参数为即将更新的props,第二个参数为上一个状态的state,可以比较props 和 state来加一些限制条件,防止无用的state更新
该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更新
更新
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
getSnapshotBeforeUpdate
该周期函数在render后执行,执行之时DOM元素还没有被更新
该方法返回的一个Snapshot值(不返回报错),作为componentDidUpdate第三个参数传入
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('#enter getSnapshotBeforeUpdate');
return 'foo';
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('#enter componentDidUpdate snapshot = ', snapshot);
}
此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态
卸载
当组件从 DOM 中移除时会调用如下方法:
- componentWillUnmount()
错误处理
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
- static getDerivedStateFromError():更改状态,从而显示降级组件
- componentDidCatch():打印错误信息
ErrorBoundry都可以捕获哪些错误
可以捕获:
- 子组件
render
方法中同步抛出的js
错误 - 子组件生命周期方法中的错误
不可以捕获:
- 异步操作中的错误(
async
用try catch
,promise
用.catch
捕获) - 事件处理器错误
- ErrorBound组件本身的错误
React父子组件的生命周期调用顺序
普通子组件
挂载阶段
更新阶段
卸载阶段
懒加载组件
挂载阶段
更新阶段(和普通组件一样)
卸载阶段(和普通组件一样)
React事件和原生事件执行顺序
// React 事件和原生事件的执行顺序
import React from "react";
class EventRunOrder extends React.Component {
constructor(props) {
super(props);
this.parent = null;
this.child = null
}
componentDidMount() {
this.parent.addEventListener('click', (e) => {
console.log('dom parent')
})
this.child.addEventListener('click', (e) => {
console.log('dom child')
})
document.addEventListener("click", (e) => {
console.log('document')
})
}
childClick = (e) => {
console.log('react child')
}
parentClick = (e) => {
console.log('react parent')
}
render() {
return (
<div onClick={this.parentClick} ref={ref => this.parent = ref}>
<div onClick={this.childClick} ref={ref => this.child = ref}>
test
</div>
</div>
)
}
}
export default EventRunOrder
dom child
dom parent
react child
react parent
document
react所有事件都挂载在document上,当真实dom触发后冒泡到document后才会对react事件进行处理,所以:
- 原生事件先执行
- react合成事件再执行
- document上挂载的事件最后执行
react的事件机制
react实现了一套自己的事件机制,包括事件注册、事件合成、事件冒泡、事件派发等。在react中这套事件被称为合成事件。
合成事件是 React模拟原生 DOM事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
根据 W3C规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口,例如:
const button = <button onClick={handleClick}>按钮</button>
如果想要获得原生DOM事件,可以通过e.nativeEvent属性获取:
const handleClick = (e) => console.log(e.nativeEvent);;
const button = <button onClick={handleClick}>按钮</button
从上面可以看到React事件和原生事件也非常的相似,但也有一定的区别:
- 事件名称命名方式不同:react采用小驼峰格式
- 事件处理函数书写不同:react使用{},而原生事件使用双引号
虽然onclick看似绑定到DOM元素上,但实际并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听。
这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象。
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。
所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:
- 阻止合成事件间的冒泡,用e.stopPropagation()
- 阻止合成事件与最外层 document 上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation()
- 阻止合成事件与除最外层document上的原生事件上的冒泡,通过判断e.target来避免
综上所述:
- React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
- React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
- React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
- React 有一套自己的合成事件 SyntheticEvent
函数组件和类组件输出差别(闭包陷阱)
以下函数组件代码,先alert再add,页面显示的值和alert的值分别是什么
import {useState} from "react";
const FunctionComponentClosure = () => {
const [value, setValue] = useState(1);
const log = () => {
setTimeout(() => {
alert(value)
}, 3000)
}
return (
<div>
<p>{value}</p>
<button onClick={log}>alert</button>
<button onClick={() => setValue(value + 1)}>add</button>
</div>
)
}
export default FunctionComponentClosure
alert :1
页面显示:2
原因:log方法内的value和点击动作触发时的value相同,后续value的变化不会对log内部的value产生任何的影响。这种现象被称为 闭包陷阱,即函数式组件每次render都产生一个新的log函数,这个log函数会产生一个当前阶段value值的闭包。
除了闭包陷阱之外,函数组件和类组件还存在如下区别:
- 写法不同:函数组件代码更加简洁
- 函数组件不需要处理this但是类组件需要
- 类组件有生命周期和state函数组件不存在(但是函数组件中可以通过hooks达到类似的效果)
如何解决闭包陷阱
const Test = () => {
const [value, setValue] = useState(1);
const countRef = useRef(value)
const log = function () {
setTimeout(() => {
alert(countRef.current)
}, 3000)
}
useEffect(() => {
countRef.current = value
}, [value])
return (
<div>
<p>{value}</p>
<button onClick={log}>alert</button>
<button onClick={() => setValue(value + 1)}>add</button>
</div>
)
}
useRef每次render都会返回同一个引用类型对象,设置和读取都在这个对象上处理的话,就可以得到最新的value值了。
在类组件中情况是否会相同呢?
class Test extends React.Component {
constructor(props) {
super(props)
this.state = {
value: 1
}
}
log = () => {
setTimeout(() => {
alert(this.state.value)
}, 3000)
}
render() {
return (
<div>
<p>{this.state.value}</p>
<button onClick={this.log}>alert</button>
<button onClick={() => this.setState({
value: this.state.value + 1
})}>add</button>
</div>
)
}
}
export default Test
alert和页面显示的值相同。
受控组件和非受控组件
受控组件:简单理解为双向绑定,数据和视图的变化是同步的,受控组件一般需要初始状态(value或者checked) 和一个 状态更新事件函数。
非受控组件:不受控制的组件,在其内部存储自身的状态,可以通过ref查询DOM的当前值。初始状态为defaultValue
推荐使用受控组件,在受控组件中数据由React组件处理。
操作DOM的情况下一般需要使用非受控组件,数据由DOM本身处理,控制能力较弱,但是代码量更少。
React如何实现状态自动保存(vue中的keep-alive)
为什么需要状态保存
在React中通常使用路由去管理不同的页面,在切换页面时,路由将会卸载掉未匹配的页面组件,所以比如从列表进入详情页面,等到退回列表页面时会回到列表页的顶部。
什么情况下需要状态保存
- 列表进入详情
- 已填写但是未提交的表单
- 管理系统中可切换和关闭的标签
总而言之就是在交互过程中离开需要对状态进行保存的场景。
React为什么不支持
状态保存在vue中可以使用keep-alive进行实现,但是react认为这个功能容易造成内存泄漏,所以暂时不支持。
如何实现
-
手动保存状态:适用于数据较少的情况
在componentWillUnmount的时候将状态通过redux进行保存,然后在componentDidMount周期进行数据恢复。
-
通过路由实现:
基本思想是,将KeepAlive中的组件也就是children取出来,通过React Portals将children渲染到一个不会被卸载的组件keeper中,再使用Dom操作将keeper内的真实内容移入对应的keepalive
useEffect和useLayoutEffect有什么区别
相同点:
- 处理副作用:函数组件内不允许操作副作用。比如:改变DOM、设置订阅、操作定时器等
- 底层都是调用mountEffectlmpl方法,基本上可以替换使用
不同点:
- useEffect在像素变化之后异步调用,改变屏幕内容可能会造成页面的闪烁
- useLayoutEffect在像素变化之前同步调用,可能会造成页面延迟显示,但是不会闪烁:主要用于处理DOM操作、调整样式、避免页面闪烁等。因为是同步执行,所以要避免做大量计算,从而避免造成阻塞。
- useLayoutEffect先于useEffect执行
副作用
执行副作用操作是指在函数或代码块执行期间,除了返回一个值以外,还对函数外部的环境产生了可观察的影响。这些影响可能包括但不限于:
- 修改函数外部的变量或状态。
- 发送网络请求或进行其他 I/O 操作。
- 修改全局状态。
- 修改 DOM 结构。
- 订阅外部事件。
- 调用其他函数产生副作用等。
对react hook的理解,解决了什么问题
官方给出的动机是解决长时间使用和维护react过程中常遇到的问题,例如:
- 难以重用和共享组件中的与状态相关的逻辑
- 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面
- 类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问题
- 由于业务变动,函数组件不得不改为类组件等等
在以前,函数组件也被称为无状态的组件,只负责渲染的一些工作
在有了hooks之后,函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理。
hooks的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用hooks能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑hooks。
React常用的hooks
useState
定义状态,解决了函数组件没有状态的问题。
接受一个初始值(初始值可以是一个具体数据类型,也可以是一个函数,该函数只执行一次返回值作为初始值)作为参数,返回一个数组,第一项是变量,第二项是设置变量的函数。
-
对象不可局部更新:state是一个对象时,不能局部更新对象属性,
useState
不会合并,会把整个对象覆盖。要用展开运算符自己进行属性值的覆盖。const [state, setState] = useState({ name: 'jerry', age: 18 }) const changeState = () => { setState({name:"tom"}) //覆盖整个state }
-
地址要变更:对于引用类型,数据地址不变的时候,认为数据没有变化,不会更新视图。
const [state, setState] = useState({ name: 'jerry', age: 18 }) const changeState = () => { const obj = state //obj和state指向同一个地址 obj.name = 'tom' setState(obj) // 地址没有变更,不会更新 }
-
useState
传入一个函数:useState
初始化是惰性的,initialState
只有在初始渲染中起作用,后续渲染会被忽略,如果初始state需要通过复杂的计算获得,可以传入一个函数,在函数中计算并返回初始state,次函数只在初始渲染时被调用。 -
多次调用会被合并:如下操作只会对age+1;
const changeState = () => { setState({...state,age:state.age+1}); setState({...state,age:state.age+1}); setState({...state,age:state.age+1}); setState({...state,age:state.age+1}); setState({...state,age:state.age+1}); };
如下方式则会直接age+3
const changeState = () => { setState(preState=>{ return {...preState,age:preState.age+1}; }); setState(preState=>{ return {...preState,age:preState.age+1}; }); setState(preState=>{ return {...preState,age:preState.age+1}; }); };
-
useState
异步问题:如何获取到更新后的state,使用useEffect
,当state
变化时触发。或者如下操作,pre是最新的stateconst changeState = () => { setState({ ...state, age: state.age + 1 }) setState(pre => { console.log('pre', pre) return pre; }); };
setState执行机制(类组件)
通过setState来修改组件内部的state,并且触发render方法进行视图的更新。
直接修改state不会引起视图的更新,因为react没有像vue一样通过proxy或者definProperty监听数据变化,必须通过setState方法来告知react组件state已经发生了改变。
关于state方法的定义是从React.Component中继承,定义的源码如下:
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
从上面可以看到setState第一个参数可以是一个对象,或者是一个函数,而第二个参数是一个回调函数,用于可以实时的获取到更新之后的数据。
同步异步
- 在组件生命周期或React合成事件中,setState是异步。要想获取更新后的值,可以通过setState的第二个参数传入一个函数(函数组件通过useEffect)。
在setTimeout或者原生dom事件中,setState是同步。React18中setState都是异步的。
批量更新
- 合成事件或者生命周期中setState传入对象会被合并。要想避免合并可以将第一个参数写成函数。
而在setTimeout或者原生dom事件中,由于是同步的操作,所以并不会进行覆盖现象。React18中setState都是异步的,所以都会被合并。
useEffect
给没有生命周期的组件添加结束渲染的信号,在渲染结束后执行。
-
如果不接受第二个参数,那么在第一次渲染完成之后和每次更新渲染页面的时候,都会调用
useEffect
的回调函数。 -
可以对第二个参数传入一个数组,这个数组表示的是更新执行所依赖的列表,只有依赖列表改变时(数组中的任意一项变化时),才会触发回调函数
-
第二项是一个空数组:只在第一次渲染完成时执行。相当于didMounted
-
清除副作用:比如绑定了自定义DOM 事件以防止内存泄漏
如何清除:
clean-up
函数useEffect(()=>{ document.addEventListener('click',func); return ()=>{ // 在每次执行useEffect之前都会执行上一次return中内容 document.removeEventListener('click',func) } })
-
异步操作:useEffect返回的是clean-up函数,因此没有办法返回一个promise实现异步
-
立即执行函数:
useEffect(() => { (async function anyNameFunction() { await loadContent(); })(); }, []);
-
在useEffect外部或者内部实现async/await函数,然后在内部调用
-
useContext
共享状态钩子。不同组件之间共享状态,避免props层层传递
- useContext 调用不受 同一 组件返回的 provider 的影响。相应的
<Context.Provider>
需要位于调用useContext()
的组件 之上。 - 从 provider 接收到不同的
value
开始,React 自动重新渲染使用了该特定 context 的所有子级。先前的值和新的值会使用Object.is
来做比较。使用memo
来跳过重新渲染并不妨碍子级接收到新的 context 值。
React.createContext
创建Context对象Context.Provider
通过value传递状态和状态改变函数
import React, { createContext, useContext, useState } from 'react';
// 创建一个 Context 对象
const ThemeContext = createContext();
// 提供一个 Context Provider,用于向下层组件提供 Context 值
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// 一个使用 Context 的子组件
const ThemeToggler = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
);
};
// 父组件
const App = () => {
return (
<ThemeProvider>
<ThemeToggler />
</ThemeProvider>
);
};
export default App;
useReducer
Action钩子,复杂版的useState
,简易版的redux。
redux
的原理是用户在页面中发起action
,从而通过reducer
方法来改变state
,从而实现页面和状态的通信。而Reducer
的形式是(state, action) => newstate
。类似,我们的useReducer()
是这样的:
const [state, dispatch] = useReducer(reducer, initialState)
import React, { useReducer } from 'react';
// 定义 reducer 函数
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unsupported action type');
}
};
// 初始状态
const initialState = { count: 0 };
// 计数器组件
const Counter = () => {
// 使用 useReducer 定义状态和操作状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
};
export default Counter;
自己创建hooks
自己创建hooks就是一个将公共代码封装的过程,比如一个hooks输出一个鼠标位置坐标,可以如下实现:
import { useState, useEffect } from 'react'
export default function useMousePosition() {
const [position, setPosition] = useState({
x: 0,
y: 0
})
useEffect(() => {
const move = (e) => {
setPosition({ x: e.x, y: e.y })
}
document.addEventListener('mousemove', move)
return () => {
document.removeEventListener('mousemove', move)
}
}, [])
return position
}
// 使用
const position = useMousePosition()
useEffect的触发时机
或者可以问:
- 数组可不可以什么都不传
- 数组里边内容如何确定
触发机制跟第二个参数有关:
- 第二个参数不传时:每次渲染完成后触发
- 第二个参数是一个空数组时:初始化渲染完成后触发,相当于didMounted
- 第二个参数是非空数组时:数组中数据有一项更新时触发
数组中的内容一般是props或者state,是普通变量时不会触发执行。
useEffect的第一个函数返回一个函数
返回一个clean-up 函数,用来清除副作用。clean-up的执行时机是每个useEffect执行前会执行上一个effect返回的clean-up函数。
useEffect 如何判断依赖项变化的
React使用严格的引用相等性检查来判断依赖项是否发生变化,用的是Object.is
方法进行判断的。
Object.is
是基于===
实现的,但有一些特殊的处理来解决 ===
中的一些不准确之处。
在useEffect
的依赖项数组中,React会比较数组中的每个依赖项的前一个值和当前值。如果任何一个依赖项发生了引用上的变化,useEffect
就会被触发。
React使用闭包的特性来保存上一次渲染时useEffect
的依赖项数组中的每个依赖项的值。当组件首次渲染时,React会在内部创建一个变量来存储依赖项的值。然后,在下一次组件渲染时,React会使用这个变量来比较前一个值和当前值。
hooks使用规则
- Hooks只在函数组件的顶层调用,不要在循环、条件判断或者嵌套函数中调用钩子。在类组件中无法使用。
- 对于自定义Hooks,使用use开头命名。
要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook 调用放到一个条件语句中会发生什么呢?
useMemo、memo、useCallback
他们三个的应用场景都是缓存结果,当依赖值没有改变时避免不必要的计算或者渲染。
- useCallback 是针对函数进行“记忆”的,当它依赖项没有发生改变时,那么该函数的引用并不会随着组件的刷新而被重新赋值。当我们觉得一个函数不需要随着组件的更新而更新引用地址的时候,我们就可以使用 useCallback 去修饰它。
- React.memo 是对组件进行 “记忆”,当它接收的 props 没有发生改变的时候,那么它将返回上次渲染的结果,不会重新执行函数返回新的渲染结果。
- React.useMemo是针对 值计算 的一种“记忆“,当依赖项没有发生改变时,那么无需再去计算,直接使用之前的值,对于组件而言,这带来的一个好处就是,可以减少一些计算,避免一些多余的渲染。当我们遇到一些数据需要在组件内部进行计算的时候,可以考虑一下 React.useMemo
useMemo与useEffect的区别
传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行不应该在渲染期间内执行的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
useEffect在渲染后执行,可以访问渲染后的值。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。和useEffect类似,但是如果每次渲染时都计算,那就没必要使用useMemo了。
ref使用场景
使用场景:直接使用dom元素的某个方法,或者直接使用自定义组件中的某个方法。在以下场景会用到ref:
- 对Dom元素的焦点控制、内容选择、控制
- 对Dom元素的内容设置及媒体播放
- 对Dom元素的操作和对组件实例的操作
- 集成第三方 DOM 库
ref作用于不同的组件时:
- 作用于内置的html组件,得到的是真实的dom
- ref作用于类组件,得到的是类的实例
- ref不能作用于函数组件
使用ref的模式有:
-
字符串:传入字符串,使用时通过 this.refs.“传入的字符串”的格式获取对应的元素。不再推荐使用,可能会被移除
-
对象:传入通过 React.createRef() 方式创建出来的对象,使用时获取到创建的对象中存在 current 属性就是对应的元素
-
函数:
ref={(el) => {this.txt = el;}}
-
传入hook,hook是通过 useRef() 方式创建,使用时通过生成hook对象的 current 属性就是对应的元素
-
ref转发:
import React, { Component } from 'react' function A(props, ref){ console.log(props, ref) return <h1 ref={ref}>A组件</h1> } // 传递函数组件,得到一个新的组件,不能传递类组件,并且函数组件必须使用第二个 const NewA = React.forwardRef(A) export default class App extends Component { ARef = React.createRef() componentDidMount() { console.log(this.ARef) // {current: h1} } render() { return ( <div> <NewA ref={this.ARef} words="sdfsd"/> </div> ) } }
可以使用:useImperativeHandle定义方法
useImperativeHandle(ref, () => ({ show: (title, content) => { setVisible(true); setTitle(title); setContent(content); }, hide: () => { setVisible(false); } }));
state和props有什么区别
一个组件的数据可以来源于组件内部,也可以来源于组件外部(比如父组件)。
组件内部的状态就是state,一般在constructor中定义。通过setState修改,会调用render方法重新渲染组件。 setState 还可以接受第二个参数,它是一个函数,会在 setState 调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成。
组件外部定义的状态是props,组件中的props不可以修改,只能通过传入新的props。
相同点:
- 两者都是 JavaScript 对象
- 两者都是用于保存状态
- props 和 state 都能触发渲染更新
区别:
- props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
- props 在组件内部是不可修改的,但 state 在组件内部可以进行修改 state 是多变的、可以修改
super和super(props)的区别
在ES6的class中:
class sup {
constructor(name) {
this.name = name;
}
printName() {
console.log(this.name);
}
}
class sub extends sup {
constructor(name, age) {
super(name); // super代表的是父类的构造函数
this.age = age;
}
printAge() {
console.log(this.age);
}
}
let jack = new sub("jack", 20);
jack.printName(); //输出 : jack
jack.printAge(); //输出 : 20
在上面的例子中,可以看到通过 super 关键字实现调用父类,super 代替的是父类的构建函数,使用 super(name) 相当于调用sup.prototype.constructor.call(this,name)
如果在子类中不使用 super关键字,则会引发报错,报错的原因是子类是没有自己的 this 对象的,它只能继承父类的 this 对象,然后对其进行加工。
而 super() 就是将父类中的 this 对象继承给子类的,没有 super() 子类就得不到 this 对象。
如果先调用 this,再初始化 super(),同样是禁止的行为。所以在子类 constructor 中,必须先代用 super 才能引用 this。
在 React 中,类组件是基于 ES6 的规范实现的,继承 React.Component,因此如果用到 constructor 就必须写 super() 才初始化 this。
这时候,在调用 super() 的时候,我们一般都需要传入 props 作为参数,如果不传进去,React 内部也会将其定义在组件实例中。 所以无论有没有 constructor,在 render 中 this.props 都是可以使用的,这是 React 自动附带的,是可以不写的。 综上所述:
- 在 React 中,类组件基于 ES6,所以在 constructor 中必须使用 super
- 在调用 super 过程,无论是否传入 props,React 内部都会将 porps 赋值给组件实例 porps 属性中
- 如果只调用了 super(),那么 this.props 在 super() 和构造函数结束之间仍是 undefined
react引入css的方式有哪些
组件式开发选择合适的css解决方案尤为重要
通常会遵循以下规则:
- 可以编写局部css,不会随意污染其他组件内的原生;
- 可以编写动态的css,可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
- 支持所有的css特性:伪类、动画、媒体查询等;
- 编写起来简洁方便、最好符合一贯的css风格特点
在这一方面,vue使用css起来更为简洁:
- 通过 style 标签编写样式
- scoped 属性决定编写的样式是否局部有效
- lang 属性设置预处理器
- 内联样式风格的方式来根据最新状态设置和改变css
而在react中,引入CSS就不如Vue方便简洁,其引入css的方式有很多种,各有利弊
常见的CSS引入方式有以下:
- 行内样式: <div style={{ width:'200px', height:'80px', }}>测试数据
- 组件中引入 .css 文件
- 组件中引入 .module.css 文件
- CSS in JS
通过上面四种样式的引入,各自的优缺点:
- 在组件内直接使用css该方式编写方便,容易能够根据状态修改样式属性,但是大量的样式编写容易导致代码混乱
- 组件中引入 .css 文件符合我们日常的编写习惯,但是作用域是全局的,样式之间会层叠
- 引入.module.css 文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编写
- 通过css in js 这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套、定义、修改状态等
react事件绑定方式有哪些
绑定方式
- render方法中使用bind
<div onClick={this.handleClick.bind(this)}>test</div>
。- 这种方式在组件每次render渲染的时候,都会重新进行bind的操作,影响性能
- render方法中使用箭头函数
<div onClick={e => this.handleClick(e)}>test</div>
- 每一次render的时候都会生成新的方法,影响性能
- constructor中bind:
this.handleClick = this.handleClick.bind(this);
- 定义阶段使用箭头函数绑定
区别
- 编写方面:方式一、方式二、方式四写法简单,方式三的编写过于冗杂
- 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实例
综合上述,方式四是最优的事件绑定方式。
react组件的创建方式以及区别
创建方式
- 函数组件:通过一个函数,return 一个jsx语法声明的结构
- React.createClass 方法创建:语法冗余,目前已经不太使用
- 继承 React.Component 创建的类组件:最终会被编译成createClass
区别
由于React.createClass创建的方式过于冗杂,并不建议使用。
而像函数式创建和类组件创建的区别主要在于需要创建的组件是否需要为有状态组件:对于一些无状态的组件创建,建议使用函数式创建的方式。
在考虑组件的选择原则上,能用无状态组件则用无状态组件。
不过,由于react hooks的出现,函数式组件创建的组件通过使用hooks方法也能使之成为有状态组件,再加上目前推崇函数式编程,所以这里建议都使用函数式的方式来创建组件。
react 中组件之间如何通信
组件传递的方式有很多种,根据传送者和接收者可以分为如下:
- 父组件向子组件传递:props
- 子组件向父组件传递:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
- 兄弟组件之间的通信:状态提升,在公共的父组件中进行状态定义
- 父组件向后代组件传递:React.createContext创建一个context进行组件传递
- 非关系组件传递:redux
React.createContext
通过使用React.createContext创建一个context
const PriceContext = React.createContext('price')
context创建成功后,其下存在Provider组件用于创建数据源,Consumer组件用于接收数据,使用实例如下:
Provider组件通过value属性用于给后代组件传递数据:
<PriceContext.Provider value={100}>
</PriceContext.Provider>
如果想要获取Provider传递的数据,可以通过Consumer组件或者或者使用contextType属性接收,对应分别如下:
contextType:
class MyClass extends React.Component {
static contextType = PriceContext;
render() {
let price = this.context;
/* 基于这个值进行渲染工作 */
}
}
Consumer组件:
<PriceContext.Consumer>
{ /*这里是一个函数*/ }
{
price => <div>price:{price}</div>
}
</PriceContext.Consumer>
React中key的作用
官网中对于diff有如下规则:
- 对比不同类型的元素:当元素类型变化时,会销毁重建
- 对比同一类型的元素:当元素类型不变时,比对及更新有改变的属性并且“在处理完当前节点之后,React 继续对子节点进行递归。”
- 对子节点进行递归:React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。若key一致,则进行更新,若key不一致,就销毁重建
react函数组件和类组件的区别
针对两种React组件,其区别主要分成以下几大方向:
- 编写形式:类组件的编写形式更加的冗余
- 状态管理:在hooks之前函数组件没有状态,在hooks提出之后,函数组件也可以维护自身的状态
- 生命周期:函数组件没有生命周期,这是因为生命周期钩子都来自于继承的React.Component,但是可以通过useEffect实现类似生命周期的效果
- 调用方式:函数组件通过执行函数调用,类组件通过实例化然后调用实例的render方法
- 获取渲染的值:函数组件存在闭包陷阱,类组件不存在(Props在 React中是不可变的所以它永远不会改变,但是 this 总是可变的,以便您可以在 render 和生命周期函数中读取新版本)
react高阶组件以及应用场景
高阶组件(Higher-Order Component,HOC)是 React 中用于复用组件逻辑的一种高级技术。HOC 本质上是一个函数,接受一个组件并返回一个新的组件。HOC 不会修改原组件,而是通过组合的方式增强组件的功能。
高阶组件是 React 中复用组件逻辑的一种重要技术。通过高阶组件,我们可以在不修改原组件的情况下,增强组件的功能,实现逻辑复用、访问控制、数据处理等功能。高阶组件的核心思想是将组件的逻辑提取到可复用的函数中,从而提高代码的可维护性和可复用性。
高阶组件的定义
高阶组件是一个函数,接受一个组件并返回一个新的组件。它的形式如下:
const withEnhancement = (WrappedComponent) => {
return class EnhancedComponent extends React.Component {
render() {
// 渲染增强后的组件
return <WrappedComponent {...this.props} />;
}
};
};
应用场景
1. 逻辑复用
HOC 可以用于复用跨多个组件的逻辑。例如,以下是一个简单的 HOC,用于提供计数功能:
import React from 'react';
// 创建高阶组件
const withCounter = (WrappedComponent) => {
return class extends React.Component {
state = { count: 0 };
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<WrappedComponent
count={this.state.count}
increment={this.increment}
{...this.props}
/>
);
}
};
};
// 使用高阶组件
const Button = ({ count, increment }) => (
<button onClick={increment}>Clicked {count} times</button>
);
const EnhancedButton = withCounter(Button);
const App = () => (
<div>
<EnhancedButton />
</div>
);
export default App;
2. 访问控制(权限控制)
HOC 可以用于权限控制,例如只允许某些用户访问特定组件:
import React from 'react';
const withAuthorization = (WrappedComponent, allowedRoles) => {
return class extends React.Component {
render() {
const { userRole } = this.props;
if (allowedRoles.includes(userRole)) {
return <WrappedComponent {...this.props} />;
} else {
return <div>You do not have access to this page</div>;
}
}
};
};
// 使用高阶组件
const AdminPage = () => <div>Admin Content</div>;
const AuthorizedAdminPage = withAuthorization(AdminPage, ['admin']);
const App = () => (
<div>
<AuthorizedAdminPage userRole="admin" />
<AuthorizedAdminPage userRole="user" />
</div>
);
export default App;
3. 处理数据
HOC 可以用于从 API 获取数据并将数据传递给组件:
import React from 'react';
const withDataFetching = (url) => (WrappedComponent) => {
return class extends React.Component {
state = {
data: null,
loading: true,
error: null,
};
componentDidMount() {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState({ data, loading: false }))
.catch((error) => this.setState({ error, loading: false }));
}
render() {
const { data, loading, error } = this.state;
return <WrappedComponent data={data} loading={loading} error={error} {...this.props} />;
}
};
};
// 使用高阶组件
const UserList = ({ data, loading, error }) => {
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
const EnhancedUserList = withDataFetching('https://jsonplaceholder.typicode.com/users')(UserList);
const App = () => (
<div>
<EnhancedUserList />
</div>
);
export default App;
高阶组件遵循的规则
- 不要改变原始组件,而应该使用组合
- HOC 应该透传与自身无关的 props
- 包装显示名字以便于调试
- 不要在 render() 方法中使用高阶组件:这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
- Refs 不会被传递:ref 实际上并不是一个 prop(就像 key 一样),它是由 React 专门处理的。如果将 ref 添加到 HOC 的返回组件中,则 ref 引用指向容器组件,而不是被包装组件。
高阶组件可以传递所有的props,但是不能传递ref,传毒ref可以使用React.forwardRef:
function withLogging(WrappedComponent) {
class Enhance extends WrappedComponent {
componentWillReceiveProps() {
console.log('Current props', this.props);
console.log('Next props', nextProps);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 把 forwardedRef 赋值给 ref
return <WrappedComponent {...rest} ref={forwardedRef} />;
}
};
// React.forwardRef 方法会传入 props 和 ref 两个参数给其回调函数
// 所以这边的 ref 是由 React.forwardRef 提供的
function forwardRef(props, ref) {
return <Enhance {...props} forwardRef={ref} />
}
return React.forwardRef(forwardRef);
}
const EnhancedComponent = withLogging(SomeComponent);
react组件间的过度动画如何实现
在日常开发中,页面切换时的转场动画是比较基础的一个场景。
当一个组件在显示与消失过程中存在过渡动画,可以很好的增加用户的体验。
在react中实现过渡动画效果会有很多种选择,如react-transition-group,react-motion,Animated,以及原生的CSS都能完成切换动画。
在react中,react-transition-group是一种很好的解决方案,其为元素添加enter,enter-active,exit,exit-active这一系列勾子
可以帮助我们方便的实现组件的入场和离场动画
其主要提供了三个主要的组件:
- CSSTransition:在前端开发中,结合 CSS 来完成过渡动画效果
- SwitchTransition:两个组件显示和隐藏切换时,使用该组件
- TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动画
安装:
npm install react-transition-group --save npm i --save-dev @types/react-transition-group
CSSTransition
其实现动画的原理在于,当CSSTransition的in属性置为true时,CSSTransition首先会给其子组件加上xxx-enter、xxx-enter-active的class执行动画
当动画执行结束后,会移除两个class,并且添加-enter-done的class
所以可以利用这一点,通过css的transition属性,让元素在两个状态之间平滑过渡,从而得到相应的动画效果
当in属性置为false时,CSSTransition会给子组件加上xxx-exit和xxx-exit-active的class,然后开始执行动画,当动画结束后,移除两个class,然后添加-exit-done的class
如下例子:
import { useState } from 'react'
import { CSSTransition } from 'react-transition-group'
import { Button } from 'antd'
const CssTransitionCom: React.FC = () => {
const [show, setShow] = useState(false)
const toggleShow = () => {
setShow(!show)
}
return (
<div style={{ margin: "20px" }}>
<Button type="primary" onClick={toggleShow}>toggleShow CSSTransition</Button>
<CSSTransition
in={show}
timeout={500}
classNames={'CSSTransition'}
unmountOnExit={true}>
<h1>hello CSSTransition</h1>
</CSSTransition>
</div>
)
}
export default CssTransitionCom
对应css样式如下:
.CSSTransition-enter {
opacity: 0;
transform: translateX(100%);
}
.CSSTransition-enter-active {
opacity: 1;
transform: translateX(0);
transition: all 500ms;
}
.CSSTransition-enter-done {
background-color: cadetblue;
}
.CSSTransition-exit {
opacity: 1;
transform: translateX(0);
}
.CSSTransition-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: all 500ms;
}
SwitchTransition
SwitchTransition可以完成两个组件之间切换的炫酷动画
比如有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入
SwitchTransition中主要有一个属性mode,对应两个值:
- in-out:表示新组件先进入,旧组件再移除;
- out-in:表示旧组件先移除,新组件再进入
SwitchTransition组件里面要有CSSTransition,不能直接包裹你想要切换的组件
里面的CSSTransition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性
下面给出一个按钮入场和出场的示例,如下:
import { SwitchTransition, CSSTransition } from "react-transition-group";
import { PureComponent } from "react";
import { Button } from "antd";
export default class SwitchAnimation extends PureComponent<{}, { isOn: boolean }> {
constructor(props: {}) {
super(props);
this.state = {
isOn: true
}
}
btnClick() {
this.setState({ isOn: !this.state.isOn })
}
render() {
const { isOn } = this.state;
return (
<div style={{ margin: "20px" }}>
<SwitchTransition mode="out-in">
<CSSTransition classNames="SwitchAnimation"
timeout={500}
key={isOn ? "SwitchAnimation-on" : "SwitchAnimation-off"}>
<Button type="primary" onClick={this.btnClick.bind(this)}>
{isOn ? "SwitchAnimation-on" : "SwitchAnimation-off"}
</Button>
</CSSTransition>
</SwitchTransition>
</div>
)
}
}
css文件对应如下:
.SwitchAnimation-enter {
transform: translate(100%, 0);
opacity: 0;
}
.SwitchAnimation-enter-active {
transform: translate(0, 0);
opacity: 1;
transition: all 500ms;
}
.SwitchAnimation-exit {
transform: translate(0, 0);
opacity: 1;
}
.SwitchAnimation-exit-active {
transform: translate(-100%, 0);
opacity: 0;
transition: all 500ms;
}
TransitionGroup
当有一组动画的时候,就可将这些CSSTransition放入到一个TransitionGroup中来完成动画
同样CSSTransition里面没有in属性,用到了key属性
TransitionGroup在感知children发生变化的时候,先保存移除的节点,当动画结束后才真正移除
其处理方式如下:
- 插入的节点,先渲染dom,然后再做动画
- 删除的节点,先做动画,然后再删除dom
如下:
import { Button } from 'antd';
import React, { PureComponent } from 'react'
import { CSSTransition, TransitionGroup } from 'react-transition-group';
export default class GroupAnimation extends PureComponent<{}, { friends: string[] }> {
constructor(props: {}) {
super(props);
this.state = {
friends: []
}
}
addFriend() {
this.setState({
friends: [...this.state.friends, "coderwhy"]
})
}
render() {
return (
<div style={{ margin: "20px" }}>
<TransitionGroup>
{
this.state.friends.map((item, index) => {
return (
<CSSTransition classNames="GroupAnimation" timeout={300} key={index}>
<div>{item}</div>
</CSSTransition>
)
})
}
</TransitionGroup>
<Button type='primary' onClick={e => this.addFriend()}>+friend</Button>
</div>
)
}
}
对应css如下:
.GroupAnimation-enter {
transform: translate(100%, 0);
opacity: 0;
}
.GroupAnimation-enter-active {
transform: translate(0, 0);
opacity: 1;
transition: all 500ms;
}
.GroupAnimation-exit {
transform: translate(0, 0);
opacity: 1;
}
.GroupAnimation-exit-active {
transform: translate(-100%, 0);
opacity: 0;
transition: all 500ms;
}
ReactRouter 组件的理解,常用的react router组件
react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面。
路由的本质就是页面的URL发生改变时,页面的显示结果可以根据URL的变化而变化,但是页面不会刷新。
因此,可以通过前端路由可以实现单页(SPA)应用
react-router主要分成了几个不同的包:
- react-router: 实现了路由的核心功能
- react-router-dom: 基于 react-router,加入了在浏览器运行环境下的一些功能
- react-router-native:基于 react-router,加入了 react-native 运行环境下的一些功能
- react-router-config: 用于配置静态路由的工具库
常用组件
react-router-dom的常用的一些组件:
- BrowserRouter、HashRouter:使用两者作为最顶层组件包裹其他组件,分别匹配history模式和hash模式
- Route:Route用于路径的匹配,然后进行组件的渲染,对应的属性如下:
- path 属性:用于设置匹配到的路径
- component 属性:设置匹配到路径后,渲染的组件
- render 属性:设置匹配到路径后,渲染的内容
- exact 属性:开启精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件
- Link、NavLink:通常路径的跳转是使用Link组件,最终会被渲染成a元素,其中属性to代替a标题的href属性
NavLink是在Link基础之上增加了一些样式属性,例如组件被选中时,发生样式变化,则可以设置NavLink的一下属性:
- activeStyle:活跃时(匹配时)的样式
- activeClassName:活跃时添加的class
- switch后边版本更新为Routes:swich组件的作用适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配
- redirect:路由的重定向
hooks
除了一些路由相关的组件之外,react-router还提供一些hooks,如下:
- useHistory:组件内部直接访问history,无须通过props获取
- useParams:获取路由参数
- useLocation:返回当前 URL的 location对象
传参
路由传递参数主要分成了三种形式:
动态路由的方式(params):
路由配置:
{ path: '/routeParamsReceive/:id', element: ParamsReceive,name:"路由接收params参数",hidden:true },
路由跳转:
navigate(`/routeParamsReceive/${paramsValue}`);
获取参数:
const params = useParams();
<h1> 接收到的参数是:{params.id}。</h1>
优点:
- 刷新页面,参数不丢失
缺点:
- 只能传字符串,传值过多url会变得很长
- 参数必须在路由上配置
search传递参数
路由不需要特别配置
路由跳转:
navigate(`/routeSearchReceive?id=${searchValue}&name=${searchValue1}`);
获取参数:
const [searchParams] = useSearchParams();
<h1> 接收到的参数1是:{searchParams.get('id')}。</h1>
<h1> 接收到的参数2是:{searchParams.get('name')}。</h1>
优点:
- 刷新页面,参数不丢失
缺点:
- 只能传字符串,传值过多url会变得很长。
state传参
路由不需要单独配置
路由跳转:
navigate(`/routeStateReceive`,{state:{name:"stateName",value:'stateValue'}});
获取参数:
const location = useLocation();
<h1> 接收到的参数name是:{location.state?.name}。</h1>
<h1> 接收到的参数value是:{location.state?.value}。</h1>
优点:
- 可以传对象
缺点:
<HashRouter>
刷新页面,参数丢失
<HashRouter>
通过state传递参数,刷新页面后参数丢失,官方建议使用<BrowserRouter>
,<BrowserRouter>
页面刷新参数也不会丢失。
将对象序列化为字符串传递
你可以将对象序列化为字符串,并作为查询字符串参数传递。在发送和接收端,分别进行序列化和反序列化操作。
在组件中导航并传递对象参数:
// Home.js
import { useNavigate } from 'react-router-dom';
const Home = () => {
const navigate = useNavigate();
const handleClick = (userData) => {
// 将对象序列化为字符串并作为查询字符串参数传递
const serializedData = encodeURIComponent(JSON.stringify(userData));
navigate(`/user?data=${serializedData}`);
};
return (
<div>
<button onClick={() => handleClick({ name: 'John', age: 25 })}>Go to User Profile</button>
</div>
);
};
export default Home;
在目标组件中获取对象参数:
// UserProfile.js
import { useSearchParams } from 'react-router-dom';
const UserProfile = () => {
const [searchParams] = useSearchParams();
const serializedData = searchParams.get('data');
// 反序列化对象
const userData = JSON.parse(decodeURIComponent(serializedData));
return (
<div>
<p>User Profile Page</p>
<p>Name: {userData.name}</p>
<p>Age: {userData.age}</p>
</div>
);
};
export default UserProfile;
React Router有几种模式,实现原理是什么
React Router 的两种路由模式分别是 HashRouter 和 BrowserRouter。它们的原理都是基于浏览器提供的 history
API 来实现的。下面将从源码的角度详细说明这两种路由模式的原理:
1. HashRouter:
实现原理:
HashRouter
利用浏览器的window.addEventListener('hashchange', listener)
来监听 URL 的 hash 部分(#
后面的部分)的变化。- 当用户点击链接或触发路由跳转时,React Router 会通过
history
包中的createHashHistory
函数创建一个history
对象,该对象监听hashchange
事件。 - 在
HashRouter
组件的componentDidMount
生命周期中,会注册一个hashchange
事件的监听器,用于监听 URL 的 hash 变化。 - 当 URL 的 hash 变化时,
history
对象的监听器会被触发,React Router 根据新的 hash 触发相应的路由匹配和渲染。
源码示例(简化版):
class HashRouter extends React.Component {
componentDidMount() {
window.addEventListener('hashchange', () => {
// 处理 hash 变化的逻辑
// ...
this.forceUpdate(); // 触发重新渲染
});
}
render() {
// 渲染子组件
}
}
2. BrowserRouter:
实现原理:
BrowserRouter
利用浏览器的history.pushState()
和history.replaceState()
方法来操作浏览器的历史记录。- 当用户点击链接或触发路由跳转时,React Router 会通过
history
包中的createBrowserHistory
函数创建一个history
对象,该对象使用pushState
和replaceState
来改变浏览器的历史记录。 - 在
BrowserRouter
组件的componentDidMount
生命周期中,会注册popstate
事件的监听器,用于监听浏览器的前进和后退按钮的点击。 - 当用户点击前进或后退按钮时,
popstate
事件会被触发,history
对象的监听器会被调用,React Router 根据新的路径触发相应的路由匹配和渲染。
源码示例(简化版):
class BrowserRouter extends React.Component {
componentDidMount() {
window.addEventListener('popstate', () => {
// 处理路径变化的逻辑
// ...
this.forceUpdate(); // 触发重新渲染
});
}
render() {
// 渲染子组件
}
}
以上是对 React Router 中两种路由模式的简化原理说明。实际源码中的实现更加复杂,涉及到路由匹配、上下文传递、组件渲染等复杂逻辑,但核心思想是基于浏览器的 history
API 实现路由的监听和导航。
BrowserRouter 与 HashRouter 对⽐
- HashRouter 最简单,每次路由变化不需要服务端接入,根据浏览器的hash来区分 path 就可以;BrowserRouter需要服务端解析 URL 返回页面,因此使用BrowserRouter需要在后端配置地址映射。
- BrowserRouter 触发路由变化的本质是使⽤ HTML5 history API( pushState、replaceState 和 popstate 事件)
- HashRouter 不⽀持 location.key 和 location.state,动态路由需要通过?传递参数。
- Hash history 只需要服务端配置一个地址就可以上线,但线上的 web 应⽤很少使用这种方式。
对immutable的理解,如何应用在react项目中
使用Immutable对象最主要的库是immutable.js
immutable.js 是一个完全独立的库,无论基于什么框架都可以用它
其出现场景在于弥补 Javascript 没有不可变数据结构的问题,通过 structural sharing来解决的性能问题
在React中应用
使用 Immutable可以给 React 应用带来性能的优化,主要体现在减少渲染的次数
在做react性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()中做对比,当返回true执行render方法
Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较
在使用redux过程中也可以结合Immutable,不使用Immutable前修改一个数据需要做一个深拷贝
import '_' from 'lodash';
const Component = React.createClass({
getInitialState() {
return {
data: { times: 0 }
}
},
handleAdd() {
let data = _.cloneDeep(this.state.data);
data.times = data.times + 1;
this.setState({ data: data });
}
}
使用 Immutable 后:
getInitialState() {
return {
data: Map({ times: 0 })
}
},
handleAdd() {
this.setState({ data: this.state.data.update('times', v => v + 1) });
// 这时的 times 并不会改变
console.log(this.state.data.get('times'));
}
react render原理,在什么时候触发
render存在两种形式:
- 类组件中的render方法
- 函数组件的函数本身
触发时机:
- 组件初始化
- 组件的状态或属性发生变化
- 父组件重新渲染导致子组件重新渲染
一旦执行了setState就会执行render方法(无论值是否发生变化),useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染
如何提高组件的渲染效率
在之前文章中,我们了解到render的触发时机,简单来讲就是类组件通过调用setState方法, 就会导致render,父组件一旦发生render渲染,子组件一定也会执行render渲染
父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下:
- shouldComponentUpdate:
- 通过shouldComponentUpdate生命周期函数来比对 state和 props,确定是否要重新渲染
- 默认情况下返回true表示重新渲染,如果不希望组件重新渲染,返回 false 即可
- PureComponent:
- 跟shouldComponentUpdate原理基本一致,通过对 props 和 state的浅比较结果来实现 shouldComponentUpdate
- React.memo
- React.memo用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似。但不同的是, React.memo 只能用于函数组件
- 如果需要深层次比较,这时候可以给memo第二个参数传递比较函数
react diff
跟Vue一致,React通过引入Virtual DOM的概念,极大地避免无效的Dom操作,使我们的页面的构建效率提到了极大的提升
而diff算法就是更高效地通过对比新旧Virtual DOM来找出真正的Dom变化之处
传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),react将算法进行一个优化,复杂度降为O(n)
react中diff算法主要遵循三个层级的策略:
-
tree层级
- DOM节点跨层级的操作不做优化,只会对相同层级的节点进行比较
- 只有删除、创建操作,没有移动操作
-
conponent 层级
- 如果是同一个类的组件,则会继续往下diff运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的
-
element 层级
- 对于比较同一层级的节点们,每个节点在对应的层级用唯一的key作为标识
- 提供了 3 种节点操作,分别为 INSERT_MARKUP(插入)、MOVE_EXISTING (移动)和 REMOVE_NODE (删除)
- 通过key可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置
- 由于dom节点的移动操作开销是比较昂贵的,在只修改文本的情况下,没有key的情况下要比有key的性能更好
对Fiber架构的理解,解决了什么问题
Fiber 的结构
在 React15 以前 React 的组件更新创建虚拟 DOM 和 Diff 的过程是不可中断,如果需要更新组件树层级非常深的话,在 Diff 的过程会非常占用浏览器的线程,而我们都知道浏览器执行JavaScript 的线程和渲染真实 DOM 的线程是互斥的,也就是同一时间内,浏览器要么在执行 JavaScript 的代码运算,要么在渲染页面,如果 JavaScript 的代码运行时间过长则会造成页面卡顿。
基于以上原因 React 团队在 React16 之后就改写了整个架构,将原来数组结构的虚拟DOM,改成叫 Fiber 的一种数据结构,基于这种 Fiber 的数据结构可以实现由原来不可中断的更新过程变成异步的可中断的更新。
Fiber 的数据结构主要长成以下的样子,主要通过 Fiber 的一些属性去保存组件相关的信息。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// 用于连接其他Fiber节点形成Fiber树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
Fiber 主要靠以下属性连成一棵树结构的数据的,也就是 Fiber 链表。
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
那么以上的 Fiber 链表的数据结构有什么特点,就是任何一个位置的 Fiber 节点,都可以非常容易知道它的父 Fiber, 第一个子元素的 Fiber,和它的兄弟节点 Fiber。却不容易知道它前一个 Fiber 节点是谁,这就是 React 中单向链表 Fiber 节点的特点。也正是因为这些即便在协调的过程被中断了,再恢复协调的时候,依然知道当前的父节点和孩子节点等信息。
在React 16 版本中,主要做了以下的操作:
- 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务
- 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行
- dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber 是对 React核心算法(即调和过程)的重写
从编码角度来看,Fiber是 React内部所定义的一种数据结构,它是 Fiber树结构的节点单位,也就是 React 16 新架构下的虚拟DOM
一个 fiber就是一个 JavaScript对象,包含了元素的信息、该元素的更新操作队列、类型.
如何解决
Fiber把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给浏览器(浏览器可以进行渲染),等浏览器不忙的时候再继续执行
即可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber节点
实现的上述方式的是requestIdleCallback方法: window.requestIdleCallback()方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
Fiber 架构可以分为三层:
- Scheduler 调度器 —— 调度任务的优先级,高优任务优先进入 Reconciler。requestIdleCallback在调度器中用到。
- Reconciler 协调器 —— 负责找出变化的组件
- Renderer 渲染器 —— 负责将变化的组件渲染到页面上
相比 React15,React16 多了Scheduler(调度器) ,调度器的作用是调度更新的优先级。
在新的架构模式下,工作流如下:
- 每个更新任务都会被赋予一个优先级。
- 当更新任务抵达调度器时,高优先级的更新任务(记为 A)会更快地被调度进 Reconciler 层;
- 此时若有新的更新任务(记为 B)抵达调度器,调度器会检查它的优先级,若发现 B 的优先级高于当前任务 A,那么当前处于 Reconciler 层的 A 任务就会被中断,调度器会将 B 任务推入 Reconciler 层。
- 当 B 任务完成渲染后,新一轮的调度开始,之前被中断的 A 任务将会被重新推入 Reconciler 层,继续它的渲染之旅,即“可恢复”。
Fiber 架构的核心即是”可中断”、”可恢复”、”优先级”
React 16 是如何解决中断更新时 DOM 渲染不完全的问题呢?
在 React 16 中,Reconciler
与Renderer
不再是交替工作。当Scheduler
将任务交给Reconciler
后,Reconciler
会为变化的虚拟 DOM 打上的标记。
export const Placement = /* */ 0b0000000000010
export const Update = /* */ 0b0000000000100
export const PlacementAndUpdate = /* */ 0b0000000000110
export const Deletion = /* */ 0b0000000001000
Placement
表示插入操作PlacementAndUpdate
表示替换操作Update
表示更新操作Deletion
表示删除操作
整个Scheduler
与Reconciler
的工作都在内存中进行,所以即使反复中断,用户也不会看见更新不完全的 DOM。只有当所有组件都完成Reconciler
的工作,才会统一交给Renderer
。
fiber对生命周期的影响
新老两种架构对 React 生命周期的影响主要在 render 这个阶段,这个影响是通过增加 Scheduler 层和改写 Reconciler 层来实现的。
在 render 阶段,一个庞大的更新任务被分解为了一个个的工作单元,这些工作单元有着不同的优先级,React 可以根据优先级的高低去实现工作单元的打断和恢复。
从 Firber 机制 render 阶段的角度看 react 即将废除的三个生命周期的共同特点是都处于 render 阶段:
componentWillMount
componentWillUpdate
componentWillReceiveProps
参考链接:
jacky-summer.github.io/2021/02/07/…
JSX转换成真实DOM的过程
其渲染流程如下所示:
- 将函数组件或者类组件中的jsx结构,通过babel转换成React.createElement的形式,React.createElement对接收到的参数进行“格式化”,传递给ReactElement函数;
- ReactElement函数将接收到的参数进行整合,最终构造成一个虚拟DOM对象并返回;
- ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM
react 性能优化的手段
-
避免不必要的render:通过shouldComponentUpdate、PureComponent、React.memo
-
使用 Immutable:在做react性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()中做对比,当返回true执行render方法。Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较
-
避免使用内联函数:每次调用render函数时都会创建一个新的函数实例
-
事件绑定方式:避免在render函数中声明函数,通过在constructor绑定this,或者在声明函数的时候使用箭头函数
-
使用 React Fragments 避免额外标记:用户创建新组件时,每个组件应具有单个父标签。这个额外标签除了充当父标签之外,并没有其他作用,这时候则可以使用fragement
-
懒加载组件:从工程方面考虑,webpack存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小。而在react中使用到了Suspense和 lazy组件实现代码拆分功能,基本使用如下:
const johanComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */ './myAwesome.component')); export const johanAsyncComponent = props => ( <React.Suspense fallback={<Spinner />}> <johanComponent {...props} /> </React.Suspense> );
-
服务端渲染:采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面
在React项目中如何捕获错误
错误在我们日常编写代码是非常常见的
举个例子,在react项目中去编写组件内JavaScript代码错误会导致 React 的内部状态被破坏,导致整个应用崩溃,这是不应该出现的现象
作为一个框架,react也有自身对于错误的处理的解决方案。
为了解决出现的错误导致整个应用崩溃的问题,react16引用了错误边界新的概念
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树
错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误
形成错误边界组件的两个条件:
- 使用了 static getDerivedStateFromError()
- 使用了 componentDidCatch()
抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息,如下:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
然后就可以把自身组件的作为错误边界的子组件,如下:
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
下面这些情况无法捕获到异常:
- 事件处理
- 异步代码
- 服务端渲染
- ErrorBoundary组件自身抛出来的错误
对于错误边界无法捕获的异常,如事件处理过程中发生问题并不会捕获到,是因为其不会在渲染期间触发,并不会导致渲染时候问题
这种情况可以使用js的try...catch...语法,如下:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
try {
// 执行操作,如有错误则会抛出
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
return <h1>Caught an error.</h1>
}
return <button onClick={this.handleClick}>Click Me</button>
}
}
除此之外还可以通过监听onerror事件:
window.addEventListener('error', function(event) { ... })
react和vue渲染原理上的区别
可以看这篇文章
Redux
redux
就是一个将状态进行集中管理的容器,遵循三大基本原则:
- 单一数据源
- state 是只读的
- 使用纯函数来执行修改
注意redux并不是只有在react中使用,还可以和其他的界面库使用,比如vue
以下的情景可以使用redux:
- 某个组件的状态,需要共享
- 某个状态需要在任何地方都可以拿到
- 一个组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
首先,用户发出 Action。
store.dispatch(action);
然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。
let nextState = todoApp(previousState, action);
State 一旦有变化,Store 就会调用监听函数。
// 设置监听函数
store.subscribe(listener);
listener
可以通过store.getState()
得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。
function listerner() {
let newState = store.getState();
component.setState(newState);
}
中间件
参考:www.ruanyifeng.com/blog/2016/0…
中间件就是一个函数,对store.dispatch
方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
异步操作
redux-thunk中间件。
默认情况下的dispatch(action)
,action
需要是一个JavaScript
的对象
redux-thunk
中间件会判断你当前传进来的数据类型,如果是一个函数,将会给函数传入参数值(dispatch,getState)
- dispatch函数用于我们之后再次派发action
- getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态
所以dispatch
可以写成下述函数的形式:
const getHomeMultidataAction = () => {
return (dispatch) => {
axios.get("http://xxx.xx.xx.xx/test").then(res => {
const data = res.data.data;
dispatch(changeBannersAction(data.banner.list));
dispatch(changeRecommendsAction(data.recommend.list));
})
}
}
在react中使用
基本概念
state:普通对象,用来存储状态
action:普通对象,用来描述变化
reducer:接收 state 和 action,并返回新的 state 的函数,将state和action连接起来
基本使用
安装:
npm install redux
npm install react-redux
-
创建store
import { createStore } from "redux" const defaultState={ counter:0 } //纯函数 let reducers =(state = defaultState ,action)=>{ switch (action.type){ case "increment": console.log("increment") return { counter:state.counter+1 } case "decrement": return { counter:state.counter-1 } default : return state } } const store = createStore(reducers) export default store
-
全局注入store
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { Provider } from 'react-redux'; import store from './store' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
-
react-redux将redux和react联系起来
import React from "react"; import { connect } from "react-redux" class ClassCom extends React.Component { constructor(props) { super(props); } incre = () => { this.props.increment() } render() { return ( <div> <h1>类组件</h1> <h1>store测试</h1> <div>store的值{this.props.num}</div> <button onClick={this.incre}>increment</button> </div> ) } } // export default ClassCom //该函数作为connect的第一个参数,能拿到state //映射state到组建的props上 function mapStateToProps(state) { return { num: state.counter } } //该函数作为connect的第二个参数,能拿到dispatch //映射dispatch方法到组建的props上 function mapDispatchToProps(dispatch) { return { increment() { dispatch({ type: "increment" }) }, decrement() { dispatch({ type: "decrement" }) } } } //connet函数执行返回一个高阶组件 //调用这个高阶组件,传入当前组件作为参数,返回一个增强的组件 //这个增强的组件props里有store的state和dispach方法 export default connect(mapStateToProps, mapDispatchToProps)(ClassCom)
这个时候组件的props中会有
- 传入的props
- mapStateToProps注入的state
- mapDispatchToProps 注入的dispatch
接下来就可以通过props去使用状态和更新状态
Redux和Vuex的异同点,以及用到的相同的思想
Redux 和 Vuex 都是用于状态管理的库,它们分别用于 React 和 Vue 框架,但在设计和使用上有一些区别:
- 异步处理不同:redux使用中间件实现异步操作,vuex使用action
- 数据可变性不同:redux数据不可变,每次都是返回新的state;vuex数据可变的,mutation中可以直接更改state
- 用途不同:vuex只能搭配vue使用,redux可以搭配多种框架使用
- Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而Vuex其实和Vue的原理一样,是通过 getter/setter来比较的
- 角色不同:vuex中有state,getters,mutation,action,Redux中有store,state,action,reducer
相同点:
- 单一数据源
- 唯一更改state的途径
- 全局注入store