如何在 EventEmitter 的 on('change') 函数中使用钩子设置状态?

How to setState with hooks in EventEmitter's on('change') function?

出于学习的目的,我试图通过从头开始实施来分解通量模式的基础知识。我知道我可以使用 Class 组件 解决下面的问题,但为什么它与 class 组件的 setState({ ... }) 一起使用而不是钩子的setValues函数?

如您所料,问题是 setValues 函数似乎没有触发新状态的重新渲染。我觉得这与闭包有关,但我不太了解它们,所以我将不胜感激任何指导和帮助。

商店

// store.js
import EventEmitter from "events";

class StoreClass extends EventEmitter {
    constructor() {
        super();
        this.values = []
    }

    addNow() {
        this.values.push(Date.now());
        this.emit('change')
    }

    getAll() {
        return this.values;
    }
}

const store = new StoreClass();

export default store;

组件

// App.js
import store from './store';

function App() {
    const [values, setValues] = useState([]);

    const handleClick = () => {
        store.addNow();
    };

    useEffect(() => {
        const onChangeHandler = () => {
            console.log("Change called");
            setValues(() => store.getAll())
        };

        store.on('change', onChangeHandler);

        return () => store.removeAllListeners('change')
    }, [values]);

    return (
        <div>
            <button onClick={handleClick}>Click to add</button>
            <ul>
                {values.map(val => <li key={val}>{val}</li>)}
            </ul>
        </div>
    );
}

useEffct 监听值的变化,但是 setValues 只存在于 useEffect 中,我认为你根本不需要 useEffect

你可以试试这样:

    // App.js
import store from './store';

function App() {
    const [values, setValues] = useState([]);


    const onChangeHandler = () => {
            console.log("Change called");
            const storeValues = store.getAll();
            setValues(storeValues);
        };

    const handleClick = () => {
        store.addNow();
        onChangeHandler();
        };


    return (
        <div>
            <button onClick={handleClick}>Click to add</button>
            <ul>
                {values.map(val => <li key={val}>{val}</li>)}
            </ul>
        </div>
    );
}

再修改几个小时并再次阅读 React 文档后,我终于可以回答我自己的问题了。简而言之,问题是 store 中的 state 没有被一成不变地更新。

Redux 是 Fl​​ux 架构的一个实现,在他们的文档中说的很清楚:

Redux expects that all state updates are done immutably

因此,上面代码中的罪魁祸首就在store的addNow函数中。 Array.push() 方法通过附加一项来改变现有数组。这样做的不可变方法是使用 Array.concat() 方法或 ES6 扩展语法(解构)[...this.values],然后将新数组分配给 this.values.

addNow() {
  this.values.push(Date.now()); // Problem - Mutable Change
  this.values = this.values.concat(Date.now()) // Option 1 - Immutable Change
  this.values = [...this.values, Date.now()] // Option 2 - Immutable Change
  this.emit('change')
}

要查看反应组件中每个更改的效果,请将 useEffect 挂钩中的 setValues 函数更改为以下内容:

setValues((prev) => {
  const newValue = store.getAll()

  console.log("Previous:", prev)
  console.log("New value:", newValue)

  return newValue
})

在控制台中可以清楚地看到,当商店中的状态发生可变变化时,在单击第一个按钮后,之前的状态和新状态将始终相同并引用相同的数组。但是,当存储中的状态发生不可变变化时,先前状态和新状态不会引用同一个数组。此外,在控制台中可以清楚地看到,之前的状态数组比新状态数组的值少了一个。

为什么在使用 class 组件时状态更改会触发重新渲染,而在使用挂钩时不会?

根据 class component's setState function 的反应文档。

setState() will always lead to a re-render unless shouldComponentUpdate() returns false.

这意味着无论 store 中的状态是可变的还是不可变的,当触发 change 事件并调用 setState 函数时,react 将重新渲染组件并显示“新”状态。然而,useState 钩子不是这种情况。

根据 useState hook 的反应文档:

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

这意味着当通过改变原始对象对存储中的数组进行更改时,反应会在决定再次渲染之前检查新状态和之前的状态是否相同。由于之前和当前状态的引用是相同的,因此 React 什么都不做。要解决此问题,商店中的更改必须不可变地完成。