React
组件化/模块化
@1 有利于团队协作开发 @2 便于组件的复用:提高开发效率、方便后期维护、减少页面中的冗余代码
如何划分组件
业务组件:针对项目需求封装的 @1 普通业务组件「没有啥复用性,只是单独拆出来的一个模块」 @2 通用业务组件「具备复用性」
功能组件:适用于多个项目「例如:UI组件库中的组件」 @1 通用功能组件
因为组件化开发,必然会带来“工程化”的处理,也就是基于webpack等工具「vite/rollup/turbopack...」
- 实现组件的合并、压缩、打包等
- 代码编译、兼容、校验等
- ...
React版本
react常用的版本很早之前是15版本「太早了」
16版本:一些项目用的最多的
17版本:最大的升级就是看不出升级「语法没变啥,只是底层处理机制上升级了」
18版本:新版本「机制和语法上都有区别」
React的工程化/组件化开发
我们可以基于webpack自己去搭建一套工程化打包的架子,但是这样非常的麻烦/复杂;React官方,为我们提供了一个脚手架:create-react-app!! + 脚手架:基于它创建项目,默认就把webpack的打包规则已经处理好了,把一些项目需要的基本文件也都创建好了!!
深入研究create-react-app脚手架
REACT脚手架
玩转react官方脚手架:create-react-app
- create-react-app.dev/docs/gettin…
- $ npm i -g create-react-app -g 安装脚手架 (v3.3.0)
- $ create-react-app --version 检查安装情况
- $ create-react-app xxx 基于脚手架创建项目
- 项目名称要遵循npm包命名规范:使用“数字、小写字母、_”命名
- $ yarn start 启动项目 / build 生成项目
- $ yarn eject 暴露webpack配置项
- github.com/browserslis…
脚手架默认安装
- react:React框架的核心
- react-dom:React视图渲染的核心「基于React构建WebApp(HTML页面)」
- react-native:构建和渲染App的 (开发原生APP的)
- react-scripts:脚手架为了让项目目录看起来干净一些,把webpack打包的规则及相关的插件/LOADER等都隐藏到了node_modules目录下,react-scripts就是脚手架中自己对打包命令的一种封装,基于它打包,会调用node_modules中的webpack等进行处理!!
修改React中的webpack配置项
和Vue一样,React脚手架也默认把配置好的webpack那些
vue是提供vue.config.js让用户自己去修改配置项
1.先把配置项暴露出来 npm run eject
2.细节点:不可逆转的 + 如果有git先要保存修改项
config webpack配置项
|- webpack.config.js
|- webpackDevServer.config.js
|- path.js 存放各配置的地址文件信息(入口文件)
|- ...
scripts
|- start.js 开发环境下 $ yarn start 先执行这个文件
|- build.js 生产环境下 $ yarn build 先执行这个文件
|- ...
使用 create-react-app 构建React工程化项目
安装create-react-app
$ npm i create-react-app -g 「mac需要加sudo」基于脚手架创建项目「项目名称需要符合npm包规范」
$ create-react-app xxx|- node_modules 包含安装的模块 |- public 页面模板和IconLogo |- favicon.ico |- index.html |- src 我们编写的程序 |- index.jsx 程序入口「jsx后缀名可以让文件支持jsx语法」 |- package.json |- ...package.json
{ ... "dependencies": { ... "react": "^18.2.0", //核心 "react-dom": "^18.2.0", //视图编译 "react-scripts": "5.0.1", //对打包命令的集成 "web-vitals": "^2.1.4" //性能检测工具 }, "scripts": { "start": "react-scripts start", //开发环境启动web服务进行预览 "build": "react-scripts build", //生产环境打包部署 "test": "react-scripts test", //单元测试 "eject": "react-scripts eject" //暴露配置项 }, "eslintConfig": { //ESLint词法检测 "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { //浏览器兼容列表 "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }package.json_dependencies
package.json_scripts
package.json_eslintConfig
package.json_browserslist
使用脚手架创建的项目
修改React中的webpack配置项
和Vue一样,React脚手架也默认把配置好的webpack那些
vue是提供vue.config.js让用户自己去修改配置项
默认情况下,会把webpack配置项隐藏到node_modules中,如果想修改,则需要暴露配置项:
1.先把配置项暴露出来 npm run eject
2.细节点:不可逆转的 + 如果有git先要保存修改项
config webpack配置项
|- webpack.config.js
|- webpackDevServer.config.js
|- path.js 存放各配置的地址文件信息(入口文件)
|- ...
scripts
|- start.js 开发环境下 $ yarn start 先执行这个文件
|- build.js 生产环境下 $ yarn build 先执行这个文件
|- ...
package.json的变化
/* package.json中的变化 */ { "dependencies":{ //暴露后,webpack中需要的模块都会列在这 ... }, "scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js", "test": "node scripts/test.js" //不在基于react-scripts处理命令,而是直接基于node去执行对应的文件 //已经没有eject命令了 }, "jest": { //单元测试配置 }, "babel": { //关于babel-loader的额外配置 "presets": [ "react-app" ] } } /* 新增的内容 */ |- scripts |- start.js |- build.js |- ... |- config |- webpack.config.js |- paths.js |- ...
babel-preset-react-app
对 @babel/preset-env 语法包的重写「目的:把ES6转为ES5」
重写的目的:让语法包可以识别React的语法,实现代码转换
真实项目中常用的一些修改操作
配置less
/* 默认安装和配置的是sass,如果需要使用less,则需要: 1. 安装 $ yarn add less less-loader@8 $ yarn remove sass-loader 2. 修改webpack.config.js */ // 72~73 const lessRegex = /\.less$/; const lessModuleRegex = /\.module\.less$/; //507~545 { test: lessRegex, exclude: lessModuleRegex, use: getStyleLoaders( ... 'less-loader' ) }, { test: lessModuleRegex, use: getStyleLoaders( ... 'less-loader' ), }1、安装:less 和 less-loader(任选一种)
npm方式: npm install less less-loader --save cnpm方式: cnpm install less less-loader --save yarn方式: yarn add less less-loader2、暴露项目配置项(任选一种)
若报错,有git的可以通过 git add . git commit -m '暴露项目配置项' npm run eject npm方式: npm run eject cnpm方式: cnpm run eject yarn方式: yarn eject3、配置webpack.config.js
config / webpack.config.js
配置完,重启项目
配置别名
//313 resolve: { ... alias: { '@': path.appSrc, ... } }
配置预览域名
// scripts/start.js // 48 const HOST = process.env.HOST || '127.0.0.1';
也可以基于 cross-env 设置环境变量
$ yarn add cross-env
配置端口号
"scripts": {
"demo": "cross-env PORT=8080 node scripts/start.js",
...
},
// npm run demo启动
配置跨域代理
/* 安装 http-proxy-middleware:实现跨域代理的模块「webpack-dev-server的跨域代理原理,也是基于它完成的」 $ yarn add http-proxy-middleware src/setupProxy.js */ const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( createProxyMiddleware("/api", { target: "http://127.0.0.1:7100", changeOrigin: true,// 默认为false,是否改变原始主机头为目标url ws: true,// 是否代理websockets pathRewrite: { "^/api": "" } }) ); }; //测试地址: //https://www.jianshu.com/asimov/subscriptions/recommended_collections //https://news-at.zhihu.com/api/4/news/latest
配置浏览器兼容
//package.json //https://github.com/browserslist/browserslist "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } /* CSS兼容处理:设置前缀 autoprefixer + postcss-loader + browserslist CSS的兼容:设置前缀 -webkit- -ms- -moz- -o- JS兼容处理:ES6语法转换为ES5语法 babel-loader + babel-preset-react-app(@babel/preset-env) + browserslist JS兼容处理:内置API 入口配置react-app-polyfill */ import 'react-app-polyfill/ie9'; import 'react-app-polyfill/ie11'; import 'react-app-polyfill/stable';
$ yarn add @babel/polyfill
在入口中:
import '@babel/polyfill'脚手架中不需要我们自己去安装:react-app-polyfill 「对@babel/polyfill的重写」
index.html需要存放哪些内容
当前项目SPA单页面应用情况下唯一的入口页面(后期所有编写的模块,都会放到index.html的#root中)
1.默认情况下,我们会把所有需要开发引入的资源(样式|图片...)和编写的模块等都放在SRC目录中(webpack本身就是打包SRC目录,根据入口index.js的依赖项打包)
2.但是有些东西我们还是需要写在index.html中的
- 页面导入最后打包的css/js,由于打包后的文件比较大,第一次请求页面需要一点时间,这个时间段内,我们看到的就是白屏效果
- 为了解决白屏效果,我们会在index.html中设置loading效果(这些内容是一加载页面就展示出来的) =>这个有对应的插件
- 给资源做304缓存
- 有一些模块不支持CommonJS/ES6Module这种模块的导入导出规范,此时需要我们把这些模块在index.html中单独script导入进来
- 还可以把一些公共资源直接在这里导入(这样webpack打包的时候就不会把内容都打包在一起了)
- ...
理解React的MVC和Vue中的MVVM
React是Web前端框架
1.目前市面上比较主流的前端框架 + React + Vue + Angular「NG」 + ... 主流的思想:不在直接去操作DOM,而是改为“数据驱动思想” 操作DOM思想: + 操作DOM比较消耗性能「主要原因就是:可能会导致DOM重排(回流)/重绘」 + 操作起来也相对来讲麻烦一些 + ... 数据驱动思想: + 我们不会在直接操作DOM + 我们去操作数据「当我们修改了数据,框架会按照相关的数据,让页面重新渲染」 + 框架底层实现视图的渲染,也是基于操作DOM完成的 + 构建了一套 虚拟DOM->真实DOM 的渲染体系 + 有效避免了DOM的重排/重绘 + 开发效率更高、最后的性能也相对较好
- React框架采用的是MVC体系;Vue框架采用的是MVVM体系; MVC:model数据层 + view视图层 + controller控制层 @1 我们需要按照专业的语法去构建视图(页面):React中是基于jsx语法来构建视图的 @2 构建数据层:但凡在视图中,需要“动态”处理的(需要变化的,不论是样式还是内容),我们都要有对应的数据模型 @3 控制层:当我们在视图中(或者根据业务需求)进行某些操作的时候,都是去修改相关的数据,然后React框架会按照最新的数据,重新渲染视图,以此让用户看到最新的效果! 数据驱动视图的渲染!! 视图中的表单内容改变,想要修改数据,需要开发者自己去写代码实现!! “单向驱动” MVVM:model数据层 + view视图层 + viewModel数据/视图监听层 @1 数据驱动视图的渲染:监听数据的更新,让视图重新渲染 @2 视图驱动数据的更改:监听页面中表单元素内容改变,自动去修改相关的数据 “双向驱动”
Vue VS React
1、都是操作数据来影响视图的,告别了传统操作DOM的时代
- Model控制View层
- Vue基于数据劫持,拦截到最新的数据,从而重新渲染视图
- React是提供对应的API,通过我们操作API,让最新数据渲染视图
2、都一定存在DOM的差异化渲染(DOM DIFF)
- 每一次数据更改,只把需要改变的视图部分进行重新渲染
3、React默认只实现了单向控制(只有数据影响视图),而Vue基于v-model实现了双向控制(即也包含了视图影响数据)
- 不论是Vue还是React,在实现视图影响数据的方式上,也都是基于change/input事件,监听表单元素内容的改变,从而去修改数据,达到数据的更新
Vue只是v+vm层,React只是v层
标准的mvc三层架构
JSX的基础语法
JSX:javascript and xml(html) 把JS和HTML标签混合在了一起「并不是我们之前玩的字符串拼接」
@1 vscode如何支持JSX语法「格式化、快捷提示...」
创建的js文件,我们把后缀名设置为jsx即可,这样js文件中就可以支持JSX语法了
webpack打包的规则中,也是会对.jsx这种文件,按照JS的方式进行处理的
@2 在ReactDOM.createRoot()的时候,不能直接把HTML/BODY做为根容器,需要指定一个额外的盒子「例如:#root」
@3 每一个构建的视图,只能有一个“根节点”
出现多个根节点则报错 Adjacent JSX elements must be wrapped in an enclosing tag.
<></> fragment空标记,即能作为容器把一堆内容包裹起来,还不占层级结构
在HTML中嵌入“JS表达式”,需要基于“{} 胡子语法”,{}胡子语法中嵌入不同的值,所呈现出来的特点
动态绑定数据使用{},大括号中存放的是JS表达式(JS表达式:执行代码得有返回的结果)
number/string:值是啥,就渲染出来啥
boolean/null/undefined/Symbol/BigInt:渲染的内容是空
除数组对象外,其余对象一般都不支持在{}中进行渲染,但是也有特殊情况:
但是如果是JSX的虚拟DOM对象,是直接可以渲染的
给元素设置style行内样式,要求必须写成一个对象格式
数组对象:把数组的每一项都分别拿出来渲染「并不是变为字符串渲染,中间没有逗号」
- 一般情况下不能直接渲染对象
函数对象:不支持在{}中渲染,但是可以作为函数组件,用方式渲染!!
....
设置行内样式,必须是 style={{color:'red'...}};设置样式类名需要使用的是className;
行内样式:需要基于对象的格式处理,直接写样式字符串会报错
<h2 style={{ color: 'red', fontSize: '18px' //样式属性要基于驼峰命名法处理 }}>设置样式类名:需要把class替换为className
<h2 className="box">JSX中进行的判断一般都要基于三元运算符来完成
JSX中遍历数组中的每一项,动态绑定多个JSX元素,一般都是基于数组中的MAP来实现的(MAP迭代数组的同时,支持返回值)
- 和vue一样,循环绑定的元素要设置key值(作用:用于DOM-DIFF差异化对比)
JSX语法具备很强的编程性,而这是Vue中模板语法不具备的,所以Vue从新版本(V2.xx)开始,支持了JSX语法 cn.vuejs.org/v2/guide/re…
JSX语法具备过滤效果(过滤非法内容),有效防止XSS攻击(扩展思考:总结常见的XSS攻击和预防方案?)
{}中可以渲染出来的值
- 原始值类型:只渲染字符串/数字,其余的值都会渲染为空
- 对象类型:
- 数组对象:可以进行渲染,而且不是转换为字符串(每一项之间没有逗号分隔),它会逐一迭代数组每一项,把每一项都拿出来单独进行渲染!!
- 函数对象:可以做为函数组件进行渲染,但是要写成 这种格式
- 其它对象:一般都是不可以直接进行渲染的
- 可以是一个jsx对象
- 如果设置的是style样式,则样式值必须写为对象格式
- ....
/* * 导入外部资源支持:CommonJS和ES6Module两种规范 * ES6Module:import/export * CommonJS:require/module.export */ import React from 'react'; import ReactDOM from 'react-dom'; /* // import './assets/common.less'; require('./assets/common.less'); let name = "珠峰培训2020 => 1024+996"; let styOBJ = { color: 'red' }; //=>JSX虚拟DOM对象 let obj = React.createElement('h3', null, '哈哈哈'); ReactDOM.render(<> <div style={styOBJ} className='text'>{name}</div> {obj} </>, document.getElementById('root')); */ let sex = 0; let arr = [{ name: '张三', age: 25 }, { name: '李四', age: 26 }] ReactDOM.render(<> <div>性别:{sex === 0 ? '男' : '女'}</div> <ul>{arr.map((item, index) => { return <li key={index}> 姓名:{item.name} 年龄:{item.age} </li>; })}</ul> </>, document.getElementById('root'));import React from 'react'; import ReactDOM from 'react-dom/client'; import './code'; /* let name = "珠峰培训"; let num = 10; let arr = [{ id: 1, title: '哈哈哈' }, { id: 2, title: '呵呵呵' }]; let obj = { name: 'xxx', age: 25 }; */ /* const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <> <div className="box">{name}</div> <div style={{ color: 'red', fontSize: '14px' }}> {num > 10 ? 'OK' : 'NO'} </div> <ul> {arr.map(item => { let { id, title } = item; return <li key={id}> {title} </li>; })} </ul> {Reflect.ownKeys(obj).map((key, index) => { let val = obj[key]; return <span key={index}> {key} : {val} </span> })} </> ); */ /* let level = 6; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <> {React.createElement(`h${level}`, null, "我是标题")} </> ); */ /* const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <div className="box"> <h2 className="title">我是标题</h2> <ul className="list" style={{ color: 'red' }}> <li>列表1</li> <li>列表2</li> <li>列表3</li> </ul> </div> ); */ /* const jsx = React.createElement( "div", { className: "box" }, React.createElement( "h2", { className: "title" }, "\u6211\u662F\u6807\u9898" ), React.createElement( "ul", { className: "list", style: { color: 'red' } }, React.createElement("li", null, "\u5217\u88681"), React.createElement("li", null, "\u5217\u88682"), React.createElement("li", null) ) ); console.log(jsx); */
JSX底层渲染机制
第一步:把我们编写的JSX语法,编译为虚拟DOM对象「virtualDOM」
虚拟DOM对象:框架自己内部构建的一套对象体系(对象的相关成员都是React内部规定的),基于这些属性描述出,我们所构建视图中的,DOM节点的相关特征!!
基于 babel-preset-react-app 语法包,可以把jsx语法,渲染解析为 React.createElement 格式
只要是元素节点,必然会基于createElement进行处理!
遇到“HTML标签/调用组件标签”,就会创建为createElement格式
前两个参数是固定的:标签名(组件)、属性,第三个及以后参数是子元素
传递了属性,第二个参数是一个对象(包含了各属性的信息),没有传递属性则第二个参数为null
React.createElement(ele,props,...children) + ele:元素标签名「或组件」 + props:元素的属性集合(对象)「如果没有设置过任何的属性,则此值是null」 + children:第三个及以后的参数,都是当前元素的子节点
再把 createElement 方法执行,创建出virtualDOM虚拟DOM对象「也有称之为:JSX元素、JSX对象、ReactChild对象...」!!
首先是一个对象
type属性存储的是标签名(或者组件)
props属性:没有传递任何属性,也没有任何的子元素,其为空对象;把传递给createElement的属性,都赋值给props;如果有子元素,则新增一个children的属性,可能是一个值,也可能是一个数组
virtualDOM = { $$typeof: Symbol(react.element), ref: null, key: null, type: 标签名「或组件」, // 存储了元素的相关属性 && 子节点信息 props: { 元素的相关属性, children:子节点信息「没有子节点则没有这个属性、属性值可能是一个值、也可能是一个数组」 } }第二步:把构建的virtualDOM渲染为真实DOM
基于ReactDOM.render把创建的虚拟DOM对象渲染到页面指定的容器中
ReactDOM.render([JSX-OBJ],[CONTAINER],[CALLBACK])
[CALLBACK]渲染触发的回调函数,在这里可以获取到真实的DOM
v16 ReactDOM.render( <>...</>, document.getElementById('root') ); v18 const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <>...</> );
- 补充说明:第一次渲染页面是直接从virtualDOM->真实DOM;但是后期视图更新的时候,需要经过一个DOM-DIFF的对比,计算出补丁包PATCH(两次视图差异的部分),把PATCH补丁包进行渲染!!
封装一个对象迭代的方法
基于传统的for/in循环,会存在一些弊端「性能较差(既可以迭代私有的,也可以迭代公有的);只能迭代“可枚举、非Symbol类型的”属性...」
解决思路:获取对象所有的私有属性「私有的、不论是否可枚举、不论类型」
Object.getOwnPropertyNames(arr) -> 获取对象非Symbol类型的私有属性「无关是否可枚举」
Object.getOwnPropertySymbols(arr) -> 获取Symbol类型的私有属性
获取所有的私有属性:
let keys = Object.getOwnPropertyNames(arr).concat(Object.getOwnPropertySymbols(arr));
可以基于ES6中的Reflect.ownKeys代替上述操作「弊端:不兼容IE」
let keys = Reflect.ownKeys(arr);
const each = function each(obj, callback) { if (obj === null || typeof obj !== "object") throw new TypeError('obj is not a object'); if (typeof callback !== "function") throw new TypeError('callback is not a function'); let keys = Reflect.ownKeys(obj); keys.forEach(key => { let value = obj[key]; // 每一次迭代,都把回调函数执行 callback(value, key); }); };createElement:创建虚拟DOM对象
export function createElement(ele, props, ...children) { let virtualDOM = { $$typeof: Symbol('react.element'), key: null, ref: null, type: null, props: {} }; let len = children.length; virtualDOM.type = ele; if (props !== null) { virtualDOM.props = { ...props }; } if (len === 1) virtualDOM.props.children = children[0]; if (len > 1) virtualDOM.props.children = children; return virtualDOM; };render:把虚拟DOM变为真实DOM
export function render(virtualDOM, container) { let { type, props } = virtualDOM; if (typeof type === "string") { // 存储的是标签名:动态创建这样一个标签 let ele = document.createElement(type); // 为标签设置相关的属性 & 子节点 each(props, (value, key) => { // className的处理:value存储的是样式类名 if (key === 'className') { ele.className = value; return; } // style的处理:value存储的是样式对象 if (key === 'style') { each(value, (val, attr) => { ele.style[attr] = val; }); return; } // 子节点的处理:value存储的children属性值 if (key === 'children') { let children = value; if (!Array.isArray(children)) children = [children]; children.forEach(child => { // 子节点是文本节点:直接插入即可 if (/^(string|number)$/.test(typeof child)) { ele.appendChild(document.createTextNode(child)); return; } // 子节点又是一个virtualDOM:递归处理 render(child, ele); }); return; } ele.setAttribute(key, value); }); // 把新增的标签,增加到指定容器中 container.appendChild(ele); } };
JSX语法转换为虚拟DOM对象
<div style={{ color: 'red' }} className="text" id="box">
<h2 index={1} data="">我是标题</h2>
欢迎来到珠峰培训
<span></span>
</div>
React.createElement("div", {
style: {
color: 'red'
},
className: "text",
id: "box"
}, /*#__PURE__*/React.createElement("h2", {
index: 1,
data: ""
}, "\u6211\u662F\u6807\u9898"), "\u6B22\u8FCE\u6765\u5230\u73E0\u5CF0\u57F9\u8BAD", /*#__PURE__*/React.createElement("span", null));
React.createElement = function (type, props, ...children) {
console.log('AAA');
let jsxOBJ = {
type,
props: {}
};
//=>传递了属性:把传递的属性都放置到JSX-OBJ的PROPS中
if (props !== null) {
//基于ES6实现对象浅克隆
jsxOBJ.props = { ...props };
}
//=>如果传递了子元素,还需要给JSX-OBJ的PROPS中设置children属性
if (children.length > 0) {
jsxOBJ.props.children = children;
//如果传递的子元素只有一项,则把第一项赋值给jsxOBJ.props.children即可
if (children.length === 1) {
jsxOBJ.props.children = children[0];
}
}
return jsxOBJ;
};
基于RENDER方法实现虚拟DOM的渲染
ReactDOM.render = function render(jsxOBJ, container, callback) {
console.log('BBB');
let { type, props } = jsxOBJ;
//=>创建DOM元素
if (typeof type === "string") {
//创建DOM元素对象(真实DOM)
let element = document.createElement(type);
//给创建的DOM设置属性
for (let key in props) {
if (!props.hasOwnProperty(key)) break;
//样式类和行内样式的特殊处理
if (key === 'className') {
element.setAttribute('class', props[key]);
continue;
}
if (key === 'style') {
let styOBJ = props['style'];
for (let attr in styOBJ) {
if (!styOBJ.hasOwnProperty(attr)) break;
element.style[attr] = styOBJ[attr];
}
continue;
}
//关于子元素的处理
if (key === 'children') {
//统一为数组
let children = props['children'];
if (!Array.isArray(children)) {
children = [children];
}
//循环子元素
children.forEach(item => {
//如果是文本,直接创建文本节点赋值给element即可,如果是新的虚拟DOM对象,则需要重复调用RENDER方法,把新创建的DOM对象增加给element(递归)
if (typeof item === "string") {
element.appendChild(document.createTextNode(item));
return;
}
render(item, element);
});
continue;
}
element.setAttribute(key, props[key]);
}
//增加到指定容器中
container.appendChild(element);
//触发回调函数
callback && callback();
}
};
ReactDOM.render(<div style={{ color: 'red' }} className="text" id="box">
<h2 index={1} data="">我是标题</h2>
欢迎来到珠峰培训
<span></span>
</div>, document.getElementById('root'));
React组件化开发
组件化开发的优势
- 利于团队协作开发
- 利于组件复用
- 利于SPA单页面应用开发
- ...
Vue中的组件化开发:
- 全局组件和局部组件
- 函数组件(functional)和类组件「Vue3不具备functional函数组件」
React中的组件化开发:
没有明确全局和局部的概念「可以理解为都是局部组件,不过可以把组件注册到React上,这样每个组件中只要导入React即可使用」
- 函数组件
- 类组件
- Hooks组件:在函数组件中使用React Hooks函数
函数式组件
// views/FunctionComponent.jsx const FunctionComponent = function FunctionComponent() { return <div> 我是函数组件 </div>; }; export default FunctionComponent; // index.jsx import React from 'react'; import ReactDOM from 'react-dom/client'; import FunctionComponent from '@/views/FunctionComponent'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <> <FunctionComponent/> </> );创建:在SRC目录中,创建一个 xxx.jsx 的文件,就是要创建一个组件;我们在此文件中,创建一个函数,让函数返回JSX视图「或者JSX元素、virtualDOM虚拟DOM对象」;这就是创建了一个“函数组件”!! 调用:基于ES6Module规范,导入创建的组件「可以忽略.jsx后缀名」,然后像写标签一样调用这个组件即可!!
<Component/> 单闭合调用 <Component> ... </Component> 双闭合调用命名:组件的名字,我们一般都采用PascalCase「大驼峰命名法」这种方式命名
渲染机制
基于babel-preset-react-app把调用的组件转换为createElement格式
React.createElement(DemoOne, { title: "\u6211\u662F\u6807\u9898", x: 10, data: [100, 200], className: "box", style: { fontSize: '20px' } })把createElement方法执行,创建出一个virtualDOM对象!!
{ $$typeof: Symbol(react.element), key: null, props: {title: '我是标题', x: 10, data: 数组, className: 'box', style: {fontSize: '20px'}}, //如果有子节点「双闭合调用」,则也包含children!! ref: null, type: DemoOne }基于root.render把virtualDOM变为真实的DOM
type值不再是一个字符串,而是一个函数了,此时:
把函数执行 -> DemoOne()
把virtualDOM中的props,作为实参传递给函数 -> DemoOne(props)
接收函数执行的返回结果「也就是当前组件的virtualDOM对象」
最后基于render把组件返回的虚拟DOM变为真实DOM,插入到#root容器中!!
属性props的处理
调用组件的时候,我们可以给调用的组件设置(传递)各种各样的属性
<DemoOne title="我是标题" x={10} data={[100, 200]} className="box" style={{ fontSize: '20px' }} />
- 如果设置的属性值不是字符串格式,需要基于“{}胡子语法”进行嵌套
- 调用组件的时候,我们可以把一些数据/信息基于属性props的方式,传递给组件!!
在JSX元素渲染的时候,如果type...
@1 字符串:创建元素标签....
@2 函数:把函数执行,把解析出来的props当做实参传递给函数
会把解析出来的props「含children」,传递给函数
- 单闭合调用,不能传递子节点信息「没有children」
- 双闭合调用,可以有children -> 实现出类似于vue中插槽的概念「有助于组件的更多复用」
- props是只读的「被冻结」
属性props的处理
调用组件,传递进来的属性是“只读”的「原理:props对象被冻结了」
Object.isFrozen(props) -> true
获取:props.xxx
修改:props.xxx=xxx =>报错
作用:父组件(index.jsx)调用子组件(DemoOne.jsx)的时候,可以基于属性,把不同的信息传递给子组件;子组件接收相应的属性值,呈现出不同的效果,让组件的复用性更强!!
虽然对于传递进来的属性,我们不能直接修改,但是可以做一些规则校验
- 设置默认值
- 设置其它规则,例如:数据值格式、是否必传... 「依赖于官方的一个插件:prop-types」
- 传递进来的属性,首先会经历规则的校验,不管校验成功还是失败,最后都会把属性给形参props,只不过如果不符合设定的规则,控制台会抛出警告错误{不影响属性值的获取}!!
//设置默认值 函数组件.defaultProps = { x: 0, ...... }; //设置规则 https://github.com/facebook/prop-types import PropTypes from 'prop-types'; 函数组件.propTypes = { // 类型是字符串、必传 title: PropTypes.string.isRequired, // 类型是数字 x: PropTypes.number, // 多种校验规则中的一个 y: PropTypes.oneOfType([ PropTypes.number, PropTypes.bool, ]) }; -------------------------------------------------- import PropTypes from 'prop-types'; const DemoOne = function DemoOne(props) { let { title } = props; let x = props.x; x = 1000; return <div className="demo-box"> <h2 className="title">{title}</h2> <span>{x}</span> </div>; }; /* 通过把函数当做对象,设置静态的私有属性方法,来给其设置属性的校验规则 */ DemoOne.defaultProps = { x: 0 }; DemoOne.propTypes = { title: PropTypes.string.isRequired, x: PropTypes.number, y: PropTypes.oneOfType([ PropTypes.number, PropTypes.bool, ]) }; export default DemoOne;如果就想把传递的属性值进行修改,我们可以:
把props中的某个属性赋值给其他内容「例如:变量、状态...」
我们不直接操作props.xxx=xxx,但是我们可以修改变量/状态值!!
插槽
属性和插槽都可以让组件具备更强的复用性
实现具名插槽
// index.jsx root.render( <> <FunctionComponent> <div className='slot-box' slot="head"> 我是插槽信息1 </div> <div className='slot-box' slot="foot"> 我是插槽信息2 </div> </FunctionComponent> </> );// views/FunctionComponent.jsx import React from "react"; const FunctionComponent = function FunctionComponent(props) { // let children = props.children; //获取的值:undefined/一个值/一个数组 let children = React.Children.toArray(props.children),//这样可以保证children一定是个数组 headSlots = children.filter(item => item.props.slot === 'head'), footSlots = children.filter(item => item.props.slot === 'foot'); return <div> {headSlots} 我是组件内容 {footSlots} </div>; }; export default FunctionComponent;函数组件是
静态组件
- 不具备状态、生命周期函数、ref等内容
- 第一次渲染完毕,除非父组件控制其重新渲染,否则内容不会再更新
- 优势:渲染速度快
- 弊端:静态组件,无法实现组件动态更新
import React from "react"; const FunctionComponent = function FunctionComponent() { let num = 0; return <div> {num} <br /> <button onClick={() => { num++; console.log(num); //变量值累加,但是组件不会重新渲染 }}>增加</button> </div>; }; export default FunctionComponent;函数式组件(函数返回JSX元素):Clock时钟组件
- 调取组件可以是单闭合,也可以是双闭合(双闭合调用可以把子元素当做属性中的children传递给组件,类似于vue中的slot)
- 底层运作的时候,如果虚拟DOM对象的type是一个函数(或者一个类),首先会把函数执行(把解析出来的props传递给这个函数),函数会把一个新的虚拟DOM对象返回,最后整体渲染
- React.Children提供对应的方法专门用来处理传递进来的子元素children的
- 传递进来的属性是只读的(只能获取,但是不能直接修改其里面的值),如果非要修改某一个值,可以把其赋值给一个变量(状态)再去修改变量(状态);再或者把属性深克隆出来一份,供调取和修改;
函数式组件属于静态组件,调取组件渲染出一个结果,后续除非重新渲染组件,否则第一次渲染的内容不会改变(当然REACT HOOKS可以帮我们解决这个问题)
function Clock(props) { let index = props.index; index = 1000; let FOOT = null, HEAD = null; React.Children.forEach(props.children, item => { let name = item.props.name; if (name === "HEAD") { HEAD = item; } if (name === "FOOT") { FOOT = item; } }); /* let children = props.children, FOOT = null, HEAD = null; if (children) { if (!Array.isArray(children)) { children = [children]; } HEAD = children.find(item => item.props.name === 'HEAD'); FOOT = children.find(item => item.props.name === 'FOOT'); } */ return <div className={props.className} style={props.style}> {HEAD} <br /> {new Date().toLocaleString()} <br /> {FOOT} <br /> {index} </div>; } ReactDOM.render(<> <Clock index={1} className="text" style={{ color: 'red' }}> <span name="FOOT">哈哈哈哈</span> <span name="HEAD">珠峰培训</span> </Clock> </>, document.getElementById('root'));
扫盲知识点:关于对象的规则设置
- 冻结 冻结对象:Object.freeze(obj) 检测是否被冻结:Object.isFrozen(obj) =>true/false
- 被冻结的对象:不能修改成员值、不能新增成员、不能删除现有成员、不能给成员做劫持「Object.defineProperty」
- 密封 密封对象:Object.seal(obj) 检测是否被密封:Object.isSealed(obj)
- 被密封的对象:可以修改成员的值,但也不能删、不能新增、不能劫持!!
- 扩展 把对象设置为不可扩展:Object.preventExtensions(obj) 检测是否可扩展:Object.isExtensible(obj)
- 被设置不可扩展的对象:除了不能新增成员、其余的操作都可以处理!!
被冻结的对象,即是不可扩展的,也是密封的!!同理,被密封的对象,也是不可扩展的!!

