自己造React系列——为什么在列表组件中需要指定唯一Key

开始

前面我们只完成了 添加 东西到 DOM 上这个操作,那么更新和删除 node 节点呢?

我们还需要比较 render 中新接收的 element 生成的 fiber 树和上次提交到 DOM 的 fiber 树。

这里需要保存”上次提交到 DOM 节点的 fiber 树” 的”引用”(reference)。我们称之为 currentRoot

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)
    }
    
    domParent.appendChild(fiber.dom)
    commitWork(fiber.child)
    commitWork(fiber.sibling)
    
}

在每一个 fiber 节点上添加 alternate 属性用于记录旧 fiber 节点(上一个 commit 阶段使用的 fiber 节点)的引用。

function render(element, container) {

    wipRoot = {
        dom: container,
        props: {
            children: [element],
        },
        // 记录上一次的fiber 节点
        alternate: currentRoot,
    }
    deletions = []
    nextUnitOfWork = wipRoot
}

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

requestIdleCallback(workLoop)

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

        // TODO compare oldFiber to element
        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) {
            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
    }
}

把 performUnitOfWork 中创建新 fiber 节点的代码抽出来

扔到 reconcileChildren 函数中。


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

这个函数会调和(reconcile)旧的 fiber 节点 和新的 react elements。

在迭代整个 react elements 数组的同时我们也会迭代旧的 fiber 节点(wipFiber.alternate)。

如果我们忽略掉同时迭代数组和对应的link中的一些标准模板,我们就剩下两个最重要的东西: oldFiber 和 elementelement 是我们想要渲染到 DOM 上的东西,oldFiber 是我们上次渲染 fiber 树.

我们需要比较这两者之间的差异,看看需要在 DOM 上应用哪些改变。

以下是比较的步骤:

  • 对于新旧节点类型是相同的情况,我们可以复用旧的 DOM,仅修改上面的属性
  • 如果类型不同,意味着我们需要创建一个新的 DOM 节点
  • 如果类型不同,并且旧节点存在的话,需要把旧节点的 DOM 给移除

React中使用Key来优化reconciliation 过程

React使用 key 这个属性来优化 reconciliation 过程。比如, key 属性可以用来检测 elements 数组中的子组件是否仅仅是更换了位置。

当新的 element 和旧的 fiber 类型相同, 我们对 element 创建新的 fiber 节点,并且复用旧的 DOM 节点,但是使用 element 上的 props。

我们需要在生成的fiber上添加新的属性:effectTag。在 commit 阶段(commit phase)会用到它。

对于需要生成新 DOM 节点的 fiber,我们需要标记其为 PLACEMENT

对于需要删除的节点,我们并不会去生成 fiber,因此我们在旧的fiber上添加标记。

但是当我们提交(commit)整颗 fiber 树(wipRoot)的变更到 DOM 上的时候,并不会遍历旧 fiber。

因此我们需要一个数组去保存要移除的 dom 节点。

之后我们提交变更到 DOM 上的时候,也需要把这个数组中的 fiber 的变更(其实是移除 DOM)给提交上去。

现在,我们对 commitWork 函数略作修改来处理我们新添加的 effectTags

如果 fiber 节点有我们之前打上的 PLACEMENT 标,那么在其父 fiber 节点的 DOM 节点上添加该 fiber 的 DOM。(有点拗口)

相反地,如果是 DELETION 标记,我们移除该子节点。

如果是 UPDATE 标记,我们需要更新已经存在的旧 DOM 节点的属性值。

我们把上述操作封装在 updateDom 函数中。

function updateDom(dom, prevProps, nextProps) {

// TODO

}

比较新老 fiber 节点的属性, 移除、新增或修改对应属性。

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) {

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

比较特殊的属性值是事件监听,如果属性值以 “on” 作为前缀,我们需要以不同的方式来处理这个属性。

    const isEvent = key => key.startsWith("on")
    const isProperty = key => key !== "children" && !isEvent(key)

对应的监听事件如果改变了我们需要移除旧的。

并且添加新的。

在线调试

全部评论

相关推荐

评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务