8月更文挑战 | react手写实现渲染虚拟结构Fiber版

570 阅读5分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

前言

本次主要是接上一篇渲染JSX普通版本也是react16之前的渲染方式,今天来实现下一个版本使用fiber链表结构的方式渲染虚拟dom,上一篇地址:juejin.cn/post/699985…

介绍

fiber是reactV16版本发布的一个核心架构来替换以前的渲染方式,那么就有几个问题为什么会出现fibe?fiber解决了什么问题?目前现有的fiber架构还存在其他问题吗?我们依次解答之后在手写一版fiber结构,渲染我们JSX之后大家可以体验一下

1、为什么会出现fiber?
因为在V16版本以前的架构方式在渲染结构包括在更新过程是没有优先级区分的,那么也就是说当你进行一次更新的时候他是从0到1渲染完成中间并不会因为那个优先级高就停顿去渲染那个(因为js是单线程的没得办法🤔),这样势必有些问题如果有些操作需要更高的优先级但是却没有及时的渲染,那么对于用户看到的体验感就非常差例如渲染动画可能会有一些卡顿的感觉,我们来看个图

WechatIMG16.png

2、fiber解决了什么问题?
v16版本之后采用fiber链表的方式来渲染结构,实际原理是把每一个耗时的任务进行分片,每一个分片运行时间很短(这也是常说的时间分片的原理),运行完成之后会把任务交给reconcile模块进行调和实际就是区分接下来任务执行的优先级这样在渲染的过程中看起来就很丝滑

WechatIMG17.png

3、目前现有的fiber架构还存在其他问题吗?
我认为还是存在其他问题的像vue架构在进行计算渲染查找的时候使用的双端查找的方式效率上是比较高的,而当前的fiber使用的单向链表在diff速度上还是有待提升的,但是每个框架或者是库都有自己的优劣相信以后会越来越完善的

开始

开始编写fiber架构,第一步还是创建一个基础架子使用官方提供的脚手架即可,和以前一样我会在代码中写入注释如果有哪里不够清楚或者有问题可以在评论区讨论

创建根目录index.js

import React from 'react';
import ReactDOM from './react-dom'
import Component from './Component'
import './index.css' //样式就一个border就不贴了class ClassComponent extends Component {
  render() {
    return (
      <div className="box">
        <p>{this.props.name}</p>
      </div>
    );
  }
}
​
function FunctionComponent(props) {
  return (
    <div className="box">
      <p>{props.name}</p>
    </div>
  );
}
​
//这是我们要渲染的结构,目的能正常渲染到页面就ok
const JSX = (
  <div className='box'>
    <h1>飞雪连天射白鹿,笑书神侠倚碧鸳</h1>
    <a href='https://juejin.cn/post/6995918802546343973' >my world</a>
    <FunctionComponent name='函数组件' />
     <ClassComponent name='类组件'/>
  </div>
)
​
ReactDOM.render(
  JSX,
  document.getElementById('root')
);

类需要继承的Component

function Component(props) {
  this.props = props;
}
​
Component.prototype.isReactComponent = {}; //需要区分是class类组件还是函数组件
export default Component;
​

到这里准备工作已经做好开始写我们的主要逻辑react-dom

render.js

//vnode 虚拟dom节点
//node 真实dom节点
  
/**
 * filber结构
 * child 子节点
 * sibling 兄弟节点
 * retur 父节点
 * stateNode 指向真实dom节点
 * 
*/let nextUnitOfWork = null //下一个要执行的任务
let wipRoot = null // 当前正在执行的任务
​
​
function render(vnode, container) {
    //初始结构
    wipRoot = {
        type: 'div',
        props: { children: { ...vnode } },
        stateNode: container
    }
  
    nextUnitOfWork = wipRoot //当前任务
}
​

workLoop获取浏览器空闲时间执行任务,但是在源码中是有自己的一套分片时间规则

/**
*@IdleDeadline 可以浏览器空闲时间
*/
function workLoop(IdleDeadline) {
    //有任务并且有空闲时间的情况
    while (nextUnitOfWork && IdleDeadline.timeRemaining() > 1) {
        //执行每个任务
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    }
​
    //最后提交任务
    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }
}
​
//利用浏览器空闲时间
window.requestIdleCallback(workLoop)

performUnitOfWork执行任务&返回下一个要执行的任务

/**
*@workInProgress 当前正在执行的任务
*/
function performUnitOfWork(workInProgress) {
    //1.执行当前任务
    const { type } = workInProgress
  
    if (typeof type === 'string') {
        updateHostComponent(workInProgress)
    } else if (typeof type === 'function') {
        //区分函数组件还是类组件特殊处理
        type.prototype.isReactComponent ? updateClassComponent(workInProgress) :
            updateFunComponent(workInProgress)
    }
​
    //2.返回下一个任务这里大概的流程是如果兄弟节点没有就向上找父节点的兄弟节点
    while (workInProgress.child) {
        //如果子节点存在直接返回
        return workInProgress = workInProgress.child
    }
​
    let nextFiber = workInProgress
    while (nextFiber) {
        //如果当子节点不存在就找兄弟节点
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }
        //寻找父节点
        nextFiber = nextFiber.return
    }
​
}

updateHostComponent标签节点

function updateHostComponent(workInProgress) {
    if (!workInProgress.stateNode) {
        //首次渲染没有dom节点需要生成
        workInProgress.stateNode = createNode(workInProgress)
    }
    //协调子节点一会在下面看
    reconcileChildren(workInProgress, workInProgress.props.children)
}

updateClassComponent生成类组件节点

function updateClassComponent(workInProgress) {
    const { type, props } = workInProgress
    //类组件需要实例化才可以获取到vnode
    const instance = new type(props)
    const child = instance.render()
    //协调子节点
    reconcileChildren(workInProgress, child)
}

updateFunComponent生成函数式组件节点

function updateFunComponent(workInProgress) {
    const { type, props } = workInProgress
    //执行函数获取vnode
    let child = type(props)
      
    //协调子节点
    reconcileChildren(workInProgress, child)
}

核心结构reconcileChildren生成fiber结构

/**
*@workInProgress 当前正在执行的任务
*@children vnode
*///如果是字符串或者数字都当是文本
function isStringOrNumber(type) {
    return typeof type === 'string' || typeof type === 'number'
}
​
function reconcileChildren(workInProgress, children) {
​
    if (isStringOrNumber(children)) {
        //如果是文本节点直接退出不需要在生成fiber
        return
    }
    
    //为了方便遍历组件
    const newChildren = Array.isArray(children) ? children : [children]
​
    let prevNewFiber = null //记录上一轮fiber
    for (let i = 0; i < newChildren.length; i++) {
        let child = newChildren[i]
        //生成fiber对象
        let newFilber = {
            key: child.key,
            type: child.type,
            props: { ...child.props },
            stateNode: null,
            child: null,
            sibling: null,
            return: workInProgress
        }
        if (i === 0) {
            //newFilber 是 workInProgress的第一个子fiber
            workInProgress.child = newFilber
        } else {
            prevNewFiber.sibling = newFilber
        }
        prevNewFiber = newFilber
    }
}

commitRoot为了防止重复提交

function commitRoot() {
    commitWorker(wipRoot.child)
    //防止重复提交
    wipRoot = null
}

commitWorker提交任务

/**
*@workInProgress 当前正在执行的任务
*/
function commitWorker(workInProgress) {
    if (!workInProgress) {
        //自己不存在直接返回
        return
    }
  
    //获取到父节点,因为要将当前任务插入到父节点
    let parentNodeFiber = workInProgress.return
​
    while (!parentNodeFiber.stateNode) {
        //向上找父节点主要是为了类组件和函数组件
        parentNodeFiber = parentNodeFiber.return
    }
​
    let parentNode = parentNodeFiber.stateNode
  
    if (workInProgress.stateNode) {
        //开始插入节点
        parentNode.appendChild(workInProgress.stateNode)
    }
​
    //提交自己
    commitWorker(workInProgress.child)
    //提交兄弟节点
    commitWorker(workInProgress.sibling)
}

到这里代码就全部完成了,我们来看一下页面的效果如下图说明就成功了

WechatIMG18.png

流程图

我们还是画个流程图帮助大家梳理一下主要逻辑

WechatIMG19.png

结束

本篇幅到这里就结束了,如果大家觉得有帮助的话点个赞吧,谢谢☕️