手写一个react,看透react运行机制
创始人
2024-05-06 06:45:54
0

适合人群

本文适合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 (
Hello Word
)实际上是: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 
你好
} ReactDOM.render(, 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

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

    然后新建public下边的index.html

      

    再新建src下边的index.js

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

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

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

2) React的源码

let obj = (
你好
); 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 = (
你好
); 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 = (
你好
); // 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);//创建一个真实domconst { 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 = (
你好
); 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);//创建一个真实domconst { 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 (
MyClassCmp表示:{this.props.name}
);}}function MyFuncCmp(props) {return
MyFuncCmp表示:{props.name}
; } let jsx = (

你好

前端小伙子
); 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属性,分别表示

    1. 普通标签
    1. 函数组件标签
    1. 类组件标签

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

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 (
MyClassCmp表示:{this.props.name}
);} }function MyFuncCmp(props) {return
MyFuncCmp表示:{props.name}
; }let jsx = (

你好

前端小伙子
);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-> nodemount(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,
};

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

相关内容

热门资讯

安卓系统计划软件推荐,精选计划... 你有没有发现,手机里的安卓系统越来越智能了?这不,最近我可是挖到了一些超棒的安卓计划软件,它们不仅能...
收钱吧安卓系统插件,便捷支付新... 你有没有发现,现在的生活越来越离不开手机了?手机里装满了各种应用,而今天我要跟你聊聊一个特别实用的工...
鸿蒙系统是否还属于安卓,独立于... 你有没有想过,那个在我们手机上默默无闻的鸿蒙系统,它到底是不是安卓的“亲戚”呢?这个问题,估计不少手...
安卓系统手机用什么钱包,轻松管... 你有没有想过,你的安卓系统手机里装了那么多应用,但最离不开的,可能就是那个小小的钱包了。没错,就是那...
安卓系统能玩部落冲突吗,部落冲... 你有没有想过,安卓系统上的手机,是不是也能玩那款风靡全球的《部落冲突》呢?这款游戏自从推出以来,就吸...
智能机器人安卓系统,引领未来智... 你知道吗?在科技飞速发展的今天,智能机器人已经不再是科幻电影里的专属了。它们正悄悄地走进我们的生活,...
华为win10系统改装安卓系统... 你有没有想过,你的华为笔记本电脑里的Windows 10系统,能不能来个华丽变身,变成安卓系统呢?这...
旧电脑上安什么安卓系统,适配不... 你那台旧电脑是不是已经闲置好久了?别让它默默无闻地躺在角落里,给它来个华丽变身吧!今天,就让我来告诉...
安卓app语言跟随系统,随系统... 你知道吗?在手机世界里,有一个神奇的小功能,它就像你的贴身翻译官,无论你走到哪里,都能帮你轻松应对各...
惠城安卓系统降级在哪,揭秘降级... 你有没有遇到过手机系统升级后,发现新系统让你头疼不已,想回到那个熟悉的安卓系统呢?别急,今天就来告诉...
阿里云系统转安卓,揭秘安卓平台... 你知道吗?最近有个大动作在互联网圈里引起了不小的波澜,那就是阿里云系统竟然要转战安卓阵营了!这可不是...
安卓系统有最美壁纸么,探寻最美... 哦,亲爱的安卓用户,你是否曾在某个午后,百无聊赖地翻看着手机,突然被那一张张壁纸惊艳了眼眸?是的,我...
安卓系统采用Linux操作系统... 你知道吗?安卓系统,这个在我们手机上无处不在的小家伙,它的心脏竟然是Linux操作系统内核!是不是觉...
安卓原生平板通用系统,探索安卓... 你有没有发现,现在市面上平板电脑的品牌和型号真是五花八门,让人挑花了眼?不过,你知道吗?在众多安卓平...
小米1系统是安卓几,搭载安卓几... 你有没有想过,你的小米手机里那个熟悉的系统,其实是基于安卓的哦!没错,就是那个全球最流行的手机操作系...
可以安装安卓系统的相机,智能摄... 你有没有想过,一台相机不仅能拍出美美的照片,还能像智能手机一样,玩转各种应用?没错,现在市面上就有这...
安卓系统gps定位不准,安卓G... 你是不是也遇到过这种情况?手机里的安卓系统GPS定位总是不准,让人头疼不已。有时候,你明明就在家附近...
电信机顶盒装安卓系统,开启智能... 你有没有想过,家里的电信机顶盒其实也可以装上安卓系统呢?听起来是不是有点不可思议?别急,让我带你一步...
安卓系统可以做苹果桌面,打造个... 你知道吗?现在科技的发展真是让人眼花缭乱,竟然有人想出了安卓系统可以做苹果桌面的神奇想法!是不是觉得...
安卓系统自带的网页,功能与特色... 你有没有发现,每次打开安卓手机,那熟悉的系统界面里总有一个默默无闻的小家伙——安卓系统自带的网页浏览...