从零到一:打造你的Mini-React (三)

简介

在本篇文章中,我们将一起探索如何从零构建一个迷你版的React框架。通过这个过程,不仅能够加深对React内部机制的理解,还能学习到虚拟DOM、组件生命周期和状态管理等核心概念的实际应用。这篇文章旨在深入浅出地理解React工作原理。


问题清单:


在上一节中,我们完成了分块渲染和批量提交的功能。接下来,我们将进行事件绑定和数据更新。

事件绑定

Q1:怎么处理事件绑定?

首先,让我们更新测试用例,加入一个新的事件。

jsx 复制代码
// App.jsx

function Foo({ num }) {
  function handleClick() {
    console.log('click')
  }
  return (
    <div>
      {num}
      <div>
        <button onClick={handleClick}>click</button>
      </div>
    </div>
  )
}

function App() {
  return (
    <div>
      <Foo num={10} />
    </div>
  )
}

到目前为止,我们还未处理该事件。我们将在updateProps中添加事件处理逻辑。如你所见,我们已经完成了事件的绑定。

jsx 复制代码
// React.js

// 省略...

function updateProps(dom, props) {
    const isEvent = (key) => key.startsWith('on')
    const isProperty = (key) => key !== 'children' && !isEvent(key)
    Object.keys(props)
        .filter(isProperty)
        .forEach((name) => {
            dom[name] = props[name]
        })
    Object.keys(props)
        .filter(isEvent)
        .forEach((name) => {
            const eventType = name.toLowerCase().substring(2)
            dom.addEventListener(eventType, props[name])
        })
}

数据更新

数据更新时,如何知道哪些元素需要更新呢?

这时候就需要diff来对新旧两个Fiber树进行差异比较。

我们需要做到的是:

  • 记录新旧两个Fiber节点
  • 执行Diff算法
    • 同级type相同
    • 同级type不同
  • 根据Diff结果更新页面

Q2:如何实现React的数据更新?

jsx 复制代码
// React.js

function commitRoot() {
    commitWork(workInProcessRoot.child)
    // 记录当前的fiber树  oldFiber
    currentRoot = workInProcessRoot
    workInProcessRoot = null
}

// 增加alternate属性,用于保存上一次的fiber树
function update() {
    workInProcessRoot = {
        dom: currentRoot.dom,
        props: currentRoot.props,
        alternate: currentRoot
    }
    nextUnitOfWork = workInProcessRoot
}

// 执行diff
function reconcileChildren(wipFiber, children) {
    let prevSibling = null
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child
    let index = 0
    while (index < children.length) {
        // 对比旧的fiber和新的children
        // effectTag用于标记fiber节点的操作类型
        const element = children[index]
        const isSameType = oldFiber && element && element.type === oldFiber.type
        let newFiber = null
        if (isSameType) {
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                parent: wipFiber,
                dom: oldFiber.dom,
                alternate: oldFiber,
                effectTag: 'UPDATE'
            }
        } else {
            newFiber = {
                type: element.type,
                props: element.props,
                parent: wipFiber,
                dom: null,
                alternate: null,
                effectTag: 'PLACEMENT'
            }
        }
        if (oldFiber) {
            //  下一次循环中找到oldFiber的兄弟节点
            oldFiber = oldFiber.sibling
        }

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

        prevSibling = newFiber
        index++
    }
}

// commitWork来处理更新还是创建
function commitWork(fiber) {
    if (!fiber) {
        return
    }
    let fiberParent = fiber.parent
    while (!fiberParent.dom) {
        fiberParent = fiberParent.parent
    }

    if (fiber.dom) {
        if (fiber.effectTag === 'PLACEMENT') {
            fiberParent.dom.appendChild(fiber.dom)
        } else if (fiber.effectTag === 'UPDATE') {
            // 将新旧节点的props传入
            updateProps(fiber.dom, fiber.alternate.props, fiber.props)
        }
    }
    commitWork(fiber.child)
    commitWork(fiber.sibling)
}



function updateProps(dom, prevProps, nextProps) {
    const isEvent = (key) => key.startsWith('on')
    const isProperty = (key) => key !== 'children' && !isEvent(key)
    // 判断属性是否更新
    const isNew = (prev, next) => (key) => prev[key] !== next[key]
    // 判断属性是否被删除
    const isGone = (next) => (key) => !(key in next)

    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])
        })
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(nextProps))
        .forEach((name) => {
            // 为什么不是 dom.removeAttribute(name)?
            // 因为removeAttribute会移除所有属性,包括value,checked等
            dom[name] = ''
        })
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach((name) => {
            dom[name] = nextProps[name]
        })
    Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach((name) => {
            const eventType = name.toLowerCase().substring(2)
            dom.addEventListener(eventType, nextProps[name])
        })
}

到此为止,我们已经完成了数据更新。但实际上,在使用过程中,我们并不是这样定义变量的。如果采用类似React的方式,应该如何实现呢?

useState

当然,在日常使用中,我们通常不是用update来更新变量,而是这样创建变量的。

jsx 复制代码
import React from './core/React'

function Foo({ num }) {
  const [count, setCount] = React.useState(0)
  const [foo, setFoo] = React.useState(111)
  function handleClick() {
    setCount((c) => c + 1)
    setFoo(222)
  }
  return (
    <div>
      <div>Num: {num}</div>
      <div>Foo: {foo}</div>
      <div>Count: {count}</div>
      <div>
        <button onClick={handleClick}>click</button>
      </div>
    </div>
  )
}

function App() {
  return (
    <div>
      <Foo num={10} />
    </div>
  )
}

export default App

Q3:如何实现useState?

需求分析:

  • useState 是一个函数,它接收状态的初始值作为参数,返回一个数组。
  • 返回的数组包含两个元素:当前的状态值和一个更新该状态的函数。
  • 更新函数(例如上面的 setCount)可以被用来修改状态值,当状态值被修改时,组件将重新渲染。
  • useState 可以在一个组件中多次被调用,以创建多个状态变量。
jsx 复制代码
// React.js

// 保存state的hook
let stateHooks = null
// 保存当前的hook索引
let currentHookIndex = 0
function useState(initial) {
    let currentFiber = workInProcessRoot
    // 查找是否有上一次的fiber树
    const oldHook = currentFiber.alternate?.stateHooks[currentHookIndex]
    const stateHook = {
        state: oldHook ? oldHook.state : initial
    }
    currentHookIndex++
    stateHooks.push(stateHook)

    currentFiber.stateHooks = stateHooks

    function setState(newState) {
        // 处理 setState(newState) 和 setState((state) => newState) 两种情况
        const isFunction = typeof newState === 'function'
        stateHook.state = isFunction ? newState(stateHook.state) : newState
        update()
    }
    return [stateHook.state, setState]
}

function commitRoot() {
    commitWork(workInProcessRoot.child)
    currentRoot = workInProcessRoot
    workInProcessRoot = null
    // 重置hook
    stateHooks = null
    currentHookIndex = 0
}

总结

我们成功实现了事件绑定和数据更新机制。通过这一过程,我们深入理解了React框架下如何将事件监听器附加到DOM元素上,并且在Fiber架构中引入了一个专门的变量用于存储组件状态。同时,我们也提供了一个setState函数,它能够有效触发并管理状态的更新。这些成果不仅展示了React的交互性特点,还强调了状态管理对于构建响应式用户界面的重要性。

相关推荐
2301_7665360528 分钟前
调试无痛入手
开发语言·前端
@大迁世界2 小时前
构建 Next.js 应用时的安全保障与风险防范措施
开发语言·前端·javascript·安全·ecmascript
IT、木易3 小时前
ES6 新特性,优势和用法?
前端·ecmascript·es6
青茶绿梅*23 小时前
500字理透react的hook闭包问题
javascript·react.js·ecmascript
计算机软件程序设计3 小时前
vue和微信小程序处理markdown格式数据
前端·vue.js·微信小程序
指尖时光.3 小时前
【前端进阶】01 重识HTML,掌握页面基本结构和加载过程
前端·html
前端御书房3 小时前
Pinia 3.0 正式发布:全面拥抱 Vue 3 生态,升级指南与实战教程
前端·javascript·vue.js
NoneCoder3 小时前
JavaScript系列(84)--前端工程化概述
前端·javascript·状态模式
晚安7203 小时前
idea添加web工程
java·前端·intellij-idea
零凌林4 小时前
vue3中解决组件间 css 层级问题最佳实践(Teleport的使用)
前端·css·vue.js·新特性·vue3.0·teleport