react源码解析之react-dom

887 阅读6分钟

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

虚拟dom

什么是虚拟dom

虚拟dom 是一种编程概念,在这个概念里,UI以一种理想化的,或者说‘虚拟的’表现形式被保存于内存中,并通过如ReactDom等类库使之与‘真实的’DOM同步,这一过程叫做协调

用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对 象结构。这个 JavaScript 对象称为virtual dom

为什么使用虚拟dom

DOM操作很慢,轻微的操作都可能导致页面重新排版,非常耗性能。相对于DOM对象,js对象 处理起来更快,而且更简单。通过diff算法对比新旧vdom之间的差异,可以批量的、最小化的执行 dom操作,从而提高性能。

jsx

什么是JSX

  • 语法糖
  • React 使用 JSX 来替代常规的 JavaScript。
  • JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。

为什么需要JSX

  • 开发效率:使用 JSX 编写模板简单快速。
  • 执行效率:JSX编译为 JavaScript 代码后进行了优化,执行更快。
  • 类型安全:在编译过程中就能发现错误。

react 16和react 17使用区别

React 16原理:babel-loader会预编译JSX为React.createElement(...)

React 17原理:React 17中的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。另外此次升级不会改变 JSX 语法,旧的 JSX 转换 也将继续工作

fiber

  • 处理时间切片
  • 链表结构的js对象
  • 对象里的一些关键key

fiber 的一些关键key

fiber{
    stateNode:当前的dom 节点
    tag:在react 中定义的节点值
    type:字符串,标记类型,eg:'div'
    child:dom 节点里的第一个子节点
    sibling:下一个兄弟节点,
}

ReactDOM渲染的使用方法

  • jsx :直接渲染,
  • 函数组件:直接执行函数,
  • 类组件:会实例化一下,然后调用render()函数

判断组件是类组件还是函数组件 Component.prototype.isReactComponent = {}

手动实现reactDom

通过create-react-app 创建一个文件,打开index.js文件

import ReactDOM from './kreact/react-dom';
const jsx=(
  <div>
    <h1>learn react</h1>
    <p>k-react</p>
     <a href='https://learn.kaikeba.com/video/411409'>this is a link</a>
    <FunctionComponent name='functionComponent' />
    <>
    <p>Fragments</p>
    </>
    <App name='react component'/> 
 
  </div>
)


ReactDOM.render(
  jsx,
  document.getElementById('root')
);

创建kreact 文件,创建react-dom js文件。

ReactDOM.render()

ReactDOM.render(element, container[, callback])

当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 DOM 差分 算法(DOM diffing algorithm)进行高效的更新。

如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。

主要实现:返回一个render 函数

import {scheduleUpdateOnFiber} from './ReactFiberWorkloop';

function render(element, container) {
  console.log('react-dom', element);
  console.log('container', container.nodeName);
  // 设置根fiber 节点
  const FiberRoot = {
    type: container.nodeName.toLowerCase(),
    props: {children: element},
    stateNode: container,
  };
  scheduleUpdateOnFiber(FiberRoot);
}

export default {render};

创建一个reactFiberworkloop js 文件 处理节点的操作 react 里是根据Scheduler(调度器),目的是当浏览器有剩余时间时通知我们。 这里我们通过浏览器api ,window.requestIdleCallback(callback[, options]) ,可以返回浏览器的空闲时间来实现

react为何不使用requestIdleCallback

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低
import {isStr} from './utils';

import {updateHostCompose} from './ReactFiberReconclier';

// 设置节点更新 之后要根据更新好的fiber 节点再去更新dom 节点

let wipRoot = null; // 当前正在工作中的根节点
let nextUnitOfWork = null; // 下一个fiber 节点


export function scheduleUpdateOnFiber(fiber) {
  wipRoot = fiber;
  nextUnitOfWork = wipRoot; // 从根fiber 开始更新
}

//处理节点更新
function workloop(IdleDeadline) {
  while (nextUnitOfWork && IdleDeadline.timeRemaining() > 0) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
}

// 当浏览器有空闲时间时,去处理节点的更新
requestIdleCallback(workloop);

// performUnitOfWork目的
 //  1 更新wip
  //2 返回下一个要更新的任务
function performUnitOfWork(wip) {
  const {type} = wip;
  if (isStr(type)) {
    // 代表是原生标签
    updateHostCompose(wip);
  }
  // 返回下一个要更新的任务,深度优先遍历
  if (wip.child) {
    return wip.child;
  }
  // 没有子节点,要找兄弟节点,若没有兄弟节点,再找父的兄弟节点,一直往上找,直到找到树的最顶层节点
  let next = wip;
  while (next) {
    if (next.sibling) {
      // sibling 兄弟节点
      return next.sibling;
    }
    next = next.return; // next 的父节点 通过next.return 获得的
  }
  return null;
}

不同组件的更新方式不同,创建ReactFiberReconclier.js文件

import { isStr,isArray} from "./utils";
import {createFiber} from './createFiber'

// 处理节点上的属性值
export function updateNodeOne(node,nextVal) {
    Object.keys(nextVal).forEach(k=>{
        if(k === 'children'){
            if(isStringOrNumber(nextVal[k])) {
                node.textContent = nextVal[k];
            }
        }else if(k.slice(0,2) === 'on'){
            const eventName = k.slice(2).toLocaleLowerCase();
        node.addEventListener(eventName, nextVal[k]);
        }else{
            node[k] = nextVal[k];
        }
    })
}


// 更新原生标签
export function updateHostCompose(wip){
    if(!wip.stateNode){    // 初次渲染,还没有真实的dom
        wip.stateNode = document.createElement(wip.type);
        updateNodeOne(wip.stateNode,wip.props); //处理节点上的属性值
      
    }
    // 协调子节点,就是diff 
    reconcileChildren(wip,wip.props.children);
}

// 更新函数组件
export function updateFunctionCompose(wip){
    // 函数的children 是函数组件调用,返回的值
    const {type,props} = wip;
    const children = type(props);
    // 协调子节点,就是diff 
   reconcileChildren(wip,children);
  updateNodeOne(wip.stateNode,wip.return.props); //处理节点上的属性值
  
}

// 更新类组件
export function updateClassCompose(wip){
    // 函数的children 是函数组件调用,返回的值
    const {type,props} = wip;
    const instance = new type(props);
    const children = instance.render();
    // 协调子节点,就是diff 
    reconcileChildren(wip,children);
 //   updateNodeOne(wip.stateNode,wip.return.props); //处理节点上的属性值
  
}

// 更新fragment
export function updateFragmentsCompose(wip){
    // 协调子节点,就是diff 
    console.log('======45',wip)
   reconcileChildren(wip,wip.props.children);
}


 // 处理子节点
function reconcileChildren(returnFiber,children) {
    //returnFiber父节点,children 子节点
    let  previousNewFiber = null;
    if(isStr(children)){
        // 若是文本节点
        return;
    }
    const newChildren = isArray(children) ? children :[children];
    for(let i =0;i<newChildren.length;i++){
        const newChild = newChildren[i];
        const newFiber = createFiber(newChild,returnFiber)
        if(previousNewFiber === null){
            // 上一个节点不存在,说明是初始节点
            returnFiber.child = newFiber
        } else {
            previousNewFiber.sibling = newFiber;
            }
            previousNewFiber= newFiber;  
    }

}

按照fiber结构创建节点

import { Placement } from "./utils";

export function createFiber(vnode,returnFiber){
/**
* fiber:
* type 标记节点类型
* key 标记节点在当前层级下的唯一性
* props 属性
* index 标记当前层级下的位置
* child 第一个子节点
* sibling 下一个兄弟节点
* return 父节点
* stateNode 如果组件是原生标签则是dom节点,如果是类组件则是类实例
*/

    const newFiber={
        type: vnode.type,
        key:vnode.key,
        props:vnode.props,
        stateNode:null,
        child:null,
        return:returnFiber,
        sibling:null,
        alternate:null,
        flags:Placement , //  当前fiber 要做什么事情,eg 插入,删除,更新呀
    }
    return newFiber
}

总结

ReactDOM 实现路径

  • render 函数

  • scheduleUpdateOnFiber :定义节点

  • workloop:浏览器空闲时间对节点进行处理 requestIdleCallback(workloop)

  • performUnitOfWork:更新节点,返回下一个节点,节点分为不同的类型(原生,函数,类组件)

  • updateHostCompose: 更新原生节点

  • createFiber: 改造节点,按照fiber 的格式

  • 节点改造完成,没有下一个子节点,并且wipRoot 存在,说明可以提交节点了 ,要进行提交节点

  • commitRoot:提交节点

  • commitWork(wipRoot.child) // 从子节点开始提交,自己提交自己,提交子节点,提交兄弟节点