赞
踩
阅读了掘金小课《React进阶实战指南》,做的笔记加自己的一些总结。部分资源来自掘金小课《React进阶实战指南》
Jsx统统被转为React.createElement,
createElement参数:
如
<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样子:
element类型,如div => reactElement类型,type为’div’
文本类型,如’aaa’=> ‘aaa’,直接转为字符串
数组类型,如[<div>,<div>] => [{type: div…}, {type: div…}],转化后是element的数组
组件类型,转为react element,type是函数或者类本身
三元运算 / 表达式,先运算,再按照上述规则转化
函数执行,先执行,再按照上述规则转化
针对不同的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 返回的组件
如果没有在 constructor 的 super 函数中传递 props,那么接下来 constructor 执行上下文中就获取不到 props ,这是为什么呢?
函数组件和类组件本质的区别是什么呢
组件通信方式:
setState
和函数组件中的 useState
有什么异同?
componentWillReceiveProps(没有getDerviedStateFormProps的时候执行)
getDerviedStateFormProps
shouldComponentUpdate
componentWillUpdate
render
getSnapShotBeforeUpdate(在dom更新前执行,before-mutation阶段)
componentDidUpdate
类的静态属性,接受新的props和老的state。返回值会合并到最新的state去。
getDerivedStateFromProps
,不管是 props 改变,还是 setState ,或是 forceUpdate 。一个是同步执行,一个是异步调用。useLayoutEffect会在layout阶段执行,useEffect会在layout阶段之后异步调用。
通过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/>)
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,监控点击事件等等。
三部分:
事件合成的概念: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到数组尾部。一个循环下来,就收集完所有的事件了。然后根据数组依次执行回调。
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,,直接跳出当前循环,不再执行接下去的事件。
v15的react面临着js执行过久的问题,如何解决呢?对比vue
react通过MessageChannel创建宏任务,并通过最小堆队列,和赋予不同任务优先级,以及过期时间,通过while循环,每执行一段js就判断是否到时间,如果到了,就继续调度宏任务,等下一帧再执行js,这一帧剩余的时间交给浏览器进行其他工作。
满足这两点的只有宏任务了,
setTimeout(fn, 0)
可以满足创建宏任务,让出主线程,但是递归执行 setTimeout(fn, 0) 时,最后间隔时间会变成 4 毫秒左右,而不是最初的 1 毫秒Scheduler:
异步调度+调和:
expirationTime
( v17 版本叫做优先级 lane
)来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染如同canvas,优先在内存构建好下一桢的动画,绘制完毕后直接替换,省去了白屏时间,这种在内存中构建并直接替换的技术叫做双缓存。
React 用 workInProgress 树(内存中构建的树) 和 current (渲染树) 来实现更新逻辑。双缓存一个在内存中构建,一个渲染视图,两颗树用 alternate 指针相互指向,在下一次渲染的时候,直接复用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树,这样可以防止只用一颗树更新状态的丢失的情况,又加快了 DOM 节点的替换与更新。
render阶段包括beginWork和completeWork。
beginWork
:是向下调和的过程。就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点,打不同effectTag。
completeUnitOfWork
:是向上归并的过程,如果有兄弟节点,会返回 sibling兄弟,没有返回 return 父级,一直返回到 fiebrRoot ,期间可以形成effectList,对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理style,className等。
核心函数: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
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
commit可以分为:
Before-mutation之前
before-mutation(更新dom之前)
mutation(更新dom)
Layout(更新dom之后)
layout之后
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; } }
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:{} // 删除元素
}
}
}
function commitLayoutEffects(root){
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
commitLayoutEffectOnFiber(root,current,nextEffect,committedExpirationTime)
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
}
}
Hooks 出现本质上原因是:
hooks作为函数组件fiber和函数组件之间沟通的桥梁
React hooks以三种处理策略存在 React 中:
ContextOnlyDispatcher
: 第一种形态是防止开发者在函数组件外部调用 hooks ,所以第一种就是报错形态,只要开发者调用了这个形态下的 hooks ,就会抛出异常。HooksDispatcherOnMount
: 第二种形态是函数组件初始化 mount ,因为之前讲过 hooks 是函数组件和对应 fiber 桥梁,这个时候的 hooks 作用就是建立这个桥梁,初次建立其 hooks 与 fiber 之间的关系。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的顺序。
每一个hooks执行的时候,会创建一个hooks对象,hooks对象通过next指针关联。等到更新阶段执行hooks的时候,会复用第一次创建的hooks对象。假设有一个存在条件语句下的hooks,在第一次更新的时候,他执行了,创建了hooks对象。,到第二次更新的时候,他不执行了,而这个时候却少一个hooks来消费hooks对象,会导致出现如下结果:
第二次
hook2服用了hook1的hooks对象,而useRef执行的时候,指向的hooks.next却是useState,也就是hook2的hooks对象,因为useState!== useRef,所以就会报错。这也是为什么hooks不能出现在条件语句的原因,会破坏hooks的结构顺序。
对于mount的useEffect,执行mountEffect, mountEffect调用mountEffectImpl
// useEffect的mount
function mountEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(
PassiveEffect | PassiveStaticEffect, //effectTag标志
HookPassive, //useEffect的标志
create,
deps,
);
}
// fiberFlags表示effectTag的标志,而hookFlags表示是useEffect的标志
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook(); //创建hooks
const nextDeps = deps === undefined ? null : deps; //依赖
currentlyRenderingFiber.flags |= fiberFlags; //添加effectTag
hook.memoizedState = pushEffect( //创建effect保存在hook.memoizedState
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
可以看到,对于useEffect,第一次会创建一个hooks,然后打上标记。最后调用puseEffect创建一个effect存放到hook.memoizedState上。
这里插一嘴,对于useEffect,标志是HookPassive,对于useLayoutEffect,他的执行是mountLayoutEffect
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
let fiberFlags: Flags = UpdateEffect;
if (enableSuspenseLayoutEffectSemantics) {
fiberFlags |= LayoutStaticEffect;
}
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
这里可以看到,useLayoutEffect的标志是HookLayout,他跟useEffect一样都是调用同一个方法,只不过给了不同的标记。
接着看pushEffect,pushEffect不只会创建effect,如
// 创建effect, tag是effectTag, create是执行函数, deps是依赖项 function pushEffect(tag, create, destroy, deps) { // 创建effect const effect: Effect = { tag, create, destroy, deps, // Circular next: (null: any), }; // 获取当前fiber的updateQueue,函数组件执行前,fiber已经赋予了currentlyRenderingFiber let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any); // 通过环状链表将effect存放在fiber.updateQueue上 if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any); componentUpdateQueue.lastEffect = effect.next = effect; } else { const lastEffect = componentUpdateQueue.lastEffect; if (lastEffect === null) { componentUpdateQueue.lastEffect = effect.next = effect; } else { const firstEffect = lastEffect.next; lastEffect.next = effect; effect.next = firstEffect; componentUpdateQueue.lastEffect = effect; } } return effect; }
pushEffect创建effect之后,还会将effect以环状链表的形式,存放在fiber.updateQueue上。
这样设计是因为函数组件可能存在多个useEffect和useLayoutEffect,将这些effect收集起来,在commit阶段的时候统一处理。
比如
React.useEffect(()=>{
console.log('第一个effect')
},[ props.a ])
React.useLayoutEffect(()=>{
console.log('第二个effect')
},[])
React.useEffect(()=>{
console.log('第三个effect')
return () => {}
},[])
updateQueue就是这样的
更新阶段的useEffect,调用updateEffect,updateEffect调用updateEffectImpl
function updateEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
// useEffect的Update阶段 function updateEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = updateWorkInProgressHook(); //获取hooks const nextDeps = deps === undefined ? null : deps; //获取依赖项 let destroy = undefined; if (currentHook !== null) { const prevEffect = currentHook.memoizedState; //获取上一次的effect destroy = prevEffect.destroy; if (nextDeps !== null) { const prevDeps = prevEffect.deps; //获取之前的依赖项 if (areHookInputsEqual(nextDeps, prevDeps)) { //如果依赖项一样,更新当前effect对象就行,无需打上新的标记。 hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps); return; } } } currentlyRenderingFiber.flags |= fiberFlags; // 打上新的标记HookHasEffect,这样commit阶段的时候就会再执行一次。 hook.memoizedState = pushEffect( HookHasEffect | hookFlags, create, destroy, nextDeps, ); }
如上,主要做了:
对于useLayoutEffect,更新阶段调用的函数也是跟useEffect一样。
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
可以看到,也是调用了updateEffectImpl,只不过标记不同。
React 会用不同的 EffectTag 来标记不同的 effect,对于useEffect 会标记 UpdateEffect | PassiveEffect, UpdateEffect 是证明此次更新需要更新 effect ,HookPassive 是 useEffect 的标识符,对于 useLayoutEffect 第一次更新会打上 HookLayout 的标识符。
React 就是在 commit 阶段,通过标识符,证明是 useEffect 还是 useLayoutEffect ,接下来 React 会同步处理 useLayoutEffect ,异步处理 useEffect 。
如果函数组件需要更新副作用,会标记 UpdateEffect,至于哪个effect 需要更新,那就看 hooks 上有没有 HookHasEffect 标记,所以初始化或者 deps 不相等,就会给当前 hooks 标记上 HookHasEffect ,所以会执行组件的副作用钩子。
useEffect和useLayoutEffect的处理函数是一样的,只不过入参不同。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。