React
React
React事件机制
React(JSX)并不是将事件绑定到了真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document的时候,React将事件内容封装并交由真正的处理函数进行处理。这种方式不仅减少了内存的消耗,还能在组件挂载销毁时统一进行事件的订阅和移除。
除此之外,冒泡到document上的事件也不是原生的浏览器事件,而是由react实现的而合成事件(SyntheticEvent)。如果不需要事件冒泡,应该下龙event.stopPropagation()方法,而不是调用event.preventDefault()方法。
实现合成事件的目的
-
浏览器兼容
合成事件首先解决了浏览器之间的兼容问题,另外,合成事件是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力。
-
事件对象统一管理
对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象,如果有许多事件监听,就需要分配很多的事件对象,造成高额的内存分配问题。
但是对于合成事件来说,有一个事件池来专门管理事件对象的创建和销毁,当事件被使用时,就会从事件池中复用对象,事件回调结束后,销毁事件对象上的属性,为下次复用事件对象做准备。
生命周期
React生命周期
以React16.0以后的生命周期为例:
React通常将组件的生命周期分为以下三个阶段:
- Mount,挂载阶段:组件第一次在DOM树中被渲染的过程
- Update,更新阶段:组件状态发生变化,重新更新渲染的过程
- Unmount,卸载阶段:组件从DOM树中移除的过程
挂载阶段
该阶段组件被创建,然后组件实例插入到DOM树中,完成组件的第一次渲染。这个过程只会发生一次。
依次调用的方法:
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
-
constructor
组件的构造函数,第一个被执行。
若不显式定义,会有默认的构造函数;
若显式定义,必须在构造函数中执行super(props),否则无法在构造函数中拿到this。如果不需要初始化state或者不需要绑定方法,就不需要实现React组件的constructor。
constructor中一般只做两件事情:
- 初始化组件的state
- 为事件处理方法绑定this
constructor(props) { super(props); // 直接给state设置初始值,不要在构造函数中调用setState this.state = { counter: 0 }; this.handler = this.handler.bind(this); }
-
getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
静态方法,无法在函数中使用this,有两个参数props和state,该函数返回一个对象用来更新state对象,若不需要更新state,可以返回null。
getDerivedStateFromProps函数在挂载阶段,当接收到新的props或者调用setState、forceUpdate时被调用。如果接收到新的属性,需要更新state时可以使用该函数。
// 当props.msg改变,更新state class App extends React.Component { constructor(props) { super(props); this.state = { msg: 'init' } } static getDerivedStateFromProps(props, state) { if (props.msg !== state.msg) { return { msg: props.msg } } return null; } handler = () => { this.setState({ msg: 'update' }); } render() { return ( <div onClick={this.handler}>{this.state.msg}</div> ) } }
注意:React16.4^的版本,setState和forceUpdate也会触发getDerivedStateFromProps这个生命周期。所以更新时可能会出现更新不正确的问题,需要注意。
-
render
React最核心的方法,组件中必须有的方法。
render函数根据props和state渲染组件,只做一件事,就是返回需要渲染的内容,不要在render函数内做其他业务逻辑。函数返回类型:
- React元素:原生DOM、React组件
- 数组和Fragment(片段):可以返回多个元素
- Portals(插槽):可以将子元素渲染到不同的DOM子树中
- 字符串和数组:渲染成DOM中的text节点
- 布尔值或null:不渲染任何内容
-
componentDidMount
componentDidMount在组件挂载(插入DOM树)后立即调用。
该阶段可以进行的操作:
- 执行依赖于DOM的操作
- 发送网络请求
- 添加订阅消息(取消订阅可以放在componentWillUnmount中)
如果在component中调用setState,会触发一次额外的渲染(多调用一次render函数)。由于这次额外渲染是在浏览器刷新屏幕前执行,用户是无感知的,但是应当避免这样使用,会降低性能,尽量在constructor中进行state的初始化。
class App extends React.Component { constructor(props) { super(props); this.state = { msg: 'mounting' } } componentDidMount() { this.setState = { msg: 'mounted' } } render() { return ( <div>状态:{ this.state.msg }</div> ) } }
更新阶段
当组件的props发生改变,或组件内调用setState/forceUpdate,就会触发重新渲染,这个过程可能发生多次。
当前阶段会依次调用下述方法:
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
-
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
需要注意下边两个组件更新的问题:
- 组件内setState的值即使与原来state中的值相同,也会引起组件重新渲染
- 如果父组件重新渲染,不管传入子组件中的props是否变化,都会引起子组件的重新渲染
可以用shouldComponentUpdate解决上述两个问题,提升性能。该方法在组件重新渲染前触发,默认返回true,可以根据比较this.props和nextProps以及this.state和nextState,来返回true或false来控制组件是否更新。如果返回false,组件停止更新过程,那么后续的render和componentDidUpdate不会被调用。
-
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps, prevState) { return {} // return 必须,默认return null // 返回值作为第三个参数传给componentDidUpdate } prevProps:更新之前的props prevState:更新之前的state
getSnapshotBeforeUpdate方法在render之后、componentDidUpdate之前调用,这个函数必须要和componentDidUpdate一起使用。
-
componentDidUpdate
componentDidUpdate会在更新后立即调用,首次渲染(挂载阶段)不会执行此方法。
componentDidUpdate(prevProps, prevState, snapshot) {} prevProps: 更新之前的props prevState: 更新之前的state snapshot: getSnapshotBeforeUpdate()生命周期的返回值
在该阶段可以执行的操作:
组件更新后,对DOM进行操作
如果对更新前后的props进行比较,可以在这个方法内进行网络请求,不改变,不请求。
卸载阶段
只有componentWillUnmount这一个生命周期函数,在组件卸载、销毁之前调用,在这个函数中使用setState是无用的,因为组件被卸载就不会重新渲染。
可以在这个函数中进行清理操作:
- 消除定时器、取消或消除网络请求
- 取消在componentDidMount()中创建的订阅
错误处理阶段
在后代组件抛出错误后,调用componentDidCatch方法
componentDidCatch(error, info)
error: 抛出的错误
info: 带有componentStack key的对象,其中包含有关组件引发错误的栈信息
React常见生命周期过程
过程:
- 挂载阶段,首先执行constructor构造方法,创建组件
- 创建完成后,执行render方法,返回需要渲染的内容
- React将需要渲染的内容挂载到DOM树上
- 挂载完成后,执行componentDidMount生命周期函数
- 给组件创建一个props(组件通信)、调用setState、调用forceUpdate(强制更新),都会重新调用render函数
- render函数重新执行之后,重新进行DOM树的挂载
- 挂载完成后,执行componentDidUpdate
- 移除组件后,执行componentWillUnmount
总结:
-
getDefaultProps
初始化组件的props,仅在组件创建之前被调用一次。
-
getInitialState
初始化组件的state值
-
componentWillMount
组件创建后、render前调用。React官方不推荐,React16废弃。 -
render
唯一一个必须要实现的方法。
一般需要返回jsx元素,React根据props和state把组件渲染出来,如果不需要渲染内容,返回null或false -
componentDidMount
组件挂载后立即调用,标志组件挂载完成。
需要获取DOM节点信息的操作可以在这个阶段完成。
也可以发起ajax请求,React官方推荐的请求时机。
与componentWillMount一样,仅调用一次。
React16中的新生命周期总结
React16对生命周期自上而下地做了另一维度的解读:
-
render阶段
用于计算一些必要的状态信息。这个阶段可能会被React暂停(react 16 引入fiber)
-
Pre-commit阶段
commit指的是更新真正的DOM节点。
Pre-commit阶段,是指还没有真正更新真实的DOM,但是DOM信息可以被读取了。 -
commit阶段
该阶段,React会完成真实DOM的更新工作。commit阶段,可以拿到真实的DOM、refs
流程方面还是按照挂载、更新、卸载进行划分
挂载过程:
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
更新过程:
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
卸载过程:
- componentWillUnmount
React废弃的生命周期
fiber出现之后,废弃了render之前调用的三个函数:componentWillMount、componentWillReceiveProps、componentWillUpdate。
这三个函数,可能会因为高优先级任务的出现被打断而导致多次执行。 另一个原因可能是React想约束使用者,好的框架能够让人写处易维护、易扩展的代码,而新增和将废弃的生命周期入手分析代码,能够提升这一点。
componentWillMount
可以用componentDidMount和constructor来代替。
componentWillReceiveProps
老版本React中,如果state与porps密切相关,需要在componentWillReceiveProps中判断前后props是否相同,并触发state的更新。但是这样做会破坏state数据源的单一性。并且会增加组件的重绘次数。
React引入一个新的生命周期函数getDerivedStateFromPorps。
getDerivedStateFromPorps优点:
- 静态函数,纯函数,不能使用this,无副作用
- 开发者只能通过prevState而不是prevProps来对比,保证了state和props之间的而简单关系,也不要处理第一次渲染时prevProps为空的情况
componentWillUpdate
与componentWillReceiveProps类似,在componentWillUpdate根据props的变化触发一些回调,但是无论是componentWillReceiveProps还是componentWillUpdate,在一次更新中都可能会被调用多次。
将原来卸载componentWillUpdate中的回调迁移至componentDidUpdate可以解决上述问题。
还有一种情况,如果需要获取DOM元素的状态,但是由于fiber中,render可以被打断,在componentWillUpdate中获取到的状态很可能与真正的状态不同。这个可以使用新增的生命函数getSnapshotBeforeUpdate去解决
React 16.x中props改变后调用的生命周期
getDerivedStateFromProps
替代了被废弃的componentWillReceiveProps
该函数是静态函数,无法通过this访问class的属性
通过nextProps以及prevState来进行判断,根据新传入的props来选择是否更新state
static getDerivedStateFromProps(nextProps, prevState) {
const { msg } = nextProps;
// 当传入的msg改变,更新state
if (msg !== prevState.msg) {
return { msg };
}
return null; // 不需要更新state,返回null
}
React性能优化在哪个生命周期?优化原理?
react父组件render函数重新渲染会引起子组件render方法的重新渲染。但是如果子组件接受父组件的数据没有改变,这是子组件render就是无用的,影响性能的。
上述问题可以在shouldComponentUpdate中解决
shouldComponentUpdate(nextProps) {
if (this.props.msg === nextProps.msg) {
return false;
}
return true;
}
但是对于引用数据类型,如果数据引用地址没变,哪怕内容改变了,会被判定为false,不进行更新;或者数据引用地址改变,内容没有改变,会被判定为true,进行更新。
解决方法:
- setState改变数据前,采用assgin进行拷贝,但是因为是浅拷贝,不完美 (与扩展运算符一样的效果)
const o = Object.assign({}, this.state.obj);
o.student.count = '111';
this.setState({ obj: o });
- 使用JSON方法进行深拷贝,但是遇到数据为undefined和函数时会报错
state、setState、props
调用原理
执行过程:
- 调用setState入口函数,入口函数充当的是分发器的角色,根据入参不同,分发到不同的功能函数中去
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
}
- enqueueSetState方法将新的state放入组件的状态队列里,并调用enqueueUpdate来处理将要更新的实例对象。
enqueueSetState: function(publickInstance, partialState) {
// 通过this拿到组件实例
const internalInstance = getInternalInstanceReadyForUpdate(publickInstance, 'setState');
// 这个queue对应的就是一个组件实例的state数组
const queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate用来处理当前的组件实例
enqueueUpdate(internalInstance);
}
- 在enqueueUpdate方法中,引出一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates属性直接决定了当下是要走更新流程(false)还是排队等待(true)。
如果轮到执行更新,就调用batchedUpdates方法来直接发起更新流程。
batchingStrategy或许是React内部专门用于管控批量更新的对象。
function enqueueUpdate(component) {
ensureInjected();
// 关键判断
if (!batchingStrategy.isBatchingUpdates) { // 若当前没有处于批量创建/更新组件的阶段,立即更新
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 否则组件塞入dirtyComponents队列里,排队等待
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
batchingStrategy对象可以理解为锁管理器。
锁是指React全局唯一的isBatchingUpdates变量,isBatchingUpdates初始值为false,代表当前未进行任何批量更新操作。
当React调用batchedUpdate去执行更新动作时,先将锁给锁上,也就是isBatchingUpdates置为true,表示当前正在进行批量更新。当isBatchingUpdates为true(锁被锁上)时,任何需要更新的组件只能暂时进入dirtyComponents队列中进行排队(等待下一次的批量更新,并且不能随意插队)。
任务锁的思想,是React面对大量状态仍然能够实现有序分批处理的基石。
setState函数调用之后发生了什么
调用setState函数后,React将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。
经过调和过程,React会以相对高效的方式根据新的状态构建React元素树,并且着手重新渲染整个UI界面。
React得到新构建的元素树后,会自动计算出新元素树与老元素树的节点差异,然后根据差异对界面进行最小程度的渲染。在差异计算算法中,React能够相对精确的知道发生改变的位置以及如何改变,这样就保证了按需更新,而不是全部重新渲染。
如果短时间内频繁的setState。React将state的改变压入栈中,在合适的时机,批量更新state和试图,达到性能优化。
setState批量更新的过程
调用setState,组件的state不会立即改变,setState是把需要修改的state放入一个等待更新的队列中。React对真正的执行时机进行优化,为了提高性能,React事件处理程序中如果有多次setState,那么setState的状态将被修改合并成一次状态修改,这样最终更新只产生一次组件及其子组件的重新渲染。
this.setState({
count: this.state.count + 1 // 入队,[count+1]
});
this.setState({
count: this.state.count + 1 // 入队,[count+1, count+1]
});
// 上面两次setState将合并,但是例如上述多次+1,最终只有一次生效,因为在同一个方法中多的setState的合并动作不是单纯地将更新累加。例如相同属性的设置,React只保留最后一次更新
setState是同步还是异步
setState并不是单纯的同步或者异步操作,它的表现由调用场景决定。
源码中,通过isBatchingUpdates来判断setState是直接更新还是存入state队列中等待(true等待执行异步操作,false直接更新)。
-
异步
React可以控制的地方,比如React生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
-
同步
在React无法控制的地方,比如原生事件,例如addEventListener、setTimeout、setInterval等事件中,只能同步更新。
React中setState异步设计?
-
提升性能
假如每次调用setState都进行更新,那么意味着render函数会频繁的调用,界面重新进行渲染,这样效率非常低。
而异步设计,可以让React获取到多个更新操作,然后进行批量更新。 -
保持state和props的一致性
如果同步更新了state,但是还没有执行render函数,这样state和props就无法保证同步,如果无法保证state和props的一致性,那么在开发过程中会产生很多问题。
setState的第二个参数的作用
setState的第二个参数是可选的回调函数。
回调函数在组件重新渲染后执行,相当于在componentDidUpdate生命周期内执行(通常建议用componentDidUpdate代替使用这个回调函数)。
在回调函数中,可以拿到更新后的state值。
this.setState({
xx: 'xxx'
}, newState => {
console.log(newState); // 更新完成后的回调函数,函数中可以拿到更新后的state
});
setState和replaceState的区别
-
setState
用于设置状态对象,是React事件处理函数中和请求回调函数中触发UI更新的主要方法。
setState(nextState[, callback]); nextState: 将要设置的新状态,该状态会和当前的state合并 callback: 可选,更新后的回调函数,在组件重新渲染后调用
-
replaceState
与setState方法类似,但是只保留nextState中的状态。
原来state中如果存在nextState中不存在的状态,会被删除。replaceState(nextState[, callback]); nextState:将要设置的新状态,该状态会替换当前state callback:可选,更新后的回调函数,在组件重新渲染后调用
区别:
setState是修改state中的部分状态,而replaceState是直接替换原来的state,如果新状态中的属性减少,state中这个属性将不再存在。
React类组件中的this.state和this.setState的区别
this.state通常是用来初始化state的,而this.setState是用来修改state值的。
如果初始化state之后再使用this.state进行赋值操作,之前的state将会被覆盖掉,如果使用this.setState则会替换掉相应的state值。
如果需要修改state的值,使用this.setState。
如果需要替换state的值,那么直接给this.state赋值。
state如何注入组件,从reducer到组件的过程
props和state的区别
-
props
props是一个从外部传进组件的参数,主要作用是从父组件向子组件传递数据。
具有可读性和不变性,只能通过外部组件主动传入新的props来重新渲染子组件,否则子组件的props以及展现形式不会改变。 -
state
state的主要作用是,组件维护自身的状态,类组件中只能在constructor中初始化,是组件的私有属性,无法在外部访问和修改,只能在组件内部通过this.setState来修改。
state发生改变,可能会导致组件的重新渲染。
区别:
- props是父到子组件中进行传递的,而state是组件内用于管理状态的。
- props不可修改;state可以修改,setState操作后会异步更新。
props为什么只读
this.props是组件之间沟通的一个接口,原则上,只能从父组件流向子组件。
this.props汲取了纯函数的思想。props的不可变性保证了相同的输入,页面显示的内容是一样的,并且不会有副作用。
纯函数特点:
- 给定相同的输入,输出总是相同
- 没有副作用
- 不依赖外部状态
React中组件props改变时,更新组件的方法
在组件传入的props更新时,重新渲染该组件常用的方法是在componentWillReceiveProps将新的props更新到组件的state中(这种state称为派生状态),从而实现重新渲染。
在React 16.3中,引入一个新的钩子函数getDerivedStateFromProps
-
componentWillReceiveProps(已废弃)
在componentWillReceiveProps生命周期中,可以在子组件的render函数执行前,通过this.props获取旧的属性,通过nextProps获取新的props,对比两次props是否相同,来更新子组件自己的state。
好处:
可以将数据请求放在这个生命周期中执行,需要父组件传递的参数,从componentWillReceiveProps(nextProps)中获取。不必将所有的请求都放在父组件中,该请求只会在组件渲染时才会发出,从而减少请求次数。
-
getDerivedStateFromProps(16.3引入)
替代componentWillReceiveProps,要使用componentWillReceiveProps的场景,可以考虑使用getDerivedStateFromProps。
getDerivedStateFromProps是一个静态函数,这个函数不能通过this访问到class的属性(本身也不推荐直接访问属性)。getDerivedStateFromProps提供nextProps以及prevState参数,通过这两个参数,可以进行判断,是否根据新传入的props来同步到state。
如果props传入的内容不需要同步到state中,那么需要返回一个null,并且返回值是必须的。
static getDerivedStateFromProps(nextProps, prevState) { const { msg } = nextProps; // 当传入的msg发生变化时,更新state if (msg !== prevState.msg) { return { msg }; } // 否则,对于state不进行任何操作 return null; }
如何检验props,以及验证的目的
React中提供了PropTypes以供验证使用。当Props传入的数据无效(传入的数据类型和验证的数据类型不符),会在控制台发出警告信息。
验证的目的:可以避免随着应用越来越复杂时,数据类型不一致带来的问题;可以提升代码的可读性。
import PropTypes from 'prop-types';
class Test extends React.Component {
render() {
return (
<p>Test msg, {this.props.msg}</p>
);
}
}
Test.propTypes = {
msg: PropTypes.string
}
React中使用getDefaultProps的作用
通过实现组件中的getDefaultProps方法,为属性设置默认值。
var ShowTitle = React.createClass({
getDefaultProps: function() {
return {
title: 'React'
}
},
render: function() {
return <h1>{this.props.title}</h1>
}
});
React 组件通信
父子组件间通讯方式
父向子组件通信
通过props向子组件传递需要的信息
// 父组件
const Parent = () => {
return <Child msg="parent"/>;
}
// 子组件
const Child = props => {
return <p>{props.msg}</p>
}
子向父组件通信
props+回调函数的方式
// 父组件
const Parent = () => {
const cb = info => {
console.log('来自子组件的信息');
}
return <Child msg="parent" cb={cb} />
}
// 子组件
const Child = props => {
const { cb, msg } = props;
const handleClick = info => {
cb('子组件信息:' + info);
}
return <button onClick={msg => handleClick(msg + '已点击')}>click: {msg}</button>
}
跨级组件的通讯方式
组件向其孙子组件或更深层子组件通信
-
使用props
利用中间件层层传递,但是层级过深,每一个中间层都需要去传递props,增加了复杂度,而且这些props并不是中间组件自己需要的。
-
使用context
context相当于一个大容器,可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意去用,对于跨级多层的全局数据可以使用context实现。
const BatteryContext = createContext(); // 父组件 class Parent extends React.Component { state = { color: 'red' } render() { const { color } = this.state; return ( <BatteryContext.Provider value={color}> <Child></Child> </BatteryContext> ); } } // 子组件 const Child = () => <GrandChild /> // 孙子组件 class GrandChild extends React.Component { render() { return ( <BatteryContext.Cunsumer> {color => <h1 style={{ color: color }}>{color}</h1>} </BatteryContext.Cunsumer> ) } }
非嵌套关系组件间通信
即没有任何包含关系的组件,包括兄弟组件已经不在同一个父级中的非兄弟组件。
- 自定义事件通信(发布订阅模式)
// 接收
class ComponentA extends React.Component {
componentDidMount() {
document.addEventListener('myEvent', this.handleEvent)
}
componentWillUnmount() {
document.removeEventListener('myEvent', this.handleEvent)
}
handleEvent = (e) => {
console.log(e.detail.log) //i'm zach
}
}
// 发布
class ComponentB extends React.Component {
sendEvent = () => {
document.dispatchEvent(new CustomEvent('myEvent', {
detail: {
log: "i'm zach"
}
}))
}
render() {
return <button onClick={this.sendEvent}>Send</button>
}
}
改进:上边代码中事件都绑定在document上,可能会有冲突。创建一个EventBus,专门用于处理这种事件
class EventBus {
constructor() {
this.bus = document.createElement('eventBus');
}
addEventListener(event, callback) {
this.bus.addEventListener(event, callback);
}
removeEventListener(event, callback) {
this.bus.removeEventListener(event, callback);
}
dispatchEvent(event, detail = {}) {
this.bus.dispatchEvent(new CustomEvent(event, { detail }));
}
}
export default new EventBus;
import EventBus from './EventBus';
class ComponentA extends React.Component {
componentDidMount() {
EventBus.addEventListener('myEvent', this.handleEvent);
}
componentWillUnmount() {
EventBus.removeEventListener('myEvent', this.handleEvent);
}
handleEvent = (e) => {
console.log(e.detail.log); //i'm zach
}
}
class ComponentB extends React.Component {
sendEvent = () => {
EventBus.dispatchEvent('myEvent', {log: "i'm zach"}));
}
render() {
return <button onClick={this.sendEvent}>Send</button>
}
}
- 通过redux等进行全局状态管理
- 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点,结合父子间通信方式进行通信。
解决props嵌套层级过深问题
- 使用context
- 使用redux