{"action":"created","issue":{"url":"https://api.github.com/repos/hujiulong/blog/issues/7","repository_url":"https://api.github.com/repos/hujiulong/blog","labels_url":"https://api.github.com/repos/hujiulong/blog/issues/7/labels{/name}","comments_url":"https://api.github.com/repos/hujiulong/blog/issues/7/comments","events_url":"https://api.github.com/repos/hujiulong/blog/issues/7/events","html_url":"https://github.com/hujiulong/blog/issues/7","id":314442459,"number":7,"title":"从零开始实现一个React（四）：异步的setState","user":{"login":"hujiulong","id":13267437,"avatar_url":"https://avatars1.githubusercontent.com/u/13267437?v=4","gravatar_id":"","url":"https://api.github.com/users/hujiulong","html_url":"https://github.com/hujiulong","followers_url":"https://api.github.com/users/hujiulong/followers","following_url":"https://api.github.com/users/hujiulong/following{/other_user}","gists_url":"https://api.github.com/users/hujiulong/gists{/gist_id}","starred_url":"https://api.github.com/users/hujiulong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hujiulong/subscriptions","organizations_url":"https://api.github.com/users/hujiulong/orgs","repos_url":"https://api.github.com/users/hujiulong/repos","events_url":"https://api.github.com/users/hujiulong/events{/privacy}","received_events_url":"https://api.github.com/users/hujiulong/received_events","type":"User","site_admin":false},"labels":[{"id":894723288,"url":"https://api.github.com/repos/hujiulong/blog/labels/React","name":"React","color":"d93f0b","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":3,"created_at":"2018-04-15T19:07:33Z","updated_at":"2018-04-18T03:02:57Z","closed_at":null,"author_association":"OWNER","body":"# 前言\r\n在[上一篇文章](https://github.com/hujiulong/blog/issues/6)中，我们实现了diff算法，性能有非常大的改进。但是文章末尾也指出了一个问题：按照目前的实现，每次调用setState都会触发更新，如果组件内执行这样一段代码：\r\n```js\r\nfor ( let i = 0; i < 100; i++ ) {\r\n    this.setState( { num: this.state.num + 1 } );\r\n}\r\n```\r\n那么执行这段代码会导致这个组件被重新渲染100次，这对性能是一个非常大的负担。\r\n\r\n# 真正的React是怎么做的\r\nReact显然也遇到了这样的问题，所以针对setState做了一些特别的优化：React会将多个setState的调用合并成一个来执行，这意味着当调用setState时，state并不会立即更新，举个栗子：\r\n```js\r\nclass App extends Component {\r\n    constructor() {\r\n        super();\r\n        this.state = {\r\n            num: 0\r\n        }\r\n    }\r\n    componentDidMount() {\r\n        for ( let i = 0; i < 100; i++ ) {\r\n            this.setState( { num: this.state.num + 1 } );\r\n            console.log( this.state.num );    // 会输出什么？\r\n        }\r\n    }\r\n    render() {\r\n        return (\r\n            <div className=\"App\">\r\n                <h1>{ this.state.num }</h1>\r\n            </div>\r\n        );\r\n    }\r\n}\r\n```\r\n我们定义了一个`App`组件，在组件挂载后，会循环100次，每次让`this.state.num`增加1，我们用真正的React来渲染这个组件，看看结果：\r\n\r\n![1](https://user-images.githubusercontent.com/13267437/38770037-3587b81c-403f-11e8-8f99-4f8a4427e205.png)\r\n\r\n组件渲染的结果是1，并且在控制台中输出了100次0，说明每个循环中，拿到的state仍然是更新之前的。\r\n\r\n这是React的优化手段，但是显然它也会在导致一些不符合直觉的问题（就如上面这个例子），所以针对这种情况，React给出了一种解决方案：setState接收的参数还可以是一个函数，在这个函数中可以拿先前的状态，并通过这个函数的返回值得到下一个状态。\r\n\r\n我们可以通过这种方式来修正App组件：\r\n```js\r\ncomponentDidMount() {\r\n    for ( let i = 0; i < 100; i++ ) {\r\n        this.setState( prevState => {\r\n            console.log( prevState.num );\r\n            return {\r\n                num: prevState.num + 1\r\n            }\r\n        } );\r\n    }\r\n}\r\n```\r\n> 这种用法是不是很像数组的`reduce`方法？\r\n\r\n现在来看看App组件的渲染结果：\r\n![1](https://user-images.githubusercontent.com/13267437/38770164-fbeef622-4040-11e8-9680-958394f9bb9e.png)\r\n现在终于能得到我们想要的结果了。\r\n\r\n所以，这篇文章的目标也明确了，**我们要实现以下两个功能**：\r\n\r\n1. 异步更新state，将短时间内的多个setState合并成一个\r\n2. 为了解决异步更新导致的问题，增加另一种形式的setState：接受一个函数作为参数，在函数中可以得到前一个状态并返回下一个状态\r\n\r\n# 合并setState\r\n回顾一下[第二篇文章](https://github.com/hujiulong/blog/issues/5)中对setState的实现：\r\n```js\r\nsetState( stateChange ) {\r\n    Object.assign( this.state, stateChange );\r\n    renderComponent( this );\r\n}\r\n```\r\n这种实现，每次调用setState都会更新state并马上渲染一次。\r\n\r\n## setState队列\r\n为了合并setState，我们需要一个队列来保存每次setState的数据，然后在一段时间后，清空这个队列并渲染组件。\r\n\r\n> 队列是一种数据结构，它的特点是“先进先出”，可以通过js数组的push和shift方法模拟\r\n\r\n```js\r\nconst queue = [];\r\nfunction enqueueSetState( stateChange, component ) {\r\n    queue.push( {\r\n        stateChange,\r\n        component\r\n    } );\r\n}\r\n```\r\n然后修改组件的setState方法\r\n```js\r\nsetState( stateChange ) {\r\n    enqueueSetState( stateChange, this );\r\n}\r\n```\r\n现在队列是有了，怎么清空队列并渲染组件呢？\r\n\r\n## 清空队列\r\n我们定义一个flush方法，它的作用就是清空队列\r\n```js\r\nfunction flush() {\r\n    let item;\r\n    // 遍历\r\n    while( item = setStateQueue.shift() ) {\r\n\r\n        const { stateChange, component } = item;\r\n\r\n        // 如果没有prevState，则将当前的state作为初始的prevState\r\n        if ( !component.prevState ) {\r\n            component.prevState = Object.assign( {}, component.state );\r\n        }\r\n\r\n        // 如果stateChange是一个方法，也就是setState的第二种形式\r\n        if ( typeof stateChange === 'function' ) {\r\n            Object.assign( component.state, stateChange( component.prevState, component.props ) );\r\n        } else {\r\n            // 如果stateChange是一个对象，则直接合并到setState中\r\n            Object.assign( component.state, stateChange );\r\n        }\r\n\r\n        component.prevState = component.state;\r\n\r\n    }\r\n}\r\n```\r\n这只是实现了state的更新，我们还没有渲染组件。渲染组件不能在遍历队列时进行，因为同一个组件可能会多次添加到队列中，我们需要另一个队列保存所有组件，不同之处是，这个队列内不会有重复的组件。\r\n\r\n我们在enqueueSetState时，就可以做这件事\r\n```js\r\nconst queue = [];\r\nconst renderQueue = [];\r\nfunction enqueueSetState( stateChange, component ) {\r\n    queue.push( {\r\n        stateChange,\r\n        component\r\n    } );\r\n    // 如果renderQueue里没有当前组件，则添加到队列中\r\n    if ( !renderQueue.some( item => item === component ) ) {\r\n        renderQueue.push( component );\r\n    }\r\n}\r\n```\r\n在flush方法中，我们还需要遍历renderQueue，来渲染每一个组件\r\n```js\r\nfunction flush() {\r\n    let item, component;\r\n    while( item = queue.shift() ) {\r\n        // ...\r\n    }\r\n    // 渲染每一个组件\r\n    while( component = renderQueue.shift() ) {\r\n        renderComponent( component );\r\n    }\r\n\r\n}\r\n```\r\n\r\n## 延迟执行\r\n现在还有一件最重要的事情：什么时候执行flush方法。\r\n我们需要合并一段时间内所有的setState，也就是在一段时间后才执行flush方法来清空队列，关键是这个“一段时间“怎么决定。\r\n\r\n一个比较好的做法是利用js的事件队列机制。\r\n\r\n先来看这样一段代码：\r\n```js\r\nsetTimeout( () => {\r\n    console.log( 2 );\r\n}, 0 );\r\nPromise.resolve().then( () => console.log( 1 ) );\r\nconsole.log( 3 );\r\n```\r\n你可以打开浏览器的调试工具运行一下，它们打印的结果是：\r\n```\r\n3\r\n1\r\n2\r\n```\r\n具体的原理可以看[阮一峰的这篇文章](http://www.ruanyifeng.com/blog/2014/10/event-loop.html)，这里就不再赘述了。\r\n\r\n我们可以利用事件队列，让flush在所有同步任务后执行\r\n```js\r\nfunction enqueueSetState( stateChange, component ) {\r\n    // 如果queue的长度是0，也就是在上次flush执行之后第一次往队列里添加\r\n    if ( queue.length === 0 ) {\r\n        defer( flush );\r\n    }\r\n    queue.push( {\r\n        stateChange,\r\n        component\r\n    } );\r\n    if ( !renderQueue.some( item => item === component ) ) {\r\n        renderQueue.push( component );\r\n    }\r\n}\r\n```\r\n定义defer方法，利用刚才题目中出现的Promise.resolve\r\n```js\r\nfunction defer( fn ) {\r\n    return Promise.resolve().then( fn );\r\n}\r\n```\r\n这样在一次“事件循环“中，最多只会执行一次flush了，在这个“事件循环”中，所有的setState都会被合并，并只渲染一次组件。\r\n\r\n### 别的延迟执行方法\r\n除了用`Promise.resolve().then( fn )`，我们也可以用上文中提到的`setTimeout( fn, 0 )`，setTimeout的时间也可以是别的值，例如16毫秒。\r\n\r\n> 16毫秒的间隔在一秒内大概可以执行60次，也就是60帧，人眼每秒只能捕获60幅画面\r\n\r\n另外也可以用`requestAnimationFrame`或者`requestIdleCallback`\r\n```js\r\nfunction defer( fn ) {\r\n    return requestAnimationFrame( fn );\r\n}\r\n```\r\n\r\n# 试试效果\r\n就试试渲染上文中用React渲染的那两个例子：\r\n```js\r\nclass App extends Component {\r\n    constructor() {\r\n        super();\r\n        this.state = {\r\n            num: 0\r\n        }\r\n    }\r\n    componentDidMount() {\r\n        for ( let i = 0; i < 100; i++ ) {\r\n            this.setState( { num: this.state.num + 1 } );\r\n            console.log( this.state.num ); \r\n        }\r\n    }\r\n    render() {\r\n        return (\r\n            <div className=\"App\">\r\n                <h1>{ this.state.num }</h1>\r\n            </div>\r\n        );\r\n    }\r\n}\r\n```\r\n效果和React完全一样\r\n![1](https://user-images.githubusercontent.com/13267437/38770037-3587b81c-403f-11e8-8f99-4f8a4427e205.png)\r\n同样，用第二种方式调用setState：\r\n```js\r\ncomponentDidMount() {\r\n    for ( let i = 0; i < 100; i++ ) {\r\n        this.setState( prevState => {\r\n            console.log( prevState.num );\r\n            return {\r\n                num: prevState.num + 1\r\n            }\r\n        } );\r\n    }\r\n}\r\n```\r\n结果也完全一样：\r\n![1](https://user-images.githubusercontent.com/13267437/38770164-fbeef622-4040-11e8-9680-958394f9bb9e.png)\r\n\r\n# 后话\r\n在这篇文章中，我们又实现了一个很重要的优化：合并短时间内的多次setState，异步更新state。\r\n到这里我们已经实现了React的大部分核心功能和优化手段了，所以这篇文章也是这个系列的最后一篇了。\r\n\r\n这篇文章的所有代码都在这里：https://github.com/hujiulong/simple-react/tree/chapter-4\r\n\r\n# 从零开始实现React系列\r\nReact是前端最受欢迎的框架之一，解读其源码的文章非常多，但是我想从另一个角度去解读React：从零开始实现一个React，从API层面实现React的大部分功能，在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。\r\n\r\n整个系列大概会有四篇左右，我每周会更新一到两篇，我会第一时间在github上更新，有问题需要探讨也请在github上回复我~\r\n> 博客地址: https://github.com/hujiulong/blog\r\n关注点star，订阅点watch\r\n\r\n## 上一篇文章\r\n[从零开始实现一个React（三）：diff算法 ](https://github.com/hujiulong/blog/issues/6)\r\n"},"comment":{"url":"https://api.github.com/repos/hujiulong/blog/issues/comments/382237337","html_url":"https://github.com/hujiulong/blog/issues/7#issuecomment-382237337","issue_url":"https://api.github.com/repos/hujiulong/blog/issues/7","id":382237337,"user":{"login":"aszx87410","id":2755720,"avatar_url":"https://avatars1.githubusercontent.com/u/2755720?v=4","gravatar_id":"","url":"https://api.github.com/users/aszx87410","html_url":"https://github.com/aszx87410","followers_url":"https://api.github.com/users/aszx87410/followers","following_url":"https://api.github.com/users/aszx87410/following{/other_user}","gists_url":"https://api.github.com/users/aszx87410/gists{/gist_id}","starred_url":"https://api.github.com/users/aszx87410/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/aszx87410/subscriptions","organizations_url":"https://api.github.com/users/aszx87410/orgs","repos_url":"https://api.github.com/users/aszx87410/repos","events_url":"https://api.github.com/users/aszx87410/events{/privacy}","received_events_url":"https://api.github.com/users/aszx87410/received_events","type":"User","site_admin":false},"created_at":"2018-04-18T03:02:57Z","updated_at":"2018-04-18T03:02:57Z","author_association":"NONE","body":"剛好看到這系列文章，寫得很不錯，持續關注！\r\n\r\n順便分享一下 React Issue 的這篇，在探討為什麼 setState 是異步的：https://github.com/facebook/react/issues/11527"}}
{"actor":{"display_login":"aszx87410"}}