深入浅出react(带你手写一个简易版react)
前言
大家好,本篇是进阶学习react的首篇文章。看完会学习到以下知识,希望大家喜欢。
react
基本工作流程。- 什么是
工作单元
- 什么是
fiber
,实现fiber
结构树 函数组件
的基本原理useState
的基本原理
介绍
我们在使用react
程序时,其实只需要三个步骤
- 定义一个
react
元素(JSX
) - 获取
DOM
中的一个节点 - 将
react
元素渲染到容器中
const element = <h1 title='nihao'>hello world</h1>
const container = document.getElementById('app')
ReactDOM.render(element, container)
现在开始,我们一步一步对上面三行代码进行改造。
第一行:首先我们js
是识别不了jsx
语法的,所以对于react
元素,我们要转换一下,转换成我们js
认识的代码,通过调用React.createElement
进行装换。
JSX 通过 Babel 等构建工具转换为 JS。转换通常很简单:在React中使用
createElement
,将type
、props
和children
作为参数传递。
const element = React.createElement(
'h1',
{title:'nihao'},
'hello world'
)
React.createElement
会根据它的参数创造一个对象。
const element = {
type:'h1',
props:{
title:'nihao',
children:'hello world'
}
}
这就是一个元素,它有两个属性(其实有很多,现在我们只关心这两个)。
- type:是一个字符串类型,表示我们要创建的DOM节点类型。
- props:它具备JSX属性中的所有键值对,同时还有一个特殊的属性
children
.它表示此元素的子元素,此时为string类型
,但也有可能为Array类型。
目前元素已经有了,渲染容器也有,我们开始改写ReactDOM.render
这个方法。
const element = {
type:'h1',
props:{
title:'nihao',
children:'hello world'
}
}
const container = document.getElementById('app');
// 为元素创造节点
const node = document.createElement(element.type);
// 给元素title属性赋值
node['title'] = element.props.title;
// 创建text节点
const text = document.createTextNode("");
// 给text节点赋值
text['nodeValue'] = element.props.children;
// 将text节点添加到node节点里
node.appendChild(text);
// 将node节点添加到container节点里
container.appendChild(node);
- 首先,知道元素类型,为此元素创建一个节点
- 拿到
props
中关于此元素的属性进行赋值。 - 为
children
创建一个文本节点(此情况下为文本,所以创建文本节点) - 给文本节点进行赋值
- 将文本节点添加到元素节点里
- 将元素节点添加到容器里
为此,我们已经完成了整个操作,在没有使用
React
的情况下,有了和一前一样的操作。
实现createElement功能
这一章,我们来学习createElement
功能.
首先,我们要知道createElement
函数功能是干嘛的,通过上面的例子我们了解到,它的作用是创造一个带有type
和props
等的一个对象。
function createElement(type,props,...children){
return {
type,
props:{
...props,
children
}
}
}
我们发现,这个时候我用...children
扩展运算法来接收子元素,所以children
的值就一定是一个数组了。
createElement("div"):
{
type:'div',
props:{
children:[]
}
}
createElement("div",{title:'hello'}):
{
type:'div',
props:{
title:'hello'
children:[]
}
}
createElement("div",{title:'hello'},'hi'):
{
type:'div',
props:{
title:'hello'
children:['h1']
}
}
实现render功能
render
功能其实就是将元素添加到容器里(其实还有更新和删除功能,后续再说。)
我们首先使用元素类型创建 DOM
节点,然后将新节点附加到容器中。
function render(element, container) {
const dom = document.createElement(element.type)
container.appendChild(dom)
}
其次,我们需要递归children
为每个孩子做一样的事情
function render(element, container) {
const dom = document.createElement(element.type);
// 新增============================================================
element.props.children.forEach(child =>
render(child, dom)
)
// ===========================================================
container.appendChild(dom)
}
最后,我们需要把元素的属性分配给节点
function render(element, container) {
const dom = document.createElement(element.type);
// 新增============================================================
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
// ===========================================================
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
什么是工作单元
在上面的render
函数中,其实有个问题,就是递归调用
.
一旦我们调用render
函数进行渲染后,我们不会停止,直到我们渲染了完整的元素树。如果元素树很大,可能会阻塞主线程太久。如果浏览器需要做高优先级的事情,比如处理用户输入或保持动画流畅,它必须等到渲染完成。
所以我们要把工作分解成小单元
,在我们完成每个单元之后,如果还有其他需要做的事情,我们会让浏览器中断渲染
。所以我们需要知道,何时浏览器是空闲状态,刚好浏览器提供一个apirequestIdleCallback
,浏览器空闲的时候就会调用我们传入的函数workLoop。
React
是自己实现了一套类似的requestIdleCallback
机制,不过大同小异。
function workLoop(){
// todo
}
requestIdleCallback(workLoop)
requestIdleCallback
还给了我们一个截止日期参数。我们可以使用它来检查在浏览器需要再次控制之前我们还有多少时间。 通过剩余时间我们可以使用循环
,同时我们要设置一个工作单元
,然后编写一个函数,执行本次工作单元且返回下次工作单元任务
。
// 工作单元
let nextUnitOfWork = null
function workLoop(deadline) {
// 控制时间是否结束
let canWork = false
while (nextUnitOfWork && !canWork) {
// 执行本次工作单元同时返回下次工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 判断控制时间是否结束
canWork = deadline.timeRemaining() < 1
}
// 控制时间结束,调用requestIdleCallback等待执行下次workLoop
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// todo
}
实现Fibers
为了组织一个个的工作单元,我们需要一个数据结构:Fiber
。
我们会为每个元素使用一个Fiber
,每个fiber
也是一个工作单元
.
假设我们要渲染下面这个元素树:
<div>
<h1>
<p>
<a></a>
</p>
</h1>
<h2></h2>
</div>
可以看一下他的Fiber
结构:
在
render
函数中,我们将创建根fiber
并将其设置为nextUnitOfWork
. 其余的工作将发生在performUnitOfWork
函数上,我们将为每个fiber
做三件事:
- 将元素添加到DOM中
- 为元素的子元素创建
Fiber
- 选择下一个工作单元
我们再来看看之前图上的箭头含义:
每个
Fiber
都会连接它的第一个孩子,下一个兄弟姐妹以及和父母有一个链接。这种数据结构的目标之一是使查找下一个工作单元变得容易。
那么整体的工作流程是什么样:首先我们完成divFiber
工作时,会判断它有没有子元素,如果有就移动到子元素,如果没有子元素,我们就会去找它的兄弟元素,如果没有兄弟元素,就会回到它的父节点,继续寻找校兄弟元素,直到我们到达了根元素,就意味着完成了所有render
所以再render函数中,我们要设置根fiber
,其实这个根fiber
就是容器,然后将创建dom属性赋值的逻辑抽离出来
function createDom(fiber){
const dom = document.createElement(fiber.type);
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
// 工作单元
let nextUnitOfWork = null
当浏览器准备就绪的时候,他就会执行workloop函数
,我们开始编写里面的performUnitOfWork
函数功能:
1.添加dom节点
function performUnitOfWork(fiber) {
// 判断是否存在,不存在就创建dom节点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 判断是否有父元素,有就添加进父元素里
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
}
2. 创造一个新的fibers
// 拿到children元素
const elements = fiber.props.children
let index = 0
let prevSibling = null
// 开始循环
while (index < elements.length) {
const element = elements[index]
// 创建fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 判断是否为首个孩子
if (index === 0) {
// 父fiber的child指向首个child
fiber.child = newFiber
} else {
// prevSibling的sibling指向新的fiber
prevSibling.sibling = newFiber
}
// prevSibling指向新的child
prevSibling = newFiber
index++
}
3. 返回下一个工作单元
// 判断是否有child,有就直接返回
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
// 开始循环查找,首先判断是否有兄弟元素,没有就从父级的兄弟找,直到循环到根元素
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
完整代码
function performUnitOfWork(fiber) {
// 1.判断是否存在,不存在就创建dom节点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 判断是否有父元素,有就添加进父元素里
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// 拿到children元素
const elements = fiber.props.children
let index = 0
let prevSibling = null
// 2.开始循环
while (index < elements.length) {
const element = elements[index]
// 创建fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 判断是否为首个孩子
if (index === 0) {
// 父fiber的child指向首个child
fiber.child = newFiber
} else {
// prevSibling的sibling指向新的fiber
prevSibling.sibling = newFiber
}
// prevSibling指向新的child
prevSibling = newFiber
index++
}
// 3.
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
// 开始循环查找,首先判断是否有兄弟元素,没有就从父级的兄弟找,直到循环到根元素
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
这就是我们的performUnitOfWork
.
render阶段优化
我们这里还有另一个问题。
每次我们处理一个元素时,我们都会向 DOM
添加一个新节点。而且,请记住,浏览器可能会在我们完成渲染整个树之前中断我们的工作。在这种情况下,用户将看到一个不完整的 UI
。这样子体验肯定不好。所以我们要修改一下添加dom的代码,不能每次创建的时候都进行添加。我们需要在所有fiber创建完成之后(即下一个工作单元为根fiber
)
function performUnitOfWork(fiber) {
// 1.判断是否存在,不存在就创建dom节点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 移出 ++++++++++++++++++++++++++++++++++++++++++++++++++
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }
// 移出 ++++++++++++++++++++++++++++++++++++++++++++++++++
// 拿到children元素
定义一个根fiber变量
function render(element, container) {
rootFiber = {
dom: container,
props: {
children: [element],
},
}
// 新增
nextUnitOfWork = rootFiber;
}
// 根fiber
let rootFiber = null;
判断当循环回到根fiber时
,我们将fiber树提交给dom,在递归的添加所有节点到dom中。
function workLoop(deadline) {
// 控制时间是否结束
let canWork = false
while (nextUnitOfWork && !canWork) {
// 执行本次工作单元同时返回下次工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 判断控制时间是否结束
canWork = deadline.timeRemaining() < 1
}
// 新增 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
if (!nextUnitOfWork && rootFiber) {
commitRoot()
}
// 新增 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 控制时间结束,调用requestIdleCallback等待执行下次workLoop
requestIdleCallback(workLoop)
}
function commitRoot(){
// todo 创建节点到dom中
}
如何更新和删除节点
进行commitRoot函数编写
// 提交根元素开始添加节点
function commitRoot(rootFiber) {
commitWork(rootFiber.child)
rootFiber = null
}
// 递归添加节点
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
核心工作就是一个递归进行添加节点
上面的代码我们只有添加节点的功能,下面就是实现如何更新和删除节点。更新和删除肯定需要对比,对比新的fiber树和旧的fiber树,所以我们需要将每次生成的fiber进行一个引用,用来与新的fiber树进行对比。我们称之为currentRoot
。我们还需要将该alternate
属性添加到每根fiber
中。此属性是旧fiber
的链接,旧fiber
是我们在前一个提交阶段提交到 DOM
的纤程。
function commitRoot(rootFiber) {
commitWork(rootFiber.child)
// 新增 +++++++++++++++++++++++++++++++++
currentRoot = rootFiber
// 新增 +++++++++++++++++++++++++++++++++
wipRoot = null
}
function render(element, container) {
rootFiber = {
dom: container,
props: {
children: [element],
},
// 新增 +++++++++++++++++++++++++++++++++
alternate: currentRoot,
}
nextUnitOfWork = rootFiber;
}
现在我们将performUnitOfWork
中创建fiber
的逻辑抽出来
function reconcileChildren(fiber, elements){
let index = 0
let prevSibling = null
// 2.开始循环
while (index < elements.length) {
const element = elements[index]
// 创建fiber
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 判断是否为首个孩子
if (index === 0) {
// 父fiber的child指向首个child
fiber.child = newFiber
} else {
// prevSibling的sibling指向新的fiber
prevSibling.sibling = newFiber
}
// prevSibling指向新的child
prevSibling = newFiber
index++
}
}
在这个函数中,我们就需要对比旧的fiber
和新的元素
了。最重要的地方:oldFiber
和element
。这element
是我们想要渲染到 DOM 的东西,oldFiber
也是我们上次渲染的东西。我们需要比较他们以查看是否需要对DOM应用的任何修改
为了比较它们,我们是用一下三种判断:
- 如果旧的
Fiber
和新的元素具有相同的类型,我们可以保留DOM
节点并使用新的props
更新它 - 如果类型不同并且有一个新元素,则意味着我们需要创建一个新的
DOM
节点 - 如果类型不同并且有
旧fiber
,我们需要删除旧节点
function reconcileChildren(fiber, elements){
let index = 0
let prevSibling = null
let oldFiber = fiber.alternate && fiber.alternate.child;
while (index < elements.length) {
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(elment && !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) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
我们还为fiber添加了一个新属性:effectTag
. 我们稍后会在commit
阶段使用这个属性.目前有三个类型
- UPDATE:更新节点
- PLACEMENT:添加新节点
- DELETION:删除节点
对于我们需要删除的
fiber
时,因为新的fiber树
是没有这个fiber
,所以我们需要对删除的fiber
进行一个标记。通过deletions
属性来来记录要删除的fiber
.
function commitRoot(rootFiber) {
// 新增 ++++++++++++++++++++++++++++++++++++++++
deletions.forEach(commitWork)
// 新增 ++++++++++++++++++++++++++++++++++++++++
commitWork(rootFiber.child)
currentRoot = rootFiber
rootFiber = null
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
// 新增 ++++++++++++++++++++++++++++++++++++++++
deletions = []
}
现在,让我们更改commitWork
函数来处理新的effectTags
.
如果fiber
有一个PLACEMENT
效果标签,我们和以前一样,将 DOM 节点附加到父fiber
的节点上。如果它是 DELETION
,我们做相反的事情,删除孩子。
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 === "DELETION") {
domParent.removeChild(fiber.dom)
}
// 新增 ++++++++++++++++++++++++++++++++++++++++
commitWork(fiber.child)
commitWork(fiber.sibling)
}
如果是UPDATE
,我们需要用改变的 props
更新现有的 DOM
节点,我们将在这个updateDom
函数中完成它。操作步骤如下:
我们将旧 Fiber 的 props 与新 Fiber 的 props 进行比较,移除掉掉的 props,设置新的或更改的 props。我们需要更新的一种特殊道具是事件侦听器,因此如果道具名称以“on”前缀开头,我们将以不同的方式处理它们。如果事件处理程序发生更改,我们将其从节点中删除。
function updateDom(dom, prevProps, nextProps) {
//删除旧的或者改变的节点
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]
)
})
// 删除旧的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 添加新的属性或者修改的属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// 添加新的事件
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}
实现函数组件
首先我们先看下面这个例子:
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
我们将jsx代码转换成js代码就变成下面这个样子:
function App(props) {
return React.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = React.createElement(App, {
name: "foo",
})
函数组件在两个方面有所不同:
- 功能组件的
fiber
没有 DOM 节点 - 并且孩子们来自运行函数而不是直接从
props
. 我们检查fiber
类型是否是一个函数,根据不同的类型我们调用不同的更新函数。在updateHostComponent
我们做和以前一样的事情。
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
在updateFunctionComponent
我们运行函数来获取孩子。对于我们的示例,这里fiber.type
是App
函数,当我们运行它时,它会返回h1
元素。
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
接下来需要改变的是commitWork
函数。现在我们有了没有 DOM
节点的fiber
,我们需要改变两件事:
- 要找到
DOM
节点的父节点,我们需要沿着fiber树
向上查找,直到找到具有DOM
节点的fiber
。 - 并且当移除一个节点时,我们还需要继续,直到我们找到一个带有
DOM
节点的子节点。
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
// 新增 ++++++++++++++++++++++++++++++++++++++++
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
// 新增 ++++++++++++++++++++++++++++++++++++++++
const domParent = domParentFiber.dom
......
}
实现useState
下面的例子为一个简单的计数器组件,每次点击后数字加1。下面我们开始实现。(setState已传入函数为例子)
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
我们需要在调用函数组件之前初始化一些全局变量,方便我们可以在useState
函数内部使用它们。
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
我们通过wipFiber
来追踪当前的fiber
,通过hookIndex
我们可以追踪当前的hook
。同时设置hooks
属性为一个数组,以支持useState在同一个组件多次调用useState
当函数组件调用useState
时,我们检查是否有旧的钩子。我们alternate
使用钩子索引检查fiber
的。
如果我们有一个旧钩子,我们将状态从旧钩子复制到新钩子,否则我们初始化状态。
然后我们将新的钩子添加到fiber
中,将钩子索引加一,并返回状态。
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state]
}
useState
还应该返回一个函数来更新状态,所以我们定义了一个setState
接收动作的函数(Counter
例如,这个动作是将状态加一的函数)。我们将该动作推送到我们添加到钩子中的队列中。
然后我们做一些类似于我们在render
函数中所做的事情,将一个新的正在进行的工作根设置为下一个工作单元,这样工作循环就可以开始一个新的渲染阶段。
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
const setState = action => {
// 执行函数推送到队列中
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state,setState]
}
但是我们还没有运行该操作。
我们在下一次渲染组件时这样做,我们从旧的钩子队列中获取所有动作,然后将它们一一应用到新的钩子状态,所以当我们返回状态时,它会更新。
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue:oldHook ? oldHook.queue : []
}
// 新增+++++++++++++++++++++++++++++++++++++++++++++
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
// 执行每次推送的执行函数
hook.state = action(hook.state)
})
// 新增+++++++++++++++++++++++++++++++++++++++++++++
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state,setState]
}
模拟整个计数器组件的过程:
- 第一步初始化:
hook.state
为1
,hook.queue为[]
- 第二步点击计数器:我们会将执行函数
c=>c+1
push到hook.queue
中,设置根fiber
为下次工作单元,等待执行。 - 第三步工作单元执行:执行
hook
所在的函数组件,执行useState
函数,oldhook
为上次创建的hook
,新的hook
对象state值为1
,queue
为[c=>c+1]
,遍历queue数组
里的每个执行函数,hook.state
作为参数,同时返回的值赋给hook.state
,最后将最新的值返回出去完成响应式。
全部代码
// 创建react元素
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children
},
}
}
// 创建节点
function createDom(fiber) {
const dom = 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)
// 更新节点
function updateDom(dom, prevProps, nextProps) {
//移出旧的或者改变的事件
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]
)
})
// 移出旧的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})
// 设置新的(改变的)属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})
// 添加新的事件
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)
// 执行根元素的子节点(一般是我们的容器节点)
commitWork(wipRoot.child)
// 工作单元执行完毕,通过currentRoot进行保存
currentRoot = wipRoot
wipRoot = null
}
// 执行每个工作单元
function commitWork(fiber) {
if (!fiber) {
return
}
// 父fiber
let domParentFiber = fiber.parent
// 寻找带有dom属性的父fiber(函数组件没有dom属性)
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
// 通过effectTag标识进行新增删除
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") {
commitDeletion(fiber, domParent)
}
// 执行孩子的工作单元
commitWork(fiber.child)
// 执行相邻的工作单元
commitWork(fiber.sibling)
}
// 删除节点
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
// 函数组件需要找到带有dom属性的元素进行删除
commitDeletion(fiber.child, domParent)
}
}
// 渲染函数
function render(element, container) {
// 设置根元素
wipRoot = {
dom: container,
props: {
children: [element],
},
// alternate属性保存旧的fiber树
alternate: currentRoot,
}
// 删除元素的集合
deletions = []
// 设置下一个工作单元
nextUnitOfWork = wipRoot
}
// 下次执行单元
let nextUnitOfWork = null
// 旧的fiber树
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)
}
// 浏览器空闲时会执行workLoop函数
requestIdleCallback(workLoop)
// 执行传入的工作单元,并返回下一个工作单元
function performUnitOfWork(fiber) {
// 判断是否为函数组件
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// 有孩子元素返回孩子元素
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
// 返回兄弟元素
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
// 本次执行的fiber节点
let wipFiber = null
let hookIndex = null
// 更新函数组件
function updateFunctionComponent(fiber) {
wipFiber = fiber
// hook索引
hookIndex = 0
// 本函数组件hook集合
wipFiber.hooks = []
// 执行函数组件 得到子元素
const children = [fiber.type(fiber.props)]
// 为子元素创建或者更新fiber
reconcileChildren(fiber, children)
}
// hook
function useState(initial) {
// 获取旧的hook,如果没有表示第一次调用
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
// hook对象的值进行初始化,有旧的hook时读取旧的hook值
const hook = {
// 值
state: oldHook ? oldHook.state : initial,
// 数组里面存放每次更新的执行函数
queue: oldHook ? oldHook.queue : [],
}
// 遍历这个hook的所有执行函数,并以此调用赋值
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
// 每次调用获取执行后的值
hook.state = action(hook.state)
})
// 更新函数
const setState = action => {
// 执行函数存放进queue属性中
hook.queue.push(action)
// 设置wipRoot为旧fiber树
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
// 设置下次执行单元为wipRoot
nextUnitOfWork = wipRoot
deletions = []
}
// 将hook添加进hooks中,并与hookIndex一一对应
wipFiber.hooks.push(hook)
hookIndex++
// 返回值和更新函数
return [hook.state, setState]
}
// 更新组件
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
// 为子元素创建fiber节点 对比新旧节点进行打标签
function reconcileChildren(wipFiber, elements) {
// 子元素的索引
let index = 0
// 旧节点
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
// 相邻节点
let prevSibling = null
// 循环
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 (oldFiber) {
oldFiber = oldFiber.sibling
}
// 如果是首个子节点,将父节点的child执行本节点
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
总结
喜欢的可以点个赞收藏哦,如果表述有问题或者不对的地方及时指出来哈,欢迎大家提问题。
转载自:https://juejin.cn/post/7152093251619520549