本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/…) 项目地址
环境准备
项目打包工具选择了 parcel,使用其可以快速地进入项目开发的状态。快速开始
此外需要安装以下 babel 插件:
1 2 3 4
| "@babel/core": "^7.0.0", "@babel/preset-env": "^7.0.0", "@babel/preset-react": "^7.0.0", "babel-loader": "v8.0.0-beta.0",
|
同时 .babelrc
配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { "presets": [ [ "@babel/preset-env", { "targets": "> 0.25%, not dead", "useBuiltIns": "entry" } ], [ "@babel/preset-react", { "pragma": "cpreact.createElement" } ] ] }
|
配置好 babel 后,接着提供两套打包工具的配置方案,读者可以自行选择。
方案 1:使用 webpack
webpack 拥有一个活跃的社区,提供了更为丰富的打包能力。
首先安装以下模块:
1 2 3
| "webpack": "^4.17.2", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.8"
|
在根目录的 webpack.config.js
配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const webpack = require('webpack') const path = require('path') const rootPath = path.resolve(__dirname)
module.exports = { entry: path.resolve(rootPath, 'test', 'index.js'), mode: 'development', devtool: 'inline-source-map', devServer: { contentBase: './dist' }, output: { filename: 'cpreact.js', path: path.resolve(rootPath, 'dist'), libraryTarget: 'umd' }, module: { rules: [{ test: /\.js$/, loader: "babel-loader", }] }, }
|
然后在 package.json
里加上如下配置:
1 2 3
| "scripts": { "start": "webpack-dev-server --open", },
|
具体可以参照 0.4.3 版本
方案 2:使用 parcel
parcel 是一款上手极快的打包工具,使用其可以快速地进入项目开发的状态。在 package.json
加上如下配置,具体可以参照 0.1 版本
1 2 3
| "scripts": { "start": "parcel ./index.html --open -p 8080 --no-cache" },
|
JSX 和 虚拟 DOM
1 2 3 4 5
| const element = ( <div className="title"> hello<span className="content">world!</span> </div> )
|
JSX 是一种语法糖,经过 babel 转换结果如下,可以发现实际上转化成 React.createElement()
的形式:
1 2 3 4 5 6 7 8 9 10
| var element = React.createElement( "div", { className: "title" }, "hello", React.createElement( "span", { className: "content" }, "world!" ) );
|
打印 element, 结果如下:
1 2 3 4 5 6
| { attributes: {className: "title"} children: ["hello", t] key: undefined nodeName: "div" }
|
因此,我们得出结论:JSX 语法糖经过 Babel 编译后转换成一种对象,该对象即所谓的虚拟 DOM
,使用虚拟 DOM 能让页面进行更为高效的渲染。
我们按照这种思路进行函数的构造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const React = { createElement }
function createElement(tag, attr, ...child) { return { attributes: attr, children: child, key: undefined, nodeName: tag, } }
const element = ( <div className="title"> hello<span className="content">world!</span> </div> )
console.log(element)
|
虚拟 DOM 转化为真实 DOM
上个小节介绍了 JSX 转化为虚拟 DOM 的过程,这个小节接着来实现将虚拟 DOM 转化为真实 DOM (页面上渲染的是真实 DOM)。
我们知道在 React 中,将虚拟 DOM 转化为真实 DOM 是使用 ReactDOM.render
实现的,使用如下:
1 2 3 4 5 6
| import ReactDOM from 'react-dom'
ReactDOM.render( element, document.getElementById('root') )
|
接着来实现 ReactDOM.render
的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| const ReactDOM = { render }
function render(vdom, container) { if (_.isString(vdom) || _.isNumber(vdom)) { container.innerText = container.innerText + vdom return } const dom = document.createElement(vdom.nodeName) for (let attr in vdom.attributes) { setAttribute(dom, attr, vdom.attributes[attr]) } vdom.children.forEach(vdomChild => render(vdomChild, dom)) container.appendChild(dom) }
function setAttribute(dom, attr, value) { if (attr === 'className') { attr = 'class' } if (attr.match('/on\w+/')) { const eventName = attr.toLowerCase().substr(2) dom.addEventListener(eventName, value) } else if (attr === 'style') { let styleStr = '' let standardCss for (let klass in value) { standardCss = humpToStandard(klass) value[klass] = _.isNumber(+value[klass]) ? value[klass] + 'px' : value[klass] styleStr += `${standardCss}: ${value[klass]};` } dom.setAttribute(attr, styleStr) } else { dom.setAttribute(attr, value) } }
|
至此,我们成功将虚拟 DOM 复原为真实 DOM,展示如下:
另外配合热更新,在热更新的时候清空之前的 dom 元素,改动如下:
1 2 3 4 5 6
| const ReactDOM = { render(vdom, container) { container.innerHTML = null render(vdom, container) } }
|
小结
JSX
经过 babel 编译为 React.createElement() 的形式,其返回结果就是 Virtual DOM
,最后通过 ReactDOM.render() 将 Virtual DOM 转化为真实的 DOM 展现在界面上。流程图如下:
思考题
如下是一个 react/preact 的常用组件的写法,那么为什么要 import 一个 React 或者 h 呢?
1 2 3 4 5 6 7 8 9 10
| import React, { Component } from 'react'
class A extends Component { render() { return <div>I'm componentA</div> } }
render(<A />, document.body)
|
项目说明
该系列文章会尽可能的分析项目细节,具体的还是以项目实际代码为准。
鸣谢
Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react