react

284 阅读16分钟

# 基础

副作用:就是执行一系列复杂的逻辑比如ajax请求等异步工作,这类逻辑使得函数在每次执行过程中,都会产生不同的变化,这样与外界的交互统称为副作用

## jsx

jsx更像react的createElement的语法糖

return函数在babel中转换成 **react.createElement** ,而这个函数会将元素转化成react element对象,

jsx元素类型 react.createElement 转换后 type 属性

element元素类型 react element类型 标签字符串,例如 div

fragment类型 react element类型 symbol react.fragment类型

文本类型 直接字符串 无

数组类型 返回数组结构,里面元素被react.createElement转换 无

组件类型 react element类型 组件类或者组件函数本身

三元运算 / 表达式 先执行三元运算,然后按照上述规则处理 看三元运算返回结果

函数执行 先执行函数,然后按照上述规则处理 看函数执行返回结果

最终,在调和(render)阶段,这些react element对象的每一个子节点都会形成一个与之对应的fiber对象,这些fiber对象通过sibling,return,child将每一个fiber对象连接起来

//扁平化,规范children数组

const flatChildren = React.Children.toArray(reactElement.props.children)

//循环react的chilren

const newChildren = []

React.Children.forEach(flatChildren, (item)=>{

//检测是否为React element元素

if(React.isValidElement(item)) newChildren.push(item)

})

//把reactElement复制一份,再用新的children属性,从而达到改变render结果的目的

const newReactElement = React.cloneElement(reactElement,{} ,...newChildren )

## 生命周期

类组件才有生命周期函数

分为初始化,挂载阶段,更新阶段,卸载阶段

1. constructor,目前可以不用,constructor本身不属于react生命周期,这个是class初始化函数

2. getDerivedStateFromProps:作用是在props发生变化时更新state ,什么时候起效呢?当传入props时,state变化时,调用forceUpdate时

3. componentWillMount: 在引入新的fiber机制后,由于更新是异步且可以被中断的,这个生命周期可能调用多次,已被废弃

4. render: 该函数返回jsx结构,用于创建react element,该函数只是纯函数,不应在里面产生任何副作用,比如setState或者绑定事件,因为render每次渲染时都会被调用,里面的副作用会多次执行

5. componentDidMount:主要用于组件加载完成时的某些操作。在浏览器下可以认为此时页面已经加载完成,但某些场景下比如react native会由于机器的性能所限,视图可能还在绘制

6. componentWillReceiveProps: 废弃,被getDerivedStateFromProps所替代

7. getDerivedStateFromProps:和挂载阶段的一模一样

8. shouldComponnetUpdate:增加判断条件阻止不必要的渲染

pureComponent就是利用这个函数,在这个函数中对props和state进行**浅比较(仅比较值与引用,不会比较Object的每一项值)**

shouldComponnetUpdate(nextProps, nextState){

if(shadowEqual(nextProps, this.props) || shadowEqual(nextState, this.state)){

return true

}

return false

}

9. componentWillUpdate:被废弃,因为异步渲染中会有暂停更新渲染的情况

10. componentDidUpdate:getSnapshotBeforeUpdate的返回值被当作componentDidUpdate的第三个参数

11. componentWillUnMount:执行清理工作

### ErrorBoundary 错误边界

使用componentDidCatch(error, errorInfo)捕获错误

也可以使用getDerivedStateFromError捕获

#### 类组件和函数组件的区别

1. 类组件是面向对象编程,函数组件是面向函数变成

2. 类组件有生命周期,函数组件没有,但是有useEffect替代。 问题?为什么useEffect能 替代

3. 类组件有继承,函数组件没有,不过继承不推荐使用,灵活性不高。官方更推荐组合优于继承的理念

4. 类组件中更容易上手,但是因为hooks的推出,函数组件更加流行

5. 类组件使用shouldComponentupdate阻断渲染,而函数组件使用memo

## props

getDerivedStateFromProps 监听props变化

props.children可以传组件也可以传方法,所以需要判断

### 注入props

#### 显示注入props

一般直接在标签中直观看见,

#### 隐式注入props

使用reactElement.clone对props.children克隆再注入新的props

function Father(prop){

return React.cloneElement(prop.children,{ mes:'let us learn React !' })

}

## state

类组件的seState:如果是处于react生命周期以及合成事件中,setState可以算是异步,此时isBatchingupdates为true,会将多个setState放入更新队列中,然后一个一个执行。而如果不是在react控制中,比如window.addEventLisnter,setTiemout,setInterval中,此时isBatchingupdates为false,setState是同步的。这么做是为了性能,也是为了保持内部一致性,以及为了后续升级做准备。

ReactDOM.unstable_batchedUpdates装饰成isBatchingUpdates为true

## ref

类组件的ref保存位置在类的instance中维护,而函数组件由于每一次更新都是一个新的开始,所有变量重新声明,为了解决随着ref随着函数组件执行就被重置的问题,就将函数组件的ref保存在对应的fiber上,因为即使函数重新执行,只要组件不被销毁,那么对应的fiber对象就一直存在

### 高阶用法

1. forwardRef:

(1)跨层级获取,可以从祖父组件中获取到孙组件实例,就是把ref通过**props**进行**传递和转发**

(2)高阶组件转发,如果高阶组件没有处理ref,那么ref就指向了高阶组件返回的组件,而不是高阶组件返会的原始组件

2. ref 实现组件通信

(1) 就是通过ref获取实例方法再进行调用,是类组件的应用,具体参考ant 的form

(2) 函数组件 forwardRef + useImperativeHandle

3. 函数组件缓存数据

## hooks

react hooks只能在react的函数组件中使用,并且不能在循环,条件或嵌套函数中使用

之所以不能在函数组件中使用,

1是因为react hooks的设计初衷就是想改变react组件开发模式,

2是因为复杂的组件如果使用类组件来编写,会造成当前类生命周期代码越来越臃肿内聚,缺乏专注,并且本该同一体的发布订阅和取消订阅得写在不同得生命周期中,生命周期与业务逻辑耦合太深,越来越难维护。而且对于类中的this,即为何要对方法进行bind包一层既繁琐也难以理解。而且react也觉得类难以进行前端编译层优化,如常数折叠,内联展开及死码删除。

不能在循环条件或嵌套函数中使用,是因为react hooks的设计是基于数组实现的,在调用时按照顺序加入数组中,如果在循环,条件,嵌套函数中使用,很有可能会造成数组的取值错位,取出错误的hooks。当然,react源码里的这个hooks数组是一个链表。

这些限制会在新手写的时候造成一定的心智负担,新手可能会写错,为了预防这种事情,我们可以使用eslint中引入eslint-plugin-react-hooks插件来完成自动化检查进行规范,限制不在循环,条件或嵌套函数中使用

为什么顺序调用hooks很重要?

在重新渲染时,hooks会被顺序调用来保证正确识别

### useEffect 和 useLayoutEffect区别

他们的共同点都是底层调用了相同的函数签名mountEffectImpl,使用上也可以等价的替换,都是用于处理副作用。

不同点在于useEffect在react渲染过程中是被异步调用的,用于绝大多数场景。而useLayoutEffect会在所有dom变更之后同步调用,主要用于dom操作,调整样式,避免页面闪烁。也正是因为是同步调用,所以要避免在useLayoutEffect中调用计算量很大的任务,造成页面阻塞

## context

创建 const Context = React.createContext({})

提供者 <Context.Provider value={contextValue}> </Context.Provider>

消费者

① 类组件之contextType 方式

② 函数组件之 useContext 方式

③ 订阅者之 Consumer 方式

const ThemeConsumer = ThemeContext.Consumer // 订阅消费者

{ /* 将 context 内容转化成 props */ }

{ (contextValue)=> <ConsumerDemo {...contextValue} /> }

### 高阶用法

1.逐层传递Provider

// 逐层传递Provder

const ThemeContext = React.createContext(null)

function Son2(){

return <ThemeContext.Consumer>

{ (themeContextValue2)=>{

const { color , background } = themeContextValue2

return <div className="sonbox" style={{ color,background } } > 第二层Provder

} }

</ThemeContext.Consumer>

}

function Son(){

const { color, background } = React.useContext(ThemeContext)

const [ themeContextValue2 ] = React.useState({ color:'#fff', background:'blue' })

/* 第二层 Provder 传递内容 */

return <div className='box' style={{ color,background } } >

第一层Provder

<ThemeContext.Provider value={ themeContextValue2 } >

</ThemeContext.Provider>

}

export default function Provider1Demo(){

const [ themeContextValue ] = React.useState({ color:'orange', background:'pink' })

/* 第一层 Provider 传递内容 */

return <ThemeContext.Provider value={ themeContextValue } >

</ThemeContext.Provider>

}

Provider 特性总结:

1 Provider 作为提供者传递 context ,provider中value属性改变会使所有消费context的组件重新更新。

2 Provider可以逐层传递context,下一层Provider会覆盖上一层Provider。

现在使用**useContext**

### 其他api

1. displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

const MyContext = React.createContext(/* 初始化内容 */);

MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中

<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

react-redux 就是通过 Provider 模式把 redux 中的 store 注入到组件中的。

## HOC

两种方式实现

1. 属性代理 组件包裹一层代理组件,代理组件可以强化源组件并直接return 源组件

2. 反向继承 包装后的组件继承了原始组件本身,无须再去挂载业务组件。

### 高阶组件功能说明

1. 强化props

2. 控制渲染

(1)、渲染劫持:HOC 反向继承模式,可以通过 super.nt 对象。 修改渲染树,也是根据判断render() 得到 render 之后的内容,利用这一点,可以做渲染劫持 ,更有甚者可以修改 render 之后的 React eleme需要渲染什么组件,最后使用React.cloneELement(props.render(),element.props, newchild)

(2)、动态加载:

3. 注意事项

(1)、 谨慎修改原型链

(2)、不要在类组件的render函数或者函数组件里使用HOC,每一次触发类组件的render函数或者函数组件执行,都会生成新的HOC返回新组件,react diff会判定两次不是同一个组件,会卸载掉老的组件挂载新的组件。造成性能浪费

例如:

`function Index(){

const WrapHome = HOC(Home)

return

}`

(3)、ref处理

(4)、HOC嵌套顺序:要注意一下包装顺序,越靠近 **Index** 组件的,就是**越内层**的 HOC ,离组件 Index 也就越近。

(5)、继承静态属性,属性代理的方法就是返回了一个新的component,如果给原来的component绑定一些静态属性方法,如果不处理,那么新的component就会丢失,我们可以手动绑定,每个静态属性方法都手动绑定肯定会很累,尤其对于开源的 HOC ,对原生组件的静态方法是未知 ,为了解决这个问题可以使用 hoist-non-react-statics

4. 应用 **权限管理**

# 优化

## 渲染控制

react本身已有很多优化,diff,fiber,时间分片,设立任务优先级,异步调度,我们只需要告诉react哪些需要更新,哪些不需要更新。

所以优化只是告诉react哪些需要更新,哪些不需要更新,所以也就是控制render的方式

1. 从父组件直接隔离子组件的渲染,经典的memo,缓存element对象

(1)、{ useMemo(()=> ,[ numberA ]) }

**useMemo原理**:会记录上一次执行create的返回值,并把他绑定在函数组件对应的fiber上,只要组件不销毁,缓存值就会一直存在,但是依赖项中有一项被改变,就会重新执行create,返回值作为新的值记录到fiber对象上

**react更新原理**:createElement会产生一个新的props作为fiber的penddingProps,在此fiber更新调和阶段,会将上一个老的props与这个pendingProps比较,如果不相同则重新渲染,相同则不重新渲染,因为函数式组件是每一次更新都是执行一次函数,里面的变量其实都是另一个新的变量。useMemo就是利用这个原理,将element缓存起来,此时props也就和fiber上的oldProps指向相同的内存空间,也就跳过本次更新

(2)、useCallback

export default function (){

const callback = React.useCallback(function handerCallback(){},[])

return

}

**useMemo和useCallback区别**

useMemo需要执行第一个函数,返回值为缓存的内容,更像是缓存了一段执行逻辑后返回的结果

useCallback第一个参数就是缓存的内容

2. 组件从自身判断控制是否render,比如PureComponent,shouComponentUpdate

(1)、PureComponent:浅比较state和props是否相等。浅比较只会比较基本的数据类型,对于修改了对象的属性是不会促使组件更新的。可以使用浅拷贝解决这个问题。

changeObjNumber=()=>{

const { obj } = this.state

obj.number++

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

}

**PureCOmponent原理**

(2)、shouldComponentUpdate** 已废弃

(3)、React.memo:React.memo(COmponent, compare),第二个参数是一个函数,可以根据一次更新中的props是否相同决定原始组件是否重新渲染, 返回 true 组件不渲染 , 返回 false 组件重新渲染,memo 当二个参数 compare 不存在时,会用浅比较原则处理 props ,相当于仅比较 props 版本的 pureComponent

## 渲染调优

1. 懒加载和异步渲染,两者一起使用效果更加

(1)、 异步渲染

Suspense

fallback属性就是在loading的时候展示的内容,suspense的子组件就是异步组件

(2)、 懒加载

Lazy

2. 渲染错误边界

(1)、componentDidCatch修改state

(2)、getDerivedStateFromError替代componentDidCatch,由于是静态方法,不能直接修改state,所以getDerivedStateFromError返回的值可以合并到state

3. diff中key的使用

合理的使用key可以精准的找到用于新节点复用的老节点

## 细节优化

1. 防抖节流,可以配合使用useCallback或者useMemo缓存来提高性能

2. 按需引入,

# 原理篇

## 调度(schduler)和时间分片

时间片:浏览器在一帧中做的事情,处理事件,执行js,执行requestAnimation,布局,绘制,如果没有其他事,浏览器就会进入空闲时间。

javascript线程和gui线程是互斥的,如果执行js线程太久,就会导致浏览器没有时间进行样式绘制导致卡死。所以react使用messageChannel模拟一个requestIdelCallback,将同步任务变为可中断的异步任务,一帧一帧的向浏览器请求,等到浏览器有空闲时间后可以执行。

因爲requestIdelCallback目前只有chrome支持,所以react需要寫一個模拟requestIdelCallback。模拟一个requestIdelCallback需要实现两个功能。需要能让出主线程执行,让浏览器去渲染线程,需要保证每一个事件循环只执行一次,执行完事件循环后,需要请求下一个时间片。能这么完成的只有宏任务。排除setTiomeout是因为递归执行时会有4ms的误差,所以选择MessageChannel。

**调度**:通过时间分片获取到空闲时间片,接收到更新,并执行高优先级任务更新。**最后将其转交给reconciler**

### MessageChannel

### 异步调度原理

## 调和器(reconciler)

负责找出变化的组件,会为发生变化的虚拟dom打上增删改查的标记,当所有的组件都完成了reconciler的工作,就会统一交给rederer。

新的reconciler从递归变为可中断的循环过程, 每次循环都会调用shouldYield判断当前是否有剩余时间

fiber reconciler的特点是:协作多任务模式,基于循环遍历计算diff

### render阶段

构建workInProgress树来计算diff

#### beginWork

#### completeWork

## 渲染器(renderer)

接收到Reconciler的通知,对被添加增删改查标记的虚拟dom执行对应的操作, 负责将变化的组件渲染到页面上

### commit阶段

处理effect列表,这里的effect列表包含了diff更新dom树,回调生命周期,响应ref等

#### before mutation

整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。

commitBeforeMutationEffects:

1. 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑。

2. 调用getSnapshotBeforeUpdate生命周期钩子。

3. 调度useEffect。

#### mutation

#### layout

react渲染过程大致一致,但协调并不相同,以react16为分界线,分为stack reconciler和fiber reconciler,通常包含了diff算法和公共逻辑,stack reconciler的核心调度方式是递归,基本调度单位是事务,他的事务基类是transaction,这里的事务是从后端开发中加入的概念。在react16以前,挂载主要由reactMount(reactMount.render()完成实际初始化React组件的整个过程),更新由reactUpdate完成,模块之间相互分离。在react16之后,协调改为了fiber reconciler,它有两个特点,第一个是多任务协作模式,在这个模式下,线程会定时放弃自己的运行权力,交还给主线程,通过requestidelCallback实现。第二个特点是策略优先级,调度任务通过标记tag的方式分优先级执行,如动画。fiber reconciler的基本单位是fiber,fiber基于react element提供二次封装,提供了指向父、子、兄弟节点的引用,为diff工作的双向链表实现了基础。在新的架构下,整个生命周期划分成了render和commit两个阶段。render阶段的特点是可中断停止无副作用,主要是通过构造workInProgress树计算diff,以current树为基础,将每个fiber作为一个基本单位,自下而上的逐个节点检查并构造workInProgress树,这个过程不再是递归,而是基于循环来完成。在执行上,requestidelCallback来调度执行每组任务,每组的每个计算任务被称为work。每个work完成后确认是否有优先级更高的work需要插入,如果有,就让位,如果没有,就继续。优先级通常是标记为动画会先做处理,每完成一组后,将调度全交还给主线程,等下一次requestIdelCallback调用再继续调用workInProgress树。在commit阶段,主要处理effect列表,effect列表包含了根据diff更新dom树,回调生命周期。这个阶段是的同步执行的,不可中断的,所以不要在componentDidMount,componentDidUpdate,componentWillUnmount执行重度消耗算力的任务。

在管理后台,h5展示页等,两者性能差距并不大,但是在动画画布及手势等场景下,stack reconciler的设计会占用主线程,造成卡顿,而fiber reconciler会带来高性能的表现。

## 事件系统

为什么要有一套自己的事件系统?因为为了抹平浏览器之间的差异,其次,react事件是绑定在document或者容器上,这个减少了不可控的情况,但同时也需要配react模拟出一套捕获,事件源,冒泡的阶段,也包括重写事件源对象event

阻止默认行为: e.preventDefault()

阻止冒泡:e.stopPropagation(),17之前因为将事件绑定在了document上,造成了**重合**,导致无法使用stopPropagation进行阻止冒泡

1. 如何阻止冒泡

当使用stopPagation()来阻止冒泡时,那么事件源里将会有一个状态证明此次事件已经停止冒泡,那么下次遍历的时候,react内部通过event.isPropagationStopped()函数来判断是否需要阻止,如果是,就会break跳出循环,其他的事件函数将不再执行

```function runEventsInBatch(){

const dispatchListeners = event._dispatchListeners;

if (Array.isArray(dispatchListeners)) {

for (let i = 0; i < dispatchListeners.length; i++) {

if (event.isPropagationStopped()) { /* 判断是否已经阻止事件冒泡 */

break;

}

dispatchListeners[i](event) /* 执行真正的处理函数 及handleClick1... */

}

}```

### 合成事件

react中的事件比如onChange是blur,change,focus事件合成的,每个事件都对应着一个原生事件数组

#### 事件插件机制

不同的事件都有其对应的事件插件进行处理,不同的事件都有不同的逻辑,也因为事件和事件源都是react自己合成的,所以需要事件插件机制

### 事件绑定

react会将事件保存在对应dom节点的fiber的memoizedProps属性上memoizedProps = {onClick: handleClick},而挂在在document的事件处理函数并不是写的事件方法名如handleClick,而是dispatchEvent,由dispatchEvent进行统一处理

### 源码流程

1. react的初始化真实dom的时候,会用一个指针指向fiber,fiber有一个statenode指向真实dom,

## hooks原理

hooks 可以作为函数组件本身和函数组件对应的 fiber 之间的沟通桥梁。因为函数式组件

## virtual dom

React.createElement() 根据真实dom模拟返回的一个对象就是虚拟dom。react会持有一颗虚拟dom树,当状态发生改变时,会触发虚拟dom树的修改,再以此为基础修改真实dom

跨平台成本低

有效避免xss攻击,因为保留了danderouslySetInnerHTML 的api

内存占用高

无法进行极致优化

### dom diff

大量的操作真实dom容易引起网页性能下降,这是React基于虚拟dom的diff处理与批处理操作可降低dom的操作范围和频次,提升页面性能

## react渲染流程

# 生态篇

## redux React-Redux

为什么需要状态管理库?

**因为context很难追踪数据源以及确认变动点**

redux是状态管理js库,而React-Redux是沟通react和redux的桥梁,react-redux负责将redux的store注入react应用中,并且派发store的更新消息到每一个需要状态的组件(对于完整的状态管理生态,大家可以尝试一下 dvajs)

react应用dispatch到redux, react-redux订阅redux的消息并将state注入到react应用

1. redux(action:触发事件,state:管理UI状态,reducer:处理业务逻辑)

(1)、是单向数据流,并且state**只读**,只能通过action进行更改,action会执行每个reducer,reducer是一个纯函数,里面不要执行任何副作用,返回值作为新的state,state改变会触发store中的subscribe(订阅)

(2)、采用发布订阅思想,redux有一个store,里面存放了状态信息,以及改变store的方法dispath,和订阅sotre改变的subscribe

(3)、运用了中间件思想,可以用来**强化distpatch**。因为传统的dispath不支持异步,所以一些redux-thunk,redux-actions中间件,本质上应用了函数柯里化

缺点:1.书写不方便,需要一直写Store.subScribe以及unsubScribe

2. 更改了某个组件a需要的状态a,可能会导致需要b状态的b组件也进行更新

2. react-redux

为了改变以上缺点react-redux应运而生

原理:

(1)、 Provider注入Store

(2)、

(3)、

### 中间件

1. redux-thunk

//如果是数组或者是promise就不执行,直接执行下一个

function createthunkMiddleware(extraArgument){

return ({dispatch, getState}) => (next) => (Action) => {

if(typeof Action === 'function'){

return Action(dispatch, getState, extraArgument);

}

return next(Action)

}

}

const thunk = createthunkMiddleware()

thunk.withExtraArgument = createthunkMiddleware

export default thunk

2. redux-saga

## mobx

## react-router

# react渲染流程