一个迷你react的实现

102 阅读6分钟

一个迷你 react 实现, 实现了解析 jsx, render,useState, useEffect, 用浏览器的requestIdleCallback来实现模拟react调度器的效果并为了兼容性实现了这个api, 帮助你深入了解 react 的底层原理

觉得有帮助的可以去github给个starwinhhh666/mini-react: 一个迷你react实现, 实现了解析jsx, render,useState, useEffect, 帮助你深入了解react的底层原理 (github.com)

(function () {
  //总览
  //jsx 通过render function(注意这个render function不是我们react实现的render函数这两只是名字相似,我们在react里面具体实现的render function是 createElement)进行加工变成vdom
  //然后vdom经过加工变成fiber链表,然后执行fiber链表
  //记住这里的fiber链表是边构成边执行的
  //详细的说就是走完了一个完整组件的fiber链表就去渲染一遍
  //然后再去搞下一个组件
  //整个构建和渲染的流程都是在一个主循环里面完成的(就是类似于游戏一样,一直在一个主循环里面执行)
  //大致结构讲解完毕, 至于具体怎么构建, 构建时候怎么更新,渲染, 以及如何不阻塞页面,就看具体函数就行

  //为什么要实现这个函数?
  //上面我们说过jsx变成vdom
  //其实中间的流程具体是这样
  //先由bable/tsc将jsx编译成react.createElement(类型, 参数, 子元素)
  //然后再由这个createElement实现将其转换为vdom, 这就是加工的整体细节

  //下面看这个函数, createElement返回对象, 其中children是用对象数组, 如果子元素是数字或字符, 直接返回一个node节点, 否则返回这个元素执行后的值
  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;
        }),
      },
    };
  }

  //这是创建字符/数字节点
  function createTextNode(nodeValue) {
    return {
      type: "TEXT_ELEMENT",
      props: {
        nodeValue,
        children: [],
      },
    };
  }

  let nextUnitOfWork = null; //这个是指向下一个要处理的fiber节点
  let wipRoot = null; //这个是指向当前fiber链表的根节点
  let currentRoot = null; //指向上一个fiber链表的根节点
  let deletions = null; //更新后需要删除的元素的数组

  //这个render函数是用来初始化的
  //也就是用户调用的那个render
  function render(element, container) {
    wipRoot = {
      dom: container,
      props: {
        children: [element],
      },
      alternate: currentRoot,
    };

    deletions = [];

    nextUnitOfWork = wipRoot;
  }

  //重头戏来了, 这个minireact框架能跑起来,进行一系列更新渲染, 和并发模式都靠这个
  //但是怎么实现这个主循环呢
  //react中是自己实现的调度器,
  //我这边是用浏览器的api来实现时间分片的requestIdleCallback
  //其实这个api就是利用一帧渲染之后的剩余时间来执行下面的脚本
  function workLoop(deadline) {
    let shouldYield = false;
    while (nextUnitOfWork && !shouldYield) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
      shouldYield = deadline.timeRemaining() < 1; //这里deadline是requestIdleCallback传过来的数
    }

    if (!nextUnitOfWork && wipRoot) {
      commitRoot();
    }

    requestIdleCallback(workLoop);
  }

  //这个 requestIdleCallback 它有的浏览器不支持怎么办?自己实现一个
  //浏览器一帧执行正常是16.6ms 如果执行时间大于这个值 可以任务浏览器处于繁忙状态。否则即代表空闲。
  //因为requestAnimationFrame这个函数是和渲染保持同步的 可以通过函数获取帧的开始时间,然后使用帧率(开始时间+16.6ms)计算出帧的结束时间, 然后开启一个宏任务,当宏任务被执行时 比较当前的执行时间和帧结束的时间 判断出当前帧是否还有空闲
  //因为是宏任务不会像微任务优先级那么高,可以被推迟到下一个事件循环中不会阻塞渲染。这里使用MessageChannel宏任务来实现。
  //其实核心就是 获取一帧渲染剩余时间+让执行的任务不阻塞下一次渲染
  window.requestIdleCallback =
    window.requestIdleCallback ||
    function (callback, params) {
      const channel = new MessageChannel(); // 建立宏任务的消息通道
      const port1 = channel.port1;
      const port2 = channel.port2;
      const timeout = params === undefined ? params.timeout : -1;
      let cb = callback;
      let frameDeadlineTime = 0; // 当前帧结束的时间
      const begin = performance.now();
      let cancelFlag = 0;
      const frameTime = 16.6;
      const runner = (timeStamp) => {
        // 获取当前帧结束的时间
        frameDeadlineTime = timeStamp + frameTime;
        if (cb) {
          port1.postMessage("task");
        }
      };
      port2.onmessage = () => {
        const timeRemaining = () => {
          const remain = frameDeadlineTime - performance.now();
          return remain > 0 ? remain : 0;
        };
        let didTimeout = false;
        if (timeout > 0) {
          didTimeout = performance.now() - begin > timeout;
        }
        // 没有可执行的回调 直接结束
        if (!cb) {
          return;
        }
        // 当前帧没有时间&没有超时 下次再执行
        if (timeRemaining() <= 1 && !didTimeout) {
          cancelFlag = requestAnimationFrame(runner);
          return cancelFlag;
        }
        //有剩余时间或者超时
        cb({
          didTimeout,
          timeRemaining,
        });
        cb = null;
      };
      cancelFlag = requestAnimationFrame(runner);
      return cancelFlag;
    };

  requestIdleCallback(workLoop);

  //这个函数上面是构建fiber链表(一开始是初始化, 后面是更新), 下面是返回fiber链表的fiber节点
  function performUnitOfWork(fiber) {
    const isFunctionComponent = fiber.type instanceof Function;
    if (isFunctionComponent) {
      updateFunctionComponent(fiber);
    } else {
      updateHostComponent(fiber);
    }
    if (fiber.child) {
      return fiber.child;
    }
    let nextFiber = fiber;
    while (nextFiber) {
      if (nextFiber.sibling) {
        return nextFiber.sibling;
      }
      nextFiber = nextFiber.return;
    }
  }

  let wipFiber = null; //当前fiber节点
  let stateHookIndex = null; //为了存取前一个fiber节点的useState的hook函数并将其执行完而设立的坐标, 
  //为什么effect hook没有这种坐标?
  //因为useEffect, 是通过队列搞定的

  //初始化/更新 函数组件的fiber节点
  function updateFunctionComponent(fiber) {
    wipFiber = fiber;
    stateHookIndex = 0;
    wipFiber.stateHooks = []; //挂载
    wipFiber.effectHooks = [];

    const children = [fiber.type(fiber.props)];
    reconcileChildren(fiber, children);
  }

  //这里是初始化/更新原生组件的fiber节点
  //为啥要把原生和函数分开?
  //原生有fom需要创建, 函数组件无dom, 并且它们的处理子节点的方式也不一样
  function updateHostComponent(fiber) {
    if (!fiber.dom) {
      fiber.dom = createDom(fiber);
    }
    reconcileChildren(fiber, fiber.props.children);
  }

  //创建dom节点
  function createDom(fiber) {
    const dom =
      fiber.type == "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(fiber.type);

    updateDom(dom, {}, fiber.props);//添加dom节点内容

    return dom;
  }

  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);

  //可做初始化使用 , 或者根据 前面遍历子节点的时候打好的标签进行更新操作(这一步在commitRoot里面才执行)
  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]);
      });
  }

  //构建子组件的fiber节点
  //在构建之前, 我们给新旧节点上标记
  // 遍历比较新旧两组fiber节点的子元素 , 打上删除/新增/更新 三种标记effectTag, 其中删除标记要存在上面创建的deletions数组中
  function reconcileChildren(wipFiber, elements) {
    let index = 0;
    let oldFiber = wipFiber.alternate?.child;
    let prevSibling = null;

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

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

      if (sameType) {
        newFiber = {
          type: oldFiber.type,
          props: element.props,
          dom: oldFiber.dom,
          return: wipFiber,
          alternate: oldFiber,
          effectTag: "UPDATE",
        };
      }
      if (element && !sameType) {
        newFiber = {
          type: element.type,
          props: element.props,
          dom: null,
          return: wipFiber,
          alternate: null,
          effectTag: "PLACEMENT",
        };
      }
      if (oldFiber && !sameType) {
        oldFiber.effectTag = "DELETION";
        deletions.push(oldFiber);
      }

      if (oldFiber) {
        oldFiber = oldFiber.sibling;
      }

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

      prevSibling = newFiber;
      index++;
    }
  }

  function useState(initialState) {
    const currentFiber = wipFiber;

    const oldHook = wipFiber.alternate?.stateHooks[stateHookIndex];

    const stateHook = {
      state: oldHook ? oldHook.state : initialState,
      queue: oldHook ? oldHook.queue : [],
    };

    stateHook.queue.forEach((action) => {
      stateHook.state = action(stateHook.state);
    });

    stateHook.queue = [];

    stateHookIndex++;
    wipFiber.stateHooks.push(stateHook);

    function setState(action) {
      const isFunction = typeof action === "function";

      stateHook.queue.push(isFunction ? action : () => action);

      wipRoot = {
        ...currentFiber,
        alternate: currentFiber,
      };
      nextUnitOfWork = wipRoot;
    }

    return [stateHook.state, setState];
  }

  function useEffect(callback, deps) {
    const effectHook = {
      callback,
      deps,
      cleanup: undefined,
    };
    wipFiber.effectHooks.push(effectHook);
  }

  //先把删除的节点搞掉
  //然后再去执行子节点更新和新增

  function commitRoot() {
    deletions.forEach(commitWork);
    commitWork(wipRoot.child);
    commitEffectHooks();
    currentRoot = wipRoot;
    wipRoot = null;
    deletions = [];
  }

  //递归执行增删改查的工作, 将此时的fiber节点看成一颗二叉树, 左子树是fiber.child(孩子节点), 右子树是fiber.sibling(兄弟节点)
  function commitWork(fiber) {
    if (!fiber) {
      return;
    }

    let domParentFiber = fiber.return;
    while (!domParentFiber.dom) {
      domParentFiber = domParentFiber.return;
    }
    const domParent = domParentFiber.dom;

    if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
      domParent.appendChild(fiber.dom);
    } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
      updateDom(fiber.dom, fiber.alternate.props, fiber.props);
    } else if (fiber.effectTag === "DELETION") {
      commitDeletion(fiber, domParent);
    }

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

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

  function isDepsEqual(deps, newDeps) {
    if (deps.length !== newDeps.length) {
      return false;
    }

    for (let i = 0; i < deps.length; i++) {
      if (deps[i] !== newDeps[i]) {
        return false;
      }
    }
    return true;
  }

  //先清除之前状态的effect函数(就是调用之前状态的return),再去执行当前状态的effect
  function commitEffectHooks() {
    function runCleanup(fiber) {
      if (!fiber) return;

      fiber.alternate?.effectHooks?.forEach((hook, index) => {
        const deps = fiber.effectHooks[index].deps;

        if (!hook.deps || !isDepsEqual(hook.deps, deps)) {
          hook.cleanup?.();
        }
      });

      runCleanup(fiber.child);
      runCleanup(fiber.sibling);
    }

    function run(fiber) {
      if (!fiber) return;

      fiber.effectHooks?.forEach((newHook, index) => {
        if (!fiber.alternate) {
          newHook.cleanup = newHook.callback();
          return;
        }

        if (!newHook.deps) {
          newHook.cleanup = newHook.callback();
        }

        if (newHook.deps.length > 0) {
          const oldHook = fiber.alternate?.effectHooks[index];

          if (!isDepsEqual(oldHook.deps, newHook.deps)) {
            newHook.cleanup = newHook.callback();
          }
        }
      });

      run(fiber.child);
      run(fiber.sibling);
    }

    runCleanup(wipRoot);
    run(wipRoot);
  }

  const MiniReact = {
    createElement,
    render,
    useState,
    useEffect,
  };
  
//用立即执行函数包裹起来, 防止全局变量污染
  window.MiniReact = MiniReact;
})();