likes
comments
collection
share

实现mini-react(一)

作者站长头像
站长
· 阅读数 67

项目地址 jsx在babel编译后的结果是React.createElement,由createElement生成虚拟DOM 实现mini-react(一) React.createElement有三个参数

  1. type
  2. props
  3. children

实现createElement函数

递归判断children是普通元素还是文本元素,如果是文本元素就用createTextNode

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child => {
        return typeof child === "string" ? createTextNode(child) : child
      }),
    },
  }
}

function createTextNode(text, ...children) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children,
    },
  }
}

实现render函数

根据传入的虚拟DOM的type属性来创建对应的DOM,再把props挨个设置到DOM,再递归遍历children调用render,最终把DOM添加到container

function render(el, container) {
  const dom = el.type === "TEXT_ELEMENT" ? document.createTextNode("") : document.createElement(el.type)

  Object.keys(el.props).forEach(key => {
    if (key !== "children") {
      dom[key] = el.props[key]
    }
  })

  const children = el.props.children
  children.forEach(child => {
    render(child, dom)
  })
  container.append(dom)
}

实现ReactDOM.createRoot函数

实现mini-react(一) 实际是对React.render做一层封装

import React from './React';

const ReactDOM = {
  createRoot(container) {
    return {
      render(el) {
        React.render(el, container);
      },
    };
  },
};

export default ReactDOM;

实现效果

使用vite创建一个Vanilla项目 实现mini-react(一) 创建App.jsx文件,使用React.createElement

实现mini-react(一) 在main.js文件中使用ReactDOM.createRoot传入虚拟DOM

实现mini-react(一) 渲染成功 实现mini-react(一)

实现fiber

在上面的render中使用了递归,这样会导致无法中断,页面会卡顿。可以使用requestIdleCallback这个函数,在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应

function workLoop(deadline) {
  let shouldRun = false
  while (!shouldRun) {
    // 当剩余时间小于1的时候,就执行下个任务
    shouldRun = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

虚拟DOM怎么转成fiber结构? 每个fiber做了三件事:

  1. 根据type创建dom并添加
  2. 遍历子元素创建fiber
  3. 选择下一个工作单元

从根节点开始,先修改render,将创建dom代码抽离

let nextWork = null
function render(el, container) {
  nextWork = {
    dom: container,
    props: {
      children: [el],
    },
  }
}

function createDom(fiber) {
  const dom =
    fiber.type == 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(fiber.type);
  const isProperty = (key) => key !== 'children';
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = fiber.props[name];
    });
  return dom;
}

Fiber每一个节点都是一个fiber,一个 fiber 包括了 child(第一个子节点)、sibling(兄弟节点)、parent(父节点)属性。创建完fiber节点再依次返回子节点>兄弟节点>父节点进行下个工作单元

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

  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  const children = fiber.props.children;

  // 上一个兄弟节点
  let prevChild = null;

  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      child: null,
      parent: fiber,
      sibling: null,
      dom: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevChild.sibling = newFiber;
    }
    prevChild = newFiber;
  });

  // 寻找下一个子节点,如果有返回
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    // 如果有兄弟节点,返回兄弟节点
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    // 否则返回父节点
    nextFiber = nextFiber.parent;
  }
}

在工作循环中从根节点开始创建fiber

function workLoop(deadline) {
  let shouldYield = false
  while (!shouldYield && nextWork) {
+    nextWork = performWorkOfUnit(nextWork)
    shouldYield = deadline.timeRemaining() < 1
  }

  requestIdleCallback(workLoop)
}

实现效果

实现mini-react(一)

实现mini-react(一) 打印每个工作单元的nextWork依次为app节点>div1节点>111文本节点>div2节点>222文本节点 实现mini-react(一)

实现统一提交

现在处理一个元素,都要向DOM添加一个新的节点,再加上shouldRun可中断渲染,中途有可能没空余时间,会看到渲染一半的DOM 实现mini-react(一) nextWork会代替掉,新增root保存一开始的根节点。再判断nextWork为空代表节点遍历完了再递归渲染元素

let nextWork = null;
+let root = null;
function render(el, container) {
  nextWork = {
    dom: container,
    props: {
      children: [el],
    },
  };
+  root = nextWork;
}
function performWorkOfUnit(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }

-  if (fiber.parent) {
-    fiber.parent.dom.appendChild(fiber.dom);
-  }
}
function workLoop(deadline) {
  let shouldRun = false;
  while (nextWork && !shouldRun) {
    nextWork = performWorkOfUnit(nextWork);
    // 当剩余时间小于1的时候,就执行下个任务
    shouldRun = deadline.timeRemaining() < 1;
  }
+  if (!nextWork && root) {
+    commitRoot();
+  }
  requestIdleCallback(workLoop);
}
function commitRoot() {
  commitWork(root.child);
  root = null;
}
function commitWork(fiber) {
  if (!fiber) return;
  fiber.parent.dom.append(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

实现函数组件

函数组件fiber的type属性是函数,加个判断是否是函数组件,如果是函数组件就不创建DOM

实现mini-react(一)

function performWorkOfUnit(fiber) {
+  const isFunctionComponent = typeof fiber.type === 'function';
+  if (isFunctionComponent) {
+    // 更新函数组件
+    updateFunctionComponent(fiber)
+  } else {
+    // 更新普通节点
+    updateHostComponent(fiber)
+  }
-  if (!fiber.dom) {
-    fiber.dom = createDom(fiber);
-  }

-  const children = fiber.props.children;
+  reconcileChildren(fiber, children);

}
// 遍历子节点创建fiber
function reconcileChildren(fiber, children) {
  let prevChild = null;
  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      child: null,
      parent: fiber,
      sibling: null,
      dom: null,
    };

    if (index === 0) {
      fiber.child = newFiber;
    } else {
      prevChild.sibling = newFiber;
    }
    prevChild = newFiber;
  });
}

因为函数组件的type是函数,所以要把将fiber中的参数传入并执行得到DOM作为数组传入reconcileChildren,不是函数组件就走之前逻辑,创建DOM传入reconcileChildren

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

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  reconcileChildren(fiber, fiber.props.children);
}

此时报错是因为函数组件的fiber没有创建DOM

实现mini-react(一) fiber.type(fiber.props)得到是函数组件真正的节点存在fiber的child属性,需要把得到的DOM挂载到父节点的DOM

实现mini-react(一) 实现mini-react(一) 在同一渲染时判断fiber对应的父节点没有DOM属性时,一直向上取到有DOM属性的父节点把fiber的DOM挂载到父节点的DOM

function commitWork(fiber) {
  if (!fiber) return;
+  let fiberParent = fiber.parent;
+  while (!fiberParent.dom) {
+    fiberParent = fiberParent.parent;
+  }

+  if (fiber.dom) {
+    fiberParent.dom.append(fiber.dom);
+  }
-  fiber.parent.dom.append(fiber.dom);
  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

实现传递数字

之前在createElement函数中判断child为string才调用createTextNode,应该再加个判断判断child为number也调用createTextNode 实现mini-react(一)

实现mini-react(一)

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) => {
+        const testNode = typeof child === 'string' || typeof child === 'number';
+        return testNode ? createTextNode(child) : child;
-        return typeof child === 'string' ? createTextNode(child) : child;
      }),
    },
  };
}

实现效果

实现mini-react(一)

实现mini-react(一) 未完待续...

转载自:https://juejin.cn/post/7376582861662011433
评论
请登录