最近了解了Fiber树,发现对react的原理不太熟悉,出于对技术的好奇心就查询资料实现了一下React的基本代码以及useState、useEffect。
前言
fiber树节点结构
newFiber = {
type: '',
props: '',
dom: '',
parent: '',
alternate: '',
effectTag: '',
sibling: '',
child: ''
}
type—— 用于存放标签名如<h1>、<span>,如果是函数组件的话那么存放的就是该函数props—— dom节点上的属性dom—— 存放真实的dom元素parent—— 指向父节点alternate—— 存放历史节点effectTag—— 存放更新标签,如UPDATE、PLACEMENT、DELETIONsibling—— 指向相邻的下一个兄弟节点child—— 指向第一个孩子节点
Tip:如果代码还是不好理解可以把项目拉下来打断点,这样更容易理解数据的流向以及具体的呈现形态。
gitee地址: gitee.com/stone710/my…
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: []
}
}
}
// 根据fiber节点去创建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') //监听事件的属性都是以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])
})
}
// 提交fiber树对dom节点进行更新,删除的节点因为已经不存在当前的fiber树上面,所以需要用deletions存储并且单独遍历
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
// 递归遍历整棵树
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
// 这个循环是因为如果是fiber是函数类型的话他是没有dom节点的,那么就需要往上去找dom节点
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') {
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)
}
}
// 设置fiber树的根节点并且赋值给nextUniOfWork,wrokLoop函数判断nextUniOfWork不为空则开始遍历fiber树
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
}
deletions = []
nextUniOfWork = wipRoot
}
let nextUniOfWork = null //下一个执行的任务
let currentRoot = null // 当前提交给DOM的最近一次的fiber树
let wipRoot = null // wipRoot为fiber树的根节点
let deletions = null // 记录需要删除的fiber节点
function workLoop(deadline) {
let shouldYield = false
// 当遍历完整个fiber树或者剩余时间不够时结束循环
while (nextUniOfWork && !shouldYield) {
// 处理当前工作区内容并且返回下一个工作区内容
nextUniOfWork = performUnitOfWork(
nextUniOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
// 如果没有下一个工作内容而且fiber树的根节点存在
if(!nextUniOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
// requestIdleCallback 是一个用于在浏览器空闲时执行回调函数的API,回调函数接受一个 deadline 对象作为参数。deadline 对象有一个 timeRemaining 方法,用于返回剩余的执行时间。
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
//判断是否是函数组件 函数组件没有dom节点
const isFunctionComponent = fiber.type instanceof Function
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// 判断是否有子节点 如果有子节点先遍历子节点 类似于深度优先遍历
if (fiber.child) {
return fiber.child
}
// 到了这里说明当前fiber已经没有了child,但是也不是最深的节点,因为兄弟节点里面可能也会有child
// 先判断当前节点有没有兄弟节点 如果有兄弟节点就返回兄弟节点(然后遍历所有的兄弟节点),如果没有这返回父节点的兄弟节点(然后遍历所有的父节点的兄弟节点)
let nextFiber = fiber
while(nextFiber) {
if(nextFiber.sibling) {
// 这里返回的是fiber的父节点的兄弟节点,因为在上一次循环结束的时候nextFiber已经指向了fiber的父节点 注意没有直接返回父节点是因为上一个判断当前节点是否有子节点 这样会造成死循环
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
let wipFiber = null // 当前处理的函数fiber,当执行fiber.type的时候会调用useStatue函数,用于useState获取到当前函数fiber
let stateHookIndex = null // 钩子队列的索引,用于获取上一个hook的状态
let effectHookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
stateHookIndex = 0
effectHookIndex = 0
// 存放在函数Fiber里面的钩子
wipFiber.stateHooks = []
wipFiber.effectHooks = []
// fiber.type执行返回的就是函数组件里面最外层的dom元素,这样就解决了函数组件没有dom的问题
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.stateHooks &&
wipFiber.alternate.stateHooks[stateHookIndex]
const hook ={
// 更新的话拿上一次的值,初始化则使用传入的值
state: oldHook? oldHook.state : initial,
queue: [],
}
// 初始化的时候是没有操作的,需要调用了setState队列里面才会有具体操作(下文所说的上面的代码!!!)
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
// 将一些更新的操作放到队列里面,触发更新以后会在上面的代码里面执行
hook.queue.push(action)
// 给wipRoot赋值就会触发页面更新,调用workLoop函数
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot
}
nextUniOfWork = wipRoot
deletions = []
}
wipFiber.stateHooks.push(hook)
stateHookIndex++
return [hook.state, setState]
}
function useEffect(callback, dependencies) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.effectHooks &&
wipFiber.alternate.effectHooks[effectHookIndex]
const hook = {
callback,
dependencies,
}
if(oldHook) {
// 判断依赖是否发生变化,如果发生变化则执行回调函数
if(dependencies.some((dep, index) => dep !== oldHook.dependencies[index])) {
// 判断是否有清除副作用的函数 有的话优先执行
if (oldHook.cleanup) {
oldHook.cleanup()
}
hook.cleanup = oldHook.callback()
}
} else {
// 判断是否第一次执行,并且传入依赖值为空
if(!wipFiber.alternate && dependencies.length === 0) {
hook.cleanup = callback()
}
}
wipFiber.effectHooks.push(hook)
effectHookIndex++
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
// 遍历children为子节点创建fiber
function reconcileChildren(wipFiber, elements) {
let index = 0
let olderFiber = wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (index < elements.length || olderFiber != null) {
const element = elements[index]
let newFiber = null
const sameType = olderFiber && element && element.type === olderFiber.type
if (sameType) {
newFiber = {
type: olderFiber.type,
props: element.props,
dom: olderFiber.dom,
parent: wipFiber,
alternate: olderFiber,
effectTag: 'UPDATE'
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: 'PLACEMENT'
}
}
if (olderFiber && !sameType) {
olderFiber.effectTag = 'DELETION'
deletions.push(olderFiber)
}
if (olderFiber) {
olderFiber = olderFiber.sibling
}
// fiber的父节点只有一个child指向子节点的第一个元素 后续的子节点不能通过父节点直接访问 都是以单向链表的形式通过child去访问
// fiber
// ↓child
// element[0] --sibling--> element[1] --sibling--> element[2]
// 子节点都有一个parent指向父节点
if(index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
const Didact = {
createElement,
render,
useState,
useEffect
}
/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
Didact.useEffect(() => {
window.alert('你点击了'+ state +'次')
}, [state])
Didact.useEffect(() => {
window.alert('第一次进入')
}, [])
return (
<div>
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
<h3>
Clicked: {state} times
</h3>
</div>
)
}
const element = <Counter />
const container = document.getElementById('root')
Didact.render(element, container)