React 源码学习

基础源码编写

一:render的基础实现

将index.js 文件替换成自己实现的react

import reactDom from './cwhReact/react-dom'
import Component from './cwhReact/component'

function FuncComponent (props){
  return <div className='func'>
    <p>这是函数组件:{props.name}</p>
  </div>
}

class ClassComponent extends Component{
  render(){
    return <div className='class'>
    <p>这是类组件:{this.props.name}</p>
  </div>
  }
}

const vnode = (
  <div className="App">
    <div  className="father">
      <div className='son'>
        son
      </div>
    </div>
    <h1>h1 - 文本</h1>
    <a href='https://www.baidu.com'>a - 链接</a>

    <FuncComponent name='function'/>

    <ClassComponent name='class'/>
  </div>
);

 reactDom.render(vnode,document.getElementById('root'));

在react17中,默认会将文中的vnode转换为jsx对象,其数据结构为

vnode:{

    // Symbol标识当前节点,为了防止 XSS 攻击,使用 Symbol 类型是因为 JSON 中无法传递 Symbol。            
    // React 会检查 element.$$typeof 然后拒绝处理非法的元素。
    $$typeof:Symbol(react.element), 

    key:null, // 当前节点的key

    props:{ // 当前节点拥有的属性及其子节点

        children:[

            vnode1, // 子节点1

            vnode2, // 子节点2

        ], 

        classname:'App', // 当前节点的样式名称

        // ... 其他属性
    },

    ref:null, // 当前节点的ref指向

    type:'div', // 原生标签会返回字符串形式的节点类型,文本类型无type

    owner:null,
}



Component基类

// Component基类
function Component (props){
  this.props = props; // 使其内部能够通过this.props访问
}

// 类组件的标识,为何不用布尔类型?历史遗留问题,在pureComponent中是使用布尔值标识的
Component.prototype.isReactComponent = {} ; 

export default Component

react-dom 实现

// 初始渲染 - 直接生成全部
// 更新渲染 - 需要diff比较

function render(vnode, container) {
  console.log('vnode ===> ', vnode);
  // 将vnode => node
  const node = vnode2Node(vnode);
  // 将node 挂载到container上
  container.appendChild(node);
}

function vnode2Node(vnode) {
  let node;
  const {
    type
  } = vnode;
  if (typeof type === 'string') {
    node = updateHostComponent(vnode)
  } else if (typeof type === 'function') {
    // 对于vnode来说,类组件和函数组件都是以function的方式传入的,可以在Component基类的原型链上添加标识
    node = type.prototype.isReactComponent ? updateClassCompinent(vnode) : updateFunctionCompinent(vnode);
  } else {
    node = updateTextComponent(vnode)
  }
  return node;
}

// 原生标签
function updateHostComponent(vnode) {
  const {
    type,
    props
  } = vnode;
  let node = document.createElement(type);
  nodeAddAttribute(node, props)
  reconcileChildren(node, props.children)
  return node;
}

// 函数组件
function updateFunctionCompinent(vnode) {
  const {
    type,
    props
  } = vnode
  let vvnode = type(props); // 执行函数组件,使用其props参数,会得到其返回的vnode,此处为了不与入参重名,命名为vvnode
  // 使用vnode->node函数转换虚拟dom
  return vnode2Node(vvnode);
}

// 类组件
function updateClassCompinent(vnode) {
  const {
    type,
    props
  } = vnode
  let instance = new type(props); // 类组件需要new出实例
  let vvnode = instance.render(); // 类组件在render函数中返回vnode
  // 使用vnode->node函数转换虚拟dom
  return vnode2Node(vvnode);
}

// 文本标签
function updateTextComponent(vnode) {
  let node = document.createTextNode(vnode);
  return node;
}

// 将子节点vnode挂载到父节点node上
function reconcileChildren(parentNode, children) {
  let childrenArr = Array.isArray(children) ? children : [children]; // 原本children可能是对象或者数组,此处统一转成数组
  // 遍历数组,将子节点逐个添加到父节点上。
  childrenArr.forEach(vnode => {
    render(vnode, parentNode);
  })
}

// 添加属性
function nodeAddAttribute(node, attrVal) {
  // 排除里面的children属性,将其余属性添加到当前节点
  Object.keys(attrVal).filter(k => k !== 'children').forEach(key => {
    node[key] = attrVal[key]
  })
}

// eslint-disable-next-line import/no-anonymous-default-export
export default {
  render
};

fiber架构下使用diff算法的render

// 初始渲染 - 直接生成全部
// 更新渲染 - 需要diff比较

let wipRoot = null; // work in progress 的根节点
// 浏览器空闲时候调用的函数
window.requestIdleCallback(workLoop);

// 作为链表指针指向下一个单元任务
let nextUnitOfWork = null;

// fiber对象的数据结构
// fiber:{
//   type 类型
//   key
//   props // 属性值
//   stateNode // 当前的dom节点
//   child // 第一个子节点
//   sibling // 下一个子节点
//   return // 父节点
// }

// 执行单元任务
function performUnitOfWork(workInProgress) {
  // workInProgress 就是表示当前传入的待执行的fiber

  // step1 执行任务
  const {
    type
  } = workInProgress;
  if (typeof type === 'string') {
    // 原生标签
    updateHostComponent(workInProgress)
  } else if (typeof type === 'function') {
    // 函数组件
    updateFunctionCompinent(workInProgress)
  } else if (typeof type === 'undefined') {
    // 文本组件
    updateTextComponent(workInProgress)
  }


  // step2 返回下一个任务
  if (workInProgress.child) {
    return workInProgress.child; // 有子节点,返回子节点fiber
  }

  let nextFiber = workInProgress; // 先把当前的fiber存储起来
  while (nextFiber) {
    if (nextFiber.sibling) {
      // 如果当前节点有兄弟节点fiber,则返回
      return nextFiber.sibling;
    }
    // 无则继续往上传递,直到根节点的fiber没有上一级,则跳出while循环
    nextFiber = nextFiber.return;
  }

}

// 链表循环
function workLoop(IdleDeadline) {
  // requestIdleCallback调用时会返回浏览器的空闲时间
  // 在此处执行我们的单元任务(剩余空闲时间>1时执行)
  while (nextUnitOfWork && IdleDeadline.timeRemaining() > 1) {
    // 执行任务,并且返回下一个任务
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }

  // 执行完任务后提交
  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }
}

// 提交节点
function commitRoot() {
  commitWork(wipRoot.child);
  wipRoot = null;
}

// 执行提交任务
function commitWork(workInProgress) {
  // 提交自己
  if (!workInProgress) {
    return;
  }

  let parentNodeFiber = workInProgress.return; // 更新它的父节点

  // 父fiber没有dom节点时,向上查找
  while(!parentNodeFiber.stateNode){
    parentNodeFiber = parentNodeFiber.return;
  }
  let parentNode = parentNodeFiber.stateNode; // 通过父节点的stateNode定位到父级dom节点
  if (workInProgress.stateNode) {
    // 将当前fiber的stateNode,真实dom节点添加到父级dom节点上
    parentNode.appendChild(workInProgress.stateNode)
  }

  // 提交子节点child
  commitWork(workInProgress.child)

  // 提交其他子节点sibling
  commitWork(workInProgress.sibling)
}

function render(vnode, container) {
  // 初始化根节点
  wipRoot = {
    type: 'div',
    props: {
      children: {
        ...vnode
      }
    },
    stateNode: container
  }

  nextUnitOfWork = wipRoot; // 给链表的下一次单元任务传入workInProgress
}

//  废除
// function render(vnode, container) {
//   console.log('vnode ===> ', vnode);
//   // 将vnode => node
//   const node = creteNode(vnode);
//   // 将node 挂载到container上
//   container.appendChild(node);
// }

// 废除
// function vnode2Node(vnode) {
//   let node;
//   const {
//     type
//   } = vnode;
//   if (typeof type === 'string') {
//     node = updateHostComponent(vnode)
//   } else if (typeof type === 'function') {
//     // 对于vnode来说,类组件和函数组件都是以function的方式传入的,可以在Component基类的原型链上添加标识
//     node = type.prototype.isReactComponent ? updateClassComponent(vnode) : updateFunctionCompinent(vnode);
//   } else {
//     node = updateTextComponent(vnode)
//   }
//   return node;
// }

// 将原生标签的workInProgress 转变为 node节点
function creteNode(workInProgress) {
  const {
    type,
    props
  } = workInProgress;
  let node;
  node = document.createElement(type); // 创建dom节点
  nodeAddAttribute(node, props) // 更新dom的属性
  return node;
}

// 原生标签
function updateHostComponent(workInProgress) {
  const {
    props
  } = workInProgress;
  // fiber的stateNode指向当前dom节点
  if (!workInProgress.stateNode) {
    workInProgress.stateNode = creteNode(workInProgress);
  }

  // 协调
  reconcileChildren(workInProgress, props.children)

  console.log('workInProgress ===>', workInProgress);
}

// 函数组件
function updateFunctionCompinent(workInProgress) {
  const {
    type,
    props
  } = workInProgress
  let vvnode = type(props); // 执行函数组件,使用其props参数,会得到其返回的vnode,此处为了不与入参重名,命名为vvnode

  // 将其 协调成fiber结构
  reconcileChildren(workInProgress, vvnode)
}

// 类组件
function updateClassComponent(vnode) {
  const {
    type,
    props
  } = vnode
  let instance = new type(props); // 类组件需要new出实例
  let vvnode = instance.render(); // 类组件在render函数中返回vnode
  // 使用vnode->node函数转换虚拟dom
  // return vnode2Node(vvnode);
}

// 文本标签
function updateTextComponent(workInProgress) {
  if(!workInProgress.stateNode){
    workInProgress.stateNode =  document.createTextNode(workInProgress.props);
  }
}

// 协调子节点,将子节点workInProgress也转变成fiber结构 
function reconcileChildren(workInProgress, children) {
  if (typeof children === 'string' || typeof children === 'number') {
    // 基础文本/数值,不再转成fiber结构
    return;
  }
  let childrenArr = Array.isArray(children) ? children : [children]; // 原本children可能是对象或者数组,此处统一转成数组

  let previousNewFiber = null; // 用于暂存在for循环中生成的fiber,用于在循环下一次的fibe是,给其上一个fiberr的sibling节点赋值
  for (let i = 0; i < childrenArr.length; i++) {
    let child = childrenArr[i];
    let newFiber = {
      type: child.type,
      props: {
        ...child.props
      },
      stateNode: null,
      child: null,
      sibling: null,
      return: workInProgress
    }

    if(typeof child === 'string'){
      newFiber.props = child;
    }

    // 把第一个生成的fiber当做workInProgress的child
    if (i === 0) {
      workInProgress.child = newFiber
    } else {
      // 除了第一个生成的fiber,其余都可作为上一个newFiber的sibling节点(链表结构,每个fiber都能指向下一个fiber)
      previousNewFiber.sibling = newFiber;
    }

    previousNewFiber = newFiber; // 更新上一个fiber节点
  }
}

// 添加属性
function nodeAddAttribute(node, attrVal) {
  // children属性中存放着文本,也需要添加到当前节点(新加文本节点插入)
  Object.keys(attrVal).forEach(key => {
    if (key === 'children') {
      if (typeof attrVal[key] === 'string') {
        node.textContent = attrVal[key]
      }
    } else {
      node[key] = attrVal[key]
    }
  })
}

// eslint-disable-next-line import/no-anonymous-default-export
export default {
  render
};

 

 

Fiber架构

一:前言

在 React Fiber 架构面世一年多后,最近 React 又发布了最新版 16.8.0,又一激动人心的特性:React Hooks 正式上线,让我升级 React 的意愿越来越强烈了。在升级之前,不妨回到原点,了解下人才济济的 React 团队为什么要大费周章,重写 React 架构,而 Fiber 又是个什么概念。

二、React 15 的问题

在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。请看以下例子:
https://claudiopro.github.io/...

clipboard.png

其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

针对这一问题,React 团队从框架层面对 web 页面的运行机制做了优化,得到很好的效果。

clipboard.png

三、解题思路

解决主线程长时间被 JS 运算占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。

旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这一 API。官方的解释是这样的:

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。

有了解题思路后,我们再来看看 React 具体是怎么做的。

四、React 的答卷

React 框架内部的运作可以分为 3 层:

  • Virtual DOM 层,描述页面长什么样。
  • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
  • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

这次改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫Fiber Reconciler。这就引入另一个关键词:Fiber。

Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:

const fiber = {
    stateNode,    // 节点实例
    child,        // 子节点
    sibling,      // 兄弟节点
    return,       // 父节点
}

为了加以区分,以前的 Reconciler 被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑:

 

而 Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行:

 

为了达到这种效果,就需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:

  • synchronous,与之前的Stack Reconciler操作一样,同步执行
  • task,在next tick之前执行
  • animation,下一帧之前执行
  • high,在不久的将来立即执行
  • low,稍微延迟执行也没关系
  • offscreen,下一次render时或scroll时才执行

优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。

Fiber Reconciler 在执行过程中,会分为 2 个阶段。

 

  • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
  • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。

阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。

五、Fiber 树

Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。

 

Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程:

 

如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。

在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List当中,在阶段二执行的时候,会批量更新相应的节点。

六、总结

本文从 React 15 存在的问题出发,介绍 React Fiber 解决问题的思路,并介绍了 Fiber Reconciler 的工作流程。从Stack ReconcilerFiber Reconciler,源码层面其实就是干了一件递归改循环的事情。

 

 

GitHub地址:

https://github.com/CWH0908/MyReact

参考文章:

React源码揭秘
React Fiber 原理介绍

完全理解React Fiber

React Elements  $$typeof 属性

 

教学视频:

React17源码训练营

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值