行文思路参考pomb.us/build-your-…
环境搭建
我们需要一个可以转换 jsx 的 vanilla js 环境,使用 vite 可以很方便设置好我们的开发环境
yarn create vite .
#选择vanilla js
# 安装依赖
yarn
touch vite.config.js
复制代码
// vite.config.js
export default {
esbuild: {
jsxFactory: "createElement",
},
};
复制代码
这里我们还是安装下 react 的依赖,方便对比我们实现版本和调试
yarn add react react-dom
复制代码
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
复制代码
// main.jsx
import React, { createElement } from "react";
import ReactDom from "react-dom";
const element = <h1>hello world</h1>;
const root = document.getElementById("root");
ReactDom.render(element, root);
复制代码
yarn dev
复制代码
可以看到我们的项目跑起来了。
createElement
在我们实现 createElement 之前, 我们需要理解 JSX 是什么。
我们来看babel是如何把jsx转换成js的
我们打印返回值 console.log(element)
接下来我们再看createElement的文档:
React.CreateElement(
type,
[props],
[...children]
)
复制代码
创建并返回指定类型的新 React 元素。其中的类型参数既可以是标签名字符串(如 'div'
或 'span'
),也可以是 React 组件 类型 (class 组件或函数组件),或是 React fragment 类型。
总结: JSX转换成createElement
, 返回一个js对象(React Element)
我们来实现createElement
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child => {
if (typeof child === 'string') {
return createTextElement(child)
}
return child
})
},
}
}
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
}
}
复制代码
这里我们特殊处理下了文本节点,方便后面的代码组织。
React:
我们的:
render
有了React Element, 我们来实现render。 现在我们只关心创建DOM, 更新和删除会在后续实现。
function createDom(element) {
// 创建节点
const dom = element.type === 'TEXT_ELEMENT' ?
document.createTextNode('') :
document.createElement(element.type)
// 添加属性
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
return dom
}
function render(element, container) {
const dom = createDom(element)
// 递归渲染child
element.props.children.forEach((child) => {
render(child, dom)
})
container.appendChild(dom)
}
复制代码
concurrent mode
以上的render有一个问题:如果渲染树很大,render会占据主线程一段时间。而在此期间,动画、处理用户输入等优先级更高的操作就会被阻塞。(event loop)
我们把render分成一个个小的任务单元
这会用到浏览器的api:requestIdleCallback
,react则是自己实现了这个方法
window.requestIdleCallback()
方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
复制代码
要让这个workloop跑起来
我们要设置第一个unitOfWork
performUnitOfWork(nextUnitOfWork)
返回下一个unitOfWork
.
fiber
为了组织unitOfWork
, 我们需要一种数据结构:fiber树
我们的render函数只做一件事, 设置root fiber为nextUnitOfWork
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
复制代码
在我们performUnitOfWork(nextUnitOfWork)
函数中,需要做三件事
- 把元素添加到dom
- 创建这个元素的所有children的fiber结构
- child 指向首个子fiber
- sibling 指向兄弟fiber
- parent 指向父fiber
- 返回下一个fiber
如何设置下一个fiber呢?这里使用的是深度优先遍历,先找child,没有child,找sibling,没有sibling, 找parent的sibling ,一直到root,此次渲染完成.
function performUnitOfWork(fiber) {
//1. 把元素添加到dom
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
//2. 创建这个元素的所有children的fiber结构
// - child 指向首个子fiber
// - sibling 指向兄弟fiber
// - parent 指向父fiber
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// 根据是否是第一个child,设置child或者sibling
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 有child
if (fiber.child) {
return fiber.child
}
// 没有child找sibling或者parent的sibling
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
复制代码
Render and Commit Phases
以上实现还有一个问题: 我们是一个一个节点渲染的,每次performUnitOfWork(nextUnitOfWork)
浏览器都会渲染一次,所以用户会看到不完整的ui。
所以我们需要把渲染分为两个阶段: commit
和render
// 设置wipRoot, 并把nextUnitOfWork设为wipRoot
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
// unitWork全部执行完后commit
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
//不用渲染, 交给commitRoot统一处理
// if (fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom)
// }
...
复制代码
Reconciliation
至此,我们已经实现了首次渲染过程。接下来,要实现的是更新
-
在每个fiber节点(包括root)新增一个alternate属性,存储上一个更新的oldFiber
-
两次更新,fiber.type相同,就认为是同一个元素,标记为
UPDATE
。
element存在但type不同,标记为PLACEMENT
。old filber存在但type不同,标记为DELETION
。
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot
}
deletions = []
nextUnitOfWork = wipRoot
}
// preformNextUnitOfWork对所有child添加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
// compare oldFiber to element
const sameType = oldFiber && element && element.type === oldFiber.type
if (sameType) {
// update
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
if (element && !sameType) {
// add this node
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
// TODO delete the oldFiber's node
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// const newFiber = {
// type: element.type,
// props: element.props,
// parent: fiber,
// dom: null,
// }
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
复制代码
- 在
commitWork
时,根据tag处理dom
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 === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
复制代码
函数组件
函数组件有2个不同的地方
- 没有dom节点
- 函数的children是通过调用返回而不是
props.children
直接获取的
updateFunctionComponent(fiber){
const elements = [fiber.type(fiber.props)]
reconcileChildren(fiber, elements)
}
updateHostComponent(fiber){
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
}
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
}
}
复制代码
在commitWork
中,如果父元素没有dom继续往上查找
function commitWork(fiber) {
if (!fiber) {
return
}
// const domParent = fiber.parent.dom
let domParentFiber = fiber.parent
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 === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
复制代码
Hooks
let wipFiber = null
// 每次调用useState 加1
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// 每次更新,重置为0
hookIndex = 0
// 使用hookIndex跟踪多次调用useState的结果
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
// action保存在hook.queque中,然后触发更新
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]
}
复制代码