From 2f9587c845c5c72864942d54162f9e6321d5e015 Mon Sep 17 00:00:00 2001 From: ZiuChen Date: Sun, 16 Apr 2023 21:49:48 +0800 Subject: [PATCH] docs: React Note update --- docs/note/React.md | 691 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 691 insertions(+) diff --git a/docs/note/React.md b/docs/note/React.md index 7d22b8e1..19633186 100644 --- a/docs/note/React.md +++ b/docs/note/React.md @@ -341,4 +341,695 @@ root.render() - All in JS - 不同于Vue的模板语法 不需要专门学习模板语法中的指令(v-for/v-if/v-bind) +### JSX的使用 +#### 书写JSX的规范与注意事项 + +- JSX的顶层只能有一个根元素 元素必须包裹在单独的闭合标签中 + - 后续会接触到Fragment标签 Vue3也是将元素包裹在了Fragments标签中 +- 为了方便阅读 通常在JSX外层包裹一个小括号`()`方便阅读 + +#### JSX的注释 + +在JSX中编写注释,需要以`{/* ... */}`的形式,在`.jsx/.tsx`文件中,通过快捷键就可以快捷的生成注释内容 + +本质上是通过花括号语法`{}`嵌入了一段JavaScript表达式,在表达式中书写注释 + +```tsx{4} +... +return ( +
+ {/* Some Comment... */} +

Count: {count}

+ + +
+) +... +``` + +#### JSX嵌入变量作为子元素 + +可以通过花括号语法将变量内容嵌入到JSX语法中: + +```tsx +const message = 'Hello, React!' +const arr = ['abc', 'cba', 'nba'] + +return ( +
+

{ message }

+
{ arr }
+
+) +``` + +- 变量类型为number string array类型时,可以直接展示 +- 变量类型为null undefined boolean类型时,内容为空 + - 如果希望可以展示null/undefined/boolean类型,需要通过`.toString()`方法将其转为字符串 + - 空字符串拼接、String构造函数等方式 +- Object对象类型不能作为子元素 (Objects are not valid as a React child) + +下例中,只有number类型会被正常展示,而其余变量则不会展示在视图中 + +```tsx +render() { + const number = 123 + const n = null + const u = undefined + const b = true + + return ( +
+
+ Number: {number} +
+
+ Null: {n} +
+
+ Undefined: {u} +
+
+ Boolean: {b} +
+
+ ) +} +``` + +将对象类型变量嵌入到JSX语法中,React会抛出错误: + +```tsx {6} +... +render() { + const obj = { name: 'Ziu' } + return ( +
+ { obj } +
+ ) +} +... +``` + +#### JSX的属性绑定 + +- 在Vue中我们通过`v-bind`绑定属性 +- 在React中如何绑定元素属性? +- `title` `src` `href` `class` 内联`style`等 + +下例中,我们通过花括号语法对元素的属性进行了动态绑定,点击按钮可以切换className状态 + +同时,动态绑定的内联样式也会发生改变,通过花括号语法动态绑定style属性 + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isActive: false, + title: 'Description' + } + this.changeActive = this.changeActive.bind(this) + } + + changeActive() { + this.setState({ + isActive: !this.state.isActive + }) + } + + render() { + const { isActive, title } = this.state + const classList = ['title', isActive ? 'active' : ''] + + return ( +
+
+ Hello, React! +
+ +
+ ) + } +} +``` + +当我们通过脚手架创建项目时,可以使用第三方库来帮我们完成className的绑定 + +- `classnames`库 `pnpm add classnames` +- 提供了多种创建className的语法 + +### JSX事件绑定 + +先前的例子中,我们已经通过`onClick`给按钮绑定过事件处理函数了,其中涉及了this绑定 + +回顾一下this的四种绑定规则: + +1. 默认绑定 独立执行 foo() this => undefined +2. 隐式绑定 被一个对象执行 obj.foo() this => obj +3. 显式绑定 call/bind/apply foo.call('aaa') this => String('aaa') +4. new绑定 new Foo() 创建一个新对象,并且赋值给this + +除了之前通过`function + bind`绑定事件处理函数的方式,还可以通过箭头函数来帮我们完成处理 + +箭头函数的内部使用this时会自动向上层作用域查找this 实际开发中这种方式并不常用 + +```tsx {2} +... +changeActive = () => { + this.setState({ + isActive: !this.state.isActive + }) +} +... +``` + +相比之下更推荐使用的,是下面这种方式: + +```tsx {2} +... + +... +``` + +这样书写有几种好处: + +- 给事件处理函数传递参数更方便 +- 书写更方便 不必主动考虑this绑定问题 + +它的原理是,我们对外暴露的本质上是一个箭头函数,当调用箭头函数时,本质上是执行`this.changeActive`,这是 一种隐式绑定,找到的this为当前组件实例 + +### 事件绑定参数传递 + +- Event参数传递 +- 额外参数传递 + +事件回调函数的第一个默认参数就是Event对象,这个Event对象是经过React包装后的,但是原生的属性都包含在内,React对其进行了一些扩展 + +```tsx {13} +... +changeActive(ev) { + console.log('Event', ev) +} + +render() { + return ( +
+ {/* event将作为默认入参传递给changeActive */} + + + {/* 通过箭头函数绑定事件监听回调函数时 需要手动透传一下event */} + +
+ ) +} +... +``` + +当我们需要传递额外的参数时,通过箭头函数传递也更容易: + +```tsx {13} +changeActive(ev, name, age) { + console.log('Event', ev) + console.log('Name', name) + console.log('Age', age) +} + +render() { + return ( +
+ {/* NOT Recommand */} + + {/* Recommand */} + +
+ ) +} +``` + +需要注意,当通过`.bind`传递额外参数时,最后一个参数才是默认传递的Event对象,这会导致非预期行为 + +```sh +> Event 'Ziu' +> Name 18 +> Age {Event} +``` + +### JSX事件绑定案例 + +创建一个Tab栏,选中哪个选项,哪个选项被激活切换为红色,同一时间仅有一个激活项目 + +结合之前学习的内容,很容易就可以写出下述 代码: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + activeIndex: 0, + tabList: ['Home', 'Recommend', 'Hot', 'User'] + } + } + + changeActive(index) { + this.setState({ + activeIndex: index + }) + } + + render() { + const { activeIndex, tabList } = this.state + + return ( +
+
+ {tabList.map((item, index) => ( + + ))} +
+
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +### 条件渲染 + +控制元素按照某种条件渲染,以加载状态为例 + +列表未加载出来时,展示`加载中`,加载完毕则渲染完整内容: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isLoading: true + } + } + + changeLoading() { + this.setState({ + isLoading: !this.state.isLoading + }) + } + + render() { + const { isLoading } = this.state + + return ( +
+ {isLoading ? ( +
Loading ...
+ ) : ( +
Some Content
+ )} + +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +常用的条件渲染方式 + +- `if/else/else-if` + - 适合判断逻辑较复杂的情况 将条件渲染抽离出来 +- 三元运算符 `?:` + - 适合判断逻辑简单的情况 +- 逻辑与运算符 `&&` + - 如果条件成立则渲染某个组件,否则什么内容都不渲染 +- 可选链 `user?.info?.name` + +下例中通过逻辑与运算符`&&`决定`VIP`标签是否展示在视图中 + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isVip: false + } + } + + changeVip() { + this.setState({ + isVip: !this.state.isVip + }) + } + + render() { + const { isVip } = this.state + + return ( +
+
+ username: Ziu + {isVip && VIP } +
+ + +
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +#### 在React中简单写一个"v-show" + +`v-show`是Vue提供的语法糖,不同于`v-if`,它只切换元素的`display`属性。 + +下面我们在React中简单复现一个`v-show`的效果: + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + isShow: true + } + } + + changeShow() { + this.setState({ + isShow: !this.state.isShow + }) + } + + render() { + const { isShow } = this.state + + return ( +
+
Target Element
+ +
+ ) + } +} +``` + +实际使用中,将其封装为hooks来调用更具通用性,也更方便管理 + +### 列表渲染中的高阶函数 + +- `filter`函数 过滤器 +- `slice`函数 分页 +- `sorc`函数 排序 +- ... + +```tsx +class App extends React.Component { + constructor() { + super() + this.state = { + stuList: [ + { name: 'Ziu', age: 18, score: 88 }, + { name: 'Kobe', age: 19, score: 59 }, + { name: 'Why', age: 20, score: 61 }, + { name: 'James', age: 21, score: 99 } + ] + } + } + + render() { + const { stuList } = this.state + + // 及格的学生 + const passStuList = stuList.filter((item) => item.score >= 60) + + // 分数最高的两个学生 + const top2StuList = stuList.sort((a, b) => b.score - a.score).slice(0, 2) + + return ( +
+
+ {stuList.map(({ name, age, score }) => ( +
+ {name} + {age} + {score} +
+ ))} +
+
+ ) + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +### JSX的本质 + +#### JSX的转换过程 + +假设我们有下面的JSX代码: + +```tsx +class App extends React.Component { + constructor() { + super() + } + + render() { + const page = ( +
+
Header
+
+ Content +
Banner
+
    +
  • Item 1
  • +
  • Item 2
  • +
  • Item 3
  • +
  • Item 4
  • +
  • Item 5
  • +
+
+
Footer
+
+ ) + console.log(page) + return
{page}
+ } +} +``` + +通过JSX语法描述出来的template会经过Babel转化,转化为JavaScript树的数据结构 + +在控制台中我们可以看到,子节点都存放进了父节点的`props.children`中 + +#### 虚拟DOM树 + +JSX仅仅是`React.createElement(component, props, ...children)`的语法糖 + +所有的JSX语法都会被Babel转化为这样的命令式语法 + +.createElement函数的参数 + +- type + - 当前ReactElement的类型 + - 如果是标签元素,值为字符串如:`"div"` + - 如果是组件元素,那么值为组件的名称 +- config + - 所有JSX中绑定的属性都在config中以键值对的形式存储 + - 例如`className` => `class` + +我们借助Babel官网的Playground来检查一下JSX语法的转化 + +```js +import { jsx as _jsx } from "react/jsx-runtime"; +import { jsxs as _jsxs } from "react/jsx-runtime"; +const page = /*#__PURE__*/_jsxs("div", { + className: "page", + children: [/*#__PURE__*/_jsx("div", { + className: "header", + children: "Header" + }), /*#__PURE__*/_jsxs("div", { + className: "content", + children: ["Content", /*#__PURE__*/_jsx("div", { + className: "banner", + children: "Banner" + }), /*#__PURE__*/_jsxs("ul", { + children: [/*#__PURE__*/_jsx("li", { + children: "Item 1" + }), /*#__PURE__*/_jsx("li", { + children: "Item 2" + }), /*#__PURE__*/_jsx("li", { + children: "Item 3" + }), /*#__PURE__*/_jsx("li", { + children: "Item 4" + }), /*#__PURE__*/_jsx("li", { + children: "Item 5" + })] + })] + }), /*#__PURE__*/_jsx("div", { + className: "footer", + children: "Footer" + })] +}); +console.log(page); +``` + +这时经过Babel转义后的纯JS函数,这段函数可以在浏览器中直接运行 + +如果移除了相关JSX代码,并将他们都替换为`React.createElement`函数调用,那么得到的代码也可以直接在浏览器中运行。Babel帮助我们完成了转化,提高了开发效率,相比于通过调用`React.createElement`来描述视图,通过JSX编写的代码更加容易维护 + +这些代码最终形成的就是虚拟DOM树,React可以将虚拟DOM渲染到页面上,形成真实DOM + +虚拟DOM允许React可以通过diff算法,高效地对真实DOM树进行更新 + +### 声明式编程 + +- 虚拟DOM帮我们从命令式编程转到了声明式编程的模式 +- 对虚拟DOM作何处理,如何渲染是由React决定的,由于做了一层抽象,那么同样可以将虚拟DOM渲染成原生组件(React Native) + +### 购物车案例 + +下面写一个经典的购物车案例 + +```tsx +function formatPrice(price) { + return `$ ${price.toFixed(2)}` +} + +class App extends React.Component { + constructor() { + super() + this.state = { + books: [ + { name: 'book1', author: 'author1', price: 100, count: 0 }, + { name: 'book2', author: 'author2', price: 200, count: 0 }, + { name: 'book3', author: 'author3', price: 300, count: 0 }, + { name: 'book4', author: 'author4', price: 400, count: 0 } + ] + } + } + + changeCount(index, count) { + this.setState((state) => { + const books = [...state.books] + books[index].count += count + return { books } + }) + } + + removeItem(index) { + this.setState((state) => { + const books = [...state.books] + books.splice(index, 1) + return { books } + }) + } + + getTotal() { + const { books } = this.state + return books.reduce((acc, { price, count }) => acc + price * count, 0) + } + + renderBookCart() { + const { books } = this.state + const total = this.getTotal() + return ( +
+

Shopping Cart

+
+ {books.map(({ name, author, price, count }, index) => ( +
+ {index + 1} + {name} + {author} + {formatPrice(price)} + + + {count} + + + +
+ ))} +
+
+ Total: {formatPrice(total)} +
+
+ ) + } + + renderEmptyTip() { + return
Shopping Cart is Empty
+ } + + render() { + const isEmpty = this.state.books.length === 0 + + return !isEmpty ? this.renderBookCart() : this.renderEmptyTip() + } +} + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() +``` + +## React脚手架 + +- 认识脚手架工具 +- create-react-app +- 创建React项目 +- Webpack的配置 + +类似于Vue提供的 `pnpm create vite` 创建一个模板,React也可以通过 `create-react-app` 来初始化一个空的React模板 + +```sh +pnpm add create-react-app -g # 全局安装create-react-app +create-react-app react-app # 创建一个名为react-app的React项目 +# 删除node_modules package-lock.json +cd react-app +pnpm i # 使用pnpm重新安装依赖 +``` + +```tsx +// index.js +import ReactDOM from 'react-dom/client' +import App from './App' + +const root = ReactDOM.createRoot(document.querySelector('#root')) +root.render() + +// App.js +import React from 'react' + +export default class App extends React.Component { + render() { + return
Hello, React!
+ } +} +``` \ No newline at end of file