React 框架基本实现

313 阅读5分钟
  1. React的基本概念
  2. createElement函数的实现
  3. render函数的实现
  4. render阻塞浏览器优化
  5. Fiber
  6. render渲染不完全的ui优化
  7. Reconciliation

React的基本概念

const element = <h1 title="foo">Hello World</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)
  • 创建一个react元素element
  • 获取一个DOM节点container
  • 将react元素渲染container节点上

用js替换jsx

  • jsx 语法来定义的元素(h1)不符合原生的js语法,jsx转换成js用babel等编译工具,把元素转换给createElement函数传递标签、属性、和标签子节点
// 替换 const element = <h1 title="foo">Hello World</h1>
const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)

// React.createElement会创建一个对象来表示react元素,
let element = {
    type:"h1",
    props:{
        title:"foo",
        children:"Hello World"
    }
}

  • 替换ReactDOM.render函数,render函数是把react元素渲染到DOM节点上
// 替换 ReactDOM.render(element, container)
const node = document.createElement(element.type)
node["title"] = element.props.title

const text = document.createTextNode("")
text["nodeValue"] = element.props.children

node.appendChild(text)
container.appendChild(node)

createElement实现

  • 一个react element实际上就是一个拥有type和props属性的对象,createElemen就是根据react元素的标签、属性、子节点,返回上述的对象
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
}

render函数的实现

  • 目前先考虑只添加节点,render函数根据createElement返回的对象渲染到指定的节点上
function render(element, container) {
  const dom = document.createElement(element.type)
  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

render渲染优化

  • js是单线程,当我们开始render element成真实的DOM节点的时候,我们就会占用主线程过长时间,当我们有别的高级操作就会产生卡顿
  • 解决办法是我们把工作拆分成一个个小的单元(Fiber),每个单元工作完成之后就去查看是否有重要的工作,有就打断当前渲染循环
let nextUnitOfWork = nullfunction workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1// 浏览器多少时间分配
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)

function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

通过使用浏览器的requestIdleCallback这个api来完成,基本功效就是requestIdleCallback会在浏览器会在主线程空闲的时候执行回调函数workLoop

Fiber

  • 把工作分成一个个工作单元,每一个工作单元就是Fiber,每一个react element都将对应一个fiber结构,每一个fiber结构都对应一个单元的工作
  • Fiber的作用是非常容易的找到下一个单元,所以他是一个包含他的子Fiber(child),父Fiber(parent),兄弟Fiber(silbing)的对象
  • 在render函数中我们先需要创建Fiber(根Fiber),剩下的工作将在 performUnitOfWork 函数中完成,我们将对每一个 fiber 节点做三件事
    • 将react element 渲染成DOM
    • 给react element 子节点创建fiber节点
    • 选择下一个的单元工作
  • 我们将render函数改造一下,抽离出根据fiber创建DOM这个功能,然后在render中创建根Fiber
// 创建DOM节点根据fiber
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
}
let nextUnitOfWork = null
// 将nextUnitOfWork设置为fiber根节点
fucntion render(element,container){
   nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}

当浏览器空闲的时候就会开始调用workLoop,开始在我们创建的根节点nextUnitOfWork开始工作

  • 开始完善我们的performUnitOfWork函数
function performUnitOfWork(fiber) {
  // 1.将fiber渲染成真实DOM放的父DOM节点上
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
​
  // 2.创建子fiber
  const elements = fiber.props.children
  let index = 0
  let prevSibling = nullwhile (index < elements.length) {
    const element = elements[index]
​    // 创建新 fiber
     const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }
   // 根据是否为第一个节点,添加到对应的 child / sibling 上面
   if (index === 0) {
     fiber.child = newFiber
   } else {
     prevSibling.sibling = newFiber
   }
​
   prevSibling = newFiber
   index++
}
  
  // 3.返回下一个工作单元
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
  }
}

render渲染不完全的ui优化

  • 由于渲染分成了一个个工作单元所以当浏览器有更重要的任务会打断我们的渲染,会让用户看到一个不完全的ui
  • 我们将performUnitOfWork中DOM处理删除,统一当没有工作单元并且存在循环更新的节点的时候(也就是render了的时候),用wipRoot来记录一下,一次性提交所有的fiber树更新到 document 上面。
// 删除performUnitOfWork中这行添加 node 的操作。
function performUnitOfWork(fiber) {
  // ... 省略
  /* 删除 */
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
  /* 删除 */
  // ... 省略
}

// 用wipRoot记录一下存在循环更新节点的时候
function render(element, container) {
  // 流程树
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}


let nextUnitOfWork = null
let wipRoot = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
​  // 一次性全部提交
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }
​
  requestIdleCallback(workLoop)
}

把这个提交所有 fiber 树过程在全新的函数 commitRoot 中实现。我们递归的把节点添加到 document 上面。

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}
​
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

Reconciliation调和

  • 我们之前只是在document 上面添加元素,更新和删除没有做,添加这部分功能,比较这次render的fiber树跟上次fiber结构有什么不同
  • 具体需要保存一份完整的上次的fiber树为currentRoot,每一个fiber中也添加alternate属性指向上一次更新的fiber节点
function commitRoot() {
  commitWork(wipRoot.child)
  // 添加 currentRoot
  currentRoot = wipRoot
  wipRoot = null
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    // 添加 alternate
    alternate: currentRoot,
  }
  // 新增记录删除的数组
  deletions = []
  nextUnitOfWork = wipRoot
}

let currentRoot = null
  • performUnitOfWork 函数中创建新 fiber 节点部分的代码抽取成 reconcileChildren 函数。根据老的 fiber 节点来调和新的 react 元素。
function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  const elements = fiber.props.children
  reconcileChildren(fiber, elements)
​
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
  • 我们同事循环老的老的 fiber 树的子节点和我们需要调和新的的 react 节点,有任何改变都需要更新到document上面,根据type(DOM节点)来区分是否改变,源码中还根据key属性,可以得到react elements 中被替换的明确位置
    • 老fiber和react element有相同的type,只要更新它的属性
    • 如果 type 不同说明这里替换成了新的 dom 节点,我们需要创建。
    • 如果 type 不同 且同级仅存在 old fiber 说明节点老节点删除了,我们需要移除老的节点。
function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = nullwhile (
    index < elements.length ||
    oldFiber != null
  ) {
    const element = elements[index]
    let newFiber = nullconst sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type
      // 用effectTag来区分协调的结果// 相同type,我们创建一个新的 fiber 节点来复用老 fiber 的 dom节点,然后从 react element 上面取到新的props。
    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)
    }
}
  • 同步到commitRoot函数上面
function commitRoot() {
  // 删除节点操作
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}

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") {
    domParent.removeChild(fiber.dom)
  }
​
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

参考资料