前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从零实现一个React(四):异步的setState

从零实现一个React(四):异步的setState

作者头像
前端迷
发布2019-08-27 15:08:17
8380
发布2019-08-27 15:08:17
举报
文章被收录于专栏:前端迷

有问题需要探讨也请在github上回复我~ https://github.com/hujiulong/blog/issues/7

前言

在上一篇文章中,我们实现了diff算法,性能有非常大的改进。但是文章末尾也指出了一个问题:按照目前的实现,每次调用setState都会触发更新,如果组件内执行这样一段代码:

代码语言:javascript
复制
for ( let i = 0; i < 100; i++ ) {
    this.setState( { num: this.state.num + 1 } );
}

那么执行这段代码会导致这个组件被重新渲染100次,这对性能是一个非常大的负担。

真正的React是怎么做的

React显然也遇到了这样的问题,所以针对setState做了一些特别的优化:React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新,举个栗子:

代码语言:javascript
复制
class App extends Component {
    constructor() {
        super();
        this.state = {
            num: 0
        }
    }
    componentDidMount() {
        for ( let i = 0; i < 100; i++ ) {
            this.setState( { num: this.state.num + 1 } );
            console.log( this.state.num );    // 会输出什么?
        }
    }
    render() {
        return (
            <div className="App">
                <h1>{ this.state.num }</h1>
            </div>
        );
    }
}

我们定义了一个App组件,在组件挂载后,会循环100次,每次让this.state.num增加1,我们用真正的React来渲染这个组件,看看结果:

组件渲染的结果是1,并且在控制台中输出了100次0,说明每个循环中,拿到的state仍然是更新之前的。

这是React的优化手段,但是显然它也会在导致一些不符合直觉的问题(就如上面这个例子),所以针对这种情况,React给出了一种解决方案:setState接收的参数还可以是一个函数,在这个函数中可以拿先前的状态,并通过这个函数的返回值得到下一个状态。

我们可以通过这种方式来修正App组件:

代码语言:javascript
复制
componentDidMount() {
    for ( let i = 0; i < 100; i++ ) {
        this.setState( prevState => {
            console.log( prevState.num );
            return {
                num: prevState.num + 1
            }
        } );
    }
}

这种用法是不是很像数组的reduce方法?

现在来看看App组件的渲染结果:

现在终于能得到我们想要的结果了。

所以,这篇文章的目标也明确了,我们要实现以下两个功能:

  1. 异步更新state,将短时间内的多个setState合并成一个
  2. 为了解决异步更新导致的问题,增加另一种形式的setState:接受一个函数作为参数,在函数中可以得到前一个状态并返回下一个状态

合并setState

回顾一下第二篇文章中对setState的实现:

代码语言:javascript
复制
setState( stateChange ) {
    Object.assign( this.state, stateChange );
    renderComponent( this );
}

这种实现,每次调用setState都会更新state并马上渲染一次。

setState队列

为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后,清空这个队列并渲染组件。

队列是一种数据结构,它的特点是“先进先出”,可以通过js数组的push和shift方法模拟 然后需要定义一个”入队“的方法,用来将更新添加进队列。

代码语言:javascript
复制
const queue = [];
function enqueueSetState( stateChange, component ) {
    queue.push( {
        stateChange,
        component
    } );
}

然后修改组件的setState方法,不再直接更新state和渲染组件,而是添加进更新队列。

代码语言:javascript
复制
setState( stateChange ) {
    enqueueSetState( stateChange, this );
}

现在队列是有了,怎么清空队列并渲染组件呢?

清空队列

我们定义一个flush方法,它的作用就是清空队列

代码语言:javascript
复制
function flush() {
    let item;
    // 遍历
    while( item = setStateQueue.shift() ) {

        const { stateChange, component } = item;

        // 如果没有prevState,则将当前的state作为初始的prevState
        if ( !component.prevState ) {
            component.prevState = Object.assign( {}, component.state );
        }

        // 如果stateChange是一个方法,也就是setState的第二种形式
        if ( typeof stateChange === 'function' ) {
            Object.assign( component.state, stateChange( component.prevState, component.props ) );
        } else {
            // 如果stateChange是一个对象,则直接合并到setState中
            Object.assign( component.state, stateChange );
        }

        component.prevState = component.state;

    }
}

这只是实现了state的更新,我们还没有渲染组件。渲染组件不能在遍历队列时进行,因为同一个组件可能会多次添加到队列中,我们需要另一个队列保存所有组件,不同之处是,这个队列内不会有重复的组件。

我们在enqueueSetState时,就可以做这件事

代码语言:javascript
复制
const queue = [];
const renderQueue = [];
function enqueueSetState( stateChange, component ) {
    queue.push( {
        stateChange,
        component
    } );
    // 如果renderQueue里没有当前组件,则添加到队列中
    if ( !renderQueue.some( item => item === component ) ) {
        renderQueue.push( component );
    }
}

在flush方法中,我们还需要遍历renderQueue,来渲染每一个组件

代码语言:javascript
复制
function flush() {
    let item, component;
    while( item = queue.shift() ) {
        // ...
    }
    // 渲染每一个组件
    while( component = renderQueue.shift() ) {
        renderComponent( component );
    }

}

延迟执行

现在还有一件最重要的事情:什么时候执行flush方法。 我们需要合并一段时间内所有的setState,也就是在一段时间后才执行flush方法来清空队列,关键是这个“一段时间“怎么决定。

一个比较好的做法是利用js的事件队列机制。

先来看这样一段代码:

代码语言:javascript
复制
setTimeout( () => {
    console.log( 2 );
}, 0 );
Promise.resolve().then( () => console.log( 1 ) );
console.log( 3 );

你可以打开浏览器的调试工具运行一下,它们打印的结果是:

代码语言:javascript
复制
3
1
2

具体的原理可以看阮一峰的这篇文章,这里就不再赘述了。

我们可以利用事件队列,让flush在所有同步任务后执行

代码语言:javascript
复制
function enqueueSetState( stateChange, component ) {
    // 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加
    if ( queue.length === 0 ) {
        defer( flush );
    }
    queue.push( {
        stateChange,
        component
    } );
    if ( !renderQueue.some( item => item === component ) ) {
        renderQueue.push( component );
    }
}

定义defer方法,利用刚才题目中出现的Promise.resolve

代码语言:javascript
复制
function defer( fn ) {
    return Promise.resolve().then( fn );
}

这样在一次“事件循环“中,最多只会执行一次flush了,在这个“事件循环”中,所有的setState都会被合并,并只渲染一次组件。

别的延迟执行方法

除了用Promise.resolve().then( fn ),我们也可以用上文中提到的setTimeout( fn, 0 ),setTimeout的时间也可以是别的值,例如16毫秒。

16毫秒的间隔在一秒内大概可以执行60次,也就是60帧,人眼每秒只能捕获60幅画面

另外也可以用requestAnimationFrame或者requestIdleCallback

代码语言:javascript
复制
function defer( fn ) {
    return requestAnimationFrame( fn );
}

试试效果

就试试渲染上文中用React渲染的那两个例子:

代码语言:javascript
复制
class App extends Component {
    constructor() {
        super();
        this.state = {
            num: 0
        }
    }
    componentDidMount() {
        for ( let i = 0; i < 100; i++ ) {
            this.setState( { num: this.state.num + 1 } );
            console.log( this.state.num );
        }
    }
    render() {
        return (
            <div className="App">
                <h1>{ this.state.num }</h1>
            </div>
        );
    }
}

效果和React完全一样

同样,用第二种方式调用setState:

代码语言:javascript
复制
componentDidMount() {
    for ( let i = 0; i < 100; i++ ) {
        this.setState( prevState => {
            console.log( prevState.num );
            return {
                num: prevState.num + 1
            }
        } );
    }
}

结果也完全一样:

后话

在这篇文章中,我们又实现了一个很重要的优化:合并短时间内的多次setState,异步更新state。 到这里我们已经实现了React的大部分核心功能和优化手段了,所以这篇文章也是这个系列的最后一篇了。

这篇文章的所有代码都在这里:https://github.com/hujiulong/simple-react/tree/chapter-4

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-08-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端迷 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 真正的React是怎么做的
  • 合并setState
    • setState队列
      • 清空队列
        • 延迟执行
          • 别的延迟执行方法
      • 试试效果
      • 后话
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档