我跟着这篇文章 # 构建你自己的 React 一步一步的从零实现了一个自定义的基于Fiber的React,如果你也想这样的话,请直接点开这个连接,尽管内容是英文的,你可以用浏览器自带的谷歌翻译,一样会有很棒的体验OvO,祝顺利
自定义React的功能
- createElment
- render
- 并发模式
- fiber
- 渲染和提交
- 协调
- 函数组件
- hook
自定义React的运行流程
代码
index.js
小提示:如果想让babel调用我们自定义的createElement,需要加上两行注释
/** @jsxRuntime classic */,/** @jsx Didact.createElement */
/** @jsxRuntime classic */
import { Didact } from './Didact'
/** @jsx Didact.createElement */
function Counter(){
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1) }>
Count: {state}
</h1>
)
}
const element = <Counter></Counter>
const root =document.getElementById('root')
Didact.render(element, root)
Didact.js
/*
创建React元素---也就是虚拟DOM,为了区分,虚拟DOM称为React元素,真实DOM称为DOM
*/
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
//children数组里既有基本类型(String)又有对象类型(JSX元素),为基本类型创建一个元素,元素的类型为'TEXT_ELEMENT'
return typeof child === 'object' ? child : createTextElement(child)
})
}
}
}
//React 不会包装原始值或在没有 时创建空数组children,但我们这样做是因为它会简化我们的代码,
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)
Object.keys(fiber.props).filter(key => key !== 'children').forEach(name => {
if (isEvent(name)) {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, fiber.props[name])
}else{
dom[name] = fiber.props[name]
}
})
return dom
}
//在render函数中,创建根fiber,并设置为nextUnitOfWork,剩下的工作发生在performUnitOfWork函数上
function render(element, container) {
wipRoot = { // fiber对象
dom: container, // dom属性指向虚拟DOM对应的真实DOM
props: {
children: [element] //子元素
},
alternate: currentRoot //保存对旧fiber树的指针
}
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null //下一个工作单元
let wipRoot = null //我们将跟踪纤维树的根。我们将其称为进行中的根或wipRoot.
let currentRoot = null //保存上一个fiber树的引用
let deletions = null //保存将要删除的旧fiber
//循环工作函数
function workLoop(deadline) {
let shouldYield = false //是否交出执行权
while (nextUnitOfWork && !shouldYield) { // 有下一个工作单元 && 有执行权
nextUnitOfWork = performUnitOfWork(nextUnitOfWork) //执行当前工作单元,并返回下一个工作单元
shouldYield = deadline.timeRemaining() < 1 //判断是否有时间片
}
//一旦完成所有的工作(没有下一个工作单元),我们就将整个fiber树提交给DOM
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
//代码跳出while,有两点原因:1. 时间片已完,需要交出执行权 2. 已经没有下一个工作单元,任务已结束
//请求浏览器在空闲时执行此任务
//这里其实是一个无限循环,在每一帧都会进入workLoop
requestIdleCallback(workLoop)
}
//开启任务,请求浏览器在空闲时执行此任务
requestIdleCallback(workLoop)
/**
* 做了3件事
* 1. 为元素创建一个DOM
* 2. 为元素的子元素创建fiber
* 3. 选择下一个工作单元(fiber)
* @param {*} fiber 每个fiber也是最小的工作单元
*/
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
//1. 为元素创建一个DOM
//2. 为元素的子元素创建fiber
updateHostComponent(fiber)
}
// 3.最后我们寻找下一个工作单元,我们首先联系孩子,然后联系兄弟,然后与叔叔,等等
if (fiber.child) {
return fiber.child //如果有子fiber,则作为下一个工作单元返回
}
let nextfiber = fiber
while (nextfiber) {
if (nextfiber.sibling) { //如果有弟弟节点,则作为下一个工作单元返回
return nextfiber.sibling
}
nextfiber = nextfiber.parent //向fiber树上层查找,这一步是联系叔叔节点
// 一直向上查找,知道找到根节点,结束
}
}
/**
* 更新原生DOM组件
*/
function updateHostComponent(fiber) {
// 1. 为元素创建一个DOM
if (!fiber.dom) { //如果DOM不存在则创建 注意根fiber不会进入这行代码,因为根fiber的dom指向容器DOM
fiber.dom = createDOM(fiber)
}
//这里有一个问题,每次我们处理一个元素时,都会向DOM树上挂载一个DOM节点,
//但是浏览器在我们渲染完整个DOM树之前中断我们的工作,这是会显示一个不完整的UI
//我们并不希望这样,所以此时创建了DOM,但并不是挂载到DOM树上的好时机
/* if (fiber.parent) { //注意根fiber不会进入这行代码,因为根fiber没有parent属性
fiber.parent.dom.appendChild(fiber.dom) //将DOM挂载到DOM树上
} */
// 2. 为每一个子元素创建一个fiber,并维护一个fiber树
const elements = fiber.props.children //拿到fiber的所有子元素
reconcileChildren(fiber, elements)
}
//调用函数组件之前初始化一些全局变量,以便我们可以在useState函数内部使用它们
let hookIndex = null
let wipFiber = null
/**
* 函数式组件 在updateFunctionComponent我们运行函数来获取孩子。
*
* @param {*} fiber
*/
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)] //fiber.type 就是函数组件的那个函数,传入props,运行获取子元素
reconcileChildren(fiber, children) //协调子元素
}
function useState(initial) {
console.log('hookIndex', hookIndex)
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook?oldHook.state:initial,
queue: [], // action队列,保存对状态的修改
}
const actions = oldHook ? oldHook.queue:[]
actions.forEach( action =>{
hook.state = action(hook.state)
})
const setState = action =>{
hook.queue.push(action)
//然后我们做一些类似于我们在render函数中所做的事情,
//将一个新的正在进行的工作根设置为下一个工作单元,
//这样工作循环就可以开始一个新的渲染阶段
console.log('currentRoot', currentRoot)
//重新从根节点渲染
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let prevSibling = null //前一个兄弟
let oldFiber = wipFiber.alternate && wipFiber.alternate.child //获取到旧fiber
while (index < elements.length || oldFiber != null) {
const element = elements[index]
let newfiber = null
const sameType = oldFiber && element && element.type === oldFiber.type
//当旧的 Fiber 和元素的类型相同时, 我们创建一个新的 Fiber, 保留旧 Fiber 的 DOM 节点,更新元素的 props。
//我们还为纤维添加了一个新属性: effectTag.我们稍后会在提交阶段使用这个属性。
if (sameType) {
//更新这个节点
newfiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber, //记录旧fiber
effectTag: 'UPDATE',
}
}
//然后对于元素需要新的 DOM 节点的情况,我们用PLACEMENT效果标签标记新的fiber。
if (element && !sameType) {
//增加这个节点
newfiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null, //新增的没有fiber
effectTag: 'PLACEMENT'
}
}
if (oldFiber && !sameType) {
//删除旧节点
oldFiber.effectTag = 'DELETION'
deletions.push(oldFiber) //需要一个数组来跟踪将要删除的节点
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
//我们将新的fiber加入到fiber树中,具体加到孩子节点还是兄弟节点取决于第一个子元素(大儿子)
if (index === 0) { //是第一个子元素
wipFiber.child = newfiber //父fiber的child指向第一个子fiber
} else { //其他的子元素加入到兄弟节点
prevSibling.sibling = newfiber
}
prevSibling = newfiber //保存上一个兄弟fiber
index++
}
}
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) {
//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)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
//没有dom属性的fiber
let domParentFiber = fiber.parent
//我们需要找到DOM节点的父节点,沿着fiber树向上查找
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom)
} else if (fiber.effectTag === 'DELETION') {
//并且当移除一个节点时,我们还需要继续,直到我们找到一个带有 DOM 节点的子节点
commitDeletion(fiber, domParent)
} else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
export const Didact = {
createElement,
render,
useState
}