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

本文探讨了React中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>
    )
  }
}

效果如下:

reactref%E6%B5%8B%E8%AF%951

项目地址关于如何 pr

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

转载于:https://www.cnblogs.com/MuYunyun/p/9427911.html

### React 中受控组件与非受控组件的区别及使用场景 #### 受控组件 (Controlled Components) 在 React 中,当表单元素的状态由 React 的状态管理时,这些组件被称为 **受控组件**。这意味着所有的表单数据都存储在 React 组件的状态中,并通过 `props` 或回调函数来更新。 - 表单元素的值始终由 React 状态控制。 - 需要显式地处理每次用户输入事件并更新状态。 - 更适合复杂的交互逻辑动态验证场景。 以下是创建受控组件的一个简单例子: ```javascript class ControlledForm extends React.Component { constructor(props) { super(props); this.state = { value: '' }; } handleChange = (event) => { this.setState({ value: event.target.value }); }; handleSubmit = (event) => { alert('提交的内容: ' + this.state.value); event.preventDefault(); }; render() { return ( <form onSubmit={this.handleSubmit}> <label> 名字: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="提交" /> </form> ); } } ``` 这种模式允许开发者完全掌控表单的行为及其状态变化[^4]。 --- #### 非受控组件 (Uncontrolled Components) 相比之下,**非受控组件**更接近于原生 HTML 的行为方式。在这种情况下,React 不会维护表单元素的状态,而是让 DOM 自己保存其当前值。 - 使用 `ref` 来获取 DOM 节点中的实际值。 - 对简单的表单或者不需要频繁更新的应用程序来说更为简洁高效。 下面是一个典型的非受控组件实现方法: ```javascript class UncontrolledForm extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); // 创建 ref 实例 } handleSubmit = (event) => { alert('提交的内容: ' + this.inputRef.current.value); // 访问 input 值 event.preventDefault(); }; render() { return ( <form onSubmit={this.handleSubmit}> <label> 名字: <input type="text" ref={this.inputRef} /> {/* 将 ref 关联到 input */} </label> <input type="submit" value="提交" /> </form> ); } } ``` 这种方式更适合那些只需要读取最终结果而无需实时监听中间过程的情况[^4]。 --- #### 主要区别总结 | 特性 | 受控组件 | 非受控组件 | |--------------------------|-----------------------------------|----------------------------| | 数据源 | React State | DOM | | 更新机制 | 显式的 setState 事件处理器 | 默认浏览器行为 | | 复杂度 | 较高 | 较低 | | 场景适用 | 动态验证、复杂业务逻辑 | 简单表单或静态页面 | --- #### 使用场景分析 1. 如果应用程序需要对用户的每一次输入做出反应(例如自动完成建议),则应优先考虑采用 **受控组件**。 2. 当仅需收集最终的结果而不关心具体的变化细节时,则可以选用更加轻量级的解决方案——即 **非受控组件**。 尽管如此,在现代开发实践中推荐更多地依赖于受控组件,因为它们能够更好地融入 React 的生态系统并与诸如 Redux 这样的全局状态管理系统集成[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值