理解 build your own react

关注并将「趣谈前端」设为星标

每天定时分享技术干货/优秀开源/技术思维

网址:https://pomb.us/build-your-own-react/

  • Scheduler【调度器】:调度任务的优先级,高优任务优先进入Reconciler

  • Reconciler【协调器】:负责找出变化的组件,标识effectTag

  • Renderer【渲染器】 :负责将变化的组件渲染到页面上

调度器

异步可中断:浏览器是否有剩余时间作为任务中断的标准,需要浏览器提供机制在空闲时间进行回掉。

requestIdleCallback:方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

React当中没有使用requestIdleCallback,而是自己实现的功能,这里使用requestIdleCallback代替调度器的功能。

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

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

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

requestIdleCallback会自己在空余时间回掉执行workLoop方法,加上workLoop内部调用requestIdleCallback,所以workLoop会在浏览器有空余的时间内一直进行循环回掉。

调度器和协调器调用关系

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

如果nextUnitOfWork不为空,并且浏览器存在空余时间的情况下,调用performUnitOfWork方法。这里的nextUnitOfWrok是什么呢?

nextUnitOfWork是fiber节点,每一个fiber节点。React16将以前的同步递归遍历,改成异步可中断遍历。这里的nextUnitOfWork就是每一个需要进行调和处理的节点。处理完当前节点之后,nextUnitOfWork会被赋值为下一个需要调和处理的节点。这里下一个节点什么时候进行调和,受调度器控制。

9b5e35506007ebba740fe586186da08a.png

调度器和渲染器调用关系

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

假设当前fiber节点调和完毕,则会进行渲染阶段,虚拟dom映射到真实dom上。

协调器

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.parent;
  }
}

调和阶段是负责找出变化的组件,标识effectTag。那主要为了对fiber树节点的遍历,对比,找出差异,打上tag。

时间复杂度为O(n)的遍历方式

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

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

核心在于,fiber调和阶段对于每个节点的处理,每个节点会进行父子,兄弟关系的关联。假设树节点如下,当进行深度优先遍历的时候,顺序如连接线的顺序。

5b25ae99d4c8f99b11ed1c766735a46d.png

如果遍历完node4之后,知道下一个节点就是node5,知道node5遍历之后,需要回到node2节点,然后下一个节点是node3。这样的话,每一个节点执行之后,可以按照规律找到下一个节点是什么。那就不用递归循环遍历了。

5ce0e3aa6c479b4eb3d461b81fcbd779.png

所以这里需要对每个节点进行链表节点处理。

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;

    /* fiber 节点寻找差异这部分先不看 */
    
    /*
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,  // 着重看这一行
        alternate: oldFiber,
        effectTag: "UPDATE"
      };
    */

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

    prevSibling = newFiber;
    index++;
  }
}

举例:

  1. 当nextUnitOfWork为node1的时候

d7ff901de3f11f100ad495c18e1edec8.png

  1. 当nextUnitOfWork为node2的时候

7256fb0f11534ac3d0df07611492f8d6.png

  1. 处理完节点

6a9bbe22ddc2efbc9261f4c0dee2b4dc.png

如何进行节点遍历的?

function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function;
  
 /* 这里不关心 */

  if (fiber.child) {
    return fiber.child;
  }

  let nextFiber = fiber;

  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

  • node1 -》node2:因为node2是node1的第一个节点,设置了node1.child = node2。

  • node4 -》node5:node2调和之后,因为node2当前节点有child节点,指向node4。node4没有child节点,所会执行nextFiber.sibling,所以node4.sibling = node5。

  • node5 -》node3:node5没有child节点,也没有sibling节点,所以执行nextFiber = nextFiber.parent,nextFiber指向了node2。node2.sibling = node3。

找出差异

先了解代码里的两个变量:

  • currentRoot:当前已经渲染在页面上的fiber root节点。

  • wipRoot:workInProgress root,本次更新在调和阶段创建的root fiber节点。

双缓存概念

这里两个fiber树,是为了在wipRoot完成之后,直接替换现有的currentRoot,不会造成过多的等待时间。

两个阶段

首屏渲染

首屏渲染的时候currentRoot为null,wipRoot创建成功之后,直接替换currentRoot,所有的阶段的effectTag均为PLACEMENT。

这里需要关心的是下面的代码

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

render方法执行的时候,会将alternate属性赋值为currentRoot。因为这里是首屏渲染,所以currentRoot是null。

更新阶段

此时currentRoot已经存在,在调和阶段会进行currnetRoot树和wipRoot树进行对比。

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

更新阶段,首屏渲染已经结束,所以currentRoot是首屏渲染的节点数据。

对比逻辑

这里的问题是如何进行每个节点和原先的节点进行处理。

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) {
      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 (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

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

    prevSibling = newFiber;
    index++;
  }
}

判断分为三种类型:

  • sameType:相同类型,标识 UPDATE

  • element && !sameType:不同类型,新元素标识 PLACEMENT

  • oldFiber && !sameType:不同类型,老元素标识 DELETION

渲染器

更新页面元素

function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  let domParentFiber = fiber.parent;
  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 === "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);
}

操作的还是根节点#root。经过首屏渲染之后,current Fiber树和wip Fiber(workInProgress)之间的差异会被同步到dom节点上。

Hooks

function useState(initial) {
  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);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

这里是将hooks作为根节点的属性进行存储。每一个更新,都会先去currentRoot查看之前的state数据,进行赋值。

const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: []
  };

当然这里的hooks处理是同步的,不会进行batchUpdate处理。

代码:

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: []
    }
  };
}

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");
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) {
  //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]);
    });
}

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

function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  let domParentFiber = fiber.parent;
  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 === "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 render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;

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

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

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

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.parent;
  }
}

let wipFiber = null;
let hookIndex = null;

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

function useState(initial) {
  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);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

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

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) {
      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 (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

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

    prevSibling = newFiber;
    index++;
  }
}

const Didact = {
  createElement,
  render,
  useState
};

/** @jsx Didact.createElement */
function Counter() {
  const [state, setState] = Didact.useState(1);
  const [state1, setState1] = Didact.useState(1);

  return (
    <div>
      <h1 onClick={() => setState(c => c + 1)} style="user-select: none">
        Count: {state}
      </h1>
      <h1 onClick={() => setState1(c => c + 1)} style="user-select: none">
        Count: {state1}
      </h1>
    </div>
  );
}
const element = <Counter />;
const container = document.getElementById("root");
Didact.render(element, container);

更多推荐

点个在看你最好看

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
构建React完整项目涉及以下几个步骤: 1. 创建项目:通过使用create-react-app等工具,可以快速创建一个基本的React项目结构。运行命令后,会自动生成一个包含所有必要文件和文件夹的项目结构。 2. 配置Webpack:Webpack是一种模块打包工具,用于将项目中的各种资源(如JavaScript、CSS、图片等)打包成静态资源。你需要配置Webpack,以确保你的React项目能够正确地打包。 3. 设计组件:在React中,组件是构建用户界面的基本单元。你需要根据项目需求,设计并创建不同的组件。组件应具有可重用性,并按照单一职责原则进行设计。 4. 管理状态:状态是React项目中的重要概念。你可以使用React的内置状态管理机制,例如使用state和props来管理组件的状态。另外,你还可以选择使用其他库,如Redux来管理全局状态。 5. 路由管理:对于大型项目来说,路由管理非常重要。你可以使用React Router等库来实现路由功能,以便根据URL在不同的页面之间进行导航。 6. 数据请求:在React项目中,你可能需要从服务器获取数据。你可以使用fetch或Axios等库来进行数据请求,并将数据传递给相应的组件进行显示或处理。 7. 测试和调试:编写测试用例对于确保项目质量非常重要。你可以使用测试框架如Jest或Enzyme来编写单元测试,并通过console.log等工具进行调试。 8. 构建和部署:完成开发后,你需要将项目构建为生产版本,并进行部署。通过Webpack的打包功能以及使用minify和gzip等优化方式,可以将项目打包为较小的文件。然后,将打包后的文件上传到服务器或部署到云服务商。 这些是构建React完整项目的一些关键步骤。通过合理地规划和设计,你可以构建出高质量、可维护和可扩展的React项目。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值