实现一个mini-react
Posted 我是真的不会前端
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现一个mini-react相关的知识,希望对你有一定的参考价值。
首先 说下原理
react自v16以后发生了很多变化,v16以后底层的“虚拟DOM”不再是简单JSON数据了,React采用了最新的Fiber(双向链表)的数据结构,作为“协调”(Diff)运算的基础数据。React背后还提供了强大的 react-reconciler 和 scheduler 库实现Fiber链表的生成、协调与调度。相比vue组件,react在较大组件方面的性能更高。如果要手写一个简易版本的React,其核心要实现以下功能,createElement(用于创建元素)、createDOM/updateDOM(用于创建和更新DOM)、render/workLoop(用于生成Fiber和协调运算)、commitWork(用于提交)等,如果还有支持Hooks,还得封闭Hooks相关的方法。
思路
-
下载react官网仓库中的代码,搞清楚目录结构、各个包的作用。
-
下载地址:https://github.com/facebook/react,进一步查看packages目录。
-
react-dom 这是DOM渲染的若干功能。
-
react 这是React核心语法及其API封装的包
-
react-reconciler 用于生成“Fiber树”和“协调运算”的。
-
scheduler,它是模拟requestIdleCallback(fn)的兼容性实现,用于执行复杂的任务,当浏览器主线程有“空闲”时执行这些复杂的任务,不霸占浏览器主线程。
-
用工程化环境或者html页面,引入react.js和react.dom.js,在源码中进行调试学习。
-
如果采用HTML页面的方式来分析React源码,还要引入babel.js,对JSX语法进行编译,在script标签还要添加 type=‘text/babel’。
-
慢慢通过删减、调试的方式,把react.js中无用的逻辑都删除,得到一个mini-react。
我们需要什么
实现一个简易react
需要用到babel.min.js
react.development.js
react-dom.development.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>React</title>
</head>
<body>
<div id="app"></div><hr>
<div id='root'></div>
<script src='./dist/babel.min.js'></script>
<script src='./dist/react.development.js'></script>
<script src='./dist/react-dom.development.js'></script>
<script type='text/javascript'>
// 创建了一个React元素
const app = React.createElement('h1', title:'testReact', 'Hello React')
ReactDOM.render(app, document.getElementById('app'))
</script>
<script type='text/babel'>
const App = () =>
const [num, setNum] = React.useState(0)
return (
<div>
<h1> num </h1>
<button onClick=()=>setNum(num-1)>自减</button>
<button onClick=()=>setNum(num+1)>自增</button>
</div>
)
ReactDOM.render(<App />, document.getElementById('root'))
</script>
</body>
</html>
首先实现基本的render和react-dom
// return Fiber基础单元 = type, props: children
// type 表示当前节点的HTML元素名,也有可能是'TEXT-ELEMENT'
// props 表示当前节点的jsx属性,还包括一个特殊属性children
function createElement(type, props, ...children)
// 返回一个Fiber单元
return
type,
props:
...props,
children: children.map(ele=>(typeof ele==='object') ? ele : createTextElement(ele))
// 文本Fiber
function createTextElement(text)
return
type: 'TEXT_ELEMENT',
props:
nodeValue: text,
children: []
function isProperty (key)
return key !== 'children'
function render (element, container)
const dom =
element.type === 'TEXT_ELEMENT'
? document.createTextNode(element.props.nodeValue)
: document.createElement(element.type)
// 遍历element.props上的非children属性,并将其添加到DOM上
Object.keys(element.props)
.filter(isProperty)
.forEach(attr=>dom[attr]=element.props[attr])
element.props.children.forEach(child=>
render(child, dom)
)
container.appendChild(dom)
const React =
createElement,
render
引入Fiber思想
// return Fiber基础单元 = type, props: children
// type 表示当前节点的HTML元素名,也有可能是'TEXT-ELEMENT'
// props 表示当前节点的jsx属性,还包括一个特殊属性children
function createElement(type, props, ...children)
// 返回一个Fiber单元
return
type,
props:
...props,
children: children.map(ele=>(typeof ele==='object') ? ele : createTextElement(ele))
// 文本Fiber
function createTextElement(text)
return
type: 'TEXT_ELEMENT',
props:
nodeValue: text,
children: []
// 特别注意:如何区分element和fiber呢?
// element上没有link指针,它是孤立的,它纯粹的 type,props
// fiber是element添加了link指针后的结果,它会形成与其它fiber的链接关系
function createDom (fiber)
const dom =
fiber.type === 'TEXT_ELEMENT'
? document.createTextNode(fiber.props.nodeValue)
: document.createElement(fiber.type)
// 遍历element.props上的非children属性,并将其添加到DOM上
Object.keys(fiber.props)
.filter(isProperty)
.forEach(attr=>dom[attr]=fiber.props[attr])
// 没有遍历fiber.props.children,也没有执行DOM插入操作
return dom
// 相当于你正在封装ReactDOM库(***)
// 入参:element是Fiber单元,container挂载的节点
// 判断一个key是不是非children属性,
function isProperty (key)
return key !== 'children'
// render(<App/>, document.getElementById('app'))
function render(element, container)
// 把root这个工作单元,设置成第一个工作单元(第一个Fiber单元)
nextUnitOfWork =
dom: container, // 当前element将要挂载的父节点,这相当于是vue.$el
props:
children: [element] // 这个element相当是APP这个元素
// 当第一个工作单元被设置完成时。其实此时,浏览器早已准备好了,浏览器要执行requestIdleCallback(workLoop)
// --------------------------------------------------------
let nextUnitOfWork = null // 即将执行的下一个“工作单元”
function workLoop(dealine)
let shouldYield = false // 当它为假时,表示浏览器中断了当前的工作
while (nextUnitOfWork && !shouldYield)
// 如果有下一个事儿(工作单元),并且浏览没有中断当前工作
// 如果浏览器没有中断当前工作,说明浏览器还是比较“空闲”的
// 意思:执行当前的工作单元,执行完成后再返回一个新的工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
// 当nextUnitOfWork为空时,说明没有任务了,停下来
// 当浏览器主线程比较忙时,也要停下来,这个“停下来”是浏览器强制的
shouldYield = dealine.timeRemaining() < 1 // 单位是:毫秒
// 使用reqiuestIdleCallback()实现render的异步工作
// 因为这个api有兼容性问题,所以react官方自己实现了scheduler调试
requestIdleCallback(workLoop)
requestIdleCallback(workLoop)
// 要想让requestIdleCallback()开始工作,我们需要给nextUnitOfWork第一个工作单元。由谁来执行工作呢?由performUnitOfWork执行工作,这个方法不仅仅是执行工作,还要能够返回下一个要执行的工作。
function performUnitOfWork(fiber)
// 第一件事:把当前工作单元(Fiber单元)添加到真实的DOM中去
if (!fiber.dom) fiber.dom = createDom(fiber)
if (fiber.parent) fiber.parent.dom.appendChild(fiber.dom)
// 第二件事:遍历当前Fiber单元的孩子们,并给每一个孩子创建一个Fiber
// element是什么时候变成有指针的Fiber的?答案,就在此刻。
const elements = fiber.props.children
let index = 0 // 用于while循环
let prevSibling = null // 上一个兄弟节点
while (index < elements.length)
let element = elements[index]
// 给循环中的这个element添加指针、dom,将其变成真正意义的Fiber单元
const newFiber =
type: element.type,
props: element.props,
parent: fiber, // ? 你添加了parent指针,请求sibling、child指针在哪里?
dom: null
if (index === 0)
// 当循环第一个子节点时,那么这个子节点其实就是当前fiber的child
fiber.child = newFiber
else
// 当循环第二个、第三个。。。子节点时,把上一个节点的sibling指向当前被循环的这个节点
prevSibling.sibling = newFiber
// 循环第一个元素时,prevSibling等于null,继续循环时,prevSibling就等于上一次循环那个节点。
prevSibling = newFiber
// 不断地循环
index++
// 第三件事:根据Fiber工作流程原则,找到下一个工作单元并返回
// 先孩子节点,如果有,直接返回;如果没有,进入下步
if (fiber.child) return fiber.child
// 找下一个兄弟(这个兄弟有可能是当前fiber的下一个兄弟,也可能是祖宗的下一个兄弟)
let next = fiber
while (next)
if (next.sibling) return next.sibling
next = next.parent
简易实现usestate(模拟实现)
// return Fiber基础单元 = type, props: children
// type 表示当前节点的HTML元素名,也有可能是'TEXT-ELEMENT'
// props 表示当前节点的jsx属性,还包括一个特殊属性children
function createElement(type, props, ...children)
// 返回一个Fiber单元
return
type,
props:
...props,
children: children.map(ele=>(typeof ele==='object') ? ele : createTextElement(ele))
// 文本Fiber
function createTextElement(text)
return
type: 'TEXT_ELEMENT',
props:
nodeValue: text,
children: []
// 特别注意:如何区分element和fiber呢?
// element上没有link指针,它是孤立的,它纯粹的 type,props
// fiber是element添加了link指针后的结果,它会形成与其它fiber的链接关系
function createDom (fiber)
const dom =
fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type)
// 遍历element.props上的非children属性,并将其添加到DOM上
updateDom(dom, , fiber.props)
// 没有遍历fiber.props.children,也没有执行DOM插入操作
return dom
// 相当于你正在封装ReactDOM库(***)
// 入参:element是Fiber单元,container挂载的节点
// render(<App/>, document.getElementById('app'))
function render(element, container)
// 把root这个工作单元,设置成第一个工作单元(第一个Fiber单元)
wipRoot =
dom: container, // 当前element将要挂载的父节点,这相当于是vue.$el
props:
children: [element] // 这个element相当是<APP/>这个元素
,
alternate: currentRoot
// wipRoot正在render那个根Fiber
// currentRoot 是当前节点的旧Fiber
deletions = []
nextUnitOfWork = wipRoot
// 当第一个工作单元被设置完成时。其实此时,浏览器早已准备好了,浏览器要执行requestIdleCallback(workLoop)
// --------------------------------------------------------
let nextUnitOfWork = null // 即将执行的下一个“工作单元”
let wipRoot = null // 记录当前正在执行工作单元的root节点
let currentRoot = null // 它指向上一次已经commit过的那颗“Fiber树”
let deletions = [] // 用于收集每次更新阶段中即将要删除的Fiber节点
function workLoop(dealine)
let shouldYield = false // 当它为假时,表示浏览器中断了当前的工作
while (nextUnitOfWork && !shouldYield)
console.log('render start')
// 如果有下一个事儿(工作单元),并且浏览没有中断当前工作
// 如果浏览器没有中断当前工作,说明浏览器还是比较“空闲”的
console.log('nextUnitOfWork', nextUnitOfWork)
// 意思:执行当前的工作单元,执行完成后再返回一个新的工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
console.log('nextUnitOfWork', nextUnitOfWork)
// 问题:这个while循环将一直工作,那什么时候能停下来了?
// 当nextUnitOfWork为空时,说明没有任务了,停下来
// 当浏览器主线程比较忙时,也要停下来,这个“停下来”是浏览器强制的
shouldYield = dealine.timeRemaining() < 1 // 单位是:毫秒
// 什么时候该commit()提交更新真实DOM呢?
if (!nextUnitOfWork && wipRoot)
console.log('commit start')
commitRoot()
// 使用reqiuestIdleCallback()实现render的异步工作
// 因为这个api有兼容性问题,所以react官方自己实现了scheduler调试
requestIdleCallback(workLoop)
requestIdleCallback(workLoop)
// 要想让requestIdleCallback()开始工作,我们需要给nextUnitOfWork第一个工作单元。由谁来执行工作呢?由performUnitOfWork执行工作,这个方法不仅仅是执行工作,还要能够返回下一个要执行的工作。
function performUnitOfWork(fiber)
// 判断是不是函数式组件
const isFunctionComponent = fiber.type instanceof Function
console.log('isFunctionComponent', isFunctionComponent)
if (isFunctionComponent)
console.log('---function')
updateFunctionComponent(fiber)
else
console.log('---host')
updateHostComponent(fiber)
// 第三件事:根据Fiber工作流程原则,找到下一个工作单元并返回
// 先孩子节点,如果有,直接返回;如果没有,进入下步
if (fiber.child) return fiber.child
// 找下一个兄弟(这个兄弟有可能是当前fiber的下一个兄弟,也可能是祖宗的下一个兄弟)
let next = fiber
while (next)
if (next.sibling) return next.sibling
next = next.parent
function commitRoot()
// 删除那些需要移除的Fiber所对应的DOM节点
deletions.forEach(commitWork)
// add nodes to dom
// wipRoot在这里就相当是#app所对应的节点
// wipRoot.child 这个代表就是<App/>这个Fiber节点
commitWork(wipRoot.child)
// 为了执行协调运算,在这里我们要记录这个被commit过的Fiber树
currentRoot = wipRoot
// 当DOM渲染更新完成时,清除掉wipRoot
wipRoot = null
// 为了兼容考虑React组件的更新阶段,在这时我们不能“一刀切”地更新DOM。
// 要根据 effectTag 这个标记,来分门别类地渲染或更新DOM.
function commitWork(fiber)
if (!fiber) return
// 向上寻找非函数式Fiber节点来挂载当前Fiber节点
let parent = fiber.parent // 这个parent节点可能是函数式的Fiber节点
while (!parent.dom)
parent = parent.parent
const $el = parent.dom
// 因为在下面用到了三种标记,这些标记是由“协调运算”添加的
// 所以,如果下面不区分装载阶段和更新阶段,我们应该在commitWork之前已经完成了协调运算。
// 新增了DOM节点
if (fiber.effectTag === 'PLACEMENT' && fiber.dom !== null)
$el.appendChild(fiber.dom)
else if (fiber.effectTag === 'DELETION')
// 删除Fiber节点时,也需要找到一个拥有dom的节点,而不是函数式的Fiber节点
commitDeletion(fiber, $el)
else if (fiber.effectTag === 'UPDATE' && fiber.dom !== null)
// 如果oldFiber.effectTag='UPDATE',只是说明这个节点的属性有可能变了
// 所以,这里我们进一步遍历fiber.props来找出最小变化差异点。
// 这个udpateDom方法,用于对比当前fiber节点上props的变化
updateDom(fiber.dom, fiber.alternate.props, fiber.props)
// 递归插入DOM节点
commitWork(fiber.child)
commitWork(fiber.sibling)
function commitDeletion(fiber, $el)
if (fiber.dom)
// 删除非函数式的Fiber节点
$el.removeChild(fiber.dom)
else
// 删除函数式组件的Fiber,向下寻找可移除的DOM节点
commitDeletion(fiber.child, $el)
// 协调运算(在装载阶段、更新阶段都执行)
function reconcileChildren(fiber, elements)
以上是关于实现一个mini-react的主要内容,如果未能解决你的问题,请参考以下文章
React 18发布,仅用400行代码就能实现一个Mini-React
jQuery 利用 parent() parents() 寻找父级 或祖宗元素