yaofly2012 / note

Personal blog
https://github.com/yaofly2012/note/issues
44 stars 5 forks source link

React如何渲染 #206

Open yaofly2012 opened 3 years ago

yaofly2012 commented 3 years ago

常见问题:

  1. render函数不能返回undefined
  2. 父组件re-render时,即使子组件props没有变化,也会触发re-render;
  3. React必须使用setState更新state,而Vue却可以直接通过赋值方式更新。
  4. 双向绑定和单向数据流 双向绑定:视图和数据直接是一致的。 单向数据流:只组件直接的数据传输。 Vue里既有双向绑定,又有单向数据流。而React却只有单向数据流。

组件化

鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM

渲染和更新

  1. 重新渲染整个组件 在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。看似这样容易造成不必要的渲染,从而造成性能问题,但是大部分情况下性能不是问题。如果真的遇到性能问题可以采用优化手段避免不必要的渲染?

    • PureComponent/React.memo
    • 手动实现shouldComponentUpdate

    主要表达如何构建新vDOM树。即使重新渲染整个组件子树,但性能一般也不是问题,因为这些都是JS执行的,并没有操作DOM。

  2. 局部渲染 对比新旧vDOM树,更新变化的。 主要表单如何更新真实DOM。

vDOM如何跟真实DOM关联的? 利用结构关系,vDOM和生成的DOM结构是一致的。

列表渲染为啥需要key ?

queues a state update props.children is always a new reference

同步&异步

Because of this, React will always run renders in commit-phase lifecycles synchronously

同步就会立马拿到最新值吗?

reconcilers

unstable_batchedUpdates

  1. react-three-fiber
  2. ink

    跨组件层级数据传递

  3. context
  4. provide/inject

设计模式

  1. 发布者-订阅者模式

参考

  1. A (Mostly) Complete Guide to React Rendering Behavior
  2. Vue 深入响应式原理
  3. 语雀(三十二)Vue和React实现数据监听原理比较
yaofly2012 commented 3 years ago

渲染行为

一、什么是渲染(Rendering)?

1.1 什么是渲染(Rendering)

组件根据当前的propsstate生成vDOM树的过程,即组件转成vDOM的过程。

1.2 渲染过程(render process)

  1. 从根组件开始构建vDOM树;
  2. 对比新旧vDOM树,收集更新;
  3. React根据收集的更新以同步方式修改DOM。

注意整个过程都是同步的,不过在Concurrent Mode 下会存在异步场景(下面说到)。

1.3 渲染阶段(render phases)

整个渲染过程大致分为三个阶段(主要是1,3):

  1. Render 阶段
  2. Pre-commit 阶段
  3. Commit 阶段

每个阶段做的事情,可以做的事情,不可以做的事情。

1. Render阶段

Concurrent Mode Render阶段的生命周期函数可能会被终止,暂停,重新执行,所以这些生命周期函数必须是Pure,保证是个纯函数,不能有副作用。

2. Pre-commit阶段

3. Commit阶段

DOM已经被修改了,refs也已经更新了,但是因为调用栈还没空,浏览器无法渲染DOM。 Commit阶段的生命周期函数可以访问最新的DOM(但用户还没看到)和refs了,并且触发执行都是同步的。

二、同步渲染、异步渲染

React文档里提到State Updates May Be Asynchronous

2.1 异步渲染

异步渲染只是指批处理state更新,只触发一次渲染。 注意:更新state,计算最新的state,触发相关生命周期函数和render函数执行这整个过程都是同步的,这也是React下一步要优化的点。

批处理 & state更新队列

在一个调用栈里(比如事件处理函数里)多次setState调用会统一批处理。等函数执行完后,在批处理state更新,并计算最新的state

const [counter, setCounter] = useState(0);

    const onClick = async () => {
        console.log('onClick begin')
        setCounter(2);
        setCounter(3);
        console.log('onClick end')
    }
    console.log('render ', counter)
    return (<div onClick={onClick}>Count: {counter}</div>)

点击看下输出:

onClick begin onClick end render 3

函数组件和class组件批处理执行时机

class组件中当回调函数执行完毕后会立马执行批处理,并计算最新的state

export default class Default extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        }
    }

    componentDidMount() {
        console.log('componentDidMount begin')        
        this.setState((state) => {
            console.log('componentDidMount.setState.callback: ', state.count)
            return  {
                count: state.count + 1
            }
        })
        console.log('componentDidMount: ', this.state.count)
        this.setState((state) => {
            console.log('componentDidMount.setState.callback: ', state.count)
            return  {
                count: state.count + 2
            }
        })
        console.log('componentDidMount: ', this.state.count)
        Promise.resolve().then(() => {
            console.log('componentDidMount done in promise');
        })
    }

    componentDidUpdate() {
        console.log('componentDidUpdate')        
    }

    render() {        
        const { count } = this.state;
        console.log('render: ', count);

        return (
            <div>
                <p>Count: {count}</p>
            </div>
        )
    }
}

组件渲染后控制台输出: image

但是对于函数组件情况复杂些,存在立即计算懒计算策略,并且默认采用立即计算策略。想知道原因就戳解密React state hook

异步原理(本质应该是批处理原理)

内部利用unstable_batchedUpdates实现的,并且这个函数是对外导出的。

2.2 同步渲染

同步渲染则是每次调用setState都会同步的计算最新的state,如果state发生变化则触发render以及相关生命周期函数。 注意:整个过程都是同步的。

const [counter, setCounter] = useState(0);

const onClick = async () => {
  setCounter(0);
  setCounter(1);

  const data = await fetchSomeData();

  setCounter(2);
  setCounter(3);
}

上面代码会触发3次渲染。

同步渲染里批处理

  1. 如果是同步渲染,则在处理函数里最好只触发一次状态更新。等确定最终值时再调用setState。 就如同react-dom函数unstable_batchedUpdates的实现的注释说的那样,此时:

    Batching should be implemented at the renderer level。

  2. 使用unstable_batchedUpdates函数

    
    import { unstable_batchedUpdates } from 'react-dom'

const onClick = async () => { setCounter(0); setCounter(1);

const data = await fetchSomeData(); unstable_batchedUpdates(() => { setCounter(2); setCounter(3); }) }



## 2.3 总结
### 1. 批处理的场景
- 生命周期函数
- React事件处理函数
- `render`函数

### 2. 不会进行批处理的场景
非上面的场景,如:
1. 异步回调函数
 - `setTimeout`/`setInterval`回调函数
 - `Promise.then`回调函数
 - `XMLHttpRequest`回调函数
2. 非React事件处理函数

因为这些函数执行不是React能控制的(脱离的React的上下文),无法进行批处理优化。

# 三、跳过DOM渲染
## 3.1 现状
Commit阶段的生命周期函数里(`componentDidMount`, `componentDidUpdate`, `useLayoutEffect`)调用状态更新会跳过当前的DOM渲染。
React依旧会进行批处理,计算最新的`state`,触发渲染执行相关的生命周期函数(也包含`useLayoutEffect `的回调函数)。 但是会跳过当前组件渲染:
1. 整个过程都是同步进行的,会阻塞浏览器渲染,也就是说最终浏览器只会渲染最终的`state`值,会跳过中间的`state`值。
2. 也会跳过`useEffect`回调函数;
3. 跳过子组件调用。

`class`组件和[函数组件效果](https://github.com/yaofly2012/note/issues/204)一样。

## 3.2 效果
>Immediately re-render with the updated data
`render`函数里更新`state`也会同样的效果。

React防止阻塞浏览器渲染太久,会设置个重新渲染触发次数,超过了会报错:
>Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

## 3.3 用途
1. Hooks可用于实现[`getDerivedStateFromProps`](https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops);
2. `state`依赖DOM的属性,在Commit阶段利用`refs`获取DOM的属性,让后再更新`state`。

# 参考
1. [React lifecycle methods diagram.](https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/)
2. [解密React state hook](https://github.com/yaofly2012/note/issues/204)
3. [Simplifying state management in React apps with batched updates](https://blog.logrocket.com/simplifying-state-management-in-react-apps-with-batched-updates/)
4. [Batch Your React Updates](https://dev.to/raibima/batch-your-react-updates-120b)