(一)实现自己的React
1.1 createElement函数
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
以上jsx语法会被编译成createElement函数层层嵌套的形式:
const element = React.createElement(
"div",
{
id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
createElement函数会返回一个描述dom节点结构的对象,现在我们先实现自己的createElement函数。
这个函数返回一个有type属性和props属性的对象。并且我们新建一个Didact的对象,把createElement作为其的方法。
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
const Didact = {
createElement,
}
1.2 关于提高渲染性能的想法
我们如何去实现render函数呢?我们希望我们的渲染是可以中断的,如果我们的渲染不可中断,那么当dom树层级很深时,更新时间过长,用户交互就会卡顿,它必须等到渲染完成。所以我们要把工作分解成小单元,在我们完成每个单元之后,如果还有其他需要做的事情,我们会让浏览器中断渲染。
1.2.1 我们的工作单元是什么?
是fiber。每个元素都对应有一个fiber,而且每个fiber都是一个工作单元。
每个Fiber节点保存了该组件的类型(这里先只考虑原生组件,类型是"div","p"这些标签名),对应的DOM节点等信息,本次更新中该组件要执行的工作(需要被删除/被插入页面中/被更新…)等等。
Fiber节点对象的结构:
this.type // 组件的类型
this.props // 组件上的属性
this.dom // dom元素
this.parent // 指向父节点的fiber节点
this.sibling // 指向自己的兄弟节点
this.child // 指向第一个child的fiber节点
this.alternate // 指向和自己做比对的oldFiber节点
this.effectTag // "DELETION"|"UPDATE"|"PLACEMENT"
其中,fiber通过parent,sibling和child属性连接成一棵fiber树。
1.2.2 渲染的整体思路
根据确定好的工作单元确定我们的渲染的整体思路:
初次渲染:
我们拿到createElement返回给我们的虚拟dom树之后,我们去创建对应的fiber树,fiber树构建完成后,再通过递归遍历每个fiber节点,根据这个节点的effectTag属性(初次渲染时,每个节点的effectTag属性都是"PLACEMENT"),把新增的dom节点插入到页面上。
更新渲染:
我们拿到createElement返回给我们的新虚拟dom树之后,我们去创建对应的新fiber树,在创建每个新的fiber节点时,我们会和对应对旧的fiber节点做比对来确定这个新的fiber节点的effectTag属性(“UPDATE”|“PLACEMENT”),在比对新旧fiber节点的过程中,我们还会收集effectTag为‘DELETION’的old Fiber节点。新fiber树构建完成后,我们根据收集effectTag为‘DELETION’的old Fiber节点,删除页面上的dom。通过递归遍历每个新的fiber节点,根据这个节点的effectTag属性,把新增的dom节点插入到页面上/更新dom的属性。
1.2.3 规避出现不完整UI问题的思路
注意,我们在一边构建fiber树的时候,就一边把要新增的dom创建好挂在其fiber的dom属性下,但是没有添加到页面上,等fiber树构建完成,再才插入到页面。
为什么我们不选择在构建fiber树中,创建一个fiber节点的dom属性的时候,顺手把dom节点添加到页面上呢,因为执行单元任务(一点点构建fiber树的任务)是可能被中断的(注意我们刚说过,为了用户交互不卡顿,我们选择了可以中断的渲染方式),如果中断,用户将看到一个不完整的 UI,我们不希望这样,所以我们选择在fiber树构建完成后,一次性去把fiber树实现到页面中(这个过程是不会被打断的)。
1.2.4 中断任务的相关机制
我们用什么让我们渲染工作(构建fiber树)可以被中断?我们requestIdleCallback用来做一个循环。
你可以把它想象requestIdleCallback成一个setTimeout,但不是我们告诉它什么时候运行,浏览器会在主线程空闲时运行回调。requestIdleCallback还给了我们一个截止日期参数。我们可以使用它来检查在浏览器需要再次控制之前我们还有多少时间。
注意:我们可以中断的是创建fiber树的过程,后续将fiber树在页面上的实现是不可中断的。
1.3 关于1.2中想法的具体代码实现
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
// performUnitOfWork函数(构建fiber树的函数)
// 传入fiber节点,去给你构建其他的一些fiber节点,返回下一个要处理的fiber节点
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
// wipRoot(fiber树)
// 构建完成,一次性将创建的dom添加到页面上,而非在单元工作中一点点添加,
// 因为这个工作不会被中断,所以用户不会看到不完整的UI
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
上面是一段一直在循环运行的代码。
下面我们定义四个全局变量并实现render函数。
render函数:初始化wipRoot,nextUnitOfWork和deletions
let wipRoot = null
// wipRoot: Fiber树的根,进行中(work in progress)的根,
// 会在构建fiber树的过程中逐步完善成一整棵Fiber树。
let currentRoot = null
// currentRoot为当前渲染好的fiber树
let deletions = null
// 在构建新fiber树中,通过比对新旧fiber节点,收集起来的要删除的旧的fiber节点数组
// 当我们将fiber树提交到 DOM 时,我们是从正在进行的工作根中进行的,它没有旧fibers。
// 所以我们需要一个数组来跟踪我们想要删除的节点
let nextUnitOfWork = null
// 下一步要执行单元任务的fiber节点
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
const Didact = {
createElement,
render
}
const updateValue = e => {
rerender(e.target.value)
}
// render的调用
const rerender = value => {
// const element = (
// <div>
// <input onInput={updateValue} value={value} />
// <h2>Hello {value}</h2>
// </div>
// )
// 自行转成jsx形式的代码
const element = Didact.createElement('div',null,
Didact.createElement('input',{
'onInput':updateValue,'value':value}),
Didact.createElement('h2',null,'hello'+value)
)
Didact.render(element, container)
}
rerender("World")
render调用完成后,浏览器空闲时,performUnitOfWork(构建fiber节点的函数)被调用,传参nextUnitOfWork。
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
const isNew = (prev, next) => key =>
prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
// 1.创建传入的fiber节点的dom
// 2.创建传入的fiber节点的所有子节点的fiber节点
// 3.返回下一个要处理的工作任务(fiber节点)
function performUnitOfWork(fiber) {
if (!fiber.dom) {
// 根节点不会
// 新增的节点
// 如果是更新的节点不会,更新的节点在创建fiber的时候,用的是旧fiber的dom
// 删除的节点是旧节点,在wipRoot上是没有的,所以不会走这个函数
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
// 返回下一个要处理的节点:
// 返回fiber节点的优先级:
// 1.第一个子节点的fiber
// 2.右手边的兄弟fiber节点
// 3.叔叔fiber节点(父节点的兄弟节点)
// 4.爷爷的兄弟节点
// ...
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
// 创建dom节点,并且给dom节点上加上属性props
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {
}, fiber.props)
return dom
}
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
如何创建所有子节点的fiber?
// 处理所有的children,创建对应的新的fiber,并且用child,parent,siblings把他们联结起来。
function reconcileChildren(wipFiber, elements) {
let index = 0
// 当前处理的子元素的index
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
// 新节点要比对的旧节点
let prevSibling = null
// 存储上一个创建的fiber节点,以便设置它的sibling属性连结现在新创建的fiber节点
while (
index < elements.length ||
oldFiber != null
) {
const element = elements[index]
let newFiber = null
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
prevSibling = newFiber
index++
}
}
performUnitOfWork执行fiber节点的顺序是多叉树里的前序遍历,fiber树构建完成。
一旦我们完成了所有工作(我们知道这一点,因为没有下一个工作单元),我们将整个 Fiber 树提交给 DOM。
// 将新的fiber树实现到页面上
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
return
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
1.4 整体代码:
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
// 创建dom节点,并且更新节点上的属性
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {
}, fiber.props)
return dom
}
const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)
const isNew = (prev, next) => key =>
prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
// 处理dom节点的前后属性,更新其属性
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
// 最后的渲染工作: 就是把子节点挂载到父节点上面去
function commitRoot() {
deletions.forEach(commitWork)
console.log('deletions',deletions)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
// 这句是自己加的。
return
}
// 如果是删除的话,感觉后面没必要去递归了。
commitWork(fiber.child)
commitWork(fiber.sibling)
}
// 初始化nextUnitOfWork和wipRoot的
// 两者的区别,wipRoot是整个fiber树,nextUnitOfWork是fiber树的子树
function render(element, container) {
// 重新渲染的时候element变化了,container还是不变 ,alternate 是之前的wipRoot
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
// 一个单元一个单元的处理元素
// 1.创建元素自身的dom
// 2.创建元素所有子节点的fiber,并且建立联结关系
// 3.返回下一个要处理的节点
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
// 处理所有的children,创建新的fiber,并且用child,parent,siblings把他们联结起来。
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (
index < elements.length ||
oldFiber != null
) {
// 注意,如果oldFiber存在,那么这个循环还是会继续下去的
const element = elements[index]
let newFiber = null
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
const Didact = {
createElement,
render,
}
/** @jsx Didact.createElement */
const container = document.getElementById("root")
const updateValue = e => {
rerender(e.target.value)
}
const rerender = value => {
// const element = (
// <div>
// <input onInput={updateValue} value={value} />
// <h2>Hello {value}</h2>
// </div>
// )
// 自行转成jsx形式的代码
const element = Didact.createElement('div',null,Didact.createElement('input',{
'onInput':updateValue,'value':value}),
Didact.createElement('h2',null,'hello'+value)
)
Didact.render(element, container)
}
rerender("World")
(二)React16源码原理解析
2.1 React16架构
React16架构可以分为三层:
● Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
● Reconciler(协调器)—— 负责找出变化的组件
● Renderer(渲染器)—— 负责将变化的组件渲染到页面上
2.1.1 Scheduler(调度器)
React15中使用的不可中断的更新渲染,如果组件数量繁多,JS脚本执行时间过长,页面掉帧,造成卡顿。
React16为了解决这个问题,使用可中断的更新。
在浏览器每一帧的时间中,预留一些时间给JS线程,React利用这部分时间更新组件(在源码中,预留的初始时间是5ms)。当预留的时间不够用时,React将线程控制权交还给浏览器,React则等待下一帧时间到来继续被中断的工作。这种将长任务分拆到每一帧中,像蚂蚁搬家一样一次执行一小段任务的操作,被称为时间切片。
我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。
其实部分浏览器已经实现了这个API,这就是requestIdleCallback。但是React已经弃用了,React实现了功能更完备的requestIdleCallback polyfill,这就是Scheduler(react 16.6.3)。但是两者在概念上是一样的,都可以在空闲时触发回调的功能,Scheduler还提供了多种调度优先级供任务设置。
更新工作是可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
2.1.2 Reconciler(协调器)
在React16为了不让中断更新时DOM渲染不完全的问题出现。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样:
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
整个Scheduler与Reconciler的工作都在内存中进行,不会更新页面上的DOM,所以即使反复中断,用户也不会看见更新不完全的DOM。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。可以被中断的是Scheduler和Reconciler的工作,Render是不会被中断的。
2.1.3 Renderer(渲染器)
Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。
2.2 Fiber架构
2.2.1 Fiber的三层含义:
1.作为架构来说,React16的Reconciler基于Fiber节点实现,被称为Fiber Reconciler。
每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:
// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;
2.Fiber作为一种静态的数据结构,保存了组件相关的信息:
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,
// 对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;
3.作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息,
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
2.2.1 Fiber的架构的工作原理—双缓存Fiber树
当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。
如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。
为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。
在内存中构建并直接替换的技术叫做双缓存。
React使用“双缓存”来完成Fiber树的构建与替换:
在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。workInProgress fiber的创建可以复用current Fiber树对应的节点数据。current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。