2019-12-24
https://pomb.us/build-your-own-react/
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
// const element = <h1 title="foo">Hello</h1>
const element = React.createElement(
"h1", { title: "foo" },
"Hello"
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
// const element = <h1 title="foo">Hello</h1>
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
ReactDOM.render(element, container)
// const element = <h1 title="foo">Hello</h1>
const element = {
type: "h1",
props: {
title: "foo",
children: "Hello",
},
}
const container = document.getElementById("root")
// ReactDOM.render(element, container)
const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)
createElement Functionconst element = React.createElement(
"div", { id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
}
}
createElement("div")
/*
{
"type": "div",
"props": { "children": [] }
}
*/
createElement("div", null, a, b)
/*
{
"type": "div",
"props": { "children": [a, b] }
}
*/
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
// children 可能是 string 或 numbers
children: children.map(child =>
typeof child === "object" ?
child :
createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT", // 自定义为 TEXT_ELEMENT 类型
props: {
nodeValue: text,
children: [], // 简化 (React 对于基本类型或没有 children 时并没有创建一个空数组)
},
}
}
const MyReact = {
createElement,
}
const element = MyReact.createElement(
"div", { id: "foo" },
MyReact.createElement("a", null, "bar"),
MyReact.createElement("b")
)
Tell to use MyReact’s createElement instead of React’s
If we have a comment like below, when babel transpiles the JSX it will use the function we define
/** @jsx MyReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
render Function/** @jsx MyReact.createElement */
const element = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)
// ...
function render(element, container) {
const dom = document.createElement(element.type)
// 迭代 children
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
// ...
// ...
function render(element, container) {
const dom = element.type == "TEXT_ELEMENT" // 自定义的 TEXT_ELEMENT 类型时创建 text node
?
document.createTextNode("") :
document.createElement(element.type)
// 属性
const isProperty = key => key !== "children"
Object.keys(element.props)
.filter(isProperty)
.forEach(name => {
dom[name] = element.props[name]
})
// 迭代 children
element.props.children.forEach(child =>
render(child, dom)
)
container.appendChild(dom)
}
// ...
// ...
function render(element, container) {
//...
// 迭代 children, 只有渲染完整个树时,此迭代才会结束,因此会阻塞整个主线程
element.props.children.forEach(child =>
render(child, dom)
)
//...
}
// ...
after we finish each unit we’ll let the browser interrupt the rendering if there’s anything else that needs to be done
We use requestIdleCallback to make a loop
requestIdleCallback also gives us a deadline parameter. performUnitOfWork function that not only performs the work but also returns the next unit of work.performUnitOfWork 完成当下小单元的任务,并返回下一个待处理的小单元任务requestIdleCallback anymore. Now it uses the scheduler package.// ...
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
}
// ...
To organize the units of work we’ll need a data structure: a fiber tree.
We’ll have one fiber for each element and each fiber will be a unit of work.
MyReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
In the render we’ll create the root fiber and set it as the nextUnitOfWork.
render 负责创建 root fiber, 并将其作为 nextUnitOfWorkperformUnitOfWork 函数中完成每个 fiber 需要做三件事
nextUnitOfWorkMake it easy to find the next unit of work. That’s why each fiber has a link to its first child, its next sibling and its parent.
nextUnitOfWork 的 fiber 的顺序
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT" ?
document.createTextNode("") :
document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
function render(element, container) {
// TODO set next unit of work
}
// ...
// ...
function render(element, container) {
nextUnitOfWork = { // 创建 root fiber, 并将其作为 nextUnitOfWork
dom: container,
props: {
children: [element],
},
}
}
let nextUnitOfWork = null
// ...
workLoop and we’ll start working on the root// ...
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(fiber) {
// TODO add dom node
// TODO create new fibers
// TODO return next unit of work
}
// ...
// ...
function performUnitOfWork(fiber) {
// create a new node and append it to the dom
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// for each child we create a new 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,
}
// we add it to the fiber tree setting it either as a child or as a sibling
// depending on whether it’s the first child or not
if (index === 0) {
fiber.child = newFiber // as a child
} else {
prevSibling.sibling = newFiber // as a sibling
}
prevSibling = newFiber
index++
}
// Finally we search for the next unit of work.
if (fiber.child) { // first try with the child
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) { // then with the sibling, then with the uncle, and so on
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
// ...
// ...
function performUnitOfWork(fiber) {
//...
// Remove this
if (fiber.parent) {
// adding a new node to the dom each time we work on an element
// the browser could interrupt our work before we finish rendering the whole tree
// the user will see an incomplete UI
fiber.parent.dom.appendChild(fiber.dom)
}
//...
}
// ...
//...
function commitRoot() {
// TODO add nodes to dom
}
function render(element, container) {
wipRoot = { // track of the root of the fiber tree (the work in progress root)
dom: container,
props: {
children: [element],
},
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
// once we finish all the work
// we know it because there isn’t a next unit of work
// we commit the whole fiber tree to the dom.
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
// ...
// ...
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
// recursively append all the nodes to the dom
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
// ...
render function to the last fiber tree we commited to the dom.// ...
function commitRoot() {
commitWork(wipRoot.child)
currentRoot = wipRoot // save a reference to that “last fiber tree we commited to the dom”
wipRoot = null
}
// ...
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
// Add the alternate property to every fiber.
// This property is a link to the old fiber,
// the fiber that we commited to the dom in the previous commit phase.
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
// ...
performUnitOfWork that creates the new fibers to a new reconcileChildren functionfunction performUnitOfWork(fiber) {
// create a new node and append it to the dom
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
// Finally we search for the next unit of work.
if (fiber.child) { // first try with the child
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) { // then with the sibling, then with the uncle, and so on
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
Here React also uses keys, that makes a better reconciliation. For example, it detects when children change places in the element array
// ...
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
// iterate at the same time over the children of the old fiber (wipFiber.alternate)
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 the node
// we create a new fiber
newFiber = {
type: oldFiber.type,
props: element.props, // the props from the element
dom: oldFiber.dom, // keeping the dom node from the old fiber
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE", // use this property later, during the commit phase
}
}
if (element && !sameType) {
// add this node
// the element needs a new dom node
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
// delete the oldFiber's node
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
// ...
// ...
function updateDom(dom, prevProps, nextProps) {
// TODO
}
function commitRoot() {
deletions.forEach(commitWork) // use the fibers from the deletions array
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
// handle the new effectTags
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
// If the fiber has a PLACEMENT effect tag
// we do the same as before,
// append the dom node to the node from the parent fiber.
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
// if it’s an UPDATE
// we need to update the existing dom node with the props that changed.
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
// If it’s a DELETION, we do the opposite, remove the child.
domParent.removeChild(fiber.dom)
}
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = [] // need an array to keep track of the nodes we want to remove.
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = null
// ...
// ...
const isEvent = key => key.startsWith("on") // event listeners are special, need handle them differently
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)
// If the event handler changed we remove it from the node.
dom.removeEventListener(
eventType,
prevProps[name]
)
})
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps)) // remove the props that are gone
.forEach(name => {
dom[name] = ""
})
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps)) // set the props that are new or changed
.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 components are differents in two ways:
the fiber from a function component doesn’t have a dom node
and the children come from running the function instead of getting them directly from the props
/** @jsx MyReact.createElement */
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
MyReact.render(element, container)
/*
function App(props) {
return MyReact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = MyReact.createElement(App, {
name: "foo",
})
*/
function createElement(type, props, ...children) {
return {
type,
// ...
}
}
// ...
function performUnitOfWork(fiber) {
const isFunctionComponent = fiber.type instanceof Function // fiber.type === App
if (isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
// ...
}
// ...
function updateFunctionComponent(fiber) {
// TODO
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
// ...
// ...
function updateFunctionComponent(fiber) {
// this fiber doesn’t have a dom node
const children = [fiber.type(fiber.props)] // run the function to get the children.
reconcileChildren(fiber, children)
}
// ...
// ...
function commitWork(fiber) {
if (!fiber) {
return
}
// to find the parent of a dom node
// we’ll need to go up the fiber tree until we find a fiber with a dom node
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") {
// when removing a node
// we also need to keep going until we find a child with a dom node
commitDeletion(fiber, domParent)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
// ...
/** @jsx MyReact.createElement */
function Counter() {
const [state, setState] = MyReact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
MyReact.render(element, container)
hooks array// ...
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
// keep track of the current hook index
hookIndex = 0
// add a hooks array to the fiber
// to support calling useState several times in the same component
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
// TODO
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
// ...
// ...
function useState(initial) {
// check if we have an old hook
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
// run the action the next time we are rendering the component
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
// set a new work in progress root as the next unit of work
// so the work loop can start a new render phase
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
// ...