Search by
    简体中文
    主题设置

    anyspace

    构建一个自己的React

    2024年4月16日 • ☕️☕️☕️☕️ 21 min read阅读量 : •••

    简单小🌰

    我们将下面这个简单的 React App 转变为纯 js 实现

    Copy
    const element = <h1 title="foo">Hello</h1>
    const container = document.getElementById("root")
    ReactDOM.render(element, container)
    

    React App 中,Bable将第一行JSX代码编译为React.createElement的形式如下

    Copy
    const element = React.createElement(
      "h1",
      { title: "foo" },
      "Hello"
    )

    createElement 将返回一个纯 js 对象来描述 element ,即 Virtual Dom

    Copy
    const element = {
        type: "h1",
        props: {
            title: "foo",
            children: "Hello",
        },
    }

    我们有了 真实Dom 的描述( Virtual Dom ),那么我们只需要根据描述创建对应 真实Dom 节点插入到container容器中即可。即我们要完成 ReactDom.render 的任务。

    Copy
    const container = document.getElementById("root")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)

    😍✌成功将上述 React App 转变为纯 js 实现

    img

    通过上述简单🌰已经对React有了简单理解,下面我们将一步步构建一个完整的自己的React。

    createElement函数

    我们从另一个较复杂 React App 小🌰开始!

    Copy
    const element = (
      <div id="foo">
    
        <a>bar</a>
        <b />
    
      </div>
    )
    const container = document.getElementById("root")
    ReactDOM.render(element, container)
    

    JSX将转换为

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

    我们可以得到 createElement 的雏形。

    Copy
    //  ...children 参数保证了我们的函数从第3个参数开始输入多个参数都会放入children属性中,是一个数组。
    //  props:{ children: [...] }
    
    function createElement(type, props, ...children) {
      return {
    
        type,
        props: {
          ...props,
          children,
        },
    
      }
    }
    

    但是很明显上述children属性的处理完全还不够。因为children数组中可能有element节点,也有可能是纯文本。所以我们将对纯文本做一些特别的处理,给它一个特别的type:TEXT_ELEMENT为了方便我们自己的React构建。

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

    我们将🌰中 React.createElement 替换为我们自己的 createElement , 并且给我们自己的 React 库起一个名字 Myact 😂

    Copy
    const Myact = {
        createElement,
    }const element = Myact.createElement(
        "div", {
            id: "foo"
        },
        Myact.createElement("a", null, "bar"),
        Myact.createElement("b")
    )

    我们还需要告诉 Bable 去使用 Myact.createElement

    Copy
    /** @jsx Myact.createElement */
    //上面这句注释将告诉Bable使用我们自己createElement
    
    const element = (
      <div id="foo">
    
        <a>bar</a>
        <b />
    
      </div>
    )
    const container = document.getElementById("root")
    ReactDOM.render(element, container)
    

    render 函数

    从上面小🌰中我们可以知道render函数的作用是根据createElement返回的虚拟Dom创建真实Dom插入到container容器中,也要将虚拟Dom的props放进真实Dom中哦。

    我们得到render函数的雏形。

    Copy
    function render(element, container) {
      const dom = document.createElement(element.type)
    ​
      container.appendChild(dom)
    }const Myact = {
      createElement,
      render,
    }

    当然,这样完全不够,因为我还需要将element的children元素同样被createElement。而且需要判断element节点的类型,来创建真实Dom容器。

    Copy
    function render(element, container) {
      //  通过element的type属性判断该创建什么样的真实Dom节点
      const dom =
    
        element.type == "TEXT_ELEMENT"
          ? document.createTextNode("")
          : document.createElement(element.type)
    
    //  遍历递归children为其每一个元素调用createElement创建其真实Dom节点并插入到父容器
      element.props.children.forEach(child =>
    
        render(child, dom)
    
      )
    ​
      container.appendChild(dom)
    }
    

    最后,只需要完成将虚拟Dom的props放进真实Dom上就可以啦。

    Copy
    function render(element, container) {
      const dom =
        element.type == "TEXT_ELEMENT"
          ? document.createTextNode("")
          : document.createElement(element.type)//  因为虚拟Dom props中除了属性还有我们的children,所以需要过滤掉
      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)
    }

    😍👍我们的Myact库构建完成了!

    Copy
    <span
    title='Click Me'
    style='cursor:pointer;background:#f7a046;padding:1px 8px;border-radius:5px;color:#fff'>
    点击查看Myact完整代码
    </span>
    Copy
    /** @jsxRuntime classic */
    /** 可能在运行Myact时,会出现 错误:pragma and 
    
        pragmafrag cannot be set when runtime is 
        automatic.我们只需要加上上方注释即可改变
        runtime,让JSX加载我们的Myact代码。 */
    
    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 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)
    }const Myact = {
      createElement, 
      render, 
    }/** @jsx Myact.createElement */
    const element = (
      <div id="foo">
    
        <a>bar</a>
        <b />
    
      </div>
    )
    const container = document.getElementById("root")
    Myact.render(element, container)
    

    img

    Concurrent Mode并发模式

    我们构建的Myact库render函数中,使用了递归。这里我们可以知道,我们的render一旦执行就不会停止,直到递归结束。那么如果我们的虚拟Dom树是一个超级大超级深的树,那render可就会执行很长时间啦。这时如果浏览器想要做一些更高优先级的事情,比如用户输入事件或者动画的流畅,显然是不能的,仍要等待我们的render。

    Copy
    element.props.children.forEach(child => render(child, dom))

    所以,现在我们需要将工作分成小单元,在完成每个小单元后,如果有其他优先级高的事情,我们就中断我们的render,将控制权交给浏览器。

    那么我们接下来将递归循环更改为可控的单元执行循环,我们使用window.requestIdleCallback()来构建我们的循环。React不再使用requestIdleCallback,而是自己构建了一个scheduler调度器。但是概念是相同的,我们的Myact不再自己构建scheduler调度器。

    window.requestIdleCallback()方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。

    Copy
    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
        // 此函数要完成的是执行单元任务,并且返回下一个单元任务
    }

    Fiber

    为了组织我们的工作单元,我们需要一种数据结构:fiber树。每一个元素节点都将有一个fiber对应,而每一个fiber都将是一个工作单元。

    小例子🌰

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

    在渲染中,我们将创建根fiber并将其设置为nextUnitOfWork。其余的工作将发生在performUnitOfWork,在那里我们将为每个fiber做三件事:

    • 将元素添加到Dom中
    • 为元素的子元素创建fiber(为下一步做准备)
    • 选择下一个工作单元

    img

    fiber树的单元任务执行流程: 当我们完成一个fiber的工作时,它的child将是下一个工作单元,如果没有child那么就会它的sibling将是下一个工作单元。如果也没有sibling将返回父fiber节点,父fiber节点的sibling将会是下一个工作单元。如果父fiber仍没有sibling,将去寻找父fiber的父fiber的sibling。直到返回root fiber,则说明完成了所有的单元任务。

    首先,我们将render函数中创建真实Dom的部分封装为一个独立的函数。方便后续使用。

    Copy
    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
    }
    function render(element, container) {
      // TODO 设置下一个工作单元
    }let nextUnitOfWork = null
    

    在render函数中,我们设置工作单元为fiber树的root节点

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

    当我们的浏览器有空闲时间时,将会执行我们的workLoop,并且我们将从root执行任务。

    Copy
    function workLoop(deadline) {
      let shouldYield = false
      while (nextUnitOfWork && !shouldYield) {
    
        nextUnitOfWork = performUnitOfWork(
          nextUnitOfWork
        )
        shouldYield = deadline.timeRemaining() < 1
    
      }
      requestIdleCallback(workLoop)
    }requestIdleCallback(workLoop)
    function performUnitOfWork(fiber) {
      // TODO 将元素添加到Dom节点
      // TODO 为元素得的子元素创建fiber
      // TODO 返回下一个单元任务
    }
    

    执行工作单元函数

    接下来我们来完成performUnitOfWork函数的三件事。👌

    第一步我们先将元素添加到Dom节点,我们将Dom存放在fiber的dom属性上。

    Copy
    function performUnitOfWork(fiber) {
      // 将元素添加到Dom节点
      // 判断fiber是否有dom,没有就为其创建dom
      if (!fiber.dom) {
        fiber.dom = createDom(fiber)
      }// 将fiber.dom添加到其父fiber dom树上
      if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom)
      }// TODO 为元素的子元素创建fiber
      // TODO 返回下一个单元任务
    }

    第二步,我们为元素的每个子元素创建fiber。

    Copy
    function performUnitOfWork(fiber) {
        // 将元素添加到Dom节点
        ...
        // 为元素的子元素创建fiber
        const elements = fiber.props.children
        let index = 0
        let prevSibling = null// 循环为所有孩子元素创建fiber
        while (index < elements.length) {
            const element = elements[index]// fiber初始化
            const newFiber = {
                type: element.type,
                props: element.props,
                parent: fiber,
                dom: null,
            }
            // 判断该元素是否为父fiber的第一个子元素
            // 如果是,该fiber为父fiber的child,否则该fiber为上个fiber的兄弟fiber
            if (index === 0) {
                fiber.child = newFiber
            } else {
                prevSibling.sibling = newFiber
            }// 指针移动
            prevSibling = newFiber
            index++
        }
        // TODO 返回下一个单元任务

    第三步,我们具体判断下一个工作单元是谁并返回。

    Copy
    function performUnitOfWork(fiber) {
        // 将元素添加到Dom节点
        ...
        // 为元素的子元素创建fiber
        ...
        // 返回下一个单元任务
        // 判断是否有子fiber,如果有这个子fiber就是下一个工作单元
        if (fiber.child) {
            return fiber.child
        }
        //如果没有子fiber,那么该fiber还有兄弟fiber
        let nextFiber = fiber
        while (nextFiber) {
            // 如果有兄弟fiber,该兄弟fiber就是下一个工作单元
            if (nextFiber.sibling) {
                return nextFiber.sibling
            }
            // 如果既该fiber没有子fiber,又没有兄弟fiber,那将返回其父fiber
            nextFiber = nextFiber.parent
        }
    }

    我们的performUnitOfWork函数完成啦!🎉

    Copy
    <span
    title='Click Me'
    style='cursor:pointer;background:#f7a046;padding:1px 8px;border-radius:5px;color:#fff'>
    点击查看该函数完整代码
    </span>
    Copy
    function performUnitOfWork(fiber) {
        //  将元素添加到Dom节点
        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 = nullwhile (index < elements.length) {
            const element = elements[index]const newFiber = {
                type: element.type,
                props: element.props,
                parent: fiber,
                dom: null,
            }
    
            if (index === 0) {
                fiber.child = newFiber
            } else {
                prevSibling.sibling = newFiber
            }​
            prevSibling = newFiber
            index++
        }
    
        // 返回下一个单元任务
        if (fiber.child) {
            return fiber.child
        }
        let nextFiber = fiber
        while (nextFiber) {
            if (nextFiber.sibling) {
                return nextFiber.sibling
            }
            nextFiber = nextFiber.parent
        }
    
    }

    vue为什么不需要fiber架构

    Copy
    <span
    title='Click Me'
    style='cursor:pointer;background:#f7a046;padding:1px 8px;border-radius:5px;color:#fff'>
    点击查看
    </span>

    为什么有react fiber,而没有vue fiber

    render commit阶段

    我们遇到了一个问题,因为我们已经更改为并发模式,我们每次处理一个元素都会向Dom添加一个新节点。在我们渲染完整棵Dom树前浏览器都可以中断我们的任务去执行优先级更高的任务。所以用户可能将看到一个不完整的UI页面,我们不希望这样。所以我们需要做出调整。

    删除performUnitOfWork函数此处代码。

    Copy
    function performUnitOfWork(fiber) {
      if (!fiber.dom) {
    
        fiber.dom = createDom(fiber)
    
      }
      if (fiber.parent) {                         ❌删除
    
        fiber.parent.dom.appendChild(fiber.dom)   ❌删除
    
      }                                           ❌删除
    
      ...}
    

    用来代替的是我们将跟踪fiber树的root,把它称作工作中的root(work in progress)或wipRoot。每执行一个单元我们并不把真实Dom添加到root真实Dom上,而是只是先把创建好的Dom存放到每个fiber节点上,等到没有下一个任务单元了,我们就知道完成了所有任务,然后才把整个fiber树提交,再去一次性添加到root真实Dom上。

    Copy
    function commitRoot() {
      // TODO add nodes to dom
    }
    
    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)
    }

    下面我们来完成commitRoot函数。我们只需要递归遍历fiber树,将每个fiber节点的dom插入到真实dom root上即可。

    Copy
    function commitRoot() {
        commitWork(wipRoot.child) //从root的child开始递归遍历添加dom节点
        wipRoot = null //当commit完成,将正在工作的fiber树根节点置为null
    }function commitWork(fiber) {
        if (!fiber) {
            return
        }
        // 添加Dom到其父Dom中
        const domParent = fiber.parent.dom
        domParent.appendChild(fiber.dom)
        // 先递归孩子,再递归兄弟
        commitWork(fiber.child)
        commitWork(fiber.sibling)
    }

    我们已经完成了commit阶段😋。

    Reconciliation

    现在我们已经完成了第一次渲染,但是还有更新删除页面节点的操作。现在当我们渲染时需要去比较旧的fiber树。所以我们需要在commit阶段记录上一次的fiber树,我们将它记录为 currentRoot 。我们也在每个fiber上添加一个新的属性 alternate ,这个属性指向旧的fiber树即我们上次commit阶段的提交的fiber树。

    Copy
    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中创建新fiber的代码抽离到一个新的函数中:reconcileChildren

    Copy
    function performUnitOfWork(fiber) {
      //为fiber创建dom
      ...// 为元素的子元素创建fiber
      const elements = fiber.props.children
      reconcileChildren(fiber,elements)
    
      //返回下一个fiber
      ...
    
    }
    
    function reconcileChildren(wipFiber, elements) {
      let index = 0
      let prevSibling = nullwhile (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进行对比。

    Copy
    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 = null
    
            // TODO 比较新旧fiber
        }
    
        if (oldFiber) {
            oldFiber = oldFiber.sibling
        }
    }

    我们使用fiber的type进行比较新旧fiber:

    • 如果新旧fiber的type相同,我们将使用原来的dom,只需要更新props即可。
    • 如果没有旧fiber,有新fiber,则说明我们需要新增元素节点,新增一全新的fiber。
    • 如果有旧fiber,但是没有新增fiber,则说明我们需要删除旧fiber。
    Copy
    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 = null
    
        // 比较新旧fiber
    
        const sameType = oldFiber && element && element.type == oldFiber.type
        if (sameType) {
          // TODO 更新节点
        }
        if (element && !sameType) {
          // TODO 新增节点
        }
        if (oldFiber && !sameType) {
          // TODO 删除旧fiber节点
        }
    
      }
    
      if (oldFiber) {
    
          oldFiber = oldFiber.sibling
    
      }
    }
    

    接下来我们来完成reconcile中的更新节点、新增节点、删除节点。我们先给三种不同的操作fiber打上不同的tag属性,后续commit阶段将会用到。

    • 更新节点我们直接只需要更新旧fiber的props即可,其它都复用旧fiber
    • 新增节点,新增一个全新的fiber,、
    • 删除节点,只为旧fiber打上删除标记,存入当前渲染fiber树中的deletions数组中
    Copy
        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)
        }

    新增deletions数组, 在每次render时初始化该数组。

    Copy
    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
    

    每次render都会收集deletions元素及要删除的旧fiber,那么我们对应的需要在commit阶段删除这些fiber。

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

    改造commitWork函数,上面我们已经为更新、新增、删除fiber打上了tag标记,现在我们根据这些标记改造commitWork函数,完成dom的更新。

    Copy
    function commitWork(fiber) {
      if (!fiber) {
    
        return
    
      }
      const domParent = fiber.parent.dom
      domParent.appendChild(fiber.dom)   //❌删除 现在我们需要根据不同的fiber标记情况,对dom进行操作。
      commitWork(fiber.child)
      commitWork(fiber.sibling)
    }
    
    • 如果effectTag是PLACEMENT,则为新增节点,我们直接将其dom插入父元素中。
    • 如果effectTag是DELETION,则为删除节点,我们将该dom元素移除。
    • 如果effectTag是UPDATE,则为更新节点,我们将更新dom的props。(update比较复杂,我们将其代码抽出为updateDom函数)
    Copy
    function commitWork(fiber) {
      if (!fiber) {
        return
      }
      const domParent = fiber.parent.dom
      
      //操作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)
    }
    
    function updateDom(dom, prevProps, nextProps) {
      // TODO
    }

    接下来我们来完成updateDom函数。

    Copy
    //是否为属性
    const isProperty = key => key !== "children"
    //是否为新添加的节点
    const isNew = (prev, next) => key => prev[key] !== next[key]
    //是否为要删除的节点
    const isGone = (prev, next) => key => !(key in next)
    
    function updateDom(dom, prevProps, nextProps) {
        // 移除旧属性
        Object.keys(prevProps)
            .filter(isProperty)
            .filter(isGone(prevProps, nextProps))
            .forEach(name => {
                dom[name] = ""
            })// 设置新的或者更新的属性
        Object.keys(nextProps)
            .filter(isProperty)
            .filter(isNew(prevProps, nextProps))
            .forEach(name => {
                dom[name] = nextProps[name]
            })
    }

    还需要注意的是,节点的属性有可能是绑定的事件,所以我们需要进行处理。on开始的我们需要进行不同的操作。

    Copy
    //是否事件函数
    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) {
        // 移除旧属性
        Object.keys(prevProps)
            .filter(isProperty)
            .filter(isGone(prevProps, nextProps))
            .forEach(name => {
                dom[name] = ""
            })// 设置新的或者更新的属性
        Object.keys(nextProps)
            .filter(isProperty)
            .filter(isNew(prevProps, nextProps))
            .forEach(name => {
                dom[name] = nextProps[name]
            })
    }
    `

    Function组件

    我们将Myact支持函数组件。我们从一个小栗子开始。

    Copy
    function App(props) {
        return <h1 > Hi {
            props.name
        } < /h1>
    }
    const element = < App name = "foo" / >
        const container = document.getElementById("root")
    Myact.render(element, container)

    上述jsx代码将会编译成js为:

    Copy
    function App(props) {
        return Myact.createElement(
            "h1",
            null,
            "Hi ",
            props.name
        )
    }
    const element = Myact.createElement(App, {
        name: "foo",
    })
    const container = document.getElementById("root")
    Myact.render(element, container)
    Copy
    function performUnitOfWork(fiber) {
        if (!fiber.dom) {
            fiber.dom = createDom(fiber)
        }const elements = fiber.props.children
        reconcileChildren(fiber, elements)
    
            ...
    }

    函数组件有两处不同点:

    • 来自function组件的fiber没有dom节点
    • 并且它的children不是直接从其props中获取,而是通过运行function

    我们检查fiber的类型是否为function,并且根据是否为function去执行不同的更新。updateHostComponent做之前我们做的操作,updateFunctionComponent去执行function组件获取children。

    Copy
    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
    
      }
    }
    
    function updateFunctionComponent(fiber) {
      // TODO
    }function updateHostComponent(fiber) {
      if (!fiber.dom) {
    
        fiber.dom = createDom(fiber)
    
      }
      reconcileChildren(fiber, fiber.props.children)
    }
    

    我们这个小栗子,App函数组件,运行后将会return h1节点。 我们得到fiber的children后,后续的reconciliation阶段将和原来保持一致。

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

    因为函数组件fiber没有dom,我们也需要更改commitWork function。

    首先,要找到DOM节点的父节点,沿着fiber树向上查找,直到找到具有DOM节点的fiber。

    并且在删除一个节点时,我们也需要直到找到一个具有 DOM 节点的子节点。

    Copy
    function commitWork(fiber) {
      if (!fiber) {
    
        return
    
      }
    
      const domParent = fiber.parent.dom  //删除❌
    
      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") {
    
        domParent.removeChild(fiber.dom) //删除❌
        commitDeletion(fiber, domParent)
    
      }
    
      commitWork(fiber.child)
      commitWork(fiber.sibling)
    }
    
    Copy
    function commitDeletion(fiber, domParent) {
      if (fiber.dom) {
        domParent.removeChild(fiber.dom)
      } else {
        commitDeletion(fiber.child, domParent)
      }
    }

    Hooks

    我们已经有了函数式组件了。🎉 接下来我们给组件添加state。

    我们从一个新栗子开始。

    Copy
    function Counter() {
        const [state, setState] = Myact.useState(1)
        return ( <
            h1 onClick = {
                () => setState(c => c + 1)
            } >
            Count: {
                state
            } <
            /h1>
        )
    }
    const element = < Counter / >
        const container = document.getElementById("root")
    Myact.render(element, container)

    为Myact增加了一个useState hook。

    Copy
    const Myact = {
      createElement, 
      render, 
      useState, 
    }
    
    function useState(initial) {
      // TODO
    }
    

    初始化一些全局变量供useState函数使用。 首先设置当前工作中的fiber。然后为该fiber添加一个hook数组,以支持同意组件多次调用useState函数。

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

    当函数式组件调用useState时,通过fiber身上的alternate去判断是否有旧hook:fiber.alternate.hooks[hookIndex],有则需要使用初始化状态,反之不需要。 然后我们向fiber上添加新hook,hook下标增加,返回状态。

    Copy
    function useState(initial) {
        const oldHook =
            wipFiber.alternate &&
            wipFiber.alternate.hooks &&
            wipFiber.alternate.hooks[hookIndex]
        const hook = {
            state: oldHook ? oldHook.state : initial,
        }​
        wipFiber.hooks.push(hook)
        hookIndex++
        return [hook.state]
    }

    Kou ShiXiang

    一个记录知识和生活的神秘小空间