React setState、useState核心实现原理--模拟实现

class组件

基本使用

首先,先确认一个点,React类组件中setState渲染组件是异步还是同步?答案既可以同步又可以异步! 乍一看,woca,还有这种操作? 我们知道,setStatereact的一步操作,每次调用setState都会触发更新,异步操作是为了提高性能,并且将多个状态合并一起更新,减少render调用。先看下面代码:

 class Foo extends React.Component {
    state = {
        num: 0,
    }
    componentDidMount() {
        for (let i = 0; i < 5; i++) {
             this.setState({ num: this.state.num + 1 });
             console.log(this.state.num);
        }
    }
    render() {
        return (
            <div>Foo:{this.state.num}</div>
        )
    }
}

复制代码

效果: image.png 从打印结果来看,循环5次的结果组件状态中的num始终是0,直到最后一次num更新为1,所以能清楚的知道setState更新的过程是异步的。异步更新完了,那同步又是什么鬼。不急,接着往下看:
我们在componentDidMount中代码修改为:

    componentDidMount() {
         setTimeout(() => {
            for (let i = 0; i < 5; i++) {
                console.log(this.state.num);
                this.setState({ num: this.state.num + 1 });
            }
        })
    }
复制代码

效果: image.png 加了setTimeout过后,我们发现setState竟然变成了同步更新,render里面也被执行,也就意味着如果你多次 setState ,会导致多次更新,这样导致了我们dom也会被更新,这是毫无意义并且浪费性能的。
为什么会有这种情况(这里不做详细探究),只大概说一下原因: 因为在react有一套自定义的事件系统和生命周期流程控制,我们只需要知道只要代码进入了 react 的调度流程,那就是异步的。只要你没有进入 react 的调度流程,那就是同步的。什么东西不会进入 react 的调度流程? setTimeout 、setInterval 、在 DOM 上绑定原生事件等。这些操作方式会跳出react这个体系,这些都不会走 React 的调度流程,所以会直接更新this.state,在这种情况下调用 setState  就是同步的。 否则就是异步的。

上面列举了setState同步和异步更新的两种情况,异步这种情况是React的优化手段,,但是显然它也会在导致一些不符合直觉的问题(就如上面第一个异步的例子),所以针对这种情况,React给出了一种解决方案:setState接收的参数还可以是一个函数,在这个函数中可以拿先前的状态,并通过这个函数的返回值得到下一个状态。我们来修改上面的代码:

componentDidMount() {
        for (let i = 0; i < 5; i++) {
            console.log( prevState.num );
            this.setState(prevState => {
                 return {
                     num: prevState.num + 1
                 }
           });
        }
    }
复制代码

这有点像reduce方法,每次循环都能拿到上次执行完的结果(这里是setState合并后的结果);

渲染结果:

image.png 现在就是我们想要的结果了。
react是如何处理异步的呢,又是怎么把上一次合并后结果的放在setState的回调函数里面的。接下来我们来模拟实现一下。

模拟实现

合并setState

// stateOrStateFn可以是一个方法
setState(stateOrStateFn) {
       Object.assign(this.state, typeof stateOrStateFn==='function'? stateOrStateFn(this.prevState):stateOrStateFn);// stateOrStateFn可能是方法,也可能state对象
       renderComponent(this);// 更新组件
    }
复制代码

这种实现,每次执行setState都会renderComponent(显然不符合其更新优化机制),所以我们要合并setState。

任务队列taskEnque

要合并state,我们需要开启一个任务队列taskEnque来保存每次setState过后的值,等所有setState操作完成后合并state并且renderComponent更新dom。
我们知道js可以用数组Array来实现队列(shiftunshift)

const taskEnque = [];
/**
 * @des 保存当前的state状态
 * @params stateOrStateFn传递的state,可以是一个方法
 * @params component为当前的组件
 */
const enqueueSetState = (stateOrStateFn, component) => {
    taskEnque.push({
        stateOrStateFn,
        component
    });
}
复制代码

然后修改最初的setState方法,不让其直接更新state和渲染组件,而是添加在队列中。

 setState(stateOrStateFn) {
    enqueueSetState(stateOrStateFn, this);
    //renderComponent(this);// 更新组件
}
复制代码

保存过后接下来的事就是清空队列并且合并state了。

清空队列合并state

const emptyQueue = () => {
    let item;
    while (item = taskEnque.shift()) {
        const { stateOrStateFn, component } = item;
        // 第一次prevState为空
        if (!component.prevState) {
            component.prevState = Object.assign({}, component.state);
        }
        // 如果stateOrStateFn是一个方法,则执行这个方法
        if (typeof stateOrStateFn === 'function') {
            // prevState用来保存上一次合并的结果
            Object.assign(component.state, stateOrStateFn(component.prevState, component.props));
        } else {
            Object.assign(component.state, stateOrStateFn);
        }
        // 更新prevState
        component.prevState = component.state;
    }
}
复制代码

这里我们只完成了state的更新,组件并没有渲染。重点来了,仔细想一下,我们组件应该什么时候更新,组件怎么更新。组件的更新是不能和清空队列同时进行的,如果同时进行,那就成了同步了。所以组件更新只能等taskEnque被清空也就是state都合并完后才执行更新操作。
所以我们必须要用一个新的值来保存,考虑到存在多个组件情况,我们需要用数组来保存。

    const components = [];
    const enqueueSetState = (stateOrStateFn, component) => {
        // 添加不重复的组件
        if (!components.includes(component)) {
            components.push(component);
        }
        taskEnque.push({
            stateOrStateFn,
            component
        });
    }
复制代码

因为保存的组件在state更新过后只执行一次,所以在enqueueSetState时需要对components去重。
现在需要更新的组件就保存好了,然后放在emptyQueue等state执行完后执行。

    const emptyQueue = () => {
        let item, conpoment;
        // 更新状态
        while (item = taskEnque.shift()) {
            //...
        }
        /** 更新组件 */
        while (conpoment = components.shift()) {
            // forceUpdate更新当前组件(模拟渲染过程)
            conpoment.forceUpdate();
        }
    }
复制代码

延迟执行

现在还有一个重要的事情,就是emptyQueue什么时候执行,我们需要合并一段时间内所有的setState,也就是在一段时间后才执行emptyQueue方法来清空队列,关键是这个“一段时间“怎么决定。
我们利用js的事件队列机制Eventloop。这里循环机制不(想)多(偷)讲(懒),可自行百度。也就是emptyQueue放在下一个任务队里执行,例如:setTimeoutPromisethen方法。这里利用Promise中then微任务来处理。

    const defer = (fn) => Promise.resolve().then(fn);
    const enqueueSetState = (stateOrStateFn, component) => {
        if (taskEnque.length === 0) {
            // 放在下一个任务队列
            defer(emptyQueue);
        }
        // 添加不重复的组件
        if (!components.includes(component)) {
            components.push(component);
        }
        taskEnque.push({
            stateOrStateFn,
            component
        });
    }
复制代码

到这里,我们模拟实现setState就已经完成了,忙活半天,是时候看看效果了。

class Foo extends React.Component {
    state = {
        num: 0,
    }
    componentDidMount() {
        for (let i = 0; i < 5; i++) {
        this.setState(prevState => {
            return {
                num: prevState.num + 1
            }
          });
       }
    }
    // 模拟setState,通过this.forceUpdate更新组件
    setState(stateOrStateFn) {
        enqueueSetState(stateOrStateFn, this);
    }
    render() {
        this.testCount = Math.random();
        console.log('forceUpdate')
        return (
            <div>
                <div>Foo:{this.state.num}</div>
            </div>
        )
    }
}
复制代码

image.png 同样,用另一种方式来调用。

    componentDidMount() {
        for (let i = 0; i < 5; i++) {
           console.log(this.state.num);
           this.setState({ num: this.state.num + 1 });
    }
复制代码

image.png 看效果,和react一样。

function组件

讲完了class组件中的setState原理和实现,现在来看看React >=16.8.0 中hooks里面useState是如何运作的。

基本使用

useState更新渲染组件过程是和class组件中setState更新机制一样的,即能同步也能异步(这里就不举例了)。

const App = () => {
    const [num, setNum] = useState(0);
    const [count, setCount] = useState(0);
    const inputClick = (type) => {
        return ()=>{
            type==='num'?setNum(1+num):setCount(count+1);
        }
    }
    return (<>
        <div>hooks</div>
        <button onClick={inputClick('num')}>addNum </button>
        <span>{num}</span>
        <div style={{height:'10px'}}></div>
        <button onClick={inputClick('count')}>addCount </button>
        <span>{count}</span>
    </>)
}

function renderComponent() {
    ReactDOM.render(
        <App />,
        document.getElementById('hooks-root')
    );
}
renderComponent();
复制代码

按照 React 16.8.0 版本之前的机制,如果某个组件是函数组件,则这个 function 就相当于 Class 组件中的 render() ,不能拥有自己的状态(故又称其为无状态组件,stateless components),所以数据(输入)必须是来自父组件的 props 。而在 >=16.8.0 中,函数组件支持通过使用 Hooks 来为其引入自身状态的能力。如上图:通过useState来为组件注入状态,并且每次更新组件不会重新初始化已有状态。要更新自己的状态只能通过useState返回的第二个参数来进行更改。

模拟实现

React.useState() 里都做了些什么:

  1. 将初始值赋给一个变量我们称之为 state
  2. 返回这个变量 state 以及改变这个 state 的回调函数我们称之为 setState
  3. 当 setState() 被调用时, state 被其传入的新值重新赋值,并且更新根视图

这里可以实现一个基础版了:

function useState(initialValue) {
  let state = initialValue;
  const setState = (newState) => {
    state = newState;
    // 更新组件
    renderComponent();
  };
  return [state, setState];
}
复制代码

4.当组件更新时,初始状态是不会改变的,所以需要把状态放在方法外面去。

let state;
function useState(initialValue) {
  state = state === undefined ? initialValue : state;
  const setState = (newState) => {
    sstate = newState;
    renderComponent();
  };
  return [state, setState];
复制代码

5.现在看起来应该大功告成了,不过还是差点,想一下,一个函数式组件是不是可以有多个状态,每个状态之间都是独立的,也就是useState可以使用多次。怎么才能保证每次的状态互不影响呢?可以用数组来保存状态

let state = [],
    index = 0;
function useState(initialValue) {
  let currentIndex = index;
  state[currentIndex] = state[currentIndex] === undefined ? initialValue : state[currentIndex];
  const setState = (newState) => {
    state[currentIndex] = newState;
    renderComponent();
    index = 0;
  }
  index += 1;
  return [state[currentIndex], setState];
}
复制代码

6.和类组件setState类似,useState中的setState也支持函数来接收上一次的state。

let state = [],
    index = 0;
const defer = (fn) => Promise.resolve().then(fn);
function useState(initialValue) {
    // 保存当前的索引;
    let currentIndex = index;
    if (typeof initialValue === "function") {
        initialValue = initialValue();
    }
    // render时候更新state
    state[currentIndex] = state[currentIndex] === undefined ? initialValue : state[currentIndex];
    const setState = newValue => {
        if (typeof newValue === "function") {
            // 函数式更新
            newValue = newValue(state[currentIndex]);
        }
        state[currentIndex] = newValue;
        if (index!==0) {
            defer(renderComponent);
        }
        index = 0;
    };
    index += 1;
    return [state[currentIndex], setState];
}
复制代码

注意 虽然我们通过这种形式实现了useState,这要求我们保证 useState() 的调用顺序,所以我们不能在循环、条件或嵌套函数中调用 useState() ,因为这些情况下不能保证useState的执行顺序,setState时不能精确的设置state状态,这在React官网还也给出了专门的解释

React真正的实现并不是这样,上面的索引index,React实际上是用链表实现。

最后

至此,本文已结束,如有写的不对或者错误的地方,欢迎指正!
演示代码地址已经整理到了github.com/javascript-… 有需要的同学可以自行clone;

Supongo que te gusta

Origin juejin.im/post/7066656973571391496
Recomendado
Clasificación