React笔记-首次渲染
渲染机制
渲染机制主要分为两部分: 首次渲染和更新渲染。
首次渲染
首先通过一个小例子,来讲解首次渲染过程。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
import React from 'react';
import ReactDOM from 'react-dom';
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState((state) => {
return {count: state.count + 1};
});
}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>,
]
}
}
ReactDOM.hydrate(<ClickCounter />, document.getElementById('root'));
程序运行到ReactDOM.hydrate时,其中的<ClickCounter />已被babel转换为React.createElement(ClickCounter, null),生成的element如下:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
接下来执行hydrate函数,生成root节点。首先了解下fiber的部分数据结构。
- alternate(对应的
workInProgress或fiber) - stateNode(关联的
fiber,组件实例或者DOM节点) - type(组件或
HTML tag,如div,span等) - tag(类型,详见workTags)
- effectTag(操作类型,详见sideEffectTag)
- updateQueue(更新队列)
- memoizedState(
state) - memoizedProps(
props) - pendingProps(
VDOM) - return(父
fiber) - sibling(兄弟
fiber) - child(孩子
fiber) - firstEffect(第一个待处理的
effect fiber) - lastEffect(最后一个待处理的
effect fiber)
首次渲染会以同步渲染的方式进行渲染,首先创建一个update,将element装载到其payload属性中,然后合并到root.current.updateQueue,如果没有updateQueue会创建一个。我们暂且将root.current看成HostRoot。
接着根据HostRoot克隆一棵workInProgress更新树。将HostRoot.alternate指向workInProgress,workInProgress.alternate指向HostRoot。然后进入workLoop进行更新树操作部分。workLoop的任务也很简单,就是将所有节点的更新挂载到更新树上。下面详细看看reconciliation阶段。
reconciliation阶段
reconciliation的核心在于workLoop。workLoop会以workInProgress为起点,即克隆的HostRoot,不断向下寻找。如果workInProgress.child不为空,会进行diff;如果为空会创建workInProgress.child`。
// 第一次循环nextUnitOfWork为workInProgress
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until there's a higher priority event
while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
因为只涉及首次渲染,所以这里将performUnitOfWork简单化。beginWork根据workInProgress.tag选择不同的处理方式。先暂且看看如何处理HostRoot。进入updateHostRoot方法,先进行workInProgress.updateQueue的更新,计算新的state,将update.baseState和workInProgress.memoizedState指向新的state。这里新的state装载的是element。
接下来调用createFiberFromElement创建fiber,将workInProgress.child指向该fiber,fiber.return指向workInProgress。
function performUnitOfWork(workInProgress) {
let next = beginWork(workInProgress); // 创建workInProgress.child并返回
if (next === null) { // 没有孩子,收集effect list,返回兄弟或者父fiber
next = completeUnitOfWork(workInProgress);
}
return next;
}
function beginWork(workInProgress) {
switch(workInProgress.tag) {
case HostRoot:
return updateHostRoot(current, workInProgress, renderExpirationTime);
case ClassComponent:
...
}
}
用一张图体现更新树创建完成后的样子:

当workInProgress没有孩子时,即创建的孩子为空。说明已经到达底部,开始收集effect。
function completeUnitOfWork(workInProgress) {
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;
nextUnitOfWork = completeWork(workInProgress);
...// 省略收集effect list过程
if (siblingFiber !== null) {
// If there is a sibling, return it
// to perform work for this sibling
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber,
// continue the loop to complete the parent.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
}
}
}
function completeWork(workInProgress) {
//根据workInProgress.tag创建、更新或删除dom
switch(workInProgress.tag) {
case HostComponent:
...
}
return null;
}
协调算法过程结束后,workInProgress更新树更新完毕,收集的effect list如下:

commit阶段
effect list(链表)是reconciliation阶段的结果,决定了哪些节点需要插入、更新和删除,以及哪些组件需要调用生命周期函数。firstEffect记录第一个更新操作,firstEffect.nextEffect(fiber)记录下一个,然后继续通过其nextEffect不断往下寻找直至为null。下面是commit阶段的主要流程:
// finishedWork为更新树
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles();
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
变量nextEffect每次执行完上面一个函数会被重置为finishedWork。
commitBeforeMutationLifecycles
检查effect list中每个fiber是否有Snapshot effect,如果有则执行getSnapshotBeforeUpdate。
// 触发getSnapshotBeforeUpdate
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
commitAllHostEffects
提交所有effect,实现dom的替换、更新和删除。
function commitAllHostEffects() {
while(nextEffect !== null) {
var effectTag = nextEffect.effectTag;
var primaryEffectTag = effectTag & (Placement | Update | Deletion);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
...
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
var _current = nextEffect.alternate;
commitWork(_current, nextEffect);
...
}
case Update: {
var _current2 = nextEffect.alternate;
commitWork(_current2, nextEffect);
...
}
case Deletion: {// 触发componentWillUnmout
commitDeletion(nextEffect);
...
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitAllLifeCycles
触发componentDidMount或componentDidUpdate
function commitAllLifeCycles(finishedRoot, committedExpirationTime) {
while (nextEffect !== null) {
var effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
var current$$1 = nextEffect.alternate;
commitLifeCycles(finishedRoot, current$$1, nextEffect, committedExpirationTime);
}
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
if (effectTag & Passive) {
rootWithPendingPassiveEffects = finishedRoot;
}
nextEffect = nextEffect.nextEffect;
}
}
总结
这里并未逐一细说,不想读起来直犯困,更多讲述了大概流程。如果觉得有疑惑的地方,也知道该在什么地方找到对应的源码,解答疑惑。
更好的阅读体验在我的github,欢迎
React笔记-首次渲染的更多相关文章
- How React Works (一)首次渲染
How React Works (一)首次渲染 一.前言 本文将会通过一个简单的例子,结合React源码(v 16.4.2)来说明 React 是如何工作的,并且帮助读者理解 ReactElement ...
- React笔记_(3)_react语法2
React笔记_(3)_react语法2 state和refs props就是在render渲染时,向组件内传递的变量,这个传递是单向的,只能继承下来读取. 如何进行双向传递呢? state (状态机 ...
- react服务端渲染(同构)
学习react也有一段时间了,使用react后首页渲染的速度与seo一直不理想.打算研究一下react神奇服务端渲染. react服务端渲染只能使用nodejs做服务端语言实现前后端同构,在后台对re ...
- (十分钟视频教程)nodejs基础实战教程3:react服务端渲染入门篇
视频截图如下: (具体视频见文末) 前言: 这是小猫的第三篇node教程,本篇内容是由公众号粉丝票选得出的,相信大家对这篇教程是抱有较大希望的,这篇教程由小猫和一位多年的好朋友合作完成(笔名:谷雨,博 ...
- React 避免重渲染
组件的重新渲染 我们可以在 React 组件中的 props 和 state 存放任何类型的数据,通过改变 props 和 state,去控制整个组件的状态.当 props 和 state 发生变化时 ...
- React条件性渲染
React条件性渲染的方式和Vue是不同的,之前用vue做项目时觉得vue是在是强大,通过v-if就可以选择性的渲染组件,另外,对于列表的渲染更是方便,一个v-for就可以进行快速的渲染,但是Reac ...
- react基础学习和react服务端渲染框架next.js踩坑
说明 React作为Facebook 内部开发 Instagram 的项目中,是一个用来构建用户界面的优秀 JS 库,于 2013 年 5 月开源.作为前端的三大框架之一,React的应用可以说是非常 ...
- Electron结合React,在渲染进程中使用 node 模块
Electron结合React,在渲染进程中使用 node 模块 问题 将create-react-app与electron集成在了一个项目中.但是在React中无法使用electron.当在Reac ...
- React学习笔记 - 元素渲染
React Learn Note 3 React学习笔记(三) 标签(空格分隔): React JavaScript 二.元素渲染 元素是构成react应用的最小单位. 元素是普通的对象. 元素是构成 ...
随机推荐
- redis之禁用保护模式以及修改监听IP
今天在安装filebeat的时候,出现了关于redis报错的问题,所以来总结一下: 报错信息是: (error) DENIED Redis is running in protected mode b ...
- UML学生成绩管理系统需求分析
学生成绩管理系统工作室高校教育工作的一项重要内容.教务管理工作是指学校管理人员按照一定的教育方针,运用先进的管理手段,组织.协调.指挥并指导各用户活动,一边高效率.高质量地完成各项教学任务,完成国家所 ...
- 【Android自动化】Subprocess.check_output()简单用法
# -*- coding:utf-8 -*- import os import sys import subprocess from uiautomator import device as d cm ...
- java将Excel文件上传并解析为List数组
前端 //导入excel文件 layui.use('upload', function() { var upload =layui.upload; //指定允许上传的文件类型 var uploadIn ...
- JavaScript中的单例模式
单例模式 在JavaScript中,单例(Singleton)模式是最基本又最有用的模式之一.这种模式提供了一种将代码组织为一个逻辑单元的手段,这个逻辑单元中的代码可以通过单一的变量进行访问.确保单例 ...
- 【openjudge】【搜索(bfs)】P4980拯救行动
[描述:] 公主被恶人抓走,被关押在牢房的某个地方.牢房用N*M (N, M <= 200)的矩阵来表示.矩阵中的每项可以代表道路(@).墙壁(#).和守卫(x). 英勇的骑士(r)决定孤身一人 ...
- luogu P3690 【模板】Link Cut Tree (动态树)
嘟嘟嘟 LCT竟然看了整整一天,但好歹是看懂了. 教程这里不写,强烈推荐 闪狐大佬的博客 . 但是还是有几句想说的. 1.尽管LCT和splay很像,但是有一些细节还是不一样的.首先是rotate,我 ...
- tusen 刷题
//1.single number和变体 //2.lru lfu 3.给一个正整数集合,求一个和最大且能被3整除的子集.Follow up: 如果集合里有正有负 4.leetcode200-numbe ...
- 如何利用Grunt生成对应的Source Map文件,线上代码压缩使用chrome浏览器便于调式
如何利用Grunt生成对应的Source Map文件,线上代码压缩使用chrome浏览器便于调式 首先我们来说说为何要生成sourceMap文件呢?简单的说,sourceMap是为了压缩后的代码调式提 ...
- Python2.7-filecmp
filecmp 模块,定义了比较文件或目录的函数,比较文件只会有 True 和 False 两种结果,比较目录会返回目录下相同的文件,不同的文件,出错的文件.比较文件也可以用 difflib 模块,d ...