【学习记录】构建基于Fiber的自定义React

106 阅读3分钟

我跟着这篇文章 # 构建你自己的 React 一步一步的从零实现了一个自定义的基于Fiber的React,如果你也想这样的话,请直接点开这个连接,尽管内容是英文的,你可以用浏览器自带的谷歌翻译,一样会有很棒的体验OvO,祝顺利

自定义React的功能

  1. createElment
  2. render
  3. 并发模式
  4. fiber
  5. 渲染和提交
  6. 协调
  7. 函数组件
  8. hook

自定义React的运行流程

React运行流程图.png

代码

index.js

小提示:如果想让babel调用我们自定义的createElement,需要加上两行注释 /** @jsxRuntime classic */,/** @jsx Didact.createElement */

/** @jsxRuntime classic */
import { Didact } from './Didact'

/** @jsx Didact.createElement */
function Counter(){
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1) }>
        Count: {state}
      </h1>
  )
}

const element = <Counter></Counter>

const root =document.getElementById('root')
Didact.render(element, root)

Didact.js

/* 
创建React元素---也就是虚拟DOM,为了区分,虚拟DOM称为React元素,真实DOM称为DOM
*/
function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                //children数组里既有基本类型(String)又有对象类型(JSX元素),为基本类型创建一个元素,元素的类型为'TEXT_ELEMENT'
                return typeof child === 'object' ? child : createTextElement(child)
            })
        }
    }
}

//React 不会包装原始值或在没有 时创建空数组children,但我们这样做是因为它会简化我们的代码,
function createTextElement(text) {
    return {
        type: 'TEXT_ELEMENT',
        props: {
            nodeValue: text,
            children: []
        }
    }
}

//创建真实DOM
function createDOM(fiber) {
    const dom = fiber.type === 'TEXT_ELEMENT' ?
        document.createTextNode('') : document.createElement(fiber.type)

    Object.keys(fiber.props).filter(key => key !== 'children').forEach(name => {
        if (isEvent(name)) {
            const eventType = name.toLowerCase().substring(2)
            dom.addEventListener(eventType, fiber.props[name])
        }else{
            dom[name] = fiber.props[name]
        }
      })
    return dom
}


//在render函数中,创建根fiber,并设置为nextUnitOfWork,剩下的工作发生在performUnitOfWork函数上
function render(element, container) {
    wipRoot = { // fiber对象
        dom: container, // dom属性指向虚拟DOM对应的真实DOM
        props: {
            children: [element] //子元素
        },
        alternate: currentRoot //保存对旧fiber树的指针
    }
    deletions = []
    nextUnitOfWork = wipRoot
}

let nextUnitOfWork = null //下一个工作单元
let wipRoot = null //我们将跟踪纤维树的根。我们将其称为进行中的根或wipRoot.
let currentRoot = null //保存上一个fiber树的引用
let deletions = null //保存将要删除的旧fiber

//循环工作函数
function workLoop(deadline) {
    let shouldYield = false //是否交出执行权
    while (nextUnitOfWork && !shouldYield) { // 有下一个工作单元 && 有执行权
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork) //执行当前工作单元,并返回下一个工作单元
        shouldYield = deadline.timeRemaining() < 1 //判断是否有时间片
    }

    //一旦完成所有的工作(没有下一个工作单元),我们就将整个fiber树提交给DOM
    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }

    //代码跳出while,有两点原因:1. 时间片已完,需要交出执行权 2. 已经没有下一个工作单元,任务已结束
    //请求浏览器在空闲时执行此任务
    //这里其实是一个无限循环,在每一帧都会进入workLoop
    requestIdleCallback(workLoop)
}

//开启任务,请求浏览器在空闲时执行此任务
requestIdleCallback(workLoop)


/**
 * 做了3件事
 * 1. 为元素创建一个DOM
 * 2. 为元素的子元素创建fiber
 * 3. 选择下一个工作单元(fiber)
 * @param {*} fiber 每个fiber也是最小的工作单元
 */
function performUnitOfWork(fiber) {

    const isFunctionComponent = fiber.type instanceof Function
    if (isFunctionComponent) {
        updateFunctionComponent(fiber)
    } else {
        //1. 为元素创建一个DOM
        //2. 为元素的子元素创建fiber
        updateHostComponent(fiber)
    }


    // 3.最后我们寻找下一个工作单元,我们首先联系孩子,然后联系兄弟,然后与叔叔,等等
    if (fiber.child) {
        return fiber.child //如果有子fiber,则作为下一个工作单元返回
    }
    let nextfiber = fiber
    while (nextfiber) {
        if (nextfiber.sibling) { //如果有弟弟节点,则作为下一个工作单元返回
            return nextfiber.sibling
        }
        nextfiber = nextfiber.parent //向fiber树上层查找,这一步是联系叔叔节点
        // 一直向上查找,知道找到根节点,结束
    }
}

/**
 * 更新原生DOM组件
 */
function updateHostComponent(fiber) {
    // 1. 为元素创建一个DOM
    if (!fiber.dom) { //如果DOM不存在则创建  注意根fiber不会进入这行代码,因为根fiber的dom指向容器DOM
        fiber.dom = createDOM(fiber)
    }

    //这里有一个问题,每次我们处理一个元素时,都会向DOM树上挂载一个DOM节点,
    //但是浏览器在我们渲染完整个DOM树之前中断我们的工作,这是会显示一个不完整的UI
    //我们并不希望这样,所以此时创建了DOM,但并不是挂载到DOM树上的好时机
    /* if (fiber.parent) { //注意根fiber不会进入这行代码,因为根fiber没有parent属性
        fiber.parent.dom.appendChild(fiber.dom) //将DOM挂载到DOM树上
    } */

    // 2. 为每一个子元素创建一个fiber,并维护一个fiber树
    const elements = fiber.props.children //拿到fiber的所有子元素
    reconcileChildren(fiber, elements)
}

//调用函数组件之前初始化一些全局变量,以便我们可以在useState函数内部使用它们
let hookIndex = null
let wipFiber = null
/**
 * 函数式组件 在updateFunctionComponent我们运行函数来获取孩子。
 * 
 * @param {*} fiber 
 */
function updateFunctionComponent(fiber) {
    wipFiber = fiber
    hookIndex = 0
    wipFiber.hooks = []
    const children = [fiber.type(fiber.props)] //fiber.type 就是函数组件的那个函数,传入props,运行获取子元素
    reconcileChildren(fiber, children) //协调子元素
}

function useState(initial) {
    console.log('hookIndex', hookIndex)
    const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hooks &&
        wipFiber.alternate.hooks[hookIndex]
    const hook = {
        state: oldHook?oldHook.state:initial,
        queue: [],  // action队列,保存对状态的修改
    }
    
    const actions = oldHook ? oldHook.queue:[]
    actions.forEach( action =>{
        hook.state = action(hook.state)
    })

    const setState = action =>{
        hook.queue.push(action)
        //然后我们做一些类似于我们在render函数中所做的事情,
        //将一个新的正在进行的工作根设置为下一个工作单元,
        //这样工作循环就可以开始一个新的渲染阶段
        console.log('currentRoot', currentRoot)
        //重新从根节点渲染
        wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot
        }
        nextUnitOfWork = wipRoot
        deletions = []
    }

    wipFiber.hooks.push(hook)
    hookIndex++
    return [hook.state, setState]
}

function reconcileChildren(wipFiber, elements) {
    let index = 0
    let prevSibling = null //前一个兄弟
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child //获取到旧fiber
    while (index < elements.length || oldFiber != null) {
        const element = elements[index]
        let newfiber = null

        const sameType = oldFiber && element && element.type === oldFiber.type

        //当旧的 Fiber 和元素的类型相同时, 我们创建一个新的 Fiber, 保留旧 Fiber 的 DOM 节点,更新元素的 props。
        //我们还为纤维添加了一个新属性: effectTag.我们稍后会在提交阶段使用这个属性。
        if (sameType) {
            //更新这个节点
            newfiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber, //记录旧fiber
                effectTag: 'UPDATE',
            }
        }
        //然后对于元素需要新的 DOM 节点的情况,我们用PLACEMENT效果标签标记新的fiber。
        if (element && !sameType) {
            //增加这个节点
            newfiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null, //新增的没有fiber
                effectTag: 'PLACEMENT'
            }
        }
        if (oldFiber && !sameType) {
            //删除旧节点
            oldFiber.effectTag = 'DELETION'
            deletions.push(oldFiber) //需要一个数组来跟踪将要删除的节点
        }

        if (oldFiber) {
            oldFiber = oldFiber.sibling
        }

        //我们将新的fiber加入到fiber树中,具体加到孩子节点还是兄弟节点取决于第一个子元素(大儿子)
        if (index === 0) { //是第一个子元素
            wipFiber.child = newfiber //父fiber的child指向第一个子fiber
        } else { //其他的子元素加入到兄弟节点
            prevSibling.sibling = newfiber
        }
        prevSibling = newfiber //保存上一个兄弟fiber
        index++

    }
}

const isEvent = key => key.startsWith("on")
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)
              dom.removeEventListener(
                  eventType,
                  prevProps[name]
              )
          })

    // Remove old properties
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {dom[name] = ''})

    // Set new or changed properties
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .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 commitRoot() {
    deletions.forEach(commitWork)
    commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
}

function commitWork(fiber) {
    if (!fiber) {
        return
    }
    //没有dom属性的fiber
    let domParentFiber = fiber.parent
    //我们需要找到DOM节点的父节点,沿着fiber树向上查找
    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 === 'DELETION') {
        //并且当移除一个节点时,我们还需要继续,直到我们找到一个带有 DOM 节点的子节点
        commitDeletion(fiber, domParent)
    } else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
        updateDom(
            fiber.dom,
            fiber.alternate.props,
            fiber.props
        )
    }

    commitWork(fiber.child)
    commitWork(fiber.sibling)
}

function commitDeletion(fiber, domParent) {
    if (fiber.dom) {
        domParent.removeChild(fiber.dom)
    } else {
        commitDeletion(fiber.child, domParent)
    }
}


export const Didact = {
    createElement,
    render,
    useState
}