React 【 工作原理浅析】

前言

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

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", // div 类型
      { className: "HelloClass", num: "1" }, // div 的属性
      "Hello ",  // div 的内容
      this.props.name, // div 的内容 , 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) {
  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 行 
// 删掉了一些 if(_DEV_)的多余代码
export function createElement(type, config, children) {
  // 一系列声明
  let propName;
  const props = {};
  let key = null;
  let ref = null;
  let self = null;
  let source = null;
  // 如果传入了 config , 对以上声明属性进行赋值
  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // children : (arguments.length - 2) 去除前两个参数的剩下所有参数
  // 最后将 children 并入 props 属性中
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray; // children 返回一个数组
  }

  // 默认值
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  // 最后返回 ReactElement 这个对象 
  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 模块里边

// 位置 : react/packages/react-dom/src/client/ReactDOMLegacy.js 287 行
export function render(
  element: React$Element<any>, // react 元素
  container: Container, // 要放置的容器
  callback: ?Function, // 回调函数
) {
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  // ··· 这里返回调用了  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 元素,渲染 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) {
  const { type, props } = vnode;
  const instance = new type(props);
  const vvnode = instance.render();
  const node = createNode(vvnode, parentNode);
  return node;
}
函数组件
function FunctionComponent (props) {
  return <div className="border">FunctionComponent-{props.name}</div>;
}

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

function updateFunctionComponent (vnode, parentNode) {
  const { type, props } = vnode;
  // console.log(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 算法

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值