react学习手记(未完)

299 阅读11分钟

青训营课程笔记

JSX和React元素

Untitled.png

JSX是JS的扩展语法,React里基于JSX语法描述组件UI的结构。

react元素就是JS的一个对象,里面包含了一些重要属性。

//JSX标签类型:1,html标签;2,react组件作为标签(如<APP />)。
ReactDOM.render(
  <React.StrictMode>
	  <App />
  </React.StrictMode>,
  document.getElementById('root')
);

如何将JSX描述的UI结构和真实的DOM结构联系在一起?即怎么把React元素渲染到DOM中?

//使用render API
const element = <h1>Hello,world</h1>;
ReactDOM.render(element,document.getElementById('root'));

root节点在public-index.html中定义,渲染到该节点下:

 <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

React组件

React组件是react元素之上的一层封装。本质上是一个函数,签名表达:component = (props)⇒element。接受props参数,返回element元素。

  • 是React应用组成和复用的基本单元。

  • React组件必须像纯函数一样运行。

    纯函数:对于相同的入参返回同样的结果。无副作用。(副作用是函数内部做了函数作用域以外的事情,比如访问外部变量、全局变量,对传参做修改。常见副作用是网络请求的调用,这也是对全局变量的访问)

    React组件并非真正的纯函数,只是对副作用有相应规则和约束。

  • 有函数组件和类组件

  • 列表渲染,列表中的每一项必须设置key属性。这是react内部出于性能优化的设计,会基于key判断当前元素是可以复用还是需要创建一个新元素。

props

父组件传递给子组件的属性,在子组件内部只读。

props根据固定的入参渲染出固定的React元素。

state

组件内部自己维护的属性,可以被修改。

如果根据变量动态渲染react元素,需要使用state,即react内部的可变状态。state只能在类组件中使用,以clock类组件为例:

import React, { Component } from 'react';

export default class Clock extends Component {
  constructor(props) {
    super(props);
    this.state = { date: new Date() };
  }

  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date(),
    });
  }

  render() {
    return (
      <div>
        <h1>Current Time: {this.state.date.toLocaleTimeString()}.</h1>
      </div>
    );
  }
}

当前时间赋值为初始值,初始值的更新需要setState API,即tick()中的方法。在哪里调用tick方法涉及生命周期。

两个生命周期方法:componentDidMount()指组件被挂载到DOM节点的时候被调用;componentWillUnmount()指组件被从DOM节点卸载前被调用。在组件被挂载之后,通过setInterval API创建一个循环任务,这个循环任务每隔一秒调用tick方法,实现state的更新。在组件卸载时,使用clearInterval清除setInterval 的定时任务。这个setInterval 也是一个副作用,对副作用要有相应规则和约束,所以将其放在React组件的生命周期方法中。render中不能有副作用代码。

把clock组件引入app.js,就完成了页面效果。

  • 函数组件是无状态组件;类组件可能是有状态组件。

组件三种状态:挂载-更新-卸载阶段,不同阶段对应不同生命周期方法,生命周期方法只对类组件有意义。函数式组件函数式组件没有生命周期方法。但是函数组件也会有三个阶段,挂载阶段更新阶段卸载阶段。三阶段会被调用的方法:

Untitled (1).png

setState

  • 必须使用setState API来修改state
//Wrong
this.state.comment = 'Hello';
//Correct
this.setState({comment:'Hello'});
  • 如果新状态需要依赖当前状态和props计算获得,不能给setState传入对象形式的参数,而是要传入函数形式的参数。函数接收当前应用的最新state和当前组件最新的props。这和react内部组件对于setState的触发机制有关。
// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});
// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

出于性能考虑,React 可能会把多个setState()调用合并成一个调用。因为this.props和this.state可能会异步更新,所以不要依赖他们的值来更新下一个状态。要解决这个问题,可以让setState()接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数。

  • 对state部分修改而不是全量替换。

受控组件

Untitled (2).png

如果需要和页面交互,就涉及表单元素,如input。

这里render里使用form标签,里面使用input标签。input通过value属性设置值,value值通过state中获取。当input发生变化,通过监听onchange事件,更改state中的value。onchange的响应函数,通过调用setstate API将value更新为最新的值。将state更新之后,render方法重新执行,这时候this.state.value得到的就是最新的状态值,从而实现input值的更新。

input值的来源于组件的状态,其变化也依赖于通过setstate更新组件的状态。所以input值和值的变化的管理逻辑都受控于react组件,即“通过受控组件的方式使用表单元素”。

非受控组件

Untitled (3).png

表单元素的值不受react组件控制,是自己内部控制。不再有value和onchange属性。

当组件内部想使用input元素的值,就需要使用ref属性。所有JSX标签都可以设置ref属性,ref引用的是当前标签对应的元素。ref的值,必须通过React.createRef API创建一个ref类型的值,才能给ref属性设置。将ref属性复制给this.input,当想要获取input值,通过this.input获取,如this.input.current.value()。【过程?】

常用模式:组件的组合用法

Untitled (4).png

解耦:不同的组件关注不同的层面。相当于在FancyBorder中打洞,具体洞里的内容在使用FancyBorder时决定。

多打几个洞:

Untitled (5).png

其实就是聊天室布局。

是更灵活的组合方式。

Hooks

State Hook

管理状态的函数。返回结果是一个数组类型,第一个参数是state状态,第二个参数是一个方法,用来修改state。参数名可以随便起,习惯上是xxx,setxxx。

const[state,setState] = useState(initialState);

只能在函数组件中使用。

/** hooks 版本 */
function AddPostForm(props){

  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const handleChange = (e) => {
    if (e.target.name === 'title') {
      setTitle(e.target.value)

    } else if (e.target.name === 'content') {
      setContent(e.target.value)
    }
  };

  const handleSave = async () => {
    if (title.length > 0 && content.length > 0) {
      console.log(title, content)
    }
  };

  return (
      <section>
        <h2>Add a New Post</h2>
        <form>
          <label htmlFor="title">Post Title:</label>
          <input
            type="text"
            id="title"
            name="title"
            value={title}
            onChange={handleChange}
          />
          <label htmlFor="content">Content:</label>
          <textarea
            id="content"
            name="content"
            value={content}
            onChange={handleChange}
          />
          <button type="button" className="button" onClick={handleSave}>
            Save Post
          </button>
        </form>
      </section>
    );
};

AddPostForm有两个可变状态,用两个usestate得到这两个state的值。input表单元素里的value属性和texttarea里的value属性,接收title和content状态。当状态发生改变时,onchange的响应函数,通过调用settitle和settexttarea API将状态修改。状态修改后,AddPostForm组件重新运行,input和texttarea得到更新后的title和content。AddPostForm重新运行时,useState初始值不起作用。

const [{title,content},setState] = useState({
	title:'',
	content:''
})
//可以,但不再是对状态的局部修改了。

Effect Hook

Effect副作用。管理react函数中的副作用。曾经组件的副作用可以放到生命周期方法中,Effect Hook也可以做到。

useEffect(()=>{effectFn();return clearFn},[...dependencies]);

useEffect接收两个参数,第一个参数是一个函数,函数内可以执行带有副作用逻辑的函数effectFn(),函数可以有一个返回值clearFn。每次执行useEffect的时候都会先执行上一次useEffect内的返回函数clearFn,所以在clearFn里可以做清除副作用的逻辑;第二个参数是可选的,数组类型参数,如果不传则每次useEffect执行,effectFn都会执行。如果传入了参数,只有数组内的变量发生变化的时候,effectFn才会被执行。如果传入了空数组,effectFn只会被执行一次,是在组件被挂载完成之后执行一次。注意:useEffect每次都会执行,但不代表effectFn会被执行。

//发送网络请求获取文章数据,发送网络请求是副作用。
function App() {
  const [articles, setArticles] = useState([])

  useEffect(()=> {
    const fetchArticles = async () => {
      try{
        const response = await client.get('/fakeApi/posts');
        console.log(response)
        setArticles(response.posts);
      }catch(err){
        console.error(err)
      }
    }

    fetchArticles();
  }, [])

  return (
    <div>
      <ArticleList articles={articles}/>
    </div>
  )

}

Untitled (6).png

自定义Hooks

  • 以use作为函数名开头,封装通用逻辑的函数

Untitled (7).png

上例即模拟组件挂载阶段的生命周期函数。在组件挂载之后实现fn函数。

```
import React, { useEffect,useState } from 'react';

function useCurrentDate(){
  //首先在useCurrentDate中用useState提前创建出date状态和设置date的方法
  const [date, setCurrentDate] = useState(new Date());

  const tick = () => {
    setCurrentDate(new Date())
  }
//第二在useEffect中创建一个副作用逻辑,通过setInterval创建一个定时任务
//每隔一秒调用tick函数,date状态每隔一秒更新一次
//useEffect的第一个参数函数返回一个clearInterval
//保证组件卸载时清除setInterval创建的逻辑,防止内存泄漏
  useEffect(() => {
    const timer = setInterval(()=>{tick()}, 1000)
    return () => {
      clearInterval(timer)
    }
  }, [])

  return date;
}

//clock组件借助hooks使用函数组件就变成了以下几行
//前提是封装出来一个useCurrentDate()的Hook函数,获取当前时间
//如果其他组件需要获取当前时间,只需调用useCurrentDate()函数
export default function Clock() {

  const date = useCurrentDate();

  return (
    <div>
      <h1>Current Time: {date.toLocaleTimeString()}.</h1>
    </div>
  );
}
```

类组件中,对于date相关状态管理分散在组件的各个生命周期函数中,很难复用这堆逻辑。通过自定义Hook方法,可以将状态管理逻辑封装到一个Hook函数中,在函数组件中使用,从而实现状态管理逻辑复用的目的。

Hooks规则

  • 只能在函数组件、自定义hook中调用hook

  • 只能在函数的最顶层控制流中调用hook

Untitled (8).png

两个错误例子:

在if分支内使用hook,错误。和if这一层逻辑平级的控制流才是最顶层控制流。

在点击button的响应函数中使用hook,错误。

Why Hooks?

  • 组件状态管理逻辑的封装和复用
  • 面向react未来:类组件受生命周期方法、this问题限制,复杂度比函数组件高,性能优化和更高级的特性受到阻碍。通过函数组件和hooks结合,使react更轻量。

官方文档学习札记

新数据替换旧数据

var newPlayer = Object.assign({}, player, {score: 2});
// player 的值没有改变, 但是 newPlayer 的值是 {score: 2, name: 'Jeff'}

// 使用对象展开语法,就可以写成:
// var newPlayer = {...player, score: 2};

不直接修改(或改变底层数据)可以让我们追溯并复用历史记录。
跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次,现在只要发现对象变成了一个新对象,那么我们就可以说对象发生改变了。
帮助我们在 React 中创建 pure components。确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。

JSX

React 认为渲染逻辑本质上与其他 UI 逻辑内在耦合。并没有采用将标记与逻辑进行分离到不同文件,而是将二者共同存放在称之为“组件”的松散耦合单元之中。
在编译之后,JSX 表达式会被转为普通 JavaScript 函数调用,并且对其取值后得到 JavaScript 对象。
因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。
Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。

向事件处理程序传递参数

在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind 来实现。

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。【第二种是为什么?如何隐式传递?】

事件处理

通常情况下,如果你没有在方法后面添加 (),例如 onClick={this.handleClick},你应该为这个方法绑定 this。【为什么?】

如果觉得使用 bind 很麻烦,这里有两种方式可以解决。如果你正在使用实验性的 public class fields 语法,你可以使用 class fields 正确的绑定回调函数:【什么是Pubic class fields语法?】

如果你没有使用 class fields 语法,你可以在回调中使用箭头函数:此语法问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。【为什么?】

条件渲染

 render() {
    const isLoggedIn = this.state.isLoggedIn;
    let button;
  const unreadMessages = props.unreadMessages;

【为什么?】

return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&        <h2>          You have {unreadMessages.length} unread messages.        </h2>      }    </div>
  );

【如果false && ... 返回什么?】 如果条件是 true&& 右侧的元素就会被渲染,如果是 false,React 会忽略并跳过它。