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, 并将其作为 nextUnitOfWork
performUnitOfWork
函数中完成每个 fiber 需要做三件事
nextUnitOfWork
Make 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]
}
// ...