React笔记

83 阅读17分钟

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让用户自己去修改配置项

cli.vuejs.org/zh/config/

1.先把配置项暴露出来 yarneject/yarn eject / 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

QQ图片20221128002927

package.json_scripts

QQ图片20221128002443

package.json_eslintConfig

QQ图片20221128003010

package.json_browserslist

QQ图片20221128003115

使用脚手架创建的项目

QQ图片20221128003737

QQ图片20221128003741

修改React中的webpack配置项

和Vue一样,React脚手架也默认把配置好的webpack那些

vue是提供vue.config.js让用户自己去修改配置项

cli.vuejs.org/zh/config/

默认情况下,会把webpack配置项隐藏到node_modules中,如果想修改,则需要暴露配置项:

1.先把配置项暴露出来 yarneject/yarn eject / npm run eject

QQ图片20221128021226

2.细节点:不可逆转的 + 如果有git先要保存修改项

QQ图片20221128021233

config webpack配置项

|- webpack.config.js

|- webpackDevServer.config.js

|- path.js 存放各配置的地址文件信息(入口文件)

|- ...

QQ图片20221128021447

scripts

|- start.js 开发环境下 $ yarn start 先执行这个文件

|- build.js 生产环境下 $ yarn build 先执行这个文件

|- ...

QQ图片20221128021451

QQ图片20221128021549

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
        |- ...

QQ图片20221128021922

babel-preset-react-app

对 @babel/preset-env 语法包的重写「目的:把ES6转为ES5」
重写的目的:让语法包可以识别React的语法,实现代码转换

QQ图片20221128022235

真实项目中常用的一些修改操作

配置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-loader

2、暴露项目配置项(任选一种)

若报错,有git的可以通过 
    git add .
    git commit -m '暴露项目配置项'
    npm run eject

npm方式:
    npm run eject

cnpm方式:
    cnpm run eject

yarn方式:
    yarn eject

3、配置webpack.config.js

config / webpack.config.js

QQ图片20221128022410

QQ图片20221128022414

QQ图片20221128022418

配置完,重启项目

配置别名

//313
resolve: {
  ...
  alias: {
    '@': path.appSrc,
    ...
  }
}

QQ图片20221128022513

QQ图片20221128022553

配置预览域名

// scripts/start.js
// 48
const HOST = process.env.HOST || '127.0.0.1';

QQ图片20221128022712也可以基于 cross-env 设置环境变量

$ yarn add cross-env

QQ图片20221128022759

配置端口号

 "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

QQ图片20221128023200

QQ图片20221128023220

配置浏览器兼容

//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';

QQ图片20221128022909

$ yarn add @babel/polyfill

在入口中:
import '@babel/polyfill'

脚手架中不需要我们自己去安装:react-app-polyfill 「对@babel/polyfill的重写」

QQ图片20221128023029

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的重排/重绘 + 开发效率更高、最后的性能也相对较好

  1. 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三层架构

image-20220508134302000

image-20220710213729379

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渲染机制流程图

第一步:把我们编写的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中的组件化开发:

fivedodo.com/upload/html…

  • 全局组件和局部组件
  • 函数组件(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)
    • 被设置不可扩展的对象:除了不能新增成员、其余的操作都可以处理!!

被冻结的对象,即是不可扩展的,也是密封的!!同理,被密封的对象,也是不可扩展的!!