react介绍 - 更新中...

118 阅读4分钟

react组件

react的出现让创建交互式UI变成轻而易举。在react世界里,函数和类就是UI的载体,我们甚至可以理解为,将数据传入react的类或者函数中,返回的就是UI界面。

  • 组价就是生成UI的载体。若结合函数组件,那组件就是输入props返回UI的函数。
  • 组件核心思想:UI=render(props)\color{red}{UI = render(props)}
  • 组件是复用的基石。
  • 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。
IMG_8EA48212CF21-1.jpeg

state深入解析

react的异步更新是通过‘批量更新’方式实现的。将多次setState的结果值合并起来,然后汇总做一次更新渲染。

  • 是同步渲染还是异步渲染。 在legacy模式下,异步执行的代码里的setState是同步的。根本原因是因为异步代码执行的时候其异步更新标记已关闭,导致后续执行的setState会触发同步渲染。
    IMG_0061.jpg

  • 在concurrent模式下的一次渲染。react会合并之前的更新任务做一次数据的汇总,然后执行一次更新渲染。即使在flushSync()里也是一样的。
    在concurrent模式下已是异步渲染。即使是异步执行的代码其setState依然是异步渲染的。除非手动触发次同步渲染flushSync() IMG_0059.jpg

手动触发次同步渲染flushSync()的代码分析。 IMG_9D1AC05FE0CB-1.jpeg

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调和时会识别为发生更新,从而触发了组件的更新渲染。

总结: IMG_720295106643-1.jpeg

生命周期

IMG_7C5A75819A1A-1.jpeg

  • 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事件执行说明

IMG_F073430D2B2A-1.jpeg

  • 当原生事件触发时。
    获取到目标原生DOM。然后通过_reactFilber$ + {randomKey}引用来获取原生DOM对应的fiber节点。

  • 合成事件源。
    用原生事件源event + 对应的事件处理插件plugin = React事件源

  • 收集事件处理方法形成列队。
    从目标filber沿着return引用向上遍历,收集对应的React事件处理方法。

  • 执行React事件。
    遍历事件列队依次执行事件处理方法,并传入React事件源作为参数。

说明

  • React事件阻止冒泡为什么只能调用stopPropagation方法,不能在事件处理方法里返回false。
    因为React事件只是普通函数的执行,当其显示的调用stopPropagation方法后,React在下次的事件队列遍历时会优先检查isPropagationStopped,如果是则中止。效果上就达到了阻止冒泡效果。

异步调度

  • 以前的同步渲染会造成长时间占用主线程,易造成卡顿。
  • 对于60HZ,一帧大约是16ms,一帧内需要完成js计算,页面的绘制和渲染。否则就会卡顿。
  • 时间分片是指一帧中的空闲时间片段。

requestHostCallback 申请时间分片

调用该方法会向浏览器申请时间分片来执行其回调内容。 IMG_D305D337455C-1.jpeg 简单的说就是通过MessageChannel来和浏览器通讯,其MessageChannnel监听的onmessage会在浏览器空闲后执行,以实现时间分片方式来执行逻辑。

调度逻辑

  • 仔细的流程调度图 IMG_FFD1DB9DC35B-1.jpeg

  • 大致的流程调度图 IMG_85182E813751-1.jpeg

说明

  • scheduleCallback是开始执行React的更新调度逻辑。无论是同步更新任务workLoopSync还是有优先级的任务workLoopConcurrent,都是由调度器scheduleCallback统一调度的。
    IMG_2843719FA6F2-1.jpeg

  • requestHostTimeout是未过期的更新任务执行的方法。用于创建定时器,根据剩余过期时间后执行handleTimeout方法。 IMG_8CB04B5F424C-1.jpeg

  • 调度workLoop会遍历taskQueue队列并依次申请时间分片来执行更新任务。 IMG_1E60C1274DB0-1.jpeg

调和

render阶段是可中断,可恢复的调和阶段。commit阶段是同步,不可中断的真实DOM更新阶段。

什么是fiber

  • fiber是真正的虚拟DOM。
  • 引进可中断的异步更新,能有效的防止页面卡顿。其中断方式就是流程执行的终止,其恢复方式就是重新执行;并非恢复到原来的位置继续执行。
  • diff对比也是基于fiber进行对比的。 IMG_1EFA17ED5479-1.jpeg

fiber是如何建立关联的

每个element都会对应一个fiber,每一个fiber是通过return,child,sibling三个属性建立起联系的。

  • return:指向父级fiber。
  • child:指向第一个子fiber。
  • sibling:指向同级兄弟节点的下一个fiber。

IMG_5C64DC81BF9B-1.jpeg

双缓存树 - workInProgress树和current树。

  • workInProgress树是调和的fiber树,current是渲染真实DOM的fiber树。
  • 调和过程中会复用rootFiber根节点的alternate作为workInProgress树来构建下次更新fiber树。
  • 在构建调和好的workInProgress树后,该树可用于渲染真实DOM了。在commit阶段,current和workInProgress的引用会交换,最后current树就是最新调和好的fiber树,就可用于真实DOM的更新。
  • 双缓冲图相对应的fiber节点间会通过alternate相互引用。fiber镜像。
  • 渲染树和调和树的角色会交互替换使用。 IMG_3032F91C0627-1.jpeg