基于vite搭建项目
pnpm create vite
因为 vite 有解析 jsx 文件的功能,为了能实现 jsx 的解析,所以使用 vite 创建项目
最简实现-v1
- 创建 dom 节点,设置 id
- 把 dom 添加到 #root 容器
- 创建 textNode,设置 nodeValue
- 把 textNode 添加到 dom 容器
// main.js
const dom = document.createElement('div')
dom.id = 'app'
document.querySelector('#root').append(dom)
const textNode = document.createTextNode('')
textNode.nodeValue = 'app'
dom.append(textNode)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<!-- 修改默认的 id 为 `root` -->
<div id="root"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
最简实现-v2
改进:实现动态创建
- 动态创建 dom - render 实现 dom 的渲染
- 动态创建 vdom - createElemet、createTextNode
// main.js
function render(el, container) {
// 1. 创建 dom
const dom = el.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(el.type)
// 2. 处理 props
Object.keys(el.props).forEach((key) => {
if (key !== "children") {
dom[key] = el.props[key]
}
})
// 递归 children
const children = el.props.children
children.forEach(child => {
render(child, dom)
})
// 添加 dom 到容器
container.append(dom)
}
function createTextNode(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
// 扩展运算符`...children`处理多个 child
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
// 考虑 child 是字符串的情况
children: children.map((child) =>
typeof child === "string" ? createTextNode(child) : child
),
},
}
}
const App = createElement(
"div",
{ id: "app" },
"Hello ",
createTextNode("App")
)
render(App, document.querySelector('#root'))
- 实现 ReactDom API
- core/React.js
// core/React.js
function render(el, container) {
// 1. 创建 dom
const dom = el.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(el.type)
// 2. 处理 props
Object.keys(el.props).forEach((key) => {
if (key !== 'children') {
dom[key] = el.props[key]
}
})
// 3. 递归 children
const children = el.props.children
children.forEach(child => {
render(child, dom)
})
// 4. 添加 dom 到容器
container.append(dom)
}
// 动态创建 text 类型 vdom
function createTextNode(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
// 动态创建标签类型 vdom
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "string" ? createTextNode(child) : child
),
},
}
}
const React = {
render,
createElement,
}
export default React
- core/ReactDom.js
实现 ReactDom API
,实现 dom 添加到 html 页面
import React from './React.js'
const ReactDom = {
createRoot: function(container) {
return {
render(App) {
React.render(App, container)
}
}
}
}
export default ReactDom
- App.js
// App.js
import React from './core/React.js'
const App = React.createElement(
'div',
{ id: 'app' },
'Hello ',
'App'
)
export default App
- main.js
// main.js
import ReactDom from './core/ReactDom.js'
import App from './App.js'
ReactDom.createRoot(document.querySelector('#root')).render(App)
实现 jsx
// App.jsx
import React from './core/React.js'
const App = <div>Hello App jsx</div>
export default App
// main.js
import ReactDom from './core/ReactDom.js'
import App from './App.jsx'
ReactDom.createRoot(document.querySelector('#root')).render(App)
fiber 架构
使用 requestIdleCallback 实现,利用浏览器空闲时间处理 dom
// main.js
let nextWorkOfUnit = null
function render(el, container) {
nextWorkOfUnit = {
dom: container,
props: {
children: [el],
}
}
}
function workLoop(deadline) {
let shouldYeild = false
while(!shouldYeild && nextWorkOfUnit) {
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit) // TODO
shouldYeild = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
// 利用浏览器空闲时间处理 dom
requestIdleCallback(workLoop)
// main.js
function performWorkOfUnit(fiber) {
// 创建 dom
if(!fiber.dom) {
const dom = (fiber.dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type))
// 🌟 添加 dom 到 容器
fiber.parent.dom.append(fiber.dom)
// 处理 props
Object.keys(fiber.props).forEach((key) => {
if (key !== 'children') {
dom[key] = fiber.props[key]
}
})
}
// 处理 children
const children = fiber.props.children
let prevChild = null
children.forEach((child, index) => {
let newFiber = {
type: child.type,
props: child.props,
dom: null,
parent: fiber,
child: null,
sibling: null,
}
// 初始化 child、sibling
if(index === 0) {
fiber.child = newFiber // 首先指向第一个孩子节点
} else {
prevChild.sibling = newFiber // 如果有多个孩子节点,就接着连接到 child 的后面,用 sibling 表示兄弟节点
}
prevChild = newFiber
})
// 链表指针指向下一个节点
if(fiber.child) {
return fiber.child
}
// 如果没有 child,则指向兄弟节点
if(fiber.sibling) {
return fiber.sibling
}
// 向上找父节点的兄弟节点
return fiber.parent?.sibling
}
实现统一提交
在完成整个链表的转换后,统一提交,从root开始
let nextWorkOfUnit = null
let wipRoot = null // 整个应用树的根节点
function render(el, container) {
wipRoot = {
dom: container,
props: {
children: [el],
}
}
nextWorkOfUnit = wipRoot
}
// 不在这里添加dom
function performWorkOfUnit(fiber) {
// 创建 dom
if(!fiber.dom) {
const dom = (fiber.dom = fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type))
// // 添加 dom 到 容器
// fiber.parent.dom.append(fiber.dom)
// 处理 props
Object.keys(fiber.props).forEach((key) => {
if (key !== 'children') {
dom[key] = fiber.props[key]
}
})
}
// 省略...
}
// 改为在完成整个链表结构转换后,统一添加
function commitRoot() {
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if(!fiber) return
fiber.parent.dom.append(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function workLoop(deadline) {
let shouldYeild = false
while(!shouldYeild && nextWorkOfUnit) {
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit)
shouldYeild = deadline.timeRemaining() < 1
}
// 完成所有节点的链表结构转换后,统一添加
if(!nextWorkOfUnit && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
实现 function component
funtion component 的 type 为一个函数,所以可以调用 type(),返回函数组件的 children,但是需要放到一个数组里面,保持数据结构一致
主要关注以下几点:
1. 区分函数组件、普通标签
创建 DOM 的条件:函数组件不需要创建 dom
function performWorkOfUnit(fiber) {
const isFunctionComponent = typeof fiber.type === 'function'
if(isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// 链表指针指向下一个节点
if(fiber.child) {
return fiber.child
}
if(fiber.sibling) {
return fiber.sibling
}
return fiber.parent?.sibling
}
function updateFunctionComponent(fiber) {
// 函数组件不需要创建 dom
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function updateHostComponent(fiber) {
// 创建 dom
if(!fiber.dom) {
const dom = (fiber.dom = createDom(fiber))
// 处理 props
Object.keys(fiber.props).forEach((key) => {
if (key !== 'children') {
dom[key] = fiber.props[key]
}
})
}
const children = fiber.props.children
reconcileChildren(fiber, children)
}
function createDom(fiber) {
return fiber.type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(fiber.type)
}
2. 处理 children
都有相同的 children 处理逻辑,抽取为 reconcileChildren
function reconcileChildren(fiber, children) {
let prevChild = null
children.forEach((child, index) => {
let newFiber = {
type: child.type,
props: child.props,
dom: null,
parent: fiber,
child: null,
sibling: null,
}
if(index === 0) {
fiber.child = newFiber
} else {
prevChild.sibling = newFiber
}
prevChild = newFiber
})
}
3. 处理函数组件没有 dom 属性无法 append 的问题
while 循环向上寻找有 fiber.dom 的节点进行 append
function commitWork(fiber) {
if(!fiber) return
// 区分函数组件节点和普通节点,因为函数组件不存在 fiber.dom
// 所以其孩子节点需要向上寻找有 fiber.dom 的祖先节点进行 append
let fiberParent = fiber.parent
while(!fiberParent.dom) {
fiberParent = fiberParent.parent
}
if(fiber.dom) {
fiberParent.dom.append(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
4. 处理多个函数组件相邻,没找到 sibling 的问题
向上找有 sibling 的节点,指向它
function performWorkOfUnit(fiber) {
const isFunctionComponent = typeof fiber.type === 'function'
if(isFunctionComponent) {
updateFunctionComponent(fiber)
} else {
updateHostComponent(fiber)
}
// 链表指针指向下一个节点
if(fiber.child) {
return fiber.child
}
// if(fiber.sibling) {
// return fiber.sibling
// }
// return fiber.parent?.sibling
// 多个函数组件相邻,因为函数组件的fiber没有sibling属性,需要向上寻找有 sibling 属性的节点指向下一个节点
let newFiber = fiber
while(newFiber) {
if(newFiber.sibling) {
return newFiber.sibling
}
newFiber = newFiber.parent
}
}
4. 处理函数组件传参, textNode 为 number 的情况
修改 createElement 判断 isTextNode
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
const isTextNode =
typeof child === 'string' || typeof child === 'number'
return isTextNode ? createTextNode(child) : child
}),
},
}
}
测试
import React from './core/React.js'
function Foo() {
return (
<div>
Foo
<p>Foo content</p>
</div>
)
}
function Bar({ num }) {
return (
<div>
Bar
<p>Bar content</p>
<p>num is: {num}</p>
</div>
)
}
function AppContainer() {
return (
<div>
<h1>App</h1>
<Foo />
<Bar num={10} />
</div>
)
}
const App = AppContainer()
export default App
更新
更新 vdom
- 如何得到新的 dom 树 通过添加一个 currentRoot 控制新的链表结构,使用 alternate 指向旧节点
// React.js
let nextWorkOfUnit = null // 指向下一个fiber任务
let wipRoot = null // 实现统一提交
let currentRoot = null // 用于更新 dom
- 如何找到老的节点
- 初始化旧指针
oldFiber = fiber.alternate?.child
- 判断是否改变
isSameType
- isSameType,newFiber 属性
- 移动指针
- 初始化旧指针
// React.js
function reconcileChildren(fiber, children) {
let prevChild = null
let oldFiber = fiber.alternate?.child // 旧fiber,指向当前fiber的后备指针的child
children.forEach((child, index) => {
// 判断新旧fiber是否同类型
const isSameType = oldFiber && oldFiber.type === child.type
let newFiber = {
type: child.type,
props: child.props,
parent: fiber,
child: null,
sibling: null,
}
if(isSameType) {
newFiber = {
...newFiber,
dom: oldFiber.dom,
alternate: oldFiber,
}
} else {
newFiber = {
...newFiber,
dom: null,
}
}
// 移动指针
if(oldFiber) {
oldFiber = oldFiber.sibling
}
// 初始化 child、sibling
if(index === 0) {
fiber.child = newFiber
} else {
prevChild.sibling = newFiber
}
prevChild = newFiber
})
}
- 如何 diff props、绑定事件
- newFiber 添加属性
effectTag
,标识是更新(update)/占位(placement)
- newFiber 添加属性
// React.js
function reconcileChildren(fiber, children) {
// 省略...
children.forEach((child, index) => {
// 省略...
if(isSameType) {
newFiber = {
...newFiber,
dom: oldFiber.dom,
alternate: oldFiber,
effectTag: 'update'
}
} else {
newFiber = {
...newFiber,
dom: null,
effectTag: 'placement',
}
}
// 省略...
})
}
- 在 commitWork 时,判断 effectTag 进行更新/创建
function commitWork(fiber) {
if(!fiber) return
// 区分函数组件节点和普通节点,因为函数组件不存在 fiber.dom
// 所以其孩子节点需要向上寻找有 fiber.dom 的祖先节点进行 append
let fiberParent = fiber.parent
while(!fiberParent.dom) {
fiberParent = fiberParent.parent
}
// fiber.effectTag 与 fiber.dom,优先判断 fiber.effectTag,
// 只要是 update,就走 update 逻辑
if(fiber.effectTag === 'update') {
updateProps(fiber.dom, fiber.props, fiber.alternate?.props)
} else if(fiber.effectTag === 'placement') {
if(fiber.dom) {
fiberParent.dom.append(fiber.dom)
}
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
- 更新 props、绑定事件
function updateProps(dom, nextProps, prevProps) {
Object.keys(prevProps).forEach(key => {
if(key !== 'children') {
// 旧有新无,删除
if(!(key in nextProps)) {
dom.removeAttribute(key)
}
}
})
Object.keys(nextProps).forEach((key) => {
if (key !== 'children') {
// 新有旧有,修改;新有旧无,新增
if(nextProps[key] !== prevProps[key]) {
// 绑定事件
if(key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase()
dom.removeEventListener(eventName, prevProps[key])
dom.addEventListener(eventName, nextProps[key])
// 绑定 props 属性
} else {
dom[key] = nextProps[key]
}
}
}
})
}
更新 children
- type 不一致(!iSameType),删除旧的,创建新的
- 收集 oldFiber 到 deletions,在 commitWork 之前删除
// React.js
let nextWorkOfUnit = null
let wipRoot = null
let currentRoot = null // 用于更新 dom
let deletions = [] // 收集旧的 fiber,删除
收集,!iSameType 收集 oldFiber
// 收集 oldFiber
function reconcileChildren(fiber, children) {
let prevChild = null
let oldFiber = fiber.alternate?.child // 旧fiber,指向当前fiber的后备指针的child
children.forEach((child, index) => {
// 判断新旧fiber是否同类型
const isSameType = oldFiber && oldFiber.type === child.type
let newFiber = {
type: child.type,
props: child.props,
parent: fiber,
child: null,
sibling: null,
}
if(isSameType) {
newFiber = {
...newFiber,
dom: oldFiber.dom,
alternate: oldFiber,
effectTag: 'update'
}
} else {
newFiber = {
...newFiber,
dom: null,
effectTag: 'placement',
}
// !isSameType,收集oldFiber
if(oldFiber) {
deletions.push(oldFiber)
}
}
// 移动指针
if(oldFiber) {
oldFiber = oldFiber.sibling
}
// 初始化 child、sibling
if(index === 0) {
fiber.child = newFiber
} else {
prevChild.sibling = newFiber
}
prevChild = newFiber
})
}
删除,在 commitWork 之前先删除oldFiber.dom
// 删除
function commitRoot() {
deletions.forEach(commitDeletions) // 在 commitWork 之前先删除oldFiber.dom
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
deletions = [] // 完成一次提交,需要清空这一轮的旧节点集合
}
function commitDeletions(fiber) {
// 函数组件
if(!fiber.dom) {
commitDeletions(fiber.child)
} else {
let fiberParent = fiber.parent
while(!fiberParent.dom) {
fiberParent = fiberParent.parent
}
// 删除 oldFiber.dom
fiberParent.dom.removeChild(fiber.dom)
}
}
-
新的比老的少,多出来的删除
循环children结束,还有 oldFiber 的话,需要收集到 deletions(旧有新无)
// 收集 oldFiber
function reconcileChildren(fiber, children) {
// 省略...
children.forEach((child, index) => {
// 省略...
})
// 循环结束,还有 oldFiber 的话,需要收集到 deletions(旧有新无)
while(oldFiber) {
deletions.push(oldFiber)
// 移动指针指向兄弟节点
oldFiber = oldFiber.sibling
}
}
- 解决 edge case
let bar = <div>bar</div>
let foo = <div>foo</div>
let show = false
function Foo() {
return (
<div id="foo">
{show ? foo : bar}
</div>
)
}
对于这种情况,jsx 会把 show
解析为 div#foo
的一个 child,所以 child 有可能是一个 boolean
值,需要对 child 做一下判断
function reconcileChildren(fiber, children) {
let prevChild = null
let oldFiber = fiber.alternate?.child // 旧fiber,指向当前fiber的后备指针的child
children.forEach((child, index) => {
// 判断新旧fiber是否同类型
const isSameType = oldFiber && oldFiber.type === child.type
// 这里就先不初始化,让其为 undefined
let newFiber
if(isSameType) {
newFiber = {
type: child.type,
props: child.props,
parent: fiber,
child: null,
sibling: null,
dom: oldFiber.dom,
alternate: oldFiber,
effectTag: 'update'
}
} else {
// child 为 true,才赋值给 newFiber
if(child) {
newFiber = {
type: child.type,
props: child.props,
parent: fiber,
child: null,
sibling: null,
dom: null,
effectTag: 'placement',
}
}
// !isSameType,收集oldFiber
if(oldFiber) {
deletions.push(oldFiber)
}
}
// 移动指针
if(oldFiber) {
oldFiber = oldFiber.sibling
}
// 初始化 child、sibling
if(index === 0) {
fiber.child = newFiber
} else {
prevChild.sibling = newFiber
}
// 如果存在 newFiber 的话才把 prevChild 更新为 newFiber
if(newFiber) {
prevChild = newFiber
}
})
// 省略...
}
测试
// App.jsx
import React from './core/React.js'
let count = 1
let props = { id: 'foo' }
let bar = <div>bar</div>
let show = false
function Foo() {
let foo = <div>foo</div>
function handleClick() {
count++
props = { className: 'foo' }
show = !show
console.log('foo click count: ', count)
React.update()
}
return (
<div {...props}>
{show ? foo : bar}
<button onClick={handleClick}>foo click</button>
</div>
)
}
function AppContainer() {
return (
<div id="app">
<h1>App</h1>
<Foo />
</div>
)
}
const App = AppContainer()
export default App
初始状态
更新后
- 优化,无需更新的函数组件不用处理
// App.jsx
import React from './core/React.js'
function Foo() {
console.log('foo function component')
function handleClick() {
console.log('foo click')
React.update()
}
return (
<div id="foo">
<h1>Foo</h1>
<button onClick={handleClick}>foo click</button>
</div>
)
}
function Bar() {
console.log('bar function component')
function handleClick() {
console.log('bar click')
React.update()
}
return (
<div id="bar">
<h1>Bar</h1>
<button onClick={handleClick}>bar click</button>
</div>
)
}
function App() {
console.log('app function component')
function handleClick() {
console.log('app click')
React.update()
}
return (
<div id="app">
<h1>App</h1>
<button onClick={handleClick}>app click</button>
<Foo />
<Bar />
</div>
)
}
export default App
像这样子,每点击按钮更新 Foo、Bar,都会从根节点一直往下直到最后一个函数组件 Bar 调用一遍
因为在 update 函数内,nextWorkOfUnit 每次都是更新为 wipRoot,指向的是 currentRoot
链表头节点
function update() {
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot // 后备指针,指向上一次的 root(currentRoot,在 commitWork 之后被赋值了)
}
nextWorkOfUnit = wipRoot
}
但更新函数组件,起始点和终点还是可以控制在当前函数组件树上的。
-
起始点:进行调用函数组件的时候
-
终点:当遍历完整棵树,开始处理兄弟节点之前
- 要控制 nextWorkOfUnit 为当前函数组件的根节点,需要引入变量
wipFiber
,在函数组件被调用的时候更新值
// React.js
let nextWorkOfUnit = null
let wipRoot = null
let currentRoot = null // 用于更新 dom
let deletions = []
let wipFiber = [] // 用于指向当前更新的函数组件根节点
function updateFunctionComponent(fiber) {
// 更新 wipFiber 为当前函数组件的根节点
wipFiber = fiber
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
- 在 update 里获取 wipFiber
function update() {
console.log(wipFiber)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot // 后备指针,指向上一次的 root(currentRoot,在 commitWork 之后被赋值了)
}
nextWorkOfUnit = wipRoot
}
但发现wipFiber 被最后的函数组件fiber覆盖了
2.1 update函数返回闭包,在函数组件被调用时候立即调用 React.update,获取返回值,这样可以避免获取的 wipFiber 被最后的函数组件覆盖的问题
// React.js
function update() {
const currentFiber = wipFiber
return () => {
wipRoot = {
...currentFiber,
alternate: currentFiber,
}
nextWorkOfUnit = wipRoot
}
}
// App.jsx
import React from './core/React.js'
function Foo() {
console.log('foo function component')
const update = React.update()
function handleClick() {
console.log('foo click')
update()
}
return (
<div id="foo">
<h1>Foo</h1>
<button onClick={handleClick}>foo click</button>
</div>
)
}
function Bar() {
console.log('bar function component')
const update = React.update()
function handleClick() {
console.log('bar click')
update()
}
return (
<div id="bar">
<h1>Bar</h1>
<button onClick={handleClick}>bar click</button>
</div>
)
}
function App() {
console.log('app function component')
const update = React.update()
function handleClick() {
console.log('app click')
update()
}
return (
<div id="app">
<h1>App</h1>
<button onClick={handleClick}>app click</button>
<Foo />
<Bar />
</div>
)
}
export default App
再看下输出的wipFiber
是正常对应到的函数组件
- 终点:当遍历完整棵树,开始处理兄弟节点之前
(1)在执行完 performWorkOfUnit,得到新的 nextWorkOfUnit,nextWorkOfUnit 可能是 Foo 的 child,也可能是 Foo 的 sibling,也可能是到达了最后的节点了返回undefined
(2)此时,通过判断 wipRoot.sibling?.type 与 nextWorkOfUnit?.type 是否相等,可以得知相等则nextWorkOfUnit指向了 Foo 的 sibling,处于 Foo vdom 树的末尾,则可以结束 此 Foo vdom 树的更新,把 nextWorkOfUnit 置为 undefined,表示不再进行下一轮循环,也就不会对 Foo 的 sibling 做处理
function workLoop(deadline) {
let shouldYeild = false
while(!shouldYeild && nextWorkOfUnit) {
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit)
// 如果nextWorkOfUnit是当前函数组件树的兄弟节点,则结束
if(wipRoot.sibling?.type === nextWorkOfUnit?.type) {
console.log(',,,', wipRoot, nextWorkOfUnit)
nextWorkOfUnit = undefined // 置为undefined退出 while 循环
}
shouldYeild = deadline.timeRemaining() < 1
}
// 完成所有节点的链表结构转换后,统一添加
if(!nextWorkOfUnit && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
useState
最简实现
useState 需要重新设置 nextWorkOfUnit
function useState(initial) {
const currentFiber = wipFiber
const oldHook = currentFiber.alternate?.stateHook
const stateHook = {
state: oldHook ? oldHook.state : initial,
}
// 把 stateHook绑定到 currentFiber,方便更新后通过oldHook获取
currentFiber.stateHook = stateHook
function setState(action) {
stateHook.state = action(stateHook.state)
// 需要更新 wipRoot,nextWorkOfUnit,
// 以重新调用函数组件->调用useState->更新 state
wipRoot = {
...currentFiber,
alternate: currentFiber,
}
nextWorkOfUnit = wipRoot
}
// 以数组形式返回,这样外面可以自定义命名
return [stateHook.state, setState]
}
// App.jsx
import React from './core/React.js'
function App() {
console.log('app function component')
const update = React.update()
const [count, setCount] = React.useState(10)
function handleClick() {
setCount((n) => n + 1)
}
return (
<div id="app">
<h1>App</h1>
<div>count is: {count}</div>
<button onClick={handleClick}>app click</button>
</div>
)
}
export default App
考虑多个 useState
-
使用数组索引来获取对应的stateHook
-
每次在函数组件调用的时候,置空
let stateHooks // 用于存储 stateHook
let stateHookIndex // 用于记录当前 stateHook 的 index
function useState(initial) {
const currentFiber = wipFiber
const oldHook = currentFiber.alternate?.stateHooks[stateHookIndex]
const stateHook = {
state: oldHook ? oldHook.state : initial,
}
stateHooks.push(stateHook)
stateHookIndex++
currentFiber.stateHooks = stateHooks
function setState(action) {
stateHook.state = action(stateHook.state)
// 需要更新 wipRoot,nextWorkOfUnit,
// 以重新调用函数组件->调用useState->更新 state
wipRoot = {
...currentFiber,
alternate: currentFiber,
}
nextWorkOfUnit = wipRoot
}
return [stateHook.state, setState]
}
function updateFunctionComponent(fiber) {
wipFiber = fiber
stateHooks = [] // 每次调用函数组件,先清空
stateHookIndex = 0 // 先置0
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
// App.jsx
import React from './core/React.js'
function App() {
console.log('app function component')
const update = React.update()
const [count, setCount] = React.useState(10)
const [name, setName] = React.useState('TOM')
function handleClick() {
setName((n) => n + ' Jerry')
setCount((n) => n + 1)
}
return (
<div id="app">
<h1>App</h1>
<div>count is: {count}</div>
<div>name is: {name}</div>
<button onClick={handleClick}>app click</button>
</div>
)
}
export default App
调用顺序:
-
updateFunctionComponent -> useState(10) -> useState('Tom')
- setCount、setName 保存着它们自己的 stateHook 状态
- setCount,
stateHook.state = stateHooks[0]
- setName,
stateHook.state = stateHooks[1]
-
handleClick
-
setName((n) => n + ' Jerry')
stateHook.state = 'Tom Jerry',即
stateHooks[1].state = 'Tim Jerry'
-
setCount((n) => n + 1)
stateHook.state = 11,即
stateHooks[0].state = 11
-
updateFunctionComponent -> useState(10) -> useState('Tom')
-
根据索引找到对应的 oldHook 进行更新 state
统一处理 action
添加 stateHook.queue
属性,收集 action
function useState(initial) {
const currentFiber = wipFiber
const oldHook = currentFiber.alternate?.stateHooks[stateHookIndex]
const stateHook = {
state: oldHook ? oldHook.state : initial,
queue: oldHook ? oldHook.queue : [],
}
// 执行 action
stateHook.queue.forEach(action => {
const eagerState = typeof action === 'function' ? action(stateHook.state) : action
// 状态没变,不做处理
if(eagerState != stateHook.state) {
stateHook.state = action(stateHook.state)
}
})
stateHooks.push(stateHook)
stateHookIndex++
stateHook.queue = [] // 遍调用完清空
currentFiber.stateHooks = stateHooks
function setState(action) {
// stateHook.state = action(stateHook.state)
// 收集action
// 考虑 action 不是函数的情况
stateHook.queue.push(typeof action === 'function' ? action : () => action)
// 需要更新 wipRoot,nextWorkOfUnit,
// 以重新调用函数组件->调用useState->更新 state
wipRoot = {
...currentFiber,
alternate: currentFiber,
}
nextWorkOfUnit = wipRoot
}
return [stateHook.state, setState]
}
useEffect
-
- useEffect
- 调用时机:在React完成对DOM的渲染后,并且浏览器完成绘制之前
- wipFiber.effectHooks
-
- clearup
- 调用时机:在调用useEffect 之前调用,当deps 为空,不会调用返回的
let effectHooks // 用于存储 useEffect 的 effectHook
function useEffect(callback, deps) {
const effectHook = {
callback,
deps,
clearup: null,
}
effectHooks.push(effectHook)
// 不需要重新设置 nextWorkOfUnit,
// 直接把 effectHooks 绑定到当前的函数组件 wipFiber
wipFiber.effectHooks = effectHooks
}
function updateFunctionComponent(fiber) {
effectHooks = [] // 每次调用函数组件,先清空
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function commitRoot() {
deletions.forEach(commitDeletions) // 在 commitWork 之前先删除 oldFiber
commitWork(wipRoot.child)
commitEffectHooks() // 在React完成对DOM的渲染后,并且浏览器完成绘制之前
currentRoot = wipRoot
wipRoot = null
deletions = [] // 完成一次提交,需要清空这一轮的旧节点集合
}
function commitEffectHooks() {
function run(fiber) {
if(!fiber) return
// 初始化
if(!fiber.alternate) {
fiber.effectHooks?.forEach(hook => {
hook.clearup = hook.callback()
})
// update
} else {
fiber.effectHooks?.forEach((newHook, index) => {
if(newHook.deps.length > 0) {
const oldHook = fiber.alternate?.effectHooks?.[index]
const needEffect = oldHook.deps.some((oldDep, i) => {
return oldDep !== newHook.deps[i]
})
if(needEffect) {
newHook.clearup = newHook.callback()
}
}
})
}
run(fiber.child)
run(fiber.sibling)
}
function runClearup(fiber) {
if(!fiber) return
// 执行 alternate 的 clearup
fiber.alternate?.effectHooks?.forEach(hook => {
hook.clearup?.()
})
runClearup(fiber.child)
runClearup(fiber.sibling)
}
// 在触发 effect callback 前,先把更新前的 clearup 执行了
runClearup(wipRoot)
run(wipRoot)
}