基础篇-jsx
- React JSX 在 babel 和 webpack 编译后是
React.createElement()方法。因此老版本中需要引入import React from 'react'来防止报错。 - React element 会被处理成 fiber 对象,通过 sibling (兄弟节点)、return (父节点)、child (子节点)互相联系,这个过程称之为“调和”。
React.Children.toArray()API,用于扁平化 children 数组。React.Children.forEach()API,可以遍历所有子节点数组,包括子节点。React.isValidElement()API,用于判断是否为 React Element元素,返回 boolean 。
基础篇-Components
- 在调和阶段,React 通过 fiber tag 来判断并选择组件的处理逻辑。
- 对于类组件,底层只需实例化一次,state 等状态保存在实例化的组件中,每次更新只需要调用 render 以及各生命周期函数;对于函数组件,每次更新都是一次新函数的执行,所有的东西都会重新声明。
- 主流的通信方式:
- props 和 callback ,父子通信
- ref
- context 上下文
- mobx、redux 状态管理
- event bus 发布订阅(不推荐❌)
问与答
Q:如果没有在 constructor 的 super 函数中传递 props,那么接下来 constructor 执行上下文中就获取不到 props ,这是为什么呢?
/* 假设我们在 constructor 中这么写 */
constructor(){
super()
console.log(this.props) // 打印 undefined 为什么?
}
绑定 props 是在父类 Component 构造函数中,执行 super 等于执行 Component 函数,此时 props 没有作为第一个参数传给 super() ,在 Component 中就会找不到 props 参数,从而变成 undefined ,在接下来 constructor 代码中打印 props 为 undefined 。
A:
constructor(props){
super(props)
}
基础篇-state
- 在类组件中,一次 setState 的流程:
- setState 产生当前更新的优先级( old : expirationTime; new : lane; )
- 调和阶段:从 fiber root 根节点向下调和子节点,找出需要更新的组件,合并 state。
- render阶段: 触发 render 函数,得到新的 UI 视图层。
- commit阶段: 替换真实 Dom,并执行 setState 第二个参数的 callback。
- 优化更新次数的方法:
- pureComponent:对 state 和 props 进行浅比较。
- shouldComponentUpdate 生命周期,可自定义判断是否需要更新。
- 类组件中,通过 setState 的第二个参数,或者生命周期 ComponentDidMount 来检测 state 改变或者组件更新;函数组件中通过 useEffect() 来检测。
- 在函数组件中,在本次函数执行的上下文中,永远拿不到最新的 state 值,只能通过 useEffect 去监听值改变。
问与答
Q:demo中为何点击后 state 不更新?
export default function Index(){
const [ state , dispatchState ] = useState({ name:'alien' })
const handleClick = ()=>{ // 点击按钮,视图没有更新。
state.name = 'Alien'
dispatchState(state) // 直接改变 `state`,在内存中指向的地址相同。
}
return <div>
<span> { state.name }</span>
<button onClick={ handleClick } >changeName++</button>
</div>
}
如上例子🌰中,当点击按钮后,发现视图没有改变,为什么会造成这个原因呢?
在 useState 的 dispatchAction 处理逻辑中,会浅比较两次 state ,发现 state 相同,不会开启更新调度任务; demo 中两次 state 指向了相同的内存空间,所以默认为 state 相等,就不会发生视图更新了。
A:
把上述的 dispatchState 改成 dispatchState({...state}) 根本解决了问题,浅拷贝了对象,重新申请了一个内存空间。
Q:类组件中的 setState 和函数组件中的 useState 有什么异同?
A:
相同点:
- 首先从原理角度出发,setState和 useState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则。
不同点:
- 在不是 pureComponent 组件模式下, setState 不会浅比较两次 state 的值,只要调用 setState,在没有其他优化手段的前提下,就会执行更新。但是 useState 中的 dispatchAction 会默认比较两次 state 是否相同,然后决定是否更新组件。
- setState 有专门监听 state 变化的回调函数 callback,可以获取最新state;但是在函数组件中,只能通过 useEffect 来执行 state 变化引起的副作用。
- setState 在底层处理逻辑上主要是和老 state 进行合并处理,而 useState 更倾向于重新赋值。
问:setState是同步还是异步
解决问题:
React 的 setState 是同步还是异步? - 掘金 (juejin.cn)
基础篇-props
基础篇-生命周期
React lifecycle methods diagram (wojtekmaj.pl)
-
react 渲染流程:
- jsx 语法解析为
React.createElement - 生成
virtual dom - 进入 render 阶段:
virtual dom调和成fiber对象 - 深度遍历
fiber树,通过 diff 算法查找需要更新的节点 - 对于需要变化的组件,执行
render函数 - render 阶段结束,进入 commit 阶段
- 创建并修改真实 DOM 节点
- jsx 语法解析为
-
fiber 阶段会生成双缓冲树:
current树和workInProgress树(当前正在调和的树)。在初始化时,current = null,在第一次 fiber 调和后,将workInProgress赋值给current,通过两树来保证 React 在一次更新中快速构建并保持状态不丢失 -
class instance(组件实例)上通过
_reactinternals属性访问对应的 class fiber 对象,class fiber对象上通过stateNode属性来访问 class instance -
getDerivedStateFromProps,getSnapshotBeforeUpdate生命周期是在 render 阶段执行 -
componentDidMount,componentDidUpdate声明周期是在 commit 阶段执行的
问与答
Q:React.useEffect 回调函数 和 componentDidMount / componentDidUpdate 执行时机有什么区别 ?
A:useEffect 对 React 执行栈来看是异步执行的,而 componentDidMount / componentDidUpdate 是同步执行的,useEffect代码不会阻塞浏览器绘制。在时机上 ,componentDidMount / componentDidUpdate 和 useLayoutEffect 更类似。
基础篇-Ref
- 类组件中使用
React.createRef();函数组件中使用React.useRef(null) - 类组件中,ref 被实例 instance 维护;函数组件中 ref 对象挂载在 fiber 对象上
- 函数组件中,使用
forwardRef和useImperativeHandle实现 ref 通信
优化篇-渲染控制
React.useMemo(create,deps)第一个参数为一个函数,函数的返回值作为缓存值,如上 demo 中把 Children 对应的 element 对象,缓存起来。第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,如果有改变重新执行 create ,得到新的缓存值。- useMemo 原理:useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。
- useMemo 是将数据缓存在fiber上的。
React.memo(Component,compare)接受两个参数,第一个参数 Component 原始组件本身,第二个参数 compare 是一个函数,可以根据一次更新中 props 是否相同决定原始组件是否重新渲染。第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染。
问与答
Q:useCallback 和 useMemo 有什么区别?
A: useCallback 第一个参数就是缓存的内容,useMemo 需要执行第一个函数,返回值为缓存的内容,比起 useCallback , useMemo 更像是缓存了一段逻辑,或者说执行这段逻辑获取的结果。那么对于缓存 element 用 useCallback 可以吗,答案是当然可以了。
原理篇-调度与时间片
问与答
Q: 异步调度原理?
A: React 发生一次更新,
Q: React为什么不用settimeout?
A: setTimeout(fn,0)虽然可以是宏任务,满足让出主线程的要求,但是递归 setTimeout(fn,0) 的时候,最后时间差会有 4ms,而一帧时间也就 16ms,比较浪费。
Q: 说一说 React 的时间分片?
A: 根据每秒60帧的频率来划分时间片。浏览器执行一次事件循环的时间为16ms(一帧),一帧内浏览器会做如下事情:处理事件、执行js、渲染动画、布局 Layout 、绘制 Paint。React 的异步事件就是通过类似requestIdleCallback()的方法一帧一帧的去请求,如果浏览器有空闲时间,就开始执行异步任务,以此来防止卡顿。
Q: React 如何模拟 requestIdleCallback?
A: MessageChannel接口。在一次更新中,React 会调用 requestHostCallback ,把更新任务赋值给 scheduledHostCallback ,然后 channel.port2 向 channel.port1 发送消息通知, port1 通过 onmessage 接收消息后执行更新任务 scheduledHostCallback ,然后置空 scheduledHostCallback ,以此来达到异步目的。
Q: 简述一下调度流程?
A:
原理篇-调和与 fiber
问与答
Q: 什么是fiber ? Fiber 架构解决了什么问题?
A: Fiber 是 React 中 的虚拟 DOM。没有 Fiber 之前,React 一次渲染需要从根节点开始,并且不能打断,大型项目就会因此卡顿。 Fiber 作为执行的单元,可以根据 lane 优先级来中断,让浏览器可以及时渲染,等到浏览器空余时间时,再恢复更新 Fiber,提高了用户体验。
Q: Fiber root 和 root fiber 有什么区别?
A: 首次构建时,React 会创建一个Fiber root 作为整个应用的根基(应用根节点);Root fiber 可以理解为,例如一个 Index 组件, Index 就是一个 root fiber。
Q: 不同fiber 之间如何建立起关联的?
A: fiber 通过 return 指向父节点,通过 sibling 指向下一个兄弟节点,通过 child 指向子节点。
Q: React 调和流程?
A:
- 初始化
- 创建fiber root 和 root fiber
- 生成 workInProgress 和 current 树
- 深度调和子节点
- 渲染视图
- 更新
- 生成 workInProgress 和 current 树
Q: 两大阶段 commit 和 render 都做了哪些事情?
A:
-
render 阶段
beginWork()向下调和子节点completeUnitOfWork()向上归并兄弟节点- 完成调和
-
commit 阶段
a. Before mutation(执行 DOM 操作前)
getSnapshotBeforeUpdate()生命周期- 异步调用 useEffect
b. mutation(执行 DOM 操作)
- 置空 ref ,在 ref 章节讲到对于 ref 的处理
- 对新增元素,更新元素,删除元素。进行真实的 DOM 操作
c. layout(执行 DOM 操作后)
- commitLayoutEffectOnFiber 对于类组件,会执行生命周期,setState 的callback,对于函数组件会执行 useLayoutEffect 钩子
- 如果有 ref ,会重新赋值 ref
Q: 什么是双缓冲树? 有什么作用?
A: workInProgress (内存中构建的树) 和 current (渲染树); workInProgress 是在内存中构建的 fiber 树,在一次更新中,更新都是再 workInProgress fiber 树上进行的。更新结束后,会赋值给 current 树,用于渲染视图。
两棵树用 alternate 指针相互指向,防止更新状态丢失并加快了 DOM 节点的替换与更新。
Q: Fiber 深度遍历流程?
A:
Q: Fiber的调和能中断吗? 如何中断?
A: