认识react脚手架
使用create-react-app
快速搭建一个react脚手架。首先必须全局安装:npm i create-react-app -g
create-react-app --version
可以查看当前安装版本号
使用该命令创建一个项目:create-react-app 项目名
(项目名字需要遵循npm规范),使用该命令创建的react项目是基于webpack完成的。创建完成的目录结构如下
-
在
package.json
文件中默认会安装:react(核心框架)
和react-dom(基于React构建的WebAp,即HTML页面)
。(react-native
构建app)。 -
react-scripts
是脚手架中自己对打包命令的封装,基于它打包会调用node_modules中的webpack进行处理。(react脚手架为了简洁目录结构,并没有vue的配置文件等,将webpack打包的规则及其插件/loader等隐藏到了node_modules目录下)。
start
是开发环境,本地启动web服务器运行。build
是生成环境,打包部署,将打包的内容放在build目录中。test
是单元测试。eject
暴露webpack配置,供用户修改打包规则。
-
web-vitals
是性能检测工具 -
browserslist
是设置浏览器的兼容情况,如postcss-loader+autoprefixer
设置css3相关,babek-loader
设置ES版本兼容。
在react中入口文件名为index,js
。删除部分引入文件保留初始代码如下。<React.StrictMode>
是react专属的严格模式。
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<div>hello</div>
</React.StrictMode>
);
将webpack配置向外暴露
执行命令npm run eject
。控制台给出选项选择,该选项的意思是一旦将配置项暴露出来后就是永久行为,无法复原。
但是这个时候会发现选择yes之后,紧跟着报了错误,这是因为我们之前修改过代码删除过文件,git历史区并没有我们目前修改过代码的记录。解决方法:先将我们修改后的代码提交到git历史区保留备份(防止暴露后的代码覆盖了我们的代码)
执行git步骤
git add -A
:将工作目录中的所有更改(包括新文件、修改的文件和已删除的文件)添加到暂存区,准备进行提交git commit -m 'init'
:提交暂存区中的更改到本地代码仓库,并附带一条简短的提交消息 “init”。完成之后再次执行npm run eject
命令即可
出现如下图所示即代表暴露成功。同时文件目录中也会多一个config
目录文件(webpack的配置相关信息)和scripts
目录。
在scripts
目录中结构如图,这里保存的是执行命令的入口文件。如执行npm run start
就会找到该文件中的start.js
文件去执行
当向外暴露webpack信息后,packjson
文件中的代码也会发送改变这里简单介绍几个:babel-preset-react-app
:原@babel/preset-env
语法包的重写,实现ES版本转换。重写后可以识别react语法,实现代码转换sass-loader
:使用create-react-app
命令默认会按照sass预编译语言- 同时多出一个如下代码结构,类似编写
babel.config.js
配置文件,这里是对babel-loader
的额外配置。如果自己后期需要修改babel,直接在这里修改即可,不需要再次创建文件。
"babel": {
"presets": [
"react-app"
]
}
修改webpack配置
自定义less预编译语言
在默认环境下,脚手架给我们安装的是sass-loader
预编译语言,我们可以替换。步骤如下
npm uninstall sass-loader
:先移除旧的预编译器npm i less
和npm i less-loader@8
:安装less预编译,注意版本不能太高,否则不兼容。
修改配置信息
修改sass为less直接在向外暴露的webpack.config.js
目录中修改即可。具体位置在return返回值中的module配置项内部的rules配置项中的oneOf下
。该位置处理了相关文件信息。在这里修改相关的代码。
首先将页面中原sass
相关的代码全部修改为less
。之后在rule配置项中将sass相关替换。
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
const lessRegex = /\.less$/;
const lessModuleRegex = /\.module\.less$/;
{
test: lessRegex,
exclude: lessModuleRegex,
use: getStyleLoaders(
。。。
'less-loader'
),
sideEffects: true,
},
{
test: lessModuleRegex,
use: getStyleLoaders(
....
'less-loader'
),
},
设置别名
在return返回中的resolve配置项中的alias中配置路径别名
。(在path.js
文件中已经帮助我们处理指向src目录的方法,只需要调用即可)。
alias: {
'@': paths.appSrc,
}
在src目录下创建一个index.less文件并添加样式,并在index.js
入口文件中使用别名引入。经过测试,页面可以正常按样式显示。
在目录下创建jsconfig.json
添加代码别名提示
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}
设置域名
默认情况,使用的域名和端口号为:http://localhost:3000/。如果想修改url信息,可以进入scripts/start.js
文件修改。(为什么在这里修改,可以简单理解用户启动本地服务器使用npm run start
命令,该命令会去执行"start": "node scripts/start.js"
最后进入scripts/start.js
,在这里会生成域名信息)
在文件中修改域名信息代码如下,其中process.env.HOST
和process.env.PORT
均为环境变量。可以直接修改后面的默认3000端口号或0.0.0.0域名信息
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0'; //这里修改为127.0.0.1并打开页面查看,发现域名已经修改
如果想使用环境变量去设置域名信息需要安装:npm i cross-env
。然后在package.json
文件中在设置入口路径的时候,直接设置环境变量。
"scripts": {
"start": "cross-env PORT=8080 node scripts/start.js",
}
设置浏览器兼容
在browserslist
配置项中实现浏览器兼容,会完成以下步骤
- 对
postcss-loader
生效:控制CSS3的前缀 - 对
babel-loader
生效:控制ES6的转换
但是无法处理ES6内容的API兼容问题。在webpack中我们需要借助@babel/polyfill
对常见内置API进行重写。但是在脚手架中不需要手动引入安装,脚手架中已经默认帮助我们安装好了react-app-polyfill
库,该库就是对@babel/polyfill
进行重写。
直接在index.js
入口文件中引入使用,其中stable是提供新语法,以便在旧浏览器中可以运行。ie9和ie11是为了让新特性能够兼容旧浏览器进行的版本兼容处理。*
// 处理浏览器兼容
import 'react-app-polyfill/ie9'
import 'react-app-polyfill/ie11'
import 'react-app-polyfill/stable'
配置跨域代理
react中处理跨域,是在config/webpackDevServe.config.js
中完成代理。具体代码如下,会先根据paths.proxySetup
读取config/path.js
文件下导出的proxySetup: resolveApp('src/setupProxy.js')
代码部分,该代码的意思是读取src/setupProxy.js
文件处理跨域代理。因此我们需要在在该文件中处理。
proxy,
onBeforeSetupMiddleware(devServer) {
devServer.app.use(evalSourceMapMiddleware(devServer));
if (fs.existsSync(paths.proxySetup)) {
require(paths.proxySetup)(devServer.app);
}
},
创建完成setupProxy.js
文件后需要安装处理跨域的库:npm i http-proxy-middleware
,并引入createProxyMiddleware
方法编写代码如下。这种方式的优点就是可以配置多个代理
注意这里的http-proxy-middleware是2.0.6的版本,这种写法是可以的,但是在最新的3.0版本中更换api了
const { createProxyMiddleware } = require('http-proxy-middleware')
module.exports = function (app) {
// 第一个代理地址
app.use(
createProxyMiddleware('/zhi', { //2.0.6版本写法
target: "https://news-at.zhihu.com/api/4",
changeOrigin: true,
ws: true,
pathRewrite: { "^/zhi": "" } //重写
})
)
}
之后编写测试代码
fetch('/zhi/news/latest')
.then(res => res.json())
.then(val => {
console.log(val);
})
最新的http-proxy-middleware
3.0版本使用legacyCreateProxyMiddleware 来处理代理,createProxyMiddleware 可以使用,但是地址重写的时候一直出错,最后看官网说使用新api代替
const { legacyCreateProxyMiddleware } = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
"/api",
legacyCreateProxyMiddleware({
target: "http://localhost:4455",
pathRewrite: {
"^/api": "/v1",
},
changeOrigin: true,
ws: true,
})
);
};
MVC和MVVM模式
采用数据推动视图更新,不直接操作DOM元素。
在原生js中操作DOM可能会引起回流和重绘,这样子会降低性能,并且每次操作的时候都需要实现将需要的DOM元素获取,这样子过于麻烦。
数据驱动思想
- 非必要情况下不直接操作DOM
- 修改数据,框架会帮助我们根据数据取驱动视图的改变,重新渲染页面
- 框架底层采用了虚拟DOM->真实DOM的算法比对。既避免了回流和重绘,也能大大提升性能和效率
下面是一段react中点击按钮实现计数功能
class Count extends React.Component {
state = { num: 0 }
render() {
let { num } = this.state
return <>
<span>{num}</span>
<br />
<button onClick={() => {
num++
this.setState({ num: num })
}}>点击</button>
</>
}
}
root.render(
<Count></Count>
);
MVC和MVVM区别
- React基于MVC模式,属于单向数据绑定,即数据驱动视图
- M:Model数据层 + View:视图层 + Controller:控制层
- 使用React的语法构建视图(React基于JSX语法构建视图)
- 构建数据层:但凡是在视图层中,需要动态变化的内容,不论样式还是内容,都需要有对应的数据模型
- 控制层:当我们对视图层中的数据进行改变的时候,React会拿着最新的数据去重新渲染视图
- React属于单向数据驱动(数据驱动视图渲染),但是并不是说React无法执行视图驱动数据这个操作,我们可以基于自己的代码实现。步骤:监听表单元素change事件,触发的时候获取最新表单内容,手动修改内容,将值赋值给表单的value属性。(类型vue的v-model)
- Vue基于MVVM,属于双向数据绑定,数据能驱动视图功能,视图中如表单操作也能更新数据
- M:Model数据层,V:View视图层,VM:ViewModel视图模型层
构建视图
在react中构建视图就需要了解什么是JSX。
JSX:JavaScript and Xml。js和xml或html混合 (html是提供好的标签语言,我们直接使用即可。而Xml是自定义标签。需要手动定义)。
在脚手架默认生成的index.js
文件如下,其中既有js代码又有html代码。
import React from 'react'; //引入react框架核心语法
import ReactDOM from 'react-dom/client'; //引入构建HTML(webapp)页面核心
import '@/index.less'
// 获取唯一根容器,将render中的内容最终放到跟容器中,与vue一致
const root = ReactDOM.createRoot(document.getElementById('root'));
let str = 'hello world'
// 基于render方法渲染编写的视图
root.render(
<div>
{str}
</div>
);
在vscode中,默认情况下,会发现在上面代码中的render
函数内部编写html代码片段的时候没有提示。那么如何让vscode能够识别支持JSX语法,即代码提示。
有两种方法让vscode有够实现支持JSX语法
- 第一种方法,前提入口文件为
index.js
,先点击vscode底部该标签,然后在弹出的输入框中输入react,选择第一个即可。这个时候html的快捷操作就可以实现了。缺点是每次都需要手动选择。
- 方法二:直接将
index.js
文件的后缀名修改为index.jsx
,该操作会默认执行方法一。在webpack.config.js
配置规则中,会jsx后缀的文件按照js去处理。
如果想在HTML中使用JS表达式,需要使用{}包裹js部分代码。区分Vue的{{}}。在react中{}
中js表达式需要写有返回值的。如下代码中,数组的map方法一定会返回一个值所以不报错,但是forEach不行。且普通的for,for/in等循环会报错。
root.render(
<div>
{[1, 2, 3].map(item => <h1>{item}</h1>)}
</div>
);
注意点:
- 不能指定body或html为根容器,需要额外指定容器
const root = ReactDOM.createRoot(document.body); //错误
- 构建视图的
render函数
中,只能有一个根节点,这一点和vue2一致。因此必须使用一个根标签包裹内容。但是这样子可能会额外的添加无用的标签增加层级,这个时候react为我们提供了一个空文档标签或React.Fragment:<></>
或<React.Fragment></React.Fragment>
。这样子该标签最终不会出现在页面中。查看该博客了解
root.render(
<>
<div>{str}</div>
<h1>你好</h1>
</>
);
root.render(
<React.Fragment>
<div>{str}</div>
<h1>你好</h1>
</React.Fragment>
);
JSX中{}的应用举例
如下代码中,给params赋予不同类型的值,查看页面打印效果
let params = 1
root.render(
<>
{params}
</>
);
- 如果直接为数值型 1,1.111,-10这种情况,赋什么值直接显示什么值。但是1.00,最终会被解析为1显示
- 字符串赋什么值,页面就显示什么值
- 赋予
Infinity
,NaN
,最终页面显示的Infinity
,NaN
,而不是显示空 - 当赋值
布尔值
,undefined
,null
,Symbol()
,10n
,void 0(特殊的null表达形式)
页面什么也不会显示 - 如果直接赋值一个对象给变量,而在
{}
括弧中直接使用该标签变量的时候,react会直接报错(vue中可以使用)。但是在{params.age}
就没有问题吗,因为这样就是操作一个普通数值数据。
let params = { age: 29 }
root.render(
<>
{params}
</>
);
- 如果直接赋值一个数组给变量,那么react会将数组中的每一个元素分别渲染到页面上。如下代码,最终页面渲染结果:123。一旦数组中存在一个对象,那么这样子就会报错。解决方法参照上面对象的处理。
let params = [1, 2, 3] // 若为[1, 2, { age: 2 }]
root.render(
<>
{params}
</>
);
- 如果赋值一个函数给变量,则不支持在
{}
中渲染,会报错,react认为这应该是一个组件,需要参照函数组件是使用方式,即 < params/> 。
let params = function () { }
root.render(
<>
{params}
</>
);
- 使用
new
关键字的特殊情况,如new Number(1)
,new RegExp(/\d+/)
,/\d+/
等会报错。但是new Array(1, 2)
和new Array(5).fill(1)
不会出现问题。(重点了解) - JSX的虚拟DOM对象支持在
{}
里面渲染
*:在JSX模板中的代码最终都和被转化为为React.createElement()
形式,而该方法会返回一个虚拟DOM对象,这是被允许的,所以可以在JSX语法中使用如下代码:{React.createElement('button', { className: 'btn' }, '按钮')}
。这样子会直接创建一个虚拟DOM对象并最终转化为真实DOM放在页面中。 - 给元素设置style的行内样式的时候,必须使用对象格式。格式:
style={{marginRight: spacing + 'em'}}
。(vue无该事项)
*:小驼峰命名:camelCase
*:大驼峰命名:PascalCase
*:kabab-case命名:user-box
root.render(
<>
<div style='color:red'>Hello World</div> //错误写法
<div style={{ color: 'red', fontSize: 36 + 'px' }}>Hello World</div >
</>
);
- 给元素绑定类名的时候,需要使用
className
替换class
,否则报错。(vue无该事项)
<div className='box' style={{ color: 'red', fontSize: 36 + 'px' }}>Hello World</div >
- JSX中的注释写法:
{/* */}
,但是如果是在{}
中添加代码注释,为/* */或//
- 如果直接在
{}
编写HTML元素,那么会被编译放在页面中。(vue不允许)
root.render(
<>
{<button>按钮</button>}
</>
);
根据变量值控制按钮
let flag = false
let isRunning = true
root.render(
<>
{/* 第一种方法,按钮会被渲染到DOM结构中,但是不会显示出来。类似vue的v-show */}
<button style={{ display: flag ? 'block' : 'none' }}>按钮1</button>
<hr />
{/* 第二种方式,按钮直接不被处理,不会再DOM结构显示 */}
{flag ? <button>按钮2</button> : null}
<hr />
<button>{isRunning ? "正在执行。。。" : "执行完毕"}</button>
</>
);
循环打印列表
如果想对一个数组中的内容进行操作的话,可以借助遍历实现。目前知识储备情况下,可以借助数组的map方法实现。
在map方法中,需要返回一个li标签组成的结构,在该结构中需要注意每次遍历的item为对象,不能直接使用,需要item.title使用对象中的普通数据。 最后返回的数组中,每一个元素结构如:[< li>…</ li>,< li>…</ li>],然后在{}
语法中,会自己识别数组中的内容,并将每一个元素分别渲染到页面上
let arr = [
{ id: 1, title: "今天天气很好" },
{ id: 2, title: "今天天气很差" },
{ id: 3, title: "今天下雨了" },
]
root.render(
<>
<div className='box'>
<h3>新闻列表</h3>
<ul>
{arr.map((item, index) => {
// react的遍历和vue一样都需要绑定唯一key值,做虚拟dom和真实dom的比对
return <li style={{ listStyle: 'none' }} key={item.id}>
<em>{index + 1}</em>
<span>{item.title}</span>
</li>
})}
</ul>
</div>
</>
)
在不指定数组的前提下遍历
new Array(5)
:只指定一个数值的时候,会创建长度为5的空数组
let a1 = new Array(5,1)
:会创建包含两个数组元素
let a2 = new Array('5')
:创建长度为1包含字符的数组
第一种情况就是稀疏数组,后面的为密集数组。当使用数组的forEach/map等方法的时候,他们不会去迭代稀疏数组。数组的fill
方法用来填充数组
{new Array(5).fill(null).map((_, index) => {
return <button key={index}>按钮{index + 1}</button>
})}
JSX底层渲染机制
let str = 'hello world';
root.render(
<>
<h1>{str}</h1>
<div>
{['小王', '大王'].map(item => <p>{item}</p>)}
</div>
</>
);
- 上面这段编写的JSX代码,首先会被编译为虚拟DOM对象,即virtualDOM。
*:虚拟DOM:React框架内部构建的对象体系,对象的相关成员都是由react规定。基于这些对象的属性描述,构建视图中DOM节点的相关特征。( 每次虚拟DOM都会被缓存,用作下次比对 ) - 将构建的虚拟DOM渲染为真实DOM
*:真实DOM:浏览器页面中,最终被渲染的元素。
*:初次渲染的时候,虚拟DOM是直接被渲染为真实DOM到页面中显示。后期视图更新的时候,需要结果DOM-DIFF比对,根据比对计算出补丁包PATCH(两次视图的差异部分),根据补丁包渲染页面内容。
虚拟DOM
React.createElement方法详解
在package.json文件中babel-preset-react-app
包负责对@babel/preset-env
的重写。重写的时候加入了负责支持JSX语法的编译。在babel官网中可以进行测试,查看JSX代码是如何被转换的。
<>
<h1 className='title' style={{ color: 'red' }}>{str}</h1>
<div className='box'>
<span>{x}</span>
<span>{y}</span>
</div>
<div></div>
</>
转换代码如下
React.createElement(
React.Fragment, //即<></>空文档标签
null,
React.createElement(
"h1",
{ className: "title", style: { color: 'red' } },
str),
React.createElement(
"div",
{ className: "box" },
React.createElement(
"span",
null,
x),
React.createElement(
"span",
null,
y)
),
React.createElement("div", null)
);
分析
- 上面这段代码基于
babel-preset-react-app
实现,将JSX语法转换为React.createElement(...)
格式。只要是元素标签,都会调用createElement()
。并且每一个React.createElement(ele,props,...children)
都需要传入对应的参数信息。
*:ele:即调用createElement()
方法需要创建的标签名,为第一个参数项
*:props:为该标签的属性对象集合,如类名,样式名等。如果没有设置任何标签的属性,则为null。为第二个参数项
*:children:从第三个往后开始,均为当前标签元素的子节点,可以是文本节点,也可以是新的标签。如果当前标签的内容为空,则该配置项不显示。如果存在多个子节点,则当前配置项为一个数组保存 - 使用
React.createElement(...)
方法创建的为虚拟DOM—virtualDOM,也称为JSX对象或JSX元素或ReactChild对象,该方法会返回一个虚拟DOM对象结构。将上面的代码打印输出。
手写一个React.createElement方法
export function createElement(ele, props, ...children) {
let virtualDOM = {
$$typeof: Symbol('react.element'),
key: null,
ref: null,
type: null, //初始为空
props: {},// 默认为一个对象
}
virtualDOM.type = ele;
// 判断标签的属性是否存在
if (props) {
virtualDOM.props = {
...props
}
}
let len = children.length
if (len === 1) {
//如文本信息,数值信息
virtualDOM.props.children = children[0]
}
if (len > 1) {
// 多个子元素,则为数组
virtualDOM.props.children = children
}
return virtualDOM
}
到这一步,基本输出格式就和react提供的一模一样。
真实DOM
将虚拟DOM最终转换为真实DOM,都是通过render
方法实现的,但是在不同版本的react中,书写格式是不同的。
在react18版本中
const root = ReactDOM.createRoot(document.getElementById('root')); //获取根容器
root.render( //通过react.createRoot().render()转换为真实DOM
<>...</> //创建虚拟DOM,调用React.createElement()
)
但是在react16版本中,代码如下
ReactDOM.render(
<>...</>,
document.getElementById('root')
)
首先基于react16版本写一段render
方法,大体思路:传染两个参数,第一个为虚拟DOM,第二个为容器,主要难处理的是如何第一个参数中的标签属性添加到标签身上,因为所有的标签属性跟children都存放在了一个props对象中,所以需要遍历取出来。那么如何遍历一个对象的属性这里需要扩展一个又关for/in
循环的弊端。
一般情况下,一个对象的属性打印输出为深红色和浅红色,而深色代表可枚举的属性,而浅色代表不可枚举的属性(不可枚举代表不能被for/in或Object.keys()取出)。通常内置的属性均为不可枚举,自定义属性是可以枚举的。 但是我们可以使用Object.defineProperty
方法修改成员的枚举性。
将设有一个数组对象
Array.prototype.BB = 'bb'
let arr = [1, 2] //定义的私有成员
arr[Symbol('3')] = 3 //定义的私有成员
console.log(arr);
for/in
循环弊端:性能差,因为会迭代所以公共或私有的成员,并且只能迭代可枚举的,非Symbol类型的值。,下面这段输出就代表了for/in循环的弊端
for (let i in arr) {
console.log(i); //输出0 1 BB
}
我们需要自己借助相应的方法替代for/in循环,获取所有的私有属性相应的方法如下
- 方法一
Object.getOwnPropertyNames(arr) //只获取一个对象的私有属性,不包括Symbol类型
Object.getOwnPropertySymbols(arr) //只获取一个对象的私有属性并且为Symbol类型
//然后调用两个方法,将返回的结果拼接一起即可
let keys = [...Object.getOwnPropertyNames(arr), ...Object.getOwnPropertySymbols(arr)]
或者
let keys = Object.getOwnPropertyNames(arr).concat(Object.getOwnPropertySymbols(arr))
//输出结果
keys.forEach(key => console.log(key, arr[key])) //Symbol为字符串,不能再次添加字符串包裹
- 方法二
实现效果和上面代码一致,缺点不兼容IE
let keys = Reflect.ownKeys(arr) //获取私有成员,不论是不是Symbol类型
最后封装一个方法代替forin循环
function each(obj, callback) {
// null也为object类型,需要先判断
if (obj === null && typeof obj !== 'object') throw new ('obj is not a object')
if (typeof callback !== 'function') throw new ('callback is not a function')
let keys = Reflect.ownKeys(obj)
keys.forEach(key => {
let value = obj[key]
callback(key, value)
})
}
Array.prototype.BB = 'bb'
let arr = [1, 2] //定义的私有成员
arr[Symbol('3')] = 3 //定义的私有成员
each(arr, (key, value) => {
console.log(key, value)
})
那么如何将一个对象的信息添加到一个DOM元素结构上又存在两种不同的方法,且作用各不相同
-
方法一,假设页面有一个id为root的容器,为一个元素设置属性,可以直接采样
对象.属性
的方法添加。但是这种方法中,自定义属性无法在页面标签结构中显示添加的属性,所以的属性均被添加到堆内存中,但是如果添加到内置属性时,会设置在元素的标签身上,同时堆内存不在添加。
-
使用
setAttribute
方法实现,直接将新属性设置在元素的页面结构中,同时堆内存中不在添加
以下是虚拟DOM转换为真实DOM的完整代码
let str = 'hello world';
let x = 10;
let y = 20;
let JSXObj = (
createElement(
"div",
{ className: "container" },
createElement(
"h1",
{
className: "title",
style: {
color: 'red'
}
},
str),
createElement(
"div",
{ className: "box" },
createElement("span", null, x),
createElement("span", null, y)))
);
render(JSXObj, document.getElementById('root'))
// 获取对象属性
function each(obj, callback) {
// null也为object类型,需要先判断
if (obj === null && typeof obj !== 'object') throw new ('obj is not a object')
if (typeof callback !== 'function') throw new ('callback is not a function')
let keys = Reflect.ownKeys(obj)
keys.forEach(key => {
let value = obj[key]
callback(key, value)
})
}
export function createElement(ele, props, ...children) {
let virtualDOM = {
$$typeof: Symbol('react.element'),
key: null,
ref: null,
type: null, //初始为空
props: {},// 默认为一个对象
}
virtualDOM.type = ele;
// 判断标签的属性是否存在
if (props) {
virtualDOM.props = {
...props
}
}
let len = children.length
if (len === 1) {
//如文本信息,数值信息
virtualDOM.props.children = children[0]
}
if (len > 1) {
// 多个子元素,则为数组
virtualDOM.props.children = children
}
return virtualDOM
}
export function render(virtualDOM, container) {
let { type, props } = virtualDOM
if (typeof type === 'string') {
// 不对<></>进行处理,即Symbol类型
// 创建标签
let ele = document.createElement(type)
// 给标签添加属性
each(props, (key, value) => {
// 处理JSX中的className类名情况
if (key === 'className') {
ele.className = value //原生设置类名的方式
return
}
//设置样式属性,JSX中的样式属性格式 style={{color:'red',fontSize:'18px'}}
if (key === 'style') {
each(value, (styKey, styVal) => {
ele.style[styKey] = styVal
})
return
}
// 处理children属性的情况
if (key === 'children') {
// 可能为数组又可能不是数组,值可能是节点,又可能是文本信息
// 所有的节点都是插入当前ele下
let children = value //保存值
if (!Array.isArray(children)) {
children = [children] //强制文本内容转换为数组,方便比较
}
children.forEach((child) => {
// 第一种使用正则判断
// if (/^(string|number)/.test(typeof child))
if (typeof child === 'string' || typeof child === 'number') {
// 代表只是字符串文本,创建文本节点,直接插入
let textNode = document.createTextNode('child')
// 插入当前元素中
ele.appendChild(textNode)
return
}
// 否则为虚拟DOM,继续递归调用render函数
render(child, ele)
})
return
}
//普通情况 如name='root'
ele.setAttribute(key, value) //给标签设置属性
})
// 插入容器中
container.appendChild(ele)
}
}
最终页面效果
函数组件底层渲染机制
React中组件没有全局和局部概念,将需要使用的组件注册到React上。组件分为以下三类
- 函数组件
- 类组件
- Hooks组件,在函数组件中使用React Hooks函数
函数组件
函数组件:在src目录下,根据需求创建一个文件夹存放编写的组件名.jsx
组件,在该文件中编写一个函数,该函数必须有返回值,且返回值为JSX视图,最后将整个函数导出。之后在需要引入的地方使用,在render
函数中使用自闭和或双标签的形式使用。组件的名字推荐使用大驼峰
函数组件不具备状态,生命周期钩子,ref等。第一次渲染完毕后数据不会更新视图,除非父组件重新调用渲染。并且函数组件属于静态组件。
export const Demo1 = () => {
return <div className="demoBox">
<h1>我是demo1</h1>
</div>
}
在使用该组件标签的时候,最终也是基于React.createElement
方法创建虚拟DOM。同时可以给改标签绑定自定义属性使用。
传递参数的时候需要注意遵循React的语法:使用{}包裹中需要传递的JS部分,如果只是一个字符串,则不用{},这里类似vue的 :x='10’的写法,
import { Demo1 } from './views/Demo1';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Demo1 title="我是demo1" x={10} y={[1, 2, 3]} obj={{ age: 666 }} className="test" style={{ color: 'red' }}></Demo1>
);
该代码最终会被转换为如下格式,打印后输出如下会发现,原来type
字段表示标签名的地方现在变成一个函数了,最终会去调用这个函数返回组件的虚拟DOM。
React.createElement(Demo1, {
title: "\u6211\u662Fdemo1",
x: 10,
y: [1, 2, 3],
obj: {
age: 666
},
className: "test",
style: {
color: 'red'
}
});
执行的基本步骤
- 首先基于
babel-preset-react-app
将JSX语法转换为createElement
方法格式 - 根据转换的
createElement
方法执行该函数,该函数会返回一个虚拟DOM对象,对象的格式上图所示,重点是type
变为组件函数 - 在
render
函数中,会根据生成的虚拟DOM,先去执行type
所对应的函数。同时将当前组件标签定义的属性(即上图的props)传递给该函数。 - 最终调用该函数会返回一个组件标签内部的虚拟DOM,
render
函数会将根据组件标签返回的虚拟DOM渲染为真实DOM放在容器中。
函数组件的参数细节
根据上面的内容可知,如果给一个组件标签设置属性,那么在在转换为虚拟DOM的时候,会将定义的属性传递给函数组件。
root.render(
<Demo1 title='我是demo1' x={10} y={[1, 2, 3]} obj={{ age: 666 }} className="test" style={{ color: 'red' }}>
<div>你好</div>
</Demo1>
);
我们定义变量接收定义的属性,然后我们尝试修改title
属性的值,这个时候会发现,控制台报错,代表传递给函数组件的属性参数,只能读,不能修改,这一点和vue一致。原理是将传递来的对象参数冻结了!!!
export const Demo1 = (props) => {
props.title = 'hello'
return <div className="demoBox">
<h1>我是demo1</h1>
</div>
}
关于上面的报错信息扩展对象的规则设置
-
冻结:被冻结的对象不能再被更改:不能修改现有属性,不能添加新的属性,不能移除现有的属性,不能更改它们的可枚举性、可配置性、可写性或值,对象的原型也不能被重新指定
*:使用Object.freeze()
冻结一个对象
*:使用Object.isFrozen()
检测一个对象是否被冻结,通过打印测试函数组件中参数是否冻结,结果为true,代表是冻结后的参数
-
密封:不能添加新属性、不能删除现有属性或更改其可枚举性和可配置性、不能重新分配其原型。但是现有属性的值是可写的,它们仍然可以更改。
*:Object.seal()
设置一个对象的密封性
*:Object.isSealed()
判断一个对象是否密封 -
不可扩展:处理不能添加新的属性,其他均能实现
*:Object.preventExtensions()
设置一个对象为不可扩展
*:Object.isExtensible()
检测一个对象是否可扩展
被冻结的对象,包含了不可扩展和密封。同理密封包含了不可扩展
设置函数组件参数的规则
父组件传递给子组件的参数,在子组件中可以设置参数的规则,(这里跟vue的props配置项一致,均可设置父传子的参数信息,如默认值,类型等,但是并不是所有的配置都可以直接设置,需要借助插件)
设置参数默认值
假设有如下代码,x在第二个参数中并没传递,那么在复用函数组件的时候就会为undefined
<>
<Demo1 title='标题1' x={10}>
</Demo1>
<Demo1 title='标题2' ></Demo1>
</>
export const Demo1 = (props) => {
return <div title={props.title}>
<h1>{`${props.title}:${props.x}`}</h1>
</div>
}
通过把函数当做对象,设置静态的私有属性方法,来给其设置属性的校验规则,设置:defaultProps静态属性
Demo1.defaultProps = {
x: 0
}
当然也可以在传参的时候解构其值,同时指定参数的默认值
export const Demo1 = ({ title, x = 0 }) => {}
如果想指定其他默认值,如数据的类型,是否必传,需要借助prop-types
库。
设置:propTypes静态属性
import PropTypes from 'prop-types'
。。。。。
Demo1.propTypes = {
title: PropTypes.string.isRequired, //类型为字符串,且必传
x: PropTypes.number //类型为数值
y: PropTypes.array, //类型为数组
z: PropTypes.oneOfType([PropTypes.number, PropTypes.string]) //类型可以为字符串或数值
}
React中的插槽处理机制
首先需要知道,位于组件标签中的元素属于子节点,都会被存放到一个children
属性中保持,但是不同情况下,children
字段的值可能不一样。所有使用插槽传递的子节点都会经过createElement方法转换为虚拟DOM传递到函数参数中
有如下三种情况
- 多个子节点的时候会被组成一个数组保存在
children
中 - 只有一个子节点的时候,直接放在
children
中保存,(不组成数组) - 若无子节点,则
children
字段直接为undefined
<>
<DemoSlot title='hello world'> //两个子节点
<h2>我是子节点1</h2>
今天天气很棒
</DemoSlot>
<DemoSlot title='你好,世界'> //一个子节点
<h2>我是子节点2</h2>
</DemoSlot>
<DemoSlot title='你好,世界' /> //无子节点
</>
总而言之如果只是灵活性的更改内容,那么直接做为标签属性传递即可,如果是复杂的结构,可以使用插槽传递,灵活性更高,不需要在组件中写死。
在React中,需要自己处理一些插槽的机制, 在上面的代码中,需要针对不同的children
值去进行判断处理。
如下一段例子,将插槽中的第一个元素放到首部,将第二个元素放到尾部显示。如果children
为数组的情况下,那么没有问题,但是如果是非数组或者undefined的时候则会出错,因此需要进行类型判断,并全部转换为数组。
//组件函数中代码
let { title, children } = props
if (!children) {
children = []
} else if (!Array.isArray(children)) {
children = [children]
}
return <div>
{children[0]}
<hr />
<h1>{title}</h1>
<hr />
{children[1]}
</div>
但是每次都这样子判断的话过于麻烦,所以我们可以借助import React from 'react'
中提供的react核心语法实现上面的过程。在该对象中提供了一个Children
配置项,该配置项中提供了封装好的方法,已经帮我们将上面的情况进行了处理。对于常见的forEach方法处理了,使用可能会和普通的有区别。
在这里调用React.Children.toArray(children)
传入参数,每次都会将该值转换为数组,无论是什么情况,最后都会返回一个新数组。
// 修改后的代码简化为一句即可
children = React.Children.toArray(children)
具名插槽
上面这种情况就是默认插槽的使用方法,接下来介绍带有名字的插槽使用
在React中使用具名插槽就是给标签起一个名字区分。,如下代码的作用是将页眉和页脚固定放在首部,其余的内容均放在主体部分展示。如果按照之前的默认插槽的使用方法,那么这么多的标签组成的children
数组长度是超过2的,不好处理在页面中。
<DemoSlot title='hello world'>
<div slot='header'> //slot是自定义的标识,非内置
<h2>这是页眉部分</h2>
</div>
<div>
<h2>这是内容部分1</h2>
</div>
<div>
<h2>这是内容部分1</h2>
</div>
<div slot='footer'>
<h2>这是页脚部分</h2>
</div>
</DemoSlot>
export const DemoSlot = (props) => {
let { title, children } = props
// children由多个虚拟DOM组成的数组对象
children = React.Children.toArray(children)
let headerSlot = [], footerSlot = [], defalutSlot = []
children.forEach(child => {
// 每一个child均为一个虚拟DOM对象,每一个对象都存放props保存自身的属性
let { slot } = child.props
if (slot === 'header') {
headerSlot.push(child)
} else if (slot === 'footer') {
footerSlot.push(child)
} else {
defalutSlot.push(child)
}
})
return <div>
{headerSlot}
<hr />
<h1>{title}</h1>
这里是内容部分
{defalutSlot}
<hr />
{footerSlot}
</div>
}