开篇
我们在阅读React源码的过程中,实际上会遇到很多的算法,当你了解这些算法的时候才能更好也更快的去阅读理解源码中的细节,不然我们总是在雾里看花(就是知道一个大概流程,当可能有时候面试官问深一点就G的那种阶段),所以这里就简单介绍一下React里大篇使用的算法。
链表简介
链表(Linked list)是一种常见的基础数据结构, 是一种线性表, 但是并不会按线性的顺序存储数据, 而是在每一个节点里存到下一个节点的指针(Pointer).由于不必须按顺序存储,链表在插入的时候可以达到 O(1)的复杂度, 但是查找一个节点或者访问特定编号的节点则需要 O(n)的时间。
节点:一个节点包含两个域,一个是数据域,数据域用来保存数据,一个是指针域,指针域代表当前节点的指向。
特点总结:1.链表在指定位置插入删除不需要移动元素,只需要修改指针。 2.查找效率相对于数组低很多。 3.链表同时也相对于数组多了指针域的空间开销。
链表分为:静态链表、动态链表。又向下细分为单向链表、双向链表、循环链表、单项循环链表、双向循环链表。
单链表:其实没什么好说的就是由头节点next不停的向下指,然后尾部最终指向一个null表示链表结束。
双链表:链表的向前是很容易的,而向后我们就可以定义一个prev属性的指针来指向于上个节点,去形成一个双向链表。
简单双向链表实现
/*
* @Description: 简单双向链表
* @Date: 2022-10-30 18:57:05
*/
class Node {
constructor(data) {
//数据域
this.data = data;
//表示节点域
this.next = null;
this.prev = null;
}
}
class LinkedList {
head = new Node("head");
/**
* 显示链表
*/
display() {
let currentNode = this.head;
while (currentNode !== null) {
console.log(currentNode.data);
currentNode = currentNode.next;
}
}
/**
* 查找节点
* @param {*} item 给定节点
*/
find(item) {
let currentNode = this.head;
while (currentNode !== null && currentNode.data !== item) {
currentNode = currentNode.next;
}
return currentNode;
}
/**
*
* @param {*} item 给定节点
* @param {*} newdata 插入的新节点
*/
insert(item, newData) {
let newNode = new Node(newData);
let currNode = this.find(item);
newNode.next = currNode.next;
newNode.prev = currNode;
if (currNode.next) {
currNode.next.prev = newNode;
}
currNode.next = newNode;
}
/**
* 查找前一个节点
* @param {*} item 给定节点
*/
findPrev(item) {
let currentNode = this.head;
while (currentNode !== null && currentNode.next.data !== item) {
currentNode = currentNode.next;
}
return currentNode;
}
/**
* 删除节点
* @param {*} item
*/
remove(item) {
let currNode = this.find(item);
if (currNode.prev !== null) {
currNode.prev.next = currNode.next;
}
if (currNode.next !== null) {
currNode.next.prev = currNode.prev;
}
currNode.next = null;
currNode.prev = null;
}
}
react与链表
fiber中的链表属性
effect链表(链式队列): 存储有副作用的子节点, 构成该队列的元素是fiber对象。
//下一个副作用链表中的fiber节点
nextEffect: Fiber | null,
//副作用指针指向第一个副作用链表的fiber节点
lastEffect: Fiber | null,
//副作用指针指向最后副作用链表的fiber节点
firstEffect: Fiber | null,
completeUnitOfWork回溯阶段
好这里我们看看源码的实现
function completeUnitOfWork(unitOfWork: Fiber): void {
//忽略代码
if (
returnFiber !== null &&
(returnFiber.flags & Incomplete) === NoFlags
) {
//只有父节点的firstEffect不存在时,才能将父节点的firstEffect指向当前节点的副作用单向链表头
if (returnFiber.firstEffect === null) {
//让父节点的firstEffect指向当前节点的firstEffect
returnFiber.firstEffect = completedWork.firstEffect;
}
//当前节点不存在副作用链表才加
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
// 将当前节点加到副作用链表中
returnFiber.lastEffect = completedWork.lastEffect;
}
const flags = completedWork.flags;
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
//将当前节点添加到其副作用链表末尾
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
}
这就是自底而上向父节点提交副作用链表,构建完整的副作用链表。但真的挺难理解的,所以后面18就直接抛弃洛。
Hook 对象
在react 高频对象中对Hook对象的属性做了说明, Hook对象具备.next属性, 所以Hook对象本身就是链表中的一个节点。
此外hook.queue.pending也构成了一个链表。
在react中, 发起更新之后, 会通过链表合并的方式把等待(pending状态)更新的队列(updateQueue)合并到基础队列(class组件:fiber.updateQueue.firstBaseUpdate;function组件: hook.baseQueue), 最后通过遍历baseQueue筛选出优先级足够的update对象, 组合成最终的组件状态(state). 这个过程发生在reconciler阶段, 分别涉及到class组件和function组件。
具体场景:
class组件中
在class组件中调用setState, 会创建update对象并添加到fiber.updateQueue.shared.pending链式队列(源码地址)。
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
const updateQueue = fiber.updateQueue;
// ...
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
// 将新的update对象添加到fiber.updateQueue.shared.pending链表上
const pending = sharedQueue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
由于fiber.updateQueue.shared.pending是一个环形链表, 所以fiber.updateQueue.shared.pending永远指向末尾元素(保证快速添加新元素)在fiber树构建阶段(或reconciler阶段), 会把fiber.updateQueue.shared.pending合并到fiber.updateQueue.firstBaseUpdate队列上(源码地址)。
export function processUpdateQueue<State>(
workInProgress: Fiber,
props: any,
instance: any,
renderLanes: Lanes,
): void {
// This is always non-null on a ClassComponent or HostRoot
const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
let firstBaseUpdate = queue.firstBaseUpdate;
let lastBaseUpdate = queue.lastBaseUpdate;
// Check if there are pending updates. If so, transfer them to the base queue.
let pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
queue.shared.pending = null;
// The pending queue is circular. Disconnect the pointer between first
// and last so that it's non-circular.
const lastPendingUpdate = pendingQueue;
const firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;
// Append pending updates to base queue
if (lastBaseUpdate === null) {
firstBaseUpdate = firstPendingUpdate;
} else {
lastBaseUpdate.next = firstPendingUpdate;
}
lastBaseUpdate = lastPendingUpdate;
}
}
function组件中
在function组件中使用Hook对象(useState), 并改变Hook对象的值(内部会调用dispatchAction), 此时也会创建update(hook)对象并添加到hook.queue.pending链式队列(源码地址)。
hook.queue.pending也是一个环形链表(与fiber.updateQueue.shared.pending的结构很相似)
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
// ... 省略部分代码
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
在fiber树构建阶段(或reconciler阶段), 会将hook.queue.pending合并到hook.baseQueue队列上(源码地址).
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
// ... 省略部分代码
if (pendingQueue !== null) {
if (baseQueue !== null) {
// 在这里进行队列的合并
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
}
总结
主要介绍了链表的概念和它在react源码中的使用情况. react链表使用非常高频。源码中链表合并, 环形链表拆解, 链表遍历的代码篇幅很多,大家可以多看看。