关于react.js:build-your-own-react

一、这段代码怎么个逻辑

const element = <h1 title="foo">Hello</h1>;

const container = document.getElementById("root");
ReactDOM.render(element, container);

babel帮咱们转化(react17当前,react内置了转化工具)

const element = <h1 title="foo">Hello</h1>;
// 会转成
const element = React.createElement("h1", { title: "foo" }, "Hello");

最终转成对象

  • type是dom节点的类型, 它是通过document.createElement 创立的标签名称
  • type也可能是个函数,sep II 会讲到
  • children在这里是个字符串, 然而它通常是个数组,蕴含多个元素.

    // 最终转成对象
    const element = {
    type: "h1",
    props: {
      title: "foo",
      children: "Hello",
    },
    };

二、react render干了啥

为了防止争执, 我用element标识react element, 用node标识Dom元素

const node = document.createElement(element.type)
node["title"] = element.props.title

// 不要间接抄作dom,不便最初一起操作
const text = document.createTextNode("")
text["nodeValue"] = element.props.children

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

三、createElement函数

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
// to
const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)

间接放代码

function createElement(type, props, ...children) { // 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: [],
    },
  }
}

四、render函数

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

  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })
​
​  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

五、并发模式

首先咱们要先重构,应为render不能进行,可能会block主线程,导致用户input或者动画不晦涩

首先咱们要分成更小的单元,咱们实现每个单元后,如果有其它事件要做,咱们就能够让浏览器终端渲染

react用的是sheduler package. 然而概念上是一样的

let nextUnitOfWork = null

// requestIdleCallback给咱们提供了deadline参数
// 咱们能够晓得到浏览器下一次管制渲染过程还剩多少工夫
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

要开始应用循环,咱们须要设置第一个工作单元,而后编写一个 performUnitOfWork 函数,该函数不仅执行工作,还返回下一个工作单元

对于requestIdleCallback和requestAnimationFrame 请看 https://segmentfault.com/a/11…

六、Fibers

为了组织工作单元,咱们须要一个数据结构:纤维树。
咱们将为每个元素应用一个fiber,每个fiber都是一个工作单元。

如果咱们想渲染上面的树

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

在渲染中,咱们将创立root fiber并将其设置为 nextUnitOfWork。其余的工作将在 performUnitOfWork 函数上进行,咱们将为每个光纤做三件事:

  • 将元素增加到dom中
  • 将该元素的children创立fibers
  • 抉择nextUnitOfWork

看下方数据结构,这种数据结构的指标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber都有一个链接到它的第一个child、下一个sibling和它的parent。

当咱们实现对fiber的工作时,如果它有子fiber,则该fiber将是nextUnitOfWork。
在咱们的示例中,当咱们实现 div fiber的工作时,nextUnitOfWork将是 h1 fiber。

形式就是爸爸找儿子,儿子找弟弟,弟弟找叔叔的步骤

如果fiber既没有child也没有sibling,咱们就去找“uncle”:也就是parent的sibling。就像示例中的 a 和 h2 fiber一样。

此外,如果parent没有sibling,咱们会持续通过parent往上找,直到咱们找到有sibling的父母,或直到咱们达到根。如果咱们曾经到了根,就意味着咱们曾经实现了这个渲染的所有工作

当初咱们开始写代码
这是原来的render函数,须要重写

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

  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })
​
​  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

咱们将创立dom节点局部,放到本人的函数中,前面会用

// fiber
function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type)

  // 更新属性可换成updateDom(dom,{},fiber.props)​
  const isProperty = key => key !== "children"
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = fiber.props[name]
    })

  return dom
}

在render函数中咱们设置 nextUnitOfWork为root fiber

function render(element, container) {
  // TODO set next unit of work
  nextUnitOfWork = {
    dom: container, // 跟节点
    props: {
      children: [element], 
    },
  }
}
​
let nextUnitOfWork = null

而后,当浏览器筹备好时,它将调用咱们的 workLoop,咱们将开始在根上工作。

首先,咱们创立一个新节点并将其附加到 DOM。

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
  
  // 而后每个子节点,咱们创立一个fiber
  const elements = fiber.props.children
  let index = 0
  let prevSibling = null
​
  while (index < elements.length) {
    const element = elements[index]
​
    const newFiber = {
      type: element.type,
      props: element.props, // props蕴含children和属性
      parent: fiber,
      dom: null,
    }
    
    // 咱们将newFiber增加到以后fiber树中,将其设置为child或sibling,具体取决于它是否是第一个孩子。
    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
  
  // 最初咱们寻找nextUnitOfWork。咱们首先是child,而后sibling,而后是叔叔uncle。  
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

七、渲染和提交 commit

有一个问题,每次解决元素时,咱们都会向 DOM 增加一个新节点。而且,请记住,浏览器可能会在咱们实现渲染整个树之前中断咱们的工作。在这种状况下,用户将看到不残缺的 UI。咱们不心愿那样

所以咱们须要从这里移除扭转 DOM 的局部,看正文1

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  } 
  // 1. 移除扭转 DOM 的局部
  // if (fiber.parent) {
  //  fiber.parent.dom.appendChild(fiber.dom)
  // }
  ...
}

代替,咱们将跟踪fiber tree的根。咱们称其为正在进行的工作 root 或 wipRoot。

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let wipRoot = null

一旦咱们实现了所有的工作(咱们晓得这是因为没有nextUnitOfWork)咱们将整个fiber tree提交给 DOM。

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
​
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }
​
  requestIdleCallback(workLoop)
}

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}
// 在这里,咱们递归地将所有节点附加到 dom。​
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

八、Reconciliation 和谐

到目前为止,咱们只向 DOM 增加了货色,然而更新或删除节点呢?
这就是咱们当初要做的,咱们须要将咱们在render函数上收到的element与咱们提交给 DOM 的最初一个fiber树进行比拟

因而,咱们须要在实现提交后, 保留对“咱们提交给 DOM 的最初一个fiber树”的援用。咱们称之为currentRoot。
咱们还为每个fiber增加了alternate属性。该属性是old filber的链接,即咱们在前一个提交阶段提交给 DOM 的fiber

function commitRoot() {
  commitWork(wipRoot.child)
  // 新加的援用
  currentRoot = wipRoot
  wipRoot = null
}


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

let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null

当初让咱们从 performUnitOfWork 中提取创立新filber的代码……
到一个新的 reconcileChildren 函数

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
  }
}

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let prevSibling = null
​
  while (index < elements.length) {
    const element = elements[index]
​
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: wipFiber,
      dom: null,
    }
​
    if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

在reconcileChildren中,咱们将协调旧fiber与新elements。
咱们同时迭代旧fiber (wipFiber.alternate) 的子children和咱们想要协调的elements数组。

如果咱们疏忽同时迭代数组和链表所需的所有样板文件,那么咱们只剩下这个 while 中最重要的货色:oldFiber 和 element
元素是咱们想要渲染到 DOM 的货色,而 oldFiber 是咱们上次渲染的货色。

function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child // 新加
  let prevSibling = null
​
  while (index < elements.length || oldFiber != null) { // oldFiber
    const element = elements[index]
    
     let newFiber = null
    // 咱们须要比拟它们, 以查看是否须要对DOM做扭转。​
    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type
       // 这里 React 也应用了key,这能够更好地协调。例如,它检测子元素何时更改元素数组中的地位​
    if (sameType) {
      // update the node 如果旧的 Fiber 和新的元素具备雷同的类型,咱们能够保留 DOM 节点并应用新的 props 更新它
       newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE", // 咱们稍后会在commit阶段应用这个属性
      }
    }
    if (element && !sameType) {
      // add this node 如果类型不同并且有新元素,则意味着咱们须要创立一个新的 DOM 节点
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT", // PLACEMENT 标签标记新的fibr。
      }
    }
    
    // *******然而当咱们将fiber树提交到 DOM 时,是在work in progress root实现的,是没有old fiber的
    // 所以咱们须要一个数组来跟踪咱们想要删除的节点。看下方的render
    if (oldFiber && !sameType) {
      // delete the oldFiber's node 如果类型不同并且有fiber,咱们须要删除旧节点
      oldFiber.effectTag = "DELETION" // 咱们没有新的 Fiber,因而咱们将成果标签增加到旧的 Fiber
      deletions.push(oldFiber)
    }
    
    if (oldFiber) { // 如果是假,下一个oldFiber还是假
      oldFiber = oldFiber.sibling
    }
    ​
    if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

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

而后,当咱们将更改提交到 DOM 时,咱们也会应用该数组中的fiber。

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

当初,让咱们更改 commitWork 函数来解决新的 effectTags。

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
  ) {
    // 如果是 UPDATE,咱们须要用扭转的 props 更新现有的 DOM 节点。
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }
  
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

updateDom

一个非凡的props是事件监听,如果props以on结尾,咱们须要解决不同的

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) {
  // TODO 次要就是更新props
  //Remove 旧的或者曾经扭转的 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]
      )
    })
    // 增加新的或者曾经扭转的props
     Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[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]
    })
}

到这里能够测试一下下面的代码了

const Didact = {
  createElement,
  render,
}

/** @jsx Didact.createElement */
const container = document.getElementById("root")

const updateValue = e => {
  rerender(e.target.value)
}

const rerender = value => {
  const element = (
    <div>
      <input onInput={updateValue} value={value} />
      <h2>Hello {value}</h2>
    </div>
  )
  Didact.render(element, container)
}

rerender("World")

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理