likes
comments
collection
share

mini-react 实现实践

作者站长头像
站长
· 阅读数 42

背景

最近参加了mini-react小训练,参加的原因是想看看别人是怎么思考的,对比下,做个参照。然后,最近也在面试,也有些帮助吧。

文章内容

  1. 从直观感受开始,0起步实现以下api;
ReactDOM.createRoot(document.getElementById('root')).render(App)
  1. 实现react的fiber,以及简单的任务调度
  2. 实现集中commit和函数组件
  3. 实现简单的dom更新和children的初步更新,新增和删除
  4. useState和useEffect的实现
  5. 面试问react原理要怎么回答

createRoot和render的初步实现

我们的大概脑子里的印象是,react整个过程可以大概分为render和diff两个过程,后面有了调度,所以 就有了,render,调度,和diff,这三个阶段。这里的render只是执行react的render的函数执行过程,并没有包括浏览器渲染到页面的过程,也就是paint的过程。

所以我们要实现的render就是可以输出,可以diff的东西,这个东西就是react element了。

fun(参数)->react element->fiber

因为是第一步,fiber我们忽略,通过react element 来更新dom

const createTextNode = (str) => {
  return {
    type: 'text',
    props: {
      nodeValue: str,
    },
  }
}

const createElement = (type, props, ...children) => {
  return {
    type,
    props: {
      ...props,
    },
    children: children.map((child) => {
      const isText = typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean'
      return isText ? createTextNode(child) : child
    }),
  }
}

同时提供一个React.dom来封装render方法

import React from './React.js'

const ReactDOM = {
  createRoot: (container) => {
    return {
      render: (el) => {
        React.render(el, container)
      },
    }
  },
}

export { ReactDOM }

render通过调用create函数来更新dom,这里不再写了。

fiber和任务调度

为了模仿真实的react,所以就要实现fiber,之前更新dom是通过不断递归树的结构来,如果节点很多,会阻塞浏览器渲染过程,fiber有多兄弟和父母的指针管理,通过这些指针可以知道,遍历到哪个节点,就可以把每个节点当成每一个任务去执行,通过requestIdleCallback来调度。

fiber的关键属性有
{
  dom:null,
  parent:null,
  sibling:null,
  child:null,
  props:null
}

//添加react element 转fiber的过程

fiber={...elemnt,dom,parent,sibling,child}

let needCommit = true
function workLoop(deadline) {
  let shouldYaild = true
    // workgress是每个fiber,把每个fiber当成任务
  while (shouldYaild && workgress) {
    shouldYaild = deadline.timeRemaining() > 1
    workgress = performanceUnit()
    console.log('////', '000')
    if (!workgress) needCommit = true
  }

  requestIdleCallback(workLoop)
}
调度
requestIdleCallback(workLoop)




//每个任务
const performanceUnit = () => {
  while (workgress) {
    workgress = begainWork(workgress)
  }
  return workgress
}

begainWork里初始化dom

实现集中commit和函数组件

requestIdleCallback是不太适合,直接去改dom的,所以,我们在每个任务里去更新dom,所以,在fiber我们要知道,这个dom要怎么处理要添加标识,并且放到requestAnimationFrame去处理

function workLoop(deadline) {
  let shouldYaild = true

  while (shouldYaild && workgress) {
    shouldYaild = deadline.timeRemaining() > 1
    workgress = performanceUnit()
    console.log('////', '000')
    if (!workgress) needCommit = true
  }

  if (!workgress && needCommit) {
    console.log('commit')
    requestAnimationDom(rootFiber)
    needCommit = false
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)


const requestAnimationDom = function (fiber) {
  requestAnimationFrame(function () {
    commitRoot(fiber)
    effectActions(fiber)
  })
}

const commitLoopFiber = (fiber) => {
  if (!fiber) return
  fiber.dom = updateDomProps(fiber, fiber.dom)
  if (fiber.dom) {
    let parent = fiber.parent
    while (!parent.dom) {
      parent = parent.parent
    }
    if (fiber.effectTags === 'update') {
    } else {
      parent.dom.append(fiber.dom)
    }
  }
  commitLoopFiber(fiber.headChildFiber)
  commitLoopFiber(fiber.sbling)
}

const commitRoot = (root) => {
  commitLoopFiber(root.headChildFiber)
}


const updateDomProps = (fiber, dom) => {
  if (!dom) {
    Object.keys(fiber.props).forEach((key) => {
      if (key.startsWith('on')) {
        fiber.headChildFiber.props[key] = fiber.props[key]
      }
    })
    return null
  }

  const props = fiber.props || Object.create(null)
  Object.keys(props).forEach((key) => {
    if (key.startsWith('on')) {
      const evenName = key.slice(2).toLowerCase()
      dom.removeEventListener(evenName, fiber.alternate?.props[key])
      dom.addEventListener(evenName, props[key])
    } else if (key === 'style') {
      const styles = props.style
      Object.keys(props.style).forEach((styleKey) => {
        dom.style[styleKey] = styles[styleKey]
      })
    } else {
      dom[key] = props[key]
    }
  })
  let deletes = fiber.deletes
  if (deletes) {
    deletes.forEach((childDom) => {
      childDom.parentNode.removeChild(childDom)
    })
    deletes = null
  }
  return dom
}

关于函数组件,React.createElement会对方法函数赋值个这个element的type 所以只要执行这个type就能知道这个方法函数的要渲染的节点了。 这里要做的就是: 1、方法函数代表的fiber是没有dom的,所以与它相关的操作,要根据逻辑引向它的父节点,和第一个孩子节点。比如删除节点的dom操作

实现简单的dom更新和children的初步更新,新增和删除

  const children = fiber.children || new Array()
  let preChild = Object.create(null)
  let oldChildFiber = fiber.alternate?.headChildFiber
  let childFiber = null
  children.forEach((child, index) => {
    if (isSameType(child, oldChildFiber)) {
      childFiber = {
        ...child,
        dom: oldChildFiber?.dom,
        parent: fiber,
        headChildFiber: null,
        effectTags: 'update',
        alternate: oldChildFiber,
      }
    } else {
      childFiber = { ...child, dom: null, sbling: null, parent: fiber, headChildFiber: null }
      if (oldChildFiber) {
        addDelets(fiber, oldChildFiber)
        childFiber.effectTags = 'placement'
      }
    }
    preChild.sbling = index === 0 ? null : childFiber
    if (index === 0) fiber.headChildFiber = childFiber
    preChild = childFiber
    oldChildFiber = oldChildFiber?.sbling
    //   render(child, dom)
  })
  while (oldChildFiber) {
    addDelets(fiber, oldChildFiber)
    oldChildFiber = oldChildFiber.sbling
  }
  return children
}
  1. useState和useEffect的实现
let effectStates = []
let effectStateIndex = 0
const useState = (initialValue) => {
  const currentFiber = workgress
  const oldFiber = workgress.alternate
  console.log('??')
  let effect = {
    state: oldFiber?.effectStates[effectStateIndex].state || initialValue,
    quque: oldFiber?.effectStates[effectStateIndex].quque || [],
  }
  effectStateIndex++
  effect.quque.forEach((cb, index) => {
    effect.state = cb(effect.state)
  })
  effect.quque = []
  effectStates.push(effect)

  currentFiber.effectStates = effectStates

  const setState = function (callback) {
    const cb = typeof callback === 'function' ? callback : () => callback
    if (cb(effect.state) === effect.state) return
    effect.quque.push(cb)
    // effect.state = cb(effect.state)
    console.log(effectStateIndex)
    workgress = {
      ...currentFiber,
      alternate: currentFiber,
    }
    rootFiber = workgress
  }

  return [effect.state, setState]
}

let effectsHooks = []
let effectsHooksIndex = 0
const useEffect = (callback, deps) => {
  const currentFiber = workgress

  const effect = {
    deps,
    callback,
    cleanUp: currentFiber?.alternate?.effectsHooks[effectsHooksIndex]?.cleanUp,
  }

  effectsHooks.push(effect)
  effectsHooksIndex++
  currentFiber.effectsHooks = effectsHooks
}
  1. 面试问react原理要怎么回答

包括JSX、Diffing、Fiber、Time Slice 所以对这四个内容进行描述:可能会部分或者全部进行细化提问 1、为什么react要用jsx? 2、jsx是怎么转成fiber,在什么时候转到 3、react的diff是怎么做的。 4、react是怎么时间分片的,它是怎么调度的。 5、render函数是什么原理 等。

进行细化描述

转载自:https://juejin.cn/post/7352387558746210356
评论
请登录