普通视图

发现新文章,点击刷新页面。
今天 — 2025年9月7日首页

五、Redux进阶:UI组件、容器组件、无状态组件、异步请求、Redux中间件:Redux-thunk、redux-saga,React-redux

2025年9月7日 11:08

一、UI组件和容器组件

  1. UI组件负责页面的渲染(傻瓜组件)
  2. 容器组件负责页面的逻辑(聪明组件)

当一个组件内容比较多,同时有逻辑处理和UI数据渲染时,维护起来比较困难。这个时候可以拆分成“UI组件”和"容器组件"。 拆分的时候,容器组件把数据和方法传值给子组件,子组件用props接收。

需要注意的是: 子组件调用父组件方法函数时,并传递参数时,可以把方法放在箭头函数中(直接在函数体使用该参数,不需要传入箭头函数)。

拆分实例

未拆分前原组件

import React, {Component} from 'react';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
import { Input, Button, List } from 'antd';
// 引用store
import store from './store';
import { inputChangeAction, addItemAction, deleteItemAction } from './store/actionCreators';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }
  
  render() {
    return (
      <div style={{margin: '10px'}}>
        <div className="input">
          <Input
            style={{width: '300px', marginRight: '10px'}}
            value={this.state.inputValue}
            onChange={this.handleInputChange}
          />
          <Button type="primary" onClick={this.handleClick}>提交</Button>
        </div>
        <List
          style={{marginTop: '10px', width: '300px'}}
          bordered
          dataSource={this.state.list}
          renderItem={(item, index) => (<List.Item onClick={this.handleDelete.bind(this, index)}>{item}</List.Item>)}
        />
      </div>
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }

  // 输入内容时(input框内容改变时)
  handleInputChange(e) {
    const action = inputChangeAction(e.target.value);
    store.dispatch(action);
  }

  // 添加一项
  handleClick () {
    const action = addItemAction();
    store.dispatch(action);
  }
  
  // 点击删除当前项
  handleDelete (index) {
    const action = deleteItemAction(index);
    store.dispatch(action);
  }
}

export default TodoList;

拆分后-容器组件

import React, {Component} from 'react';

// 引用store
import store from './store';
import { inputChangeAction, addItemAction, deleteItemAction } from './store/actionCreators';
import TodoListUI from './TodoListUI';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }
  
  render() {
    return (
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}
        handleInputChange={this.handleInputChange}
        handleClick={this.handleClick}
        handleDelete={this.handleDelete}
      />
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }

  // 输入内容时(input框内容改变时)
  handleInputChange(e) {
    const action = inputChangeAction(e.target.value);
    store.dispatch(action);
  }

  // 添加一项
  handleClick () {
    const action = addItemAction();
    store.dispatch(action);
  }

  // 点击删除当前项
  handleDelete (index) {
    const action = deleteItemAction(index);
    store.dispatch(action);
  }
}

export default TodoList;

拆分后-UI组件

import React, { Component } from 'react';
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
import { Input, Button, List } from 'antd';

class TodoListUI extends Component {
  render() {
    return (
      <div style={{margin: '10px'}}>
        <div className="input">
          <Input
            style={{width: '300px', marginRight: '10px'}}
            value={this.props.inputValue}
            onChange={this.props.handleInputChange}
          />
          <Button type="primary" onClick={this.props.handleClick}>提交</Button>
        </div>
        <List
          style={{marginTop: '10px', width: '300px'}}
          bordered
          dataSource={this.props.list}
          // renderItem={(item, index) => (<List.Item onClick={(index) => {this.props.handleDelete(index)}}>{item}-{index} </List.Item>)}
          renderItem={(item, index) => (<List.Item onClick={() => {this.props.handleDelete(index)}}>{item}-{index} </List.Item>)}
        />
        {/* 子组件调用父组件方法函数时,并传递参数时,可以把方法放在箭头函数中(直接在函数体使用该参数,不需要传入箭头函数)。 */}
      </div>
    )
  }
}

export default TodoListUI;

二、无状态组件

当一个组件只有render函数时,可以用无状态组件代替。

  1. 无状态组件比普通组件性能高; 因为无状态组件只是函数,普通组件是class声明的类要执行很多生命周期函数和render函数。
  2. 无状态组件中的函数接收一个参数作为父级传过来的props。

例如下面这个例子 普通组件:

class TodoList extends Component {
  render() {
    return <div> {this.props.item} </div>
  }
}

无状态组件:

const TodoList = (props) => {
  return(
    <div> {props.item} </div>
  )}

三、Redux 中发送异步请求获取数据

1、引入axios,使用axios发送数据请求

import axios from 'axios';

2、在componentDidMount中调用接口

componentDidMount() {
  axios.get('/list.json').then(res => {
    const data = res.data;
    // 在actionCreators.js中定义好initListAction,并在reducer.js中作处理(此处省略这部分)
    const action = initListAction(data);
    store.dispatch(action);
  })
}

四、使用Redux-thunk 中间件实现ajax数据请求

1、安装和配置Redux-thunk

1.1、安装Redux-thunk

npm install redux-thunk --save

1.2、正常使用redux-thunk中间件在store中的写法

// 引用applyMiddleware
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

// 创建store时,第二个参数传入中间件
const store = createStore(
  reducer,
  applyMiddleware(thunk)
);

export default store;

redux-thunk使用说明

1.3、redux-thunk中间件 和 redux-devtools-extension 一起使用的写法

// 引入compose
import { createStore, applyMiddleware, compose} from 'redux';
import reducer from './reducer';
import thunk from 'redux-thunk';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
  applyMiddleware(thunk),
);

const store = createStore(reducer, enhancer);

export default store;

Redux DevTools插件配置说明

2、redux-thunk 的作用和优点

  1. 不使用redux-thunk中间件,store接收的action只能是对象;有了redux-thunk中间件,action也可以是一个函数。这样子就可以在action中做异步操作等。
  2. store接收到action之后发现action是函数而不是对象,则会执行调用这个action函数。
  3. 可以把复杂的异步数据处理从组件的生命周期里摘除出来(放到action中),避免组件过于庞大,方便后期维护、自动化测试。

3、使用redux-thunk的流程

  1. 在创建store时,使用redux-thunk。详见以上配置说明。

  2. 在actionCreators.js中创建返回一个方法的action,并导出。在这个方法中执行http请求。

import types from './actionTypes';
import axios from 'axios';

export const initItemAction = (value) => ({
  type: types.INIT_TODO_ITEM,
  value: value
})

// 当使用redux-thunk后,action不仅可以是对象,还可以是函数
// 返回的如果是方法会自动执行
// 返回的方法可以接收到dispatch方法,去派发其它action
export const getTodoList = () => {
  return (dispatch) => {
    axios.get('/initList').then(res => {
      const action = initItemAction(res.data);
      dispatch(action);
    })
  }
}

export const inputChangeAction = (value) => ({
  type: types.CHANGE_INPUT_VALUE,
  value: value
})

export const addItemAction = (value) => ({
  type: types.ADD_TODO_ITEM
})

export const deleteItemAction = (index) => ({
  type: types.DELETE_TODO_ITEM,
  value: index
})
  1. 在组件中引用这个action,并在componentDidMount中派发该action给store
import React, {Component} from 'react';

import store from './store';
import { getTodoList } from './store/actionCreators';

class TodoList extends Component {

  ...

  // 初始化数据(使用redux-thunk派发/执行一个action函数)
  componentDidMount() {
    const action = getTodoList();
    store.dispatch(action);
  }

  ...
}

export default TodoList;

4、具体执行流程

  1. 组件加载完成后,把处理异步请求的action函数派发给store;
  2. 因使用了redux-thunk中间件,所以可以接收一个action函数(正常只能接收action对象)并执行该方法;
  3. 在这个方法中执行http异步请求,拿到结果后再次派发一个正常的action对象给store;
  4. store发现是action对象,则根据拿来的值修改store中的状态。

五、什么是Redux的中间件

  1. 中间件指的是action 和 store 中间。
  2. 中间件实现是对store的dispatch方法的升级。

Redux数据流

几个常见中间件的作用(对dispatch方法的升级)

  1. redux-thunk:使store不但可以接收action对象,还可以接收action函数。当action是函数时,直接执行该函数。
  2. redux-log:每次dispatch时,在控制台输出内容。
  3. redux-saga:也是处理异步逻辑,把异步逻辑单独放在一个文件中管理。

六、redux-saga中间件入门

1、安装和配置redux-saga

1.1、安装redux-saga

npm install --save redux-saga

yarn add redux-saga

1.2、正常使用redux-saga中间件在store中的写法

import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducer';
import mySaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mySaga);

export default store;

redux-saga使用说明

1.3、redux-saga中间件 和 redux-devtools-extension 一起使用的写法

import { createStore, applyMiddleware, compose} from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducer';
import mySaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
const enhancer = composeEnhancers(
  applyMiddleware(sagaMiddleware),
);
const store = createStore(reducer, enhancer);
sagaMiddleware.run(mySaga);

export default store;

Redux DevTools插件配置说明

2、redux-saga 的作用和与redux-thunk的比较

  1. redux-saga也是解决异步请求的。但是redux-thunk的异步处理还是在aciton中,而redux-saga的异步处理是在一个单独的文件(sagas.js)中处理。
  2. redux-saga同样是作异步代码拆分的中间件,可以使用redux-saga完全代替redux-thunk。(redux-saga使用起来更复杂,更适合大型项目)
  3. redux-thunk只是把异步请求放到action中,并没有多余的API。而redux-saga是单独放在一个文件中处理,并且有很多PAI。
  4. 使用流程上的区别; 4.1. 使用redux-thunk时,从组件中派发action(action函数)时,监测到是函数,会在action中接收并处理,然后拿到结果后再派发一个普通action交给store的reducer处理,更新store的状态。 4.2. 使用redux-saga时,从组件中派发action(普通action对象)时,会先交给sagas.js匹配处理异步请求。拿到结果后再使用put方法派发一个普通action交给store的reducer处理,更新store的状态。

3、使用redux-saga的流程

  1. 在创建store时,使用redux-saga。详见以上配置说明。

  2. 在actionCreators.js中创建一个普通的action,并导出。

import types from './actionTypes';
// import axios from 'axios';

export const initItemAction = (value) => ({
  type: types.INIT_TODO_ITEM,
  value: value
})

// redux-thunk的写法,异步请求依然在这个文件中
// export const getTodoList = () => {
//   return (dispatch) => {
//     axios.get('/initList').then(res => {
//       const action = initItemAction(res.data);
//       dispatch(action);
//     })
//   }
// }

// redux-saga的写法,这里返回一个普通action对象;
// sagas.js中会用takeEvery监听这个type类型,然后执行对应的异步请求
export const getTodoList = () => ({
  type: types.GET_INIT_ACTION,
})

export const inputChangeAction = (value) => ({
  type: types.CHANGE_INPUT_VALUE,
  value: value
})

export const addItemAction = (value) => ({
  type: types.ADD_TODO_ITEM
})

export const deleteItemAction = (index) => ({
  type: types.DELETE_TODO_ITEM,
  value: index
})
  1. 在store文件夹中,创建一个文件sagas.js,使用redux-saga的takeEvery方法监听刚才派发的type类型,然后执行对应的函数,执行异步请求代码。拿到结果后再使用redux-saga的put方法派发一个普通的action对象,交给store的reducer处理。
import { takeEvery, put } from 'redux-saga/effects';
import types from './actionTypes';
import axios from 'axios';
import { initItemAction } from './actionCreators';

function* getInitList() {
  try {
    const res = yield axios.get('/initList');
    const action = initItemAction(res.data);
    yield put(action);
  } catch(e) {
    console.log('接口请求失败');
  }
}

// generator 函数
function* mySaga() {
  yield takeEvery(types.GET_INIT_ACTION, getInitList);
}

export default mySaga;
  1. 在组件中引用这个action,并在componentDidMount中派发该action给store
import React, {Component} from 'react';

import store from './store';
import { getTodoList } from './store/actionCreators';

class TodoList extends Component {

  ...

  // 初始化数据(使用redux-saga派发一个普通action对象,经由sagas.js的generator 函数匹配处理后,再交由store的reducer处理)
  componentDidMount() {
    const action = getTodoList();
    store.dispatch(action);
  }

  ...
}

export default TodoList;

4、具体执行流程

  1. 组件加载完成后,把一个普通的action对象派发给store;
  2. 因使用了redux-saga中间件,所以会被sagas.js中的generator函数匹配到,并交给对应的函数(一般也是generator函数)处理;
  3. sagas.js的函数拿到结果后,使用redux-saga的put方法再次派发一个普通action对象给store;
  4. sagas.js中没有匹配到对应的类型,则store交由reducer处理并更新store的状态。

七、如何使用React-redux完成TodoList功能

安装React-redux

npm install react-redux --save

1、把redux写法改成React-redux写法

1.1、 入口文件(src/index.js)的修改

  • 使用react-redux的Provider组件(提供器)包裹所有组件,把 store 作为 props 传递到每一个被 connect() 包装的组件。
  • 使组件层级中的 connect() 方法都能够获得 Redux store,这样子内部所有组件就都有能力获取store的内容(通过connect链接store)。

原代码

import React from 'react';
import ReactDOM from 'react-dom';
import TodoList from './todoList';

ReactDOM.render(<TodoList />, document.getElementById('root'));

修改后代码 ```jsx import React from 'react'; import ReactDOM from 'react-dom'; import TodoList from './TodoList'; import { Provider } from 'react-redux'; import store from './store';

// Provider向内部所有组件提供store,内部组件都可以获得store const App = ( )

ReactDOM.render(App, document.getElementById('root'));

<br/>
#### 1.2、组件(TodoList.js)代码的修改

Provider的子组件通过react-redux中的connect连接store,写法:
```jsx
connect(mapStateToProps, mapDispatchToProps)(Component)
  • mapStateToProps:store中的数据映射到组件的props中;
  • mapDispatchToProps:把store.dispatch方法挂载到props上;
  • Component:Provider中的子组件本身;

导出的不是单纯的组件,而是导出由connect处理后的组件(connect处理前是一个UI组件,connect处理后是一个容器组件)。


原代码
import React, { Component } from 'react';
import store from './store';

class TodoList extends Component {
  constructor(props) {
    super(props);
    // 获取store,并赋值给state
    this.state = store.getState();
    
    // 统一在constructor中绑定this,提交性能
    this.handleChange = this.handleChange.bind(this);
    this.handleStoreChange = this.handleStoreChange.bind(this);
    this.handleClick = this.handleClick.bind(this);

    // 在组件中订阅store,只要store改变就触发这个函数
    this.unsubscribe = store.subscribe(this.handleStoreChange);
  }

  // 当store状态改变时,更新state
  handleStoreChange() {
    // 用从store中获取的state,来设置state
    this.setState(store.getState());
  }

  render() {
    return(
      <div>
        <div>
          <input value={this.state.inputValue} onChange={this.handleChange} />
          <button onClick={this.handleClick}>提交</button>
        </div>
        <ul>
          {
            this.state.list.map((item, index) => {
              return <li onClick={() => {this.handleDelete(index)}} key={index}>{item}</li>
            })
          }
        </ul>
      </div>
    )
  }

  // 组件注销前把store的订阅取消
  componentWillUnmount() {
    this.unsubscribe();
  }
  
  handleChange(e) {
    const action = {
      type: 'change-input-value',
      value: e.target.value
    }
    store.dispatch(action);
  }

  handleClick() {
    const action = {
      type: 'add-item'
    }
    store.dispatch(action)
  }

  handleDelete(index) {
    const action = {
      type: 'delete-item',
      value: index
    }
    store.dispatch(action);
  }
}

export default TodoList;

修改后代码

省去了订阅store使用store.getState()更新状态的操作。组件会自动更新数据。

import React, { Component } from 'react';
import { connect } from 'react-redux';

class TodoList extends Component {
  render() {
    // const { inputValue, handleChange, handleClick, list, handleDelete} = this.props;

    return(
      <div>
        <div>
          <input value={this.props.inputValue} onChange={this.props.handleChange} />
          <button onClick={this.props.handleClick}>提交</button>
        </div>
        <ul>
          {
            this.props.list.map((item, index) => {
              return <li onClick={() => {this.props.handleDelete(index)}} key={index}>{item}</li>
            })
          }
        </ul>
      </div>
    )
  }
}

// 把store的数据 映射到 组件的props中
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}

// 把store的dispatch 映射到 组件的props中
const mapDispatchToProps = (dispatch) => {
  return {
    handleChange(e) {
      const action = {
        type: 'change-input-value',
        value: e.target.value
      }
      dispatch(action);
    },
    handleClick() {
      const action = {
        type: 'add-item'
      }
      dispatch(action)
    },
    handleDelete(index) {
      const action = {
        type: 'delete-item',
        value: index
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

#### 1.3、store/index.js 代码不需要修改 ```jsx import { createStore } from 'redux'; import reducer from './reducer'

const store = createStore(reducer);

export default store;

<br/>
#### 1.4、store/reducer.js 代码也不需要修改
```jsx
const defaultState = {
  inputValue: '',
  list: []
}
export default (state = defaultState, action) => {
  const { type, value } = action;
  let newState = JSON.parse(JSON.stringify(state));

  switch(type) {
    case 'change-input-value':
      newState.inputValue = value;
      break;
    case 'add-item':
      newState.list.push(newState.inputValue);
      newState.inputValue = '';
      break;
    case 'delete-item':
      newState.list.splice(value, 1);
      break;
    default:
      return state;
  }

  return newState;
}

2、代码精简及性能优化

  • 因现在组件(TodoList.js)中代码只是用来渲染,是UI组件。并且没有状态(state),是个无状态组件。所以可以改成无状态组件,提高性能。
  • 但connect函数返回的是一个容器组件。
import React from 'react';
import { connect } from 'react-redux';

const TodoList = (props) => {
  const { inputValue, handleChange, handleClick, list, handleDelete} = props;

  return(
    <div>
      <div>
        <input value={inputValue} onChange={handleChange} />
        <button onClick={handleClick}>提交</button>
      </div>
      <ul>
        {
          list.map((item, index) => {
            return <li onClick={() => {handleDelete(index)}} key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
}


// 把store的数据 映射到 组件的props中
const mapStateToProps = (state) => {
  return {
    inputValue: state.inputValue,
    list: state.list
  }
}

// 把store的dispatch 映射到 组件的props中
const mapDispatchToProps = (dispatch) => {
  return {
    handleChange(e) {
      const action = {
        type: 'change-input-value',
        value: e.target.value
      }
      dispatch(action);
    },
    handleClick() {
      const action = {
        type: 'add-item'
      }
      dispatch(action)
    },
    handleDelete(index) {
      const action = {
        type: 'delete-item',
        value: index
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

二、React基础精讲:编写TodoList、事件绑定、JSX语法、组件之间传值

2025年9月7日 11:03

一、使用React编写TodoList功能

JSX语法:render返回元素最外层必须由一个元素包裹。 Fragment 可以作为React的最外层元素占位符。

import React, {Component, Fragment} from 'react';

class TodoList extends Component {
  render() {
    return (
      <Fragment>
        <div>
          <input/>
          <button>提交</button>
        </div>
        <ul>
          <li>1111111</li>
          <li>2222222</li>
          <li>3333333</li>
        </ul>
      </Fragment>
    )
  }
}

export default TodoList;

二、React 中的响应式设计思想和事件绑定

  1. React在创建实例的时候, constructor(){} 是最先执行的;
  2. this.state 负责存储数据;
  3. 如果修改state中的内容,不能直接改,需要通过setState向里面传入对象的形式进行修改;
  4. JSX中js表达式用{}包裹;
  5. 事件绑定需要通过bind.(this)对函数的作用域进行变更;
import React, {Component, Fragment} from 'react';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: []
    }
  }
  
  render() {
    return (
      <Fragment>
        <div>
          <input value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
          <button>提交</button>
        </div>
        <ul>
          <li>1111111</li>
          <li>2222222</li>
          <li>3333333</li>
        </ul>
      </Fragment>
    )
  }

  handleChange (e) {
    this.setState({
      inputValue: e.target.value
    })
  }
}

export default TodoList;

三、实现 TodoList 新增删除功能

import React, {Component, Fragment} from 'react';

lass TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: ['学习英语', '学习React']
    }
  }
  
  render() {
    return (
      <Fragment>
        <div>
          <input value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
          <button onClick={this.handleBtnClick.bind(this)}>提交</button>
        </div>
        <ul>
          {this.state.list.map((item, index) => {
            return <li key={index} onClick={this.handleItemDelete.bind(this, index)}>{item}</li>
          })}
        </ul>
      </Fragment>
    )
  }
  handleChange (e) {
    this.setState({
      inputValue: e.target.value
    })
  }

  // 点击提交后,列表中添加一项,input框中内容清空
  handleBtnClick () {
    this.setState({
      list: [...this.state.list, this.state.inputValue],
      inputValue: ''
    })
  }

  // 删除
  handleItemDelete(index) {
    let list = [...this.state.list]; // 拷贝一份原数组,因为是对象,所以不能直接赋值,会有引用问题
    list.splice(index, 1);

    this.setState({
      list: list
    })

    // 以下方法可以生效,但是不建议使用。
    // React中immutable的概念:  state 不允许直接操作改变,否则会影响性能优化部分。
    
    // this.state.list.splice(index, 1);
    // this.setState({
    //   list: this.state.list
    // })
  }
}

export default TodoList;

四、JSX语法细节补充

1、在jsx语法内部添加注释:

  {/*这里是注释*/}

或者:

{
  //这里是注释
}

2、JSX语法中的属性不能和js中自带的属性和方法名冲突

元素属性class 替换成 className lable标签中的for 替换成 htmlFor

3、解析html内容

如果需要在JSX里面解析html的话,可以在标签上加上属性dangerouslySetInnerHTML属性(标签中不需要再输出item):如dangerouslySetInnerHTML={{__html: item}}

...

render() {
  return (
    <Fragment>
      {/* 这是一个注释 */}
      {
        // class 换成 className
        // for 换成 htmlFor
      }
      <div className="input">
        <lable htmlFor={"insertArea"}>请输入内容</lable>
        <input id="insertArea" value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
        <button onClick={this.handleBtnClick.bind(this)}>提交</button>
      </div>
      <ul>
        {this.state.list.map((item, index) => {
          return (
            <li
              key={index}
              onClick={this.handleItemDelete.bind(this, index)}
              dangerouslySetInnerHTML={{__html: item}}
            >
            </li>

            // <li
            //   key={index}
            //   onClick={this.handleItemDelete.bind(this, index)}
            // >
            //   {item}
            // </li>
          )
        })}
      </ul>
    </Fragment>
  )
}

...

五、拆分组件与组件之间的传值

父子组件之间通讯:

①父=>子

父组件通过属性向子组件传递数据,子组件通过this.props.属性名 获取父组件传来的数据。

②子=>父

子组件调用父组件的方法来改变父组件的数据。也是父组件通过属性把父组件对应的方法传递给子组件(在父组件向子组件传入方法时,就要绑定this,不然在子组件找不到方法),然后在子组件中通过this.props.方法名(属性名) 调用对应的父组件的方法并传递对应的参数。通过触发父组件方法改变数据,数据改变从而重新渲染页面。

父组件(todoList.js)
import React, {Component, Fragment} from 'react';
import TodoItem from './todoItem';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: ['学习英语', '学习React']
    }
  }
  
  render() {
    return (
      <Fragment>
        <div className="input">
          <label htmlFor={"insertArea"}>请输入内容</label>
          <input id="insertArea" value={this.state.inputValue} onChange={this.handleChange.bind(this)}/>
          <button onClick={this.handleBtnClick.bind(this)}>提交</button>
        </div>
        <ul>
          {this.state.list.map((item, index) => {
            return (
              <TodoItem
                key={index}
                index={index}
                item={item}
                deleteItem={this.handleItemDelete.bind(this)}
              />
            )
          })}
        </ul>
      </Fragment>
    )
  }

  handleChange (e) {
    this.setState({
      inputValue: e.target.value
    })
  }

  // 点击提交后,列表中添加一项,input框中内容清空
  handleBtnClick () {
    this.setState({
      list: [...this.state.list, this.state.inputValue],
      inputValue: ''
    })
  }

  // 删除
  handleItemDelete(index) {
    let list = [...this.state.list];
    list.splice(index, 1);

    this.setState({
      list: list
    })
  }
}

export default TodoList;
子组件(todoItem.js)
import React, { Component } from 'react';

class TodoItem extends Component {
  constructor(props) {
    super(props);
    this.handleDeleteItem = this.handleDeleteItem.bind(this);
  }

  render() {
    return (
      <li onClick={this.handleDeleteItem}>
        {this.props.item}
      </li>
    )
  }

  handleDeleteItem() {
    this.props.deleteItem(this.props.index);
  }
}

export default TodoItem;

六、TodoList 代码优化

  1. 事件方法的this指向要在constructor里面统一进行绑定,这样可以优化性能,如:this.fn = this.fn.bind(this)
  2. setState在新版的react中写成:this.setState(()=>{retuen {}}) 或 this.setState(()=>({}))。第一中写法可以在return前写js逻辑,新版的写法有一个参数prevState,可以代替修改前的this.state,同样是可以提高性能,也能避免不小心修改state导致的bug。
  3. JSX中也可以把某一块代码提出来,直接定义一个方法把内容return出来,再在JSX中引用这个方法。以达到拆分代码的目的。
import React, {Component, Fragment} from 'react';
import TodoItem from './todoItem';

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: '',
      list: ['学习英语', '学习React']
    }
    
    // 统一在constructor中绑定this,提交性能
    this.handleChange = this.handleChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    this.getTodoItem = this.getTodoItem.bind(this);
  }
  
  render() {
    return (
      <Fragment>
        <div className="input">
          <label htmlFor={"insertArea"}>请输入内容</label>
          <input id="insertArea" value={this.state.inputValue} onChange={this.handleChange}/>
          <button onClick={this.handleBtnClick}>提交</button>
        </div>
        <ul>
          {this.getTodoItem()}
        </ul>
      </Fragment>
    )
  }

  handleChange (e) {
    // this.setState({
    //   inputValue: e.target.value
    // })

    // 因这种写法setState是异步的,有时e.target获取不到,所以先赋值给一个变量再使用。
    const value = e.target.value;
    // 新版写法,setState不但可以接受一个对象,也可以接受一个方法
    // this.setState(() => {
    //   return {
    //     inputValue: value
    //   }
    // })

    // this.setState(()=>{retuen {}}) 简写成 this.setState(()=>({}))
    // 还可以再简写成
    this.setState(() => (
      {
        inputValue: value
      }
    ))
  }

  // 点击提交后,列表中添加一项,input框中内容清空
  handleBtnClick () {
    // this.setState({
    //   list: [...this.state.list, this.state.inputValue],
    //   inputValue: ''
    // })

    // 新版写法,可以使用prevState代替修改前的this.state,不但可以提高性能,也能避免不小心修改state导致的bug。
    this.setState((prevState) => {
      return {
        list: [...prevState.list, prevState.inputValue],
        inputValue: ''
      }
    })
  }

  // 删除
  handleItemDelete(index) {
    // let list = [...this.state.list];
    // list.splice(index, 1);

    // this.setState({
    //   list: list
    // })

    // 新版写法,可以在return前写js逻辑
    this.setState(() => {
      let list = [...this.state.list];
      list.splice(index, 1);
      return {list: list}
    })
  }

  // 把循环提取出来,放在一个方法中
  getTodoItem () {
    return this.state.list.map((item, index) => {
      return (
        <TodoItem key={index} index={index} item={item} deleteItem={this.handleItemDelete}/>
      )
    })
  }
}

export default TodoList;

七、围绕 React 衍生出的思考

1、声明式开发 可减少大量的dom操作; 对应的是命令式开发,比如jquery,操作DOM。

2、可以与其它框架并存 React可以与jquery、angular、vue等框架并存,在index.html页面,React只渲染指定id的div(如:root),只有这个div跟react有关系。

3、组件化 继承Component,组件名称第一个字母大写。

4、单向数据流 父组件可以向子组件传递数据,但子组件绝对不能改变该数据(应该调用父级传入的方法修改该数据)。

5、视图层框架 在大型项目中,只用react远远不够,一般用它来搭建视图,在作组件传值时要引入一些框架(Fux、Redux等数据层框架);

6、函数式编程 用react做出来的项目更容易作前端的自动化测试。

解锁时光机:用 React Hooks 轻松实现 Undo/Redo 功能

2025年9月7日 10:31

解锁时光机:用 React Hooks 轻松实现 Undo/Redo 功能

在日常应用开发中,撤销(Undo)和重做(Redo)功能几乎是用户体验的标配。它让用户可以大胆尝试,无需担心犯错。但你是否曾觉得实现这个功能很复杂?本文将带你深入理解一个优雅而强大的设计模式,并结合 React useReducer,手把手教你如何用最简洁的代码实现一个完整的带“时光机”功能的计数器。


思路核心:从“操作”到“状态快照”

大多数人在初次尝试实现 Undo/Redo 时,会陷入一个误区:记录操作本身。例如,我们记录下用户做了“增加”或“减少”操作。当需要撤销时,我们再根据记录反向计算出上一个状态。

这种方法看似合理,但当操作类型变得复杂时,逻辑会迅速膨胀,难以维护。

而更优雅的解决方案是:记录状态的快照。我们不关心用户做了什么,只关心每个操作发生前,状态是什么样子。这就像为每一个重要的时刻拍张照片,需要撤销时,我们直接回到上一张照片。

我们的数据模型将由三个部分组成:

  • present:当前的状态值。
  • past:一个数组,存储所有历史状态的快照。
  • future:一个数组,存储所有被撤销的状态,以便重做。

接下来,我们将基于这个思路,一步步构建我们的 React 应用。


实现详解:用 useReducer 驱动状态流转

useReducer 是一个强大的 Hook,特别适合管理复杂状态和状态间的转换。我们的“时光机”逻辑将全部封装在 reducer 函数中。

1. 初始化状态

首先,我们定义初始状态。计数器从 0 开始,past 和 future 数组都是空的。

const initialState = {
  past: [],
  present: 0,
  future: []
};

2. 处理正常操作 (increment 和 decrement)

当用户点击“增加”或“减少”按钮时,我们的 reducer 需要做两件事:

  1. 当前的 present 值,作为“历史快照”,添加到 past 数组的末尾。
  2. 更新 present 的新值。
  3. 最关键的一步:清空 future 数组。因为任何新的操作都意味着所有“重做”的历史都失效了。
if (action.type === "increment") {
  return {
    past: [...past, present], // 存储当前值到历史
    present: present + 1,     // 更新为新值
    future: []                // 新操作清空未来
  };
}

if (action.type === "decrement") {
  return {
    past: [...past, present],
    present: present - 1,
    future: []
  };
}

past: [...past, present]  这一行是整个设计的核心。我们存的不是“操作”,而是“操作前的状态值”。

3. 处理撤销操作 (undo)

撤销是“时光机”的核心功能。当用户点击“撤销”时:

  1. 当前的 present 值,移动到 future 数组的开头。这是为了以后能够“重做”这个状态。
  2. 从 past 数组中取出最后一个元素(也就是上一个状态),并将其设置为新的 present 值。我们可以使用 past.slice(0, -1) 来得到新的 past 数组,并用 past.at(-1) 获取最后一个元素。
if (action.type === "undo") {
  return {
    past: past.slice(0, -1),      // 移除最后一个历史状态
    present: past.at(-1),         // 上一个状态成为当前状态
    future: [present, ...future]  // 将当前状态存入未来
  };
}

4. 处理重做操作 (redo)

重做是撤销的逆过程。当用户点击“重做”时:

  1. 当前的 present 值,添加到 past 数组的末尾。
  2. 将 future 数组的第一个元素(即下一个状态)取出,并将其设置为新的 present 值。
  3. 移除 future 数组的第一个元素。
if (action.type === "redo") {
  return {
    past: [...past, present], // 当前状态存入历史
    present: future[0],       // 下一个未来状态成为当前状态
    future: future.slice(1)   // 移除已重做的未来状态
  };
}

完整的 React 组件代码

结合上述 reducer 逻辑,我们可以轻松构建出完整的 CounterWithUndoRedo 组件。

import * as React from "react";

const initialState = {
  past: [],
  present: 0,
  future: []
};

function reducer(state, action) {
  const { past, present, future } = state;

  if (action.type === "increment") {
    return {
      past: [...past, present],
      present: present + 1,
      future: []
    };
  }

  if (action.type === "decrement") {
    return {
      past: [...past, present],
      present: present - 1,
      future: []
    };
  }

  if (action.type === "undo") {
    // 如果没有历史记录,则不执行
    if (!past.length) {
      return state;
    }
    return {
      past: past.slice(0, -1),
      present: past.at(-1),
      future: [present, ...future]
    };
  }

  if (action.type === "redo") {
    // 如果没有未来记录,则不执行
    if (!future.length) {
      return state;
    }
    return {
      past: [...past, present],
      present: future[0],
      future: future.slice(1)
    };
  }

  throw new Error("This action type isn't supported.")
}

export default function CounterWithUndoRedo() {
  const [state, dispatch] = React.useReducer(reducer, initialState);

  const handleIncrement = () => dispatch({ type: "increment" });
  const handleDecrement = () => dispatch({ type: "decrement" });
  const handleUndo = () => dispatch({ type: "undo" });
  const handleRedo = () => dispatch({ type: "redo" });

  return (
    <div>
      <h1>Counter: {state.present}</h1>
      <button className="link" onClick={handleIncrement}>
        Increment
      </button>
      <button className="link" onClick={handleDecrement}>
        Decrement
      </button>
      <button
        className="link"
        onClick={handleUndo}
        disabled={!state.past.length} // 禁用条件past为空
      >
        Undo
      </button>
      <button
        className="link"
        onClick={handleRedo}
        disabled={!state.future.length} // 禁用条件future为空
      >
        Redo
      </button>
    </div>
  );
}

通过这种  “状态快照”  的思维方式,我们成功地将 Undo/Redo 逻辑与具体操作类型解耦。这不仅让代码变得简洁明了,更重要的是,它为未来的功能扩展奠定了坚实的基础。当你的应用变得更加复杂时,你无需修改核心的 undo 和 redo 逻辑,只需在处理新操作时,记得保存好状态快照即可。

昨天 — 2025年9月6日首页

从0到1搭建react-native自动更新(OTA和APK下载)

作者 卸任
2025年9月5日 16:00

前言

如何实现RN自动更新,这是一个好问题,之前写electron时接触过electron-updater这个库,electron有这个库,那么RN应该也有差不多的库。试着找了一下,是的Expo有一个差不多的,但是不适用我们的项目。

那就自己写一个差不多的update库,反正原理都知道了。

先来看看效果

第2幅图为OTA更新时,第3和第4是apk更新。

image.png

后面有仓库地址

正文

OTA更新

OTA 的核心优势在于它能够绕过传统的更新流程。例如,在移动应用开发中,传统的更新方式是开发者发布新版本到应用商店,用户再从商店下载并安装完整的应用新版本。

而有了 OTA,开发者可以直接将更新后的代码(通常是脚本文件)和资源文件推送到用户设备上,用户在下次打开应用时,就可以直接应用这些更新,无需再次通过应用商店下载和安装。

RN中的体现就是.bundle文件。这个Bundle文件实际上是一个JavaScript文件,它包含了应用的所有逻辑。当用户打开应用时,会加载并执行这个Bundle文件,从而渲染出界面并运行应用。

OTA 更新的本质就是 替换 这个 Bundle 文件。

APK更新

这个就简单了,安装apk安装包就可以了。

实现

打包相应文件到远程服务器

实现原理跟electron-updater差不多,远程得要有我们需要的Bundleapk文件,还要有一个版本记录文件version.json用来记录当前版本。

所以远程地址的目录大致长这个样子

OTA更新时。

image.png

有更新类型和下载地址,这个zip文件解压出来就是Bundle文件

image.png

APK更新时

image.png

有更新类型和下载地址

image.png

version.json中的内容

image.png

为了更好的生成这些东西,也准备了打包的脚本。只要把生成的东西放到远程服务器上就可以了。

image.png

判断更新类型,资源下载到设备

在生成文件时,生成的updateType字段有两个值,一个是full,一个是apk_requiredfull时下载Bundle文件压缩包,然后解压;apk_required时下载apk安装就可以了。

下载后会长成这个样子。

image.png

何时使用Bundle文件

上面说了OTA 更新的本质就是 替换 这个 Bundle 文件。那我们什么时候替换,什么时候使用本身的呢?

我们在OTA更新时versionName值是不会变动的,只有我们安装apk时它才会变动。

image.png

我们在下载Bundle文件时,也将对应的版本记录下来了。那么我们就可以这样,记录的版本大于versionName时就替换。

image.png

代码表示

code.png

使用样例

如何使用这个库就更简单了,两个方法checkForUpdate检查更新,installUpdate安装更新。剩下的事,不用你管。

code1.png

结语

感兴趣的可以去试试。

源码仓库: github.com/lzt-T/RNUpd…

样例:github.com/lzt-T/RNUpd…

React Fiber 架构与渲染流程

作者 维维酱
2025年9月5日 15:11

React 的 Fiber 架构是 React 16 中引入的重大重构,它彻底改变了 React 的渲染机制,为并发特性(如 Concurrent Mode)奠定了基础。

为什么需要 Fiber 架构?

传统 Stack Reconciler 的局限性

在 React 16 之前,React 使用栈协调器(Stack Reconciler),其存在以下问题:

  1. 不可中断的递归遍历:渲染过程是同步、不可中断的
  2. 阻塞主线程:大型组件树会导致界面卡顿
  3. 无法优先处理高优先级更新:所有更新同等对待

Fiber 架构的解决方案

Fiber 架构引入了:

  1. 可中断的渲染过程:将工作分解为小单元
  2. 优先级调度:不同更新有不同的优先级
  3. 并发渲染能力:为 Concurrent Mode 提供基础

Fiber 节点的核心结构

Fiber 是 React 的最小工作单元,每个组件对应一个 Fiber 节点:

// Fiber 节点结构(简化版)
type Fiber = {
  // 标识信息
  tag: WorkTag,           // 组件类型(函数组件、类组件、宿主组件等)
  key: null | string,     // 唯一标识
  type: any,              // 组件函数/类或DOM标签名
  
  // 树结构信息
  return: Fiber | null,   // 父节点
  child: Fiber | null,    // 第一个子节点
  sibling: Fiber | null,  // 下一个兄弟节点
  
  // 状态信息
  pendingProps: any,      // 新的 props
  memoizedProps: any,     // 上一次渲染的 props
  memoizedState: any,     // 上一次渲染的状态(hooks、state等)
  stateNode: any,         // 对应的实例(DOM节点、组件实例)
  
  // 副作用相关
  flags: Flags,           // 需要执行的副作用标记(增、删、更新)
  subtreeFlags: Flags,    // 子树中的副作用标记
  deletions: Fiber[] | null, // 待删除的子节点
  
  // 工作进度相关
  alternate: Fiber | null, // 上一次渲染的fiber节点(用于diff)
  lanes: Lanes,           // 优先级车道
  childLanes: Lanes,      // 子节点的优先级车道
  
  // Hook 相关(函数组件)
  memoizedState: any,     // Hook 链表头
};

Fiber 树的双缓存机制

React 使用双缓存技术来避免渲染过程中的视觉闪烁:

  1. Current Tree:当前屏幕上显示内容对应的 Fiber 树
  2. WorkInProgress Tree:正在构建的新 Fiber 树
// 双缓存工作机制
function updateComponent() {
  // 从当前fiber创建workInProgress fiber
  const current = currentlyRenderingFiber.alternate;
  if (current !== null) {
    // 复用现有的fiber节点
    workInProgress = createWorkInProgress(current, pendingProps);
  } else {
    // 创建新的fiber节点
    workInProgress = createFiberFromTypeAndProps(
      // ...参数
    );
  }
  
  // 处理workInProgress树...
}

完整的渲染流程

React 的渲染过程分为两个主要阶段:

1. Render 阶段(可中断)

Render 阶段是异步、可中断的,负责计算变更:

// Render 阶段工作循环
function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork !== null && !shouldYield) {
    // 执行当前工作单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    
    // 检查是否需要让出主线程
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  if (nextUnitOfWork !== null) {
    // 还有工作,稍后继续
    requestIdleCallback(workLoop);
  } else {
    // 所有工作完成,进入提交阶段
    commitRoot();
  }
}

performUnitOfWork 的深度优先遍历

function performUnitOfWork(fiber) {
  // 1. 开始工作:创建子fiber节点(调用组件render方法)
  const next = beginWork(fiber);
  
  if (next !== null) {
    return next; // 如果有子节点,返回子节点继续处理
  }
  
  // 2. 没有子节点,完成当前节点工作,转向兄弟节点或父节点
  let current = fiber;
  while (current !== null) {
    // 完成当前节点(生成effect列表等)
    completeWork(current);
    
    if (current.sibling !== null) {
      return current.sibling; // 处理兄弟节点
    }
    current = current.return; // 返回父节点
  }
  
  return null; // 遍历完成
}

beginWork:处理组件更新

function beginWork(fiber) {
  switch (fiber.tag) {
    case FunctionComponent:
      // 处理函数组件
      return updateFunctionComponent(fiber);
    case ClassComponent:
      // 处理类组件
      return updateClassComponent(fiber);
    case HostComponent:
      // 处理DOM元素
      return updateHostComponent(fiber);
    // ... 其他组件类型
  }
}

function updateFunctionComponent(fiber) {
  // 准备Hooks环境
  prepareToUseHooks(fiber);
  
  // 调用组件函数,获取子元素
  const children = fiber.type(fiber.pendingProps);
  
  // 协调子元素
  reconcileChildren(fiber, children);
  
  return fiber.child; // 返回第一个子节点
}

completeWork:完成节点处理

function completeWork(fiber) {
  switch (fiber.tag) {
    case HostComponent:
      // 处理DOM元素的属性更新等
      if (fiber.stateNode !== null) {
        // 更新现有的DOM节点
        updateDOMProperties(fiber.stateNode, fiber.memoizedProps, fiber.pendingProps);
      } else {
        // 创建新的DOM节点
        const instance = createInstance(fiber.type, fiber.pendingProps);
        fiber.stateNode = instance;
      }
      break;
    // ... 其他组件类型
  }
  
  // 收集effect到父节点
  if (fiber.flags !== NoFlags) {
    // 将当前fiber的effect添加到父节点的effect列表中
    let parent = fiber.return;
    while (parent !== null) {
      parent.subtreeFlags |= fiber.flags;
      parent = parent.return;
    }
  }
}

2. Commit 阶段(不可中断)

Commit 阶段是同步、不可中断的,负责将变更应用到DOM:

function commitRoot() {
  // 1. 预处理:调用getSnapshotBeforeUpdate等
  commitBeforeMutationEffects();
  
  // 2. 应用DOM变更
  commitMutationEffects();
  
  // 3. 将workInProgress树切换为current树
  root.current = finishedWork;
  
  // 4. 处理布局effect(如useLayoutEffect)
  commitLayoutEffects();
  
  // 5. 调度被动effect(useEffect)
  schedulePassiveEffects();
}

commitMutationEffects:处理DOM变更

function commitMutationEffects() {
  // 遍历effect列表,执行DOM操作
  while (nextEffect !== null) {
    const flags = nextEffect.flags;
    
    if (flags & Placement) {
      // 插入新节点
      commitPlacement(nextEffect);
    }
    if (flags & Update) {
      // 更新节点
      commitUpdate(nextEffect);
    }
    if (flags & Deletion) {
      // 删除节点
      commitDeletion(nextEffect);
    }
    
    nextEffect = nextEffect.nextEffect;
  }
}

优先级调度机制

Fiber 架构引入了优先级概念,确保高优先级更新优先处理:

// 优先级类型(简化)
const SyncLane = 0b0000000000000000000000000000001; // 同步优先级
const InputContinuousLane = 0b0000000000000000000000000000100; // 连续输入
const DefaultLane = 0b0000000000000000000000000010000; // 默认优先级

// 基于优先级的调度
function scheduleUpdateOnFiber(fiber, lane) {
  // 标记优先级
  fiber.lanes = mergeLanes(fiber.lanes, lane);
  
  // 调度更新
  if (lane === SyncLane) {
    // 同步更新,立即执行
    performSyncWorkOnRoot(root);
  } else {
    // 异步更新,根据优先级调度
    ensureRootIsScheduled(root);
  }
}

并发模式下的工作方式

在并发模式下,React 可以中断低优先级工作来处理高优先级更新:

// 高优先级更新中断低优先级工作
function handleUserInput() {
  // 高优先级更新(用户输入)
  scheduleUpdateOnFiber(root, InputContinuousLane);
  
  // 如果当前有低优先级渲染正在进行...
  // React 会中断它,先处理高优先级更新
}

// 被中断的工作可以稍后重新开始
function resumeInterruptedWork(interruptedFiber) {
  // 从中断的地方继续工作
  nextUnitOfWork = interruptedFiber;
  requestIdleCallback(workLoop);
}

错误处理机制

Fiber 架构改进了错误处理:

function renderRoot() {
  try {
    // 正常的渲染工作
    workLoop();
  } catch (error) {
    // 处理错误,寻找错误边界
    let fiber = nextUnitOfWork;
    while (fiber !== null) {
      if (fiber.tag === ClassComponent && 
          typeof fiber.type.getDerivedStateFromError === 'function') {
        // 找到错误边界组件
        captureError(fiber, error);
        break;
      }
      fiber = fiber.return;
    }
    
    // 重新尝试渲染
    restartRender();
  }
}

性能优化特性

1. 增量渲染

Fiber 将渲染工作分解为小单元,可以分段完成:

// 时间分片示例
function workLoopConcurrent(deadline) {
  while (nextUnitOfWork !== null && deadline.timeRemaining() > 0) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  
  if (nextUnitOfWork !== null) {
    // 还有工作,稍后继续
    requestIdleCallback(workLoopConcurrent);
  }
}

2. 子树渲染跳过

当 props 未变化时,可以跳过整个子树的渲染:

function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  if (current !== null) {
    // 检查props是否变化
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    
    if (oldProps === newProps && !hasContextChanged()) {
      // props未变化,可以跳过整个子树
      return null;
    }
  }
  
  // 需要重新渲染
  return updateFunctionComponent(workInProgress);
}

总结

React Fiber 架构的核心创新:

  1. 可中断的异步渲染:将渲染工作分解为小单元,可以暂停和恢复
  2. 优先级调度:不同更新有不同优先级,确保用户体验流畅
  3. 双缓存机制:避免渲染过程中的视觉闪烁
  4. 增量提交:DOM 变更分批进行,减少布局抖动
  5. 更好的错误处理:完善的错误边界机制

参考: incepter.github.io/how-react-w…

昨天以前首页

为什么说 useCallback 实际上是 useMemo 的特例

作者 维维酱
2025年9月4日 16:54

useCallback 是 useMemo 的特例,主要是因为它们在实现机制、设计理念和最终目的上高度一致,只是应用的场景和返回的值类型不同

1. 从概念等价性来看

从概念上讲,useCallback 完全可以由 useMemo 来实现:

// useCallback 的等效 useMemo 实现
const useCallbackEquivalent = (callback, deps) => {
  return useMemo(() => callback, deps);
};

// React 中的实际 useCallback 实现也是类似的思路
function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

你看,useCallback 本质上就是:"记忆一个函数,当依赖项不变时返回相同的函数引用"。这正是 useMemo 所能做的事情——记忆任何类型的值,包括函数。

2. 从实现源码来看

虽然 React 源码中 useCallbackuseMemo 有各自的实现函数,但它们的逻辑结构几乎完全相同:

useMemo 的实现核心

function updateMemo(create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null && nextDeps !== null) {
    if (areHookInputsEqual(nextDeps, prevState[1])) {
      return prevState[0]; // 返回缓存的值
    }
  }
  
  const nextValue = create(); // 重新计算
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

useCallback 的实现核心

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null && nextDeps !== null) {
    if (areHookInputsEqual(nextDeps, prevState[1])) {
      return prevState[0]; // 返回缓存的函数
    }
  }
  
  hook.memoizedState = [callback, nextDeps];
  return callback; // "计算"就是直接返回函数本身
}

注意两者的高度相似性:

  1. 相同的依赖比较逻辑:都使用 areHookInputsEqual 比较依赖项
  2. 相同的缓存机制:都存储在 hook.memoizedState
  3. 相同的决策流程:依赖不变则返回缓存值,变化则更新缓存

唯一的关键区别是:

  • useMemo 需要执行 create() 函数来获取新值
  • useCallback 直接返回传入的 callback 函数本身

3. 从设计意图来看

useMemo 的通用性

useMemo 是一个通用的记忆化工具,可以记忆任何类型的值:

// 记忆计算结果
const expensiveValue = useMemo(() => calculateExpensiveValue(a, b), [a, b]);

// 记忆对象
const config = useMemo(() => ({ timeout: 1000, retries: 3 }), []);

// 记忆数组
const items = useMemo(() => [1, 2, 3, 4], []);

// 记忆函数(这就是 useCallback 的作用!)
const onClick = useMemo(() => () => { /* 函数逻辑 */ }, []);

useCallback 的特化性

useCallback 是专门为记忆函数这个特定场景设计的语法糖:

// 使用 useCallback
const handleClick = useCallback(() => {
  console.log('Clicked!', someValue);
}, [someValue]);

// 等效的 useMemo
const handleClick = useMemo(() => () => {
  console.log('Clicked!', someValue);
}, [someValue]);

4. 从使用场景来看

为什么需要特化的 useCallback?

虽然可以用 useMemo 实现函数记忆,但 useCallback 提供了更好的开发体验:

  1. 更简洁的语法

    // useCallback - 更简洁
    const fn = useCallback(() => {}, [deps]);
    
    // useMemo 等效写法 - 更冗长
    const fn = useMemo(() => () => {}, [deps]);
    
  2. 更清晰的意图表达

    • useCallback 明确表示"我在记忆一个函数"
    • 代码可读性更好,开发者意图更明确
  3. 避免不必要的嵌套

    • useMemo(() => () => {}) 的双箭头函数容易造成混淆
    • useCallback 直接接受要记忆的函数

5. 从实际等价关系来看

// 以下两种写法完全等价:
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

const memoizedCallback = useMemo(
  () => () => {
    doSomething(a, b);
  },
  [a, b],
);

这种等价关系清晰地展示了为什么说 useCallbackuseMemo 的特例——它只是 useMemo 在函数记忆这个特定应用场景下的语法糖。

6. 性能考虑

值得注意的是,虽然两者在功能上等价,但在实现上 React 团队还是为 useCallback 做了专门的实现,避免了 useMemo(() => callback, deps) 这种写法中不必要的函数嵌套和创建:

// useMemo 的实现需要多创建一层函数
() => callback // 这一层包装函数每次渲染都会创建

// useCallback 直接存储和返回原始函数
// 稍微更高效一些

但这种性能差异通常可以忽略不计。

7. 什么时候应该使用 useCallback

1. 函数作为 props 传递给优化过的子组件

这是 useCallback 最经典的使用场景:

// 子组件使用了 React.memo 进行优化
const ChildComponent = React.memo(({ onClick, data }) => {
  console.log('子组件渲染');
  return <button onClick={onClick}>点击我: {data}</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState('初始数据');
  
  // 使用 useCallback 避免每次渲染都创建新函数
  const handleClick = useCallback(() => {
    console.log('点击处理:', count);
    setData(`更新后的数据 ${count}`);
  }, [count]); // count 变化时才创建新函数
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加计数: {count}</button>
      <ChildComponent onClick={handleClick} data={data} />
    </div>
  );
}

2. 函数作为其他 Hook 的依赖项

当函数被用作 useEffectuseMemo 或其他 Hook 的依赖时:

function Example({ userId }) {
  const [data, setData] = useState(null);
  
  // 使用 useCallback 确保函数引用稳定
  const fetchData = useCallback(async () => {
    const response = await fetch(`/api/user/${userId}`);
    const result = await response.json();
    setData(result);
  }, [userId]); // userId 变化时创建新函数
  
  // useEffect 依赖 fetchData
  useEffect(() => {
    fetchData();
  }, [fetchData]); // 由于 fetchData 引用稳定,effect 不会无限执行
  
  return <div>{data ? data.name : '加载中...'}</div>;
}

3. 函数被传递给事件处理库

当函数需要传递给第三方库,且该库对函数引用敏感时:

function ChartComponent({ data }) {
  const chartRef = useRef();
  
  // 使用 useCallback 确保处理函数引用稳定
  const handleClick = useCallback((event, chartElement) => {
    console.log('图表点击:', chartElement);
    // 处理点击逻辑
  }, []);
  
  useEffect(() => {
    const chart = new ThirdPartyChartLibrary(chartRef.current, {
      onClick: handleClick, // 第三方库可能依赖函数引用
      data: data
    });
    
    return () => chart.destroy();
  }, [data, handleClick]); // handleClick 引用稳定
  
  return <div ref={chartRef} />;
}

8.什么时候不需要使用 useCallback

1. 简单的内联函数

// ❌ 不必要的 useCallback
const handleClick = useCallback(() => {
  console.log('点击');
}, []);

// ✅ 直接使用内联函数即可
<button onClick={() => console.log('点击')}>点击</button>

2. 函数不会传递给子组件

function Component() {
  const [value, setValue] = useState('');
  
  // ❌ 不需要 useCallback,函数只在当前组件使用
  const handleChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);
  
  // ✅ 直接定义函数即可
  const handleChange = (e) => {
    setValue(e.target.value);
  };
  
  return <input value={value} onChange={handleChange} />;
}

3. 函数没有依赖项且性能影响可忽略

// ❌ 过度优化
const handleClick = useCallback(() => {
  window.location.href = '/about';
}, []);

// ✅ 简单情况直接定义函数
const handleClick = () => {
  window.location.href = '/about';
};

9.useCallback 的最佳实践

1. 正确声明依赖项

function Component({ id, name }) {
  const [count, setCount] = useState(0);
  
  // ✅ 正确:包含所有依赖项
  const handleAction = useCallback(() => {
    console.log(`ID: ${id}, Count: ${count}, Name: ${name}`);
    apiCall(id, count);
  }, [id, count, name]); // 所有依赖项都声明
  
  // ❌ 错误:遗漏依赖项
  const badHandleAction = useCallback(() => {
    console.log(`ID: ${id}, Count: ${count}`); // 使用了 id 和 count
  }, [id]); // 遗漏了 count 依赖
  
  return <button onClick={handleAction}>执行操作</button>;
}

2. 与函数式更新结合使用

function Counter() {
  const [count, setCount] = useState(0);
  
  // ✅ 使用函数式更新,避免 count 依赖
  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // 不需要依赖 count
  
  // ❌ 不必要的 count 依赖
  const badIncrement = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 导致 count 变化时创建新函数
  
  return <button onClick={increment}>计数: {count}</button>;
}

3. 与 useMemo 配合优化对象属性

function UserProfile({ user }) {
  // 使用 useCallback 记忆函数
  const onSave = useCallback((userData) => {
    saveUser(user.id, userData);
  }, [user.id]);
  
  // 使用 useMemo 记忆配置对象
  const config = useMemo(() => ({
    title: '编辑用户',
    onSubmit: onSave, // 使用记忆化的函数
    fields: ['name', 'email']
  }), [onSave]); // onSave 引用稳定
  
  return <Form config={config} />;
}

10.常见陷阱与解决方案

1. 依赖项循环

// ❌ 可能导致依赖项循环
const [data, setData] = useState([]);
const fetchData = useCallback(async () => {
  const result = await apiCall();
  setData(result);
}, [data]); // 错误地将 data 作为依赖

// ✅ 使用函数式更新避免循环
const fetchData = useCallback(async () => {
  const result = await apiCall();
  setData(result);
}, []); // 不需要 data 依赖

// ✅ 或者使用 ref 存储最新值
const dataRef = useRef();
dataRef.current = data;

const fetchData = useCallback(async () => {
  const result = await apiCall();
  console.log('当前数据:', dataRef.current); // 通过 ref 访问最新值
  setData(result);
}, []);

2. 过度使用导致性能下降

// ❌ 过度使用 useCallback
const handleClick1 = useCallback(() => {}, []);
const handleClick2 = useCallback(() => {}, []);
const handleClick3 = useCallback(() => {}, []);
// ... 很多 useCallback

// ✅ 合理使用,只在必要时使用
const handleClick1 = () => {};
const handleClick2 = () => {};
const handleClick3 = useCallback(() => {}, []); // 只有这个需要记忆化

3. 与 useEffect 的无限循环

// ❌ 可能导致无限循环
const [data, setData] = useState(null);
const fetchData = use

总结

"useCallback 是 useMemo 的特例" 是基于以下事实:

  1. 概念上的包含关系useCallback 的功能完全可以用 useMemo 实现
  2. 实现上的高度相似:两者共享相同的缓存机制和依赖比较逻辑
  3. 设计上的特化关系useCallback 是专门为函数记忆这个特定场景设计的语法糖
  4. 功能上的等价性useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

GitHub 星标太多管不过来?这款 AI 工具帮你一键整理、智能搜索!

作者 Java陈序员
2025年9月5日 09:24

大家好,我是 Java陈序员

之前,给大家介绍过一个开源的 Github Stars 存储库管理器,用于解决 GitHub 星标库太多不好管理的问题。

开发者必备!Github Stars 存储库管理器!

今天,再给大家介绍一款 Github 星标库管理工具,与 AI 结合,实现更加强大的分类搜索功能!

项目介绍

Github Stars Manager —— 一款帮助开发者有效组织、分类和搜索其 Star GitHub 仓库的工具,通过提供直观的分类、AI 分析和高级搜索功能管理星标库。

功能特色

  • 强大的搜索能力:实时关键词搜索(300ms 防抖优化)与 AI 语义搜索无缝切换,搜索结果高亮显示、统计面板实时展示匹配率和语言分布
  • 智能分类管理:基于 AI 驱动分析,自动生成仓库摘要、提取标签和支持平台信息,预设 14 个常用分类,支持自定义分类创建与管理
  • 个性化设置:所有数据 100% 本地保存,无需担心隐私泄露,支持多种 AI 服务(OpenAI、Anthropic、本地部署等),可自定义 API 设置
  • 现代化体验:英文界面无缝切换,AI 分析结果自动匹配界面语言,深色/浅色主题自适应,所有组件均支持深色模式

快速上手

安装部署

Github Stars Manager 提供了打包好的安装包,支持 Windows、MacOS, 可直接下载安装。

1、打开下载地址

https://github.com/AmintaCCCP/GithubStarsManager/releases

2、下载对应操作系统的安装包

3、双击运行安装包并根据提示安装

同步仓库

1、首次打开 Github Stars Manager 时,系统将提示你使用个人访问令牌连接 GitHub

2、点击链接 在GitHub上创建token → 或者根据提示打开浏览器页面创建 GitHub Token

3、GitHub Token 创建成功后,记得保存存储,并填写到 Github Stars Manager 中,最后点击 连接到Github → 按钮

4、首次 Github Stars Manager 需要同步仓库,点击顶部栏的同步图标进行仓库同步

点击同步后,等待一会,即可同步成功。如果同步失败了,检查下网络是否可以正常访问 Github 再重新同步。

5、Github Stars Manager 借助于 AI 能力可以自动分析仓库、提供智能搜索等功能,需要先在设置中添加AI服务配置才能使用 AI 相关的功能。

接下来就可以开始使用 Github Stars Manager 了。

功能体验

  • 应用分类

Github Stars Manager 根据仓库名称和内容、仓库描述、主题/标签等对仓库进行应用分类,默认预设了十几种分类,跟自定义添加分类。

  • 智能搜索

Github Stars Manager 提供强大的搜索过滤功能,支持按仓库名称、描述、编程语言、标签/主题进行搜索过滤。

  • 订阅发布

对一些喜欢的仓库可以进行订阅,然后通过发布时间线了解仓库的最新版本,每个发布会显示仓库名称、发布版本/标签、发布日期、发布说明摘要、下载链接。

  • AI 分析

Github Stars Manager 可以使用 AI 分析仓库并生成有用的总结信息。

  • 设置中心

Github Stars Manager 提供多种自定义选项,支持 WebDAV 备份配置、浅色和深色主题切换、中英文界面切换等。

本地开发

1、克隆仓库或下载源代码

git clone https://github.com/AmintaCCCP/GithubStarsManager.git

2、进入项目目录并安装依赖

cd GithubStarsManager

npm install

3、启动开发服务器

npm run dev

4、打开浏览器并访问

http://localhost:5173

如果你被杂乱的 GitHub 星标库所困扰,不妨试试 Github Stars Manager 这款工具,让 AI 帮你把星标仓库变成真正有用的资源库~

项目地址:https://github.com/AmintaCCCP/GithubStarsManager

最后

推荐的开源项目已经收录到 GitHub 项目,欢迎 Star

https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:

https://chencoding.top:8090/#/

大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!


React 在线 playground 实现指南:让代码在浏览器里「原地爆炸」的黑科技

作者 日月晨曦
2025年9月4日 23:29

各位掘金的小伙伴们,不知道你们有没有这样的困惑:想快速验证一段 React 代码,但又不想搭个完整的项目环境?今天我要给大家揭秘一个「浏览器里的 React 实验室」——如何用 @babel/standalone、Monaco Editor 和 iframe 打造一个炫酷的 React 在线 playground!

@babel/standalone:浏览器里的「代码翻译官」

首先,我们需要介绍今天的第一位主角:@babel/standalone。这货可不是普通的 babel 包,它是 babel 的「浏览器端特供版」!想象一下,你有一个「代码翻译官」,能在浏览器里直接把 ES6+、JSX 这些「高级语言」翻译成浏览器能听懂的「普通话」,这是不是很神奇?

以前,我们要用 babel 编译代码,得在命令行敲 babel src -d dist 这样的命令,现在有了 @babel/standalone,直接在浏览器里就能完成编译!就像是把翻译官从「办公室」请到了「现场」,即时翻译,效率翻倍!

Babel 工作原理:代码的「变形记」

既然提到了 babel,那就不得不说说它的工作原理。其实,babel 编译代码的过程就像是一场「变形记」:

  1. Parser 阶段:源码首先被「拆解」成抽象语法树(AST),这就像是把一篇文章拆成一个个词汇和语法结构。
  2. Transform 阶段:AST 经过「改造」,变成降级后的 AST,就像是把文言文翻译成白话文。
  3. Generate 阶段:最后,降级后的 AST 被「重新组装」成目标代码,就像是把拆解后的积木重新拼成一个新模型。

这个过程看起来复杂,但有了 @babel/standalone,我们只需要一行代码就能调用这个强大的「变形机器」!

动态导入 React:浏览器里的「魔法书包」

接下来,我们需要解决一个关键问题:如何在浏览器里动态导入 React?这时候,我们需要两个「魔法道具」:Blob + URL.createObjectURL 和 import maps + esm.sh。

Blob + URL.createObjectURL:代码的「隐形传送门」

Blob 就像是一个「代码容器」,我们可以把 JS 代码装进去,然后用 URL.createObjectURL 给它创建一个「临时身份证」(blob URL)。有了这个「身份证」,浏览器就能把这段代码当作一个普通的 JS 文件来加载。

想象一下,你写了一段 React 代码,然后用「隐形传送门」把它传送到浏览器的「代码世界」里,让浏览器以为这段代码是它自己「发现」的,是不是很巧妙?

import maps + esm.sh:依赖的「外卖小哥」

有了代码,还需要依赖包。这时候,import maps 就像是一张「地址簿」,告诉浏览器去哪里找这些依赖;而 esm.sh 则像是一个「外卖平台」,能把你需要的依赖(比如 React、ReactDOM)直接「配送」到浏览器里。

以前,我们要用 React,得先 npm install react react-dom,现在有了这两个「魔法道具」,直接在代码里写 import { useState, useEffect } from 'react',浏览器就能自动帮你找到并加载这些依赖!

Monaco Editor:程序员的「超级记事本」

现在,我们需要一个「超级记事本」来写代码,这就是 @monaco-editor/react 的用武之地。它是 VS Code 的「亲兄弟」,拥有几乎一样的编辑体验:代码高亮、智能提示、自动补全……应有尽有!

为了让这个「超级记事本」更聪明,我们还可以安装 @typescript/ata,让它能给 TypeScript 代码提供更精准的提示。想象一下,你在浏览器里写 TypeScript 代码,就像在 VS Code 里一样流畅,这是不是很享受?

iframe 预览:代码的「水晶球」

最后,我们需要一个「水晶球」来实时查看代码的运行效果,这就是 iframe 的作用。我们可以把编译后的代码注入到 iframe 中,让它在一个独立的环境里运行,这样既安全又能实时预览效果。

就像是你写了一段「魔法咒语」,然后通过「水晶球」实时看到咒语生效的效果,这种「所见即所得」的体验是不是很过瘾?

实战教程:从零搭建 React Playground

说了这么多,我们来看看如何从零搭建一个 React Playground 吧!

第一步:准备「魔法材料」

首先,我们需要引入必要的依赖:

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

第二步:设置「魔法地址簿」

然后,我们需要配置 import maps,让浏览器知道去哪里找依赖:

<script type="importmap">
{
  "imports": {
    "react": "https://esm.sh/react",
    "react-dom/client": "https://esm.sh/react-dom/client"
  }
}
</script>

第三步:召唤「超级记事本」

接下来,我们需要引入 Monaco Editor:

import Editor from '@monaco-editor/react';

function CodeEditor({ code, onChange }) {
  return (
    <Editor
      height="600px"
      language="javascript"
      value={code}
      onChange={onChange}
      options={{
        minimap: { enabled: true },
        fontSize: 14,
        tabSize: 2
      }}
    />
  );
}

第四步:打造「代码翻译官」

然后,我们需要用 @babel/standalone 来编译代码:

function compileCode(code) {
  try {
    const compiledCode = Babel.transform(code, {
      presets: ['react', 'env'],
      plugins: ['transform-modules-umd']
    }).code;
    return compiledCode;
  } catch (error) {
    return error.message;
  }
}

第五步:创建「水晶球」预览器

最后,我们需要创建一个 iframe 来预览代码效果:

function Preview({ compiledCode }) {
  const iframeRef = useRef(null);

  useEffect(() => {
    if (iframeRef.current && compiledCode) {
      const iframeDoc = iframeRef.current.contentDocument;
      const script = iframeDoc.createElement('script');
      script.type = 'text/javascript';
      script.text = compiledCode;
      
      // 清空之前的内容
      iframeDoc.body.innerHTML = '';
      iframeDoc.body.appendChild(script);
    }
  }, [compiledCode]);

  return <iframe ref={iframeRef} width="100%" height="600px" />;
}

第六步:组合所有「魔法部件」

现在,我们把所有的「魔法部件」组合起来:

function ReactPlayground() {
  const [code, setCode] = useState(`import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';

function App() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <h1>Hello React!</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);`);
  
  const [compiledCode, setCompiledCode] = useState('');
  
  useEffect(() => {
    const result = compileCode(code);
    setCompiledCode(result);
  }, [code]);
  
  return (
    <div className="playground">
      <h1>React Playground</h1>
      <div className="editor-container">
        <CodeEditor code={code} onChange={(value) => setCode(value || '')} />
      </div>
      <div className="preview-container">
        <Preview compiledCode={compiledCode} />
      </div>
    </div>
  );
}

写在最后

通过 @babel/standalone、Monaco Editor 和 iframe,我们成功打造了一个「浏览器里的 React 实验室」。这个 playground 不仅能让我们快速验证 React 代码,还能帮助新手更好地理解 React 的运行原理。

想象一下,你可以在任何有浏览器的设备上,随时随地写 React 代码,实时查看效果,这是多么酷的事情!而且,这个技术还可以扩展到 Vue、Angular 等其他框架,打造一个「全栈在线实验室」。

最后,送大家一句话:「技术的魅力,在于让复杂的事情变得简单。」希望这篇文章能帮助你理解 React Playground 的实现原理,也希望你能从中获得启发,创造出更多有趣的工具!

(全文完)

React中,函数组件里执行setState后到UI上看到最新内容的呈现,react内部会经历哪些过程?

2025年9月4日 18:13

一. 背景

作为一名react开发,每天都在和setState打交道,当我们要更新某个值并且UI上需要和这个值关联,那么需要用到setState,这是表象,那我也很好奇执行setState后到底发生了什么才会导致UI的变化,脑中多多少少会浮现一些概念,例如虚拟dom,diff算法,批量更新,还有react 16后的Fiber,延伸到时间分片,可中断,可恢复等,如果能想到这些,说明还是有尝试去了解过react框架的原理。我们不妨再提出一些疑问,setState只是更新一个值而已,是怎么和UI扯上关系了?setState执行后表象看只是修改了值,那怎么触发了diff的对比? 接下来我们就详细聊聊。

二. 具体过程

每一次的渲染都会经历两个大的过程,第一个是render阶段,这个阶段的结果就是生成diff的差异树;第二个阶段commit阶段,基于上一个阶段的差异树去修改真正的dom,渲染到页面上,并且执行一系列useEffect和ref的更新。render阶段里涵盖了大部分的核心概念,它是异步的,而第二个阶段commit,是同步的,必须一次性更新完。所有例如useEffect这些钩子函数都是在render后才会执行,而state值也是在render后才能拿到,当然你也可以马上拿到,用flusync包裹住就可以,它能强制同步执行更新。
了解完大的概念后,现在细化步骤。

1) 入队:setState 发生了什么 ?

这一步入队,如果不去查资料,大部人会忽略这个过程,但是它也是整个流程的第一步,没有它无法串联整体。 当我们执行setState实质是把这个更新操作放到react内部的更新队列中去,PS(队列,先进先出,类比排队买票,第一个排队的先买到,保证按顺序执行) 类组件的setState和函数组件的useState会有不同的入队逻辑,这里我们只讨论函数组件的做法。
在说这个之前先说说fiber的概念,才能往下讲,Fiber是react16及之后版本出的概念,一直在逐步完善,到现在React19已经完全成熟,它用链表来替代以前的递归树的方式。

Fiber 的概念

Fiber = React 内部用来表示组件的单元工作(Unit of Work)

  • 每个组件、每个 DOM 节点对应一个 Fiber 对象

  • Fiber 保存:

    1. 组件类型(函数组件、类组件、原生 DOM)
    2. 当前状态(state / props / hook)
    3. 子节点 / 兄弟节点 / 父节点指针(构建 Fiber 树)
    4. 更新队列 / effect list(commit 阶段更新 DOM 的计划)
  • Fiber 的设计核心:可中断、可分片、优先级调度

简化结构:

Fiber = {
  type,                  // 组件类型
  stateNode,             // 组件实例或 DOM 节点
  child, sibling, return,// 构建树
  memoizedState,         // Hook 或类组件 state
  updateQueue,           // 更新队列
  effectTag,             // 标记 DOM 更新类型
  nextEffect,            // Effect 链表
}

每个函数组件都是个Fiber节点,而函数组件里每个hook对应这Fiber上的hook链表节点,那hook链表又是存放在哪?

Hook = {
  memoizedState: 当前 state,
  queue: UpdateQueue,
  next: Hook | null
 
}

UpdateQueue = {
  pending: Update | null
}

Update = {
  action: 新的 state  updater 函数,
  next: Update | null
  lane: 优先级 // 影响调度时机
}
  • Fiber.memoizedState → 指向第一个 Hook
  • 每个 Hook 内部有自己的 queue.pending 链表(循环链表实现)
    此时setState就存放在自己hook链表节点的queue队列里。 这里又产生一个新的疑问,是入队了,那是怎么往下执行的?是的,当更新操作入队后还会给对应的Fiber打上“脏”的标记,其实就是更新update对象里lane(React 18完全成熟引入和开放)的值,同时通知Scheduler调度器安排合适的时机执行render阶段。

函数组件

setCount(2);
  1. Hook.queue.pending 入队
  2. Fiber 上标脏:
markUpdateLaneFromFiberToRoot(fiber, lane)
  • 从当前 Fiber 向上找根节点(RootFiber)
  • 在根节点的 pendingLanes 记录本次更新的 lane
  • 根节点就是 Scheduler 调度入口

2) 调度Scheduler:通知去执行render流程,但是决定何时以什么节奏去执行

先看看Scheduler的发展史,它是任务调度的核心。

1️⃣ React 15 及以前(Stack Reconciler)

  • 递归渲染整棵树

  • 同步更新:一旦 setState 被调用,React 就会从根节点递归 render 整棵树

  • 问题

    • 大组件渲染阻塞主线程 → 卡 UI
    • 无法中断渲染 → 用户输入延迟
    • 优先级调度不可行
  • 调度机制:几乎没有,更新按调用顺序立即执行


2️⃣ React 16(Fiber 架构引入)

  • 核心目标:可中断渲染(interruptible rendering)、分片渲染(time-slicing)

  • Fiber 的出现

    • 每个组件 / DOM 节点对应一个 Fiber 对象
    • Fiber 保存 state、更新队列、effect list
    • 渲染变成 每个 Fiber 一个工作单元 → 可暂停/恢复
  • 调度机制

    • Scheduler 核心概念开始出现
    • 初期主要支持 时间分片 + 可中断 render
    • 优先级调度尚未完全完善

3️⃣ React 16.3–16.8

  • 引入 Hook(16.8)
  • 函数组件的 state 更新依赖 Fiber.memoizedState + queue
  • Scheduler 可以根据更新的 lane / 优先级调度 render
  • 开始支持 批处理事件源内更新

4️⃣ React 17

  • 自动批处理只在 React 自己的合成事件里生效
  • Scheduler 仍然以 Fiber 为单位调度
  • 低优先级更新(宏任务 / setTimeout)仍然独立 render

5️⃣ React 18(调度机制成熟)

  • Automatic Batching(自动批处理)

    • 不仅在合成事件内,跨微任务也能批处理
    • 多个 setState → 一个 render
  • 并发模式(Concurrent Mode / Concurrent Features)

    • 基于Fiber
    • Scheduler 支持 优先级 lane
    • 可中断 render → 时间切片(time-slicing)
    • 高优先级任务(用户输入)可中断低优先级任务
  • 调度工具

    • 微任务 queueMicrotask / Promise.then → 高优先级立即 flush
    • requestIdleCallback / polyfill → 空闲时间渲染低优先级任务

React 18 调度 & 渲染总流程

用户多次调用 setState
   ┌──────────────────────────────┐
   │   同一事件循环内 → 自动批处理  │
   │   多个 update 合并为一次调度任务 │
   └───────────┬──────────────────┘
               ▼
        createUpdate()
        enqueueUpdate()
               │
               ▼
     scheduleUpdateOnFiber(fiber, lane)
               │
               ▼
     ensureRootIsScheduled(root)
               │
               ▼
  scheduleCallback(priority, performConcurrentWorkOnRoot)
               │
               ▼
        ┌───────────────────────────┐
        │ Scheduler 阶段 (任务调度器) │
        │ - 排序任务队列               │
        │ - 选择最高优先级任务执行       │
        │ - 新高优先级任务会取消低优先级 │
        │   → 重新发起调度 (插队)       │
        └───────────┬───────────────┘
                    ▼
       执行 performConcurrentWorkOnRoot(root)
                    │
                    ▼
         renderRootConcurrent(root)
                    │
                    ▼
          workLoopConcurrent()
          ┌──────────────────────────────┐
          │ 渲染过程 (可中断/恢复)          │
          │ - 每处理一个 Fiber 调用 shouldYield()│
          │ - 如果需要让出 (帧到期/高优先级进来) │
          │   → 停止渲染,中断退出           │
          │ - 下次调度时从断点继续           │
          └──────────────────────────────┘
                    │
                    ▼
        render 阶段完成 → 生成新 Fiber 树
                    │
                    ▼
              commitRoot(root)
               (更新 DOM)

调度器 (Scheduler) 做的事

  • 接收任务(React 提交的 render 任务)
  • 按优先级排序
  • 在浏览器空闲时 / 下一帧回调时执行任务函数

它不是主动去触发render阶段,而是合适的调度时机下执行render的回调,也就是这一句: scheduleCallback(priority, performConcurrentWorkOnRoot),开始进入render阶段

⚠️ 它不会自己打断 render,它只是会在“下一个调度点”决定要不要继续执行任务。

4) 渲染阶段(Render):构建Fiber树(可被打断/恢复重执行),diff生成差异树(Effect List)

终于进入render阶段,这一阶段的结果是要获取到最新的虚拟dom。这里第一个疑问,第一个阶段入队,当时把更新操作存放到队列,此时这些state的更新后的值什么时候使用?拿到这些新值后怎么和UI结合?
这个阶段会基于上一次的提交后的state的值去遍历执行updateQueue,只有queue的lane和本次render的lane一致的才执行,其他跳过,此时就能获取到最新的state,然后执行整个函数组件拿到最新的JSX生成的React Element树,使用这颗React Element树和current fiber树进行通过深度遍历的diff对比生成对应的Fiber树,而这颗新树就是workInProgress fiber树,对比过程中会记录要更新的fiber节点并形成一颗Effect List树,这就是render阶段要产出的实际结果。

React Fiber 渲染阶段流程图(Render Phase)

┌──────────────────────────────┐
│          更新触发             │
│   setState / props / 高优先级任务 │
└──────────────┬───────────────┘
               ▼
┌──────────────────────────────┐
│    调度器 scheduleUpdateOnFiber │
│  - 将更新加入 fiber.updateQueue     │
│  - 按优先级调度任务                 │
│  - 合并同一事件循环的多个 setState │
└──────────────┬───────────────┘
               ▼
┌──────────────────────────────┐
│    创建 workInProgress Fiber 树 │
│  - 克隆 current Fiber → workInProgress │
│  - 准备渲染阶段构建子 Fiber           │
└──────────────┬───────────────┘
               ▼
┌──────────────────────────────┐
│      DFS 遍历 workInProgress Fiber 树 │
│  performUnitOfWork(Fiber)          │
│  ├─ beginWork(Fiber)               │
│  │   ├─ 获取最新 pendingProps      │
│  │   ├─ 遍历 updateQueue → 计算 memoizedState(最新 state) │
│  │   ├─ 执行函数组件 / render() → 返回 ReactElement │
│  │   └─ 构建子 Fiber 树 (JSX → Fiber) │
│  ├─ completeWork(Fiber)            │
│  │   ├─ 对比 current Fiber → 生成 flags │
│  │   │    ├─ Placement → 新增 DOM 标记 │
│  │   │    ├─ Update    → 更新 DOM 属性/文本 │
│  │   │    └─ Deletion  → 删除 DOM    │
│  │   ├─ 收集子树 effect list → 合并到父节点 │
│  │   └─ 如果自身有 flags → 插入 effect list 尾部 │
│  └─ 检查 shouldYield() → 超过时间片可暂停渲染 │
└──────────────┬───────────────┘
               ▼
┌──────────────────────────────┐
│ DFS 遍历完成 workInProgress 树    │
│  - workInProgress Fiber 树已构建完成 │
│  - effect list 已收集完成            │
│  - commit 阶段准备执行               │
└──────────────────────────────┘

以上流程注意workInProgress Fiber 树最开始的是由Current树拷贝而来的,通过遍历fiber执行fiber上的updateQueque去更新fiber节点,而workInProgress Fiber 的 alternate → current Fiber,这样就能开始diff对比。
此外Fiber树是UI的快照,只构建当前在dom里的内容,未加载的路由是不会纳入构建范围的。
这里还涉及两个关键点:

1、diff算法

React 使用 “同级比较” + “最小更新” 的策略(O(n) 性能优化):

同级节点才会比较(父子不同级不会对比)

判断节点是否可复用

-   **type 不同 → 不能复用**(必须销毁旧 Fiber,创建新 Fiber)
-   **key 不同 → 不能复用**(即使 type 相同,也视为不同节点,需要移动或替换)

换句话说:

同级节点必须同时 type 和 key 匹配才能复用 Fiber

所以key的稳定性对于优化至关重要,以及当我们共用一个组件,当不同props对这个组件有不同的影响,可以通过更新key来直接销毁再新建,比在组件内部去判断各种状态要更方便和安全。

2.检查 shouldYield()

shouldYield并发模式(Concurrent Mode)下的时间片调度机制核心函数,用于判断 是否该暂停当前渲染,把控制权交还给浏览器,以保持界面响应流畅。每处理完一个 Fiber 节点或一小段任务时,会调用 shouldYield()

while (workInProgress !== null && !shouldYield()) {
  workInProgress = performUnitOfWork(workInProgress);
}


如何判断时间片用完

shouldYield 内部通常依赖 浏览器提供的时间 API

  • React 18 使用 Scheduler 库,基于 performance.now()MessageChannel
  • 每个时间片默认约 5ms(可配置)
  • 判断逻辑:
function shouldYield() {
  const currentTime = Scheduler.now();
  return currentTime >= deadline; // deadline = 时间片结束时间
}

当浏览器有更高优先级任务(如用户输入事件)时,也会提前返回 true

6) 提交阶段(Commit):一次性把变更更新到真实的dom上(不可中断)

基于上面生成的Effect List,终于进到最后的阶段,也是把变化的虚拟dom更新到真实dom上,最后呈现最新的内容在页面上。

总流程概览

render 阶段完成(effect list 已构建)
        │
        ▼
commitRoot(rootFiber)
        │
        ├─ commitBeforeMutationEffects(root)   // 执行 getSnapshotBeforeUpdate
        │
        ├─ commitMutationEffects(root)        // 更新 DOM / 插入 / 删除 / 更新 props
        │
        ├─ commitLayoutEffects(root)          // 调用生命周期和 useLayoutEffect
        │
        └─ 完成后切换 workInProgress → current,effect list 清空,flags 重置

可以见到,只有完成所有阶段才调用钩子函数拿到最新的state。

以上都是基于某次setState来推论的,那当我们首次打开页面,这时候current fiber树是没有的,那WIP的初始化应该也没有,这时候是怎么构建首次fiber树?
第一次肯定是要新建fiber.

1. 根 Fiber(HostRoot)是入口

当你调用:

ReactDOM.createRoot(container).render(<Home />);

发生了:

  1. React 创建一个 HostRoot Fiber 对应根容器 DOM:

    const rootFiber = {
      tag: HostRoot,
      stateNode: container, // 根 DOM
      child: null,
      return: null,
    };
    
  2. 这个 Fiber 是整个 Fiber 树的根节点(root.current = null,第一次渲染前没有 Fiber 树)。


2. 调度器触发更新

  • scheduleUpdateOnFiber(rootFiber) 被调用,React 知道要渲染 rootFiber 下的 UI
  • 根 Fiber 的 updateQueue 存储了要渲染的 element(这里就是 <Home />

3. Render 阶段:创建 Home Fiber

  1. 开始 render 阶段

    workInProgress = createWorkInProgress(rootFiber, null)
    
  2. beginWork(workInProgress) 执行时:

    • current = rootFiber.current → null(第一次)
    • workInProgress.pendingProps = <Home />
  3. 调用 reconcileChildrenworkInProgress.pendingProps 生成子 Fiber:

    • pendingProps<Home /> JSX

    • React 看到 <Home /> 是函数组件 → 创建 Fiber:

      const homeFiber = {
        tag: FunctionComponent,
        type: Home, // 对应函数组件
        stateNode: null, // 函数组件没有实例
        child: null,
        return: workInProgress, // parent = HostRoot Fiber
      };
      
  4. Home Fiber 就这样诞生了,是 HostRoot 的 child

核心:Fiber 是在 reconcileChildren 阶段根据 JSX 类型创建的。


4. 构建子 Fiber 树

  • 执行 Home() → 返回 JSX:

    <Box>
      <Header />
      <List />
    </Box>
    
  • reconcileChildren(Home Fiber, JSX) → 为 Box、Header、List 创建对应 Fiber

  • Fiber 树的 child/sibling/return 指针全部建立

  • 这就是第一次 workInProgress Fiber 树


5. 总结 Home Fiber 的来源

步骤 说明
入口 ReactDOM.render() → 创建 HostRoot Fiber
调度 scheduleUpdateOnFiber(rootFiber) → 标记根更新
render beginWork + reconcileChildren → 根据 <Home /> JSX 创建 FunctionComponent Fiber
结果 Home Fiber 成为 HostRoot Fiber 的 child,保存 type = Home,stateNode = null

核心点:Fiber 不是预先存在的,而是 React 在渲染阶段根据 JSX 动态创建。第一次没有 current Fiber,也不影响它的生成。

以上就是整个阶段的解析,大部分都是基于询问AI得出结论,一点点的提出疑问来完善这个流程。

浅谈React中虚拟DOM、diff算法、fiber架构的关系(面试可用)

作者 天天扭码
2025年9月3日 23:02

写在前面

正如标题所讲,这里只是浅谈三者的关系,并不会很深入的去讲底层的源码之类的东西,大家可以有批判的去看

开始吧

一、偏虚拟DOM部分

首先是虚拟DOM树,为什么React要有虚拟DOM树,什么是虚拟DOM树。JS虽然是用来操作DOM的脚本,但是操作DOM的成本还是很大的,那么有没有一种成本低一点的DOM树操作?有的兄弟,我们可以用JS去模拟虚拟DOM树,哎,既然不操作真实的DOM树,页面也不会变,这不是自己骗自己吗兄弟?实则不然。现在就先理解成是自己骗自己吧。

让我们聚焦到虚拟DOM的模拟,JS如何模拟虚拟DOM,很简单一个对象代表一个节点就可以了,对象模拟节点其实很简单,简单来说只需要三个属性,现在是三个节点,三个,考虑的是最简单的情况,如下

三个属性就可以,我们拿一个最简单的DOM树来看

image.png

可以看一下这个DOM树,这里一个蓝色的圈是一个节点,彩色是附着在黑色上的哈

那么一个节点在HTML中是什么表现形式呢?

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"> <!-- meta元素,设置charset属性 -->
    <title>这里是标题文本</title> <!-- title元素,包含文本 -->
</head>
<body>
    <a href="#">这里是链接文本</a> <!-- a元素,包含href属性和文本 -->
</body>
</html>

那么现在我们要模拟一个节点,我们要考虑什么,现在就最简单的考虑。

首先要有节点的类型——head、meta、title、body、a

其次要有节点中的属性——比如charset、href

最后我们要有子节点——比如meta、title是head的子节点;a是body的子节点等。

OK!三个属性齐活了,下面的Element就是我们的模拟,对,很漂亮的模拟。很多的节点对象就组成了一个虚拟DOM树。

// element.js
// 虚拟DOM元素的类,构建实例对象,用来描述DOM
class Element {
    constructor(type, props, children) {
        this.type = type;
        this.props = props;
        this.children = children;
    }
}
// 创建虚拟DOM,返回虚拟节点(object)
function createElement(type, props, children) {
    return new Element(type, props, children);
}
export {
    Element,
    createElement
}

经过上述的讲解,我们大概了解了虚拟dom树的结构,那么接下来我们来考虑虚拟DOM树为何高效,以及虚拟DOM树是否是“自己骗自己”

虚拟DOM为何高效?

这个是对比来讲的,我们可以来看一下为什么js操作真实DOM昂贵之处就理解了——

  1. 布局:  当你修改了 DOM 元素的几何属性(如宽度、高度、位置、边距、边框等)时,浏览器需要重新计算所有受影响元素的几何位置和尺寸,这个过程叫做“布局”或“重排”。
    想象一下,你在一个大文档中移动了一个小方块。对于这个方块,浏览器需要算它的新位置。但如果这个方块周围的元素也受其影响(比如,如果它旁边的文本会因为方块移动而换行),那么浏览器需要重新计算这些周围元素的布局。更糟糕的是,如果这个方块的位置依赖于它父元素的大小,而父元素的大小又依赖于它的父元素…… 这种情况会形成一个连锁反应,可能需要浏览器重新计算整个页面或部分页面的布局。

  2. 绘制:  当像素颜色、背景、边框颜色等外观属性发生变化时,浏览器需要重新绘制受影响的区域,这个过程叫做“绘制”。
    改变一个元素的背景颜色,或者改变一个元素的文本颜色。浏览器只需要重新渲染这些受影响的像素。

为什么 JavaScript 直接操作会频繁触发这些操作?

考虑以下 JavaScript 代码:

const div = document.getElementById('myDiv');

// 第一次修改:宽度
div.style.width = '200px'; // 触发一次 Layout

// 第二次修改:高度
div.style.height = '300px'; // 再次触发一次 Layout

// 第三次修改:背景颜色
div.style.backgroundColor = 'blue'; // 触发一次 Repaint

// 第四次修改:文本颜色
div.style.color = 'white'; // 再次触发一次 Repaint

在这个简化的例子中,每一次对 div.style 的直接修改(尤其是修改几何属性)都可能立即触发一次浏览器对 DOM 的计算和更新(Reflow 或 Repaint)。如果在一个循环中进行多次 DOM 操作,浏览器就会非常忙碌,不断地 recalculate -> repaint -> recalculate -> repaint,导致明显的性能瓶颈。

那么,虚拟DOM是如何提升效率的?可以分为三个步骤

  1. 生成新的DOM树

    • 当 React 组件的状态或 props 发生变化时,React 不会立刻去操作真实 DOM。
    • 而是使用新的 state/props 去重新执行组件的 render 方法,生成一个新的虚拟 DOM 树
  2. Diff 算法的“批量更新”:

    • React 将这个新的虚拟 DOM 树与上一次渲染生成的旧虚拟 DOM 树进行比较。
    • 这个比较过程(Diff 算法)非常高效,是 O(n) 的时间复杂度,因为它遵循上述的启发式规则。Diff 算法会找出两棵虚拟 DOM 树之间“真正”的差异:哪些节点被修改了 props,哪些节点被删除,哪些节点是新增的,哪些节点被移动了。
    • React 会生成一个包含所有这些差异的“变更列表”(patch list)。
  3. 一次性更新真实 DOM:

    • 最后,React 会将这个“变更列表”一次性地应用到真实 DOM 上。
    • 因为 React 已经知道需要进行哪些更改,它可以将这些更改进行批量处理。比如,如果多个元素的位置发生了变化,React 可以一次性地计算出所有受影响元素的最终布局,而不是每次都单独计算。如果多个元素的背景色发生了变化,React 也可以批量地进行重绘。
    • 这种“批量更新(Batch Update)”策略极大地减少了浏览器执行昂贵的 Reflow 和 Repaint 的次数。

现在让我们回想第一个问题——虚拟DOM为何高效?

1.批量更新的策略把最耗性能的操作(重绘重排)减少了几倍不止,这在很多情况下就意味着性能提升了几倍,这是巨大的提升。

2.diff算法找出了虚拟DOM和真实DOM的变化差异的最小“变更列表”,没有虚拟 DOM 时,也可能只更新变化部分,但效率和控制程度不如使用虚拟 DOM 和 Diff 算法

那么第二个问题——虚拟DOM是“自己骗自己吗”?

如果虚拟DOM树只有前两步可能是的(生成新的DOM树、Diff 算法的“批量更新),因为它们并不影响真实的dom渲染,但是如果加上了最后一步(一次性更新真实 DOM),意义就变了,这就直接摆脱了“自己骗自己”的嫌疑,还被送上了“大幅提升性能”的锦旗。

虚拟DOM难道在进行完性能的优化之后就燃尽了吗?不会的,其实虚拟DOM还有其他的一些好处,我们拿一个来讲——跨平台性

通过前面的讲解,我们知道虚拟dom在不更改真实DOM前是和浏览器的DOM分离的,这就意味着虚拟DOM不止可以“映射”到浏览器DOM,可以使用 Virtual DOM 框架(如 React, Vue)来构建在多种平台运行的 UI,而无需重写核心逻辑。

但是总的来讲,虚拟DOM最大的优势还是性能上的优化。这里简单解决一下大家可能有的一个疑问——虚拟DOM可以批量更新,那么是如何判断哪些“是一批”呢?

简单来说,React 认为“一批”更新是指:

在同一个 JavaScript 事件循环周期内,或者在 React 自身的调度过程中,所有触发的状态更新被累积起来,并在一个合适的时机(通常是 React 准备进行 DOM 更新之前)进行合并和处理,最终生成一次对真实 DOM 的批量操作。

简单的总结:

React v17 及之前(非并发模式):

  • 事件处理器内的 setState 属于一批(同步批量)。
handleClick = () => {
   // 这几次 setState 发生在同一个事件处理函数内,
   // 它们会被 React 收集并合并处理。
   this.setState({ count: this.state.count + 1 });
   this.setState({ count: this.state.count + 1 }); // 第二个会覆盖第一个,最终 count + 2
   this.setState({ text: 'Clicked!' }); // 新的状态
};
  • 其他地方(setTimeout, Promise, 生命周期等)的 setState 属于另一批(异步批量)。
componentDidMount() {
   // 这是一个异步操作
   setTimeout(() => {
     this.setState({ visible: true });
   }, 100);

   // 另一个在 componentDidMount 中调用的 setState,
   // 尽管时间上不一致,但它们同属于 componentDidMount 的执行上下文,
   // 并且 React 会将它们放入一个大的异步更新队列里。
   this.setState({ data: fetchedData });
 }
  • React 会等待当前的同步任务(如事件处理器)执行完毕,然后处理所有累积的异步更新。

React v18 及以后(默认并发模式):

  • 所有 setState 默认都被视为“可以被延迟”的更新。
  • React 的调度器会根据任务的紧急程度(通过 startTransition 等 API 控制)将更新分组。
  • “一批”是指,能够被 React 在一个渲染阶段(或者一系列连续的微任务/宏任务)中一次性处理和应用到 DOM 的更新集合。

核心是:React 不会对每一次 setState 都进行一次完整的 DOM diff 和更新。它会尽可能地将多个状态更新的意图收集起来,进行合并和计算,最终通过一次或几次高效的 DOM 操作来完成 UI 的更新。

setState大家可以简单的理解为组件状态更新

所以简单来说,很大可能下的批量更新是这样进行的

1.执行调用栈中的同步代码 (如果有)。
2.检查并执行宏任务队列,每次只取出一个任务执行。
3.检查并清空微任务队列 (在每个宏任务执行后立即清空)。
4.批量更新渲染 (对于浏览器环境,例如,进行 DOM 更新)。

二、偏虚拟diff算法部分

OK,那么现在回归正题,我们在前面的讲解中可以发现在虚拟DOM批量更新的过程中有一个叫做diff算法的东西,我们可以来回顾一下diff算法的作用

image.png

我们可以得知diff算法发挥作用的阶段是比较新旧虚拟DOM树的阶段,会生成一个“变更列表”,根据这个更新列表才可以一次性更新虚拟DOM树。

我们知道,虚拟DOM树是庞大的,树与树的比较更是性能低下,而且直接js操作dom并不涉及到虚拟DOM树的比较,所以这个比较是一项额外的开销,如何把这个开销给压低到最小是很有意义的,这时候就要搬出diff算法了,那么diff算法到底是如何做到的高效比较呢?

React 的 Diff 算法是基于  “最优化的、启发式的”  策略。它的核心目标是在 O(n) 的时间复杂度内找到两个树形结构之间的最小差异,其中 n 是节点数。为了达到这个目标,它做了一些合理的假设和简化:

  1. 跨组件类型的比较:  如果根节点是不同的组件类型,React 会认为旧的组件树已经完全没有价值,直接销毁旧组件树,然后创建全新的组件树。例如,<div> 变成 <span>,或者 MyComponentA 变成 MyComponentB

    为什么?  因为跨组件类型的差异通常很大,而且维护它们的关联性(比如生命周期、state 迁移)会非常复杂和低效。

  2. 同一组件类型的比较:  如果两个组件是同一类型,React 会保留现有的 DOM 节点,并继续比较它们的子节点。

    如果子节点是不同类型:  同样遵循规则 1,直接替换。

    如果子节点是同一类型:

    • 同一种 DOM 标签:  React 会比较它们的属性。如果属性不同,则只更新修改的属性。然后,递归地对子节点进行 Diff。

    • 列表(数组)的子节点:  这是 Diff 算法中最复杂和关键的部分。如果一个节点有一个子列表,React 需要一种方式来高效地标识列表中的每个元素。

      问题:  如果列表顺序发生变化,或者有元素被插入、删除,如何快速找到对应的元素?

      解决方案:key 属性!  React 要求为列表中的每个元素提供一个 唯一且稳定的 key 属性

      key 的作用:  key 就像是列表中每个元素的身份 ID。当 Diff 算法比较列表时,它会根据 key 来匹配新旧节点。

      查找相同 key 的节点:  如果新旧列表中都有同一个 key 的节点,React 认为它们是同一个元素,然后比较它们的属性和子节点。
      查找新增加的 key  如果新列表中有 key,但在旧列表中不存在,则认为是一个新节点,进行插入。
      查找被删除的 key  如果旧列表中有 key,但在新列表中不存在,则被认为是旧节点,进行删除。

      为什么 key 必须稳定?  如果 key 每次渲染都变化,Diff 算法就无法有效地识别出哪些元素是“同一个”,反而会认为所有元素都发生了变化,导致不必要的 DOM 重建,性能更差。

      不推荐使用 index 作为 key  如果列表顺序会改变(插入、删除、移动),使用 index 作为 key 会导致 Diff 算法误判,带来性能问题。只有在列表是静态的、不会改变顺序,并且没有被插入/删除元素时,index 才勉强可以接受(但仍不推荐)。

Diff 算法的步骤概览 (针对比较两个节点 oldNode 和 newNode):

  1. 如果 newNode 为 null 或 undefined

    • 返回“删除节点”的操作。
  2. 如果 oldNode 为 null 或 undefined

    • 返回“创建节点”的操作。
  3. 如果 oldNode 和 newNode 不是同一个 DOM 元素(标签、组件类型不同):

    • 返回“替换节点”(即删除 oldNode,创建 newNode)的操作。
  4. 如果 oldNode 和 newNode 是同一个 DOM 元素(标签相同):

    • 比较属性:  找出新旧属性的差异,生成“更新属性”的操作。

    • 比较子节点:

      • 如果子节点数量不同:

        • 根据 key,找出需要新增、删除、移动的子节点,生成相应的操作。
      • 如果子节点数量相同:

        • 递归地对每一对相同 key 的子节点调用 Diff 算法。

Diff 算法的实现细节思考:

  • 虚拟 DOM 的层级遍历:  Diff 算法是在虚拟 DOM 树上进行的,它会从根节点开始,自顶向下地进行比较。
  • JavaScript 对象模拟 DOM:  虚拟 DOM 本质上就是用 JavaScript 对象来描述 DOM 结构,这使得在内存中进行高效的比较成为可能。
  • Patch:  Diff 算法的最终产物不是直接修改 DOM,而是一个包含所有实际 DOM 操作的  “补丁”(Patch)  对象。这个 Patch 对象会被传递给 DOM 更新模块,由它来批量地、高效地将这些操作应用到真实 DOM 上。

Diff 算法的核心就是通过智能的比较策略(如基于组件类型、DOM 标签、属性以及最重要的 key 属性)来找出两棵虚拟 DOM 树的差异,并生成一系列精确的 DOM 操作指令,最终实现高效的 DOM 更新。其中,key 属性对于列表的 Diff 至关重要,能够极大地提升列表项增删改查的性能。

我们知道了上面的比较逻辑之后,甚至可以自己写一个简单的diff算法

// diff.js
function diff(oldTree, newTree) {
    // 声明变量patches用来存放补丁的对象
    let patches = {};
    // 第一次比较应该是树的第0个索引
    let index = 0;
    // 递归树 比较后的结果放到补丁里
    walk(oldTree, newTree, index, patches);
    return patches;
}

function walk(oldNode, newNode, index, patches) {
    // 每个元素都有一个补丁
    let current = [];

    if (!newNode) { // rule1
        current.push({ type: 'REMOVE', index });
    } else if (isString(oldNode) && isString(newNode)) {
        // 判断文本是否一致
        if (oldNode !== newNode) {
            current.push({ type: 'TEXT', text: newNode });
        }

    } else if (oldNode.type === newNode.type) {
        // 比较属性是否有更改
        let attr = diffAttr(oldNode.props, newNode.props);
        if (Object.keys(attr).length > 0) {
            current.push({ type: 'ATTR', attr });
        }
        // 如果有子节点,遍历子节点
        diffChildren(oldNode.children, newNode.children, patches);
    } else {    // 说明节点被替换了
        current.push({ type: 'REPLACE', newNode});
    }
    
    // 当前元素确实有补丁存在
    if (current.length) {
        // 将元素和补丁对应起来,放到大补丁包中
        patches[index] = current;
    }
}

function isString(obj) {
    return typeof obj === 'string';
}

function diffAttr(oldAttrs, newAttrs) {
    let patch = {};
    // 判断老的属性中和新的属性的关系
    for (let key in oldAttrs) {
        if (oldAttrs[key] !== newAttrs[key]) {
            patch[key] = newAttrs[key]; // 有可能还是undefined
        }
    }

    for (let key in newAttrs) {
        // 老节点没有新节点的属性
        if (!oldAttrs.hasOwnProperty(key)) {
            patch[key] = newAttrs[key];
        }
    }
    return patch;
}

// 所有都基于一个序号来实现
let num = 0;

function diffChildren(oldChildren, newChildren, patches) {
    // 比较老的第一个和新的第一个
    oldChildren.forEach((child, index) => {
        walk(child, newChildren[index], ++num, patches);
    });
}

// 默认导出
export default diff;

这样看来也不是很难理解,不是么?

我们来总结一下diff算法的作用

Diff 算法的核心在于比较虚拟 DOM 的差异,生成描述这些差异的补丁(Patch)。 它通过对新旧虚拟 DOM 的节点进行递归比较,找出最小的更新集合,避免了直接操作 DOM 带来的性能损耗。 这个过程的关键在于高效地比较节点类型、属性和子节点,最终目标是只更新变化的部分,从而最大限度地减少对真实 DOM 的操作,提高页面更新的效率。

diff算法的高性能可以体现在两个方面

  • 最小限度的减少了新旧虚拟DOM树的比较开支(有负面buff,但是尽可能把buff的影响变小)
  • 最大限度地减少对真实 DOM 的操作(有正面buff)

三、偏虚拟fiber架构部分

讲实话,前面的优化已经很完美、很天才了,但是,还有高手——fiber架构

正如前面所讲,虚拟 DOM 和 Diff 算法的组合拳已经极大地提升了 React 的性能。它们通过“计算差异”和“批量更新”的策略,有效减少了直接操作真实 DOM 带来的昂贵开销。然而,随着前端应用变得越来越复杂,组件树越来越深,旧的 React 协调机制逐渐暴露出一个核心瓶颈:这是一个不可中断的同步计算过程

为什么“不可中断”是个问题?

在 React 16 之前,当组件的状态或 props 发生变化时,React 会立即开始工作:

  1. 调用 Render:  重新渲染整个组件子树(生成新的虚拟 DOM)。
  2. 进行 Diff:  递归比较新旧两棵虚拟 DOM 树。
  3. 应用更新:  将计算出的差异(Patch)应用到真实 DOM。

这个过程是同步的,并且会一次性完成。如果组件树非常庞大,这个计算过程就会长时间占用 JavaScript 主线程。

而浏览器的主线程是单线程的,它除了要执行 JavaScript,还负责样式计算、布局、绘制等任务。长时间被 JS 计算霸占主线程会导致:

  • 浏览器无法及时响应用户的输入(点击、滚动等),页面会感觉“卡顿”。
  • 浏览器无法按时完成帧渲染,导致动画掉帧、页面渲染不流畅。

“栈协调”的困境
你可以把旧的协调过程想象成 React 在“递归”地遍历整个组件树。JavaScript 的递归调用会形成一个很深的“调用栈”。React 必须完整地走完这个调用栈,才能进行下一步。它无法暂停,无法中途去处理更高优先级的任务(比如用户的点击)。这种基于递归深度优先遍历的架构被称为  “栈协调”

Fiber 架构的诞生

讲实话,前面的优化(虚拟DOM + Diff算法)已经很完美、很天才了。它们通过“计算差异”和“批量更新”的策略,极大地减少了直接操作DOM带来的性能开销。但是,随着前端应用变得越来越复杂,组件树越来越深,旧的React协调机制(Stack Reconciler)逐渐暴露出一个核心瓶颈:整个虚拟DOM的Diff过程是一个【不可中断】的同步计算过程

为什么“不可中断”是个致命问题?

在React 16之前,当状态变化触发更新时,React会这样做:

  1. 递归渲染:重新渲染整个受影响的组件子树,生成新的虚拟DOM树。
  2. 递归Diff:递归地比较新旧两棵虚拟DOM树。
  3. 提交更新:将计算出的差异(Patch)应用到真实DOM。

这个过程是同步且一气呵成的。如果组件树非常庞大,这个复杂的计算过程就会长时间霸占JavaScript主线程。

而浏览器的主线程是单线程的,它除了要执行JavaScript,还负责样式计算、布局、绘制以及响应用户交互(如点击、滚动)。长时间被JS计算阻塞会导致:

  • 页面卡顿:浏览器无法及时响应用户操作,点击按钮没反应,滚动起来一卡一卡。
  • 掉帧:动画无法在16.6ms内完成一帧的渲染,导致视觉上的不流畅。

“栈协调”的困境
旧的协调器基于递归。递归调用会在JavaScript引擎中形成一个很深的“调用栈”。React必须完整地走完这个调用栈(即处理完整个虚拟DOM树),才能进行下一步。它就像一列高速行驶的火车,无法在中间站点暂停让道,必须到终点站才能停下。这种机制被称为  “栈协调”(Stack Reconciler)

Fiber架构的诞生:为了解决“不可中断”

为了从根本上解决“同步更新不可中断”的问题,React团队从底层重写了协调算法,引入了Fiber架构Fiber Reconciler

Fiber是什么?
Fiber可以从两个层面理解:

  1. 一种数据结构:Fiber是React 16+中虚拟DOM的新表示形式。它是一个功能更强大的JavaScript对象,包含了比传统虚拟DOM节点更丰富的调度信息。
  2. 一个执行单元:Fiber代表了可以拆分、可以调度、可以中断的一个工作任务。React的渲染和更新过程不再是一次性递归完成,而是分解成一个个小的Fiber节点任务来处理。

这时候就有同学要问了:‘为什么不在原来的虚拟DOM树上直接加个中断机制,非要整一个新的Fiber出来?’

问得好!答案是:传统的虚拟DOM树数据结构不支持

  • 传统的虚拟DOM树节点之间只有父子关系,通过children数组连接。这是一种递归树形结构。中断后,你想恢复遍历,必须从头开始或者用非常复杂的方式记录进度,成本极高。
  • Fiber节点通过链表连接。每个Fiber节点不仅有指向第一个子节点(child)的指针,还有指向下一个兄弟节点(sibling)和父节点(return)的指针。这种链表树结构使得React可以用循环来模拟递归遍历。中断时,只需保存当前正在处理的Fiber节点引用,恢复时就能立刻从它开始继续处理它的childsibling,极其高效。

Fiber节点的核心属性(了解即可):

  • type & key: 同虚拟DOM,标识组件类型和列表项的Key。
  • child: 指向第一个子Fiber。
  • sibling: 指向下一个兄弟Fiber。
  • return: 指向父级Fiber。
  • alternate一个极其重要的指针。它指向另一棵树上对应的Fiber节点,是实现双缓存和Diff比较的基础。
  • stateNode: 对应的真实DOM节点或组件实例。
  • flags (旧版叫effectTag): 标记这个Fiber节点需要进行的操作(如Placement-插入, Update-更新, Deletion-删除)。

Fiber如何工作?可中断的“双缓存”策略

Fiber架构将协调过程分为两个截然不同的阶段:

  1. Render / Reconciliation Phase (渲染/协调阶段)

    • 可中断、可恢复、异步。  这个阶段负责计算“哪些需要更新”,但绝不操作真实DOM
    • React会在内存中构建一棵新的Fiber树,称为 WorkInProgress Tree(工作在进行树) 。它通过与当前屏幕上显示的 Current Tree(当前树)  上的Fiber节点进行Diff比较来完成构建。
    • 工作方式:React的调度器会循环处理每个Fiber单元。处理完一个单元,它就检查主线程是否还有空闲时间(通过requestIdleCallbackscheduler)。如果没有时间了,或者有更高优先级的任务(如用户输入),React就立刻中断当前工作,保存进度(下一个要处理的Fiber),把主线程交还给浏览器。等浏览器忙完了,React再回来从断点继续。
    • 这个阶段可能会被打断多次。
  2. Commit Phase (提交阶段)

    • 不可中断、同步执行。  这个阶段是React将协调阶段计算出的所有副作用(即需要更新的操作列表)一次性、同步地应用到真实DOM上的阶段。
    • 因为这个阶段会实际操作DOM,而DOM的变更会立刻触发浏览器的重绘重排,所以必须快速完成,用户不会看到“更新到一半”的UI。
    • 一旦开始提交,React就会一口气完成所有DOM操作。

刚才其实已经提到了“双缓存”的概念,这里展开讲讲 “双缓存”技术

Current Tree和WorkInProgress Tree通过alternate指针相互指向。当WorkInProgress Tree构建完成并在提交阶段渲染到屏幕后,这两棵树会“互换角色”:刚刚建好的WorkInProgress Tree就变成新的Current Tree,而旧的Current Tree就作为下一次更新的WorkInProgress Tree的基础。这保证了渲染的连贯性和性能。

  • 当 React 开始一个渲染周期(例如,响应一个事件,数据改变)时,它会创建一个新的 Fiber 树,称为  “工作” Fiber 树
  • 这个“工作” Fiber 树是独立于用户当前看到的 DOM 树的。
  • Fiber 节点在“工作” Fiber 树中进行计算,标记出需要更新、插入或删除的 DOM 节点。
  • 当整个“工作” Fiber 树的计算完成后,React 会执行一个  “提交”(commit)  阶段。
  • 在“提交”阶段,React 会遍历“工作” Fiber 树,并将所有需要进行的 DOM 操作一次性地应用到真实的 DOM 上。
  • 此时,老的一棵 Fiber 树(代表了用户当前看到的 UI)可以被标记为“已完成”或被丢弃,而新的 Fiber 树则成为了“当前”的 Fiber 状态,准备下一次更新。

OK,关于虚拟DOM、diff算法、fiber架构大概就是这么多内容,下面我们来进行一些有趣的探讨,来解决大家心中可能有些迷惑的地方。

react中diff算法发挥作用的阶段是比较新旧虚拟DOM树的阶段,还是生成新的虚拟DOM的阶段?

Diff 算法发挥作用的阶段是“比较新旧虚拟DOM树的阶段”,而不是“生成新的虚拟DOM的阶段”。

让我们来清晰地分解这两个阶段:

阶段一:生成新的虚拟DOM (Render Phase)

  • 发生了什么?  当组件的状态(state)或属性(props)发生变化时,React 会重新执行组件的 render 方法。
  • 结果是什么?  这个执行过程会返回一个新的 React 元素树(即新的虚拟DOM树)。这个过程是声明式的,React 只是根据当前最新的 state 和 props 计算出 UI 应该是什么样子。
  • Diff算法参与了吗?  没有。  这个阶段只是简单地根据数据生成一个新的UI描述(一棵新的树),不涉及任何比较操作。

阶段二:比较新旧虚拟DOM树 (Reconciliation Phase)

  • 发生了什么?  在生成了新的虚拟DOM树之后,React 需要弄清楚新的树和当前屏幕上显示的旧虚拟DOM树(current tree)之间的差异。
  • 如何工作?  React 会启动 Diffing 算法,递归地比较新旧的 React 元素(虚拟DOM节点)的 typekey 和 props
  • 结果是什么?  Diff 算法会得出一份精确的“变更清单”(或称为副作用 effect list),详细记录了为了将旧树更新为新树,需要对真实DOM进行的具体操作,例如:“在父节点下插入一个id为X的新节点”、“更新id为Y的节点的className属性”、“删除id为Z的节点”等。
  • Diff算法参与了吗?  是的!  这是 Diff 算法核心工作的阶段。

新虚拟DOM树的构建是否依赖于旧的虚拟DOM树?

不会。

新的虚拟DOM树的生成是一个完全独立纯粹的过程,它不依赖于旧的虚拟DOM树。这是一个非常关键的设计理念。

让我们来详细解释:

1. 生成新虚拟DOM树的逻辑

当组件的状态(state)或属性(props)发生变化时,React 会做的就是重新执行组件的渲染函数(对于类组件是 render() 方法,对于函数组件是函数体本身)。

这个过程可以简化为:
新的虚拟DOM树 = render(currentState, currentProps)

它只依赖于两个输入:

  1. 当前最新的状态(currentState)
  2. 当前接收到的属性(currentProps)

旧的虚拟DOM树(或旧的 Fiber 树)不是这个函数的输入参数。渲染函数就像一个“纯函数”,给定相同的 state 和 props,它总是会返回相同的 UI 描述(虚拟DOM树)。它完全不知道、也不关心上一次渲染出来的结果是什么。

2. 为什么这个设计如此重要?

这种“不依赖旧树”的设计是 React 声明式编程模型的核心优势:

  1. 可预测性 :UI 只由当前的 state 和 props 决定,这使得应用的行为非常容易理解和预测。你不需要考虑“当前屏幕上显示的是什么”,只需要思考“在这个数据状态下,UI 应该是什么样子”。
  2. 简化逻辑:开发者编写渲染逻辑时,只需要关注如何根据当前数据构建UI,而不需要处理如何从旧UI更新到新UI的复杂指令(这是 React 的职责)。
  3. 性能优化的基础:正因为生成新树是一个相对独立的过程,React 才能在背后灵活地调度这项工作。例如,在并发模式下,React 可以先在后台为一次即将到来的更新生成新的虚拟DOM树,而不立即提交,如果又有更高优先级的更新插队,它甚至可以丢弃这棵还没用完的树,重新开始生成,而不会破坏一致性。

在Fiber架构下diff算法到底是比较的什么?

在 Fiber 架构下,Diff 算法比较的并不是两棵完整的、传统的“虚拟DOM树”,而是两棵“Fiber 树”上的对应节点

理解这一点是理解 Fiber 架构如何工作的核心。让我们来彻底拆解它。

核心概念:两棵Fiber树

在 Fiber 架构中,React 在内存中同时维护着两棵 Fiber 树:

  1. Current Tree(当前树)

    • 这棵树代表当前已渲染到屏幕上的UI状态
    • 树中的每个 Fiber 节点都直接对应着一个真实的 DOM 节点(对于宿主组件如 divspan)或一个组件实例(对于类组件)。
    • 你可以把它想象成“旧的虚拟DOM树”的Fiber版本。
  2. WorkInProgress Tree(工作中树)

    • 当状态更新发生时,React 会在后台开始构建这棵新树。它代表了下一次渲染希望更新到的UI状态
    • 这棵树的构建过程是可中断的。
    • 你可以把它想象成“新的虚拟DOM树”的Fiber版本。

Diff 算法的本质,就是比较同一节点在 Current Tree 和 WorkInProgress Tree 上的两个 Fiber 节点。

Diff 的具体过程:ReconcileChildren

Diff 过程发生在 React 为 WorkInProgress 树创建子节点(Fiber节点)的时候。这个函数通常被称为 reconcileChildren 或 reconcileChildFibers

它的工作流程如下:

  1. 输入:一个父级 Fiber 节点(在 WorkInProgress 树中)和它通过 render 函数返回的 新的React元素(React Elements)
  2. 目标:为这些新的React元素创建或复用Fiber节点,从而构建出父级Fiber的 child 链表。
  3. 比较策略:React 会将新的React元素Current Tree中该父F节点下对应的旧子Fiber节点进行比较。

这个过程是逐层逐节点进行的,而不是一次性比较整棵树。

Diff 算法在Fiber中比较的具体内容

当处理一个新的React元素和一个旧的Fiber节点时,Diff 算法会按顺序检查以下属性:

  1. Fiber.key vs. Element.key

    • 这是第一优先级!  这是列表diff性能的关键。
    • 算法会首先检查key是否相同。如果key不同,React会认为这是一个不同的元素,即使type相同。
  2. Fiber.type vs. Element.type

    • 这是第二优先级!  检查节点类型(如 'div''MyComponent')是否相同。
    • 如果key相同但type不同,React会认为需要替换整个节点及其子树(因为一个 div 不可能直接变成一个 span)。
  3. Fiber.pendingProps vs. Element.props

    • 如果key和type都相同,React则认为这是一个可以复用的Fiber节点。
    • 接下来会比较新旧属性(props)的差异,并将需要更新的属性标记出来。

Fiber节点的“池化”

在Fiber架构中,“比较”的目的不仅仅是找出差异,更重要的是尽可能地复用现有的Fiber节点。这是一种性能优化策略,类似于“对象池”。

  • 如果可以复用(key和type都相同) :React不会销毁旧的Fiber节点并创建一个全新的对象,而是会 “克隆” 旧的Fiber节点(来自Current树),用它来构建WorkInProgress树。它只是用新的props和新的子元素引用更新这个克隆体的属性。

    • 好处:避免了频繁创建和销毁JavaScript对象的开销,极大提升了性能。
  • 如果不能复用(key或type不同) :React会为新的React元素创建一个全新的Fiber节点,并标记旧的Fiber节点及其整个子树需要被删除

读到这里,也可以猜到我的下一个问题

Fiber树的构建是否依赖虚拟DOM树

是的,Fiber树的构建完全依赖虚拟DOM树,并且这是一个持续依赖的关系。

更准确地说,虚拟DOM树(React Element Tree)是构建Fiber树的“蓝图”或“指令” 。没有虚拟DOM树,Fiber树就无法被创建或更新。

让我们来详细分解这个依赖关系:

1. 初始渲染:从虚拟DOM到Fiber树

当你的应用首次加载时,React 的工作流程是这样的:

  1. 生成虚拟DOM树:React 调用你的根组件的 render() 方法(或执行函数组件体)。这个执行过程会返回一个由 React 元素  组成的树,这就是最初的虚拟DOM树。它是对UI的声明式描述

  2. 构建Fiber树:React 接收到这棵虚拟DOM树后,以它为依据,开始创建对应的 Fiber 节点,并将这些节点连接成一棵 Fiber 树(也就是最初的 Current 树)。

    • 每个 Fiber 节点都是从对应的 React 元素中获取其 typekeyprops 等信息。
    • 虚拟DOM树描述了  “UI应该是什么样子” ,而Fiber树是 为了实现协调和更新而构建的“工作单元数据结构”

初始渲染的依赖关系:
组件render() -> 虚拟DOM树 -> Fiber树 -> 真实DOM

2. 状态更新:虚拟DOM是驱动Fiber树重建的源头

当状态发生变化,触发更新时,这个依赖关系更加明显:

  1. 生成新的虚拟DOM树:状态更新导致组件重新渲染,再次调用 render 方法。这会生成一棵新的虚拟DOM树。这个过程是纯粹的,不依赖旧的Fiber树,只依赖于当前的 state 和 props。

  2. 协调 :React 现在手上有两样东西:

    • 旧的Fiber树(Current 树):代表当前屏幕上显示的内容。
    • 新的虚拟DOM树:代表下一次渲染希望更新到的UI状态。
  3. 构建新的Fiber树:React 开始构建 WorkInProgress 树。它遍历新的虚拟DOM树,并逐节点地与旧的Fiber树进行比较(Diff算法)

    • 对于新的虚拟DOM树上的每一个 React 元素,React 都会去旧的Fiber树中寻找可以复用的Fiber节点。
    • 复用的决策(key和type是否相同)完全基于新的React元素和旧的Fiber节点的属性。
    • 最终,React 会根据比较结果,创建新的Fiber节点或复用旧的Fiber节点,从而构建出完整的 WorkInProgress 树。

更新时的依赖关系:
状态改变 -> 组件重新render() -> 新的虚拟DOM树 -> (Diff算法) -> 构建新的Fiber树(WorkInProgress) -> 更新真实DOM

现在(react18以后)UI的改变是比较两个虚拟DOM从而改变UI了,还是新旧fiber树的比较改变UI?

在 React 16 之后,UI 的改变不再仅仅依赖于比较两个虚拟 DOM (VDOM) 树来决定 DOM 的更新,更重要的是,React 的更新过程现在主要基于 Fiber 架构,并通过比较新旧 Fiber 树来实现高效的 UI 更新。

让我们更详细地解释一下:

1. 传统 (React 16 及以前) 的 VDOM 更新机制 (简化版)

  • 组件状态改变 -> 创建新的 VDOM
  • 新 VDOM 与旧 VDOM 比较 (diffing)
  • Diffing 算法识别变化,生成 DOM 更新指令
  • DOM 更新指令被应用到真实的 DOM

关键问题:

  • 同步过程:  整个 diffing 过程是同步的,会阻塞主线程,如果组件复杂,会影响用户体验(卡顿)。
  • 优先级问题:  所有更新都被同等对待,无法区分重要性和紧急程度。

2. Fiber 架构下的更新机制 (React 17及以后)

  • 组件状态改变 -> (触发更新)

  • React 根据新状态创建新的 Fiber 树 (工作 Fiber 树)

  • 构建 (render) 阶段:

    • React 遍历新的 Fiber 树。
    • 比较 新 Fiber 树与旧 Fiber 树 (旧 Fiber 树的父节点指向新 Fiber, new Fiber 存在 alternate 属性指向 old fiber , 即通过 Fiber 树的信息,来比较新旧UI)。
    • 计算需要进行的 DOM 操作 (插入、删除、更新)
    • React 可以中断这个过程,处理高优先级任务 (例如用户交互),之后 可以恢复
  • 提交 (commit) 阶段:

    • 一次性将所有 DOM 操作 (变化) 应用到真实的 DOM
    • flushPassiveEffects触发一些 useEffect
    • 此时,旧的 Fiber 树 (代表之前的 UI) 被标记为不可用, 新的 Fiber 树 (代表新的 UI) 成为“当前”的 Fiber 状态。

关键变化:

  • Fiber 树取代了 VDOM 树成为更重要的结构:  React 用 Fiber 节点来表示 UI 的工作单元。 Fiber 树 (新旧) 之间的比较,驱动了 DOM 的更新。
  • 构建阶段可中断:  React 可以在** render 阶段** 中中断耗时的计算任务,让出主线程。 这使得用户体验更好。
  • 并发 (Concurreny):  Fiber 开启了并发渲染的可能性。 React 可以在后台准备多个 UI 更新,然后以一种流畅的方式进行切换。
  • 优先级和调度:  Fiber 架构允许优先处理某些更新 (Transition, Sync 等)。

所以,现在, UI 的变化的核心是:

  • React 基于状态变化构建新的 Fiber 树 (工作 Fiber 树)。
  • React 通过比较新的 Fiber 树和旧的 Fiber 树 (主要是比较 Fiber 节点里的信息) 来确定需要进行的 DOM 操作。
  • DOM 操作在 commit 阶段一次性应用到 DOM。 这个阶段是同步的,但因为计算发生在后台,所以不会阻塞 UI 渲染太多时间。

一个更精确的解释是:

在 React 18及更高版本中,比较(diffing)仍然存在,但它在 Fiber 架构中得到了重新组织和增强。主要比较的核心是:

  • Fiber 节点:  比较的是新旧 Fiber 树中的 Fiber 节点。
  • 副作用管理:  比较 Fiber 的 effectTag 属性,effectTag 标记了需要进行哪些 DOM 操作 (例如:插入、更新、删除)。
  • 工作单元: ** Fiber 将更新过程分解成工作单元,并使用优先级**、中断和恢复等技术来优化更新。

虽然 Virtual DOM 仍然是 React 中的一个重要概念,它描述了 UI 的状态,但 Fiber 架构是核心,它控制了更新的调度、性能和并发。 新旧 Fiber 树之间的比较 (更确切地说, Fiber 节点之间的比较) 是触发 UI 变化的根本原因。

我们可以对UI的改变过程做一个React层面的分层

React的架构可以看作一个清晰的 pipeline(流水线),每一层职责分明:

第1层:开发者 (Your Code)

  • 职责:使用声明式的JSX或React.createElement编写组件。
  • 输出虚拟DOM(React元素) 。这只是UI的描述(“蓝图”),例如“这里应该有一个<div>,它的className是'active',里面有一个<span>”。

第2层:React核心 (React Core / Reconciler)

  • 职责协调(Reconciliation) 。这是Fiber架构的核心所在。
  • 输入:旧的Fiber树 + 新的虚拟DOM树(来自第1层)。
  • 工作:通过Diff算法比较新旧内容,计算出需要进行的更新操作(如PlacementUpdateDeletion)。
  • 输出:一个包含了所有更新操作的副作用链表(Effect List)注意:到这里为止,操作的仍然是JavaScript对象(Fiber节点),没有触及真实DOM。

第3层:渲染器 (Renderer - e.g., ReactDOM)

  • 职责渲染。这是真正操作平台特定API(如浏览器DOM)的层。

  • 输入:React核心计算出的副作用链表

  • 工作:遍历副作用链表,执行具体的、平台相关的命令。

    • 对于ReactDOM来说,就是调用document.createElement()element.appendChild()element.setAttribute()element.remove()浏览器DOM API
  • 输出:更新后的真实UI。

总结

OK,就谈到这里吧

useMemo 实现原理

作者 维维酱
2025年9月3日 20:21

useMemo 实现原理深度解析

useMemo 是 React Hooks 中用于性能优化的重要工具,它的实现涉及到 React 的 Fiber 架构、Hooks 机制和渲染流程。

核心实现机制

useMemo 的核心思想是记忆化(Memoization):在依赖项未变化时返回缓存的值,避免重复计算。

1. 在 React 源码中的基本结构

在 React 源码中,useMemo 的实现大致如下:

function useMemo(create, deps) {
  // 1. 获取当前正在渲染的 Fiber 节点和 Hook 链表
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

实际的实现位于 ReactFiberHooks.js 中:

function updateMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 2. 获取当前 Hook 对象
  const hook = updateWorkInProgressHook();
  
  // 3. 获取上一次的依赖项和记忆值
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      // 4. 比较依赖项是否变化
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 5. 依赖未变化,返回缓存的值
        return prevState[0];
      }
    }
  }
  
  // 6. 依赖变化或首次渲染,重新计算值
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Hook 在 Fiber 架构中的存储

要理解 useMemo,需要先了解 Hooks 在 React Fiber 架构中的存储方式:

1. Hook 链表结构

// Fiber 节点中的 Hook 存储
type Fiber = {
  memoizedState: any, // 指向当前 Hook 链表的头部
  // ... 其他属性
};

// Hook 对象结构
type Hook = {
  memoizedState: any,     // 存储记忆的值
  baseState: any,         // 基础状态
  baseQueue: any,         // 基础队列
  queue: any,             // 更新队列
  next: Hook | null,      // 指向下一个 Hook
};

对于 useMemomemoizedState 字段存储的是一个数组 [value, deps]

  • value: 记忆的计算结果
  • deps: 上一次的依赖项数组

2. Hooks 的调用顺序规则

React 依赖 Hook 的调用顺序来正确关联状态:

function MyComponent() {
  const [state] = useState(0);      // Hook 1
  const memoizedValue = useMemo(() => { // Hook 2
    return expensiveCalculation(state);
  }, [state]);
  const [anotherState] = useState(''); // Hook 3
  
  // React 通过调用顺序维护 Hook 链表:
  // Hook1 -> Hook2 -> Hook3
}

依赖比较的实现

useMemo 的核心在于依赖比较,React 使用 areHookInputsEqual 函数:

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  // 处理边界情况
  if (prevDeps === null) {
    return false;
  }
  
  // 比较数组长度
  if (nextDeps.length !== prevDeps.length) {
    console.error(
      'useMemo received a different number of dependencies. ' +
      'Previous: %s, current: %s',
      prevDeps.length,
      nextDeps.length,
    );
    return false;
  }
  
  // 逐个比较依赖项
  for (let i = 0; i < prevDeps.length; i++) {
    // 使用 Object.is 进行严格比较
    if (!objectIs(nextDeps[i], prevDeps[i])) {
      return false;
    }
  }
  
  return true;
}

// Object.is 的 polyfill(处理 NaN 和 ±0 的特殊情况)
function objectIs(a, b) {
  return (
    (a === b && (a !== 0 || 1 / a === 1 / b)) || 
    (a !== a && b !== b) // NaN === NaN
  );
}

完整的渲染流程中的 useMemo

1. 首次渲染(Mount)

function mountMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 1. 创建新的 Hook 对象并添加到链表
  const hook = mountWorkInProgressHook();
  
  // 2. 获取依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 执行计算函数
  const nextValue = create();
  
  // 4. 存储值和依赖项
  hook.memoizedState = [nextValue, nextDeps];
  
  return nextValue;
}

2. 更新渲染(Update)

function updateMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 1. 获取对应的 Hook 对象
  const hook = updateWorkInProgressHook();
  
  // 2. 获取新的依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 获取上一次存储的状态
  const prevState = hook.memoizedState;
  
  // 4. 比较依赖项
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 依赖未变化,返回缓存的值
        return prevState[0];
      }
    }
  }
  
  // 5. 依赖变化,重新计算
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

与 React 渲染周期的整合

useMemo 的执行与 React 的渲染周期密切相关:

  1. Render 阶段useMemo 在组件函数执行期间被调用
  2. 计算时机:计算函数在渲染期间执行(不是副作用阶段)
  3. 缓存策略:缓存的值仅在渲染期间有效
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps,
  renderLanes,
) {
  // 准备渲染环境
  prepareToUseHooks(current, workInProgress, renderLanes);
  
  try {
    // 执行组件函数,useMemo 在此期间被调用
    let nextChildren = Component(nextProps, ref);
    
    // 处理子节点...
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    
    return workInProgress.child;
  } finally {
    // 清理 Hook 环境
    resetHooksAfterRender();
  }
}

性能考虑与优化细节

1. 记忆化策略的权衡

useMemo 的实现需要在多个方面进行权衡:

// 伪代码:useMemo 的成本效益分析
function shouldUseMemo(create, deps) {
  const calculationCost = estimateCalculationCost(create);
  const comparisonCost = deps.length * SINGLE_COMPARISON_COST;
  const memoryCost = MEMORY_PER_MEMOIZED_VALUE;
  
  // 只有当计算成本 > (比较成本 + 内存成本) 时,使用 useMemo 才有意义
  return calculationCost > (comparisonCost + memoryCost);
}

2. 依赖项数组的特殊处理

// 处理各种依赖项情况
function normalizeDeps(deps) {
  if (deps === undefined || deps === null) {
    // 没有提供依赖项,每次渲染都重新计算
    return null;
  }
  
  if (!Array.isArray(deps)) {
    // 开发环境下警告
    console.error('useMemo expects an array of dependencies.');
    return null;
  }
  
  return deps;
}

3. 开发环境下的额外检查

React 在开发环境下提供了额外的警告和检查:

function useMemo(create, deps) {
  if (__DEV__) {
    // 验证 Hook 调用规则
    validateHookCall();
    
    // 检查依赖项是否是数组
    if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
      console.error(
        'useMemo requires an array of dependencies. Got: %s',
        typeof deps,
      );
    }
    
    // 记录 Hook 的使用以便调试
    recordHook();
  }
  
  // ... 实际实现
}

与 useCallback 的关系

useCallback 实际上是 useMemo 的特例:

function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

// 在 React 源码中的实际实现:
function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

实际应用中的注意事项

1. 正确使用依赖项

function MyComponent({ items, filter }) {
  // 正确:包含所有依赖项
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]); // ✓ 所有依赖项都包含

  // 错误:遗漏依赖项
  const badFilteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items]); // ✗ 遗漏了 filter 依赖
}

2. 避免过度优化

function MyComponent({ value }) {
  // 不必要的 useMemo:计算很简单
  const simpleValue = useMemo(() => value + 1, [value]); // ✗ 过度优化
  
  // 必要的 useMemo:计算很昂贵
  const expensiveValue = useMemo(() => {
    return expensiveCalculation(value);
  }, [value]); // ✓ 合理的优化
}

总结

useMemo 的实现原理可以概括为:

  1. 基于 Hook 机制:利用 React 的 Hook 链表结构存储记忆的值和依赖项
  2. 依赖比较:使用严格比较(Object.is)检查依赖项是否变化
  3. 记忆化策略:依赖未变化时返回缓存值,变化时重新计算
  4. 渲染期间执行:计算函数在渲染阶段执行,不是副作用
  5. 性能权衡:在计算成本、比较成本和内存使用之间取得平衡

在react中处理输入法合成问题

作者 FanetheDivine
2025年9月3日 11:11

辅助hook

useImmediateEffect

与useEffect类似 在依赖数组变化后 立刻同步地执行副作用

import { useRef } from 'react'

/** 副作用函数 接受旧的依赖项 */
export type ImmediateEffect<Deps extends any[]> = (oldDeps?: Deps) => ImmediateEffectCleanup
/** 清理函数 */
export type ImmediateEffectCleanup = (() => any) | undefined | void

export function useImmediateEffect<Deps extends any[]>(effect: ImmediateEffect<Deps>, deps: Deps) {
  const prevDepsRef = useRef<Deps>()
  const prevCleanupRef = useRef<ImmediateEffectCleanup>()
  if (isDepsChanged(prevDepsRef.current, deps)) {
    prevCleanupRef.current?.()
    prevCleanupRef.current = effect(prevDepsRef.current)
  }
  prevDepsRef.current = deps
}

function isDepsChanged<Deps extends any[]>(prevDeps: Deps | undefined, deps: Deps) {
  if (!prevDeps || prevDeps.length !== deps.length) return true
  return prevDeps.some((prevValue, i) => !Object.is(prevValue, deps[i]))
}

useSemiControlledValue

取得一个值的半受控版本.外界传入的value变化时,此值也会立刻突变;在传入value不变时,此值可以自行变更.

import { useRef, useState } from 'react'
import { useMemoizedFn } from 'ahooks'
import { useImmediateEffect } from '../useImmediateEffect'

export function useSemiControlledValue<V = any>(valueController: {
  value?: V
  onChange?: (val: V) => void
}) {
  const { value, onChange } = valueController
  const [changedValue, setChangedValue] = useState(value)
  const currentValueRef = useRef(value)
  useImmediateEffect(() => {
    currentValueRef.current = changedValue
  }, [changedValue])
  useImmediateEffect(() => {
    currentValueRef.current = value
  }, [value])
  const currentValue = currentValueRef.current

  // 对外暴露的更新函数 参数是一个新值或者更新函数
  const onInnerChange: (arg: V | ((val?: V) => V)) => void = useMemoizedFn((arg: any) => {
    const newValue = typeof arg === 'function' ? arg(currentValue) : arg
    setChangedValue(newValue)
    onChange?.(newValue)
  })

  return [currentValue, onInnerChange] as const
}

useComposition

处理输入法合成事件.在本地维护一个输入法无关的value,在合成事件结束后,向外更新合成后的值

import { CompositionEventHandler, ChangeEventHandler, useState } from 'react'
import { useMemoizedFn } from 'ahooks'
import { useSemiControlledValue } from '../useSemiControlledValue'

export function useComposition(valueController: {
  value?: string
  onChange: (val: string) => void
}) {
  const [isComposing, setComposing] = useState(false)
  const [value, onInnerChange] = useSemiControlledValue({
    value: valueController.value,
  })
  // onChange时更新本地value
  const onChange = useMemoizedFn<ChangeEventHandler<HTMLInputElement>>((e) => {
    onInnerChange(e.target.value)
  })
  const onCompositionStart = useMemoizedFn(() => {
    setComposing(true)
  })
  // 输入法合成后更新外部value
  const onCompositionEnd = useMemoizedFn<CompositionEventHandler<HTMLInputElement>>((e) => {
    setComposing(false)
    valueController.onChange?.((e.target as HTMLInputElement).value)
  })
  const compositionProps: CompositionProps = {
    value,
    onChange,
    onCompositionStart,
    onCompositionEnd,
  }
  return {
    isComposing,
    compositionProps,
  }
}

使用例

const [search,setSearch] = useState<string>()
const { compositionProps } = useComposition({ value: search, onChange: setSearch })

<input {...compositionProps} />

React语法全景指南:面试官问我用了哪些语法时,我这样回答拿到了offer

2025年9月2日 21:40

引言:为什么这个问题如此重要?

当面试官问"你的React项目中用到了哪些语法?"时,他真正想问的是:你对React的掌握程度如何?能否根据业务场景选择最合适的解决方案? 这不仅是语法盘点,更是对你React技术深度的全面考察。

一、基础必答:现代React开发的基石

1. 函数组件与JSX语法

// 函数组件是现代React的首选
function Welcome({ name }) {
  // JSX允许在JavaScript中写HTML-like语法
  return <h1>Hello, {name}!</h1>;
}

// 在项目中的应用:所有UI展示组件
export default function UserCard({ user }) {
  return (
    <div className="user-card">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

2. Hooks:React的逻辑复用革命

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  // useState:管理组件状态
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  // useEffect:处理副作用操作
  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('Failed to fetch user:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [userId]); // 依赖数组:当userId变化时重新执行

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

二、进阶展示:复杂场景的解决方案

3. 自定义Hook:逻辑抽象与复用

// 自定义Hook:提取重复逻辑
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [url]);

  return { data, loading, error };
}

// 在项目中的使用
function ProductList() {
  const { data: products, loading, error } = useApi('/api/products');
  
  // 渲染产品列表
}

4. useReducer:复杂状态管理

// 适用于复杂状态逻辑
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload]
      };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };
    default:
      return state;
  }
}

function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, { items: [] });
  
  const addItem = (product) => {
    dispatch({ type: 'ADD_ITEM', payload: product });
  };
  
  // 渲染购物车
}

5. Context API:跨组件状态共享

// 创建主题上下文
const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 在深层组件中使用
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <button 
      onClick={toggleTheme}
      className={`btn btn-${theme}`}
    >
      Toggle Theme
    </button>
  );
}

三、性能优化:打造高效React应用

6. 记忆化技术:useMemo & useCallback

function ExpensiveComponent({ items, filter }) {
  // useMemo:缓存计算结果
  const filteredItems = useMemo(() => {
    console.log('Filtering items...');
    return items.filter(item => item.category === filter);
  }, [items, filter]); // 依赖变化时重新计算

  // useCallback:缓存函数引用
  const handleItemClick = useCallback((itemId) => {
    console.log('Item clicked:', itemId);
  }, []); // 空依赖数组表示函数不会改变

  return (
    <div>
      {filteredItems.map(item => (
        <div key={item.id} onClick={() => handleItemClick(item.id)}>
          {item.name}
        </div>
      ))}
    </div>
  );
}

7. React.memo:避免不必要的重渲染

// 仅当props变化时重渲染
const UserListItem = memo(function UserListItem({ user, onEdit }) {
  console.log('Rendering user:', user.id);
  
  return (
    <li>
      <span>{user.name}</span>
      <button onClick={() => onEdit(user.id)}>Edit</button>
    </li>
  );
});

// 自定义比较函数
const arePropsEqual = (prevProps, nextProps) => {
  return prevProps.user.id === nextProps.user.id &&
         prevProps.user.name === nextProps.user.name;
};

const OptimizedUserListItem = memo(UserListItem, arePropsEqual);

四、高级特性:展示技术深度

8. Refs与DOM操作

function FocusInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
}

9. 错误边界(Error Boundaries)

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    
    return this.props.children;
  }
}

// 在项目中的使用
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

五、生态集成:现代React完整技术栈

10. 路由管理:React Router

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserProfile />} />
      </Routes>
    </BrowserRouter>
  );
}

11. 状态管理:Redux Toolkit

// 使用Redux Toolkit简化状态管理
import { createSlice, configureStore } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    incremented: state => {
      state.value += 1;
    },
    decremented: state => {
      state.value -= 1;
    }
  }
});

export const { incremented, decremented } = counterSlice.actions;
const store = configureStore({ reducer: counterSlice.reducer });

六、面试回答策略:如何组织你的回答

结构化回答模板:

  1. 开头总结:"在我的React项目中,我全面使用了现代React语法特性,主要可以分为以下几个方面:"

  2. 分类阐述

    • "首先是基础语法方面,我使用函数组件和JSX构建所有界面..."
    • "在状态管理上,我根据场景选择useState、useReducer或Context API..."
    • "对于性能优化,我大量使用useMemo、useCallback和React.memo..."
    • "在复杂场景中,我使用自定义Hook进行逻辑抽象和复用..."
    • "还与生态系统集成,如React Router路由和Redux状态管理..."
  3. 项目结合:"比如在[项目名]中,我使用useReducer+Context实现了一个复杂的购物车状态管理;在[另一个项目]中,通过自定义useApi Hook统一处理所有API请求..."

  4. 总结升华:"我认为React语法不仅是工具,更是一种设计思想。我注重选择最合适的语法方案来解决具体业务问题,同时保证代码的可维护性和性能。"

结语:超越语法本身

记住,面试官不仅仅关心你知道哪些语法,更关心你如何应用这些语法解决实际问题。展示你根据不同场景选择不同解决方案的思考过程,这才是让你脱颖而出的关键。

现在轮到你了:在你的React项目中,哪个语法特性让你觉得最有价值?欢迎在评论区分享你的经验!

子组件改状态,父组件会“炸毛”吗?

2025年9月1日 08:49

大家好,我是小杨。今天我们来聊聊React中一个非常有意思的话题:当我们在子组件中修改状态时,到底会不会影响到父组件?会不会触发父组件的生命周期?这个问题看似简单,却藏着不少React的精妙设计。

一个真实的踩坑经历

前几天我接到了一个需求:开发一个可折叠的商品分类菜单。父组件负责管理所有分类数据,子组件负责显示单个分类及其下的商品列表。

最初的代码大概是这样的:

// 父组件
function CategoryList() {
  const [categories, setCategories] = useState([]);
  
  useEffect(() => {
    // 获取分类数据
    fetchCategories().then(data => {
      setCategories(data);
    });
  }, []);
  
  return (
    <div>
      {categories.map(category => (
        <CategoryItem 
          key={category.id} 
          category={category}
        />
      ))}
    </div>
  );
}

// 子组件
function CategoryItem({ category }) {
  const [isExpanded, setIsExpanded] = useState(false);
  const [products, setProducts] = useState([]);
  
  const toggleExpand = () => {
    setIsExpanded(!isExpanded);
    if (!isExpanded && products.length === 0) {
      // 展开时加载商品数据
      fetchProducts(category.id).then(data => {
        setProducts(data);
      });
    }
  };
  
  return (
    <div className="category-item">
      <div className="header" onClick={toggleExpand}>
        <h3>{category.name}</h3>
        <span>{isExpanded ? '▼' : '►'}</span>
      </div>
      {isExpanded && (
        <div className="products">
          {products.map(product => (
            <div key={product.id}>{product.name}</div>
          ))}
        </div>
      )}
    </div>
  );
}

写完之后我心想:这逻辑清晰明了,肯定没问题!但测试时却发现了一个有趣的现象...

核心结论:各管各的,互不干扰

答案是:子组件中修改自己的状态,不会直接影响父组件,也不会触发父组件的生命周期

这就像是:你在自己的房间里收拾东西(修改状态),不会影响到客厅里的父母(父组件),他们该看电视还是看电视。

但是,事情没那么简单...

虽然子组件的状态变化不会直接影响父组件,但通过以下几种方式,子组件确实可以"间接"影响父组件:

  1. 通过回调函数传递信息
// 父组件
function Parent() {
  const [parentData, setParentData] = useState('initial');
  
  const handleChildUpdate = (newData) => {
    setParentData(newData); // 父组件状态更新
  };
  
  return <Child onUpdate={handleChildUpdate} />;
}

// 子组件
function Child({ onUpdate }) {
  const [childState, setChildState] = useState('');
  
  const handleClick = () => {
    const newData = 'updated by child';
    setChildState(newData);
    onUpdate(newData); // 通知父组件
  };
  
  return <button onClick={handleClick}>更新父组件</button>;
}
  1. 通过Context共享状态
const AppContext = createContext();

function App() {
  const [globalState, setGlobalState] = useState({});
  
  return (
    <AppContext.Provider value={{ globalState, setGlobalState }}>
      <ChildComponent />
    </AppContext.Provider>
  );
}

function ChildComponent() {
  const { globalState, setGlobalState } = useContext(AppContext);
  
  const updateGlobalState = () => {
    setGlobalState({ ...globalState, updated: true });
    // 这会影响到所有使用这个Context的组件
  };
}
  1. 状态提升(Lifting State Up)
// 状态提升到父组件
function Parent() {
  const [sharedState, setSharedState] = useState('');
  
  return (
    <div>
      <ChildA value={sharedState} onChange={setSharedState} />
      <ChildB value={sharedState} />
    </div>
  );
}

生命周期的影响范围

  • ✅ 子组件状态变化:只触发子组件自身的重渲染和useEffect
  • ✅ 父组件状态变化:触发父组件重渲染,也可能触发子组件的重渲染(如果props变化)
  • ❌ 子组件状态变化:不会触发父组件的任何生命周期方法

实际开发中的建议

  1. 状态位置要合理
// 如果多个组件需要同一状态,提升到共同的父组件
function ProductPage() {
  const [selectedCategory, setSelectedCategory] = useState(null);
  
  return (
    <div>
      <CategoryFilter 
        selectedCategory={selectedCategory}
        onSelect={setSelectedCategory}
      />
      <ProductList category={selectedCategory} />
    </div>
  );
}
  1. 使用useCallback避免不必要的重渲染
function Parent() {
  const [count, setCount] = useState(0);
  
  const handleChildEvent = useCallback((data) => {
    // 处理子组件事件
  }, []); // 依赖项数组为空,函数不会重新创建
  
  return <Child onEvent={handleChildEvent} />;
}
  1. 合理使用React.memo
const ChildComponent = React.memo(function ChildComponent({ data }) {
  // 只有当props变化时才会重渲染
  return <div>{data}</div>;
});

总结一下

  • 🎯 子组件状态变化不影响父组件
  • 🔄 状态变化只影响当前组件及其子组件
  • 📤 通过回调、Context等方式可以实现父子通信
  • 🚀 合理设计状态结构是React开发的关键

希望这篇文章能帮你理清React中状态管理的思路。如果你在开发中也遇到过类似的问题,欢迎在评论区分享你的经验!

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

为什么在render里调setState,代码会和你“翻脸”?

2025年9月1日 08:42

我明明只是修改个状态,怎么页面开始疯狂抽搐甚至直接崩了?

作为前端开发者,相信很多人都曾在React的render函数中尝试过setState,结果却遇到了意想不到的问题。今天小杨就来和大家聊聊这背后的原因,以及如何避免这个常见的陷阱。

一个亲身经历的bug案例

前几天我在开发一个商品列表组件时,遇到了一个奇怪的问题。页面在加载时会不断刷新,最后甚至直接白屏了。经过一番排查,我发现问题出在了下面这段代码:

function ProductList({ products }) {
  const [filteredProducts, setFilteredProducts] = useState(products);
  const [sortOrder, setSortOrder] = useState('asc');
  
  // 这里犯了低级错误!
  if (products.length !== filteredProducts.length) {
    setFilteredProducts(products);
  }
  
  return (
    <div>
      {/* 渲染产品列表 */}
    </div>
  );
}

看起来我只是在props变化时更新state,但这样做却导致了组件的无限重新渲染!

为什么render中不能调用setState?

简单来说,在render过程中调用setState就像是在盖房子时不断修改蓝图——工程永远无法完工,反而可能把工地搞得一团糟。

React的渲染过程分为几个阶段:

  1. Render阶段:计算虚拟DOM的变化
  2. Commit阶段:将变化应用到真实DOM
  3. 清理阶段:执行副作用和生命周期方法

当我们在render函数中调用setState时,React会:

  • 标记需要更新状态
  • 重新执行render函数
  • 发现又调用了setState
  • 再次标记需要更新状态
  • ...形成无限循环

正确的解决方案

对于上面的问题,我最终使用了useEffect来避免在render中直接调用setState:

function ProductList({ products }) {
  const [filteredProducts, setFilteredProducts] = useState(products);
  const [sortOrder, setSortOrder] = useState('asc');
  
  // 使用useEffect在适当的时候更新状态
  useEffect(() => {
    setFilteredProducts(products);
  }, [products]);
  
  const sortedProducts = useMemo(() => {
    return [...filteredProducts].sort((a, b) => {
      return sortOrder === 'asc' ? a.price - b.price : b.price - a.price;
    });
  }, [filteredProducts, sortOrder]);
  
  return (
    <div>
      {/* 渲染产品列表 */}
    </div>
  );
}

其他常见陷阱和解决方案

  1. 事件处理中的setState
    这是setState最安全的调用场所
function Counter() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(prevCount => prevCount + 1); // 正确:在事件处理中调用
  };
  
  return <button onClick={handleClick}>Count: {count}</button>;
}
  1. useEffect中的setState
    需要注意依赖项数组,避免无限循环
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetchUser(userId).then(userData => {
      setUser(userData); // 正确:在useEffect中调用
    });
  }, [userId]); // 确保依赖项正确
}

总结一下

  • 🚫 避免在render函数中直接调用setState
  • ✅ 在事件处理程序或useEffect中调用setState
  • 🔄 注意setState可能引起的重新渲染,合理使用useMemo和useCallback优化性能

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

扣子同款半固定输入模板的简单解决方案

作者 Maschera96
2025年9月1日 17:53

场景

image.png

我们的 AI 应用要做类似扣子空间的同款模板输入功能,但是这个组件肯定不能用文本域(因为里面不能再加自定义输入框),怎么办呢?看了一下扣子的实现方式,是用<div contentEditable=true></div> + span标签实现的,原来如此。

实现

实现效果:

image.png 首先要和后端约定好输入模板接口的返回格式,我们是这样实现的:

{
    "code": 200,
    "message": "success",
    "data": {
        "template_mode": "请帮我搜索__doctor_name__,他的科室是__department__,他所在的医院是__hospital__,我还想知道关于__product_info__的信息以及和客户有没有关联。",
        "keyword": [
            "doctor_name",
            "department",
            "hospital",
            "product_info"
        ]
    }
}

html部分这样写就行,注意设置contentEditable属性

<div
  id="editor"
  ref={editorRef}
  contentEditable
  className="template-editor whitespace-pre-line focus-visible:outline-0"
></div>
css部分
.template-input {
  display: inline-block;
  min-width: 5px;
  border: 0;
  border-radius: 4px;
  margin: 0 2px;
  padding: 0 6px;
  color: #969fffb3;
  background-color: rgba(181, 191, 255, 23%);

  &:focus-within {
    background-color: rgba(181, 191, 255, 23%);
  }

  &:hover {
    background-color: rgba(181, 191, 255, 23%);
  }

  // 当内容为空时,使用 data-original 作为占位符显示
  &:empty::before {
    content: attr(data-original);
    color: #969fffb3;
  }
}

.agent-input-area {
  border: 0;
  border-radius: 0;
  padding: 0;

  &:focus-within {
    box-shadow: 0 0 0 0 rgba(0, 0, 0, 0%);
  }
}

#editor:focus-visible {
  outline: 0;
}

js部分

主要分为三个功能函数:

  1. renderEditor()函数负责从后端获取约定好的输入模板,并在editor元素上渲染出来
  2. saveTextContent()是个防抖函数,通过document.createTreeWalker遍历editor元素内的子元素内容,并把内容存储起来
  3. handleKeydown()处理每次keydown的逻辑,主要是防止当用户删除完子输入框的最后一个元素时会把整个子输入框也删除掉的情况,这种时候需要展示初始值,也就是placeholder的效果
renderEditor()函数
  const renderEditor = async () => {
    const res = await services.fetchPromptTemplate();
    const keywords = res.keyword;
    const templateStr = res.template_mode.trim();
    const fragments = templateStr.split('__');
    const templateObj = {
      parts: [],
    };
    fragments.forEach((fragment) => {
      if (keywords.includes(fragment)) {
        templateObj.parts.push({
          type: 'input',
          id: fragment,
          value: fragment,
        });
      } else {
        templateObj.parts.push({
          type: 'text',
          content: fragment,
        });
      }
    });
    const editor = editorRef.current;
    editor.innerHTML = '';
    templateObj.parts?.forEach((part) => {
      if (part.type === 'text') {
        const textNode = document.createTextNode(part.content);
        editor.appendChild(textNode);
      } else if (part.type === 'input') {
        const span = document.createElement('span');
        span.className = 'template-input cursor-text';
        span.dataset.id = part.id;
        // 使用空内容 + CSS :empty::before 展示占位符
        span.dataset.original = part.value;
        span.textContent = '';

        // 让 span 始终可编辑,这样 beforeinput 事件才能正常触发
        span.contentEditable = true;

        span.addEventListener('click', (e) => {
          e.stopPropagation();
          span.focus();
        });

        // 为 span 添加一个标识,方便在父级事件中识别
        span.dataset.isTemplateInput = 'true';

        editor.appendChild(span);
      }
    });
  };
saveTextContent()函数
  const saveTextContent = debounce(() => {
    const editor = editorRef.current;
    const walker = document.createTreeWalker(editor);
    let currentNode;
    let completedText = '';

    while ((currentNode = walker.nextNode())) {
      if (currentNode.classList?.contains('template-input')) {
        const key = currentNode.dataset.id;
        const val = (currentNode.textContent || '').trim();
        setParamsObj((prev) => ({ ...prev, [key]: val }));
        continue;
      }
      completedText += currentNode.textContent;
    }
    setInputValue(completedText);
  }, 500);
handleKeydown()函数
const handleKeydown = (e) => {
  if (e.key === 'Backspace' || e.key === 'Delete') {
    // 检查当前光标是否在 template-input 内
    const selection = window.getSelection?.();
    if (selection && selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      const targetSpan =
        range.startContainer.parentElement?.closest('.template-input');

      if (targetSpan) {
        const currentText = targetSpan.textContent || '';
        if (currentText.length <= 1) {
          e.preventDefault();
          targetSpan.textContent = '';

          // 将光标保持在 span 内部
          const r = document.createRange();
          r.setStart(targetSpan, 0);
          r.collapse(true);
          selection.removeAllRanges();
          selection.addRange(r);

          saveTextContent();
        }
      }
    }
  }
  saveTextContent();
};

React 的基本概念介绍

作者 webKity
2025年9月1日 17:30

React 的简单介绍

  • 特色 - 组件化:像搭乐高一样,把页面拆分成一个个独立、可复用的“零件”(组件),然后组合起来。
  • 特色 - 虚拟DOM:虚拟DOM带来两个好处:
  1. 性能卓越: 每次数据变更,只会更新真正产生变更的地方
  2. 跨平台:虚拟DOM把HTML的DOM节点抽象成了一个JS的对象,所以React理论可以运行在所有支持js的环境中,如:nodejs(nextjs)、移动APP(React Native)、PC平台(electron)

a. 使用在线编辑器:访问 codesandbox.io ,选择 “React” 模板

b. 本地开发(推荐最终方式):使用 Create React App

  • 这是React官方推荐的脚手架工具,能一键生成项目环境。
  • 只需要一行命令(确保已安装Node.js):
    npx create-react-app react-demo
    cd react-demo
    npm start
    
  • 执行后,一个React开发环境就在 http://localhost:3000 运行起来了!

1. JSX (JavaScript XML) - 在JS里写“HTML”

1.1 什么是JSX?

JavaScript的语法扩展,允许我们在JavaScript代码中直接编写类似HTML的结构。在我的理解中JSX就是一个描述UI结构的字符串。

  • 示例

    // js生成HTML片段版本
    const name = 'World'
    const time = '2025-09-01'
    const element = '<div>'+
                      '<h1>Hello, ' + name + '!</h1>' +
                      '<p>当前时间:' + time + '</p>' +
                    '</div>';
    
    // 使用ES6语法优化版本
    const name = 'World'
    const time = '2025-09-01'
    const element = `
      <div>
        <h1>Hello, ${ name }!</h1>
        <p>当前时间:${ time }</p>
      </div>
    `;
    
    // React jsx 版本
    const [ name, nameSet ] = useState('World');
    const [ time, timeSet ] = useState('2025-09-01');
    const element = (
      <div>
        <h1>Hello, { name }!</h1>
        <input onChange={ change }/>
        <p>当前时间:{ time }</p>
      </div>
    )
    

1.2 在JSX中嵌入表达式

使用花括号 {} 可以嵌入任何有效的JavaScript表达式,如:if 语句、 for 循环或者三目运算等

const [ logged, loggedSet ] = useState(false);
const [ name, nameSet ] = useState('World');
const [ time, timeSet ] = useState('2025-09-01');
const [ devices, devicesSet ] = useState([
  { time: '2025-01-01', device: 'Android', address: '北京市朝阳区' },
  { time: '2025-05-01', device: 'IPhone 16 Pro', address: '深圳市福田区' },
  { time: '2025-09-01', device: 'Chrome 139.0.0.0', address: '深圳市福田区' },
]);

const list = (
  {/*
    JSX表达式必须有一个父元素包裹,通常使用 `<div>` 或 React Fragment `<></>`, 在React Native中一般是View; 使用React Fragment`<></>`,不会产生额外的DOM元素
  */}
  <>
    <h3>登录设备:</h3>
    {
      devices.map(item => (
        <div>
          <div>{ item.time }</div> 
          <div>{ item.device }</div> 
          <div>{ item.address }</div> 
        </div>
      ))
    }
  </>
)

const element = (
  {/* 使用 `className` 而不是 `class`(因为class是JS的保留字)。 */}
  <div className={ logged ? '' : 'disabled' }>
    <h1>Hello, { name }!</h1>

    {/* 所有标签都必须闭合,如 `<img />` 或 `<input />`。 */}
    <img src={ user.avatarUrl }></img>
    <input onChange={ change }/>

    <p>当前时间:{ time }</p>
    { list }
  </div>
)

2. 组件 - 应用的基石

使用组件可以将UI拆分为独立、可复用的代码片段。在React中组件有两种书写方式:函数组件和类组件。

2.1 函数组件 (推荐) 最简单的定义组件的方式是编写一个JavaScript函数。

// 定义一个组件,这里注意有两个关键元素:
// 1. 组件名称: Welcome (函数名必须大写)
// 2. 外部状态: props (必须是一个对象)
function Welcome(props) {
  // 定义组件内部状态, 这里的name对应java中数据的get;nameSet对应数据的set(Model)
  const [ name, nameSet ] = useState('World');

  // 定义组件内部行为 (Control)
  const change = (event) => {
    const value = event.target.value;
    // 更新state,组件会重新渲染
    nameSet(value);
  }

  // 另一个useEffect,模拟componentDidMount
  useEffect(() => {
    console.log('仅挂载时执行一次');

    // 相当于 componentWillUnmount
    return () => {
      console.log('组件即将卸载');
    };
  }, []); // 空依赖数组表示只在挂载和卸载时执行

  // 相当于 componentDidMount 和 componentDidUpdate
  useEffect(() => {
    console.log('组件已挂载或count已经更新更新');
  }, [name]); // 依赖数组,只有count变化时才执行

  // 这个组件返回一段JSX(View)
  return (
    <div>
      <h1>Hello, { name }!</h1>
      <input onChange={ change }/>
      <p>当前时间:{ props.time }</p>
    </div>
  );
}

// 3. 像使用HTML标签一样使用它
function App() {
  return (
    <div>
      <Welcome time="2025-09-01"/> {/* 这就是我们自定义的组件 */}
    </div>
  );
}

2.2 类组件 (Class Components) ES6的Class也可以用来定义组件,但现在主要用于需要生命周期函数的旧项目。

class Welcome extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: ''
    };
  }

  componentDidMount() {
    console.log('类组件: 组件已挂载');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('类组件: 组件已更新');
  }

  componentWillUnmount() {
    console.log('类组件: 组件即将卸载');
  }

  shouldComponentUpdate(nextProps, nextState) {
    console.log('类组件: 判断是否应该更新');
    // 只有当count变化时才更新
    return nextState.name !== this.state.name
  }

  change(event) {
    this.state.name = event.target.value
  }

  render() {
    const { name } = this.state;
    const { time } = this.props;

    return (
      <div>
        <h1>Hello, { name }!</h1>
        <input onChange={ this.change }/>
        <p>当前时间:{ time }</p>
      </div>
    );
  }
}

3. 组件通信

React 的数据流是“单向”的,自上而下通过 props 传递,但根据通信方向与距离的不同,我们采用不同的方式。

3.1 父组件向子组件传递信息:Props

这是最基础、最常见的通信方式。父组件通过子组件的属性(props) 将数据传递下去。

  • 方式:在子组件标签上写入属性。
  • 子组件接收:通过函数的参数 props 或解构赋值获取。

示例

// 父组件 Parent.js
import Child from './Child';

function Parent() {
  const parentData = "Data from Parent";
  const sayHello = () => { alert('Hello from Parent!'); };

  return (
    <div>
      {/* 传递字符串和数据函数 */}
      <Child 
        message={parentData} 
        onSayHello={sayHello} 
        extraContent={<span>This is JSX from Parent</span>}
      />
    </div>
  );
}

// 子组件 Child.js
interface IChildProps {
  message: string;
  extraContent: string;
  onSayHello?: () => void;
}
function Child(props: IChildProps) {
  // 也可以使用解构: function Child({ message, onSayHello, extraContent }) { ... }
  return (
    <div>
      <p>收到来自父组件的信息:{props.message}</p>
      <button onClick={props.onSayHello}>点击我</button>
      {props.extraContent}
    </div>
  );
}

export default Child;

3.2 子组件向父组件传递信息:回调函数

子组件不能直接修改父组件的 props。如果需要通知父组件某些事件(如表单提交、按钮点击)或传递数据,父组件可以通过 props 传递一个回调函数给子组件,子组件在适当时机调用此函数。

  • 方式:父组件定义函数,通过 prop 传递给子组件 -> 子组件调用该函数并传入参数。

示例

// 父组件 Parent.js
import { useState } from 'react';
import Child from './Child';

function Parent() {
  const [messageFromChild, setMessageFromChild] = useState('');

  // 定义回调函数,用于接收子组件的数据
  const handleChange = (data) => {
    console.log("父组件收到用户的输入数据:", data);
  };

  // 定义回调函数,用于接收子组件的数据
  const handleDataFromChild = (data) => {
    setMessageFromChild(data);
    console.log("父组件收到:", data);
  };

  return (
    <div>
      <p>子组件对我说:{messageFromChild}</p>
      {/* 将回调函数传递给子组件 */}
      <Child onSendData={handleDataFromChild} onChange={handleChange}/>
    </div>
  );
}

// 子组件 Child.js
function Child({ onChange, onSendData }) {
  const [count, countSet] = useState(0);

  const handleChange = (event) => {
    const value = event.target.value;
    countSet(value)
    // 调用父组件传来的函数,并传递数据
    onChange(value)
  };

  const handleClick = () => {
    // 调用父组件传来的函数,并传递数据
    onSendData("Hello Parent! - From your child");
  };

  return (
    <>
      <input onChange={handleChange}/>
      <button onClick={handleClick}>发送信息给父组件</button>
    <>
  );
}

export default Child;

3.3 多级组件跨越式传递信息:

当需要在远房组件(如孙组件、曾孙组件)之间传递数据,层层手动传递 props(称为“Prop Drilling”)会非常繁琐。此时有两种主流解决方案:

3.3.1 上下文 (Context) - (React 官方推荐)

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。它创建了一个“全局”的数据域,其下的任意组件都能订阅这个数据。

  • 适用场景:React组件中的多级组件之间的通信,如果涉及组件和普通js函数之间的通信用这种方式就不适合

示例

// 1. 创建 Context (例如:ThemeContext.js)
import { createContext } from 'react';
export const ThemeContext = createContext('light'); // 'light' 为默认值

// 2. 在顶层组件提供数据 (App.js)
import { ThemeContext } from './ThemeContext';
import Toolbar from './Toolbar';

function App() {
  const [theme, setTheme] = useState('dark');

  return (
    // 使用 Provider 提供 value,包裹需要接收数据的子组件树
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Toolbar /> {/* Toolbar 及其所有子组件都能订阅这个 Context */}
    </ThemeContext.Provider>
  );
}

// 3. 在深层子组件中消费数据 (ThemedButton.js)
import { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function ThemedButton() {
  // 使用 useContext Hook 来订阅 Context 的变化
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <button 
      style={{ 
        background: theme === 'dark' ? '#333' : '#CCC',
        color: theme === 'dark' ? 'white' : 'black'
      }}
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      My theme is {theme}
    </button>
  );
}
3.3.2 全局事件中心 (Event Bus / PubSub)

这是一种传统的设计模式,创建一个全局的中央事件管理器。组件可以“发布(Publish)”事件到中心,也可以“订阅(Subscribe)”中心的事件。彼此之间不直接通信,而是通过事件中心间接联系。

  • 适用场景:非父子组件、关系非常遥远的组件间通信。在现代React开发中,通常优先考虑 Context API 或状态管理库(如 Redux),但在某些特定场景或简单应用中仍可使用。

概念性示例

// eventBus.js (一个简单的实现)
const events = {};

const eventBus = {
  // 订阅事件
  on(eventName, callback) {
    if (!events[eventName]) {
      events[eventName] = [];
    }
    events[eventName].push(callback);
  },
  // 取消订阅
  off(eventName, callback) {
    if (!events[eventName]) return;
    events[eventName] = events[eventName].filter(cb => cb !== callback);
  },
  // 发布事件
  emit(eventName, data) {
    if (!events[eventName]) return;
    events[eventName].forEach(callback => {
      callback(data);
    });
  }
};

export default eventBus;
// Component.js (订阅者,可以是任意位置的组件)
import { useEffect } from 'react';
import eventBus from './eventBus';

function Component() {
  useEffect(() => {
    // 组件挂载时订阅事件
    const handleEvent = (data) => {
      console.log('收到事件和数据:', data);
    };
    eventBus.on('myEvent', handleEvent);

    // 组件卸载时取消订阅,防止内存泄漏
    return () => {
      eventBus.off('myEvent', handleEvent);
    };
  }, []);

  return <div>Listening for events...</div>;
}

// Socket.js
function socket() {
  const msg = {sub: "ticker", id: "123456", type:"add"}
  const socket = new WebSocket('wss://somesocket.com');

  socket.onmessage = (e: MessageEvent) => {
    eventBus.emit('myEvent', { data: e.data });
  };

  socket.send(msg)
}

通信方式总结

通信方向 使用方式 适用场景
父 -> 子 Props 最常用,直接的父子关系
子 -> 父 回调函数 (通过 Props) 子组件通知父组件,传递数据
任意组件 Context API 跨多级组件传递“全局”数据(React 首选
任意组件、函数 全局事件中心 (Event Bus) 非父子组件、极度松散的通信
任意组件 状态管理库 (Redux, Zustand) 复杂的应用状态管理,可视为Context的增强版(我们项目中没有用到)

4. Hooks

Hooks 是 React 16.8 版本引入的一项革命性特性。它允许你在函数组件中使用 state 以及其他 React 特性(如生命周期),从而摆脱了必须使用 class 组件的限制。

核心优势:

  1. 逻辑复用:解决了 Class 组件中高阶组件(HOC)和渲染属性(Render Props)带来的“嵌套地狱”问题(后续第五部分会单独讲讲),使状态逻辑的复用变得非常简单。
  2. 代码组织:允许将组件中相互关联的逻辑拆分成更小的函数(自定义 Hook)。
  3. 易于理解:函数组件更简洁,没有复杂的 this 绑定问题。

4.1 内置 Hooks

a. useState - 状态钩子

用于在函数组件中添加和管理局部状态。

  • 用法const [state, setState] = useState(initialState);

  • 参数initialState 是状态的初始值(可以是任意类型,函数也行)。

  • 返回值: 一个包含两个元素的数组:

    • state: 当前的状态值。
    • setState: 用于更新状态的函数,调用它会触发组件重新渲染。
  • 示例

    import React, { useState } from 'react';
    
    function Counter() {
      const [count, setCount] = useState(0); // 初始值为 0
    
      return (
        <div>
          <p>你点击了 {count} 次</p>
          <button onClick={() => setCount(count + 1)}>
            点击我
          </button>
          {/* 函数式更新,解决异步更新依赖问题 */}
          <button onClick={() => setCount(prevCount => prevCount - 1)}>
            减少
          </button>
        </div>
      );
    }
    
b. useEffect - 副作用钩子

用于在函数组件中执行副作用操作(数据获取、订阅、手动修改 DOM 等)。它可以看作是 componentDidMount, componentDidUpdate, 和 componentWillUnmount 的组合。

  • 用法useEffect(effectFunction, dependencyArray?)

  • 参数

    • effectFunction: 包含副作用逻辑的函数。此函数可以返回一个清理函数(cleanup),用于在组件卸载或执行下一次 effect 前清除副作用(如取消订阅、清除定时器)。
    • dependencyArray (可选): 依赖项数组。React 会根据这个数组来决定是否重新执行 effect。
  • 执行时机

    1. 无依赖数组 (useEffect(effect)): 每次组件渲染后都会执行。
    2. 空依赖数组 (useEffect(effect, [])): 只在组件首次挂载后执行一次(类似于 componentDidMount)。
    3. 有依赖项 (useEffect(effect, [state1, state2])): 只在依赖项(state1, state2)发生变化时执行。
  • 示例

    import React, { useState, useEffect } from 'react';
    
    function UserProfile({ userId }) {
      const [user, setUser] = useState(null);
    
      //  effect 依赖于 userId prop
      useEffect(() => {
        // 异步获取数据
        fetch(`/api/user/${userId}`)
          .then(response => response.json())
          .then(data => setUser(data));
    
        // 返回清理函数(可选)
        return () => {
          // 这里可以取消未完成的请求(例如使用 AbortController)
          console.log('Cleanup for userId:', userId);
        };
      }, [userId]); // 只有当 userId 变化时,才会重新执行
    
      return <div>{user ? user.name : 'Loading...'}</div>;
    }
    
c. useContext - 上下文钩子

用于接收一个 Context 对象(由 React.createContext 创建)并返回该 Context 的当前值。让你无需组件嵌套即可订阅 React 的 Context。

  • 用法const value = useContext(MyContext);
  • 示例
    // 1. 创建 Context
    const ThemeContext = React.createContext('light');
    
    function App() {
      // 2. 使用 Provider 提供值
      return (
        <ThemeContext.Provider value="dark">
          <Toolbar />
        </ThemeContext.Provider>
      );
    }
    
    function Toolbar() {
      // 中间的组件无需再传递 theme prop
      return <ThemedButton />;
    }
    
    function ThemedButton() {
      // 3. 在子组件中使用 useContext 获取值
      const theme = useContext(ThemeContext);
      return <button className={theme}>我是 {theme} 主题的按钮</button>;
    }
    
d. useCallback & useMemo - 性能优化钩子

用于避免不必要的重复渲染和计算,优化性能。

  • useCallback: 缓存一个函数本身。

    • 问题: 父组件重新渲染时,其内部定义的函数会重新创建,导致接收该函数作为 prop 的子组件不必要的重渲染。
    • 解决useCallback(fn, deps) 返回一个记忆化的回调函数,只在依赖项 deps 变化时才会更新。
    const memoizedCallback = useCallback(() => {
      doSomething(a, b);
    }, [a, b]); // 只有当 a 或 b 变化时,函数才会重新创建
    
  • useMemo: 缓存一个计算值。

    • 问题: 每次渲染时都要进行昂贵的计算(如过滤大型数组)。
    • 解决useMemo(() => computeExpensiveValue(a, b), [a, b]) 返回一个记忆化的值,只在依赖项变化时重新计算。
    const expensiveValue = useMemo(() => {
      return someExpensiveCalculation(a, b);
    }, [a, b]); // 只有当 a 或 b 变化时,计算才会重新执行
    
e. useRef 用于储存一个不会随着函数组件刷新而重新定义、赋值的数据

4.2 自定义 Hooks

自定义 Hook 是一个以 use 开头的 JavaScript 函数,它内部可以调用其他的 Hook。它的目的是将组件逻辑提取到可重用的函数中,所以当你发现一些逻辑在多个组件中重复出现时,就可以考虑将其提取为自定义 Hook。

核心规则:

  • 函数名必须以 use 开头。
  • 自定义 Hook 内部可以调用其他 Hook。
  • 两个不同的组件使用相同的自定义 Hook 不会共享 state。每次调用都有自己的独立状态。

示例:我们去看下React官方的例子:zh-hans.react.dev/learn/reusi…

5. 拓展

这部分咱们主要说说为什么推荐函数组件,而不是类组件,主要有以下几个原因:

5.1 更好的逻辑复用

5.2 无需理解 JavaScript 的 this 机制

在JS中this和其他语言有很大的不同:this 的值取决于函数被调用的方式,而不是定义的方式. 而类组件严重依赖this,这就带来了几个经典难题:

  • a. 事件处理函数的绑定问题
  • b. this 在生命周期方法中的不一致性
// 示例地址: https://codesandbox.io/p/sandbox/stoic-grass-8c9sfm?file=%2Fsrc%2FApp.js%3A6%2C25-7%2C22
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };

    // 解决: 在构造函数中手动绑定
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // 如果不绑定,这里的 `this` 将是 undefined,导致 Cannot read properties of undefined (reading 'setState') 的错误;
    this.setState(prevState => ({
      count: prevState.count + 1
    }));
  }

  componentDidMount() {
    // 这里的 `this`指向组件实例,
    console.log('this', this);
    
    setTimeout(function() {
      console.log('setTimeout this', this);
    }, 1000)
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Count: {this.state.count}
      </button>
    );
  }
}

而在函数组件中根本就没有this所以天然就不会有上面说的问题

function MyComponent() {
  const [count, setCount] = useState(0);

  // 直接声明一个函数。没有 `this`,没有绑定。
  const handleClick = () => {
    // 直接使用 `setCount`,它来自闭包,永远指向正确的函数
    setCount(prevCount => prevCount + 1);
  };

  return (
    <button onClick={handleClick}>
      Count: {count}
    </button>
  );
}

Zalo Mini App 初体验

作者 __M__
2025年9月1日 16:09

4aRp6pjqpxiVfn-NoTRmd5258cnG21MMw9WDW7KVwKA.png

这是一个风和日丽的上午,本牛马正品着咖啡哼着小曲儿打算开始一天忙碌(摸鱼),产品(很烦)突然找到我:“我们要开发一款小程序,可以搞么?”

小程序?可以搞么??!!笑话!这不洒洒水~~~

产品:“哦!是Zalo小程序”

o((⊙﹏⊙))o :“咋咯? 神马东西?能吃么?!” 一脸懵逼ing

于是,本牛马开始各种百度,Google,DeepSeek。然而查获的资源实在有限。好不容易找到一篇,(最低0.47元/天,解锁文章)。好吧,本牛马资金有限,超额要走OA。这个还是不开了。于是俺就开始用俺这 一瓶子不满,半瓶子晃荡的-4级英语加机翻硬撸官方文档。忍不住吐槽!Zalo官方文档是真的一言难尽!!!垃圾的一批。

故事讲完,下面开始正文

概述

Zalo 是越南当地的一款即时聊天软件,类似国内微信。但是小程序实现和国内出入很大,Zalo提供一些配置,可以将现有web直接转换为Zalo 小程序,灵活性很高。并且Zalo提供了一下api可以直接使用 Zalo 的一些高级功能。

网站托管服务

通过 Zalo Mini App,开发者的应用程序将被打包托管在 Zalo 的系统上,并通过 CDN 系统分发给用户,确保快速的访问速度并能够处理大量用户流量。

缓存和更新新版本

Zalo Mini App 的资源会保存在 Zalo 应用程序的 Cache 中,因此当用户第二次访问该应用程序时,几乎会立即加载,并且可以支持无需网络的离线加载机制。

安全和权限

Zalo Mini App 应用程序在分发给用户之前会经过 Zalo 系统的身份验证和测试,因此比常规的 Web 应用程序更加安全。

所有访问 Zalo Mini Apps 系统功能或访问用户信息的权限均须在实际使用前声明并获得批准。

创建小程序

  1. 前往  Zalo for Developer  并登录 Zalo 帐户。然后在 Zalo 平台上创建应用程序或使用现有应用程序。
  2. 在“设置” 中   选择   “启用应用程序”   ,以便外部用户使用。

zalo-developer-fd406f8685a9afb835517aee66b490f8.jpg 3. 接下来,你需要创建一个新的小程序。 应用管理页面,方便开发者管理自己的小程序,例如:管理权限、查看报表、发布小程序…… 创建小程序步骤:

  1. 进入应用管理页面
  2. 选择 add Mini App
  3. 填写所需信息。然后选择“新建”

create-mini-app-c0de3b97bd6514ab95f48fd301988116.jpg 创建小程序后,会收到一个小程序 ID,妥善保存。如果忘了也没关系,应用管理页面可以查。

开发

VScode 使用 Zalo Mini App 扩展

  1. 安装 Visual Studio CodeZalo Mini App 扩展
  2. 配置:在 Home 标签页,完成 配置应用ID安装依赖

image.png

  1. 登录:进入 Deploy 标签页 扫码登录开发者账号。
  2. 启动:进入 Run 标签页,选择合适的启动器,点击 Start

image.png

  1. 部署:进入 Deploy 标签页,选择部署环境,点击Deploy,注意:Deploy无法使用 env 文件。 该tab页可以查看小程序 Testing 环境发布的历史版本

image.png

使用 Zalo Mini App CLI

  1. 安装 Node JS
  2. 安装 Zalo Mini App CLI
  3. 安装依赖
pnpm install
  1. 登录 登录开发者账号:
pnpm run login
  1. 启动 开发服务器:
pnpm run start

部署 Dev环境: zmp-cli 选择 Development。

pnpm run deploy

部署 Test环境:zmp-cli 选择 Testing。

pnpm run deploy

调试

使用 pnpm run start 启动开发服务器后,命令行工具内出现二维码,用Zalo 扫描二维码查看小程序页面。

开启调试:浏览器打开 http://localhost:51019 调试页面。

点击 inspect 进入控制台,就可以愉快地敲BUG啦。

愉快地一天就这么结束了!!

从工作原理入手理解React一:React核心内容和基本使用

作者 YuspTLstar
2025年9月1日 16:09

简介

本文旨在梳理React hooks组件的基本使用及常见API的说明和一些注意事项。部分章节涉及到React的底层工作原理。

React Hooks组件就是函数组件加Hooks API。React是一个通过jsx语法描述UI的javascript前端库。其核心思想是将前端页面组件化,通过组件的组合构成视图,父子组件之间遵循单向数据流原则。在React内部维护组件状态并代理DOM操作,通过修改状态来更新视图。

函数组件基础

函数组件就是一个普通函数,接收父组件传递的props参数,返回jsx描述的UI节点。编译后,jsx被编译成创建虚拟DOM的运行时函数,在react内部通过一些列处理后根据虚拟DOM绘制真实DOM。在挂载和更新阶段都会执行该函数。

function Hello(props) {
    return (
        <div>
            <h1>hello world</h1>
        </div>
    )
}

响应式原理

React的响应式是通过数据驱动视图,当数据发生变化时,视图自动更新。

触发React Hooks组件更新的方式有两种。一种是组件setState发生变化;另外一种是组件的父组件更新(这种情况下组件会重新执行,但不一定会重新渲染视图)。

1、React Hooks组件的状态通过useState Hook创建。useState接收一个初始值做为初始状态。返回初始状态和setState,setState接收修改后的值。调用setState,传入修改后的值触发重新渲染流程。注意:state要遵循不可变原则,即不可直接修改state。想要改变state需要将一个新的值传递给setState,并且通过setState修改状态才能触发组件更新。在React内部,setState是异步的,调用后并不会立即执行,react会将修改收集起来统一调度,根据任务的优先级来执行。

2、父组件更新时,所有的子组件都会重新执行,但视图不一定会更新。

基本用法

1、创建React应用。通过createRoot api 创建根节点,createRoot接收rootElement并返回ReactDOMRoot根节点对象,根节点对象提供render和unmount等方法实现组件的渲染和卸载。如下:

const App = () => {
    return (
        <div>
            <Hello />
        </div>
    )
}
//创建根节点并渲染App组件
createRoot(document.getElementById('root')).render(<App />)

2、添加状态。上面的 HelloApp 组件都是函数组件,或者叫做静态(无状态)组件,因为它只能执行一次渲染,无法执行组件更新。为了让函数组件更够持有状态,并且当状态发生改变时更新。我们需要使用到第一个HOOK——useState。前文介绍过 useState的基本用法,这里我们给 Hello组件增加状态,如下:

function Hello(props) {
    const [text, setText] = useState("hello")
    function changeText() {
        setText(text => text == 'hello' ? 'hi' : 'hello')
    }
    return (
        <div>
            <h1>say {text}</h1>
            <button onClick={changeText}>change text</button>
        </div>
    )
}

3、父子组件通信。父组件传递给子组件的属性,会被包装成为props对象传递给子组件,在子组件中可通过props对象获取或者修改数据。父子组件通信还可用于状态提升——当有两个兄弟组件共享同一个状态时,为了方便管理状态的变化,可将状态提取到最近的公共父组件。

const App = () => {
    const [name, setName] = useState('React')
    function changeName() {
        setName(name => name == 'React' ? 'Vue' : 'React')
    }
    return (
        <div>
            <Hello name={name} changeName={changeName} />
        </div>
    )
}
function Hello({name, changeName}) {
    const [text, setText] = useState("hello")
    function changeText() {
        setText(text => text == 'hello' ? 'hi' : 'hello')
    }
    return (
        <div>
            <h1>say {text}</h1>
            <button onClick={changeText}>change text</button>
            <h1>say {name}</h1>
            <button onClick={changeName}>change Name</button>
        </div>
    )
}

注意:父子组件通信应遵循单项数据流原则,即子组件内部不可直接修改父组件传递的数据,需要通过调用父组件传递的在父组件中定义的方法来修改,确保数据变化的可预测性。

4、ref引用值。当我们希望组件记住某些信息,但又不想让这些信息触发新的渲染时,选择使用ref。ref通常用于脱围机制,用于记录一些DOM节点信息,可以直接操作DOM元素。通过useRef创建,useRef接收初始值,返回ref对象,通过ref.current获取值。

function RefCompoment() {
    const ref = useRef(null)
    function focus() {
        ref.current.focus()
    }
    return (
        <div>
            <input ref={ref} type="text" />
            <button onClick={focus}>focus</button>
        </div>
    )
}

5、Effect。Effect是一种脱围机制,用于处理一些由视图更新引起的副作用,即让一些副作用与组件状态同步。Effect通过useEffect创建,useEffect接收一个回调函数和依赖数组。依赖数组可以不传(每次组件更新时执行Effect)、空数组(仅在第一次挂载时执行Effect)、数组(数组内的状态改变时执行Effect)。回调函数返回一个清理函数,用于在Effect卸载时清理一些数据,比如清理闭包或事件监听等。

function EffectCompoment() {
    const [count, setCount] = useState(0)
    useEffect(() => {
        document.title = `count is ${count}`
    }, [count])
    return (
        <div>
            <h1>count is {count}</h1>
            <button onClick={() => setCount(count + 1)}>count</button>
        </div>
    )
}

注意:

  • 在Effect内部禁止改变依赖的状态,这样会导致组件循环更新。
  • Effect是异步执行的,不会阻塞页面的渲染,在某些情况下可能出现闪屏的现象。如果需要在渲染前执行Effect,可使用 useLayoutEffect替换,但一般不建议这样操作,除非副作用的优先级非常高。
  • 合理使用Effect

以上就是React使用层面最基础也是最核心的内容,React仅给我们提供了一套响应式API及事件处理机制(这里暂不做说明),针对其响应性和渲染流程可能存在的问题提供了解决方案以及能力的增强。所以React应用的构建变得非常灵活。后面的章节将根据响应式原理及渲染流程梳理React提供的解决方案。

❌
❌