掘金小课《React进阶实战指南》笔记
Posted coderlin_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了掘金小课《React进阶实战指南》笔记相关的知识,希望对你有一定的参考价值。
阅读了掘金小课《React进阶实战指南》,做的笔记加自己的一些总结。部分资源来自掘金小课《React进阶实战指南》
认识JSX
Jsx统统被转为React.createElement,
createElement参数:
- 第一个参数:如果是组件类型,会传入组件对应的类或函数;如果是 dom 元素类型,传入 div 或者 span 之类的字符串。
- 第二个参数:一个对象,在 dom 类型中为标签属性,在组件类型中为 props 。
- 其他参数:依次为 children,根据顺序排列。
如
<div>
<TextComponent />
<div>hello,world</div>
let us learn React!
</div>
转化成
React.createElement("div", null,
React.createElement(TextComponent, null),
React.createElement("div", null, "hello,world"),
"let us learn React!"
)
vdom样子:
JSX转化规则,经过createElement转化后
-
element类型,如div => reactElement类型,type为’div’
-
文本类型,如’aaa’=> ‘aaa’,直接转为字符串
-
数组类型,如[<div>,<div>] => [type: div…, type: div…],转化后是element的数组
-
组件类型,转为react element,type是函数或者类本身
-
三元运算 / 表达式,先运算,再按照上述规则转化
-
函数执行,先执行,再按照上述规则转化
Fiber
针对不同的react element,Fiber的tag也不同
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
Component
-
如果没有在 constructor 的 super 函数中传递 props,那么接下来 constructor 执行上下文中就获取不到 props ,这是为什么呢?
- 答:绑定 props 是在父类 Component 构造函数中,执行 super 等于执行 Component 函数,此时 props 没有作为第一个参数传给 super() ,在 Component 中就会找不到 props 参数,从而变成 undefined ,在接下来 constructor 代码中打印 props 为 undefined 。
-
函数组件和类组件本质的区别是什么呢
- 对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。
-
组件通信方式:
- props
- eventbus
- redux
- Localstorage
- context
- ref
玄学State
- state同步异步
- 在18之前,对于setState,react会采用批量更新的方式更新state,通过一个开关来实现批量更新。所以处于微任务或者红任务的setState无法被合并。
- 在18之后,对于setState,react批量更新的方式不再是通过开关,而是通过事件的优先级,同一优先级的事件会被统一处理,处于微任务和红任务之间的setState也会被合并。
- 类组件中的
setState
和函数组件中的useState
有什么异同?- 首先从原理角度出发,setState和 useState 更新视图,底层都调用了scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。
- 在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。
- 但是useState的setState,在创建第一个update的时候,会先执行得到新的state,并且进行比较,若值相同,不会开启新的一轮调度
- setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。
深入Props
- props能做什么?
- 父组件 props 可以把数据层传递给子组件去渲染消费。另一方面子组件可以通过 props 中的 callback ,来向父组件传递信息。还有一种可以将视图容器作为 props 进行渲染。
- 对于更新机制,在vue中,基于劫持数据的变化并且通知订阅者通知其更新,但是在react中,无法检测到数据更新波动的范围,所以props成为了作为组件是否更新的重要准则,变化及更新,所以后来出现了PureCompoennt和memo等优化手段。
理解生命周期
mount阶段
- constructor
- getDerivedStateFromProps(nextProps,prevState)
- componentWillMount(没有getDerviedStateFromProps或者没有getSnapShotBeforeUpdate才会执行),不再推荐
- render函数执行(beginWork)
- componentDidMount执行(layout阶段)
update阶段
-
componentWillReceiveProps(没有getDerviedStateFormProps的时候执行)
-
getDerviedStateFormProps
-
shouldComponentUpdate
-
componentWillUpdate
-
render
-
getSnapShotBeforeUpdate(在dom更新前执行,before-mutation阶段)
-
componentDidUpdate
unmount阶段
- componentWillUnMount(mutation阶段)
getDerivedStateFormProps(nextProps,prevState)
类的静态属性,接受新的props和老的state。返回值会合并到最新的state去。
- 在初始化和更新阶段,接受父组件的 props 数据, 可以对 props 进行格式化,过滤等操作,返回值将作为新的 state 合并到 state 中,供给视图渲染层消费。
- 只要组件更新,就会执行
getDerivedStateFromProps
,不管是 props 改变,还是 setState ,或是 forceUpdate 。 - getDerivedStateFromProps作用:
- 代替 componentWillMount 和 componentWillReceiveProps
- 组件初始化或者更新时,将 props 映射到 state。
- 返回值与 state 合并完,可以作为 shouldComponentUpdate 第二个参数 newState ,可以判断是否渲染组件。
componentWillMount & componnetWillReceiveProps
- 16.3之后,componentWillMount和componentWillUpdate和ComponentWillReceiveProps都带上了UNSAFE标识。因为这三个函数,都是在类组件render之前执行的,对于render,可以通过shouldUpdate控制是否执行,而这三个却没有限制,有着多次调用的风险。
- componnetWillReceiveProps是在组件更新阶段,因为props的更新触发。但是只要组件触发更新,调用render,那么React.createElement就会重新执行,而props就会被重新创建,导致props没变,却会重复执行。
- 当 props 不变的前提下, PureComponent 组件能否阻止 componentWillReceiveProps 执行?不能,组件跟生命周期执行没有关系,PureComponent相当于实现了shouldUpdate函数,但不能阻止componentWillReceiveProps执行。
- componentWillUpdate被getSnapShotBeforeUpdate替代。
getSnapshotBeforeUpdate(prevProps,preState)更新前的props和更新前的preState
- 返回值作为componentDidUpdate的第三个参数,代替componentWillUpdate,在before-mutaiton阶段执行。getSnapshotBeforeUpdate 这个生命周期意义就是配合componentDidUpdate 一起使用,计算形成一个 snapShot 传递给 componentDidUpdate 。保存一次更新前的信息。
componentDidUpdate(prevProps, prevState, snapshot) & componentDidMount
- 在layout阶段执行,此时dom更新完毕。
shouldComponentUpdate(newProps,newState,nextContext)
- 在beginWork阶段执行,用于优化手段。如果有getDerivedStateFromProps,返回值也会合并到最新的state,传给shouldComponentUpdate。
componentWillUnmount
- mutaiton阶段执行,在dom即将销毁之前执行,清除延时器,定时器。
函数hooks执行阶段对应生命周期
useEffect & useLayoutEffect
一个是同步执行,一个是异步调用。useLayoutEffect会在layout阶段执行,useEffect会在layout阶段之后异步调用。
多功能Ref
创建ref
- useRef
- createRef
- 两者的区别就是函数组件useRef是绑定在fiber上的,因为函数组件每次执行都会重新执行useRef。而update时候的useRef是直接从fiber上吗获取ref对象返回;而类有实例,它可以保存ref信息。
类组件创建ref的三种方式
- ref是一个字符串, this.refs
- ref是一个函数 ref=(node)=> this.currentDom = node
- 通过createRef创建ref对象
高阶用法
-
通过forwardRef转发ref
const NewFather = React.forwardRef((props,ref)=> <Father grandRef=ref ...props />) 在Father里面就可以使用this.props.grandRef来绑定获取ref对象,也可以给ref对象赋值其他的东西
-
组件通信,父组件通过ref控制子组件类实例,或者函数子组件的一些方法,操控子组件,如form的resetFields和setFieldsValue。函数组件没有实例,通过forwardRef + useImperativeHandle控制子组件的一些方法
function Son(props, ref) const iptRef = useRef<htmlInputElement>(null) useImperativeHandle(ref,()=> const onSonFocus = () => iptRef.current.focus() const onChangeValue = () => iptRef.current.value = 'hahahah' return onSonFocus, onChangeValue ,[]) return <input type="text" ref=iptRef/> const ForSon = React.forwardRef(Son) const App = () => const sonRef = useRef<any>(null) return <div> <button onClick=()=>sonRef.current.onSonFocus()>focus</button> <button onClick=()=>sonRef.current.onChangeValue()>change</button> <ForSon ref=sonRef/> </div> (ReactDOM as any).createRoot(document.getElementById('root')).render(<App/>)
Context
createContext
const ThemeContext = React.createContext(null) //
const ThemeProvider = ThemeContext.Provider //提供者
const ThemeConsumer = ThemeContext.Consumer // 订阅消费者
Provider使用
const ThemeProvider = ThemeContext.Provider //提供者
export default function ProviderDemo()
const [ contextValue , setContextValue ] = React.useState( color:'#ccc', background:'pink' )
return <div>
<ThemeProvider value= contextValue >
<Son />
</ThemeProvider>
</div>
消费者一共有三种方式消费:
-
类组件静态属性contextType
const ThemeContext = React.createContext(null) // 类组件 - contextType 方式 class ConsumerDemo extends React.Component render() const color,background = this.context return <div style= color,background >消费者</div> ConsumerDemo.contextType = ThemeContext const Son = ()=> <ConsumerDemo />
-
函数组件useContext
const ThemeContext = React.createContext(null) // 函数组件 - useContext方式 function ConsumerDemo() const contextValue = React.useContext(ThemeContext) /* */ const color,background = contextValue return <div style= color,background >消费者</div> const Son = ()=> <ConsumerDemo />
-
订阅者-Consumer模式
const ThemeConsumer = ThemeContext.Consumer // 订阅消费者 function ConsumerDemo(props) const color,background = props return <div style= color,background >消费者</div> const Son = () => ( <ThemeConsumer> /* 将 context 内容转化成 props */ (contextValue)=> <ConsumerDemo ...contextValue /> </ThemeConsumer> )
在 Provider 里 value 的改变,会使引用contextType
,useContext
消费该 context 的组件重新 render ,同样会使 Consumer 的 children 函数重新执行,与前两种方式不同的是 Consumer 方式,当 context 内容改变的时候,不会让引用 Consumer 的父组件重新更新。
问题:
提供者的组件的setState目的是为了触发调度,影响消费者组件获取到的context改变从而引起消费者render。但是可能会造成提供者的子组件无意义的渲染。
解决:
-
使用React.memo或者PureComponent,防止重复渲染。
-
使用useMemo缓存vdom。jsx会被转为React.createElement,返回一个vdom。重复渲染是因为React.createElement被重复调用导致生成新的vdom。
<ThemeProvider value= contextValue > React.useMemo(()=> <Son /> ,[]) </ThemeProvider>
高阶组件
属性代理&反向继承
属性代理
function HOC(WrapComponent)
return class Advance extends React.Component
state=
name:'alien'
render()
return <WrapComponent ...this.props ...this.state />
缺点:转发ref需要forwardRef;无法获取组件的原始状态,需要使用ref;无法直接继承静态属性
反向继承
class Index extends React.Component
render()
return <div> hello,world </div>
function HOC(Component)
return class wrapComponent extends Component /* 直接继承需要包装的组件 */
export default HOC(Index)
经典例子:
-
强化props:react-router的withRouter,将react-router的context作为props传入。
-
劫持渲染, 动态加载 React.lazy,接受一个promise,如()=>import(‘…/pages/index’),promise的返回值是一个组件,通过componentDidMount,等到react渲染该组件执行componentDidMount的时候,才会去接受promise.then取得组件,进行渲染。
-
组件赋能,通过ref控制类实例。
-
事件监控,在外层包裹一层div,监控点击事件等等。
注意事项
- 谨慎修改原型链上的属性,如Component.prototype.componentDidMount
- 不要在函数组件内部或类组件render函数中使用HOC,HOC每次都会生成新的组件,不满足diff优化.
React事件系统
原因:
- 不同浏览器,事件存在不同的兼容性,所以必须统一兼容。
- 其二,react的事件在17之前统统绑定在document,在17之后绑定在了容器上面,防止过多事件绑定在原生dom上,由于不是绑定在真实dom上,react需要自己实现一套机制,事件俘获,到事件冒泡。
事件合成
三部分:
- 事件合成,初始化事件插件
- 渲染过程中,收集事件,注册到container。
- 一次用户交互,事件触发,到事件执行一系列过程。
事件合成的概念:React中,事件并不是绑定原声事件,而是通过合成,如onClick由click合成,onChange由blur,change,focus等合成。
事件插件机制
React 有一种事件插件机制,比如上述 onClick 和 onChange ,会有不同的事件插件 SimpleEventPlugin ,ChangeEventPlugin 处理,如
const registrationNameModules =
onBlur: SimpleEventPlugin,
onClick: SimpleEventPlugin,
onClickCapture: SimpleEventPlugin,
onChange: ChangeEventPlugin,
onChangeCapture: ChangeEventPlugin,
onMouseEnter: EnterLeaveEventPlugin,
onMouseLeave: EnterLeaveEventPlugin,
...
onBlur: ['blur'],
onClick: ['click'],
onClickCapture: ['click'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
onMouseEnter: ['mouseout', 'mouseover'],
onMouseLeave: ['mouseout', 'mouseover'],
...
registrationNameDependencies对象,保存了合成事件和原生事件的联系。
事件绑定
onChange等这些事件,保存在了dom对应的fiber.memoizedProps。
绑定一个onChange,实际上绑定了取出了registrationNameDependencies的onChange的[‘blur’, ‘change’, ‘click’, ‘focus’, ‘input’, ‘keydown’, ‘keyup’, ‘selectionchange’],依次进行addEvenListener注册
事件触发
- 第一步 批量更新,
首先,通过dom找到fiber。然后开启批量更新,有批量更新开关。
export function batchedEventUpdates(fn,a)
isBatchingEventUpdates = true; //打开批量更新开关
try
fn(a) // 事件在这里执行
finally
isBatchingEventUpdates = false //关闭批量更新开关
- 第二步 合成事件源
通过onClick找到事件插件SimpleEventPlugin,合成新的事件源e,里面就包含了 preventDefault 和 stopPropagation 等方法。
- 第三部 形成时间队列,模拟俘获冒泡
通过一个数组收集事件,从target开始根据return指针往上找,遇到俘获事件就unshift插入到数组头部,遇到冒泡事件就push到数组尾部。一个循环下来,就收集完所有的事件了。然后根据数组依次执行回调。
React如何模拟阻止事件冒泡
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... */
如果有一个调用了e.stopPropagation(),那么事件源里将有状态证明此次事件已经停止冒泡,下一次循环的时候,event.isPropagationStopped()为ture,,直接跳出当前循环,不再执行接下去的事件。
React调度
v15的react面临着js执行过久的问题,如何解决呢?对比vue
- vue 有template 模版收集依赖的过程,通过代理可以轻松构建响应式,使得更新的时候,vue可以迅速找到依赖的ui,以组件化的粒度更新组件,渲染视图
- 而在react中,一次更新无法具体知道波及的范围,只能选择通过root从根节点开始找不同。所以一次更新会花费大量js计算事件。既然如此,react选择了时间分片,让浏览器每一帧只给react一点时间去执行j s,剩余时间作为绘制的时间,防止页面卡顿。与 vue 更快的响应,更精确的更新范围,React 选择更好的用户体验。
Schedule原理。
react通过MessageChannel创建宏任务,并通过最小堆队列,和赋予不同任务优先级,以及过期时间,通过while循环,每执行一段js就判断是否到时间,如果到了,就继续调度宏任务,等下一帧再执行js,这一帧剩余的时间交给浏览器进行其他工作。
为啥不用requestIdleCallback?
- requestIdleCallback 目前只有谷歌浏览器支持 ,为了兼容每个浏览器,React需要自己实现一个 requestIdleCallback
具备条件
- 1 实现的这个 requestIdleCallback ,可以主动让出主线程,让浏览器去渲染视图。
- 2 一次事件循环只执行一次,因为执行一个以后,还会请求下一次的时间片。
满足这两点的只有宏任务了,
setTimeout(fn, 0)
可以满足创建宏任务,让出主线程,但是递归执行 setTimeout(fn, 0) 时,最后间隔时间会变成 4 毫秒左右,而不是最初的 1 毫秒
Scheduler:
异步调度+调和:
调和
为什么要用fiber?
- 更新fiber的过程叫做调和器,Reconciler。每一个 fiber 都可以作为一个执行单元来处理,所以每一个 fiber 可以根据自身的过期时间
expirationTime
( v17 版本叫做优先级lane
)来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染 - 在浏览器空余时间执行宏任务的时候,通过Schedule调度器,再次恢复执行单元,本质上中断了js执行,提高用户体验。
fiber和React element和dom
- element 是 React 视图层在代码层级上的表象,也就是开发者写的 jsx 语法,写的元素结构,都会被创建成 element 对象的形式。上面保存了 props , children 等信息。
- DOM 是元素在浏览器上给用户直观的表象。
- fiber 可以说是是 element 和真实 DOM 之间的交流枢纽站,一方面每一个类型 element 都会有一个与之对应的 fiber 类型,element 变化引起更新流程都是通过 fiber 层面做一次调和改变,然后对于元素,形成新的 DOM 做视图渲染
双缓存树
-
如同canvas,优先在内存构建好下一桢的动画,绘制完毕后直接替换,省去了白屏时间,这种在内存中构建并直接替换的技术叫做双缓存。
-
React 用 workInProgress 树(内存中构建的树) 和 current (渲染树) 来实现更新逻辑。双缓存一个在内存中构建,一个渲染视图,两颗树用 alternate 指针相互指向,在下一次渲染的时候,直接复用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树,这样可以防止只用一颗树更新状态的丢失的情况,又加快了 DOM 节点的替换与更新。
两大阶段,render+commit
render阶段包括beginWork和completeWork。
beginWork
:是向下调和的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点,打不同effectTag。
completeUnitOfWork
:是向上归并的过程,如果有兄弟节点,会返回 sibling兄弟,没有返回 return 父级,一直返回到 fiebrRoot ,期间可以形成effectList,对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理style,className等。
BeginWork阶段
- 对于组件,执行部分生命周期,执行 render ,得到最新的 children (只会处理第一层的儿子,孙子不处理)。
- 向下遍历调和 children ,复用 oldFiber ( diff 算法)。
- 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新。
核心函数:reconcileChildren
function reconcileChildren(current,workInProgress)
if(current === null) /* 初始化子代fiber */
workInProgress.child = mountChildFibers(workInProgress,null,nextChildren,renderExpirationTime)
else /* 更新流程,diff children将在这里进行。 */
workInProgress.child = reconcileChildFibers(workInProgress,current.child,nextChildren,renderExpirationTime)
对于第一次mount,会调度children,生成子节点的fiber,并通过child和return指针关联起来。
对于第二次update,会diff children,打上对应的effectTag
effectTag的常用例子
export const Placement = /* */ 0b0000000000010; // 插入节点
export const Update = /* */ 0b0000000000100; // 更新fiber
export const Deletion = /* */ 0b0000000001000; // 删除fiebr
export const Snapshot = /* */ 0b0000100000000; // 快照
export const Passive = /* */ 0b0001000000000; // useEffect的副作用
export const Callback = /* */ 0b0000000100000; // setState的 callback
export const Ref = /* */ 0b0000010000000; // ref
completeUnitOfWork
- 首先 completeUnitOfWork 会将带有 effectTag 的 Fiber 节点会被保存在一条被称为 effectLists 的单向链表中。在 commit 阶段,将不再需要遍历每一个 fiber ,只需要执行更新 effectList 就可以了。
- completeWork 阶段对于组件处理 context ;对于元素标签初始化,会创建真实 DOM ,将子孙 DOM 节点插入刚生成的 DOM 节点中;会触发 diffProperties 处理 props ,比如事件收集,style,className 处理。
Commit阶段
commit可以分为:
-
Before-mutation之前
-
before-mutation(更新dom之前)
-
mutation(更新dom)
-
Layout(更新dom之后)
-
layout之后
Before-mutation
function commitBeforeMutationEffects()
while (nextEffect !== null)
const effectTag = nextEffect.effectTag;
if ((effectTag & Snapshot) !== NoEffect)
const current = nextEffect.alternate;
// 调用getSnapshotBeforeUpdates
commitBeforeMutationEffectOnFiber(current, nextEffect);
if ((effectTag & Passive) !== NoEffect) //如果有useEffect的effectTag
scheduleCallback(NormalPriority, () =>
flushPassiveEffects(); //异步调用useEffect,这里只是注册。
return null;
);
nextEffect = nextEffect.nextEffect;
- 因为 Before mutation 还没修改真实的 DOM ,是获取 DOM 快照的最佳时期,如果是类组件有 getSnapshotBeforeUpdate ,那么会执行这个生命周期。
- 会异步调用 useEffect ,在生命周期章节讲到 useEffect 是采用异步调用的模式,其目的就是防止同步执行时阻塞浏览器做视图渲染。
Mutation
function commitMutationEffects()
while (nextEffect !== null)
if (effectTag & Ref) /* 置空Ref */
const current = nextEffect.alternate;
if (current !== null)
commitDetachRef(current);
switch (primaryEffectTag)
case Placement: // 新增元素
case Update: // 更新元素
case Deletion: // 删除元素
- 置空 ref 。
- 对新增元素,更新元素,删除元素。进行真实的 DOM 操作。
- 调用对应的生命周期,比如函数组件的useLayoutEffect的销毁函数。或者类组件的componentWillUnMount等。
Layout
function commitLayoutEffects(root)
while (nextEffect !== null)
const effectTag = nextEffect.effectTag;
commitLayoutEffectOnFiber(root,current,nextEffect,committedExpirationTime)
if (effectTag & Ref)
commitAttachRef(nextEffect);
- commitLayoutEffectOnFiber 对于类组件,会执行生命周期(componentDIdMount,componentDidUpdate),setState 的callback,对于函数组件会执行 useLayoutEffect 钩子。
- 如果有 ref ,会重新赋值 ref 。
Hooks
Hooks 出现本质上原因是:
- 1 让函数组件也能做类组件的事,有自己的状态,可以处理一些副作用,能获取 ref ,也能做数据缓存。
- 2 解决逻辑复用难的问题。
- 3 放弃面向对象编程,拥抱函数式编程。
hooks作为函数组件fiber和函数组件之间沟通的桥梁
Hooks对象本质
思考一个问题 React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常?
React hooks以三种处理策略存在 React 中:
- 1
ContextOnlyDispatcher
: 第一种形态是防止开发者在函数组件外部调用 hooks ,所以第一种就是报错形态,只要开发者调用了这个形态下的 hooks ,就会抛出异常。 - 2
HooksDispatcherOnMount
: 第二种形态是函数组件初始化 mount ,因为之前讲过 hooks 是函数组件和对应 fiber 桥梁,这个时候的 hooks 作用就是建立这个桥梁,初次建立其 hooks 与 fiber 之间的关系。 - 3
HooksDispatcherOnUpdate
:第三种形态是函数组件的更新,既然与 fiber 之间的桥已经建好了,那么组件再更新,就需要 hooks 去获取或者更新维护状态。
所有函数组件的触发都在renderWIthHooks执行,可以看这个函数的逻辑。
let currentlyRenderingFiber
function renderWithHooks(current,workInProgress,Component,props)
currentlyRenderingFiber = workInProgress; //赋值当前fiber,hooks通过这个获取fiber
workInProgress.memoizedState = null; /* 每一次执行函数组件之前,先清空状态 (用于存放hooks列表)*/
workInProgress.updateQueue = null; /* 清空状态(用于存放effect list) */
ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate /* 判断是初始化组件还是更新组件 */
let children = Component(props, secondArg); /* 执行我们真正函数组件,所有的hooks将依次执行。 */
ReactCurrentDispatcher.current = ContextOnlyDispatcher; /* 将hooks变成第一种,防止hooks在函数组件外部调用,调用直接报错。 */
从上面可以看到,
-
对于类组件,memoizedState保存着state的信息。对于函数组件,memoizedState保存着hooks列表。
-
对于类组件,updateQueue存放着update链表等更新信息。而对于函数组件,updateQueue保存着useEffect/useLayoutEffect 产生的副作用组成的链表
-
在函数真正执行之前,React hooks对象被赋予了真正的Hooks对象,而当函数组件执行完毕之后,hooks对象被重新赋值了报错的对象。这也是解释了为什么hooks只能在函数中执行,因为。引用的 React hooks都是从 ReactCurrentDispatcher.current 中的, React 就是通过赋予 current 不同的 hooks 对象达到监控 hooks 是否在函数组件内部调用
-
每个Hooks内部可以读取到fiber的原因是因为在函数执行之前,fiber被赋值到了currentlyRenderingFiber,hooks通过currentlyRenderingFiber读取到fiber
为什么hooks不能出现在if else语句。
答:可能会破坏hooks的顺序。
每一个hooks执行的时候,会创建一个hooks对象,hooks对象通过next指针关联。等到更新阶段执行hooks的时候,会复用第一次创建的hooks对象。假设有一个存在条件语句下的hooks,在第一次更新的时候,他执行了,创建了hooks对象。,到第二次更新的时候,他不执行了,而这个时候却少一个hooks来消费hooks对象,会导致出现如下结果:
第二次
hook2服用了hook1的hooks对象,而useRef执行的时候,指向的hooks.next却是useState,也就是hook2的hooks对象,因为useState!== useRef,所以就会报错。这也是为什么hooks不能出现在条件语句的原因,会破坏hooks的结构顺序。
处理useEffect
对于mount的useEffect,执行mountEffect, mountEffect调用mountEffectImpl
// useEffect的mount
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void
return mountEffectImpl(
PassiveEffect |以上是关于掘金小课《React进阶实战指南》笔记的主要内容,如果未能解决你的问题,请参考以下文章
写给初中级前端的高级进阶指南(JSTSVueReact性能学习规划)