mini-react 实现实践
背景
最近参加了mini-react小训练,参加的原因是想看看别人是怎么思考的,对比下,做个参照。然后,最近也在面试,也有些帮助吧。
文章内容
- 从直观感受开始,0起步实现以下api;
ReactDOM.createRoot(document.getElementById('root')).render(App)
- 实现react的fiber,以及简单的任务调度
- 实现集中commit和函数组件
- 实现简单的dom更新和children的初步更新,新增和删除
- useState和useEffect的实现
- 面试问react原理要怎么回答
createRoot和render的初步实现
我们的大概脑子里的印象是,react整个过程可以大概分为render和diff两个过程,后面有了调度,所以 就有了,render,调度,和diff,这三个阶段。这里的render只是执行react的render的函数执行过程,并没有包括浏览器渲染到页面的过程,也就是paint的过程。
所以我们要实现的render就是可以输出,可以diff的东西,这个东西就是react element了。
fun(参数)->react element->fiber
因为是第一步,fiber我们忽略,通过react element 来更新dom
const createTextNode = (str) => {
return {
type: 'text',
props: {
nodeValue: str,
},
}
}
const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
},
children: children.map((child) => {
const isText = typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean'
return isText ? createTextNode(child) : child
}),
}
}
同时提供一个React.dom来封装render方法
import React from './React.js'
const ReactDOM = {
createRoot: (container) => {
return {
render: (el) => {
React.render(el, container)
},
}
},
}
export { ReactDOM }
render通过调用create函数来更新dom,这里不再写了。
fiber和任务调度
为了模仿真实的react,所以就要实现fiber,之前更新dom是通过不断递归树的结构来,如果节点很多,会阻塞浏览器渲染过程,fiber有多兄弟和父母的指针管理,通过这些指针可以知道,遍历到哪个节点,就可以把每个节点当成每一个任务去执行,通过requestIdleCallback来调度。
fiber的关键属性有
{
dom:null,
parent:null,
sibling:null,
child:null,
props:null
}
//添加react element 转fiber的过程
fiber={...elemnt,dom,parent,sibling,child}
let needCommit = true
function workLoop(deadline) {
let shouldYaild = true
// workgress是每个fiber,把每个fiber当成任务
while (shouldYaild && workgress) {
shouldYaild = deadline.timeRemaining() > 1
workgress = performanceUnit()
console.log('////', '000')
if (!workgress) needCommit = true
}
requestIdleCallback(workLoop)
}
调度
requestIdleCallback(workLoop)
//每个任务
const performanceUnit = () => {
while (workgress) {
workgress = begainWork(workgress)
}
return workgress
}
begainWork里初始化dom
实现集中commit和函数组件
requestIdleCallback是不太适合,直接去改dom的,所以,我们在每个任务里去更新dom,所以,在fiber我们要知道,这个dom要怎么处理要添加标识,并且放到requestAnimationFrame去处理
function workLoop(deadline) {
let shouldYaild = true
while (shouldYaild && workgress) {
shouldYaild = deadline.timeRemaining() > 1
workgress = performanceUnit()
console.log('////', '000')
if (!workgress) needCommit = true
}
if (!workgress && needCommit) {
console.log('commit')
requestAnimationDom(rootFiber)
needCommit = false
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
const requestAnimationDom = function (fiber) {
requestAnimationFrame(function () {
commitRoot(fiber)
effectActions(fiber)
})
}
const commitLoopFiber = (fiber) => {
if (!fiber) return
fiber.dom = updateDomProps(fiber, fiber.dom)
if (fiber.dom) {
let parent = fiber.parent
while (!parent.dom) {
parent = parent.parent
}
if (fiber.effectTags === 'update') {
} else {
parent.dom.append(fiber.dom)
}
}
commitLoopFiber(fiber.headChildFiber)
commitLoopFiber(fiber.sbling)
}
const commitRoot = (root) => {
commitLoopFiber(root.headChildFiber)
}
const updateDomProps = (fiber, dom) => {
if (!dom) {
Object.keys(fiber.props).forEach((key) => {
if (key.startsWith('on')) {
fiber.headChildFiber.props[key] = fiber.props[key]
}
})
return null
}
const props = fiber.props || Object.create(null)
Object.keys(props).forEach((key) => {
if (key.startsWith('on')) {
const evenName = key.slice(2).toLowerCase()
dom.removeEventListener(evenName, fiber.alternate?.props[key])
dom.addEventListener(evenName, props[key])
} else if (key === 'style') {
const styles = props.style
Object.keys(props.style).forEach((styleKey) => {
dom.style[styleKey] = styles[styleKey]
})
} else {
dom[key] = props[key]
}
})
let deletes = fiber.deletes
if (deletes) {
deletes.forEach((childDom) => {
childDom.parentNode.removeChild(childDom)
})
deletes = null
}
return dom
}
关于函数组件,React.createElement会对方法函数赋值个这个element的type 所以只要执行这个type就能知道这个方法函数的要渲染的节点了。 这里要做的就是: 1、方法函数代表的fiber是没有dom的,所以与它相关的操作,要根据逻辑引向它的父节点,和第一个孩子节点。比如删除节点的dom操作
实现简单的dom更新和children的初步更新,新增和删除
const children = fiber.children || new Array()
let preChild = Object.create(null)
let oldChildFiber = fiber.alternate?.headChildFiber
let childFiber = null
children.forEach((child, index) => {
if (isSameType(child, oldChildFiber)) {
childFiber = {
...child,
dom: oldChildFiber?.dom,
parent: fiber,
headChildFiber: null,
effectTags: 'update',
alternate: oldChildFiber,
}
} else {
childFiber = { ...child, dom: null, sbling: null, parent: fiber, headChildFiber: null }
if (oldChildFiber) {
addDelets(fiber, oldChildFiber)
childFiber.effectTags = 'placement'
}
}
preChild.sbling = index === 0 ? null : childFiber
if (index === 0) fiber.headChildFiber = childFiber
preChild = childFiber
oldChildFiber = oldChildFiber?.sbling
// render(child, dom)
})
while (oldChildFiber) {
addDelets(fiber, oldChildFiber)
oldChildFiber = oldChildFiber.sbling
}
return children
}
- useState和useEffect的实现
let effectStates = []
let effectStateIndex = 0
const useState = (initialValue) => {
const currentFiber = workgress
const oldFiber = workgress.alternate
console.log('??')
let effect = {
state: oldFiber?.effectStates[effectStateIndex].state || initialValue,
quque: oldFiber?.effectStates[effectStateIndex].quque || [],
}
effectStateIndex++
effect.quque.forEach((cb, index) => {
effect.state = cb(effect.state)
})
effect.quque = []
effectStates.push(effect)
currentFiber.effectStates = effectStates
const setState = function (callback) {
const cb = typeof callback === 'function' ? callback : () => callback
if (cb(effect.state) === effect.state) return
effect.quque.push(cb)
// effect.state = cb(effect.state)
console.log(effectStateIndex)
workgress = {
...currentFiber,
alternate: currentFiber,
}
rootFiber = workgress
}
return [effect.state, setState]
}
let effectsHooks = []
let effectsHooksIndex = 0
const useEffect = (callback, deps) => {
const currentFiber = workgress
const effect = {
deps,
callback,
cleanUp: currentFiber?.alternate?.effectsHooks[effectsHooksIndex]?.cleanUp,
}
effectsHooks.push(effect)
effectsHooksIndex++
currentFiber.effectsHooks = effectsHooks
}
- 面试问react原理要怎么回答
包括JSX、Diffing、Fiber、Time Slice 所以对这四个内容进行描述:可能会部分或者全部进行细化提问 1、为什么react要用jsx? 2、jsx是怎么转成fiber,在什么时候转到 3、react的diff是怎么做的。 4、react是怎么时间分片的,它是怎么调度的。 5、render函数是什么原理 等。
进行细化描述
转载自:https://juejin.cn/post/7352387558746210356