React是用于构建用户界面的 JavaScript
库。其有着许多优秀的特性,使其受到大众的欢迎。
① 声明式渲染:
所谓声明式,就是关注结果,而不是关注过程。好比咱们经常使用的html标记语言就是一种声明式的,咱们只须要在.html文件上,写上声明式的标记如<h1>这是一个标题</h1>
,浏览器就能自动帮咱们渲染出一个标题元素。一样react中也支持jsx的语法,能够在js中直接写html,因为其对DOM操做进行了封装,react会自动帮咱们渲染出对应的结果。html
② 组件化:
组件是react的核心,一个完整的react应用是由若干个组件搭建起来的,每一个组件有本身的数据和方法,组件具体如何划分,须要根据不一样的项目来肯定,而组件的特征是可复用,可维护性高。node
③ 单向数据流:
子组件对于父组件传递过来的数据是只读的。子组件不可直接修改父组件中的数据,只能经过调用父组件传递过来的方法,来间接修改父组件的数据,造成了单向清晰的数据流。防止了当一个父组件的变量被传递到多个子组件中时,一旦该变量被修改,全部传递到子组件的变量都会被修改的问题,这样出现bug调试会比较困难,由于不清楚究竟是哪一个子组件改的,把对父组件的bug调试控制在父组件之中。react
以后的内容,咱们将一步步了解React相关知识,而且简单实现一个react。webpack
刚接触react的时候,首先要了解的就是jsx语法,jsx实际上是一种语法糖,是js的一种扩展语法,它可让你在js中直接书写html代码片断,而且react推荐咱们使用jsx来描述咱们的界面,例以下面一段代码:es6
// 直接在js中,将一段html代码赋值给js中的一个变量 const element = <h1>Hello, react!</h1\>;
在普通js中,执行这样一段代码,会提示Uncaught SyntaxError: Unexpected token '<'
,也就是不符合js的语法规则。那么为何react可以支持这样的语法呢?
由于react代码在打包编译的过程当中,会经过babel进行转化,会对jsx中的html片断进行解析,解析出来标签名、属性集、子元素,而且做为参数传递到React提供的createElement方法中执行。如上面代码的转换结果为:web
// babel编译转换结果 const element = React.createElement("h1", null, "Hello, react!");
能够看到,babel转换的时候,识别到这是一个h1标签,而且标签上没有任何属性,因此属性集为null,其有一个子元素,纯文本"Hello, react!",因此通过babel的这么一个骚操做,React就能够支持jsx语法了。由于这个转换过程是由babel完成的,因此咱们也能够经过安装babel的jsx转换包,从而让咱们本身的项目代码也能够支持jsx语法。npm
由于咱们要实现一个简单的react,因为咱们使用react编程的时候是可使用jsx语法的,因此咱们首先要让咱们的项目支持jsx语法。
① 新建一个名为my-react的项目
在项目根目录下新建一个src目录,里面存放一个index.js做为项目的入口文件,以及一个public目录,里面存放一个index.html文件,做为单页面项目的入口html页面,如:编程
cd /path/to/my-react // 进入到项目根目录下 npm init --yes // 自动生成项目的package.json文件
// project_root/src/index.js 内容 const element = <h1>hello my-react</h1>;
// project_root/public/index.html 内容 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>my-react</title> </head> <body> <div id="root"></div> <script src="../src/index.js"></script> </body> </html>
② 安装 parcel-bundler
模块
parcel-bundler是一个打包工具,它速度很是快,而且能够零配置,相对webpack而言,不须要进行复杂的配置便可实现web应用的打包,而且能够以任何类型的文件做为打包入口,同时自动启动内置的web服务器方便调试。json
// 安装parcel-bundler npm install parcel-bundler --save-dev // 修改package.json,执行parcel-bundler命令并传递入口文件路径做为参数 { "scripts": { "start": "parcel -p 8080 ./public/index.html" } } // 启动项目 npm run start
parcel启动的时候会在8080端口上启动Web服务器,而且以public目录下的index.html文件做为入口文件进行打包,由于index.html文件中有一行<script src="../src/index.js"></script>
,因此index.html依赖src目录下的index.js,因此又会编译src目录下的index.js并打包。
此时执行npm run start
会报错,由于此时还不支持jsx语法的编译。数组
③ 安装@babel/plugin-transform-react-jsx
模块
安装好@babel/plugin-transform-react-jsx
模块后,还须要新建一个.babelrc文件,配置以下:
// .babelrc { "plugins": [ ["@babel/plugin-transform-react-jsx", { "pragma": "React.createElement" // default pragma is React.createElement }] ] }
其做用就是,遇到jsx语法的时候,将解析后的结果传递给React.createElement()方法,默认是React.createElement,能够自定义。此时编译就能够经过了,能够查看编译后的结果,以下:
var element = React.createElement("h1", null, "hello my-react");
此时项目虽然能编译jsx了,可是执行的时候会报错,由于尚未引入React以及其createElement()方法,React的createElement()方法做用就是建立虚拟DOM,虚拟DOM其实就是一个普通的JavaScript对象,里面包含了tag、attrs、children等属性。
① 在src目录下新建一个react目录
在react目录下新建一个index.js做为模块的默认导出,同时新建一个create-element.js做为createElement方法的实现,如:
// src/react/create-elemnt.js // 做用就是接收babel解析jsx后的结果做为参数,建立并返回虚拟DOM节点对象 function createElement(tag, attrs, ...children) { attrs = attrs || {}; // 若是元素的属性为null,即元素上没有任何属性,则设置为一个{}空的对象 const key = attrs.key || null; // 若是元素上有key,则去除key,若是没有则设置为null return { // 建立一个普通JavaScript对象,并将各属性添加上去,做为虚拟DOM进行返回 key, tag, attrs, children } } export default createElement;
babel解析jsx后,若是有多个子节点,那么全部的子节点都会以参数的形式传入createElement函数中,因此createElement的第三个参数能够用es6剩余参数语法,以一个数组的方式来接收全部的子节点。
// src/react/index.js import createElement from "./create-element"; // 引入createElement函数 export default { createElement // 将createElement函数做为react的方法导出 }
至此,react上已经添加了createElement函数了,而后在src/index.js中引入react模块便可。
// src/index.js import React from "./react"; // 引入react模块 const element = <h1>hello my-react</h1>; console.log(element);
引入react后,因为React上有了createElement方法,因此能够正常执行,而且拿到返回的虚拟DOM节点,以下:
此时,咱们已经可以拿到对应的虚拟DOM节点了,因为虚拟DOM只是一个普通的JavaScript对象,不是真正的DOM,因此须要对虚拟DOM进行render,建立对应的真实DOM并添加到页面中,才能在页面中看到,react中专门提供了一个ReactDOM模块用于处理DOM相关的操做。
① 在src目录下新建一个react-dom目录
在react-dom目录下新建一个index.js做为模块的默认导出,同时新建一个render.js做为render方法的实现,render函数须要接收一个虚拟DOM节点和一个挂载点,即将虚拟DOM渲染成了真实DOM后,须要将其挂载到哪里,这个挂载点就是一个容器,即应用的根节点。
// src/react-dom/render.js // 接收一个虚拟DOM节点,而且将虚拟DOM节点渲染成真实的DOM节点,而后添加到container容器中进行挂载 function render(vnode, container) { // 因为ReactDOM能够直接渲染一段文本,因此这个vnode多是一个字符串或数字 if (typeof vnode === "string" || typeof vnode === "number") { const textNode = document.createTextNode(vnode); // 直接建立一个文本节点 container.appendChild(textNode); // 将建立的文本节点添加到容器中便可 return } // 若是vnode不是一个字符串,那么其就是一个虚拟DOM对象,那么咱们根据tag名建立对应的真实DOM元素便可 const element = document.createElement(vnode.tag); // 处理属性 Object.keys(vnode.attrs).forEach((key) => { if (key === "className") { key = "class"; } const value = vnode.attrs[key]; if (typeof value === "function") { // 若是属性值为一个函数,说明是绑定事件 element[key.toLowerCase()] = value; } else { element.setAttribute(key, vnode.attrs[key]); } }); // 处理子节点,递归渲染子节点 vnode.children.forEach((child) => { return render(child, element); }); container.appendChild(element); // 将渲染好的虚拟DOM节点添加到容器中 } export default render;
render函数主要就是判断传递过来的vnode是一个字符串仍是虚拟DOM节点对象,若是是字符串,那么直接建立文本节点并添加到容器中便可;若是是虚拟DOM对象,那么根据其tag建立对应的真实DOM元素节点,而后遍历节点属性,将其添加到元素节点上,再处理子节点,遍历子节点递归渲染便可,整个虚拟DOM渲染好以后,将其加到容器中便可。
// src/react-dom/index.js import render from './render' // reactDOM主要负责DOM相关的操做,好比虚拟DOM的渲染、虚拟DOM与真实DOM的diff比较 export default { // react-dom只须要提供一个render()方法便可 render }
// src/index.js import React from "./react"; // 引入react模块 import ReactDOM from "./react-dom"; // 引入react-dom模块 function doClick() { console.log("doClick method run."); } const element = <h1 onClick={doClick}>hello my-react</h1>; console.log(element); ReactDOM.render(element, document.getElementById("root"));
这里绑定了一个onClick事件,此时启动项目执行,能够看到页面上已经能看到渲染后的结果了,而且点击文字,能够看到事件处理函数执行了。
此时已经完成了基本的声明式渲染功能了,可是目前只能渲染html中存在的标签元素,而咱们的react是支持自定义组件的,可让其渲染出咱们自定义的标签元素。react中的组件支持函数组件和类组件,函数组件的性能比类组件的性能要高,由于类组件使用的时候要实例化,而函数组件直接执行函数取返回结果便可。为了提升性能,尽可能使用函数组件。可是函数组件没有this、没有生命周期、没有本身的state状态。
① 首先实现函数组件功能
函数组件相对较简单,咱们先看一下怎么使用函数组件,就是直接定义一个函数,而后其返回一段jsx,而后将函数名做为自定义组件名,像html标签元素同样使用便可,如:
// src/index.js import React from "./react"; // 引入react模块 import ReactDOM from "./react-dom"; // 引入react-dom模块 function App(props) { return <h1>hello my-{props.name}-function</h1> } console.log(<App name="react"/>); ReactDOM.render(<App name="react"/>, document.getElementById("root"));
<App name="react"/>
通过babel转换以后,tag就变成了App函数,因此咱们不能直接经过document.createElement("App")去建立App元素了,咱们须要执行App()函数拿到其返回值<h1>hello my-{props.name}</h1>
,而这个返回值是一段jsx,因此会被babel转换为一个虚拟DOM节点对象,而后把虚拟DOM节点替换成函数组件执行返回的结果这个时候,tag就变成了h1了,就能够建立对应的DOM元素了,如:
// 修改src/react-dom/render.js function render(vnode, container) { ...... if (typeof vnode.tag === "function") { // 这是一个函数组件 vnode = vnode.tag(vnode.attrs || {}); // 执行函数组件,并传入属性集,拿到对应的虚拟DOM节点便可 } ...... }
把函数组件执行后,拿到的结果做为新的虚拟DOM节点对象,就能够根据tag建立对应的真实DOM了。
②支持类组件
在定义类组件的时候,是经过继承React.Component类的,咱们须要一个定义一个isReactComponent用于判断是不是react的组件,在src/react目录下新建一个component.js文件,以下:
// src/react/component.js class Component { constructor(props = {}) { this.isReactComponent = true; // 是react组件 this.props = props; // 保存props属性集 this.state = {}; // 保存状态数据 } } export default Component;
咱们在看一下类组件的使用方式,以下:
// src/index.js class App extends React.Component { constructor(props) { super(props); this.state = { count: 0 } } render() { return <h1>hell my-{this.props.name}-class-state-{this.state.count}</h1> } } console.log(<App name="react"/>); ReactDOM.render(<App name="react"/>, document.getElementById("root"));
<App name="react"/>
组件通过babel转换后,tag变成了一个class函数,若是class类函数的原型上有render()方法,那么就是一个类组件,咱们能够经过类组件的类名建立出对应的类组件对象,而后调用其render()函数拿到对应的虚拟DOM节点便可。
// 修改src/react-dom/render.js function render(vnode, container) { ...... if (vnode.tag.prototype && vnode.tag.prototype.render) { // 这是一个类组件 vnode = new vnode.tag(vnode.attrs).render(); // 根据类组件名建立组件实例并调用render()函数拿到对应的虚拟DOM节点 } ...... }
react中setState是Component中的一个方法,用于修改组件的状态数据的。当组件中调用setState函数的时候,组件的状态数据被更新,同时会触发组件的从新渲染,因此须要修改Component.js并在其中添加一个setState函数。如:
// src/react/component.js import ReactDOM from "../react-dom" class Component { constructor(props = {}) { this._container = null; // 保存组件所在容器 } setState(newState) { Object.assign(this.state, newState); // 更新状态数据 ReactDOM.render(this, this._container); // 从新渲染组件 } }
新增了一个_container属性,用于保存组件所在的容器,当传递过来新状态数据的时候,更新状态数据,并从新渲染组件实例。由于setState()函数执行的时候,渲染的时候是直接传递的组件实例,因此咱们须要对render()函数进行修改,新增,若是是直接渲染组件实例的状况,如:
function render(vnode, container) { if (vnode.isReactComponent) { // 若是是直接渲染类组件 const component = vnode; component._container = container; vnode = vnode.render(); // 直接调用组件对象的render()方法拿到对应的虚拟DOM开始渲染 } if (vnode.tag.prototype && vnode.tag.prototype.render) { // 这是一个类组件 const component = new vnode.tag(vnode.attrs); // 建立类组件实例 component._container = container; // 第一次渲染组件实例的时候,须要保存组件所在的容器 vnode = component.render(); } }
主要是,在组件第一次渲染完成的时候,将组件所在的容器保存到了_container上,而后再次渲染的时候,直接调用render()函数更新虚拟DOM节点便可。
// src/index.js上测试 class App extends React.Component { constructor(props) { super(props); this.state = { count: 0 } } doClick() { this.setState({ count: 1 }); } render() { return <h1 onClick={this.doClick.bind(this)}>hell my-{this.props.name}-class-state-{this.state.count}</h1> } } console.log(<App name="react"/>); ReactDOM.render(<App name="react"/>, document.getElementById("root"));
此时点击文本区域,能够看到从新渲染出了一个组件,可是旧的组件还在,由于从新渲染的时候,以前的内容并无清空,因此须要在render()函数执行的时候,清空以前渲染的内容,能够将render函数名修改成_render,而后从新定义一个render函数,render函数中先清空以前渲染的内容,而后调用_rener函数,如:
function _render(vnode, container) { // 以前rener的内容 vnode.children.forEach((child) => { return _render(child, element); // 这里递归的时候也要改为_render }); } function render(vnode, container) { container.innerHTML = ""; // 清空以前渲染的内容 _render(vnode, container); }
这里目前只先支持componentWillMount
和componentWillUpdate
两个生命周期,咱们只须要在类组件第一个渲染的位置进行判断,组件实例上是否有componentWillMount
便可,若是有则表示类组件即将渲染,而后在类组件从新渲染的位置进行判断,组件实例是否有componentWillUpdate
,若是有则表示组件即将更新,如:
function _render(vnode, container) { if (vnode.isReactComponent) { // 若是是直接渲染类组件 const component = vnode; if (component.componentWillUpdate) { // 这里是组件从新渲染,若是有componentWillUpdate触发组件即将更新生命周期 component.componentWillUpdate(); } component._container = container; vnode = vnode.render(); // 直接调用组件对象的render()方法拿到对应的虚拟DOM开始渲染 } if (vnode.tag.prototype && vnode.tag.prototype.render) { // 这是一个类组件 const component = new vnode.tag(vnode.attrs); // 建立类组件实例 if (component.componentWillMount) { // 这里是类组件第一次渲染,若是有componentWillMount,则触发组件即将挂载生命周期 component.componentWillMount(); } component._container = container; vnode = component.render(); } }
至此,已经基本实现react的基本功能,包括声明式渲染、组件支持、setSate、生命周期。其过程为,首先经过babel将jsx语法进行编译转换,babel会将jsx语法解析为三部分,标签名、属性集、子节点,而后用React.createElement()函数进行包裹,react实现createElement函数,用于建立虚拟DOM节点,而后调用render()函数对虚拟DOM节点进行分析,并建立对应的真实DOM,而后挂载到页面中。而后提供自定义组件的支持,自定义组件,无非就是将jsx定义到了函数和类中,若是是函数,那么就直接执行就可返回对应的jsx,也即拿到了对应的虚拟DOM,若是是类,那么就建立组件类实例,而后调用其render()函数,那么也能够拿到对应的jsx,也即拿到了对应的虚拟DOM,而后挂载到页面中。类组件中添加setSate函数,用于更新组件实例上的数据,而后setState函数会触发组件的从新渲染,从而更新渲染出带最新数据的组件。