react组件
react的出现让创建交互式UI变成轻而易举。在react世界里,函数和类就是UI的载体,我们甚至可以理解为,将数据传入react的类或者函数中,返回的就是UI界面。
- 组价就是生成UI的载体。若结合函数组件,那组件就是输入props返回UI的函数。
- 组件核心思想:。
- 组件是复用的基石。
- render方法是组件的灵魂。
JSX解析
我们讲JSX是javascript的语法扩展。
- JSX由babel通过调用React.createElement方法转译成ReactElement实例。
- JSX其实是ReactElement类型的实例。
解析下createElement方法。
参数:
第一个标签,字符串类型。组件下是组件名,原生DOM下是其字符串。
第二个配置,对象类型。元素DOM的属性配置信息。
第三个子内容,jsx类型下的对象或数组,字符串。
源码:
// 函数的定义
/**
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
*/
export function createElement(type, config, children) {
// createElement函数内重要逻辑,通过config构建props
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// 构建props中会过滤的属性名
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
// createElement返回的对象实例内容
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
- config里的key,ref不会传递进组件内部。__self,__source很少使用。
- ref会被过滤掉而无法传递进组件内部,hasOwnProperty是通过属性名判断,就是说可以通过非ref名称的传递。
- 组件的子内容是通过props的children传递的。就像vue里的默认插槽。
Automatic Runtime 自动运行时
新版本下已经不需要显示的引入React了,这种模式源于Automatic Runtime模式,plugin-syntax-jsx已经向文件中提前注入里_jsxRuntime api。
手写代码进行JSX的转换
const fs = require('fs')
const babel = require('@babel/core')
// 第一步:读取文件内容
fs.readFile('文件路径', (e, data) => {
const code = data.toString('utf-8')
// 第二步:转换jsx文件
const result = babel.transformSync(code, {
plugins: ['@babel/plugin-transform-react-jsx']
});
// 第三步:将转换后的内容写入新的文件
fs.writeFile('转换后的文件路径', result.code, null)
})
类组件
- 继承于React.Component的类
class Index extends React.Component {
render () {
return <div>index</div>
}
}
类组件的实例化
// 以下代码简化了细节,罗列了关键函数
function renderClassComponent(): void {
// ...
const instance = constructClassInstance(Component, props, maskedContext);
mountClassInstance(instance, Component, props, maskedContext);
finishClassComponent(request, task, instance, Component, props);
// ...
}
- constructClassInstance是负责类组件的实例化。
// 以下代码简化了细节,罗列了关键函数
function constructClassInstance(): any {
// ...
// 类组件的实例化
let instance = new ctor(props, context);
// 读取state信息
const state = (workInProgress.memoizedState =
instance.state !== null && instance.state !== undefined
? instance.state
: null);
adoptClassInstance(workInProgress, instance);
return instance;
}
// 挂载updater,将实例赋值给fiber的stateNode
function adoptClassInstance(workInProgress: Fiber, instance: any): void {
// ...
instance.updater = classComponentUpdater;
workInProgress.stateNode = instance;
setInstance(instance, workInProgress);
}
- 在类组件中,除了继承React.Component,内部还挂载updater。类组件中调用的setState本质上是调用updater对象的enqueueSetState方法。
Component.prototype.setState = function (partialState, callback) {
// ...
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
- 在类组件中的构造函数中,会调用super()完成父类的初始化。其React.Component初始化内容很简单。就是props, context, updater的赋值,其refs置空对象。
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
// 类实例负责更新的更新者。
this.updater = updater || ReactNoopUpdateQueue;
}
- shouldComponentUpdate方法判断组件是否需要更新。 这个逻辑能减少fiber的调和次数,但并不会带来很大的性能提升。在fiber调和过程中会进行props参数的对比以判断是否给fiber打副作用标签,所以即使shouldComponentUpdate返回false,也只是让组件生成虚拟子内容,然后diff对比调和下。
函数组件
- 函数组件不会实例化,走的是函数运行的方式。
// 以下代码简化了细节,罗列了关键函数
export function renderWithHooks<Props, SecondArg>(): any {
// 这里执行了函数组件,返回了其组件内容
let children = Component(props, secondArg);
return children;
}
1: 函数组件的重新渲染都是通过重新执行函数组件达到的。
2: 函数组件本质是函数,就存在闭包特性。如state会在下次组件渲染执行时才是最新值。
组件通信
1: props和callback方式。
2: ref方式。
3: redux,mobx等的状态管理方式。
4: context上下文方式。
5: event bus事件总线。
说明:
主要通信还是props方式,在react里组件是单向数据流的,数据只能由父组件传递给子组件。
props传递三大类型数据。数值数据,事件处理器,JSX。
跨组件层级方面传参应优选context。
state深入解析
react的异步更新是通过‘批量更新’方式实现的。将多次setState的结果值合并起来,然后汇总做一次更新渲染。
-
是同步渲染还是异步渲染。 在legacy模式下,异步执行的代码里的setState是同步的。根本原因是因为异步代码执行的时候其异步更新标记已关闭,导致后续执行的setState会触发同步渲染。
-
在concurrent模式下的一次渲染。react会合并之前的更新任务做一次数据的汇总,然后执行一次更新渲染。即使在flushSync()里也是一样的。
在concurrent模式下已是异步渲染。即使是异步执行的代码其setState依然是异步渲染的。除非手动触发次同步渲染flushSync()
手动触发次同步渲染flushSync()的代码分析。
useState特别说明
[state, dispatch] = useState(initData) useState返回state和其dispatch方法的集合。
细节备注:
- useState的初始化值应优先考虑函数方式,以做惰性初始化,也可以避免在组件后续渲染中初始化值的重新创建赋值给useState作为参数执行(后续useState方法的执行不会再需要初始化值了)。
- useState不会立即触发更新渲染,在更新渲染前其state都是旧的,只有在组件函数重新执行后state才是最新的。
- useState的参数可以是函数。其作用是基于当前最新的state返回修改后的state来更新渲染组件。
- dispatch的更新后执行回调逻辑得走useEffect方式。
特别说明:
- 函数组件的fiber调和时会根据新state和旧state做Object.is判断,当其返回false时才给fiber打需更新的副作用标签。
- 所以当state内容是对象时,应使用dispatch({...state})方法触发更新,由于是不同的引用地址,在react的diff调和时会识别为发生更新,从而触发了组件的更新渲染。
总结:
生命周期
-
constructor(构造函数) 在类组件挂载(mount)执行。用于初始化类组件,一般会初始化state。
-
getDerivedStateFromProps(从props中派生state) 在类组件的挂载(mount)和更新(update)阶段执行。有两个参数nextProps和prevState,并从中生成新的state。
-
render(生成组件内容) 在类组件的挂载(mount)和更新(update)阶段执行。用于生成组件内容。
-
componentDidMount(组件完成挂载后执行) 在类组件挂载完成后执行。这时候DOM已经创建挂载了,就可以做一些基于DOM操作,DOM事件监听了。
-
shouldComponentUpdate(判断组件是否需要更新) 在类组件更新时执行,用于判断该组件是否需要更新。该函数返回false即不需要更新,返回true即需要更新。 - 该函数的优化并不能带来多大性能提升,一般下不需要自行判断。
-
getSnapshotBeforeUpdate(获取DOM的快照) 在类组件的更新(update)阶段执行,具体是commit阶段下的before mutation阶段执行。此时的虚拟DOM已是最新值,但还未更新到真实DOM上。
getSnapshotBeforeUpdate的返回值会作为componentDidUpdate方法的第三个参数。 -
componentDidUpdate(组件完成更新后执行) 在类组件完成更新后执行,此时真实的DOM已完成同步更新。
该函数有三个参数: prevProps, prevState, snapshot。 -
componentWillMount(类组件卸载前执行) 在组件销毁前执行,主要做一些资源的释放工作。比如清除定时器,延时器,事件监听器。
-
useEffect(副作用hook) 在函数组件完成渲染后执行(首次渲染和更新渲染都会触发)。
useEffect的第一个参数callback,返回的是destory。destory作为下一次callback执行之前调用,用于清除上一次callback产生的副作用。在函数组件卸载前也会执行。
useEffect是异步调用的。 -
useLayoutEffect(布局副作用hook) 在函数组件DOM调和后,真实DOM更新前执行。就是before mutation阶段。
useLayoutEffect是同步调用的。
ref
ref引用
ref可对原生DOM引用,可直接获取真实DOM信息。这种方式是不建议的,我们应该使用受控组件开发。
ref可对组件引用。
- 类组件下指向其组件实例,可通过ref调用组件方法等。该方式不符合开闭原则,因此不建议使用。
- 函数组件下需使用forwardRef将引用转发进组件内,以调用组件某些功能函数,该方式需和useImperativeHandle结合使用,并建议慎用。比如Modal模态窗口的打开逻辑可用ref引用执行openDialog方法来打开模态窗口组件,能有效进行模块边界的划分。
创建ref
在类组件下使用createRef创建ref引用,在函数组件下使用useRef创建ref引用。
函数组件下useRef特别作用
- useRef的current内容的改变不会触发组件的更新。因此可以将useRef值作为缓存对象,缓存UI不需要的数据。
- useRef的内容在组件render间保持一致性。
ref通过props传递说明
在前面章节介绍JSX转换ReactElement过程中有说明,ref是不会通过props传递进组件内部,其过程是通过名称进行判断的,就是说换个名字就可以了,比如使用xxRef进行传参。
ref原理揭秘 - ref的置空和赋值
ref在渲染过程中会先置空,待真实dom渲染后,会重新赋值ref。
- 置空阶段。在更新过程中,在mutation阶段会执行safelyDetachRef方法,该方法会清空之前的ref值,将其设置为null。
// 之前还叫commitDetachRef方法
function safelyDetachRef() {
if (ref !== null) {
if (typeof refCleanup === 'function') {
refCleanup();
} else if (typeof ref === 'function') {
let retVal = ref(null);
} else {
// $FlowFixMe unable to narrow type to RefObject
ref.current = null;
}
}
}
- 赋值阶段。在真实DOM更新结束后,在layout阶段,需要将ref引用到真实dom上,因此该过程会重新赋值ref。
function commitAttachRef() {
if (ref !== null) {
if (typeof ref === 'function') {
finishedWork.refCleanup = ref(instanceToUse);
} else {
// $FlowFixMe unable to narrow type to the non-function case
ref.current = instanceToUse;
}
}
}
// 里面调用了commitAttachRef对ref进行重新赋值
function safelyAttachRef() {
commitAttachRef(current);
}
总结: 在一次组件渲染过程中,组件内的ref会在真实DOM更新前将其置空,会在真实DOM更新后将其重新赋值。以保证ref指向正确的DOM。
事件合成
事件合成是为了兼容各浏览器,React事件处理器本质上只是普通函数的执行。
- 事件分为冒泡阶段,捕获阶段。 在新版的react合成事件中,无法在冒泡阶段处理的事件都会在捕获阶段处理,否则默认会在冒泡阶段处理。
allNativeEvents.forEach(domEventName => {
if (!nonDelegatedEvents.has(domEventName)) {
// 在冒泡阶段绑定事件
listenToNativeEvent(domEventName, false, rootContainerElement);
}
// 在捕获阶段绑定事件
listenToNativeEvent(domEventName, true, rootContainerElement);
});
// nonDelegatedEvents 是不会冒泡事件列表
// We should not delegate these events to the container, but rather
// set them on the actual target element itself. This is primarily
// because these events do not consistently bubble in the DOM.
export const nonDelegatedEvents: Set<DOMEventName> = new Set([
'cancel',
'close',
'invalid',
'load',
'scroll',
'toggle',
// In order to reduce bytes, we insert the above array of media events
// into this Set. Note: the "error" event isn't an exclusive media event,
// and can occur on other elements too. Rather than duplicate that event,
// we just take it from the media events array.
...mediaEventTypes,
]);
-
react事件处理函数存放的位置。 是存放于DOM节点对应的fiber节点的memorizedProps里的普通方法。
-
从一个或多个原生DOM事件合成一个React事件。比如onChange事件有focus,change,blur原生DOM事件合成,所以当触发原生focus事件时也会触发React的onChange事件。
-
React采用了事件委托,新版下所有的原生React事件都会注册到root跟节点上。旧版本的是注册到document上。
React事件执行说明
-
当原生事件触发时。
获取到目标原生DOM。然后通过_reactFilber$ + {randomKey}引用来获取原生DOM对应的fiber节点。 -
合成事件源。
用原生事件源event + 对应的事件处理插件plugin = React事件源 -
收集事件处理方法形成列队。
从目标filber沿着return引用向上遍历,收集对应的React事件处理方法。 -
执行React事件。
遍历事件列队依次执行事件处理方法,并传入React事件源作为参数。
说明
- React事件阻止冒泡为什么只能调用stopPropagation方法,不能在事件处理方法里返回false。
因为React事件只是普通函数的执行,当其显示的调用stopPropagation方法后,React在下次的事件队列遍历时会优先检查isPropagationStopped,如果是则中止。效果上就达到了阻止冒泡效果。
异步调度
- 以前的同步渲染会造成长时间占用主线程,易造成卡顿。
- 对于60HZ,一帧大约是16ms,一帧内需要完成js计算,页面的绘制和渲染。否则就会卡顿。
- 时间分片是指一帧中的空闲时间片段。
requestHostCallback 申请时间分片
调用该方法会向浏览器申请时间分片来执行其回调内容。
简单的说就是通过MessageChannel来和浏览器通讯,其MessageChannnel监听的onmessage会在浏览器空闲后执行,以实现时间分片方式来执行逻辑。
调度逻辑
-
仔细的流程调度图
-
大致的流程调度图
说明
-
scheduleCallback是开始执行React的更新调度逻辑。无论是同步更新任务workLoopSync还是有优先级的任务workLoopConcurrent,都是由调度器scheduleCallback统一调度的。
-
requestHostTimeout是未过期的更新任务执行的方法。用于创建定时器,根据剩余过期时间后执行handleTimeout方法。
-
调度workLoop会遍历taskQueue队列并依次申请时间分片来执行更新任务。
调和
render阶段是可中断,可恢复的调和阶段。commit阶段是同步,不可中断的真实DOM更新阶段。
什么是fiber
- fiber是真正的虚拟DOM。
- 引进可中断的异步更新,能有效的防止页面卡顿。其中断方式就是流程执行的终止,其恢复方式就是重新执行;并非恢复到原来的位置继续执行。
- diff对比也是基于fiber进行对比的。
fiber是如何建立关联的
每个element都会对应一个fiber,每一个fiber是通过return,child,sibling三个属性建立起联系的。
- return:指向父级fiber。
- child:指向第一个子fiber。
- sibling:指向同级兄弟节点的下一个fiber。
双缓存树 - workInProgress树和current树。
- workInProgress树是调和的fiber树,current是渲染真实DOM的fiber树。
- 调和过程中会复用rootFiber根节点的alternate作为workInProgress树来构建下次更新fiber树。
- 在构建调和好的workInProgress树后,该树可用于渲染真实DOM了。在commit阶段,current和workInProgress的引用会交换,最后current树就是最新调和好的fiber树,就可用于真实DOM的更新。
- 双缓冲图相对应的fiber节点间会通过alternate相互引用。fiber镜像。
- 渲染树和调和树的角色会交互替换使用。