react 类组件的更新
类组件状态的使用
- 类组件的数据来源有两个地方:
- 父组件传过来的属性。
- 自己内部的状态。 - 属性和状态发生变化后组件都会更新,视图都会渲染。
- 在构造函数里初始化state。
定义一个clock
组件,显示时分秒,并且每一秒更新一次时间:
代码如下:
class Clock extends React.Component{
constructor(props){
super(props)
this.state = {date:new Date()}
}
// 组件已经渲染到DOM后运行
componentDidMount(){
this.timerID = setInterval(() => this.tick(), 1000);
}
componentWillUnmount(){
clearInterval(this.timerID)
}
tick = ()=>{
this.setState({date:new Date()})
}
render(){
return (
<div>
<h1>hello world</h1>
<h2>现在时间是:{this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
ReactDOM.render(
// <FunctionCmp style={{color:'red'}} title="function-title" content="function-content"></FunctionCmp>,
<Clock style={{color:'red'}}></Clock>,
document.getElementById('root')
);
这个示例在react文档中也可以看到:https://reactjs.bootcss.com/docs/state-and-lifecycle.html
state的使用注意
- 不要直接修改state,比如
this.state.comment = 'hello'
。这样不会重新渲染组件。 - 修改state 应该使用
setState()
。 - 构造函数是唯一可以给
this.state
赋值的地方。 - state的更新可能是异步的。因为
this.props
和this.state
可能会异步更新,所以不要依赖他们的值来更新下一个状态。 - state的更新可能会被合并。
state 更新可能是异步
处于性能考虑,React可能会把多个setState()
调用合并成一个调用。所以不要依赖他们的值来更新下一个状态。
来看一个例子:
class Counter extends React.Component{
constructor(props){
super(props)
this.state = {number:0}
}
// state = {number:0} // 也可以这种方式定义state
handleClick = ()=>{
this.setState({number:this.state.number + 1})
console.log(this.state.number);
this.setState({number:this.state.number + 1})
console.log(this.state.number);
}
render(){
return (
<div>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
定义一个 Counter 组件。Counter组件有一个点击事件,每次点击 number ++。按照正常思维,handleClick
的输出应该是:1,2
。那么事实真的如此吗?
打开控制台,可以看到 页面上的state 确实变了,但是并不是我们两次+1后的结果。控制台输出的两次还是最初的值,而且两个输出的值一样,并不是上一个 +1 之后的结果。
主要产生原因就是:
在事件处理函数中,setState 并不会修改this.state,等事件处理函数结束后再进行更新。
所以在handleClick
中,每次 调用this.state.number + 1
的时候拿到的this.state.number
都是默认的0。
如果想要依赖上一个状态,可以使用函数的方式,函数的参数 就是上一次的返回的新的状态。
handleClick = ()=>{
// this.setState({number:this.state.number + 1})
// console.log(this.state.number);
// this.setState({number:this.state.number + 1})
// console.log(this.state.number);
this.setState((state)=>({number:state.number+1}))
console.log(this.state.number);
this.setState((state)=>({number:state.number+1}))
console.log(this.state.number);
}
可以看到 界面的 数字就变成了连续两次+1的结果了。(至于控制台的就还是和上面说的原因一样,在函数处理中并不是修改this.state,所以两次拿到的是当前this.state.number的结果。)
但是我就是想要同步执行怎么去解决呢?
可以使用setTimeout
。setTimeout
会开启一个宏任务,在下一次事件循环的时候执行。
handleClick = ()=>{
this.setState({number:this.state.number + 1})
console.log(this.state.number); // 输出 0
this.setState({number:this.state.number + 1})
console.log(this.state.number); // 输出 0
setTimeout(() => {
this.setState({number:this.state.number + 1})
console.log(this.state.number); // 输出 2 因为setTimeout 外面的执行完毕会后 this.state.number 已经变为1
this.setState({number:this.state.number + 1})
console.log(this.state.number); // 输出 3
});
}
如何判断setState
是同步还是异步,是不是批量
1.原则 就是 凡是在React可以管控的地方,(比如上面的handleClick回调事件中,或者生命周期函数中等),他就是异步的,批量的。
2.在React不能管控到的地方(比如setTimeout中,是在宏任务队列中它管控不到,改成promise 也可以),就是同步的。非批量的。
简易的批量更新的大概原理
let isBatchUpdate = false
let queue = []
let state = {number:0}
function setState(newState){
if(isBatchUpdate){
queue.push(newState)
}else{
state = {...state,...newState}
}
}
function handleClick(){
// 模拟 react 在执行前 设置批量为true
isBatchUpdate = true
setState({number:1})
console.log(state);
setState({number:2})
console.log(state);
/**到这里我们自己处理业务逻辑结束,react开始批量更新state**/
state = queue.reduce((newState,action)=>{return {...newState,...action}},state)
isBatchUpdate = false
}
handleClick()
实现类组件的更新
首先在Component
添加setState
方法。Component
自己并不管更新调度,需要借助Updater
更新器来更新调度。它只负责更新组件。Component
再添加一个updateComponent
方法,用来更新组件。
class Updater{
constructor(classInstance){
this.classInstance = classInstance
this.pendingStates = [] // 需要更新的状态 队列
this.callbacks = [] // setState 的所有的回调
}
addState(partialState,callback){
this.pendingStates.push(partialState)
if(callback && typeof callback === 'function'){
this.callbacks.push(callback)
}
this.emitUpdate()
}
// 触发更新
emitUpdate(){
}
}
class Component{
static isReactComponent = true
constructor(props){
this.props = props
this.state = {}
// 每一个类组件的实例有一个updater更新器
this.updater = new Updater(this)
}
setState(partialState,callback){
this.updater.addState(partialState,callback)
}
forceUpdate(){
}
}
export default Component
完整基本实现的代码地址:https://gitee.com/Nsir/my-react/tree/v1.0
实现合成事件 和 批量更新
前面我们已经知道,React 在事件处理函数中会将state 进行批量更新。所以我们在updateProps
绑定事件的时候做一些处理:
在Component.js
中新加一个处理批量更新对象:
export const updateQueue = {
isBatchingUpdate :false, // 控制是否批量更新state 默认false,在事件处理函数触发的时候设置为true ,使得state 更新进行批量异步更新
updates:[],
batchUpdate(){
updateQueue.updates.forEach(updater=>updater.updateComponent())
updateQueue.updates.length = 0 // 重置为0
updateQueue.isBatchingUpdate = false
}
}
修改之前的emitUpdate
:
// 触发更新
// 专门抽取这个方法 ,是因为 后面不管是state更新,还是props 更新都会触发更新,都会调用这个方法
emitUpdate(){
// 如果是批量更新,就先将当前的updater实例缓存到 updateQueue的updates中
if(updateQueue.isBatchingUpdate){
updateQueue.updates.push(this)
}else{
this.updateComponent() // 触发组件更新
}
}
接下来实现合成事件。在react15是将事件委托到document上,react17是绑定到root容器上。这样当点击事件触发的时候,比如点击了click ,那么document也会触发click 事件,我们就可以在document的click事件中做自己的一些处理:
比如 在这里将isBatchingUpdate
置为true,然后执行原来组件上触发的click 事件,然后再将isBatchingUpdate
设为false,然后批量更新state。感觉就有点切面编程的思想。
新增一个 event.js
文件:
import { updateQueue } from "./Component"
export function addEvent(dom,eventType,handler){
let store = (dom.store = dom.store ? dom.store : {}) // store 存放着当前dom上对应的事件处理函数
store[eventType] = handler // 比如 store.onclick = handle
// 进行委托
if(!document[eventType]){
// 事件委托到document上
document[eventType] = dispatchEvent
}
}
function dispatchEvent(event){
// target 当前触发事件所点击的元素
// type 事件类型 比如:click
let {target,type} = event
let eventType = `on${type}`
updateQueue.isBatchingUpdate = true // 切换为批量更新模式
let syntheticEvent = createSyntheticEvent(event)
// 模拟事件冒泡
while (target) {
let {store} = target
let handler = store && store[eventType]
handler && handler.call(target,syntheticEvent)
target = target.parentNode
}
updateQueue.isBatchingUpdate = false
updateQueue.batchUpdate()
}
// 基于老的事件对象创建合成事件对象
// react 源码中 在此方法中做了各个浏览器的兼容适配处理
function createSyntheticEvent(event){
let syntheticEvent = {}
for (const key in event) {
if (Object.hasOwnProperty.call(event, key)) {
syntheticEvent[key] = event[key]
}
}
return syntheticEvent
}