# React源码解析

  1. React源码v16.8.6 (opens new window)
  2. React核心源码解读 (opens new window)

# 1、整体结构

  • react:核心类库,hooks的api、element、context、ref、Component、lazy、memo等
  • react-dom:csr和ssr相关dom渲染以及test相关api,render、hydrate、renderToString等
  • react-recondiler:fiber架构核心

重点:fiber架构、diff算法

# 2、react

# 1、createElement

jsx通过babel的插件(babel-plugin-jsx-transform)转换为createElement函数,createElement函数最终输出Vnode节点对象

  • 头文件需引入React库(import React from 'react'),由于babel转换后会使用React.createElement函数
  • React17+版本后可不引入React库

TIP

jsx与模版的对比:

  1. jsx较复杂无法进行编译优化,只能进行简单转译,委托给babel进行编译未集成至框架侧

  2. vue框架使用模版语法,进行编译预处理优化提升性能,集成在框架侧

发展历程:

react采用自顶向下进行状态更新(无编译预处理优化),通过shouldComponentUpdate的api向开发者提供是否向下进行更新开关进行性能优化 -> Fiber架构(利用时间切片保证渲染不卡顿)-> Streaming Server Rendering with Suspense流式渲染

vue通过模版预处理进行不端优化,添加动静分离处理进行靶向更新,添加事件缓存等处理

//1、转换编译原理
/* 
	const title = <div className="title">Hello world!</div>
	const title = React.createElement('div', {className: 'title'}, 'Hello world!')
*/
//2、用法:最终输出vnode节点
/*
React.createElement(
  type, //标签名字符串、react组件类型、React fragment类型
  [props],
  [...children]
)
{	
	$$typeof: Symbol(react.element),
  type: 'div', 
  props:{
    className: 'div',
  	children: [
      {
        type: '',
        props: {
          nodeValue: 'Hello world!'
        }
      }
    ]
  },
  key: null,
  ref: null
}
*/
//3、对于嵌套组件、类组件(本质也是函数组件)、函数组件
/* 
const test = <div>
  <Container/>
  <div>age: 18</div>
</div>
vnode对象为:
{	
	$$typeof: Symbol(react.element),
  type: 'div', 
  props:{
    className: 'div',
  	children: [
  		{
  			$$typeof: Symbol(react.element),
        type: container,
        key: null,
  			ref: null,
        props: {}
      },
      {
      	$$typeof: Symbol(react.element),
        type: 'div',
        key: null,
  			ref: null,
        props: 'age: 18'
      }
    ]
  },
  key: null,
  ref: null
}
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
  • createElement简易实现
const isArry = Array.isArray
const isText = txt => typeof txt === 'string' || txt === 'number'

//数组转换
//解析:??类似||,不会进行控制转换
//1. obj || {}, 0\""\false\null\undefined为{}
//2. obj ?? {}, null\undefined为{}
const toArray = arr => isArry(arr ?? []) ? arr : [arr]

//数组扁平化
const flatten = arr =>
   [...arr.map(ar =>
       isArry(ar) ? [...flatten(ar)]
           : isText(ar) ? createTextVNode(ar) : ar)]

//正常节点转换
function createVNode(type, props, key, ref) {
   return {
       type, props, key, ref
   }
}

//文本节点转vnode
function createTextVNode(text) {
   return {
       type: '',
       props: {
           nodeValue: text + ""
       }
   }
}

export function createElement(type, props, ...kids) {
   props = props ?? {}
   kids = flatten(toArray(props.children ?? kids)).filter(Boolean)
   if (kids.length) props.children = kids.length === 1 ? kids[0] : kids

   const key = props.key ?? null
   const ref = props.ref ?? null

   delete props.key
   delete props.ref

   return createVNode(type, props, key, ref)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  • Fragment原理
//Fragment原理:<>,</>  -> <Fragment></Fragment>
export function Fragment(props){
  return props.children
}
1
2
3
4

# 2、cloneElement

element 元素为样板克隆并返回新的 React 元素

  • config 中应包含新的 props,keyref。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。
  • 新的子元素将取代现有的子元素,如果在 config 中未出现 keyref,那么原始元素的 keyref 将被保留
  • 常用在路由组件拦截中
<element.type {...element.props} {...props}>{children}</element.type>
1
//源码查看直接查看github,node_modules中为打包代码,简化代码
//https://github.com/facebook/react/blob/v16.8.6/packages/react/src/ReactElement.js
export function cloneElement(element, config, children) {
  let propName;
  const props = Object.assign({}, element.props);
  let key = element.key;
  let ref = element.ref;
  const self = element._self;
  const source = element._source;
  let owner = element._owner;
  if (config != null) {
    for (propName in config) {
      props[propName] = config[propName];
    }
  }

  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }
  return ReactElement(element.type, key, ref, self, source, owner, props);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 3、PureComponent

React.PureComponent 中以浅层对比 prop 和 state 的方式来实现了shouldComponentUpdate函数

//https://github.com/facebook/react/blob/v16.8.6/packages/react/src/ReactBaseClasses.js
function PureComponent(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;
  this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
//寄生组合继承
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

//https://github.com/facebook/react/blob/487f4bf2ee7c86176637544c5473328f96ca0ba2/packages/react-reconciler/src/ReactFiberClassComponent.js
//先执行shouldComponentUpdate函数,没有的话自动执行判断
if (typeof instance.shouldComponentUpdate === 'function') {
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    return shouldUpdate;
  }
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# 3、Fiber架构

  1. idle mdn (opens new window)
  2. useTransition Api (opens new window)
  3. generator mdn (opens new window)
  4. why not use generator (opens new window)
  5. MessageChannel mdn (opens new window)

# 1、调度器scheduler

利用时间切片轮转,将任务放在空闲时间执行,使用idle执行机制,在浏览器空闲时间执行

  1. 浏览器自带idle的api(requestIdleCallback),react未使用,react采用Messagechannel自己实现了idle机制,因为兼容性以及50ms渲染问题
  2. 不使用generator实现scheduler,原因:改动较大,以及generator有状态
  3. 不使用web-worker实现scheduler,原因:web-worker属于结构化克隆为深拷贝,消耗性能
  4. 为什么使用Messagechannel?不使用setTimeout、raf等
    • 事件循环中执行顺序为:task-queue(执行栈) -> micro-tasks(微任务) -> render -> macro-tasks(宏任务),为了不影响渲染需使用宏任务
    • setTimeout、setInterval等宏任务,时间有延迟,后期执行间隔为4ms-5ms
    • requestAnimationFrame由浏览器决定渲染时间,且递归调用有重复调用问题

scheduler干的事情:调度控制任务的进出

  • 把任务放进一个队列,然后开始(以某种节奏ric(requestIdleCallback)执行)
  • 判断是否可以执行:shouldYield -> should yield -> generator yield(本质返回true和false函数)

解析:当状态更新时,放入队列,等待调度器执行,调度器有序有节奏的执行任务队列,采用时间分片,在浏览器空闲时间执行

/* 解析:总体实现ric,每一帧16.667空闲时间执行任务
schedule:放入任务,开始执行startTranstion
startTranstion:transtions放入flush,触发宏任务,在宏任务中执行flush
flush:取出任务,判断是否超时及输入
 无,循环执行任务,清空队列
 有,不执行任务,执行startTranstion(flush),在下一个宏任务中执行任务
*/
const queue = []
const threshold = 1000 / 60 //浏览器1帧16.667ms

// git transtions
const transtions = []
let deadline = 0

const now = () => performance.now()
const peek = arr => arr[0]

export function startTranstion(cb) {
  transtions.push(cb) && postMessage()
}

// 二合一,push / exec
export function schedule(cb) {
  queue.push({ cb })
  startTranstion(flush)
}

//触发宏任务,渲染之后,执行宏任务
const postMessage = (() => {
  const cb = () => transtions.splice(0, 1).forEach(c => c())
  const { port1, port2 } = new MessageChannel()
  port1.onmessage = cb
  return () => port2.postMessage(null)
})()

export function shouldYield () {
  return navigator.scheduling.isInputPending() || now() >= deadline
}

function flush() {
  deadline = now() + threshold
  let task = peek(queue)
  while(task && !shouldYield()) {
    const { cb } = task
    task.cb = null
    //调度器只执行next,与fiber架构解耦
    const next = cb() 
    if (next && typeof next === 'function') {
      task.cb = next
    } else { //任务执行完
      queue.shift()
    }
    task = peek(queue)
  }
  task && startTranstion(flush)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# 4、Diff算法

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

Last Updated: 1/9/2022, 4:26:02 PM