写点什么

手写一个 react,看透 react 运行机制

作者:goClient1992
  • 2022-11-24
    浙江
  • 本文字数:6656 字

    阅读完需:约 22 分钟

适合人群

本文适合 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 过程

1)基本架子的搭建

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


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


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

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

  • 然后新建 public 下边的 index.html

  • 再新建 src 下边的 index.js

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

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

2) 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 到页面

3) 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);}
复制代码

4) 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-> nodefunction 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,};
复制代码


至此,本文 mini 简单版本源码结束,代码将在文章最后段送出。因本文定位初中级, 没有涉及 react 全家桶。下一篇,fiber,redux, hooks 等概念或者源码分析,将在新文章汇总出。如对你有用,关注期待后续文章。


用户头像

goClient1992

关注

还未添加个人签名 2021-12-03 加入

还未添加个人简介

评论

发布
暂无评论
手写一个react,看透react运行机制_React_goClient1992_InfoQ写作社区