看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/...)

同步 setState 的问题

而在现有 setState 逻辑实现中,每调用一次 setState 就会执行 render 一次。因此在如下代码中,每次点击增加按钮,因为 click 方法里调用了 10 次 setState 函数,页面也会被渲染 10 次。而我们希望的是每点击一次增加按钮只执行 render 函数一次。

export default class B extends Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
this.click = this.click.bind(this)
} click() {
for (let i = 0; i < 10; i++) {
this.setState({ // 在先前的逻辑中,没调用一次 setState 就会 render 一次
count: ++this.state.count
})
}
} render() {
console.log(this.state.count)
return (
<div>
<button onClick={this.click}>增加</button>
<div>{this.state.count}</div>
</div>
)
}
}

异步调用 setState

查阅 setState 的 api,其形式如下:

setState(updater, [callback])

它能接收两个参数,其中第一个参数 updater 可以为对象或者为函数 ((prevState, props) => stateChange),第二个参数为回调函数;

确定优化思路为:将多次 setState 后跟着的值进行浅合并,并借助事件循环等所有值合并好之后再进行渲染界面。

let componentArr = []

// 异步渲染
function asyncRender(updater, component, cb) {
if (componentArr.length === 0) {
defer(() => render()) // 利用事件循环,延迟渲染函数的调用
} if (cb) defer(cb) // 调用回调函数
if (_.isFunction(updater)) { // 处理 setState 后跟函数的情况
updater = updater(component.state, component.props)
}
// 浅合并逻辑
component.state = Object.assign({}, component.state, updater)
if (componentArr.includes(component)) {
component.state = Object.assign({}, component.state, updater)
} else {
componentArr.push(component)
}
} function render() {
let component
while (component = componentArr.shift()) {
renderComponent(component) // rerender
}
} // 事件循环,关于 promise 的事件循环和 setTimeout 的事件循环后续会单独写篇文章。
const defer = function(fn) {
return Promise.resolve().then(() => fn())
}

此时,每点击一次增加按钮 render 函数只执行一次了。

ref 的实现

在 react 中并不建议使用 ref 属性,而应该尽量使用状态提升,但是 react 还是提供了 ref 属性赋予了开发者操作 dom 的能力,react 的 ref 有 stringcallbackcreateRef 三种形式,分别如下:

// string 这种写法未来会被抛弃
class MyComponent extends Component {
componentDidMount() {
this.refs.myRef.focus()
}
render() {
return <input ref="myRef" />
}
} // callback(比较通用)
class MyComponent extends Component {
componentDidMount() {
this.myRef.focus()
}
render() {
return <input ref={(ele) => {
this.myRef = ele
}} />
}
} // react 16.3 增加,其它 react-like 框架还没有同步
class MyComponent extends Component {
constructor() {
super() {
this.myRef = React.createRef()
}
}
componentDidMount() {
this.myRef.current.focus()
}
render() {
return <input ref={this.myRef} />
}
}

React ref 的前世今生 罗列了三种写法的差异,下面对上述例子中的第二种写法(比较通用)进行实现。

首先在 setAttribute 方法内补充上对 ref 的属性进行特殊处理,

function setAttribute(dom, attr, value) {
...
else if (attr === 'ref') { // 处理 ref 属性
if (_.isFunction(value)) {
value(dom)
}
}
...
}

针对这个例子中 this.myRef.focus() 的 focus 属性需要异步处理,因为调用 componentDidMount 的时候,界面上还未添加 dom 元素。处理 renderComponent 函数:

function renderComponent(component) {
...
else if (component && component.componentDidMount) {
defer(component.componentDidMount.bind(component))
}
...
}

刷新页面,可以发现 input 框已为选中状态。

处理完普通元素的 ref 后,再来处理下自定义组件的 ref 的情况。之前默认自定义组件上是没属性的,现在只要针对自定义组件的 ref 属性做相应处理即可。稍微修改 vdomToDom 函数如下:

function vdomToDom(vdom) {
if (_.isFunction(vdom.nodeName)) { // 此时是自定义组件
...
for (const attr in vdom.attributes) { // 处理自定义组件的 ref 属性
if (attr === 'ref' && _.isFunction(vdom.attributes[attr])) {
vdom.attributes[attr](component)
}
}
...
}
...
}

跑如下测试用例:

class A extends Component {
constructor() {
super()
this.state = {
count: 0
}
this.click = this.click.bind(this)
} click() {
this.setState({
count: ++this.state.count
})
} render() {
return <div>{this.state.count}</div>
}
} class B extends Component {
constructor() {
super()
this.click = this.click.bind(this)
} click() {
this.A.click()
} render() {
return (
<div>
<button onClick={this.click}>加1</button>
<A ref={(e) => { this.A = e }} />
</div>
)
}
}

效果如下:

项目地址关于如何 pr

本系列文章拜读和借鉴了 simple-react,在此特别感谢 Jiulong Hu 的分享。

从 0 到 1 实现 React 系列 —— 4.setState优化和ref的实现的更多相关文章

  1. 从 0 到 1 实现 React 系列 —— 1.JSX 和 Virtual DOM

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...

  2. 从 0 到 1 实现 React 系列 —— 5.PureComponent 实现 && HOC 探幽

    本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/...) ...

  3. 从 0 到 1 实现 React 系列 —— 3.生命周期和 diff 算法

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...

  4. 从 0 到 1 实现 React 系列 —— 2.组件和 state|props

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...

  5. 从0到1用react+antd+redux搭建一个开箱即用的企业级管理后台系列(基础篇)

    背景 ​ 最近因为要做一个新的管理后台项目,新公司大部分是用vue写的,技术栈这块也是想切到react上面来,所以,这次从0到1重新搭建一个react项目架子,需要考虑的东西的很多,包括目录结构.代码 ...

  6. React 系列教程 1:实现 Animate.css 官网效果

    前言 这是 React 系列教程的第一篇,我们将用 React 实现 Animate.css 官网的效果.对于 Animate.css 官网效果是一个非常简单的例子,原代码使用 jQuery 编写,就 ...

  7. react系列笔记1 用npx npm命令创建react app

    react系列笔记1 用npx npm命令创建react app create-react-app my-app是开始构建新的 React 单页应用程序的最佳方式.它已经为你设置好了开发环境,以便您可 ...

  8. 用SignalR 2.0开发客服系统[系列2:实现聊天室]

    前言 交流群:195866844 上周发表了 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 这篇文章,得到了很多帮助和鼓励,小弟在此真心的感谢大家的支持.. 这周继续系列2,实现聊天室 ...

  9. 用SignalR 2.0开发客服系统[系列3:实现点对点通讯]

    前言 交流群:195866844 目录: 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 用SignalR 2.0开发客服系统[系列2:实现聊天室] 真的很感谢大家的支持,今天发表系列3 ...

随机推荐

  1. js 时间转字符串,转成yyyy-MM-dd HH:mm:SS格式

    // 时间转字符串,转成yyyy-MM-dd HH:mm:SS格式 function dateToStr(datetime){ var dateTime = new Date(datetime); v ...

  2. MyBatis笔记----SSM框架mybatis3整合springmvc spring4

    上节 无springmvc框架 http://www.cnblogs.com/tk55/p/6661786.html 结构 jar包 web.xml 与index.jsp <?xml versi ...

  3. spreadJs 自动换行功能和自动增高行高

    var styleTmp = sheet.getStyle(displayRowIndex, displayColumnIndex, GcSpread.Sheets.SheetArea.viewpor ...

  4. Microsoft.AspNet.Identity 重置密码

    重置密码:先生成重置密码的Token,然后调用ResetPassword方法重置密码,密码要符合规则.. ApplicationUserManager UserManager => _userM ...

  5. VueJs入门(一)

    VueJs学习笔记:基本概念及简单demo Vue官方介绍:简单小巧的核心,渐进式技术栈,足以应付任何规模的应用. 简单小巧指的是vue.js压缩后仅有17KB,量轻.渐进式指的是我们学习和使用vue ...

  6. 解决ubuntu下,QQ重启后出现个人文件夹已被占用的问题

    首先,是wine QQ的安转教程:Wine安装最新版QQ(8.9.2)的简单教程 - Powered by Discuz! 里面作者也提到了关于重启后出现个人文件夹被占用的情况. 如下: 这里,如果不 ...

  7. js FormData方法介绍

    1. 概述 FormData类型其实是在XMLHttpRequest 2级定义的,它是为序列化表以及创建与表单格式相同的数据(当然是用于XHR传输)提供便利. 2. 构造函数 创建一个formData ...

  8. Git&GitHub-基础教程

    目录 1. Git简介 1.1 什么是版本控制系统? 1.2. Git的历史 1.3. 什么是分布式?什么是集中式? 2. Git安装 3. 创建一个版本库 4. Git的语法教程 4.1. 提交一个 ...

  9. 如何使用微信web开发者工具调试企业微信

    最近因工作需要围绕着企业微信来进行调试,从而掏出了我的微信web开发者工具,在进行授权的时候微信web开发者工具成功的提示了我:“您未绑定企业微信开发者权限”.那么关键来了,因为我也百度过,发现也有很 ...

  10. 【Linux基础】crontab定时命令详解

    周期执行的任务一般由cron这个守护进程来处理[ps -ef|grep cron].cron读取一个或多个配置文件,这些配置文件中包含了命令行及其调用时间.cron的配置文件称为“crontab”,是 ...