React 16.8 [工作原理详解]

2,124 阅读9分钟

前言

我们今天不分享 React 具体语法,组件, 通信 ,Ref ,Portals ,Context ,Hoc ,Hook 等等知识点,这些东西,大家细致的看一下官方文档都可以熟悉的进行开发任务,而今天我想谈谈 React 的工作原理 。

如有同学想了解 React 基础入门知识请查看我的一篇 demo , 近 100 start React Antd Shop

Lets Go !

什么是虚拟 Dom ? 什么是 Jsx ? React 又是怎么工作的 ? ... ....

我们来根据上边的三个问题,来对 React 进行一次浅度剖析 !

什么是虚拟 Dom ?

Virtual DOM (虚拟Dom) 是一种编程概念 ,在这个概念里 ,UI 是以一种虚拟的结构形式保存在内存中,然后通过编译转换,变成真实Dom的一种技术。那它到底是什么呢 ?

对象 ,yes ,就是一个 Javascript 对象 , 用这个 Javascript 对象来表示Dom信息和结构,当状态变更的时候,重新渲染这个对象结构,这个 Javascript 对象称为 Virtual DOM 。

为什么不直接操作DOM ? 很简单,因为DOM操作很慢,细小的改变都有可能导致页面重排与重绘,耗性能 ; 所以 , 通过diff 算法对 Js 对象的操作可以批量的,最小化的执行 Dom 操作,从而提高性能 。diff 我们后期分析

什么是Jsx ?

你可以理解为是 React 独有的语法糖吧 , 其实 Jsx 是 javascript + xml 的语法扩展 , 但凡是遇到 { } 会被当做 Javascript 来执行,否则则是 xml 来执行 ;

为什么需要用 Jsx ? 也许是因为 Jsx 模板简洁,语法灵活吧,这一点我并不是太清楚 ;

原理:babel-loader 会预编译 Jsx 为 React.createElement(...) 函数执行 。

那React 又是怎么工作的呢 ?

Jsx 入手吧 , 首先来我们看官网的一段代码 【少许加工了一下,为了看的更明白】

一个常规的 React 组件 【编译前】

class HelloMessage extends React.Component {
  render() {
    return (
      <div className="HelloClass" num="1">
        Hello {this.props.name}
        <span> is span</span>
      </div>
    );
  }
}
ReactDOM.render(
  <HelloMessage name="Taylor" />,
  document.getElementById('hello-example')
);

这个常规的 React 组件,经过 babel-loader 【编译后】

class HelloMessage extends React.Component {
  render() {
    return React.createElement(
      "div", // 类型
      { className: "HelloClass", num: "1" }, // 属性
      "Hello ",  // 内容
      this.props.name, // props 直接传递
      React.createElement( // 嵌套-同理
        "span",
        null,
        " is span"
      )
    );
  }
}

// 类组件也是转换成 `React.createElement(...)` 函数执行

ReactDOM.render(React.createElement(HelloMessage, { name: "Taylor" }), 
document.getElementById('hello-example'));

看过代码,简单的分析过后,有没有发现,验证了我们上边说到的 Jsx 原理 ,经过babel-loader编译之后变成了React.createElement(...) 函数执行,如果有嵌套,则 React.createElement(...) 也嵌套

我们不管是编译前,还是编译后,有一句代码没有变,那就是 class HelloMessage extends React.Component 继承了 React.component 类,其实简单点说,就是继承父组件的 props

okey ! 到这里 ,我们可以有一个简单的结论,就是:

  1. React 组件需要继承 React.Component 类 class HelloMessage extends React.Component
  2. Jsx 语法经过 babel-loader 编译后会变成 React.createElement(...) 函数执行,多层则嵌套
  3. 通过 ReactDom.render 方法将元素挂载在页面上

我们来依次分析,并简单实现一下 :

React.component

export default function Component (props) {
  // 含有一个 props 参数
  this.props = props;
}
// 定义一个类组件与函数组件的标识 , 源码是这样标识的,虽然不知道为啥没有写成布尔值
Component.prototype.isReactComponent = {};

React.createElement(...)

回到React.createElement(...) , 那么 React.createElement(...) 到底接收几个参数,分别又是干什么的呢 ? 通过上边的例子可以总结出:

  • 参数1:类型 (渲染的元素类型,组件类型 等)
  • 参数2:属性 (元素或者组件的属性,包含 className,id,自定义 props 属性等)
  • 参数3:子节点内容 可能有很多个子级

okey , 综上所述 ,我们来手写一个 mini 的 React.createElement() 。

在写 React.createElement(...) 之前,我们首先要搞懂两个事情,传入什么参数 ,返回什么值 , 传入什么参数,上边我们已经分析了出来,经过babel-loader 编译后需要传入三个参数分别是 类型,属性,子节点

那返回什么呢 ? 我们来看看createElement源码:

// 位置 : react/packages/react/src/ReactElement.js 348 行 
// 删掉了前边那些各种处理,方便阅读
export function createElement(type, config, children) {
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );

一目了然 ,需要返回一个 ReactElement 对象,包含type,key, ref ,self , source , props 等 ,在这里我们主要为了了解 react 的工作原理 , 所以择取重要的来简单实现一下,暂时抛弃 key,ref,self 来, 话不多说,上代码

/*
  三个参数 , 暂时返回两个属性 type 和 props 
  type:创建类型 , 原生标签 , 文本 , 函数组件 , 类组件  等
  config: 属性
  例举属性如 : 函数组件:{className: "border", name: "函数组件", __source: {…}, __self: undefined }
  children: 子节点 , 多少个就不知道了
*/
function createElement (type, config, ...children) {
  // 移除 config 暂时不用的 __source: {…}, __self: undefined 属性,方便控制台查看
  if (config) {
    delete config.__source;
    delete config.__self;
  }
  // 这里不考虑 key . ref . slef
  const props = {
    ...config,
    /*
        createTextNode 统一文本节点的数据结构,方便后续统一处理(child 直接就是文本内容)
        如果过是一个 object 说明子节点下还有子节点,依然利用 React.createEement 继续编译为
        {
            type:"类型",
            props:{}
        }
        的形式, 重点:babel-loader 将 jsx 编译成 React.creteElement 嵌套的形式 
   */  
    children: children.map(child =>
      typeof child === "object" ? child : createTextNode(child)
    )
  };
  console.log({type,props});
  return {
    type,
    props
  };
}

// 纯文本统一一下格式,跟其它元素一样的格式,方便处理,类型自定义为 "TEXT" 
function createTextNode (text) {
  return {
    type: "TEXT",
    props: {
      children: [],
      nodeValue: text
    }
  };
}

来看看最后的返回结果,我自己的样例 在这里插入图片描述

okey ! 一个 mini 版本的 React.createElement() 方法就这样实现了,好,我们接着往下走,是不是到了 render 方法了 , 走,再去看一看源码 ~~~

ReactDOM.render(···)

render 方法在 react-dom 模块里边,看上边的实例貌似要传入两个参数,其实有三个参数,只是第三个不怎么使用而已,分别是:1. react 元素 2. 解析后要放置的容器 3. 回调函数

// 位置 : react/packages/react-dom/src/client/ReactDOMLegacy.js 287 行
export function render(
  element: React$Element<any>, // react 元素
  container: Container, // 要放置的容器
  callback: ?Function, // 回调函数
) {
  // ...... 省略一大堆逻辑 
  // 这里返回调用了  legacyRenderSubtreeIntoContainer  方法 
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

legacyRenderSubtreeIntoContainer 顺藤摸瓜往下走

// 位置 : react/packages/react-dom/src/client/ReactDOMLegacy.js 175 行
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {
  let root: RootType = (container._reactRootContainer: any);
  let fiberRoot;
  // 判断是不是第一次渲染该组件 , 如果是第一次则创建,然后更新 dom , 否则直接进行 diff 更新 dom
  if (!root) {
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 更新 dom 
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // 更新 dom 
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

看到这里我们心中依然有了一个简单的概念,那就是

  • 1,render 方法接收三个参数,react 元素,解析后要放置的容器 ,回调函数
  • 2,紧跟着调用 legacyRenderSubtreeIntoContainer 方法,创建 dom || 更新dom ,这里边牵扯到两个重点,也是 React 的核心知识点【fiber 数据结构 , difff 算法】 我们后续来了解

今天在这里呢,我们以简洁的方式来实现一下 render 方法 :

创建一个 render 函数,做两件事儿(抛开回调函数来说)

/*
  vnode  jsx 经过 React.createElemnt 编译后的虚拟 dom  
  container 容器  
  步骤 1 : vnode -> node  , 虚拟 dom 转换为真实 dom 
  步骤 2 : container.appendChild(node);  , 将真实 dom 挂载给容器 container 
*/
function render (vnode, container) {
  const node = createNode(vnode, container);
  node && container.appendChild(node);
}

实现一个 createNode 将虚拟 dom 转换为真实 dom

/*
  虚拟 dom 渲染成真实 dom  
  vnode 虚拟 dom 
  parentNode 父节点 , 也就是将虚拟dom挂载的对应父容器   
*/
function createNode (vnode, parentNode) {
  // 转换组合的最后真实 dom 节点
  let node = null; 
  /*
    type:"类型"
    props:参数和子节点 children 
  */
  const { type, props } = vnode;

  // todo 根据节点类型,生成 dom 节点 
  if (type === "TEXT") {
    // 文本节点 , type 上边自定义为 "TEXT" , nodeValue 是文本节点的内容值
    node = document.createTextNode("");
  } else if (typeof type === "string") {
    // 原生标签节点,type 类型为字符串 :如 div , p , span 等 , 创建对应的标签元素
    node = document.createElement(type);
  } else if (typeof type === "function") {
    // 类组件与函数组件 ,type 类型为 function 
    // isReactComponent 自定义区分标识 , Component 原型上定义 
    node = type.prototype.isReactComponent
      ? updateClassComponent(vnode, parentNode) // 类组件转换dom方法  
      : updateFunctionComponent(vnode, parentNode);  // 函数组件转换 dom 方法 
  }

  /*
    子节点转换 dom , 其实就是遍历子节点 , 递归调用 render , 根据上边的逻辑继续转换
    node : 当前节点,也就是当前子节点的父节点
    props.children : 子节点
    像类组件,函数组件一样,我们把它放置在外边
  */
  reconcileChildren(node, props.children);

  /*
    更新 dom
    node : 真实 node 节点
    props :  props 属性和子节点属性以及子节点内容值
    将所有属性和子节点属性以及内容值解析渲染给node 
  */   
  updateNode(node, props);

  return node;
}

好了,一个简洁的 render 方法逻辑已经呈现在我们的眼前了,剩下的就是要一步步实现对应的转换逻辑了,我们一个一个来:

类组件

先模拟写一个类组件,继承了上边我们自定义的 React.component 类 ;

class ClassComponent extends Component {
  render () {
    return (
      <div className="border">
        ClassComponent - {this.props.name}
      </div>
    );
  }
}

解析类组件其实就是需要先实例化,然后再执行 render 返回对应的虚拟 dom 再通过上边 createNode 逻辑进行转换

function updateClassComponent (vnode, parentNode) {
  // 获取虚拟dom的类型和属性 
  const { type, props } = vnode;
  // 实例化,应为类组件类型是一个函数,将属性props传递进去
  const instance = new type(props);
  // 执行类组件的 render 函数 , 获取虚拟 dom
  const vvnode = instance.render();
  // 根据转换逻辑,将虚拟dom转换为真实dom
  const node = createNode(vvnode, parentNode);
  // 返回真实dom
  return node;
}

函数组件

function FunctionComponent (props) {
  return <div className="border">FunctionComponent-{props.name}</div>;
}

函数组件就比类组件更简单一些,直接执行函数,返回对应的虚拟 dom 再通过 createNode 逻辑进行转换

function updateFunctionComponent (vnode, parentNode) {
  const { type, props } = vnode;
  const vvnode = type(props); 
  const node = createNode(vvnode, parentNode);
  return node;
}

子节点

遍历子节点,递归执行 render 走到 createNode 转换逻辑 ;

function reconcileChildren (node, children) {
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    render(child, node);
  }
}

更新dom

更新 dom 节点 , nextVal => props 属性和子节点属性以及值 最后挂载给真实 dom node 节点 ,返回

function updateNode (node, nextVal) {
  Object.keys(nextVal)
    .filter(k => k !== "children")
    .forEach(k => {
      // console.log(node[k] + "----" + nextVal[k]);
      node[k] = nextVal[k];
    });
}

回炉 ~

再回到我们的官方实例, 是不是一个简单的工作原理已经了然于心了

class HelloMessage extends React.Component {
  render() {
    return (
      <div>
        Hello {this.props.name}
      </div>
    );
  }
}
ReactDOM.render(
  <HelloMessage name="Taylor" />,
  document.getElementById('hello-example')
);

mini-react-demo 入口

后续: 我们再来研究 fiber 数据结构 和 diff 算法

欢迎点赞,小小鼓励,大大成长