AndreGeng / AndreGeng.github.io

blog repository
1 stars 0 forks source link

React工作原理浅析 #63

Open AndreGeng opened 4 years ago

AndreGeng commented 4 years ago

前言

之前自己看了些资料和代码,这里尝试通过实现一个类react的框架,来理解下react的工作流程,写文章也是对自己思路的一个整理,如有谬误还请大家帮忙指出。

主要内容

  1. 什么是React Element
  2. ReactDOM的render实现
  3. jsx与React Element
  4. React reconcile过程简介
  5. React Component实现
  6. Fiber简介
  7. 基于Fiber的代码实现

    没有包含的内容

  8. 生命周期
  9. context
  10. ref ps: 另外一些地方,只是概念介绍和react实际实现存在差异,但基本原理是相同的 e.g. reconcile的过程为了简洁起见,并没有将key的作用考虑进去。

    MiniReact

React Element

根据Dan在React Components, Elements, and Instances里的讲解,react element是指

An element is not an actual instance. Rather, it is a way to tell React what you want to see on the screen. You can’t call any methods on the element. It’s just an immutable description object with two fields: type: (string | Component) and props: Object.*

简单来说,react element是我们要绘制的页面结构的javascript描述 举个例子,下方这样的页面结构

<div class="container">
  <span class="item">hello</span>
  <span class="item">world</span>
</div>

所对应的react element如下

{
  type: "div",
  props: {
    className: "container",
    children: [{
      type: "span",
      props: {
        className: "item",
        children: "hello"
      }
    }, {
      type: "span",
      props: {
        className: "item",
        children: "world"
      }
    }]
  }
}

可以看出react element是一个典型的树状结构。而React初次渲染的过程就是把react element转换为dom节点的过程,假设我们已经有了一个上面这样的react element对象,下面来看下ReactDom.render是如何把react element转换为dom树的。

ReactDom.render的实现

我们需要做的是遍历react element树来生成dom节点,对于树状结构最容易想到的遍历方式就是递归,于是有了下面的伪代码:

/**
* 把react elements渲染成dom节点
* @param {MiniReactElement} element
* @param {HTMLElement} container
* @return {void} 
*/
const render = (element, container) => {
  // 如果element为文本元素
  if (!element.type) {
    const node = createTextNode();
    container.appendChild(node);
    return;
  }
  const node = document.createElement(element.type);
  // 添加properties
  addPropertiesForNode(node);
  // 添加event listener
  addEventListenerForNode(node);
  // 递归遍历children, 生成子树
  children.forEach(child => {
    render(child, node);
  });
  container.appendChild(node);
}

渲染函数有了,那react element又是如何生成的呢,我们知道在react里是通过jsx来描述react elements的,那来看下jsx到底做了哪些工作?

jsx与React Elements

下面是babel repl中的截图 image 可以看到babel其实是把jsx转换成了对React.createElement方法的调用 通过查看@babel/plugin-transform-react-jsx的说明,看到可以通过给jsx加注释的方式来自定义转换后的函数 image 现在只要我们实现了MiniReact.createElement方法,就可以直接在代码里通过jsx来描述react elements了 因为它做的工作只是返回一个javascript对象,所以实现起来还是比较简单

/**
* 生成MiniReact Element
* @param {string} type 组件类型,e.g. div, section, span
* @param {object} config 组件需要接收的props
* @param {[]} args 组件的children
*/
const createElement = (type, config, ...args) => {
 const props = Object.assign({}, config, {
   children: flattenArray(args),
 })
 return {
   type,
   props,
 }
}

到目前为止基本实现了,从『数据』到『dom节点』的初始渲染过程 image 那当数据更新时,我们可以重新生成新的elements,然后调用render生成新的dom树。再把root container的innerHTML改为新生成的dom树就完成了页面的更新。 但这样做有两个问题:

  1. 虽然我们的改变,理论上只影响一小部分页面,但整个dom都被替换了。
  2. 我们的数据状态目前为止只能是全局的。

先来解决第一个问题,我们在update的过程中引入reconcile。

reconcile过程简介

  1. reconcile是一个diff虚拟树的过程,首先我们得记录下之前生成的虚拟树
  2. diff过程中会存在对dom的操作,我们需要保存element对应的dom节点

于是引入一个新的数据结构instance:

/**
* @typedef {Object} Instance
* @property {MiniReactElement} element
* @property {HTMLElement} dom
* @property {Instance[]} childrenInstances
* instance是MiniReactElement渲染到dom后的一种表示
*/

image

把之前生成dom树的render函数重命名为instantiate, 返回值为instance类型

/**
* 返回instance对象
* @param {MiniReactElement} element
* @return {Instance}
*/
const instatiate = (element) => {
 const instance = {
   element,
 }
 // 处理文本节点
 if (!element.type) {...}
 const node = document.createElement(element.type);
 // 设置attributes和listener
 updateDomProperties(node, [], props);
 const childInstances = props.children.map(instatiate);
 const childDoms = childInstances.map(instance => instance.dom);
 childDoms.forEach(dom => node.appendChild(dom));
 return Object.assign({}, instance, { dom: node, childInstances });
}

保存之前生成的instance对象

/**
* 保存上次渲染生成的instance对象
* @type {Instance}
*/
let rootInstance = null;

/**
* @param {MiniReactElement} element
* @param {HTMLElement} container 
*/
const render = (element, container) => {
 const preRootInstance = rootInstance;
 rootInstance = reconcile(container, preRootInstance, element);
}
/**
 * 对比新老instance,完成dom树的更新
 * @param {HTMLElement} container
 * @param {Instance} preInstance
 * @param {MiniReactElement} element
 * @return {Instance} newInstance
 */
const reconcile = (container, preInstance, element) => {
 // 旧的节点需要删除
 if (!element) {
   container.removeChild(preInstance.dom);
   return null;
 }
 // 新增节点
 if (!preInstance) {
  const newInstance = instatiate(element);
  container.appendChild(newInstance.dom);
  return newInstance;
 } 
 // 类型不一致,替换节点
 if (preInstance.element.type !== element.type) {
   const newInstance = instatiate(element);
   container.replaceChild(preInstance.dom, newInstance.dom);
   return newInstance;
 } 
 const newInstance = {
   element,
 };
 if (preInstance.element.type === element.type) {
  // 类型一致,复用节点
  newInstance.dom = preInstance.dom;
  updateDomProperties(preInstance.dom, preInstance.element.props, element.props);
 } 
 // 递归调用reconcile, 生成childInstance
 newInstance.childInstances = reconcileChildren(preInstance, newInstance);
 return newInstance;
}
/**
 * 递归调用reconcile生成childInstances 
 * @param {Instance} preInstance
 * @param {MiniReactElement} element
 * @return {Instance[]}
 */
const reconcileChildren = (preInstance, newInstance) => {
  const element = newInstance.element;
  const count = Math.max((preInstance && preInstance.childInstances.length) || 0, (element.props && element.props.children.length) || 0);
  const newChildrenInstances = [];
  for (let i = 0; i < count; i++) {
    const preChildInstance = (preInstance && preInstance.childInstances[i]) || null;
    const child = element.props && element.props.children[i];
    const childInstance = reconcile(newInstance.dom, preChildInstance, child);
    newChildrenInstances.push(childInstance);
  }
  return newChildrenInstances;
}

添加了reconcile之后发现,只有被影响到的节点会更新啦~, 那全局state的问题怎么解决呢,我们知道react 16之前只有react的类组件是可以有自己的state的,那现在我们来引入Component

Component与state

首先,我们需要有一个Component基类来供自定义组件继承

class Component {
 constructor(props) {
  this.props = props;
  this.state = this.state || {};
 }
 setState(partialState) {
  this.state = Object.assign({}, this.state, partialState);
 /** 
  * reconcile的过程,我们需要在当前实例上能访问到,
  * 之前的instance。我们把它保存在实例的__internalInstance上,为了把类组件的实例和instance
  * 区分开,这里我们把类组件的实例叫做publicInstance
  * /
  const instance = this.__internalInstance;
  reconcile(instance.dom.parentNode, instance, instance.element);
 }
 render() {
  return null;
 }
}

当在elements中引入自定义的Component后,意为着element.type可以是一个function, 而不再只能是dom节点的tagName, 我们来更改instatiate函数的实现

/**
* 返回instance对象
* @param {MiniReactElement} element
* @return {Instance}
*/
const instatiate = (element) => {
  let { type, props } = element;
  if (typeof type === "function") {
    const newInstance = {
      element,
    };
    if (typeof type.prototype.render === "function") {
      // 类组件
      const publicInstance = createPublicInstance(element, newInstance);
      const childElement = publicInstance.render();
      const childInstance = instatiate(childElement);
      Object.assign(newInstance, { 
        dom: childInstance.dom, 
        childInstance, publicInstance 
      });
      return newInstance;
    }
    // 函数组件
    const childElement = type(props);
    const childInstance = instatiate(childElement);
    Object.assign(newInstance, { childInstance, dom: childInstance.dom });
    return newInstance;
  }
 // 原有逻辑 {...}
}

/**
 * 创建与Component相关的publicInstance
 * @param {MiniReactElement} element
 */
const createPublicInstance = (element, newInstance) => {
  const { type, props } = element;
  const publicInstance = new type(props);
  publicInstance.__internalInstance = newInstance;
  return publicInstance;
}

另外reconcile的过程也需要少许更改

/**
 * 对比新老instance,完成dom树的更新
 * @param {HTMLElement} container
 * @param {Instance} preInstance
 * @param {MiniReactElement} element
 * @return {Instance} newInstance
 */
export const reconcile = (container, preInstance, element) => {
 // 旧的节点需要删除 {...}
 // 新增节点 {...}
 // 类型不一致,替换节点 {...}
 // 类型一致
 if (typeof preInstance.element.type === "function") {
   let childElement;
   if (typeof element.type.prototype.render === "function") {
     // 类组件
     preInstance.publicInstance.props = element.props;
     childElement = preInstance.publicInstance.render();
   } else {
     // 函数组件
     childElement = element.type(element.props);
   }
   const childInstance = reconcile(
      container, 
      preInstance.childInstance, 
      childElement
    );
   Object.assign(preInstance, { childInstance, })
   return preInstance;
 }
 // 原有处理dom更新逻辑 {...}
}

至此,我们引入了component从而支持了局部的state, 页面现在可以进行部分刷新了~ 上面列举的内容,与React 16之前的结构还是基本类似的,React 16主要的不同是它引入了fiber架构,那啥是fiber呢?

Fiber简介

Fiber是React 16以后引入的新的reconciliation算法,它的名称来自于React实现中的Fiber数据组构。 引入Fiber的目的是实现增量渲染:

把渲染工作分段并可以插入到多个帧内的能力

通俗的讲就是reconciliatoin的过程在16之后是可中断/暂停/继续的,它带来的优势主要是渲染任务现在支持区分优先级了。e.g.像用户交互类的渲染会更优先得到响应,而像从服务器读取数据这种IO操作就会被安排一个较低的优先级。

具体差异可以参见这个triangle动画的例子:Fiber vs Stack Demo

Fiber tree

image

有了fiber结构,我们可以把之前基于callstack的数据结构切换到链表,这样就有了暂停的先决条件, 那怎么判断何时暂停呢? 借助requestIdleCallback, 它提供了一种在浏览器空闲的情况下执行代码的机会

浏览器一帧的周期内所做的工作: image

requestIdleCallback的执行时机: image

下面来看下循环遍历fiber tree的伪代码:

let wipRoot = {
 dom: container,
 props: {
   children: [element],
 },
 alternate: currentRoot,
}
let nextUnitWork = wipRoot;
window.requestIdleCallback(workLoop);
function workLoop(idleDeadline) {
 while (idleDeadline.timeRemaining() > 1 || idleDeadline.didTimeout) {
   nextUnitWork = performUnitWork(nextUnitWork);
 }
 if (nextUnitWork) {
   window.requestIdleCallback(workLoop);
 }
}

performUnitWork做的工作主要如下:

  1. 对于非HostComponent,生成子组件的elements
  2. reconcileChildren
  3. 返回nextUnitWork
    /**
    * 1. 创建dom节点
    * 2. 返回nextUnitWork
    * @return {Fiber}
    */
    const performUnitWork = (fiber) => {
    if (typeof fiber.type === "function") {
    if (typeof fiber.type.prototype.render === "function") {
      // 类组件
    } else {
      wipFiber = fiber;
      hooksIdx = 0;
      fiber.props.children = [fiber.type(fiber.props)]
    }
    }
    reconcileChildren(fiber);
    if (fiber.child) {
    return fiber.child;
    }
    if (fiber.sibling) {
    return fiber.sibling;
    }
    let parent = fiber.return;
    while (!parent.sibling && parent.return) {
    parent = parent.return;
    }
    if (parent.sibling) {
    return parent.sibling;
    }
    return null;
    }

    reconcileChildren的作用主要是

  4. 对比新/老vdom,把改动点推入effectList
  5. 构建fiber树
    const reconcileChildren = (wipFiber) => {
    const elements = wipFiber.props.children;
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
    let index = 0;
    let prevSibling = null;
    while (index < elements.length || oldFiber) {
    if (index < elements.length) {
      const element = elements[index];
      const newFiber = {
        type: element.type,
        props: element.props || {},
        return: wipFiber,
        alternate: oldFiber,
        dom: null,
        hooks: [],
      };
      // 新增节点
      if (!oldFiber) {
        newFiber.effectTag = EFFECT_TAG.NEW;
        effectList.push(newFiber);
      }else if (oldFiber.type !== newFiber.type) {
        newFiber.alternate = null;
        oldFiber.effectTag = EFFECT_TAG.DELETE;
        effectList.push(oldFiber);
      } else if (oldFiber.type === newFiber.type) {
        newFiber.dom = oldFiber.dom;
        newFiber.stateNode = oldFiber.stateNode;
        const changeNeeded = Array.from(new Set([...getKeys(newFiber.props), ...getKeys(oldFiber.props)]))
          .some(key => newFiber.props[key] !== oldFiber.props[key])
        if (changeNeeded) {
          newFiber.effectTag = EFFECT_TAG.UPDATE;
          effectList.push(newFiber);
        }
      }
      if (!wipFiber.child) {
        wipFiber.child = newFiber;
      }
      if (prevSibling) {
        prevSibling.sibling = newFiber;
      }
      prevSibling = newFiber;
      index++;
    } else {
      // 需删除节点
      oldFiber.effectTag = EFFECT_TAG.DELETE;
      effectList.push(oldFiber);
    }
    if (oldFiber) {
      oldFiber = oldFiber.sibling || null;
    }
    }
    }

    最后生成提交渲染的过程放在commitRoot函数中,它做的工作主要是通过遍历effectlist来生成dom树,这个过程不贴代码了,感觉兴趣的同学可以自己实现下,需要注意的地方是commitRoot的过程是不可中断的。 这里主要再介绍下hooks的实现,从上面的代码可以看到fiber对象上有一个叫做hooks的数组,performUnitWork生成当前节点的elements时,会重设一个叫做hooksIdx的变量,而useState所做的工作是

  6. 生成hook对象
  7. 把hook对象推入fiber上的hooks数组
  8. 当setValue被调用时,把newValue推入hook对象上的queue, 启动新一轮的workLoop
export const useState = (initV) => {
  const oldHook = wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hooksIdx];
  const hook = {
    state: oldHook ? oldHook.state : initV,
    queue: [],
  };
  wipFiber.hooks.push(hook);
  hooksIdx++;
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach(action => {
    hook.state = typeof action === "function" ? action(hook.state) : action;
  });
  const setState = (newV) => {
    hook.queue.push(newV);
    wipRoot = {
      dom: currentRoot.dom,
      alternate: currentRoot,
      props: currentRoot.props,
    }
    nextUnitWork = wipRoot;
    window.requestIdleCallback(workLoop);
  }
  return [hook.state, setState];
}

其他用于保存数据的hooks的实现原理,应该也基本类似。。

想介绍的内容大概就是这些,肯定有写的不准确的地方,希望大家帮忙指正,我这边会进行修改的, 一边写文档一边犯懒癌😂,还是得多写吧,anyway, 希望对大家理解react工作原理有所帮助, 2020新年快乐🎉🎉🎉