React基础原理实现

React16的createElement以及render方法实现

参考文档链接

import React from "react";
import ReactDOM from "react-dom";

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        //对象不做处理,字符串或者数字进行处理
        return typeof child === "object" ? child : createTextElement(child);
      }),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [], //实际没有,只是为了方便
    },
  };
}

//定义自己的render方法
function render(element, container) {
  // element是最外层的标签,插入html中的root容器中
  let dom =
    element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);

  //将节点属性绑定到元素身上
  const isProperty = (key) => key !== "children";
  Object.keys(element.props)
    .filter(isProperty)
    .forEach((key) => {
      element[key] = element.props[key];
    });

  // 递归遍历节点信息
  element.props.children.forEach((child) => {
    render(child, dom);
  });

  container.appendChild(dom);
}

const Didact = {
  createElement,
  render,
};

/** @jsx Didact.createElement */
//预处理指令,告诉编辑器使用我指定的函数创建JSX元素
var element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
);

element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b", null)
);

const container = document.getElementById("root");

Didact.render(element, container);

// ReactDOM.render(element, container);

并发模式

但是创建虚拟DOM本质是一个JavaScript代码的执行过程。如果说创建的DOM树很大,递归很久,那么这段JavaScript程序就会长时间的阻塞主线程。如果浏览器需要执行高优先级程序,就必须等待。因此采用的是并发模式执行

// 拆分工作单元
let nextUnitOfWork = null;

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    /* 
    deadline.timeRemaining()是requestIdleCallback提供给回调函数的一个方法。
    它返回一个表示当前剩余时间的数字,以毫秒为单位。这个时间表示在当前主线程任务重新被调度之前,
    回调函数还有多少可用时间执行任务。因此,deadline.timeRemaining() < 1这个条件检查是否剩余的时间不足以继续执行工作,
    如果是,则设置shouldYield为true,以便退出当前循环。这样可以确保在浏览器主线程需要进行其他工作之前,能够及时释放控制权。
    */
    shouldYield = deadline.timeRemaining() < 1;
  }
  requestIdleCallback(workLoop);
}

//浏览器主线程空闲时候自动调用(react内部自己实现了一个)
// requestIdleCallback 还给了我们一个 deadline 参数。我们可以用它来检查我们有多少时间,直到浏览器需要再次控制
requestIdleCallback(workLoop);

//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(nextUnitOfWork) {}

fibers

每一个fiber就是一个工作单元。

Didact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

将root节点作为创建fiber的初始入口。并设置为初始工作单元nextUnitOfWorkperformUnitOfWork 函数主要做:将元素添加到DOM,为元素的子元素创建fiber,选择下一个工作单元
在这里插入图片描述
在这里插入图片描述

  • child可以理解为查找的第一个子元素,优先查找该选项
  • parent当元素的父元素
  • sibling当前元素的兄弟节点

执行的流程大致如下:

  1. 从根节点出发,依次寻找第一个子元素作为child
  2. 如果当前元素没有child项,就查找当前元素的兄弟节点,并重复1。例如p节点和a节点
  3. 如果当前元素既没有child也没有sibling项,就查找当前元素的parent父元素,如a节点的父节点h1,并重复2
  4. 如果最终处理完成,回到根节点,则代表render函数执行完成

修改代码如下,当render函数初始化工作单元后,浏览器会在空闲的时候自动去执行workloop函数,在函数内部去执行performUnitOfwork函数

function createDom(fiber) {
  // element是最外层的标签,插入html中的root容器中
  let dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);

  //将节点属性绑定到元素身上
  const isProperty = (key) => key !== "children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((key) => {
      dom[key] = fiber.props[key];
    });

  return dom;
}

//定义自己的render方法
function render(element, container) {
  //创建工作单元
  nextUnitOfWork = {
    dom: container, //root作为根执行单元
    props: {
      children: [element],
    },
  };
}
//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
  //1:创建并添加DOM
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  if (fiber.parent) {
    //节点插入到父节点(root节点不需要)
    fiber.parent.dom.appendChild(fiber.dom);
  }
  //2:为当前元素的子元素创建fiber
  const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
  let index = 0;
  let prevSibling = null;

  while (index < elements.length) {
    const element = elements[index]; //处理每一个子元素
    const newFiber = {
      dom: null,
      props: element.props, //开始可能是虚拟DOM的信息,然后会创建符合工作单元的对象
      type: element.type,
      parent: fiber,
    };
    //将新的fiber添加的fiber树中
    if (index === 0) {
      //类似记录child,即第一个子元素
      fiber.child = newFiber;
    } else {
      /* 
        div(h1,h2)从h1开始,初始走到这里不执行,只记录 prevSibling = newFiber,即上一个兄弟节点为h1
        然后第二次循环,进入该分支,prevSibling是h1,newFiber是h2,这样子就个h1绑定了兄弟节点h2,
        然后再次执行 prevSibling = newFiber往下继续绑定关系
      */
      prevSibling.sibling = newFiber; //记录当前节点的兄弟节点,idnex不为0的时候,记录第一个元素的兄弟节点
    }
    //这一步操作可以理解为链表
    prevSibling = newFiber; //准备记录节点的兄弟节点,一开始index=0,记录第一个元素
    index++;
  }
  //3:查找下一个工作单元
  if (fiber.child) {
    //深度遍历原则,优先找第一个子节点
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    //可能会一直处理到root节点
    if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
    //如果没有子节点,也没有兄弟节点就返回父节点
    nextFiber = nextFiber.parent;
  }
}

渲染

如下这段代码,会向DOM中添加一个新节点。浏览器可能会在我们完成整个树的渲染之前中断我们的工作。在这种情况下,用户将看到不完整的 UI。我们不希望那样。所以如下代码需要注释掉,不能采用这种方式。

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

可以理解如下,创建DOM的操作是在performUnitOfWork函数中完成,但是该函数的执行是根据渲染主线程是否空闲决定的。因此如果浏览器主线程繁忙,可能不会去创建DOM结构。

 while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

修改部分代码如下

//在在工作的根work in progress
let wipRoot = null; //跟踪根元素

// 完成所有的工作后,提交根节点
function commitRoot() {
  // 递归所有的节点添加到root上
}

function workLoop(deadline) {
  ....
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  requestIdleCallback(workLoop);
}

function render(element, container) {
  //创建工作单元
  wipRoot = {
    dom: container, //root作为根执行单元
    props: {
      children: [element],
    },
  };
  nextUnitOfWork = wipRoot;
}
// 完成所有的工作后,提交根节点
function commitRoot() {
  // 递归所有的节点添加到root上
  commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
  wipRoot = null;
}

function commitWork(fiber) {
  // 递归执行,将节点依次添加到父元素
  if (!fiber) return;
  const domParent = fiber.parent.dom; //父节点
  domParent.appendChild(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

更新或删除

上面的代码是进行初始化渲染操作,涉及到更新或者删除的时候,将在 render 函数上收到的元素与我们提交给 DOM 的最后一个fiber树进行比较。因此,我们需要在完成提交后保存对“我们提交到 DOM 的最后一个fiber树”的引用。我们称之为 currentRoot 。同时为每一个fiber添加一个alternate 属性,记录上一个fiber。

let currentRoot = null; //提交到 DOM 的最后一个fiber树的引用

  //创建工作单元
  wipRoot = {
    dom: container, //root作为根执行单元
    props: {
      children: [element],
    },
    alternate: currentRoot,
  };

function commitRoot() {
  // 递归所有的节点添加到root上
  commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
  currentRoot = wipRoot;
  wipRoot = null;
}

直接上最终代码了

import React from "react";
import ReactDOM from "react-dom";

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        //对象不做处理,字符串或者数字进行处理
        return typeof child === "object" ? child : createTextElement(child);
      }),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [], //实际没有,只是为了方便
    },
  };
}

// 拆分工作单元
let nextUnitOfWork = null;
//在在工作的根work in progress
let wipRoot = null; //跟踪根元素
let currentRoot = null; //提交到 DOM 的最后一个fiber树的引用
let deletions = null;

// 完成所有的工作后,提交根节点
function commitRoot() {
  deletions.forEach(commitWork);
  // 递归所有的节点添加到root上
  commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
  currentRoot = wipRoot;
  wipRoot = null;
}

//处理on开头的事件监听
const isEvent = (key) => key.startsWith("on");

const isProperty = (key) => key !== "children" && !isEvent(key); //包含子元素的不需要添加到标签上
const isNew = (prev, next) => {
  //判断属性是否发生变化
  return (key) => {
    return prev[key] !== next[key];
  };
};
/* 当更新 DOM 元素时,可能会有一些属性在更新后被移除,因此需要检查上一个属性对象 prevProps 中的属性是否在下一个属性对象 nextProps 中不存在,若不存在,则说明该属性被移除了,需要相应地在 DOM 上进行处理。 */
const isGone = (prev, next) => {
  return (key) => {
    return !(key in next);
  };
};

// 更新才走
function updateDom(dom, prevProps, nextProps) {
  //事件处理程序发生改变就直接删除
  Object.keys(prevProps)
    .filter(isEvent)
    .filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key)) //新DOM有该事件,且新旧DOM的事件不一致
    .forEach((name) => {
      // onClick小写并截取适配l2事件监听,也只有l2可以取消事件监听
      const eventType = name.toLowerCase().substring(2);
      dom.removeEvenetListener(eventType, prevProps[name]);
    });

  // 移除旧的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps)) //筛选出旧DOM的属性。不存在新DOM上
    .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]);
    });
}

function commitWork(fiber) {
  // 递归执行,将节点依次添加到父元素
  if (!fiber) return;
  const domParent = fiber.parent.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") {
    //当前fiber是需要被删除的,那么就通过查找父元素删除其子节点即可
    domParent.removeChild(fiber.dom);
  }

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

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    /* 
    deadline.timeRemaining()是requestIdleCallback提供给回调函数的一个方法。
    它返回一个表示当前剩余时间的数字,以毫秒为单位。这个时间表示在当前主线程任务重新被调度之前,
    回调函数还有多少可用时间执行任务。因此,deadline.timeRemaining() < 1这个条件检查是否剩余的时间不足以继续执行工作,
    如果是,则设置shouldYield为true,以便退出当前循环。这样可以确保在浏览器主线程需要进行其他工作之前,能够及时释放控制权。
    */
    shouldYield = deadline.timeRemaining() < 1;
  }
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  requestIdleCallback(workLoop);
}

//浏览器主线程空闲时候自动调用(react内部自己实现了一个)
// requestIdleCallback 还给了我们一个 deadline 参数。我们可以用它来检查我们有多少时间,直到浏览器需要再次控制
requestIdleCallback(workLoop);

//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
  //1:创建并添加DOM
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  // if (fiber.parent) {
  //   //节点插入到父节点(root节点不需要)
  //   fiber.parent.dom.appendChild(fiber.dom);
  // }

  //2:为当前元素的子元素创建fiber
  const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
  reconcileChildren(fiber, elements);
  //3:查找下一个工作单元
  if (fiber.child) {
    //深度遍历原则,优先找第一个子节点
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    //可能会一直处理到root节点
    if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
    //如果没有子节点,也没有兄弟节点就返回父节点
    nextFiber = nextFiber.parent;
  }
  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]; //处理每一个子元素
      // const newFiber = {
      //   dom: null,
      //   props: element.props, //开始可能是虚拟DOM的信息,然后会创建符合工作单元的对象
      //   type: element.type,
      //   parent: wipFiber,
      // };
      let newFiber = null;
      //oldFiber是旧的,element是新的,需要比较判断是否需要更改
      const sameType = oldFiber && element && element.type === oldFiber.type;

      if (sameType) {
        /* 如果旧的和新元素的类型相同,我们可以保留 DOM 节点,只需使用新 prop 更新它 */
        newFiber = {
          //复用旧节点的信息
          type: oldFiber.type,
          props: element.props,
          dom: oldFiber.dom,
          parent: wipFiber,
          alternate: oldFiber,
          effectTag: "UPDATE", //添加更新标识
        };
      }

      if (element && !sameType) {
        /* 如果类型不同并且有新元素,则意味着我们需要创建一个新的 DOM 节点 */
        newFiber = {
          type: element.type,
          props: element.props,
          dom: null, //创建新节点代表不复用旧的
          parent: wipFiber,
          alternate: null,
          effectTag: "PLACEMENT",
        };
      }

      if (oldFiber && !sameType) {
        //如果类型不同并且有旧fiber,则需要删除旧节点
        //对于需要删除的节点,暂时没有新的光fiber,所以需要将标签添加到旧fiber中
        //fiber树提交dom的时候,从正在进行的根目录开始,没有旧的fiber,因此创建数组跟踪要删除的节点
        oldFiber.effectTag = "DELETION";
        deletions.push(oldFiber);
      }

      if (oldFiber) {
        //自己的比较完了,比较兄弟节点
        oldFiber = oldFiber.sibling;
      }

      //将新的fiber添加的fiber树中
      if (index === 0) {
        //类似记录child,即第一个子元素
        wipFiber.child = newFiber;
      } else {
        /* 
          div(h1,h2)从h1开始,初始走到这里不执行,只记录 prevSibling = newFiber,即上一个兄弟节点为h1
          然后第二次循环,进入该分支,prevSibling是h1,newFiber是h2,这样子就个h1绑定了兄弟节点h2,
          然后再次执行 prevSibling = newFiber往下继续绑定关系
        */
        prevSibling.sibling = newFiber; //记录当前节点的兄弟节点,idnex不为0的时候,记录第一个元素的兄弟节点
      }
      //这一步操作可以理解为链表
      prevSibling = newFiber; //准备记录节点的兄弟节点,一开始index=0,记录第一个元素
      index++;
    }
  }
}

function createDom(fiber) {
  // element是最外层的标签,插入html中的root容器中
  let dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);

  //将节点属性绑定到元素身上
  const isProperty = (key) => key !== "children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((key) => {
      dom[key] = fiber.props[key];
    });

  return dom;
}

//定义自己的render方法
function render(element, container) {
  //创建工作单元
  wipRoot = {
    dom: container, //root作为根执行单元
    props: {
      children: [element],
    },
    alternate: currentRoot,
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

const Didact = {
  createElement,
  render,
};

/** @jsx Didact.createElement */
//预处理指令,告诉编辑器使用我指定的函数创建JSX元素
var element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
);

element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b", null)
);

const container = document.getElementById("root");

Didact.render(element, container);

// ReactDOM.render(element, container);

添加函数组件的处理过程

//函数组件
function App(props) {
  return <h1>Hi {props.name}</h1>;
}
element = <App name="foo" />;

转换为jsx如下

function App(props) {
  return Didact.createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
const element = Didact.createElement(App, {
  name: "foo",
})

函数组件的话,不需要创建DOM结构,数据是来自函数的返回值添加到函数组件的父DOM结构中。
那么在更新的时候就需要判断是函数组件还是普通标签

修改创建fiber的部分

//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
  //1:创建并添加DOM
  const isFunctionComponent = fiber.type instanceof Function; //函数组件
  if (isFunctionComponent) {
    updateFunctionComponent();
  } else {
    updateHostComponent(fiber);
  }

  //3:查找下一个工作单元
  if (fiber.child) {
    //深度遍历原则,优先找第一个子节点
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    //可能会一直处理到root节点
    if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
    //如果没有子节点,也没有兄弟节点就返回父节点
    nextFiber = nextFiber.parent;
  }
  function reconcileChildren(wipFiber, elements) {
    ....
  }
  //处理函数组件更新
  function updateFunctionComponent(fiber) {
    const children = [fiber.type(fiber.props)]; //执行函数组件并将参数传递
    reconcileChildren(fiber, children);
  }
  //处理标签更新
  function updateHostComponent(fiber) {
    if (!fiber.dom) {
      fiber.dom = createDom(fiber);
    }
    // if (fiber.parent) {
    //   //节点插入到父节点(root节点不需要)
    //   fiber.parent.dom.appendChild(fiber.dom);
    // }

    //2:为当前元素的子元素创建fiber
    const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
    reconcileChildren(fiber, elements);
  }
}

修改创建dom的部分

function commitWork(fiber) {
  // 递归执行,将节点依次添加到父元素
  if (!fiber) return;

  // const domParent = fiber.parent.dom; //父节点
  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    //比如app函数组件内部的h1元素,从h1元素开始需要找到app组件使用的地方的父级
    // 沿着fiber树向上查找
    domParentFiber = domParentFiber.parent;
  }
  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) {
  //当前fiber是需要被删除的,那么就通过查找父元素删除其子节点即可
  /* 
    如果fiber是一个普通标签div,那么直接通过父元素删除就行
    如果fiber是一个app函数组件,那么删除当前函数组件返回的最外层标签 类似这种function App(){return <div>...</div>} }
  */
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

钩子

为函数组件添加状态

function useState(initial) {}

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

//测试
function Counter() {
  const [state, setState] = Didact.useState(1);
  return (
    <div
      onClick={() => {
        setState(state + 1);
      }}
    >
      Count: {state}
    </div>
  );
}

element = <Counter />;
let wipFiber = null; //正在处理的fiber
let hookIndex = null;

function updateFunctionComponent(fiber) {
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = []; //给当前fiber添加一个数组,用来支持用一个组件中多次调用useState

  const children = [fiber.type(fiber.props)]; //执行函数组件并将参数传递
  reconcileChildren(fiber, children);
}

```javascript
function useState(initial) {
  /* 
  当函数组件调用 useState 时,我们检查是否有旧的钩子。我们使用钩子索引检查 alternate fiber。
  如果存在旧钩子,则状态从旧钩子复用到新钩子,如果没有,则进行初始化状态
*/
  //返回状态值和修改状态的函数
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [], //更新队列,批处理
  };

  // 下次渲染时候,从旧的钩子队列中获取所有操作并应用到新的钩子状态中,然后返回新的钩子状态
  const actions = oldHook ? oldHook.queue : [];

  actions.forEach((action) => {
    hook.state = action(hook.state);
  });

  const setState = (action) => {
    hook.queue.push(action);
    //设置工作根设置为下一个工作单元,每次设置setState就需要进行页面重新渲染
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

完整代码

import React from "react";
import ReactDOM from "react-dom";

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
        //对象不做处理,字符串或者数字进行处理
        return typeof child === "object" ? child : createTextElement(child);
      }),
    },
  };
}

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [], //实际没有,只是为了方便
    },
  };
}

// 拆分工作单元
let nextUnitOfWork = null;
//在在工作的根work in progress
let wipRoot = null; //跟踪根元素
let currentRoot = null; //提交到 DOM 的最后一个fiber树的引用
let deletions = null;

let wipFiber = null; //正在处理的fiber
let hookIndex = null;

// 完成所有的工作后,提交根节点
function commitRoot() {
  deletions.forEach(commitWork);
  // 递归所有的节点添加到root上
  commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
  currentRoot = wipRoot;
  wipRoot = null;
}

//处理on开头的事件监听
const isEvent = (key) => key.startsWith("on");

const isProperty = (key) => key !== "children" && !isEvent(key); //包含子元素的不需要添加到标签上
const isNew = (prev, next) => {
  //判断属性是否发生变化
  return (key) => {
    return prev[key] !== next[key];
  };
};
/* 当更新 DOM 元素时,可能会有一些属性在更新后被移除,因此需要检查上一个属性对象 prevProps 中的属性是否在下一个属性对象 nextProps 中不存在,若不存在,则说明该属性被移除了,需要相应地在 DOM 上进行处理。 */
const isGone = (prev, next) => {
  return (key) => {
    return !(key in next);
  };
};

// 更新才走
function updateDom(dom, prevProps, nextProps) {
  //事件处理程序发生改变就直接删除
  Object.keys(prevProps)
    .filter(isEvent)
    .filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key)) //新DOM有该事件,且新旧DOM的事件不一致
    .forEach((name) => {
      // onClick小写并截取适配l2事件监听,也只有l2可以取消事件监听
      const eventType = name.toLowerCase().substring(2);
      dom.removeEvenetListener(eventType, prevProps[name]);
    });

  // 移除旧的属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps)) //筛选出旧DOM的属性。不存在新DOM上
    .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]);
    });
}

function commitWork(fiber) {
  // 递归执行,将节点依次添加到父元素
  if (!fiber) return;

  // const domParent = fiber.parent.dom; //父节点
  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    //比如app函数组件内部的h1元素,从h1元素开始需要找到app组件使用的地方的父级
    // 沿着fiber树向上查找
    domParentFiber = domParentFiber.parent;
  }
  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) {
  //当前fiber是需要被删除的,那么就通过查找父元素删除其子节点即可
  /* 
    如果fiber是一个普通标签div,那么直接通过父元素删除就行
    如果fiber是一个app函数组件,那么删除当前函数组件返回的最外层标签 类似这种function App(){return <div>...</div>} }
  */
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    /* 
    deadline.timeRemaining()是requestIdleCallback提供给回调函数的一个方法。
    它返回一个表示当前剩余时间的数字,以毫秒为单位。这个时间表示在当前主线程任务重新被调度之前,
    回调函数还有多少可用时间执行任务。因此,deadline.timeRemaining() < 1这个条件检查是否剩余的时间不足以继续执行工作,
    如果是,则设置shouldYield为true,以便退出当前循环。这样可以确保在浏览器主线程需要进行其他工作之前,能够及时释放控制权。
    */
    shouldYield = deadline.timeRemaining() < 1;
  }
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
  requestIdleCallback(workLoop);
}

//浏览器主线程空闲时候自动调用(react内部自己实现了一个)
// requestIdleCallback 还给了我们一个 deadline 参数。我们可以用它来检查我们有多少时间,直到浏览器需要再次控制
requestIdleCallback(workLoop);

//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
  //1:创建并添加DOM
  const isFunctionComponent = fiber.type instanceof Function; //函数组件
  if (isFunctionComponent) {
    updateFunctionComponent();
  } else {
    updateHostComponent(fiber);
  }

  //3:查找下一个工作单元
  if (fiber.child) {
    //深度遍历原则,优先找第一个子节点
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    //可能会一直处理到root节点
    if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
    //如果没有子节点,也没有兄弟节点就返回父节点
    nextFiber = nextFiber.parent;
  }
}

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]; //处理每一个子元素
    // const newFiber = {
    //   dom: null,
    //   props: element.props, //开始可能是虚拟DOM的信息,然后会创建符合工作单元的对象
    //   type: element.type,
    //   parent: wipFiber,
    // };
    let newFiber = null;
    //oldFiber是旧的,element是新的,需要比较判断是否需要更改
    const sameType = oldFiber && element && element.type === oldFiber.type;

    if (sameType) {
      /* 如果旧的和新元素的类型相同,我们可以保留 DOM 节点,只需使用新 prop 更新它 */
      newFiber = {
        //复用旧节点的信息
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE", //添加更新标识
      };
    }

    if (element && !sameType) {
      /* 如果类型不同并且有新元素,则意味着我们需要创建一个新的 DOM 节点 */
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null, //创建新节点代表不复用旧的
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      };
    }

    if (oldFiber && !sameType) {
      //如果类型不同并且有旧fiber,则需要删除旧节点
      //对于需要删除的节点,暂时没有新的光fiber,所以需要将标签添加到旧fiber中
      //fiber树提交dom的时候,从正在进行的根目录开始,没有旧的fiber,因此创建数组跟踪要删除的节点
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber);
    }

    if (oldFiber) {
      //自己的比较完了,比较兄弟节点
      oldFiber = oldFiber.sibling;
    }

    //将新的fiber添加的fiber树中
    if (index === 0) {
      //类似记录child,即第一个子元素
      wipFiber.child = newFiber;
    } else {
      /* 
        div(h1,h2)从h1开始,初始走到这里不执行,只记录 prevSibling = newFiber,即上一个兄弟节点为h1
        然后第二次循环,进入该分支,prevSibling是h1,newFiber是h2,这样子就个h1绑定了兄弟节点h2,
        然后再次执行 prevSibling = newFiber往下继续绑定关系
      */
      prevSibling.sibling = newFiber; //记录当前节点的兄弟节点,idnex不为0的时候,记录第一个元素的兄弟节点
    }
    //这一步操作可以理解为链表
    prevSibling = newFiber; //准备记录节点的兄弟节点,一开始index=0,记录第一个元素
    index++;
  }
}
//处理函数组件更新
function updateFunctionComponent(fiber) {
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = []; //给当前fiber添加一个数组,用来支持用一个组件中多次调用useState

  const children = [fiber.type(fiber.props)]; //执行函数组件并将参数传递
  reconcileChildren(fiber, children);
}
//处理标签更新
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  // if (fiber.parent) {
  //   //节点插入到父节点(root节点不需要)
  //   fiber.parent.dom.appendChild(fiber.dom);
  // }

  //2:为当前元素的子元素创建fiber
  const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
  reconcileChildren(fiber, elements);
}

function useState(initial) {
  /* 
  当函数组件调用 useState 时,我们检查是否有旧的钩子。我们使用钩子索引检查 alternate fiber。
  如果存在旧钩子,则状态从旧钩子复用到新钩子,如果没有,则进行初始化状态
*/
  //返回状态值和修改状态的函数
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [], //更新队列,批处理
  };

  // 下次渲染时候,从旧的钩子队列中获取所有操作并应用到新的钩子状态中,然后返回新的钩子状态
  const actions = oldHook ? oldHook.queue : [];

  actions.forEach((action) => {
    hook.state = action(hook.state);
  });

  const setState = (action) => {
    hook.queue.push(action);
    //设置工作根设置为下一个工作单元,每次设置setState就需要进行页面重新渲染
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

function createDom(fiber) {
  // element是最外层的标签,插入html中的root容器中
  let dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);

  //将节点属性绑定到元素身上
  const isProperty = (key) => key !== "children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((key) => {
      dom[key] = fiber.props[key];
    });

  return dom;
}

//定义自己的render方法
function render(element, container) {
  //创建工作单元
  wipRoot = {
    dom: container, //root作为根执行单元
    props: {
      children: [element],
    },
    alternate: currentRoot,
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

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

/** @jsx Didact.createElement */
//预处理指令,告诉编辑器使用我指定的函数创建JSX元素
//普通标签
var element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
);

element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b", null)
);

//函数组件
function App(props) {
  return <h1>Hi {props.name}</h1>;
}

element = <App name="foo" />;

//测试
function Counter() {
  const [state, setState] = Didact.useState(1);
  return (
    <div
      onClick={() => {
        setState(state + 1);
      }}
    >
      Count: {state}
    </div>
  );
}

element = <Counter />;

const container = document.getElementById("root");

Didact.render(element, container);

// ReactDOM.render(element, container);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值