React运行机制超详细讲解

目录
  • 适合人群
  • 写源码之前的必备知识点
    • JSX
    • 虚拟Dom
    • 原理简介
  • 手写react过程
    • 基本架子的搭建
    • React的源码
    • ReactDom.render
    • ReactDom.Component
  • 简单源码

适合人群

本文适合0.5~3年的react开发人员的进阶。

讲讲废话:

react的源码,的确是比vue的难度要深一些,本文也是针对初中级,本意了解整个react的执行过程。

写源码之前的必备知识点

JSX

首先我们需要了解什么是JSX。

网络大神的解释:React 使用 JSX 来替代常规的 JavaScript。JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。

是的,JSX是一种js的语法扩展,表面上像HTML,本质上还是通过babel转换为js执行。再通俗的一点的说,jsx就是一段js,只是写成了html的样子,而我们读取他的时候,jsx会自动转换成vnode对象给我们,这里都由react-script的内置的babel帮助我们完成。

简单举个栗子:

return (
  <div>
    Hello  Word  </div>
)
实际上是:
return React.createElement(
  "div",
  null,
  "Hello"
)

JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面。

虚拟Dom

这里说明一下react的虚拟dom。react的虚拟dom跟vue的大为不同。vue的虚拟dom是为了是提高渲染效率,而react的虚拟dom是一定需要。很好理解,vue的template本身就是html,可以直接显示。而jsx是js,需要转换成html,所以用到虚拟dom。

我们描述一下react的最简版的vnode:

function createElement(type, props, ...children) {
  props.children = children;
  return {
    type,
    props,
    children,
  };
}

这里的vnode也很好理解,

type表示类型,如div,span,

props表示属性,如{id: 1, style:{color:red}},

children表示子元素

下边会在createElement继续讲解。

原理简介

我们写一个react的最简单的源码:

import React from 'react'
import ReactDOM from 'react-dom'
function App(props){
     return <div>你好</div>
 </div>
}
ReactDOM.render(<App/>,  document.getElementById('root'))

React负责逻辑控制,数据 -> VDOM

首先,我们可以看到每一个js文件中,都一定会引入import React from ‘react’。但是我们的代码里边,根本没有用到React。但是你不引入他就报错了。

为什么呢?可以这样理解,在我们上述的js文件中,我们使用了jsx。但是jsx并不能给编译,所以,报错了。这时候,需要引入react,而react的作用,就是把jsx转换为“虚拟dom”对象。

JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面。而引入React,就是为了时限这个过程。

ReactDom渲染实际DOM,VDOM -> DOM

理解好这一步,我们再看ReactDOM。React将jsx转换为“虚拟dom”对象。我们再利用ReactDom的虚拟dom通过render函数,转换成dom。再通过插入到我们的真是页面中。

这就是整个mini react的一个简述过程。相关参考视频讲解:进入学习

手写react过程

基本架子的搭建

react的功能化问题,暂时不考虑。例如,启动react,怎么去识别JSX,实现热更新服务等等,我们的重点在于react自身。我们借用一下一下react-scripts插件。

有几种种方式创建我们的基本架子:

利用 create-react-app zwz_react_origin快速搭建,然后删除原本的react,react-dom等文件。(zwz_react_origin是我的项目名称)

第二种,复制下边代码。新建package.json

  {
    "name": "zwz_react_origin",
    "scripts": {
      "start": "react-scripts start"
    },
    "version": "0.1.0",
    "private": true,
    "dependencies": {
      "react-scripts": "3.4.1"
    },
  }

然后新建public下边的index.html

  <!DOCTYPE html>
  <html lang="en">
    <head>
    </head>
    <body>
      <div id="root"></div>
    </body>
  </html>

再新建src下边的index.js

这时候react-scripts会快速的帮我们定为到index.html以及引入index.js

  import React from "react";
  import ReactDOM from "react-dom";
  let jsx = (
    <div>
      <div className="">react启动成功</div>
    </div>
  );
  ReactDOM.render(jsx, document.getElementById("root"));

这样,一个可以写react源码的轮子就出来了。

React的源码

let obj = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
console.log(`obj=${ JSON.stringify( obj) }`);

首先,我们上述代码,如果我们不import React处理的话,我们可以打印出:

‘React’ must be in scope when using JSX react/react-in-jsx-scope

是的,编译不下去,因为js文件再react-script,他已经识别到obj是jsx。该jsx却不能解析成虚拟dom, 此时我们的页面就会报错。通过资料的查阅,或者是源码的跟踪,我们可以知道,实际上,识别到jsx之后,会调用页面中的createElement转换为虚拟dom。

我们import React,看看打印出来什么?

+ import React from "react";
let obj = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
console.log(`obj:${ JSON.stringify( obj) }`);
结果:
jsx={"type":"div","key":null,"ref":null,"props":{"children":{"type":"div","key":null,"ref":null,"props":{"className":"class_0","children":"你好"},"_owner":null,"_store":{}}},"_owner":null,"_store":{}}

由上边结论可以知道, babel会识别到我们的jsx,通过createElement并将其dom(html语法)转换为虚拟dom。从上述的过程,我们可以看到虚拟dom的组成,由type,key,ref,props组成。我们来模拟react的源码。

此时我们已经知道react中的createElement的作用是什么,我们可以尝试着自己来写一个createElement(新建react.js引入并手写下边代码):

function createElement() {
  console.log("createElement", arguments);
}
export default {
  createElement,
};

此时的打印结果:

我们可以看出对象传递的时候,dom的格式,先传入type, 然后props属性,我们根据原本react模拟一下这个对象转换的打印:

function createElement(type, props, ...children) {
  props.children = children;
  return {
    type,
    props,
  };
}

这样,我们已经把最简版的一个react实现,我们下边继续看看如何render到页面

ReactDom.render

import React from "react";
+ import ReactDOM from "react-dom";
let jsx = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
// console.log(`jsx=${ JSON.stringify( jsx) }`);
+ ReactDOM.render(jsx, document.getElementById("root"));

如果此时,我们引入ReactDom,通过render到对应的元素,整个简版react的就已经完成,页面就会完成渲染。首先,jsx我们已经知道是一个vnode,而第二个元素即是渲染上页面的元素,假设我们的元素是一个html原生标签div。

我们新建一个reactDom.js引入。

function render(vnode, container) {
  mount(vnode, container);
}
function mount(vnode, container){
    const { type, props } = vnode;
    const node = document.createElement(type);//创建一个真实dom
    const { children, ...rest } = props;
    children.map(item => {//子元素递归
        if (Array.isArray(item)) {
          item.map(c => {
            mount(c, node);
          });
        } else {
          mount(item, node);
        }
    });
    container.appendChild(node);
}
//主页:
- import React from "react";
- import ReactDOM from "react-dom";
+ import React from "./myReact/index.js";
+ import ReactDOM from "./myReact/reactDom.js";
let jsx = (
  <div>
    <div className="class_0">你好</div>
  </div>
);
ReactDOM.render(jsx, document.getElementById("root"));

此时,我们可以看到页面,我们自己写的一个react渲染已经完成。我们优化一下。

首先,这个过程中, className="class_0"消失了。我们想办法渲染上页面。此时,虚拟dom的对象,没有办法,区分,哪些元素分别带有什么属性,我们在转义的时候优化一下mount。

 function mount(vnode, container){
    const { type, props } = vnode;
    const node = document.createElement(type);//创建一个真实dom
    const { children, ...rest } = props;
    children.map(item => {//子元素递归
        if (Array.isArray(item)) {
          item.map(c => {
            mount(c, node);
          });
        } else {
          mount(item, node);
        }
    });
    // +开始
    Object.keys(rest).map(item => {
        if (item === "className") {
          node.setAttribute("class", rest[item]);
        }
        if (item.slice(0, 2) === "on") {
          node.addEventListener("click", rest[item]);
        }
      });
    // +结束
    container.appendChild(node);
}

ReactDom.Component

看到这里,整个字符串render到页面渲染的过程已完成。此时入口文件已经解决了。对于原始标签div, h1已经兼容。但是对于自定义标签呢?或者怎么完成组件化呢。

我们先看react16+的两种组件化模式,一种是function组件化,一种是class组件化。

首先,我们先看看demo.

import React, { Component } from "react";
import ReactDOM from "react-dom";
 class MyClassCmp extends React.Component {
  constructor(props) {
    super(props);
  }
  render() {
    return (
    <div className="class_2" >MyClassCmp表示:{this.props.name}</div>
    );
  }
}
function MyFuncCmp(props) {
  return <div className="class_1" >MyFuncCmp表示:{props.name}</div>;
}
let jsx = (
  <div>
    <h1>你好</h1>
    <div className="class_0">前端小伙子</div>
    <MyFuncCmp />
    <MyClassCmp  />
  </div>
);
ReactDOM.render(jsx, document.getElementById("root"));

先看简单点一些的Function组件。暂不考虑传递值等问题,Function其实跟原本组件不一样的地方,在于他是个函数,而原本的jsx,是一个字符串。我们可以根据这个特点,将函数转换为字符串,那么Function组件即跟普通标签同一性质。

我们写一个方法:

mountFunc(vnode, container);
function mountFunc(vnode, container) {
  const { type, props } = vnode;
  const node = new type(props);
  mount(node, container);
}

此时type即是函数体内容,我们只需要实例化一下,即可跟拿到对应的字符串,即是普通的vnode。再利用我们原来的vnode转换方法,即可实现。

按照这个思路,如果我们不考虑生命周期等相对复杂的东西。我们也相对简单,只需拿到类中的render函数即可。

mountFunc(vnode, container);
function mountClass(vnode, container) {
  const { type, props } = vnode;
  const node = new type(props);
  mount(node.render(), container);
}

这里可能需注意,class组件,需要继承React.Component。截图一下react自带的Component

可以看到,Component统一封装了,setState,forceUpdate方法,记录了props,state,refs等。我们模拟一份简版为栗子:

class Component {
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  setState = () => {};
}

再添加一个标识,isReactComponent表示是函数数组件化。这样的话,我们就可以区分出:普通标签,函数组件标签,类组件标签。

我们可以重构一下createElement方法,多定义一个vtype属性,分别表示

  • 普通标签
  • 函数组件标签
  • 类组件标签

根据上述标记,我们可改造为:

function createElement(type, props, ...children) {
  props.children = children;
  let vtype;
  if (typeof type === "string") {
    vtype = 1;
  }
  if (typeof type === "function") {
    vtype = type.isReactComponent ? 2 : 3;
  }
  return {
    vtype,
    type,
    props,
};

那么,我们处理时:

function mount(vnode, container) {
  const { vtype } = vnode;
  if (vtype === 1) {
    mountHtml(vnode, container); //处理原生标签
  }
  if (vtype === 2) {
    //处理class组件
    mountClass(vnode, container);
  }
  if (vtype === 3) {
    //处理函数组件
    mountFunc(vnode, container);
  }
}

至此,我们已经完成一个简单可组件化的react源码。不过,此时有个bug,就是文本元素的时候异常,因为文本元素不带标签。我们优化一下。

function mount(vnode, container) {
  const { vtype } = vnode;
  if (!vtype) {
    mountTextNode(vnode, container); //处理文本节点
  }
  //vtype === 1
  //vtype === 2
  // ....
}
//处理文本节点
function mountTextNode(vnode, container) {
  const node = document.createTextNode(vnode);
  container.appendChild(node);
}

简单源码

package.json:

{
  "name": "zwz_react_origin",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "react-scripts": "3.2.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",      "not dead",      "not op_mini all"    ],    "development": [      "last 1 chrome version",      "last 1 firefox version",      "last 1 safari version"    ]  }}

index.js

import React from "./wzReact/";
import ReactDOM from "./wzReact/ReactDOM";
class MyClassCmp extends React.Component {
  constructor(props) {
    super(props);
  }
render() {
    return (
    <div className="class_2" >MyClassCmp表示:{this.props.name}</div>
    );
  }
}
function MyFuncCmp(props) {
  return <div className="class_1" >MyFuncCmp表示:{props.name}</div>;
}
let jsx = (
  <div>
    <h1>你好</h1>
    <div className="class_0">前端小伙子</div>
    <MyFuncCmp name="真帅" />
    <MyClassCmp name="还有钱" />
  </div>
);
ReactDOM.render(jsx, document.getElementById("root"));

/wzReact/index.js

function createElement(type, props, ...children) {
  console.log("createElement", arguments);
  props.children = children;
  let vtype;
  if (typeof type === "string") {
    vtype = 1;
  }
  if (typeof type === "function") {
    vtype = type.isReactComponent ? 2 : 3;
  }
  return {
    vtype,
    type,
    props,
  };
}
class Component {
  static isReactComponent = true;
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  setState = () => {};
}
export default {
  Component,
  createElement,
};

/wzReact/ReactDOM.js

function render(vnode, container) {
  console.log("render", vnode);
  //vnode-> node
  mount(vnode, container);
  // container.appendChild(node)
}
// vnode-> node
function mount(vnode, container) {
  const { vtype } = vnode;
  if (!vtype) {
    mountTextNode(vnode, container); //处理文本节点
  }
  if (vtype === 1) {
    mountHtml(vnode, container); //处理原生标签
  }
  if (vtype === 3) {
    //处理函数组件
    mountFunc(vnode, container);
  }
  if (vtype === 2) {
    //处理class组件
    mountClass(vnode, container);
  }
}
//处理文本节点
function mountTextNode(vnode, container) {
  const node = document.createTextNode(vnode);
  container.appendChild(node);
}
//处理原生标签
function mountHtml(vnode, container) {
  const { type, props } = vnode;
  const node = document.createElement(type);
  const { children, ...rest } = props;
  children.map(item => {
    if (Array.isArray(item)) {
      item.map(c => {
        mount(c, node);
      });
    } else {
      mount(item, node);
    }
  });
  Object.keys(rest).map(item => {
    if (item === "className") {
      node.setAttribute("class", rest[item]);
    }
    if (item.slice(0, 2) === "on") {
      node.addEventListener("click", rest[item]);
    }
  });
  container.appendChild(node);
}
function mountFunc(vnode, container) {
  const { type, props } = vnode;
  const node = new type(props);
  mount(node, container);
}
function mountClass(vnode, container) {
  const { type, props } = vnode;
  const cmp = new type(props);
  const node = cmp.render();
  mount(node, container);
}
export default {
  render,
};

到此这篇关于React运行机制超详细讲解的文章就介绍到这了,更多相关React运行机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • React全局状态管理的三种底层机制探究

    目录 前言 props context state 总结 前言 现代前端框架都是基于组件的方式来开发页面.按照逻辑关系把页面划分为不同的组件,分别开发不同的组件,然后把它们一层层组装起来,把根组件传入 ReactDOM.render 或者 vue 的 $mount 方法中,就会遍历整个组件树渲染成对应的 dom. 组件都支持传递一些参数来定制,也可以在内部保存一些交互状态,并且会在参数和状态变化以后自动的重新渲染对应部分的 dom. 虽然从逻辑上划分成了不同的组件,但它们都是同一个应用的不同部分

  • React渲染机制超详细讲解

    目录 准备工作 render阶段 workloopSync beginWork completeWork commit阶段 commitWork mutation之前 mutation fiber树切换 layout layout之后 总结 准备工作 为了方便讲解,假设我们有下面这样一段代码: function App(){ const [count, setCount] = useState(0) useEffect(() => { setCount(1) }, []) const handl

  • React事件机制源码解析

    React v17里事件机制有了比较大的改动,想来和v16差别还是比较大的. 本文浅析的React版本为17.0.1,使用ReactDOM.render创建应用,不含优先级相关. 原理简述 React中事件分为委托事件(DelegatedEvent)和不需要委托事件(NonDelegatedEvent),委托事件在fiberRoot创建的时候,就会在root节点的DOM元素上绑定几乎所有事件的处理函数,而不需要委托事件只会将处理函数绑定在DOM元素本身. 同时,React将事件分为3种类型--d

  • React事件处理的机制及原理

    React中的事件处理 在React元素中绑定事件有两点需要注意: (1)在React中,事件命名采用驼峰命名方式,而不是DOM元素中的小写字母命名方式.例如onclick要写成onClick,onchange要写成onChange等. (2)处理事件的响应函数要以对象的形式赋值给事件属性,而不是DOM中的字符串形式.例如在DOM中绑定一个点击事件应该写成: <button onclick="clickButton()"> Click </button> 而在R

  • React运行机制超详细讲解

    目录 适合人群 写源码之前的必备知识点 JSX 虚拟Dom 原理简介 手写react过程 基本架子的搭建 React的源码 ReactDom.render ReactDom.Component 简单源码 适合人群 本文适合0.5~3年的react开发人员的进阶. 讲讲废话: react的源码,的确是比vue的难度要深一些,本文也是针对初中级,本意了解整个react的执行过程. 写源码之前的必备知识点 JSX 首先我们需要了解什么是JSX. 网络大神的解释:React 使用 JSX 来替代常规的

  • Python多进程并发与同步机制超详细讲解

    目录 多进程 僵尸进程 Process类 函数方式 继承方式 同步机制 状态管理Managers 在<多线程与同步>中介绍了多线程及存在的问题,而通过使用多进程而非线程可有效地绕过全局解释器锁. 因此,通过multiprocessing模块可充分地利用多核CPU的资源. 多进程 多进程是通过multiprocessing包来实现的,multiprocessing.Process对象(和多线程的threading.Thread类似)用来创建一个进程对象: 在类UNIX平台上,需要对每个Proce

  • MyBatis插件机制超详细讲解

    目录 MyBatis的插件机制 InterceptorChain MyBatis中的Plugin MyBatis插件开发 总结 MyBatis的插件机制 MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用.默认情况下,MyBatis 允许使用插件来拦截的方法调用包括: Executor(update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler(

  • React RenderProps模式超详细讲解

    目录 正文 使用Render Props来完成关注点分离 render prop的prop名不一定叫render 注意点 render prop是一个技术概念.它指的是使用值为function类型的prop来实现React component之间的代码共享. 如果一个组件有一个render属性,并且这个render属性的值为一个返回React element的函数,并且在组件内部的渲染逻辑是通过调用这个函数来完成的.那么,我们就说这个组件使用了render props技术. <DataProvi

  • JavaGUI事件监听机制超详细讲解

    1.一个事件模型中有上对象:事件源,事件以及监听程序 2.事件监听机制: 事件源        事件发生的地方 事件            要发生的事情 事件处理     针对发生的事情做出的处理方案 事件监听     把事件源和事件关联起来 使用步骤: 新建一个组件(如 JButton) 将该组件添加到相应的面板(如 JFrame) 注册监听器以监听事件源产生的事件(如通过ActionListener来响应用户点击按钮) 定义处理事件的方法(如在ActionListener中的actionPe

  • Java 超详细讲解Spring MVC异常处理机制

    目录 异常处理机制流程图 异常处理的两种方式 简单异常处理器SimpleMappingExceptionResolver 自定义异常处理步骤 本章小结 异常处理机制流程图 系统中异常包括两类: 预期异常 通过捕获异常从而获取异常信息. 运行时异常RuntimeException 主要通过规范代码开发.测试等手段减少运行时异常的发生. 系统的Dao.Service.Controller出现都通过throws Exception向上抛出,最后SpringMVC前端控制器交由异常处理器进行异常处理,如

  • Python超详细讲解内存管理机制

    目录 什么是内存管理机制 一.引用计数机制 二.数据池和缓存 什么是内存管理机制 python中创建的对象的时候,首先会去申请内存地址,然后对对象进行初始化,所有对象都会维护在一 个叫做refchain的双向循环链表中,每个数据都保存如下信息: 1. 链表中数据前后数据的指针 2. 数据的类型 3. 数据值 4. 数据的引用计数 5. 数据的长度(list,dict..) 一.引用计数机制 引用计数增加: 1.1 对象被创建 1.2 对象被别的变量引用(另外起了个名字) 1.3 对象被作为元素,

  • Spring超详细讲解事务和事务传播机制

    目录 为什么需要事务 Spring 声明事务 Transactional参数说明 propagation isolation timeout 事务回滚失效解决方案 @Transactional工作原理 Spring 事务的传播机制 为什么需要事务传播机制? 传播机制的类型 为什么需要事务 事务是将一组操作封装成一个执行单元,要么全部成功,要么全部失败.如果没有事务,转账操作就会出现异常,因此需要保证原子性. Spring 声明事务 只需要在方法上添加@Transactional注解就可以实现,无

  • Spring Boot超详细讲解请求处理流程机制

    目录 1. 背景 2. Spring Boot 的请求处理流程设计 3. Servlet服务模式请求流程分析 3.1 ServletWebServerApplicationContext分析 3.2 Servlet服务模式之请求流程具体分析 4. Reactive服务模式请求流程分析 4.1 ReactiveWebServerApplicationContext分析 4.2 webflux服务模式之请求流程具体分析 5. 总结 1. 背景 之前我们对Spring Boot做了研究讲解,我们知道怎

随机推荐