问题:dom 树特别大导致渲染卡顿
原因:js 单线程,在执行我们的某段逻辑就会阻塞后续的渲染
而我们之前的 render 函数就是通过递归实现的,如果dom节点特别多一样会导致卡顿
解决思路:拆分,将一个大任务拆成多个小任务,每个小任务都足够小
实现:采用 requestIdleCallback 分帧运算
function workLoop(deadline) {
console.log(deadline.timeRemaining())
let shouldYield = false
while(!shouldYield) {
// 这里为什么是小于 1
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
实现 fiber
问题:如何做到每次只渲染几个节点呢?
如何在下次执行的时候依然从之前的位置执行?
解决思路:
把树结构转变成链表结构
- child
- sibling
- parent.sibling
如果当前节点有子节点,下一个节点就是子节点,如果没有子节点下一个节点就是兄弟节点,如果连兄弟节点都没有下一个节点就是父亲的兄弟节点
上图变成链表就是: a->b->d->e->c->f->g
实现:
performUnitOfWork
- 创建dom
- 把 dom 添加到父容器内
- 设置 dom 的 props
- 建立关系 chil sibling parent
- 返回下一个节点
const createTextNode = (text) => {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
},
}
}
const createElement = (type, props, ...children) => {
return {
type,
props: {
...props,
children: children.map(child => typeof child === 'string' ? createTextNode(child) : child)
}
}
}
const render = (el, container) => {
console.log(1)
nextWorkOfUnit = {
dom: container,
props: {
children: [el]
}
}
}
// 下一个任务(节点)
let nextWorkOfUnit = null
function workLoop(deadline) {
console.log(3)
let shouldYield = false
while(!shouldYield && nextWorkOfUnit) {
console.log(2)
// 返回下一个节点
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit)
// 这里为什么是小于 1
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
const createDom = (type) => {
return type === 'TEXT_ELEMENT' ? document.createTextNode('') : document.createElement(type)
}
const updateProps = (dom, props) => {
Object.keys(props).forEach(attr => {
if (attr !== 'children') {
dom[attr] = props[attr]
}
})
}
const initChildren = (fiber) => {
// 4. 建立关系 child sibling parent
const children = fiber.props.children
let prevChild = null
children.forEach((child, index) => {
const newFiber = {
type: child.type,
props: child.props,
child: null, // child 和 sibling 初始化我们不知道
sibling: null,
parent: fiber,
dom: null
}
if (index === 0) {
fiber.child = newFiber
} else {
// 如果不是第一个孩子就需要绑定到sibling,也就是上一个孩子的sibling 上,所以我们需要知道上一个孩子
prevChild.sibling = newFiber
}
// 考虑到我们还需要设置 parent.sibling,因为我们是从上往下获取的,所以work肯定是顶层也就是 parent,我们只能给 child 设置,
// 但是如果直接在child 上加就会破坏原有结构,所以我们单独维护一个newWork 对象,
prevChild = newFiber
})
}
const performWorkOfUnit = (fiber) => {
if (!fiber.dom) {
// 1. 创建dom
const dom =(fiber.dom = createDom(fiber.type))
// 2. 把 dom 添加到父容器内
fiber.parent.dom.append(dom)
// 3. 设置 dom 的 props
updateProps(dom, fiber.props)
}
initChildren(fiber)
// 5. 返回下一个节点
if (fiber.child) {
return fiber.child
}
if (fiber.sibling) {
return fiber.sibling
}
return fiber.parent.sibling
}
requestIdleCallback(workLoop)
const React = {
render,
createElement
}
export default React
问题
使用 requestIdleCallback 渲染完一部分节点后没有空余时间了,就不会再继续渲染其他的节点,这时候用户就只会看到一部分节点,等有空余时间了再去渲染其他节点,用户才会看到完整的节点
解决方案:
我们之前代码是每次创建完dom后紧接着添加到父级容器里,我们可以后置在最后阶段把所有的 dom 添加到容器中不要在中途添加
需要知道两个点:
- 链表什么时候结束
当我们的 nextWorkOfUnit 为 null 时 - 需要知道根节点(需要递归的把所有dom添加到父级容器上)
也就是我们执行 render 时候初始化的节点
const render = (el, container) => {
root = (nextWorkOfUnit = {
dom: container,
props: {
children: [el]
}
})
}
let root = null
// 下一个任务(节点)
let nextWorkOfUnit = null
function workLoop(deadline) {
let shouldYield = false
while(!shouldYield && nextWorkOfUnit) {
// 返回下一个节点
nextWorkOfUnit = performWorkOfUnit(nextWorkOfUnit)
shouldYield = deadline.timeRemaining() < 1
}
// 链表结束
if (!nextWorkOfUnit && root) {
commitRoot()
}
requestIdleCallback(workLoop)
}
const commitRoot = () => {
commitWork(root.child)
root = null
}
const commitWork = (fiber) => {
if (!fiber) return
fiber.parent.dom.append(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
const performWorkOfUnit = (fiber) => {
if (!fiber.dom) {
// 1. 创建dom
const dom =(fiber.dom = createDom(fiber.type))
// 3. 设置 dom 的 props
updateProps(dom, fiber.props)
}