现代Web编程-React入门

教程链接:全栈公开课2020

一.React简介

React官网地址:https://reactjs.org/

目前来说,创建一个React应用最简单的方式是使用一个叫做create-react-app的工具。
让我们创建一个名为 part1 的应用,并进入到它的目录。

$ npx create-react-app part1
$ cd part1

用如下命令就可以让应用运行起来了

$ npm start

默认情况下,应用在本地localhost,3000端口运行,地址为 http://localhost:3000

应用的代码位于src 文件夹中。 让我们简化一下默认代码,将文件index.js 的内容改成:

import React from 'react'
import ReactDOM from 'react-dom'
//定义一个组件,命名为App
const App = () => (
  <div>
    <p>Hello world</p>
  </div>
)

//将组件内容渲染到div元素中,其id值为'root',该元素在文件public/index.html中定义。
ReactDOM.render(<App />, document.getElementById('root'))

组件

const App = () => (
  <div>
    <p>Hello world</p>
  </div>
)

严格来说,这个组件被定义成了一个 JavaScript 函数。

由于这个函数只包含一个表达式,所以我们使用了简写,不简写的话是如下这段代码:

const App = () => {
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

你还可以在组件内部渲染动态内容。

const App = () => {
  const now = new Date()
  const a = 10
  const b = 20

  return (
    <div>
      <p>Hello world, it is {now.toString()}</p>
      <p>
        {a} plus {b} is {a + b}
      </p>
    </div>
  )
}

JSX

React组件返回的是JSX。尽管JSX看起来像 HTML,但我们其实是在用一种特殊的方法写JavaScript。在底层,React组件实际上返回的JSX会被编译成JavaScript。

编译后,我们的应用如下所示:

import React from 'react'
import ReactDOM from 'react-dom'

const App = () => {
  const now = new Date()
  const a = 10
  const b = 20
  return React.createElement(
    'div',
    null,
    React.createElement(
      'p', null, 'Hello world, it is ', now.toString()
    ),
    React.createElement(
      'p', null, a, ' plus ', b, ' is ', a + b
    )
  )
}

ReactDOM.render(
  React.createElement(App, null),
  document.getElementById('root')
)

JSX 是一种“类XML”语言,这意味着每个标签都需要关闭。 例如,换行符是一个空元素,在 HTML 中可以这样写:
<br>
但是在写 JSX 时,标签需要如下关闭:
<br />

Multiple components

const Hello = () => {
  return (
    <div>
      <p>Hello world</p>
    </div>
  )
}

const App = () => {
  return (
    <div>
      <h1>Greetings</h1>
      <Hello />
      <Hello />
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

这里我们定义了一个新的组件Hello,并在组件App 中引用了它。

实际上,React 的核心理念,就是将许多定制化的、可重用的组件组合成应用。

还有一个约定,就是应用的组件树顶部都要有一个root组件叫做App。 然而,在某些情况下,组件的根并不一定是App,而是包装在了一些工具组件中。

props:向组件传递数据

使用所谓的props,可以将数据传递给组件。

const Hello = (props) => {
  return (
    <div>
      <p>
        Hello {props.name}, you are {props.age} years old
      </p>
    </div>
  )
}

现在定义组件的函数有一个参数props。作为参数,它接收了一个对象,该对象具有组件中所定义的、用于定义user的所有“属性”所对应的字段。

props 按如下定义:

const App = () => {
  const name = 'Peter'
  const age = 10

  return (
    <div>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <Hello name={name} age={age} />
    </div>
  )
}

可以有任意数量的props,它们的值可以是“硬编码的”字符串,也可以是 JavaScript 表达式的结果。

一些注意事项

  • 在 React 的代码中编写console.log()命令(打印到控制台)是可行的,而且是提倡的。
  • React 组件名称必须大写。
  • React 组件的内容(通常)需要包含一个根元素。

使用根元素并不是唯一可行的选择,通过创建组件数组也是一个有效的解决方案:

const App = () => {
  return [
    <h1>Greetings</h1>,
    <Hello name="Maya" age={26 + 10} />,
    <Footer />
  ]
}

但是,在定义应用的根组件时,数组这种方案并不明智,而且会使代码看起来有点难看。

由于根元素是必须的,所以在Dom 树中会有“额外的”div元素。 这可以通过使用fragments来避免,即用一个空元素来包装组件的返回内容:

const App = () => {
  const name = 'Peter'
  const age = 10

  return (
    <>
      <h1>Greetings</h1>
      <Hello name="Maya" age={26 + 10} />
      <Hello name={name} age={age} />
      <Footer />
    </>
  )
}

二.JavaScript

JavaScript 标准的正式名称是ECMAScript。 目前最新的版本是2019年6月发布的ECMAScript 2019,即ES10。

浏览器不能支持所有JavaScript的最新特性。 许多在浏览器中运行的代码需要从一个新版本的JavaScript转译到一个更旧、更兼容的版本。

如今,最流行的转译方法是使用Babel。 在使用 create-React-app 创建的React应用中转译是自动配置好的。

Node.js是一个基于谷歌的chrome V8引擎的JavaScript运行时环境,可以在任何地方工作,从服务端到移动端。最新版本的 Node 能够理解 JavaScript 最新版本的特性,因此代码不需要被转译。

变量

const x = 1
let y = 5

console.log(x, y)   // 1, 5 are printed
y += 10
console.log(x, y)   // 1, 15 are printed
y = 'sometext'
console.log(x, y)   // 1, sometext are printed
x = 4               // causes an error

const定义了一个常量,也就是其值不能再更改了。 let定义了一个普通变量。

也可以使用关键字var在 JavaScript 中定义变量。 const 和 let 是在ES6版本中添加的。在本课程中明确不建议使用var,应该坚持使用const 和let!

延伸阅读:前端面试题:JS中的let和var的区别

数组

const t = [1, -1, 3]

t.push(5)

console.log(t.length) // 4 is printed
console.log(t[1])     // -1 is printed

t.forEach(value => {
  console.log(value)  // numbers 1, -1, 3, 5 are printed, each to own line
})

map方法创建一个新的数组

const t = [1, 2, 3]
const m = t.map(value => value * 2)
console.log(m)   // [2, 4, 6] is printed

数组中的单个元素可以通过解构赋值赋给变量。

const t = [1, 2, 3, 4, 5]

const [first, second, ...rest] = t

console.log(first, second)  // 1, 2 is printed
console.log(rest)          // [3, 4 ,5] is printed

延伸阅读:箭头函数

Objects

在 JavaScript 中,定义对象常见的方法是使用对象字面量,就是通过在大括号中列出它的属性来实现的:

const object1 = {
  name: 'Arto Hellas',
  age: 35,
  education: 'PhD',
}

对象的属性可以使用“句点”号或括号进行引用:

console.log(object1.name)         // Arto Hellas is printed
const fieldName = 'age' 
console.log(object1[fieldName])    // 35 is printed

你也可以使用句点符号或括号来动态地往对象中添加属性:

object1.address = 'Helsinki'
object1['secret number'] = 12341

JavaScript 中的对象也可以包含方法。从 ES6版本开始,增加了类语法,这在某些情况下有助于构造面向对象的类。

Functions

箭头函数是几年前随 ES6 一起添加到 Javascript 中。在此之前,定义函数的唯一方法是使用关键字function
有两种方法可定义函数function,一种是在函数声明中给一个名字。

function product(a, b) {
  return a * b
}

const result = product(2, 6)
// result is now 12

另一种定义函数的方法是使用函数表达式。 在这种情况下,没有必要为函数命名,定义可以放在代码的其它位置:

const average = function(a, b) {
  return (a + b) / 2
}

const result = average(2, 5)
// result is now 3.5

对象方法以及“ this”关键字

const arto = {
  name: 'Arto Hellas',
  greet: function() {
    console.log('hello, my name is', this.name)
  },
  doAddition: function(a, b) {
    console.log(a + b)
  },
}

arto.doAddition(1, 4)        // 5 is printed
const referenceToAddition = arto.doAddition
referenceToAddition(10, 15)   // 25 is printed

arto.greet()       // hello, my name is Arto Hellas gets printed
const referenceToGreet = arto.greet
referenceToGreet() // prints only hello, my name is

在 JavaScript 中,this的值是根据 方法如何调用 来定义的。 当通过引用调用该方法时,this 的值就变成了所谓的全局对象,而最终结果往往不是软件开发人员设想的那样。
一种消除this所引起的问题的一种方法就是,利用setTimeout方法,让arto对象1秒钟后调用greet。

setTimeout(arto.greet, 1000)
//hello, my name is

有几种机制可以保留这种原始的 this 。 其中一个是使用bind方法:

setTimeout(arto.greet.bind(arto), 1000)
//hello, my name is Arto Hellas

命令 arto.greet.bind(arto) 创建了一个新函数,它将 this 绑定指向到了 Arto,这与方法的调用位置和方式无关。

延伸阅读:
setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致,这些代码中包含的this关键字会指向 window(或全局)对象,这和所期望的this的值是不一样的。参考:window.setTimeout

Classes

在下面的代码中,我们定义了一个名为 Person 的“类”和一个 Person 对象。

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  greet() {
    console.log('hello, my name is', this.name)
  }
}

const adam = new Person('Adam Ondra', 35)
adam.greet()

adam对象的类型实际上是Object,因为JavaScript实质上只定义了Boolean,Null,Undefined,Number,String,Symbol,以及Object几种类型。

Javascript materials

三.组件状态,事件处理

Component helper functions【组件辅助函数】

java中,在一个方法中定义另一个方法是不可能的,但在 JavaScript中,在函数中定义函数是一种常规操作。

Destructuring【解构】

解构使变量的赋值变得更加容易,因为我们可以使用它来提取和收集对象属性的值,将其提取到单独的变量中:

const Hello = (props) => {
  const { name, age } = props
  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>Hello {name}, you are {age} years old</p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

我们可以进一步解构:

const Hello = ({ name, age }) => {
  const bornYear = () => new Date().getFullYear() - age

  return (
    <div>
      <p>
        Hello {name}, you are {age} years old
      </p>
      <p>So you were probably born in {bornYear()}</p>
    </div>
  )
}

Page re-rendering【页面重渲染】

const App = (props) => {
  const { counter } = props
  return (
    <div>{counter}</div>
  )
}

let counter = 1

const refresh = () => {
  ReactDOM.render(<App counter={counter} />, 
  document.getElementById('root'))
}

setInterval(() => {
  refresh()
  counter += 1
}, 1000)

我们可以通过使用 setInterval,通过每隔一秒来重渲染一次并让计数器+1,来实现这个有趣的功能。

Stateful component【有状态组件】

通过 React 的state hook向应用的App组件中添加状态。

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const App = (props) => {
  const [counter, setCounter] = useState(0)

  setTimeout(
    () => setCounter(counter + 1),
    1000
  )

  return (
    <div>{counter}</div>
  )
}

ReactDOM.render(
  <App />, 
  document.getElementById('root')
)

在第一行中,应用导入了 useState-函数:
import React, { useState } from 'react'
当状态修改函数—— setCounter 被调用时, React 重新渲染了这个组件,这意味着组件函数的函数体被重新执行。
参考:解构赋值

Event handling【事件处理】

<button onClick={() => setCounter(counter + 1)}> 
  plus
</button>

事件处理程序应该是一个函数或一个函数引用
通常在 JSX-模板 中定义事件处理程序并不是一个好的实践。
让我们将事件处理程序分离成单独的函数:

const App = (props) => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  
  const setToZero = () => setCounter(0)

  return (
    <div>
      <div>{counter}</div>
      <button onClick={increaseByOne}>
        plus
      </button>
      <button onClick={setToZero}>
        zero
      </button>
    </div>
  )
}

将状态传递给子组件

让我们首先实现一个Display 组件,它负责显示计数器的值。

在 React 中的一个最佳实践是将状态提升,提升到组件层次结构中足够高的位置

因此,让我们将应用的状态放在App组件中,并通过props将其传递给Display 组件:

const Display = (props) => {
  return (
    <div>{props.counter}</div>
  )
}

接下来,让我们为应用的按钮制作一个Button 组件。 我们必须通过组件的props传递事件处理程序以及按钮的标题:

const Button = (props) => {
  return (
    <button onClick={props.handleClick}>
      {props.text}
    </button>
  )
}

我们的App 组件现在看起来像这样:

const App = (props) => {
  const [ counter, setCounter ] = useState(0)

  const increaseByOne = () => setCounter(counter + 1)
  const decreaseByOne = () => setCounter(counter - 1)
  const setToZero = () => setCounter(0)

  return (
    <div>
      <Display counter={counter}/>
      <Button
        handleClick={increaseByOne}
        text='plus'
      />
      <Button
        handleClick={setToZero}
        text='zero'
      />     
      <Button
        handleClick={decreaseByOne}
        text='minus'
      />           
    </div>
  )
}

状态的改变会导致重新渲染

Refactoring the components【重构组件】

我们可以使用更紧凑的箭头函数来定义方法:

const Display = ({ counter }) => <div>{counter}</div>

四.深入React 应用调试

Complex state【复杂状态】

const App = (props) => {
  const [clicks, setClicks] = useState({
    left: 0, right: 0
  })

  const handleLeftClick = () => {
    const newClicks = { 
      left: clicks.left + 1, 
      right: clicks.right 
    }
    setClicks(newClicks)
  }

  const handleRightClick = () => {
    const newClicks = { 
      left: clicks.left, 
      right: clicks.right + 1 
    }
    setClicks(newClicks)
  }

  return (
    <div>
      <div>
        {clicks.left}
        <button onClick={handleLeftClick}>left</button>
        <button onClick={handleRightClick}>right</button>
        {clicks.right}
      </div>
    </div>
  )
}

可以通过使用对象的展开语法更加整洁地定义新的状态对象:

const handleLeftClick = () => {
  const newClicks = { 
    ...clicks, 
    left: clicks.left + 1 
  }
  setClicks(newClicks)
}

const handleRightClick = () => {
  const newClicks = { 
    ...clicks, 
    right: clicks.right + 1 
  }
  setClicks(newClicks)
}

{ ...clicks }创建了一个新对象,该对象是具有 clicks 对象的所有属性的副本。 当我们向对象添加新属性时,例如{ ...clicks, right: 1 },新对象中right属性的值将为1。
将对象分配给事件处理中的变量是没有必要的,我们可以将函数简化为如下形式:

const handleLeftClick = () =>
  setClicks({ ...clicks, left: clicks.left + 1 })

const handleRightClick = () =>
  setClicks({ ...clicks, right: clicks.right + 1 })

为什么我们不直接更新状态,像这样:

const handleLeftClick = () => {
  clicks.left++
  setClicks(clicks)
}

这违反了React 中状态不可直接修改的原则,因为它会导致意想不到的副作用。 必须始终通过将状态设置为新对象来更改状态。

Handling arrays【处理数组】

向数组中添加新元素是通过concat方法完成的,该方法不改变现有数组,而是返回数组 新副本,并将元素添加到该数组中。

调试React应用

注意:当您使用 console.log 进行调试时,不要使用“加号”,像类似于 java 的方式组合对象。 即不要写:
console.log('props value is' + props)
而应用逗号分隔需要打印到控制台的内容:
console.log('props value is', props)

可以在 Chrome 开发者控制台的debugger 中暂停应用代码的执行,只需在代码中的任何地方写入命令debugger即可。

通过在Sources 选项卡中添加断点,您还可以在不使用 debugger 命令的情况下访问调试器。
强烈建议在 Chrome 中添加React developer tools扩展

Rules of Hooks【Hook的规则】

hook 只能从定义 React component 的函数体内部调用。

Do Not Define Components Within Components

不要在组件中定义组件

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×