从 0 到 1 实现 React 系列 —— 4.setState优化和ref的实现

看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (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 的分享。

原文地址:https://www.cnblogs.com/MuYunyun/p/9427911.html

时间: 2024-08-02 04:10:02

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

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

本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/...) 项目地址 从 0 到 1 实现 React 系列 -- JSX 和 Virtual DOM 从 0 到 1 实现 React 系列 -- 组件和 state|props 从 0 到 1 实现 React 系列 -- 生命周期和 diff 算法 从 0 到 1 实现 React 系列 -- 优化 se

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

看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/...) 项目地址 组件即函数 在上一篇 JSX 和 Virtual DOM 中,解释了 JSX 渲染到界面的过程并实现了相应代码,代码调用如下所示: import React from 'react' import ReactDOM from 'react-dom' const element = ( <div className="titl

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

前言 这是 React 系列教程的第一篇,我们将用 React 实现 Animate.css 官网的效果.对于 Animate.css 官网效果是一个非常简单的例子,原代码使用 jQuery 编写,就是添加类与删除类的操作.这对于学习 React 来说是一个非常简易的例子,但是我并不会在教程中介绍相关的前置知识,比如 JSX.ES6 等,对于小白来说可能还会有一些困惑的地方,所以还要了解一下 React 相关的基础知识.虽然 React 有很多值得深究的知识,但这个系列教程并不会涉及高大深的内容

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

前言 上周发表了 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 这篇文章,得到了很多帮助和鼓励,小弟在此真心的感谢大家的支持.. 这周继续系列2,实现聊天室的功能. 开发环境 开发工具:VS2013 旗舰版 数据库:未用 操作系统:WIN7旗舰版 正文开始 首先我们来看看最终效果: 正式开始: SignalR作为一个强大的集线器,已经在hub里面集成了Gorups,也就是分组管理,使用方法如下: //作用:将连接ID加入某个组 //Context.ConnectionId 连接I

从0開始学习 GitHub 系列之「07.GitHub 常见的几种操作」

之前写了一个 GitHub 系列,反响非常不错,突然发现居然还落下点东西没写,前段时间 GitHub 也改版了,借此机会补充下. 我们都说开源社区最大的魅力是人人多能够參与进去,发挥众人的力量,让一个项目更完好.更强壮.那么肯定有人疑问,我自己眼下还没有能力开源一个项目,可是想一起參与到别的开源项目中.该怎么操作呢?那么今天,就来给大家一起介绍下 GitHub 上的一些常见的操作,看完之后你就知道方法了. 我们姑且以 Square 公司开源的 Retrofit 为例来介绍. 打开链接: http

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

原文:用SignalR 2.0开发客服系统[系列3:实现点对点通讯] 前言 目录: 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 用SignalR 2.0开发客服系统[系列2:实现聊天室] 真的很感谢大家的支持,今天发表系列3,我们的正菜马上就要来了.. 开发环境 开发工具:VS2013 旗舰版 数据库:未用 操作系统:WIN7旗舰版 正文开始 首先我们来看看实现的效果: 所用到的方法和类(重要): 其实细心的朋友应该早就发现了,在上篇博客我们就已经用到了这个方法: //调用指定

用SignalR 2.0开发客服系统[系列5:使用SignalR的中文简体语言包和其他技术点]

前言 交流群:195866844 目录: 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 用SignalR 2.0开发客服系统[系列2:实现聊天室] 用SignalR 2.0开发客服系统[系列3:实现点对点通讯] 用SignalR 2.0开发客服系统[系列4:负载均衡的情况下使用SignalR] 以上是系列目录,终于到了结束的时候了.... 为了这个系列,真的是绞尽脑汁,终于..决定在这里完结了.. 值得兴奋的是,在SignalR2.2的NuGet包中,终于出现了简体中文语言包.(

react的this.setState中的坑

react的this.setState中的有两个. 1.this.setState异步的,不能用同步的思维讨论问题 2.在进行组件通讯的回调的时候,this指向子组件,没有指向父亲这,怎么办呢.在 class gradingView extends React.Component { constructor(...args) { super(...args); this.state = { suffixIcon: <Icon type="up" />, popDivShow

【腾讯Bugly干货分享】React移动web极致优化

本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/579083d1c9da73584b02587d 最近一个季度,我们都在为手Q家校群做重构优化,将原有那套问题不断的框架换掉.经过一些斟酌,决定使用react 进行重构.选择react,其实也主要是因为它具有下面的三大特性. React的特性 1.Learn once, write anywhere 学习React的好处就是,学了一遍之后,能够写web, node直出,以及nat