都会触发一个完整的重建 流程;
当卸载一棵树时,对应的 DOM 节点也会被销毁,组件实例将执行 componentWillUnmount() 方法;
当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法, 紧接着 componentDidMount() 方法;
比如下面的代码更改:
- React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对 Counter 进行复用;
1 2 3 4 5 6 7
| <div> <Counter /> </div>
<span> <Counter /> </span>
|
情况二:对比同一类型的元素
当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
比如下面的代码更改:
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性;
比如下面的代码更改:
- 当更新 style 属性时,React 仅更新有所更变的属性。
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight。
如果是同类型的组件元素:
- 组件会保持不变,React 会更新该组件的 props,并且调用 componentWillReceiveProps() 和 componentWillUpdate() 方法;
- 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;
情况三:对子节点进行递归
keys 优化
在遍历列表时,总是会提示一个警告,让我们加入一个 key 属性:
方式一:在最后位置插入数据
方式二:在前面插入数据
- 这种做法,在没有 key 的情况下,所有的 li 都需要进行修改;
当子元素(这里的 li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:
- 在下面这种场景下,key 为 111 和 222 的元素仅仅进行位移,不需要进行任何的修改;
- 将 key 为 333 的元素插入到最前面的位置即可;
key 的注意事项:
- key 应该是唯一的;
- key 不要使用随机数(随机数在下一次 render 时,会重新生成一个数字);
- 使用 index 作为 key,对性能是没有优化的;
render 函数被重复调用问题
只要是修改了 App 中的数据,所有的组件都需要重新 render,进行 diff 算法,性能必然是很低的:
- 事实上,很多的组件没有必须要重新 render;
- 它们调用 render 应该有一个前提,就是依赖的数据(state、 props)发生改变时,再调用自己的 render 方法;
如何来控制 render 方法是否被调用呢?通过 shouldComponentUpdate 方法即可;
shouldComponentUpdate 方法
React 给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为 SCU),这个方法接受参数,并且需要有返回值:
该方法有两个参数:
- 参数一:nextProps 修改之后,最新的 props 属性
- 参数二:nextState 修改之后,最新的 state 属性
该方法返回值是一个 boolean 类型
- 返回值为 true,那么就需要调用 render 方法;
- 返回值为 false,那么久不需要调用 render 方法;
- 默认返回的是 true,也就是只要 state 发生改变,就会调用 render 方法;
比如我们在 App 中增加一个 message 属性:
- jsx 中并没有依赖这个 message,那么它的改变不应该引起重新渲染;
- 但是因为 render 监听到 state 的改变,就会重新 render,所以最后 render 方法还是被重新调用了;
PureComponent
如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
- 我们来设想一下 shouldComponentUpdate 中的各种判断的目的是什么?
- props 或者 state 中的数据是否发生了改变,来决定 shouldComponentUpdate 返回 true 或者 false;
事实上 React 已经考虑到了这一点,所以 React 已经默认帮我们实现好了,如何实现呢?
- 将 class继承自 PureComponent。
高阶函数 memo
若需要在函数式组件中避免 render 被重复调用,我们需要使用一个高阶组件 memo:
- 我们将之前的 Header、Banner、ProductList 都通过 memo 函数进行一层包裹;
- Footer 没有使用 memo 函数进行包裹;
- 最终的效果是,当 counter 发生改变时,Header、Banner、ProductList 的函数不会重新执行,而 Footer 的函数会被重新执行;
setState 不可变数据
state 是不可变的 不能脱离 setState 直接更改 state 的数据 会影响性能 继承 PureComponet 时会出现问题
1 2 3 4 5 6 7 8 9 10 11 12 13
|
const newFriends = [...this.state.friends]; newFriends.push({ name: "tom", age: 30 }); this.setState({ friends: newFriends, });
|
全局事件传递
通过 Context 主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,在 React 中,我们可以依赖一个使用较多的库 events 来完成对应的操作;
我们可以通过 npm 或者 yarn 来安装 events:
events 常用的 API:
创建 EventEmitter 对象:eventBus 对象;
```javascript
import { EventEmitter } from “events”;
// 事件总线: event bus
const eventBus = new EventEmitter();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| - 发出事件:eventBus.emit("事件名称", 参数列表);
- 监听事件:eventBus.addListener("事件名称", 监听函数);
- 移除事件:eventBus.removeListener("事件名称", 监听函数);
### refs 的使用
在 React 的开发模式中,通常情况下不需要、也不建议直接操作 DOM 原生,但是某些特殊的情况,确实需要获取到 DOM 进行某些操作:
- 管理焦点,文本选择或媒体播放; - 触发强制动画; - 集成第三方 DOM 库;
创建 refs 来获取对应的 DOM 目前有三种方式:
1. 方式一:传入字符串(不推荐 官方更新后已删除该用法)
- 使用时通过 this.refs.传入的字符串格式获取对应的元素;
2. 方式二:传入一个对象
- 对象是通过 React.createRef() 方式创建出来的; - 使用时获取到创建的对象其中有一个 current 属性就是对应的元素;
3. 方式三:传入一个函数
- 该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;
- 使用时,直接拿到之前保存的元素对象即可;
若 ref 绑定的是一个组件 则可以直接通过 ref 访问组件的方法
```javascript export default class App extends PureComponent { constructor(props) { super(props);
this.titleRef = createRef(); this.counterRef = createRef(); this.titleEl = null; }
render() { return ( <div> {/* <h2 ref=字符串/对象/函数>Hello React</h2> */} <h2 ref="titleRef">Hello React</h2> {/* 目前React推荐的方式 */} <h2 ref={this.titleRef}>Hello React</h2> <h2 ref={(arg) => (this.titleEl = arg)}>Hello React</h2> <button onClick={(e) => this.changeText()}>改变文本</button> <hr /> <Counter ref={this.counterRef} /> <button onClick={(e) => this.appBtnClick()}>App按钮</button> </div> ); }
changeText() { // 1.使用方式一: 字符串(不推荐, 后续的更新会删除) this.refs.titleRef.innerHTML = "Hello Coderwhy"; // 2.使用方式二: 对象方式 this.titleRef.current.innerHTML = "Hello JavaScript"; // 3.使用方式三: 回调函数方式 this.titleEl.innerHTML = "Hello TypeScript"; }
appBtnClick() { this.counterRef.current.increment(); } }
|
ref 的类型
ref 的值根据节点的类型而有所不同:
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性;
- 你不能在函数组件上使用 ref 属性,因为他们没有实例;
函数式组件是没有实例的,所以无法通过 ref 获取他们的实例:
- 但是某些时候,我们可能想要获取函数式组件中的某个 DOM 元素; 这个时候可以通过 React.forwardRef ,后面学习 hooks 中如何使用 ref;
受控组件和非受控组件
在 React 中,HTML 表单的处理方式和普通的 DOM 元素不太一样:表单元素通常会保存在一些内部的 state。
比如下面的 HTML 表单元素:
- 这个处理方式是 DOM 默认处理 HTML 表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
- 在 React 中,并没有禁止这个行为,它依然是有效的;
- 但是通常情况下会使用 JavaScript 函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
- 实现这种效果的标准方式是使用“受控组件”;
受控组件使用
在 HTML 中,表单元素(如input、 textarea 和 select)之类的表单元素通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
- 我们将两者结合起来,使 React 的 state 成为“唯一数据源”;
- 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
- 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;
由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。
由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| export default class App extends PureComponent { constructor(props) { super(props);
this.state = { username: "", }; }
render() { return ( <div> <form onSubmit={(e) => this.handleSubmit(e)}> <label htmlFor="username"> 用户: {/* 受控组件 */} <input type="text" id="username" onChange={(e) => this.handleChange(e)} value={this.state.username} /> </label> <input type="submit" value="提交" /> </form> </div> ); }
handleSubmit(event) { event.preventDefault(); console.log(this.state.username); }
handleChange(event) { this.setState({ username: event.target.value, }); } }
|
textarea 标签
texteare 标签和 input 比较相似:
select 标签
select 标签的使用也非常简单,只是它不需要通过 selected 属性来控制哪一个被选中,它可以匹配 state 的 value 来选中。
1 2 3 4 5 6 7 8 9 10 11 12 13
| <select name="fruits" onChange={e => this.handleChange(e)} value={this.state.fruits}> <option value="apple">苹果</option> <option value="banana">香蕉</option> <option value="orange">橘子</option> </select>
handleChange(event) { this.setState({ fruits: event.target.value }) }
|
处理多个输入
多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
可以使用 ES6 的一个语法:计算属性名(Computed property )
非受控组件
React 推荐大多数情况下使用 受控组件 来处理表单数据:
- 一个受控组件中,表单数据是由 React 组件来管理的;另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
如果要使用非受控组件中的数据,那么我们需要使用 ref 来从 DOM 节点中获取表单数据。
使用 ref 来获取 input 元素;
- 在非受控组件中通常使用 defaultValue 来设置默认值;
- 同样,input type=”checkbox” 和 input type=”radio” 支持 defaultChecked,select 和 textarea 支 持 defaultValue。
高阶组件
高阶函数的维基百科定义:至少满足以下条件之一:
JavaScript 中比较常见的 filter、map、reduce 都是高阶函数。
那么什么是高阶组件呢?
- 高阶组件的英文是 Higher-Order Components,简称为 HOC;
- 官方的定义:高阶组件是参数为组件,返回值为新组件的函数;
我们可以进行如下的解析:
- 首先, 高阶组件 本身不是一个组件,而是一个函数;
- 其次,这个函数的参数是一个组件,返回值也是一个组件;
![image-20230119203550656](/Users/chenrongqi/Library/Application Support/typora-user-images/image-20230119203550656.png)
高阶组件应用
props 的增强
不修改原有代码的情况下,添加新的 props
1 2 3 4 5
| function enhanceRegionProps(WrappedComponent) { return (props) => { return <WrappedComponent {...props} region="中国" />; }; }
|
利用高阶组件来共享 Context
1 2 3 4 5 6 7 8 9 10 11
| function withUser(WrappedComponent) { return (props) => { return ( <UserContext.Consumer> {(user) => { return <WrappedComponent {...props} {...user} />; }} </UserContext.Consumer> ); }; }
|
登录权限判断
在开发中,我们可能遇到这样的场景:
- 某些页面是必须用户登录成功才能进行进入;
- 如果用户没有登录成功,那么直接跳转到登录页面;
这个时候,就可以使用高阶组件来完成鉴权操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function withAuth(WrappedComponent) { const NewCpn = (props) => { const { isLogin } = props; if (isLogin) { return <WrappedComponent {...props} />; } else { return <LoginPage />; } };
NewCpn.displayName = "AuthCpn";
return NewCpn; }
const AuthCartPage = withAuth(CartPage); // 传递属性 <AuthCartPage isLogin={true} />
|
生命周期劫持
可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function withRenderTime(WrappedComponent) { return class extends PureComponent { UNSAFE_componentWillMount() { this.beginTime = Date.now(); }
componentDidMount() { this.endTime = Date.now(); const interval = this.endTime - this.beginTime; console.log(`${WrappedComponent.name}渲染时间: ${interval}`); }
render() { return <WrappedComponent {...this.props} />; } }; }
|
HOC 的缺陷
HOC 需要在原组件上进行包裹或者嵌套,如果大量使用 HOC,将会产生非常多的嵌套,这让调试变得非常困难;
HOC 可以劫持 props,在不遵守约定的情况下也可能造成冲突;
Hooks 的出现,是开创性的,它解决了很多 React 之前的存在的问题
比如 this 指向问题、比如 hoc 的嵌套复杂度问题等等;
ref 的转发
ref 不能应用于函数式组件,因为函数式组件没有实例,所以不能获取到对应的组件对象
但是,在开发中我们可能想要获取函数式组件中某个元素的 DOM,这个时候我们应该如何操作呢?
- 方式一:直接传入 ref 属性(错误的做法)
- 方式二:通过 forwardRef 高阶函数
forwardRef 函数第二个回调参数就是 props 传递的 ref
1 2 3
| const Profile = forwardRef(function (props, ref) { return <p ref={ref}>Profile</p>; });
|
Portals 的使用
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的 DOM 元 素上的)。
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
- 第二个参数(container)是一个 DOM 元素;
1 2 3 4 5 6 7 8
| class Modal extends PureComponent { render() { return ReactDOM.createPortal( this.props.children, document.getElementById("modal") ) } }
|
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:
然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的
fragment
在之前的开发中,我们总是在一个组件中返回内容时包裹一个 div 元素:
我们又希望可以不渲染这样一个 div 应该如何操作呢?使用 Fragment
Fragment 允许你将子列表分组,而无需向 DOM 添加额外节点;
React 还提供了 Fragment 的短语法:
- 它看起来像空标签 <> </>;
- 但是,如果我们需要在 Fragment 中添加 key,那么就不能使用短语法
StrictMode
StrictMode 是一个用来突出显示应用程序中潜在问题的工具。
- 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI;
- 它为其后代元素触发额外的检查和警告;
- 严格模式检查仅在开发模式下运行;_它们不会影响生产构建_;
可以为应用程序的任何部分启用严格模式:
- 不会对 Header 组件运行严格模式检查;
- 但是,Home 以及它们的所有后代元素都将进行检查;
1 2 3 4
| <Header/> <StrictMode> <Home/> </StrictMode>
|
严格模式检查的是什么
识别不安全的生命周期:
使用过时的 ref API
使用废弃的 findDOMNode 方法
- 在之前的 React API 中,可以通过 findDOMNode 来获取 DOM,不过已经不推荐使用了
检查意外的副作用
检测过时的 context API
React 中的样式
内联样式
内联样式是官方推荐的一种 css 样式的写法:
- style 接受一个采用小驼峰命名属性的 JavaScript 对象,而不是 CSS 字符串;
- 并且可以引用 state 中的状态来设置相关的样式;
内联样式的优点:
内联样式, 样式之间不会有冲突
可以动态获取当前 state 中的状态
内联样式的缺点:
写法上都需要使用驼峰标识
某些样式没有提示
大量的样式, 代码混乱
某些样式无法编写(比如伪类/伪元素)
所以官方依然是希望内联合适和普通的 css 来结合编写
普通的 css
普通的 css 通常会编写到一个单独的文件,之后再进行引入。
这样的编写方式和普通的网页开发中编写方式是一致的:
- 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
- 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
- 但是普通的 css 都属于全局的 css,样式之间会相互影响;
这种编写方式最大的问题是样式之间会相互层叠掉;
CSS modules
css modules 并不是 React 特有的解决方案,而是所有使用了类似于 webpack 配置的环境下都可以使用的。
- 但是,如果在其他项目中使用个,那么我们需要自己来进行配置,比如配置 webpack.config.js 中的 modules: true 等。
React 的脚手架已经内置了 css modules 的配置:
- .css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss 等,之后就可以引用并且进行使用了;
css modules 确实解决了局部作用域的问题,也是很多人喜欢在 React 中使用的一种方案。
但是这种方案也有自己的缺陷:
- 引用的类名,不能使用连接符(.home-title),在 JavaScript 中是不识别的;
- 所有的 className 都必须使用{style.className} 的形式来编写;
- 不方便动态来修改某些样式,依然需要使用内联样式的方式;
如果你觉得上面的缺陷还算 OK,那么你在开发中完全可以选择使用 css modules 来编写,并且也是在 React 中很受欢迎的一种方式。
CSS in JS
实际上,官方文档也有提到过 CSS in JS 这种方案:
- “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;
- 注意此功能并不是 React 的一部分,而是由第三方库提供。 React 对样式如何定义并没有明确态度;
在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。
- 但是在前面的学习中提到过,React 的思想中认为逻辑本身和 UI 是无法分离的,所以才会有了 JSX 的语法。
- 样式呢?样式也是属于 UI 的一部分;
- 事实上 CSS-in-JS 的模式就是一种将样式(CSS)也写入到 JavaScript 中的方式,并且可以方便的使用 JavaScript 的状态;
- 所以 React 有被人称之为 All in JS;
当然,这种开发的方式也受到了很多的批评:
styled-components 插件
批评声音虽然有,但是在我们看来很多优秀的 CSS-in-JS 的库依然非常强大、方便:
- CSS-in-JS 通过 JavaScript 来为 CSS 赋予一些能力,包括类似于 CSS 预处理器一样的样式嵌套、函数定义、逻辑复用、动态修
- 改状态等等;
- 依然 CSS 预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
- 所以,目前可以说 CSS-in-JS 是 React 编写 CSS 最为受欢迎的一种解决方案;
目前比较流行的 CSS-in-JS 的库有:
- styled-components
- emotion
- glamorous
目前可以说 styled-components 依然是社区最流行的 CSS-in-JS 库
安装 styled-components:yarn add styled-components
标签模板字符串
ES6 中增加了模板字符串的语法,模板字符串还有另外一种用法:标签模板字符串(Tagged Template
Literals)。
正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外一种调用方式:
如果我们在调用的时候插入其他的变量:
- 模板字符串被拆分了;
- 第一个元素是数组,是被模块字符串拆分的字符串组合;
- 后面的元素是一个个模块字符串传入的内容;
在 styled component 中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的
styled 的基本使用
styled-components 的本质是通过函数的调用,最终创建出一个组件:
styled-components 会给该 class 添加相关的样式;
另外,它支持类似于 CSS 预处理器一样的样式嵌套:
- 支持直接子代选择器或后代选择器,并且 直接编写样式;
- 可以通过&符号获取当前元素;
- 直接伪类选择器、伪元素等;
props、attrs 属性
props 可以穿透,props 可以被传递给 styled 组件
- 获取 props 需要通过${}传入一个插值函数,props 会作为该函数的参数;
- 这种方式可以有效的解决动态样式的问题;
也可以添加 attrs 属性
styled 高级特性
支持样式继承
styled 可以设置主题
React 中 Ant Design 的使用
React 中添加 class
React 在 JSX 给了开发者足够多的灵活性,可以像编写 JavaScript 代码一样,通过一些逻辑来决定是否添加某些 class:
这个时候我们可以借助于一个第三方的库:classnames,用于动态添加 classnames 的一个库
AntDesign 的安装
使用 npm 或 yarn 安装
1
| $ npm install antd –save
|
或
需要在 index.js 中引入全局的 Antd 样式:
1
| import "antd/dist/antd.css";
|
在 App.js 中就可以使用一些组件了:
考虑一个问题:Antd 是否会将一些没有用的代码(组件或者逻辑代码)引入,造成包很大呢?
antd 官网有提到:antd 的 JS 代码默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from ‘antd’ 就会有按需加载的效果。
使用 craco 修改默认配置
对主题等相关的高级特性进行配置,需要修改 create-react-app 的默认配置。
可以通过 yarn run eject 来暴露出来对应的配置信息进行修改;
但是对于 webpack 并不熟悉的人来说,直接修改 CRA 的配置是否会给你的项目带来负担,甚至会增加项目的隐患和不稳定性
所以,在项目开发中是不建议大家直接去修改 CRA 的配置信息的;
那么如何来进行修改默认配置呢?社区目前有两个比较常见的方案:
- react-app-rewired + customize-cra;(这个是 antd 早期推荐的方案)
- craco;(目前 antd 推荐的方案)
使用步骤
第一步:安装 craco: yarn add @craco/craco
第二步:修改 package.json 文件
原本启动时,是通过 react-scripts 来管理的;
现在启动时,需要通过 craco 来管理;
1 2 3 4 5 6
| "scripts": { "start": "craco start", "build": "craco build", "test": "craco test", "eject": "react-scripts eject" },
|
第三步:在根目录下创建 craco.config.js 文件用于修改默认配置
配置主题
按照 配置主题 的要求,自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能:
- 可以引入 craco-less 来帮助加载 less 样式和修改变量;
安装 craco-less: yarn add craco-less
修改 craco.config.js 中的 plugins:
- 使用 modifyVars 可以在运行时修改 LESS 变量;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const CracoLessPlugin = require("craco-less"); module.exports = { plugins: [ { plugin: CracoLessPlugin, options: { lessLoaderOptions: { lessOptions: { modifyVars: { "@primary-color": "#1DA57A" }, javascriptEnabled: true, }, }, }, }, ], };
|
引入 antd 的样式时,引入 antd.less 文件: import ‘antd/dist/antd.less’
修改后重启 yarn start,如果看到一个绿色的按钮就说明配置成功了。
配置别名
1 2 3 4 5 6 7 8 9 10 11
| const path = require("path"); const resolve = (dir) => path.resolve(__dirname, dir);
module.exports = { webpack: { alias: { "@": resolve("src"), components: resolve("src/components"), }, }, };
|
React中的动画-react-transition-group
react-transition-group介绍
React社区提供了react-transition-group用来完成过渡动画。React曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。
这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装:
npm
1
| $ npm install react-transition-group --save
|
yarn
1
| $ yarn add react-transition-group
|
react-transition-group本身非常小,不会为我们应用程序增加过多的负担。
react-transition-group主要组件
react-transition-group主要包含四个组件:
Transition
- 该组件是一个和平台无关的组件(不一定要结合CSS);
- 在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
CSSTransition
- 在前端开发中,通常使用CSSTransition来完成过渡动画效果
SwitchTransition
TransitionGroup
- 将多个动画组件包裹在其中,一般用于列表中元素的动画;
CSSTransition
CSSTransition是基于Transition组件构建的:
CSSTransition执行过程中,有三个状态:appear、enter、exit;
它们有三种状态,需要定义对应的CSS样式:
- 第一类,开始状态:对于的类是-appear、-enter、exit;
- 第二类:执行动画:对应的类是-appear-active、-enter-active、-exit-active;
- 第三类:执行结束:对应的类是-appear-done、-enter-done、-exit-done;
CSSTransition常见对应的属性:
in:触发进入或者退出状态
- 如果添加了unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉;
- 当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
classNames:动画class的名称
决定了在编写css时,对应的class名称:比如card-enter、card-enter-active、card-enter-done;
timeout: 过渡动画的时间
appear: 是否在初次进入添加动画(需要和in同时为true)
unmountOnExit:退出后卸载组件
官网:https://reactcommunity.org/react-transition-group/transition
CSSTransition对应的钩子函数:主要为了检测动画的执行过程,来完成一些JavaScript的操作
- onEnter:在进入动画之前被触发;
- onEntering:在应用进入动画时被触发;
- onEntered:在应用进入动画结束后被触发;
SwitchTransition
SwitchTransition可以完成两个组件之间切换的炫酷动画:
- 比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入;
- 这个动画在vue中被称之为 vue transition modes;
- react-transition-group中使用SwitchTransition来实现该动画;
SwitchTransition中主要有一个属性:mode,有两个值
- in-out:表示新组件先进入,旧组件再移除;
- out-in:表示就组件先移除,新组建再进入;
SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;
SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性
TransitionGroup
当有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <TransitionGroup> { this.state.names.map((item, index) => { return ( <CSSTransition key={item} timeout={500} classNames="item"> <div> {item} <button onClick={e => this.removeItem(index)}>-</button> </div> </CSSTransition> ) }) } </TransitionGroup>
|
Redux
纯函数
js中的纯函数
函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
在React中,纯函数的概念非常重要,学习Redux时也非常重要
纯函数的维基百科定义:
在程序设计中,若一个函数符合一下条件,那么这个函数被称为纯函数:
- 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
当然上面的定义会过于的晦涩,所以简单总结一下:
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
React中的纯函数
为什么纯函数在函数式编程中非常重要呢?
- 因为你可以安心的写和安心的用;
- 你在写的时候保证了函数的纯度,只是但是实现自己的业务逻辑即可,不需要关心传入的内容或者依赖其他的外部变量;
- 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
所有的React组件都必须像纯函数一样保护它们的props不被更改
Redux介绍
- JS的状态容器,提供了可预测的状态管理 用来控制和追踪state
- 还可以和其他界面库一起使用(Vue等)
Redux核心理念
action
所有数据的变化 需要通过派发(dispatch)action来更新
action是一个JS对象 用来描述这次更新的type和content
reducer
- 是一个纯函数
- 作用就是将传入的state和action联系起来 返回一个state
三大原则
- 单一数据源 整个应用的state被存储在一颗object tree中 并且object tree只存储在一个store中
- state是只读的
- 只能使用纯函数来执行修改
Redux使用
安装redux:
1 2 3
| $ npm install redux --save 或 $ yarn add redux
|
Redux的使用过程
创建一个对象,作为我们要保存的状态:
创建Store来存储这个state
- 创建store时必须创建reducer;
- 我们可以通过 store.getState 来获取当前的state
- 通过action来修改state
- 通过dispatch来派发action;
- 通常action中都会有type属性,也可以携带其他的数据;
- 修改reducer中的处理代码
- reducer是一个纯函数,不需要直接修改state;
- 可以在派发action之前,监听store的变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| // 1.导入redux(不能通过ES6的方式) // import/export 13.2.0开始支持 // commonjs一种实现 -> nodejs const redux = require('redux');
const initialState = { counter: 0 }
// reducer function reducer(state = initialState, action) { switch (action.type) { case "INCREMENT": return { ...state, counter: state.counter + 1 } case "DECREMENT": return { ...state, counter: state.counter - 1 } case "ADD_NUMBER": return { ...state, counter: state.counter + action.num } case "SUB_NUMBER": return { ...state, counter: state.counter - action.num } default: return state; } }
// store(创建的时候需要传入一个reducer) const store = redux.createStore(reducer)
// 订阅store的修改 store.subscribe(() => { console.log("counter:", store.getState().counter); })
// actions const action1 = { type: "INCREMENT" }; const action2 = { type: "DECREMENT" };
const action3 = { type: "ADD_NUMBER", num: 5 }; const action4 = { type: "SUB_NUMBER", num: 12 };
// 派发action store.dispatch(action1); store.dispatch(action2); store.dispatch(action2); store.dispatch(action3); store.dispatch(action4);
|
redux结构划分
将所有的逻辑代码写到一起,当redux变得复杂时代码就难以维护。会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。
注意:node中对ES6模块化的支持
从node v13.2.0开始,node才对ES6模块化提供了支持:
node v13.2.0之前,需要进行如下操作:
- 在package.json中添加属性: “type”: “module”;
- 在执行命令中添加如下选项:node –experimental-modules src/index.js;
node v13.2.0之后,只需要进行如下操作:在package.json中添加属性: “type”: “module”;
redux使用流程图
Redux融入react
自定义connect函数
自定义connect函数简化了在react中每次都需要在compontDidMount和compontWillUnMount中订阅和取消订阅store的操作,在使用时只需要把store和要触发的dispatch函数传入
![image-20230129221016444](/Users/chenrongqi/Library/Application Support/typora-user-images/image-20230129221016444.png)
上面的connect函数有一个很大的缺陷:依赖导入的store
如果将其封装成一个独立的库,需要依赖用于创建的store,应该如何去获取?
正确的做法是提供一个Provider,Provider来自于我们创建的Context,让用户将store传入到value中即可;
context处理store
react-redux使用
安装react-redux:yarn add react-redux
Redux中异步操作
网络请求可以在class组件的componentDidMount中发送,所以可以有这样的结构:
上面的流程有一个缺陷:
- 必须将网络请求的异步代码放到组件的生命周期中来完成;
事实上,网络请求到的数据也属于状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;
redux-thunk
默认情况下的dispatch(action),action需要是一个JavaScript的对象;
redux-thunk可以让dispatch(action函数),action可以是一个函数;
该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
- dispatch函数用于我们之后再次派发action;
- getState函数考虑到一些操作需要依赖原来的状态,用于可以获取之前的一些状态;
安装redux-thunkyarn add redux-thunk
在创建store时传入应用了middleware的enhance函数
- 通过applyMiddleware来结合多个Middleware, 返回一个enhancer;
- 将enhancer作为第二个参数传入到createStore中;
- 定义返回一个函数的action:
- 这里不是返回一个对象了,而是一个函数;
- 该函数在dispatch之后会被执行;
redux官网为我们提供了redux-devtools的工具; 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
安装该工具需要两步:
- 第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可,其他方法可以参考GitHub);
- 第二步:在redux中继承devtools的中间件
1 2 3 4 5 6 7 8 9 10 11
| import { createStore, compose } from 'redux'; import thunkMiddleware from 'redux-thunk';
import reducer from './reducer.js';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;
const store = createStore(reducer, composeEnhancers(storeEnhancer));
export default store;
|
Devtools工具栏如图所示 可以追踪到每一个state的改变
Redux-saga
generator使用
在JavaScript中编写一个普通的函数,进行调用会立即拿到这个函数的返回结果。
如果将这个函数编写成一个生成器函数。
调用iterator的next函数,会销毁一次迭代器,并且返回一个yield的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
|
function* foo() { console.log("111"); yield "Hello"; console.log("222"); yield "World"; console.log("333"); yield "coderwhy"; console.log("444"); }
const result = foo(); console.log(result);
function* generateNumber() { for (var i = 1; i <= 10; i++) { yield i; } }
function* bar() { console.log("1111"); const result = yield new Promise((resolve, reject) => { setTimeout(() => { resolve("Hello Generator"); }, 3000); }); console.log(result); }
const it = bar(); it.next().value.then(res => { it.next(res) })
(preValue = 0, item) => { }; (preState = defaultState, action) => { };
["abc", "cba"].reduce((preValue, item) => { }, 0)
|
redux-saga使用
redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活。
Redux-saga的使用步骤如下
安装redux-saga:yarn add redux-saga
集成redux-saga中间件
- 导入创建中间件的函数;
- 通过创建中间件的函数,创建中间件,并且放到applyMiddleware函数中;
- 启动中间件的监听过程,并且传入要监听的saga;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { createStore, applyMiddleware, compose } from 'redux'; import thunkMiddleware from 'redux-thunk'; import createSagaMiddleware from 'redux-saga';
import saga from './saga'; import reducer from './reducer.js';
const sagaMiddleware = createSagaMiddleware();
const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware); const store = createStore(reducer, storeEnhancer);
sagaMiddleware.run(saga);
export default store;
|
- saga.js文件的编写
- takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLatest,会取消前面的)
- put:在saga中派发action不再是通过dispatch,而是通过put;
- all:可以在yield的时候put多个action;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { takeEvery, put, all, takeLatest } from 'redux-saga/effects'; import axios from 'axios'; import { FETCH_HOME_MULTIDATA, ADD_NUMBER } from './constants'; import { changeBannersAction, changeRecommendAction } from './actionCreators';
function* fetchHomeMultidata(action) { const res = yield axios.get("http://123.207.32.32:8000/home/multidata"); const banners = res.data.data.banner.list; const recommends = res.data.data.recommend.list; yield all([ yield put(changeBannersAction(banners)), yield put(changeRecommendAction(recommends)) ]) }
function* mySaga() { yield all([ takeLatest(FETCH_HOME_MULTIDATA, fetchHomeMultidata), ]); }
export default mySaga;
|
使用Monkey Patching实现中间件
可以利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;
用Monkey Patching实现日志打印
1 2 3 4 5 6 7 8 9 10 11
| function patchLogging(store) { const next = store.dispatch; function dispatchAndLogging(action) { console.log("dispatch前---dispatching action:", action); next(action); console.log("dispatch后---new state:", store.getState()); }
return dispatchAndLogging; }
|
用Monkey Patching实现redux-thunk
redux-thunk的作用:
- redux中利用一个中间件redux-thunk可以让dispatch不再只是处理对象,并且可以处理函数;
我们对dispatch进行转换,这个dispatch会判断传入的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function patchThunk(store) { const next = store.dispatch;
function dispatchAndThunk(action) { if (typeof action === "function") { action(store.dispatch, store.getState) } else { next(action); } }
return dispatchAndThunk; }
|
合并中间件
单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
1 2 3 4 5 6 7 8
| function applyMiddlewares(...middlewares) { middlewares.forEach(middleware => { store.dispatch = middleware(store); }) }
applyMiddlewares(patchLogging, patchThunk);
|
代码的流程:
reducer拆分
可以按如下的目录结构 按模块拆分redux
combineReducers函数
目前合并的方式是通过每次调用reducer函数自己来返回一个新的对象。
redux提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:
- 它也是将传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);
- 在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
- 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;
1 2 3 4 5 6
| const reducer = combineReducers({ counterInfo: counterReducer, homeInfo: homeReducer })
export default reducer;
|
路由
前端路由的原理
前端路由是如何做到URL和内容进行映射呢?监听URL的改变。
URL发生变化,同时不引起页面的刷新有两个办法:
- 通过URL的hash改变URL;
- 通过HTML5中的history模式修改URL;
当监听到URL发生变化时,可以通过自己判断当前的URL,决定到底渲染什么样的内容。
URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
注意:
- hash的优势就是兼容性更好,在老版 IE中都可以运行;
- 但是缺陷是有一个#,显得不像一个真实的路径;
HTML5的history
history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:
- replaceState:替换原来的路径;
- pushState:使用新的路径;
- popState:路径的回退;
- go:向前或向后改变路径;
- forward:向前改变路径;
- back:向后改变路径;
react-router
目前前端流行的三大框架, 都有自己的路由实现:
- Angular的ngRouter
- React的react-router
- Vue的vue-router
React Router的版本4开始,路由不再集中在一个包中进行管理了:
- react-router是router的核心部分代码;
- react-router-dom是用于浏览器的;
- react-router-native是用于原生应用的;
安装react-router:
安装react-router-dom会自动帮助安装react-router的依赖;yarn add react-router-dom
react-router基本使用
react-router最主要的API是给我们提供的一些组件:
BrowserRouter或HashRouter
- Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
- BrowserRouter使用history模式;
- HashRouter使用hash模式;
Link和NavLink:
- 通常路径的跳转是使用Link组件,最终会被渲染成a元素;
- NavLink是在Link基础之上增加了一些样式属性;
- to属性:Link中最重要的属性,用于设置跳转到的路径;
Route:
- Route用于路径的匹配;
- path属性:用于设置匹配到的路径;
- component属性:设置匹配到路径后,渲染的组件;
- exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React, { PureComponent } from "react";
import { BrowserRouter, Link, Route, withRouter } from "react-router-dom";
import "./App.css";
import Home from './pages/home' import About from './pages/about' import Profile from './pages/profile'
class App extends PureComponent { constructor(props) { super(props); }
render() { return ( <div> <Link to="/">首页</Link> <Link to="/about">关于</Link> <Link to="/profile">我的</Link>
<Route exact path="/" component={Home} /> <Route path="/about" component={About} /> <Route path="/profile" component={Profile} />
</div> ); } }
export default withRouter(App);
|
NavLink的使用
需求:路径选中时,对应的a元素变为红色
这个时候,要使用NavLink组件来替代Link组件:
- activeStyle:活跃时(匹配时)的样式;
- activeClassName:活跃时添加的class;
- exact:是否精准匹配;
但是,在选中about或profile时,第一个也会变成红色:
- 原因是/路径也匹配到了/about或/profile;
- 这个时候,可以在第一个NavLink中添加上exact属性;
默认的activeClassName:
- 在默认匹配成功时,NavLink就会添加上一个动态的active class;
- 所以也可以直接编写样式
1 2 3
| <NavLink exact to="/" activeClassName="link-active">首页</NavLink> <NavLink to="/about" activeClassName="link-active">关于</NavLink> <NavLink to="/profile" activeClassName="link-active">我的</NavLink>
|
Switch的作用
- 当匹配到某一个路径时,会发现有一些问题;
- 比如/about路径匹配到的同时,/:userid也被匹配到了,并且最后的一个NoMatch组件总是被匹配到;
原因是什么呢?默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染;
使用了Switch 只要匹配到了第一个,那么后面的就不应该继续匹配了 需要将所有的Route进行包裹
Redirect
Redirect用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
1 2 3 4 5 6
| return this.state.isLogin ? ( <div> <h2>User</h2> <h2>用户名: coderwhy</h2> </div> ): <Redirect to="/login"/>
|
手动路由跳转
如何可以获取到history的对象呢?两种方式
- 方式一:如果该组件是通过路由直接跳转过来的,那么可以直接获取history、location、match对象;
- 方式二:如果该组件是一个普通渲染的组件,那么不可以直接获取history、location、match对象;
如果希望在App组件中获取到history对象,必须满足一下两个条件:
- App组件必须包裹在Router组件之内;
- App组件使用withRouter高阶组件包裹;
注意:使用withRouter必须被BrowserRouter或HashRouter包裹 所以根组件(App)要想使用 就得在index.js当中最外层就用BrowserRouter或HashRouter包裹
参数传递
传递参数有三种方式:
- 动态路由的方式;
- search传递参数;
- Link中to传入对象;
动态路由的概念指的是路由中的路径并不会固定:
- 比如/detail的path对应一个组件Detail;
- 如果将path在Route匹配时写成/detail/:id,那么 /detail/abc、/detail/123都可以匹配到该Route,并且进行显示; 这个匹配规则,就称之为动态路由;
- 通常情况下,使用动态路由可以为路由传递参数。
search传递参数
1
| <NavLink to={`/detail2?name=why&age=18`} activeClassName="link-active">详情2</NavLink>
|
Link中to可以直接传入一个对象(推荐)
1 2 3 4 5 6 7 8
| <NavLink to={{ pathname: "/detail3", search: "name=abc", state: info }} activeClassName="link-active"> 详情3 </NavLink>
|
react-router-config
将所有的路由配置放到一个地方进行集中管理:可以使用react-router-config来完成;
安装react-router-configyarn add react-router-config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import Home from '../pages/home'; import About, { AboutHisotry, AboutCulture, AboutContact, AboutJoin } from '../pages/about'; import Profile from '../pages/profile'; import User from '../pages/user';
const routes = [ { path: "/", exact: true, component: Home }, { path: "/about", component: About, routes: [ { path: "/about", exact: true, component: AboutHisotry }, { path: "/about/culture", component: AboutCulture }, { path: "/about/contact", component: AboutContact }, { path: "/about/join", component: AboutJoin }, ] } ]
export default routes;
|
1 2 3
| import { renderRoutes } from 'react-router-config';
{renderRoutes(routes)}
|
- 还提供了一个matchRoutes方法 可以拿到匹配到的route和match信息 第一个参数为匹配的数组 第二个参数为匹配的路径
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 poicc!