手写react六

88 阅读3分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」。

实现

我们将分为8步,一步一步的实现一个小型的React

  • 实现createElement函数
  • 实现render函数
  • Currnet Mode模式
  • fibers
  • render和commit阶段
  • 协调器
  • function组件
  • hooks

协调器reconciliation

在更新dom时,我们需要对新旧fiber节点进行对比,看看能不能复用,所以需要一个全局变量保存最后一次commit的fiber tree,我们命名为currentFiber

我们还需要对每个fiber进行连接,即新fiber有一个指针指向旧fiber,方便我们查找对比。

function commitRoot() {
    commitWork(workInProcessFiber.child);
    currentFiber = workInProcessFiber; // current tree与workInProcess tree互换
    workInProcessFiber = null;
}
function commitWork(fiber) {
    if (!fiber) {
        return;
    }
    const domParent = fiber.parent.dom;
    domParent.appendChild(fiber.dom);
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}
function render(element, container) {
    workInProcessFiber = {
        dom: continer,
        props: {
            children: [element]
        },
        alternate: currentFiber, // 指针指向最后一次提交的fiber tree
    }
    nextUnitOfWork = workInProcessFiber
}
let currentFiber = null; // 保存最后一次提交的fiber tree
let nextUnitOfWork = null;
let workInProcessFiber = null;

下一步我们重构一下performUnitOfWork方法,将创建fiber tree的代码抽离出来成一个新函数reconcileChildren,我们会根据旧fiber来创建出新fiber来。

function performUnitOfWork(fiber) {
    // 创建dom
    if (!fiber.dom) {
        fiber.dom = createDom(fiber);
    }
    
    const elements = fiber.props.children;
    reconcileChildren(fiber, elements);
   
    // 省略。。。
}

function reconcileChildren(wipFiber, elements) {
    // TODO
}

在reconcileChildren里我们可以通过alternate获取旧fiber的子节点,拿新元素跟旧fiber节点对比:

  • 如果旧fiber和子元素具有相同的type,仅需要更新它props,标记为更新
if (sameType) {
    // 如果类型相同,可以复用旧dom节点,标记为更新
    newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",
    }
}
  • 如果type不同并且是新元素,意味着我们需要创建新的dom节点,我们给这个fiber加上新建的标记
if (element && !sameType) {
    // 如果类型不同且有新节点,标记为替换
    newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
    }
}
  • 如果type不同并且是旧fiber,则需要删除旧fiber
if (oldFiber && !sameType) {
    // 如果类型不同且存在旧节点,标记删除
    oldFiber.effectTag = "DELETION";
    deletions.push(oldFiber);
}

这里还需要新增一个全局变量,来记录需要删除的fiber

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element],
        },
        alternate: currentRoot,
    }
    deletions = [];
    nextUnitOfWork = wipRoot
}


let currentFiber = null; // 保存最后一次提交的fiber tree
let nextUnitOfWork = null;
let workInProcessFiber = null;
let deletions = null; // 存储需要删除的fiber

来看下完整的代码

function reconcileChildren(wipFiber, elements) {
    let index = 0;
    let oldFiber =
        wipFIber.alternate &&
        wipFiber.alternate.child;
    let prevSibling = null;

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

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

        if (sameType) {
            // 如果类型相同,可以复用旧dom节点,标记为更新
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: "UPDATE",
            }
        }

        if (element && !sameType) {
            // 如果类型不同且有新节点,标记为替换
            newFiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null,
                effectTag: "PLACEMENT",
            }
        }

        if (oldFiber && !sameType) {
            // 如果类型不同且存在旧节点,标记删除
            oldFiber.effectTag = "DELETION";
            deletions.push(oldFiber);
        }

        if (index === 0) {
            wipFiber.child = newFiber;
        } else {
            prevSibling.sibling = newFiber
        }
        prevSibling = newFiber;
        index++
    }
}

前面我们对新旧fiber进行了对比,并给fiber添加标记,标记完之后会在commit阶段,根据标记一次性完成dom操作,下面我们来改造commitWork方法,根据effectTag来完成对应的操作。

function commitRoot() {
    deletions.forEach(commitWork)
    commitWork(workInProcessFiber.child)
    currentFiber = workInProcessFiber
    workInProcessFiber = null
}
function updateDom(dom, prevProps, nextProps) {
    // TODO
}
function commitWork(fiber) {
    if (!fiber) {
        return
    }
    let domParent = fiber.parent.dom;
    if (fiber.effectTag === 'PALCEMENT' &&
        fiber.dom != null
    ) {
        // 如果标记是PALCEMENT并且dom存在,直接复用dom
        domParent.appendChild(fiber.dom);
    } else if (fiber.effectTag === 'UPDATE' &&
        fiber.dom != null
    ) {
        // 如果标记是UPDATE并且dom不存在,更新dom
        updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    } else if (fiber.effectTag === 'DELETION') {
        // 如果标记是DELETION,我们直接删除dom
        domParent.removeChild(fiber.dom);
    }
    
    commitWork(fiber.child);
    commitWork(fiber.sibling);
}

如果标记是UPDATE并且dom不存在,我们会更新dom的props,具体是对比新旧props,进行增删。

const isProperty = key => key !== "children"
const isNew = (prev, next) => key => prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
    // 删除旧的props
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {
            dom[name] = ""
        })
    // 加入新的props或者更新同名props
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            dom[name] = nextProps[name]
        })
}

有一类props需要特殊处理的,那就是on开头的监听事件

const isEvent = key => key.startsWith("on")
const isProperty = key =>
key !== "children" && !isEvent(key)

const isProperty = key => key !== "children"
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]
      )
  })
  
  // 删除旧的props
  Object.keys(prevProps)
      .filter(isProperty)
      .filter(isGone(prevProps, nextProps))
      .forEach(name => {
          dom[name] = ""
      })
      
  // 加入新的props
  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])
      })
}

总结

  1. 在render阶段,也就是reconciliation中,会根据render传入的dom和最后一次提交的fiber tree进行对比,给fiber添加标记

  2. react中的reconciliation会使用diff算法进行优化,后面单独梳理

  3. 在commit阶段,会根据fiber的标记,对dom进行增删改