一、state相关
1、setState
-
不可变值(数组修改不能使用 push pop splice 等,这样违反了不可变值,会影响 shouldCompententUpdate 判断)
-
可能是异步更新,通常情况下,
setState()为异步更新数据,在setTimeout()中setState()是同步的,自己定义的 DOM 事件,setState()是同步的,只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout中都是同步的。setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)中的callback拿到更新后的结果。 -
可能会被合并,传入对象,会被合并,结果只执行一次,传入函数,不会被合并,因为函数无法合并
正确修改数组值
// 不能使用 push pop splice 等,这样违反了不可变值,会影响 shouldCompententUpdate 判断
this.setState(() => ({
list1: this.state.list1.concat(100), // 追加
list2: [...this.state.list2, 100], // 追加
list3: this.state.list3.slice(0, 3) // 截取
list4: this.state.list4.filter(item => item > 100) // 筛选
}))
正确修改对象值
this.setState(() => ({
obj1: Object.assign({}, this.state.obj1, {a: 100}),
obj2: {...this.state.obj2, a: 100}
}))
通常情况下,setState()为异步更新数据
const count = this.state.count + 1
this.setState({
count: count
}, () => {
// 这个函数没有默认参数
// 类比 Vue 的 $nextTick
console.log(this.state.count) // 打印更新后的值
})
console.log(this.state.count) // 打印更新前的值
setState()同步更新数据,在setTimeout()中setState()是同步的
setTimeout(() => {
const count = this.state.count + 1
this.setState({ count })
console.log(this.state.count)
})
复制代码
自己定义的 DOM 事件,setState() 是同步的
componentDidMount () {
document.body.addEventListener('click', () => {
const count = this.state.count + 1
this.setState({ count })
console.log(this.state.count)
})
}
【重点】 传入对象,会被合并,结果只执行一次,类似于Object.assgin()
初始值 this.state.count = 0
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
输出值 this.state.count = 1
【重点】 传入函数,不会被合并,因为函数无法合并
初始值 this.state.count = 0
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
输出值 this.state.count = 3
2、state 和 props 区别是啥?
props和state是普通的 JS 对象。虽然它们都包含影响渲染输出的信息,但是它们在组件方面的功能是不同的。即
state是组件自己管理数据,控制自己的状态,可变;props是外部传入的数据参数,不可变;- 没有
state的叫做无状态组件,有state的叫做有状态组件; - 多用
props,少用state,也就是多写无状态组件。
3、React 中的 useState() 是什么?
下面说明useState(0)的用途:
...
const [count, setCounter] = useState(0);
const [moreStuff, setMoreStuff] = useState(...);
...
const setCount = () => {
setCounter(count + 1);
setMoreStuff(...);
...
};
useState 是一个内置的 React Hook。useState(0) 返回一个元组,其中第一个参数count是计数器的当前状态,setCounter 提供更新计数器状态的方法。
4、setState场景题
二、生命周期相关
1、Initialization 初始化
constructor():class的构造函数,并非React独有
2、Mounting 挂载
componentWillMount(): 在组件即将被挂载到页面的时刻自动执行;render(): 页面挂载;componentDidMount(): 组件被挂载到页面之后自动执行;
componentWillMount() 和 componentDidMount(),只会在页面第一次挂载的时候执行,state变化时,不会重新执行
3、Updation 组件更新
componentWillReceiveProps(): props独有的生命周期,执行条件如下:
- 组件要从父组件接收参数;
- 只要父组件的
render()被执行了,子组件的该生命周期就会执行; - 如果这个组件第一次存在于父组件中,不会执行;
- 如果这个组件之前已经存在于父组件中,才会执行;
shouldComponentUpdate(nextProps, nextState): 该生命周期要求返回一个bool类型的结果,如果返回true组件正常更新,如果返回false组件将不会更新;componentWillUpdate(): 组件被更新之前执行,如果shouldComponentUpdate()返回false,将不会被被执行;componentDidUpdate(): 组件更新完成之后执行;
4、Unmounting 组件卸载
componentWillUnmount(): 当组件即将被从页面中剔除的时候,会被执行;
5、生命周期简单使用场景
1、使用shouldComponentUpdate()防止页面进行不必要的渲染
shouldComponentUpdate () {
if (nextProps.content !== this.props.content) {
return true;
}
return false;
}
2、componentWillReceiveProps在初始化render的时候不会执行,它会在Component接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染。这个东西十分好用,但是一旦用错也会造成十分严重的后果。
在componentWillReceiveProps这个回调函数中,我们可以获取到就是props,通过this.props来获取,然后新的props则是通过函数的参数传入,在这里我们可以比较两个props从而对本组件的state作出安全的变更然后重新渲染我们的组件或者是触发子组件内的某些方法。
// 先判断值是否变化,不重复执行应该就不会死循环了
componentWillReceiveProps (nextProps) {
if (!isEqual(this.props.defaultValues, nextProps.defaultValues)) {
this.props.form.setFieldsValue(nextProps.defaultValues)
}
}
3、Ajax 请求页面初始数据componentDidMount()
不能写在render()之中,因为会重复调用,也不能写在componentWillMount()之中,会与RN等其它框架冲突,不然也可以写在这里面,同样是只执行一次。
同样也可以写在构造函数constructor()之中,但是不建议这样做。
import axios from 'axios'
componentDidMount () {
axios.get('/api/todolist').then((res) => {
console.log(res.data);
this.setState(() => ({
list: [...res.data]
}));
}).catch((err) => {
console.log(err);
});
}
6、在shouldComponentUpdate()判断中,有一个有意思的问题,解释为什么 React setState() 要用不可变值
子组件将始终不会渲染,因为在shouldComponentUpdate()中,如数组中push函数,this.state.list.push()已经修改了this.props.list,而this.setState()修改了nextProps.list所以两个值深度比较,将始终相同。shouldComponentUpdate,比较的前后两个值,不能是两份数据,而push函数修改的是原始数据,而不是原始数据返回的副本
7、PureComponent 和 memo
- class类组件中用
PureComponent,无状态组件(无状态)中用memo - PureComponent, SCU中实现了浅比较
- 浅比较已使用大部分情况(尽量不要做深度比较)
PureComponent 与普通 Component 不同的地方在于,PureComponent自带了一个
shouldComponentUpdate(),并且进行了浅比较
8、immutable.js
- 彻底拥抱“不可变值”
- 基础共享数据(不是深拷贝),速度快
9、React的请求应该放在哪个生命周期中?
React的异步请求到底应该放在哪个生命周期里,有人认为在componentWillMount中可以提前进行异步请求,避免白屏,其实这个观点是有问题的.
由于JavaScript中异步事件的性质,当React渲染一个组件时,它不会等待componentWillMount它完成任何事情 - React继续前进并继续render,没有办法“暂停”渲染以等待数据到达。
其次,在React 16进行React Fiber重写后,componentWillMount可能在一次渲染中多次调用.
目前官方推荐的异步请求是在componentDidmount中进行.
如果有特殊需求需要提前请求,也可以在特殊情况下在constructor中请求:
10、React最新的生命周期是怎样的?
React 16之后有三个生命周期被废弃(但并未删除)
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
官方计划在17版本完全删除这三个函数,只保留UNSAVE_前缀的三个函数,目的是为了向下兼容,但是对于开发者而言应该尽量避免使用他们,而是使用新增的生命周期函数替代它们
目前React 16.8 +的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段
挂载阶段:
- constructor: 构造函数,最先被执行,我们通常在构造函数里初始化state对象或者给自定义方法绑定this
- getDerivedStateFromProps:
static getDerivedStateFromProps(nextProps, prevState),这是个静态方法,当我们接收到新的属性想去修改我们state,可以使用getDerivedStateFromProps - render: render函数是纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
- componentDidMount: 组件装载之后调用,此时我们可以获取到DOM节点并操作,比如对canvas,svg的操作,服务器请求,订阅都可以写在这个里面,但是记得在componentWillUnmount中取消订阅
更新阶段:
- getDerivedStateFromProps: 此方法在更新个挂载阶段都可能会调用
- shouldComponentUpdate:
shouldComponentUpdate(nextProps, nextState),有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此生命周期来优化React程序性能 - render: 更新阶段也会触发此生命周期
- getSnapshotBeforeUpdate:
getSnapshotBeforeUpdate(prevProps, prevState),这个方法在render之后,componentDidUpdate之前调用,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用 - componentDidUpdate:
componentDidUpdate(prevProps, prevState, snapshot),该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。
卸载阶段:
- componentWillUnmount: 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作
10.1 getDerivedStateFromProps使用方法,以及和componentWillReceiveProps区别
getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过this访问到class的属性,也并不推荐直接访问属性。而是应该通过参数提供的nextProps以及prevState来进行判断,根据新传入的props来映射到state。
需要注意的是,如果props传入的内容不需要影响到你的state,那么就需要返回一个null,这个返回值是必须的,所以尽量将其写到函数的末尾。
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 当传入的type发生变化的时候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}
10.2、componentWillReceiveProps废弃的原因
componentWillReceiveProps的写法并不存在什么功能上的问题,将componentWillReceiveProps废弃的原因也并不是因为功能问题,而是性能问题。
当外部多个属性在很短的时间间隔之内多次变化,就会导致componentWillReceiveProps被多次调用。这个调用并不会被合并,如果这次内容都会触发异步请求,那么可能会导致多个异步请求阻塞。
而setState操作是会通过transaction进行合并的,由此导致的更新过程是批量的,而react中大部分的更新过程的触发源都是setState,所以render触发的频率并不会非常频繁 因此在使用getDerivedStateFromProps的时候,遇到了上面说的props在很短的时间内多次变化,也只会触发一次render,也就是只触发一次getDerivedStateFromProps,从而提高了性能。
10.3、getSnapshotBeforeUpdate使用场景
class A extends React.Component{
state = {newsArr:[]}
// 插件挂载完毕
componentDidMount(){
setInterval(()=>{
// 获取原状态
const {newsArr} = this.state
// 模拟一条新闻
const news = '新闻'+(newsArr.length+1)
// 更新状态
this.setState({newsArr:[news,...newsArr]})
},1000);
}
// 获取更新之前的快照
getSnapshotBeforeUpdate(){
// 获取在更新之前的高度
return this.refs.list.scrollHeight
}
// 更新完成
componentDidUpdate(preProps,preState,height){
// 每次更新 将scrollTop 的大小增加 一条新闻的高度
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
}
render(){
return(
<div className='list' ref='list'>
{
this.state.newsArr.map((n,index)=>{
return <div className='news' key={index}>{n}</div>
})
}
</div>
)
}
}
ReactDOM.render(<A />,document.getElementById('test'))
11、在构造函数调用 super 并将 props 作为参数传入的作用是啥?
在调用 super() 方法之前,子类构造函数无法使用this引用,ES6 子类也是如此。将 props 参数传递给 super() 调用的主要原因是在子构造函数中能够通过this.props来获取传入的 props。
传递 props
class MyComponent extends React.Component {
constructor(props) {
super(props);
console.log(this.props); // { name: 'sudheer',age: 30 }
}
}
附:
ES6 中的继承和 super 的用法大家都不会陌生,可是一问到 super 到底是什么,估计很对人都会回答不上来。在 ES6 中,super 是一个特殊的语法,而且它比 this 还要特殊,有很多用法上的限制。
super类似于ES5语法中的call继承
class A{
constructor(n){
console.log(n); //=>100;
this.x = 100;
}
getX(){
console.log(this.x);
}
}
class B extends A{//=>extends 类似实现原型继承
constructor(){
//=>类似于call的继承:在这里super相当于把A的constructor给执行了,
//并且让方法中的this是B的实例,super当中传递的实参都是在给A的constructor传递。
super(100);
this.y = 200;
}
getY(){
console.log(this.y);
}
}
let f = new B();
12:如何避免组件的重新渲染?
React 中最常见的问题之一是组件不必要地重新渲染。React 提供了两个方法,在这些情况下非常有用:
-
React.memo():这可以防止不必要地重新渲染函数组件 -
PureComponent:这可以防止不必要地重新渲染类组件
这两种方法都依赖于对传递给组件的props的浅比较,如果 props 没有改变,那么组件将不会重新渲染。虽然这两种工具都非常有用,但是浅比较会带来额外的性能损失,因此如果使用不当,这两种方法都会对性能产生负面影响。
通过使用 React Profiler,可以在使用这些方法前后对性能进行测量,从而确保通过进行给定的更改来实际改进性能。
三、传参
1、事件传参
通过.bind()传参
1.如果你不绑定this.handleClick方法,那么在事件发生并且精确调用这个方法时,方法内部的this会丢失指向。
2.这不是React的原因,这是JavaScript中本来就有的。**如果你传递一个函数名给一个变量,然后通过在变量后加括号()来调用这个方法,此时方法内部的this的指向就会丢失;**以React中的onClick()为例,由于onClick是一个点击事件的中间变量名,所以当调用onClick()函数的时候其内部this的指向也会丢失,所以我们在使用的时候需要bind()函数来提前绑定好内部this的指向,防止this丢失。
<div onClick={this.getParams1.bind(this, 'id1', 'title1')}>get params 1</div>
getParams1 (id, title, event) {
console.log('id', id)
console.log('title', title)
console.log('event', event) // 最后一个参数为Event对象
}
复制代码
通过箭头函数传参
<div onClick={(event) => { this.getParams2('id2', 'title2', event) }}>get params 2</div>
getParams2 (id, title, event) {
console.log('id', id)
console.log('title', title)
console.log('event', event)
}
复制代码
两种绑定事件
<button onClick={bindClick1.bind(this)}> 使用 .bind(this) </button>
<button onClick={bindClick2}> 箭头函数 </button>
// 使用 class 的自带函数,需要重定向 this
bindClick1 () { alert('bindClick1') }
// 使用静态方法,使用箭头函数不需要使用 bind(this)
bindClick2 = () => { alert('bindClick2') }
2、表单传参
<div>
<label htmlFor="userName"></label>
<input value={this.state.userName} onChange={this.handleInputChange.bind(this)} />
</div>
// 实现类似双向数据绑定
handleInputChange (even t) {
const userName = event.target.value
this.setState(() => ({
userName
}))
// 下面这种写法会报错,因为 this.setState 传递一个函数时,为异步方法,等异步执行时已经没有 event
this.setState(() => ({
userName = event.target.value
}))
}
复制代码
3、组件传参
普通参数/函数
// 父组件
<div>
<Child text={this.state.text} />
</div>
// 子组件
<div>
<p>{this.props.text}</p>
</div>
复制代码
属性类型检查
import PropTypes from 'prop-types'
// 对传递的参数强校验
TodoItem.propTypes = {
content: PropTypes.string.isRequired, // 限制为字符串且必传
}
四、组件类型
1、什么是受控组件和非受控组件
class Input extends Component{
constructor(){
super();
this.state = {val:'100'}
}
handleChange = (e) =>{ //e是事件源
let val = e.target.value;
this.setState({val});
};
render(){
return (<div>
<input type="text" value={this.state.val} onChange={this.handleChange}/>
{this.state.val}
</div>)
}
}
-
受状态控制的组件,必须要有onChange方法,否则不能使用 受控组件可以赋予默认值(官方推荐使用 受控组件) 实现双向数据绑定
-
非受控也就意味着我可以不需要设置它的state属性,而通过ref来操作真实的DOM
-
react ref场景的使用场景及使用方式,ref主要用来做什么的
-
用来直接操作DOM,来完成一些操作,获取元素的宽度来完成某些动画
-
焦点,选中,动画等;获取/失去输入框焦点
-
大多数情况下,建议使用受控组件。有一种称为非受控组件的方法可以通过使用
Ref来处理表单数据。在非受控组件中,Ref用于直接从DOM访问表单值,而不是事件处理程序。我们使用
Ref构建了相同的表单,而不是使用React状态。 我们使用React.createRef()定义Ref并传递该输入表单并直接从handleSubmit方法中的DOM访问表单值。
2、函数/无状态/展示组件
class Sum extends Component{
constructor(){
super();
this.state = {result:''}
}
//通过ref设置的属性 可以通过this.refs获取到对应的dom元素
handleChange = () =>{
let result = this.refs.a.value + this.b.value;
this.setState({result});
};
render(){
return (
<div onChange={this.handleChange}>
<input type="number" ref="a"/>
{/*x代表的真实的dom,把元素挂载在了当前实例上*/}
<input type="number" ref={(x)=>{
this.b = x;
}}/>
{this.state.result}
</div>
)
}
}
函数或无状态组件是一个纯函数,它可接受接受参数,并返回react元素。这些都是没有任何副作用的纯函数。这些组件没有状态或生命周期方法。
- 纯函数
- 输入props,输出JSX
- 没有实例
- 没有生命周期
- 没有state
- 不能扩展其它方法
3、异步组件
React 16.6 引入了一些新特性,在 React 组件上使用这些新特性只需要用少量代码就能完成强大的功能。 React.lazy 和 Suspense 是其中的两个新特性;React 16.6 之前需要使用第三方库“react-loadable”它提供的功能和vue的高级异步组件一样
// 引入需要异步加载的组件
const LazyComponent = React.lazy(() => import('./lazyDemo') )
// 使用异步组件,异步组件加载中时,显示fallback中的内容
<React.Suspense fallback={<div>异步组件加载中</div>}>
<LazyComponent />
</React.Suspense>
vue中加载异步组件
Vue.js 允许将组件定义为一个工厂函数,动态地解析组件的定义。Vue.js 只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染,从而达到减少请求的目的(下面实例代码来自vue官网) vue异步组件:
```
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 require 语法告诉 webpack
// 自动将编译后的代码分割成不同的块,
// 这些块将通过 Ajax 请求自动下载。
require(['./my-async-component'], resolve)
})
```
Webpack 2 + ES2015 的语法返回一个promise:
```
Vue.component(
'async-webpack-example',
() => import('./my-async-component')
)
```
直接局部注册:
```
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
```
五、高阶组件
高阶组件(HOC)是接受一个组件并返回一个新组件的函数。基本上,这是一个模式,是从 React 的组合特性中衍生出来的,称其为纯组件,因为它们可以接受任何动态提供的子组件,但不会修改或复制输入组件中的任何行为。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
复制代码
HOC 可以用于以下许多用例
- 代码重用、逻辑和引导抽象
- 渲染劫持
- state 抽象和操作
- props 处理
1、高阶组件不是一种功能,而是一种模式
// 高阶组件 基本用法
const HOCFactory = (Component) => {
class HOC extends React.Component {
// 在此定义多个组件的公共逻辑
render () {
return <Component {...thi.props} /> // 返回拼装的结果
}
}
return HOC
}
const MyComponent1 = HOCFactory(WrappedComponent1)
const MyComponent2 = HOCFactory(WrappedComponent2)
实际案例
import React from 'react'
// 高阶组件
const withMouse = (Component) => {
class withMouseComponent extends React.Component {
constructor(props) {
super(props)
this.state = { x: 0, y: 0 }
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
{/* 1. 透传所有 props 2. 增加 mouse 属性 */}
<Component {...this.props} mouse={this.state}/>
</div>
)
}
}
return withMouseComponent
}
const App = (props) => {
const a = props.a
const { x, y } = props.mouse // 接收 mouse 属性
return (
<div style={{ height: '500px' }}>
<h1>The mouse position is ({x}, {y})</h1>
<p>{a}</p>
</div>
)
}
export default withMouse(App) // 返回高阶函数
2、高阶组件不是组件,是 增强函数,可以输入一个元组件,返回出一个新的增强组件
-
属性代理 (Props Proxy) 在我看来属性代理就是提取公共的数据和方法到父组件,子组件只负责渲染数据,相当于设计模式里的模板模式,这样组件的重用性就更高了
function proxyHoc(WrappedComponent) { return class extends React.Component { render() { const newProps = { count: 1 } return <WrappedComponent {...this.props} {...newProps} /> } } } -
反向继承
const MyContainer = (WrappedComponent)=>{ return class extends WrappedComponent { render(){ return super.render(); } } }
3、Render Props
Render Props 核心思想:通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件
class Factory extends React.Component {
constructor () {
this.state = {
/* 这里 state 即多个组件的公共逻辑的数据 */
}
}
/* 修改 state */
render () {
return <div>{this.props.render(this.state)}</div>
}
}
const App = () => {
/* render 是一个函数组件 */
<Factory render={
(props) => <p>{props.a} {props.b}...</p>
} />
}
相对高阶组件的优点:
- 不用担心props的命名冲突的问题
- 可以溯源,子组件的props一定来自父组件。
- 是动态构建的,页面在渲染后,可以动态地决定渲染哪个组件。
- 所有能用HOC完成的事情,Render Props都可以做,且更加灵活。
- 除了功能复用,还可以用作两个组件的单向数据传递。
六、React Hooks
1、下面说明useState(0)的用途:
...
const [count, setCounter] = useState(0);
const [moreStuff, setMoreStuff] = useState(...);
...
const setCount = () => {
setCounter(count + 1);
setMoreStuff(...);
...
};
复制代码
useState 是一个内置的 React Hook。useState(0) 返回一个元组,其中第一个参数count是计数器的当前状态,setCounter 提供更新计数器状态的方法。
咱们可以在任何地方使用setCounter方法更新计数状态-在这种情况下,咱们在setCount函数内部使用它可以做更多的事情,使用 Hooks,能够使咱们的代码保持更多功能,还可以避免过多使用基于类的组件。
2、使用hook的好处
(1)classes真的太让人困惑了!
我们用class来创建react组件时,还有一件很麻烦的事情,就是this的指向问题。为了保证this的指向正确,我们要经常写这样的代码:this.handleClick = this.handleClick.bind(this),或者是这样的代码:<button onClick={() => this.handleClick(e)}>。一旦我们不小心忘了绑定this,各种bug就随之而来,很麻烦。
(2)生命周期钩子函数里的逻辑太乱了吧!
我们通常希望一个函数只做一件事情,但我们的生命周期钩子函数里通常同时做了很多事情。比如我们需要在componentDidMount中发起ajax请求获取数据,绑定一些事件监听等等。同时,有时候我们还需要在componentDidUpdate做一遍同样的事情。当项目变复杂后,这一块的代码也变得不那么直观。
3、useEffect
我们写的有状态组件,通常会产生很多的副作用(side effect),比如发起ajax请求获取数据,添加一些监听的注册和取消注册,手动修改dom等等。我们之前都把这些副作用的函数写在生命周期函数钩子里,比如componentDidMount,componentDidUpdate和componentWillUnmount。而现在的useEffect就相当与这些声明周期函数钩子的集合体。它以一抵三。
在React中与watch比较相似的功能是Effect Hook,使用它可以让你在函数组件中执行副作用操作,先来看一下代码
import React, { useEffect, useState } from 'react'
export default function() {
// useState传入要初始化的状态数据,然后会返回一个数组
// 数组第一项为声明的数据,而第二个参数是一个方法,用于调用
// 修改数据
const [searchValue, setSearchValue] = useState('')
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
// 在react修改数据需要调用useState返回的方法
setSearchValue(e.target.value);
}
// useEffect接受两个参数,第一个是回调函数,第二个是要监听变化的属性,是一个数组
useEffect(() => {
// 当代码首次调用useEffect会进入这个回调函数,然后
// 当serchValue 发生变化时,会再次进入到这里
console.log(111)
},[searchValue])
return (
<div>
<input value={searchValue} onChange={handleChange}></input>
</div>
);
}
复制代码
如上代码我们使用useEffect来监听searchValue的变化,然后触发新的逻辑,但是看到上面代码,我们并没有发现取消effect的方法,那么如何取消呢?
useEffect第二个参数是一个数组,通过给数组传入要监听的变量来实现数据监听,但是却没有办法去取消这个监听,所以我们需要曲线救国,就像下面代码这样
const [isWatch] = useState(true)
useEffect(() => {
// 通过isWatch来判断是否进行监听逻辑变化
if(isWatch) {
// 监听数据变化
console.log(searchValue)
}
},[isWatch, searchValue])
4、useMemo
在说到在React中模拟计算属性之前,我们先要了解一些React Hook的规则。
Hook 使用了 JavaScript 的闭包机制,
1.只有最顶层使用hook 不能在循环,条件调用hook.
2.只能再react函数中使用hook,不能再普通的js函数中调用hook
3.自定义hook 必须以use 开头
同时呢?因为state的变化会引起整个函数重新执行,那么假如我们在代码里面写了这样一段逻辑
const [firstName, setFirstName] = useState('')
const [lastName, setLastName ] = useState('')
const [other,setOther] = useState('')
// 使用 useMemo可以模仿Vue中的计算属性
const name = firstName + "·" + lastName;
我们可以利用useMemo来模拟计算属性,如下代码
import React, { useMemo, useState } from 'react'
export default function() {
const [firstName, setFirstName] = useState('')
const [lastName, setLastName ] = useState('')
// 使用 useMemo可以模仿Vue中的计算属性,当firstName与`lastName`任何一个发生变化
//都会触发`name`重新计算,但是对于其他属性变化,并不会引起重新计算
const name = useMemo(() => firstName + '·' + lastName,[firstName,lastName])
const handleChange = (method: Function, e: React.ChangeEvent<HTMLInputElement> ) => {
method(e.target.value)
}
return (
<div>
<div>
<label>firstName</label>
<input
value={firstName}
onChange={(e) => handleChange(setFirstName, e)}
/>
<label>lastName</label>
<input
value={lastName}
onChange={(e) => handleChange(setLastName, e)}
/>
</div>
<div>用户名:{name}</div>
</div>
);
}
复制代码
但是呢,在Vue中计算属性既可以get,也可以set,这一点我们是无法使用useMemo来模拟的,当然如果有小伙伴知道如何模拟,麻烦下方评论区告诉我,谢谢。
5、useCallback
useCallback跟useMemo比较类似,但它返回的是缓存的函数。我们看一下最简单的用法:
const fnA = useCallback(fnB, [a])
上面的useCallback会将我们传递给它的函数fnB返回,并且将这个结果缓存;当依赖a变更时,会返回新的函数。既然返回的是函数,我们无法很好的判断返回的函数是否变更
知道useCallback有什么样的特点,那有什么作用呢?
使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
```
import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
const [count, setCount] = useState(1);
const [val, setVal] = useState('');
const callback = useCallback(() => {
return count;
}, [count]);
return <div>
<h4>{count}</h4>
<Child callback={callback}/>
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<input value={val} onChange={event => setVal(event.target.value)}/>
</div>
</div>;
}
function Child({ callback }) {
const [count, setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
}, [callback]);
return <div>
{count}
</div>
}
```
七、virtual DOM
1、简述一下virtual DOM 如何工作?
- 当数据发生变化,比如setState时,会引起组件重新渲染,整个UI都会以virtual dom的形式重新渲染
- 然后收集差异也就是diff新的virtual dom和老的virtual dom的差异
- 最后把差异队列里的差异,比如增加节点、删除节点、移动节点更新到真实的DOM上
八、如何在React中应用样式
九、portals 传送门
使用 Portals 渲染到 body 上,fixed 元素要放在 body 上,有更好的浏览器兼容。
常见使用场景:
-
父组件 overflow: hidden , 但是子组件又想展示;
-
父组件的 z-index 太小;
-
fixed 需要放在 body 第一层;
import ReactDOM from 'react-dom' render () { return ReactDOM.creatPortal( <div>{this.props.children}</div>, document.body ) }
十、context 上下文
使用场景:公共信息(语言、主题)传递给每个组件,如果组件层级过多,用props传递就会繁琐,用 redux 小题大做。
import React from 'react'
// 创建 Context 填入默认值(任何一个 js 变量)
export const {Provider,Consumer} = React.createContext("默认名称")
// 在最上级组件中
constructor (props) {
super(props)
this.state = { theme: 'light' }
}
render () {
// 这里使用 this.state.theme 是为了可以修改,初始化的值为默认值,不能修改
// value 中放共享的数据
return (
<Provider value={this.state.theme}>
....
<button onClick={this.changeTheme}></button>
</Provider>
)
}
// 子组件中调用
import { Consumer } from "./index";//引入父组件的Consumer容器
render () {
return (
// Consumer 容器,可以拿到上文传递下来的 theme 属性,并可以展示对应的值
<Consumer>
{ theme => <div>子组件。获取父组件的值: {theme} </div> }
</Consumer>
)
}
十一、react16的错误边界(Error Boundaries)是什么
部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。
import React from 'react';
import ReactDOM from 'react-dom';
class ErrorBoundary extends React.Component{
constructor(props) {
super(props);
this.state={hasError:false};
}
componentDidCatch(err,info) {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) {
return <h1>Something Went Wrong</h1>
}
return this.props.children;
}
}
class Page extends React.Component{
render() {
return (
<ErrorBoundary>
<Clock/>
</ErrorBoundary>
)
}
}
class Clock extends React.Component{
render() {
return (
<div>hello{null.toString()}</div>
)
}
}
ReactDOM.render(<Page/>,document.querySelector('#root'));
十二、redux
1、Redux 单项数据流图
2、什么是Redux及其工作原理
Redux 是 React的一个状态管理库,它基于flux。 Redux简化了React中的单向数据流。 Redux将状态管理完全从React中抽象出来。
它是如何工作的
在React中,组件连接到 redux ,如果要访问 redux,需要派出一个包含 id和负载(payload) 的 action。action 中的 payload 是可选的,action 将其转发给 Reducer。
当reducer收到action时,通过 swithc...case 语法比较 action 中type。 匹配时,更新对应的内容返回新的 state。
当Redux状态更改时,连接到Redux的组件将接收新的状态作为props。当组件接收到这些props时,它将进入更新阶段并重新渲染 UI。
Redux 循环细节
让我们详细看看整个redux 循环细节。
Action: Action 只是一个简单的json对象,type 和有payload作为键。type 是必须要有的,payload是可选的。下面是一个 action 的例子。
// action
{
type:"SEND_EMAIL",
payload: data
};
Action Creators:这些是创建Actions的函数,因此我们在派发action时不必在组件中手动编写每个 action。 以下是 action creator 的示例。
// action creator
export function sendEamil(data) {
return { type:"SEND_EMAIL", payload: data};
}
Reducers:Reducers 是纯函数,它将 action和当前 state 作为参数,计算必要的逻辑并返回一个新的state。 这些 Reducers 没有任何副作用。 它不会改变 state 而是总是返回 state 。
export default function emailReducer(state = [], action){
switch(action.type) {
case "SEND_EMAIL": return Object.assign({}, state, {
email: action.payload
});
default: return state;
}
}
组件如何与 redux 进行连接
mapStateToProps:此函数将state映射到 props 上,因此只要state发生变化,新 state 会重新映射到 props。 这是订阅store的方式。
mapDispatchToProps:此函数用于将 action creators 绑定到你的props 。以便我们可以在第12行中使用This . props.actions.sendemail()来派发一个动作。
connect和bindActionCreators来自 redux。 前者用于连接 store ,如第22行,后者用于将 action creators 绑定到你的 props ,如第20行。
// import connect
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
// import action creators
import * as userActions from '../../../actions/userActions';
export class User extends React.Component {
handleSubmit() {
// dispatch an action
this.props.actions.sendEmail(this.state.email);
}
}
// you are mapping you state props
const mapStateToProps = (state, ownProps) => ({user: state.user})
// you are binding your action creators to your props
const mapDispatchToProps = (dispatch) => ({actions: bindActionCreators(userActions, dispatch)})
export default connect(mapStateToProps, mapDispatchToProps)(User);
redux-thunk是什么东西
在此之前先回顾下redux的工作流程:action-> dispatcher -> reducer -> store tree changed -> relative components re-render -> UI changed。这整个过程都是同步的,只要action被dispatch到reducer,对应state发生变化,UI就立即更新。
//actionCreators
import {INCREMENT,DECREMENT} from './actionType'
//从actionType中引入action的类型
const increment = (payload) =>{
return{
type:INCREMENT,
payload
}
}
//模拟异步写在这,action就可以是一个函数
const asyncincrement=()=>{
return (dispath)=>{
setTimeout(()=>{
let payload = 8
dispath(increment(payload))
},2000)
}
}
//使用thunk前是一个扁平的对象
const decrement = () =>{
return{
type:DECREMENT
}
}
export {asyncincrement,decrement}
然后就可以在connect中这样写
import {connect} from 'react-redux'
import {asyncincrement,decrement} from './actionCreators'
//解构出方法
const mapState=(state)=>{
return {
count:state.count
}
}
const mapDispatch=(dispatch)=>{
return{
asyncincrement:()=>{
dispatch(asyncincrement())
//派发操作
console.log('connect +')
},
decrement:()=>{
dispatch(decrement())
console.log('connect -')
}
}
}
export default connect(mapState,mapDispatch)
十三、请简单谈一下react的事件机制
- 当用户在为onClick添加函数时,React并没有将Click时间绑定在DOM上面。
- 而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层SyntheticEvent(负责所有事件合成)
- 所以当事件触发的时候,对使用统一的分发函数dispatchEvent将指定函数执行。
- 为什么要事件合成机制
-
- 更好的兼容性和跨平台,摆脱传统DOM事件
- 挂载到document,减少内存消耗,避免频繁解绑
- 方便事件的统一管理,如:事务机制
十四、React性能优化
- 渲染列表时加Key
- 自定义事件、DOM事件及时销毁
- 合理使用异步组件
- 减少函数 bind this 的次数
- 合理使用 shouldComponentUpdate、PureComponent 和 memo
- 合理使用 ImmutableJS
- webpack层面优化
- 前端通用是能优化,如图片懒加载
- 使用SSR
十五、JSX 本质
- JSX 等同于 Vue 模板
- Vue 模板不是 html
- JSX 也不是 JS
讲JSX语法,通过 React.createElement()编译成Dom,BABEL 可以编译JSX
流程:JSX => React.createElement() => 虚拟DOM (JS对象) => 真实DOM
React 底层会通过 React.createElement() 这个方法,将 JSX 语法转成JS对象,React.createElement() 可以接收三个参数,第一个为标签名称,第二参数为属性,第三个参数为内容
createElement() 根据首字母大小写来区分是组件还是HTML标签,React规定组件首字母必须大写,HTML规定标签首字母必须小写
// 第一个参数为 标签(tag) 可为 'div'标签名 或 List组件
// 第二个参数为:属性(props)
// 第三个参数之后都为子节点(child),可以在第三个参数传一个数组,也可以在第三、四、五....参数中传入
React.createElement('tag', null, [child1, chlild2, child3])
或者
React.createElement('tag', { className: 'class1' }, child1, chlild2, child3)
十六、React Router
react-router-dom是应用程序中路由的库。 React库中没有路由功能,需要单独安装react-router-dom。
react-router-dom 提供两个路由器BrowserRouter和HashRoauter。前者基于url的pathname段,后者基于hash段。
前者:http://127.0.0.1:3000/article/num1
后者:http://127.0.0.1:3000/#/article/num1(不一定是这样,但#是少不了的)
react-router-dom 组件
-
BrowserRouter和HashRouter是路由器。 -
Route用于路由匹配。 -
Link组件用于在应用程序中创建链接。 它将在HTML中渲染为锚标记。 -
NavLink是突出显示当前活动链接的特殊链接。 -
Switch不是必需的,但在组合路由时很有用。 -
Redirect用于强制路由重定向
下面是组件中的Link、NavLink和Redirect 的例子
// normal link
<Link to="/gotoA">Home</Link>
// link which highlights currentlu active route with the given class name
<NavLink to="/gotoB" activeClassName="active">
React
</NavLink>
// you can redirect to this url
<Redirect to="/gotoC" />
以下是 react router 组件的示例。 如果你查看下面的示例,我们将匹配路径并使用Switch和Route呈现相应的组件。
import React from 'react'
// import react router DOM elements
import { Switch, Route, Redirect } from 'react-router-dom'
import ComponentA from '../common/compa'
import ComponentB from '../common/compb'
import ComponentC from '../common/compc'
import ComponentD from '../common/compd'
import ComponentE from '../common/compe'
const Layout = ({ match }) => {
return(
<div className="">
<Switch>
<Route exact path={`${match.path}/gotoA`} component={ComponentA} />
<Route path={`${match.path}/gotoB`} component={ComponentB} />
<Route path={`${match.path}/gotoC`} component={ComponentC} />
<Route path={`${match.path}/gotoD`} component={ComponentD} />
<Route path={`${match.path}/gotoE`} component={ComponentE} />
</Switch>
</div>
)}
export default Layout