React Fiber树代码实现(附带详细注解)

159 阅读5分钟

最近了解了Fiber树,发现对react的原理不太熟悉,出于对技术的好奇心就查询资料实现了一下React的基本代码以及useStateuseEffect

前言

fiber树节点结构

newFiber = {
    type: '',
    props: '',
    dom: '',
    parent: '',
    alternate: '',
    effectTag: '',
    sibling: '',
    child: ''
  }
  
  • type —— 用于存放标签名如<h1><span>,如果是函数组件的话那么存放的就是该函数
  • props —— dom节点上的属性
  • dom —— 存放真实的dom元素
  • parent —— 指向父节点
  • alternate —— 存放历史节点
  • effectTag —— 存放更新标签,如UPDATEPLACEMENTDELETION
  • sibling —— 指向相邻的下一个兄弟节点
  • child —— 指向第一个孩子节点

Tip:如果代码还是不好理解可以把项目拉下来打断点,这样更容易理解数据的流向以及具体的呈现形态。

gitee地址: gitee.com/stone710/my…

    function createElement(type,props,...children) {
      return {
        type,
        props: {
          ...props,
          children: children.map(child => 
            typeof child === 'object'? child : createTextElement(child)
          )
        }
      }
    }

    function createTextElement(text) {
      return {
        type: 'TEXT_ELEMENT',
        props: {
          nodeValue: text,
          children: []
        }
      }
    }

    // 根据fiber节点去创建dom节点
    function createDom(fiber) {
      const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode('') : document.createElement(fiber.type)

      updateDom(dom, {}, fiber.props)

      return dom
    }

    const isEvent = key => key.startsWith('on') //监听事件的属性都是以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) {
      // 移除旧的或者是改变事件监听
      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])
      })

      // 移除旧的属性
      Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps))
      .forEach(name => {
        dom[name] = ''
      })

      // 添加新的属性或者是更新属性
      Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps))
      .forEach(name => {
        dom[name] = nextProps[name]
      })

      // 添加事件监听
      Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps))
      .forEach(name => {
        const eventType = name.toLowerCase().substring(2)
        dom.addEventListener(eventType, nextProps[name])
      })
    }

    // 提交fiber树对dom节点进行更新,删除的节点因为已经不存在当前的fiber树上面,所以需要用deletions存储并且单独遍历
    function commitRoot() {
      deletions.forEach(commitWork)
      commitWork(wipRoot.child)
      currentRoot = wipRoot
      wipRoot = null
    }

    // 递归遍历整棵树
    function commitWork(fiber) {
      if (!fiber) {
        return
      }

      let domParentFiber = fiber.parent
      // 这个循环是因为如果是fiber是函数类型的话他是没有dom节点的,那么就需要往上去找dom节点
      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') {
        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)
      }
    }

    // 设置fiber树的根节点并且赋值给nextUniOfWork,wrokLoop函数判断nextUniOfWork不为空则开始遍历fiber树
    function render(element, container) {
      wipRoot = {
        dom: container,
        props: {
          children: [element]
        },
        alternate: currentRoot
      }
      deletions = []

      nextUniOfWork = wipRoot
    }

    let nextUniOfWork = null //下一个执行的任务
    let currentRoot = null // 当前提交给DOM的最近一次的fiber树
    let wipRoot = null // wipRoot为fiber树的根节点
    let deletions = null // 记录需要删除的fiber节点

    function workLoop(deadline) {
      let shouldYield = false
      // 当遍历完整个fiber树或者剩余时间不够时结束循环
      while (nextUniOfWork && !shouldYield) {
        // 处理当前工作区内容并且返回下一个工作区内容
        nextUniOfWork = performUnitOfWork(
          nextUniOfWork
        )
        shouldYield = deadline.timeRemaining() < 1
      }

      // 如果没有下一个工作内容而且fiber树的根节点存在
      if(!nextUniOfWork && wipRoot) {
        commitRoot()
      }

      requestIdleCallback(workLoop)
    }

    // requestIdleCallback 是一个用于在浏览器空闲时执行回调函数的API,回调函数接受一个 deadline 对象作为参数。deadline 对象有一个 timeRemaining 方法,用于返回剩余的执行时间。
    requestIdleCallback(workLoop)

    function performUnitOfWork(fiber) {
      //判断是否是函数组件 函数组件没有dom节点
      const isFunctionComponent = fiber.type instanceof Function

      if (isFunctionComponent) {
        updateFunctionComponent(fiber)
      } else {
        updateHostComponent(fiber)
      }


      // 判断是否有子节点 如果有子节点先遍历子节点 类似于深度优先遍历
      if (fiber.child) {
        return fiber.child
      }

      // 到了这里说明当前fiber已经没有了child,但是也不是最深的节点,因为兄弟节点里面可能也会有child
      // 先判断当前节点有没有兄弟节点 如果有兄弟节点就返回兄弟节点(然后遍历所有的兄弟节点),如果没有这返回父节点的兄弟节点(然后遍历所有的父节点的兄弟节点)
      let nextFiber = fiber
      while(nextFiber) {
        if(nextFiber.sibling) {
          // 这里返回的是fiber的父节点的兄弟节点,因为在上一次循环结束的时候nextFiber已经指向了fiber的父节点 注意没有直接返回父节点是因为上一个判断当前节点是否有子节点 这样会造成死循环
          return nextFiber.sibling
        }

        nextFiber = nextFiber.parent
      }
    }

    let wipFiber = null // 当前处理的函数fiber,当执行fiber.type的时候会调用useStatue函数,用于useState获取到当前函数fiber
    let stateHookIndex = null // 钩子队列的索引,用于获取上一个hook的状态
    let effectHookIndex = null

    function updateFunctionComponent(fiber) {
      wipFiber = fiber
      stateHookIndex = 0
      effectHookIndex = 0
      // 存放在函数Fiber里面的钩子
      wipFiber.stateHooks = []
      wipFiber.effectHooks = []
      // fiber.type执行返回的就是函数组件里面最外层的dom元素,这样就解决了函数组件没有dom的问题
      const children = [fiber.type(fiber.props)]
      reconcileChildren(fiber, children)
    }

    function useState(initial) {
      const oldHook = 
        wipFiber.alternate &&
        wipFiber.alternate.stateHooks &&
        wipFiber.alternate.stateHooks[stateHookIndex]

      const hook ={
        // 更新的话拿上一次的值,初始化则使用传入的值
        state: oldHook? oldHook.state : initial,
        queue: [],
      }

      // 初始化的时候是没有操作的,需要调用了setState队列里面才会有具体操作(下文所说的上面的代码!!!)
      const actions = oldHook ? oldHook.queue : []
      actions.forEach(action => {
        hook.state = action(hook.state)
      })

      const setState = action => {
        // 将一些更新的操作放到队列里面,触发更新以后会在上面的代码里面执行
        hook.queue.push(action)
        // 给wipRoot赋值就会触发页面更新,调用workLoop函数
        wipRoot = {
          dom: currentRoot.dom,
          props: currentRoot.props,
          alternate: currentRoot
        }
        nextUniOfWork = wipRoot
        deletions = []
      }

      wipFiber.stateHooks.push(hook)
      stateHookIndex++
      return [hook.state, setState]
    }

    function useEffect(callback, dependencies) {
      const oldHook = 
        wipFiber.alternate &&
        wipFiber.alternate.effectHooks &&
        wipFiber.alternate.effectHooks[effectHookIndex]

      const hook = {
        callback,
        dependencies,
      }

      if(oldHook) {
// 判断依赖是否发生变化,如果发生变化则执行回调函数
if(dependencies.some((dep, index) => dep !== oldHook.dependencies[index])) {
  // 判断是否有清除副作用的函数 有的话优先执行
  if (oldHook.cleanup) {
    oldHook.cleanup()
  }
  hook.cleanup = oldHook.callback()
}
  } else {
    // 判断是否第一次执行,并且传入依赖值为空
    if(!wipFiber.alternate && dependencies.length === 0) {
      hook.cleanup = callback()
    }
  }

      wipFiber.effectHooks.push(hook)
      effectHookIndex++
    }

    function updateHostComponent(fiber) {
      if (!fiber.dom) {
        fiber.dom = createDom(fiber)
      }

      const elements = fiber.props.children
      reconcileChildren(fiber, elements)
    }

    // 遍历children为子节点创建fiber
    function reconcileChildren(wipFiber, elements) {
      let index = 0
      let olderFiber = wipFiber.alternate && wipFiber.alternate.child
      let prevSibling = null

      while (index < elements.length || olderFiber != null) {
        const element = elements[index]
        let newFiber = null

        const sameType = olderFiber && element && element.type === olderFiber.type

        if (sameType) {
          newFiber = {
            type: olderFiber.type,
            props: element.props,
            dom: olderFiber.dom,
            parent: wipFiber,
            alternate: olderFiber,
            effectTag: 'UPDATE'
          }
        }
        if (element && !sameType) {
          newFiber = {
            type: element.type,
            props: element.props,
            dom: null,
            parent: wipFiber,
            alternate: null,
            effectTag: 'PLACEMENT'
          }
        }
        if (olderFiber && !sameType) {
          olderFiber.effectTag = 'DELETION'
          deletions.push(olderFiber)
        }

        if (olderFiber) {
          olderFiber = olderFiber.sibling
        }

        // fiber的父节点只有一个child指向子节点的第一个元素 后续的子节点不能通过父节点直接访问 都是以单向链表的形式通过child去访问
        // fiber
        // ↓child
        // element[0] --sibling--> element[1] --sibling--> element[2]
        // 子节点都有一个parent指向父节点
        if(index === 0) {
          wipFiber.child = newFiber
        } else if (element) {
          prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
      }
    }


    const Didact = {
      createElement,
      render,
      useState,
      useEffect
    }

    /** @jsx Didact.createElement */
    function Counter() {
      const [state, setState] = Didact.useState(1)
      Didact.useEffect(() => {
        window.alert('你点击了'+ state +'次')
      }, [state])
      Didact.useEffect(() => {
        window.alert('第一次进入')
      }, [])
      return (
        <div>
          <h1 onClick={() => setState(c => c + 1)}>
            Count: {state}
          </h1>
          <h3>
            Clicked: {state} times
          </h3>
        </div>
      )
    }
    const element = <Counter />

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