React Fiber

255 阅读6分钟

react fiber

为了使 react 渲染的过程中可以被中断,可以将控制权交给浏览器。可以让位给高优先级的任务,浏览器空闲后再恢复渲染。

对于计算量比较大的 js 计算或者 vdom 的计算,就不会显得特别卡顿,而是一帧一帧的有规律的执行任务。

const tasks = []

function run() {
    let task
    while (task = tasks.shift()) {
        execute(task) // 10s
    }
}

generator.

const tasks = [] // 10 个 task

function * run() {
    let task
    while(task = tasks.shift()) {
        if (hasHighPriorityTask()) { // 有高优任务
            yield // 中断执行
        }
        execute(task) // 10s
    }
}

const iterator = run()
iterator.next() // 中断恢复
  1. generator 有类似功能,为什么不直接使用
  • 要使用 generator,需要将涉及到的所有代码都包装成generator * 的形式,非常麻烦,工作量很大(在原有的基础上迭代)
  • generator 内部是有状态的
function * doWork(a, b, c) {
    const x = doExpendsiveWorkA(a)
    yield
    const y = doExpendsiveWorkB(b)
    yield
    const z = doExpendsiveWorkC(c)
    return z
}

我们已经执行完了 doExpendsiveWorkA 和 doExpendsiveWorkB,还未执行 doExpendsiveWorkC

如果此时 b 被更新了,那么在新的时间分片里,我们只能沿用之前获取到的 x,y 结果。

  1. 如何判断当前是否有高优的任务呢?

当前的 js 环境其实并没有办法去判断是否有高优先级的任务。

只能约定一个合理的执行时间,当超过了这个执行时间,如果任务仍然没有执行完成,中断当前任务,将控制权交还给浏览器。

每秒60帧,1000ms/60f=16ms/f

requestIdleCallback.

使浏览器在**有空的时候**去执行我们的回调。这个回调会穿入一个参数,表示浏览器有多少时间供我们执行任务。
  • 浏览器在一帧内要做什么事情

    处理用户输入事件 JS 的执行 requestAnimation 调用 布局 layout 绘制 paint

    16ms - 10ms 6ms -> requestIdleCallback

  • 浏览器很忙怎么办

    requestIdleCallback timeout 参数,如果超过这个timeout后,回调还没有被执行,那么会在下一帧强制执行回调。 如 100ms 16ms 16ms 16ms ... 16ms -> 100ms强制执行回调。

  • 兼容性?

    requestIdleCallback 兼容性很差,通过 messageChannel 模拟实现了 requestIdleCallback 的功能。

  • timeout 超过后就一定要被执行吗?

    task console.log(); requestIdleCallback timeout 100ms

    不是的,react 里预订了5个优先级的等级

    • Immediate 最高优先级,这个优先级的任务应该被马上执行不能中断
    • UserBlocking 这些任务一般是用户交互的结果,需要即时得到反馈
    • Normal 不需要用户立刻感受到的变化,比如网络请求
    • Low 这些任务可以延后,但是最终也需要执行
    • Idle 可以被无限期延后

平时用过高阶组件吗?什么是高阶组件?高阶组件能用来做什么?

简称 HOC, High Order Component

  1. 是一个函数
  2. 入参:原来的 react 组件
  3. 出参:新的 react 组件
  4. 是一个纯函数,不应该有任何的副作用.
function helloWorld() {
    const myName = sessionStorage.getItem('name')
    console.log(`Hello world, my name is ${myName}`)
}

function byeWorld() {
    const myName = sessionStorage.getItem('name')
    console.log(`Bye world, my name is ${myName}`)
}

HOC

function helloWorld(myName) {
    console.log(`Hello world, my name is ${myName}`)
}

function byeWorld(myName) {
    console.log(`Bye world, my name is ${myName}`)
}
function wrapWithUserName(wrappedFunc) {
    const tempFunction = () => {
        const myName = sessionStorage.getItem('name')
        wrappedFunc(myName)
    }
    return tempFunction
}

const wrappedHello = wrapWithUserName(helloWorld)
const wrappedBye = wrapWithUserName(byeWorld)

wrappedHello()
wrappedBye()

怎么写一个高阶组件?

  1. 普通方式(同上)

  2. 装饰器

不太受部分人推崇,因为推崇尽量减少 class 语法

接收一个原有的函数,在原有的函数做一个类似于劫持的东西去做自己的各种事情,返回一个新的函数

  1. 多个高阶组件的组合
// hoc 
import React, { Component} from 'react'

interface State {
    name: string
}

export const decoratorWithNameHeight = (height?: number) => {
    return (WrappedComponent: any) => {
        return class extends Component<any, State> {
            public state: State = {
                name: ''
            }

            componentWillMount () {
                let userName = localStorage.getItem('myName')
                this.setState({
                    name: userName || ''
                })
            }

            render () {
                return (
                    <div>
                        <WrappedComponent name={this.state.name} {...this.props} />
                        <p>身高为{height || 0}</p>
                    </div>
                )
            }
        }
    }
}
export const decoratorWithNameWidth = (width?: number) => {
    return (WrappedComponent: any) => {
        return class extends Component<any, any> {
            render () {
                return (
                    <div>
                        <WrappedComponent {...this.props} />
                        <p>宽度为{width || 0}</p>
                    </div>
                )
            }
        }
    }
}
// components
import React, { Component } from 'react'

import { decoratorWithNameHeight, decoratorWithNameWidth } from '../../hoc/index'

interface Props {
    name: string
}
@decoratorWithNameWidth(100)
@decoratorWithNameHeight(180)
class UglifyWorld extends Component<Props, any>{
    render() {
        return (
            <div>Bye uglify world! My name is {this.props.name}</div>
        )
    }
}

export default UglifyWorld
// 页面输出 
// Bye uglify world! My name is xxx
// 宽度为 xxx
// 身高为 xxx

高阶组件能用来做什么?

  1. 属性代理 1.1 操作 props 1.2 操作组件实例

    // refHoc.tsx
    import React, { Component} from 'react'
    
    export const refHoc = () => {
        return (WrappedComponent: any) => {
            return class extends Component<any, any> {
    
                ref:any = null
    
                componentDidMount() {
                    console.log(this.ref.state)
                }
    
                render () {
                    return (
                        <WrappedComponent 
                            {...this.props}
                            ref = {(instance: any) => {
                                this.ref = instance
                            }}
                        />   
                    )
                }
            }
        }
    }
    // demo
    import React, { Component} from 'react'
    import { refHoc } from '../hoc/refHoc'
    
    interface Props {
        name?: string
    }
    
    interface State {
        width?: number
        height?: number
    }
    
    @refHoc()
    class RefDemoComponent extends Component<any, any> {
        state: State = {
            width: 100,
            height: 100
        }
                
    
        render () {
            return (
                <div>Bye uglify world! My name is {this.props.name}</div>
            )
        }
    }
    
    export default RefDemoComponent
    // 输出:Bye uglify world! My name is xxx
    // 组件实例,输出 Object {width:100, height:100}
    
  2. 继承/劫持

// hijack.tsx
import React, { Component} from 'react'

export function HiJackHoc<T extends {new(...args:any[])}>(
    component: T
) {
    return class extends Component {
        handleClick() {
            console.log(this.handleClick)
            super.handleClick()
            alert('handleClick被我劫持了')
        }

        render() {
            const parent = super.render()
            return React.cloneElement(parent, {
                onClick: () => this.handleClick()
            })
        }
    } 
}
// test
import React, { Component} from 'react'
import { HiJackHoc } from '../../hoc/hijack'

interface Props {
    name?: string
}

interface State {
    width?: number
    height?: number
}

@HiJackHoc
class RefDemoComponent extends Component<any, any> {
    state: State = {
        width: 100,
        height: 100
    }
            
    handleClick() {
        this.setState({
            width: this.state.width + 1
        })
    }

    render () {
        return (
            <div onClick = {() => this.handleClick()}>点我呀 {this.state.width}</div>
        )
    }
}

export default RefDemoComponent

什么是 React Hooks? React Hooks 有什么优势?

可以不写 class 的情况下,使用 state 和 其他 react 特性。

useState useEffect useMemo

为什么不写 class 而转向了 hooks 写法?

React Hooks 有什么优势?

class 的缺点

  1. 组件间的状态逻辑很难复用

    组件间如果有 state 的逻辑是相似的,class 模式下基本上是用高阶组件来解决的。

    虽然能够解决问题,但是我们需要在组件外部再包一层,会导致层级非常冗余。

  2. 复杂业务的有状态组件会越来越复杂

  3. 监听和定时器的操作,被分散在多个区域

    didMount document.addEventListener('xxx')

    destroyed document.removeEventListener('xxx')

  4. this 指向问题

    class App extends React.Component<any, any> {
        constructor(props) {
            super(props)
            this.state = {
                num: 1,
                title: '123'
            }
            this.handleClick2 = this.handleClick1.bind(this)
        }
    
        handleClick1() {
            this.setState({
                num: this.state.num + 1
            })
        }
    
        handleClick3 = () => {
            this.setState({
                num: this.state.num + 1
            })
        }
    
        render() {
            return (
                {/* render 里放的每次都会返回一个新函数,造成 ChildComponent 每次都会重新渲染 */}
                <ChildComponent onClick = {this.handleClick1.bind(this)}></ChildComponent>
                <ChildComponent onClick = {() => this.handleClick1() }></ChildComponent>
                {/* 解决上述重新渲染的问题 */}
                <ChildComponent onClick = {this.handleClick2}></ChildComponent>
                <ChildComponent onClick = {this.handleClick3}></ChildComponent>
            )
        }
    }
    

Hooks 的优点

解决 class 的缺点

  1. 利于业务逻辑的封装和拆分,可以非常自由的组合各种自定义 hooks。(自定义 hooks:自己封装的用到了react hooks的公共逻辑)

  2. 可以在无需修改组件结构的情况下,复用状态逻辑

  3. 定时器、监听等等都被聚合到同一块代码下

useEffect(() => {
    const timer = setInterval(() => {
        // code
    }, 1000)
    return () => clearInterval(timer)
}, [])

Hooks 使用注意事项

  1. 只能在函数内部的最外层调用 hook,不要在循环、条件判断或者子函数中调用。

  2. 只能在 React 的函数组件中调用 hook,不要在其他的 js 函数中调用。

  3. 为什么 hooks 不能在循环、条件判断中调用?

  4. 为什么 useEffect 的第二个参数是空数组,就相当于 componentDidMount 只执行一次?

  5. 自定义的 hook 怎样操作组件的?

手写代码实现 useState

const [count, setCount] = useState(0)

setCount(1)
import React, { Component} from 'react'
import ReactDOM from 'react-dom'

function Counter() {
    const [count, setCount] = useState(0)
    const [name, setName] = useState('')

    const onClick = () => {
        setCount(count + 1)
    }

    const onClickName = () => {
        setName(`${name} ${Math.random()}`)
    }

    return (
        <div>
            <div>{count}</div>
            <button onClick={onClick}>点击修改count</button>
            <div>{name}</div>
            <button onClick={onClickName}>点击修改name</button>
        </div>
    )
}
// 实际上是用的是单向链表,不是数组
let stateArray: any[] = []
let cursor = 0
// let state:any

function useState<T>(initialState: T): [T, (newState: T) => void]{
    const currentCursor = cursor
    stateArray[currentCursor] = stateArray[currentCursor] || initialState
    // state = state || initialState

    // function setState(newState: T) {
    //     state = newState
    //     render() 
    // }
    function setState(newState: T) {
        stateArray[currentCursor] = newState
        render()
    }

    ++ cursor

    // return [state, setState]
    return [stateArray[currentCursor], setState]
}

export function render() {
    ReactDOM.render(
        <React.StrictMode>
            <Counter />
        </React.StrictMode>,
        document.getElementById('root')
    )
    cursor = 0
}

手写代码实现 useEffect

import React, { Component, useState} from 'react'
import ReactDOM from 'react-dom'

function CounterEffect() {
    effectCursor = 0
    
    const [count, setCount] = useState(0)
    const [count1, setCount1] = useState(0)

    useEffect(() => {
        console.log(`count 发生了改变 ~~~ ${count}`)
    }, [count])

    useEffect(() => {
        console.log(`count1 发生了改变 ~~~ ${count1}`)
    }, [count1])

    const onClick = () => {
        setCount(count + 1)
    }

    const onClick1 = () => {
        setCount1(count1 + 1)
    }

    return (
        <div>
            <div>{count}</div>
            <button onClick={onClick}>点击修改count</button>
            <div>{name}</div>
            <button onClick={onClick1}>点击修改count1</button>
        </div>
    )
}
const allDeps: Array<any[] | undefined> = [] // 二维数组
let effectCursor: number = 0
function useEffect(callback: () => void, depArray?: []) {
    if (!depArray) {
        callback()
        allDeps[effectCursor] = depArray
        effectCursor ++ 
        return
    }

    const deps = allDeps[effectCursor]
    const hasChanged = deps 
        ? depArray.some((el, i) => el !== deps[i]) 
        : true
    if (hasChanged) {
        callback()
        allDeps[effectCursor] = depArray
    }
    effectCursor ++ 
}


export function render() {
    ReactDOM.render(
        <React.StrictMode>
            <CounterEffect />
        </React.StrictMode>,
        document.getElementById('root')
    )
}