本文适合0.5~3年的react开发人员的进阶。
讲讲废话:
react的源码,的确是比vue的难度要深一些,本文也是针对初中级,本意让博友们了解整个react的执行过程。
上一篇,从mini源码分析vue,也许深度比较一般,但是也花了好几天的时间去汇总,明白一个博主的不易。这是笔者第一篇获得上百个赞,还有评论上的好评,还有临时关注的几十粉丝。这是我写博客的动力。
这周十分繁忙的条件下,还是抽出时间汇总react。如果觉得不错,还是点个赞吧(三大步最喜欢了)
如何还需要学习vue源码以及vue相关知识点的朋友,建议移步:juejin.im/post/5f0326…
首先我们需要了解什么是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,最终渲染出页面。
这里说明一下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')) 复制代码
为什么呢?可以这样理解,在我们上述的js文件中,我们使用了jsx。但是jsx并不能给编译,所以,报错了。这时候,需要引入react,而react的作用,就是把jsx转换为“虚拟dom”对象。
JSX本质上就是转换为React.createElement在React内部构建虚拟Dom,最终渲染出页面。而引入React,就是为了时限这个过程。
理解好这一步,我们再看ReactDOM。React将jsx转换为“虚拟dom”对象。我们再利用ReactDom的虚拟dom通过render函数,转换成dom。再通过插入到我们的真是页面中。
这就是整个mini 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源码的轮子就出来了。
第三种,你还懒的话,直接把笔者的项目导下来算了。
链接:github.com/zhuangweizh…
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到页面
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); } 复制代码
看到这里,整个字符串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, }; 复制代码
至此,本文mini简单版本源码结束,代码将在文章最后段送出。 因本文定位初中级, 没有涉及react全家桶。 下一篇,fiber,redux, hooks等概念或者源码分析,将在新文章汇总出。如对你有用,关注期待后续文章。