React基础笔记--1

认识react脚手架

使用create-react-app快速搭建一个react脚手架。首先必须全局安装npm i create-react-app -g

create-react-app --version可以查看当前安装版本号

使用该命令创建一个项目:create-react-app 项目名(项目名字需要遵循npm规范),使用该命令创建的react项目是基于webpack完成的。创建完成的目录结构如下

在这里插入图片描述
在这里插入图片描述

  1. package.json文件中默认会安装:react(核心框架)react-dom(基于React构建的WebAp,即HTML页面)。(react-native构建app)。

  2. react-scripts是脚手架中自己对打包命令的封装,基于它打包会调用node_modules中的webpack进行处理。(react脚手架为了简洁目录结构,并没有vue的配置文件等,将webpack打包的规则及其插件/loader等隐藏到了node_modules目录下)。
    start是开发环境,本地启动web服务器运行。build是生成环境,打包部署,将打包的内容放在build目录中。test是单元测试。eject暴露webpack配置,供用户修改打包规则。
    在这里插入图片描述

  3. web-vitals是性能检测工具

  4. 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 lessnpm 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.HOSTprocess.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-middleware3.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语法

  1. 第一种方法,前提入口文件为index.js,先点击vscode底部该标签,然后在弹出的输入框中输入react,选择第一个即可。这个时候html的快捷操作就可以实现了。缺点是每次都需要手动选择。
    在这里插入图片描述
    在这里插入图片描述
  2. 方法二:直接将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>
);

注意点:

  1. 不能指定body或html为根容器,需要额外指定容器
const root = ReactDOM.createRoot(document.body); //错误

在这里插入图片描述

  1. 构建视图的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显示
  • 字符串赋什么值,页面就显示什么值
  • 赋予InfinityNaN,最终页面显示的InfinityNaN,而不是显示空
  • 当赋值布尔值undefinednullSymbol()10nvoid 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> &nbsp;&nbsp;
            <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>
  </>
);
  1. 上面这段编写的JSX代码,首先会被编译为虚拟DOM对象,即virtualDOM。
    *:虚拟DOM:React框架内部构建的对象体系,对象的相关成员都是由react规定。基于这些对象的属性描述,构建视图中DOM节点的相关特征。( 每次虚拟DOM都会被缓存,用作下次比对
  2. 将构建的虚拟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循环,获取所有的私有属性相应的方法如下

  1. 方法一
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为字符串,不能再次添加字符串包裹

在这里插入图片描述

  1. 方法二
    实现效果和上面代码一致,缺点不兼容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元素结构上又存在两种不同的方法,且作用各不相同

  1. 方法一,假设页面有一个id为root的容器,为一个元素设置属性,可以直接采样对象.属性的方法添加。但是这种方法中,自定义属性无法在页面标签结构中显示添加的属性,所以的属性均被添加到堆内存中,但是如果添加到内置属性时,会设置在元素的标签身上,同时堆内存不在添加
    在这里插入图片描述

  2. 使用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'
  }
});

在这里插入图片描述
执行的基本步骤

  1. 首先基于babel-preset-react-app将JSX语法转换为createElement方法格式
  2. 根据转换的createElement方法执行该函数,该函数会返回一个虚拟DOM对象,对象的格式上图所示,重点是type变为组件函数
  3. render函数中,会根据生成的虚拟DOM,先去执行type所对应的函数。同时将当前组件标签定义的属性(即上图的props)传递给该函数。
  4. 最终调用该函数会返回一个组件标签内部的虚拟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>
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值