跳到内容

d

深入React 应用调试

复杂状态

在我们之前的例子中,应用的状态比较简单,只包括一个整数。如果我们的应用需要更复杂的状态呢?

在大多数情况下,最简单但也是最好的方法是通过多次使用useState函数来创建专门的状态“片段”。

在下面的代码中,我们为应用创建了两个名为leftright的状态片段,它们的初始值都是0:

const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)

  return (
    <div>
      {left}
      <button onClick={() => setLeft(left + 1)}>
        left
      </button>
      <button onClick={() => setRight(right + 1)}>
        right
      </button>
      {right}
    </div>
  )
}

该组件可以通过调用函数setLeftsetRight来更新这两个状态片段。

组件的状态或状态的一部分可以是任何类型。我们可以通过将leftright按钮的点击次数保存在单个对象中来实现同样的功能:

{
  left: 0,
  right: 0
}

在这种情况下,应用将如下所示:

const App = () => {
  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>
      {clicks.left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {clicks.right}
    </div>
  )
}

现在这个组件只有一个状态,事件处理函数必须处理整个应用的状态

事件处理函数看起来有点乱。当左键被点击时,以下函数被调用:

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

应用的新状态被设为:

{
  left: clicks.left + 1,
  right: clicks.right
}

现在left属性的新值等同于之前状态的left的值+ 1,而right属性的新值等同于之前状态right属性的值。

我们可以通过使用2018年夏添加到语言规范中的对象传播语法来更整齐地定义新的状态对象:

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,新对象中的right属性值将是1。

在上面的例子中:

{ ...clicks, right: clicks.right + 1 }

创建一个clicks对象的副本,其中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中是禁止直接改变状态的,因为这可能导致意想不到的副作用。改变状态必须始终通过将状态设置为一个新的对象来完成。如果前一个状态对象的属性没有改变,那么只需要复制它们。这可以通过将这些属性复制到一个新的对象中,并将其设置为新的状态来完成。

对这个应用来说,将所有的状态存储在单个状态对象中是一个糟糕的选择;没有明显的好处,还会使应用变得更复杂。在这种情况下,将点击计数器存储在专门的状态片段中是一个更合适的选择。

有些情况下,将一段应用的状态存储在一个更复杂的数据结构中会有好处。React的官方文档包含了一些关于这个主题的有用指导。

处理数组

让我们为我们的应用添加一个数组allClicks的状态片段来记录应用中发生的每一次点击。

const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])
  const handleLeftClick = () => {    setAll(allClicks.concat('L'))    setLeft(left + 1)  }
  const handleRightClick = () => {    setAll(allClicks.concat('R'))    setRight(right + 1)  }
  return (
    <div>
      {left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {right}
      <p>{allClicks.join(' ')}</p>    </div>
  )
}

每一次点击都被存储在一个专门的状态片段allClicks中。allClicks一开始是一个空数组:

const [allClicks, setAll] = useState([])

left按钮被点击时,我们将字母L添加到allClicks数组中:

const handleLeftClick = () => {
  setAll(allClicks.concat('L'))
  setLeft(left + 1)
}

存储在allClicks中的状态片段现在被设为了一个包含之前状态数组的所有项目加上字母L的新数组。将新项添加到数组中是通过concat方法完成的,该方法并不改变当前的数组,而是返回一个添加了新项的数组的新副本

之前提到过,在JavaScript中也可以用push方法向数组中添加项。如果我们通过把新项“push”到allClicks数组中,然后更新状态来添加项,这个应用看起来仍然可以运行。

const handleLeftClick = () => {
  allClicks.push('L')
  setAll(allClicks)
  setLeft(left + 1)
}

然而,不要这样做。之前提到过,像allClicks这样的React组件的状态是不能直接更改的。即使改变状态在某些情况下看起来是有效的,它也会导致一些很难调试的问题。

让我们仔细看一下点击的情况是如何被渲染到页面上的:

const App = () => {
  // ...

  return (
    <div>
      {left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {right}
      <p>{allClicks.join(' ')}</p>    </div>
  )
}

我们在allClicks数组上调用join方法,将所有项连接成单个字符串,并用函数参数传递的字符串(在这个的例子中是一个空格)分开。

状态的更新是异步的

让我们给我们的应用添加一个记录按钮点击总次数的状态total。每次点击按钮的时候,都更新total的值。

const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])
  const [total, setTotal] = useState(0)
  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    setLeft(left + 1)
    setTotal(left + right)  }

  const handleRightClick = () => {
    setAll(allClicks.concat('R'))
    setRight(right + 1)
    setTotal(left + right)  }

  return (
    <div>
      {left}
      <button onClick={handleLeftClick}>left</button>
      <button onClick={handleRightClick}>right</button>
      {right}
      <p>{allClicks.join(' ')}</p>
      <p>total {total}</p>    </div>
  )
}

这样写不大对:

fullstack content

出于什么原因,按钮点击次数的总数总是比实际点击的次数少一次。

让我们向事件处理函数里添加一些console.log语句。

const App = () => {
  // ...
  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    console.log('left before', left)    setLeft(left + 1)
    console.log('left after', left)    setTotal(left + right) 
  }

  // ...
}

控制台揭示了问题

fullstack content

即使已经调用setLeft(left + 1)来更新left的值,left的值还是旧的,并没有更新。结果,尝试计算按钮点击总次数的结果总是比实际值要小:

setTotal(left + right) 

原因在于在React中,状态的更新是异步进行的,也就是说,状态的更新并非立即生效,而是在组件再次渲染前的“某一刻”进行的。

我们可以这样修代码:

const App = () => {
  // ...
  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    const updatedLeft = left + 1
    setLeft(updatedLeft)
    setTotal(updatedLeft + right) 
  }

  // ...
}

现在点击按钮的次数和实际按左侧按钮的次数一样了。

我们对右侧按钮也用同样的方式处理异步更新:

const App = () => {
  // ...
  const handleRightClick = () => {
    setAll(allClicks.concat('R'));
    const updatedRight = right + 1;
    setRight(updatedRight);
    setTotal(left + updatedRight);
  };

  // ...
}

条件渲染

让我们修改我们的应用,让点击历史的渲染由一个新的History组件来处理。

const History = (props) => {  if (props.allClicks.length === 0) {    return (      <div>        the app is used by pressing the buttons      </div>    )  }  return (    <div>      button press history: {props.allClicks.join(' ')}    </div>  )}
const App = () => {
  // ...

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

现在,该组件的行为取决于是否有任何按钮被点击过。如果没有,意味着allClicks数组是空的,那么组件会渲染一个带有使用说明的div元素:

<div>the app is used by pressing the buttons</div>

而在其他所有情况下,该组件会渲染点击历史:

<div>
  button press history: {props.allClicks.join(' ')}
</div>

History组件根据应用的状态渲染完全不同的React元素。这被称为条件渲染

React还提供了许多其他的方法来进行条件渲染。我们将在第2章节中仔细研究。

让我们对我们的应用做最后一次修改,使用我们先前定义的Button组件重构它:

const History = (props) => {
  if (props.allClicks.length === 0) {
    return (
      <div>
        the app is used by pressing the buttons
      </div>
    )
  }

  return (
    <div>
      button press history: {props.allClicks.join(' ')}
    </div>
  )
}

const Button = ({ onClick, text }) => <button onClick={onClick}>{text}</button>
const App = () => {
  const [left, setLeft] = useState(0)
  const [right, setRight] = useState(0)
  const [allClicks, setAll] = useState([])

  const handleLeftClick = () => {
    setAll(allClicks.concat('L'))
    setLeft(left + 1)
  }

  const handleRightClick = () => {
    setAll(allClicks.concat('R'))
    setRight(right + 1)
  }

  return (
    <div>
      {left}
      <Button onClick={handleLeftClick} text='left' />      <Button onClick={handleRightClick} text='right' />      {right}
      <History allClicks={allClicks} />
    </div>
  )
}

旧React

在本课程中,我们使用状态Hook来向我们的React组件添加状态,这是较新版本React的一部分,从16.8.0起才可以使用。在增加Hook之前,无法向函数式组件添加状态。需要状态的组件必须被定义为类式组件,使用JavaScript类的语法。

在这个课程中,我们做了一个略显激进的决定,从第一天开始就只使用Hook,以确保我们学习React的当前和未来形式。即使函数式组件是React的未来,学习类的语法仍然很重要,因为有数十亿行的React遗留代码,你有一天可能会维护它们。这同样适用于你在互联网上意外发现的React的文档和例子。

我们将在后面的课程进一步学习关于React的类式组件。

调试React应用

典型开发者的大部分时间都花在调试和阅读现有的代码上。我们确实时不时会写一两行新代码,但我们的大部分时间都花在试图弄清楚为什么某段代码运行不了,或者某段代码是如何运行的。因此,良好的调试实践和工具是非常重要的。

幸运的是,在调试方面,React是一个对开发者极其友好的库。

在我们继续之前,让我们提醒自己网络开发中最重要的规矩之一。

web开发的第一规矩

始终保持浏览器的开发者控制台是打开的

特别是控制台标签页应该一直打开,除非有特别的原因要查看其他标签页。

始终、同时打开你的代码和网页。

如果当你的代码无法编译,你的浏览器像圣诞树一样亮起来的时候:

fullstack content

不要写更多的代码,而是要立即找到并解决这个问题。在编程的历史上,还没有出现过在编写了大量的额外代码之后,无法编译的代码会奇迹般地开始运行的事情。我非常怀疑在这个课程中是否会发生这样的事情。

传统的、基于打印的调试总是一个好主意。如果组件

const Button = ({ onClick, text }) => <button onClick={onClick}>{text}</button>

不能按预期运行,开始将其变量打印到控制台是很有用的。为了有效地做到这一点,我们必须将我们的函数转化为不太紧凑的形式,并接收整个props对象,而不是立即解构:

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

这将立即显示出,例如,在使用该组件时,其中一个属性被拼错了。

注意 当你使用console.log进行调试时,不要像Java那样用加号运算符组合objects

console.log('props value is ' + props)

如果这样做的话,你最终会得到一个信息量很小日志信息:

props value is [Object object]

而是用逗号把你想记录到控制台的东西分开:

console.log('props value is', props)

这样,用逗号分隔的项目将在浏览器控制台中全部可用,以供进一步检查。

将输出记录到控制台决不是调试我们应用的唯一方法。你可以在Chrome开发者控制台的调试器中暂停执行应用代码,方法是在代码的任何地方写下debugger命令。

一旦执行到debugger命令,就会暂停执行。

fullstack content

通过进入Console标签页,很容易检查变量当前的状态。

fullstack content

一旦发现问题的原因,你就可以删除debugger命令并刷新页面。

调试器也使我们能够通过Sources标签页右侧的控件来逐行执行我们的代码。

你也可以不用debugger命令,而通过在Sources标签页中添加断点来访问调试器。检查组件的变量值可以在Scope部分完成:

fullstack content

强烈建议在Chrome浏览器中添加React开发者工具扩展。它在开发者工具中增加了一个新的Components标签页,可以用来检查应用中不同的React元素,以及它们的状态和props。

fullstack content

App组件的状态是这样定义的:

const [left, setLeft] = useState(0)
const [right, setRight] = useState(0)
const [allClicks, setAll] = useState([])

Dev tools按照定义的顺序显示Hook的状态。

fullstack content

第一个State包含left状态的值,第二个包含right状态的值,最后一个包含allClicks状态的值。

你还可以在Chrome中学习调试JavaScript,比如通过Chrome开发者工具指导视频

Hook的规则

为了确保我们的应用正确使用基于Hook的状态函数,必须遵循一些限制和规则

不能在循环、条件表达式或任何不是定义组件的函数的地方调用useState函数(以及课程后面介绍的useEffect函数)。这样做是为了确保Hook总是以相同的顺序被调用,如果不这样做的话,应用将表现得不稳定。

简而言之,只能在定义React组件的函数体内调用Hook:

const App = () => {
  // these are ok
  const [age, setAge] = useState(0)
  const [name, setName] = useState('Juha Tauriainen')

  if ( age > 10 ) {
    // this does not work!
    const [foobar, setFoobar] = useState(null)
  }

  for ( let i = 0; i < age; i++ ) {
    // also this is not good
    const [rightWay, setRightWay] = useState(false)
  }

  const notGood = () => {
    // and this is also illegal
    const [x, setX] = useState(-1000)
  }

  return (
    //...
  )
}

重提事件处理函数

前几期的课程显示事件处理是一个难点。

因此,我们将重提这个话题。

让我们假设我们在使用如下组件App开发这个简单的应用:

const App = () => {
  const [value, setValue] = useState(10)

  return (
    <div>
      {value}
      <button>reset to zero</button>
    </div>
  )
}

我们希望点击按钮能重置存储在value变量中的状态。

为了使按钮对点击事件作出反应,我们必须给它添加一个事件处理函数

事件处理函数必须始终是一个函数或对一个函数的引用。如果事件处理函数被设置为任何其他类型的变量,按钮将无法运行。

如果我们将事件处理函数定义为一个字符串:

<button onClick="crap...">button</button>

React会在控制台警告我们:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `string` type.
    in button (at index.js:20)
    in div (at index.js:18)
    in App (at index.js:27)

下面的尝试也不会成功:

<button onClick={value + 1}>button</button>

我们试图将事件处理函数设置为value + 1,也就是返回操作的结果。React会在控制台中善意地警告我们:

index.js:2178 Warning: Expected `onClick` listener to be a function, instead got a value of `number` type.

这种尝试也不会成功:

<button onClick={value = 0}>button</button>

事件处理函数不是一个函数,而是一个变量赋值语句,React将再次向控制台发出警告。这种尝试也是错的,因为我们决不能在React中直接改变状态。

那这样呢:

<button onClick={console.log('clicked the button')}>
  button
</button>

当组件被渲染时,信息被打印到控制台一次,但当我们点击按钮时,什么也没有发生。为什么即使当我们的事件处理函数包含一个函数console.log时,也不能运行呢?

这里的问题是我们的事件处理函数被定义为一个函数调用,这意味着事件处理函数实际上是函数的返回值,console.log的返回值是undefined

当组件被渲染时,console.log函数调用被执行,也因此会打印一次信息到控制台。

下面的尝试也是错的:

<button onClick={setValue(0)}>button</button>

我们再次尝试将一个函数调用设置为事件处理函数。这并不奏效。这个尝试也导致了另一个问题。当组件被渲染时,函数setValue(0)被执行,这反过来导致组件被重新渲染。重新渲染又会再次调用setValue(0),从而导致无限的递归。

当按钮被点击时,执行一个特定的函数调用可以这样完成:

<button onClick={() => console.log('clicked the button')}>
  button
</button>

现在事件处理函数是一个用箭头函数语法() => console.log('clicked the button')定义的函数。当组件被渲染时,没有函数被调用,只有箭头函数的引用被设为事件处理函数。只有当按钮被点击时才会调用该函数。

我们可以用同样的方法在我们的应用中实现重置状态:

<button onClick={() => setValue(0)}>button</button>

现在事件处理函数是() => setValue(0)

直接在按钮的属性中定义事件处理函数,并不是最好的做法。

你经常会看到事件处理函数被定义在一个单独的地方。在我们应用的以下版本中,我们定义了一个函数,然后将其赋值给组件函数主体中的handleClick变量:

const App = () => {
  const [value, setValue] = useState(10)

  const handleClick = () =>
    console.log('clicked the button')

  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    </div>
  )
}

handleClick变量现在是一个函数定义的引用。我们将这个引用传递给按钮的onClick属性:

<button onClick={handleClick}>button</button>

当然,我们的事件处理函数可以由多个命令组成。在这种情况下,我们对箭头函数使用较长的大括号语法:

const App = () => {
  const [value, setValue] = useState(10)

  const handleClick = () => {    console.log('clicked the button')    setValue(0)  }
  return (
    <div>
      {value}
      <button onClick={handleClick}>button</button>
    </div>
  )
}

返回函数的函数

另一种定义事件处理函数的方法是使用返回函数的函数

在本课程的任何练习中,你可能都不需要使用返回函数的函数。 如果这个话题看起来特别令人困惑,你可以暂时跳过这一节,以后再来讨论。

让我们将我们的代码修改如下:

const App = () => {
  const [value, setValue] = useState(10)

  const hello = () => {    const handler = () => console.log('hello world')    return handler  }
  return (
    <div>
      {value}
      <button onClick={hello()}>button</button>
    </div>
  )
}

尽管代码看起来很复杂,但它能正确运行。

事件处理函数现在被设置为一个函数调用:

<button onClick={hello()}>button</button>

先前我们说过,一个事件处理函数不能是对一个函数的调用,它必须是一个函数或对一个函数的引用。那为什么在这种情况下,函数调用就能正确运行了呢?

当组件被渲染时,下面的函数被执行:

const hello = () => {
  const handler = () => console.log('hello world')

  return handler
}

该函数的返回值是另一个函数,这另一个函数被赋值给handler变量。

当React渲染这一行时:

<button onClick={hello()}>button</button>

它把hello()的返回值赋值给onClick属性。本质上,这一行被转化为:

<button onClick={() => console.log('hello world')}>
  button
</button>

由于hello函数返回一个函数,事件处理函数现在是一个函数。

这个概念的意义在哪儿?

让我们稍微改变一下代码:

const App = () => {
  const [value, setValue] = useState(10)

  const hello = (who) => {    const handler = () => {      console.log('hello', who)    }    return handler  }
  return (
    <div>
      {value}
      <button onClick={hello('world')}>button</button>      <button onClick={hello('react')}>button</button>      <button onClick={hello('function')}>button</button>    </div>
  )
}

现在这个应用有三个按钮,其事件处理函数由接受一个参数的hello函数定义。

第一个按钮的定义是

<button onClick={hello('world')}>button</button>

事件处理函数是通过执行函数调用hello('world')创建的。该函数调用返回函数:

() => {
  console.log('hello', 'world')
}

第二个按钮的定义是:

<button onClick={hello('react')}>button</button>

创建事件处理函数的函数调用hello('react')返回:

() => {
  console.log('hello', 'react')
}

两个按钮都得到了自己的个性化的事件处理函数。

返回的函数的函数可以用在定义通过参数个性化的通用功能。创建事件处理函数的hello函数可以被想象成一个产生向用户问好的个性化的事件处理函数的工厂。

我们目前的定义略显冗长:

const hello = (who) => {
  const handler = () => {
    console.log('hello', who)
  }

  return handler
}

让我们去掉辅助变量,直接返回创建的函数:

const hello = (who) => {
  return () => {
    console.log('hello', who)
  }
}

由于我们的hello函数由单个返回命令组成,我们可以省略大括号,并使用箭头函数的更紧凑的语法:

const hello = (who) =>
  () => {
    console.log('hello', who)
  }

最后,让我们把所有的箭头写在同一行:

const hello = (who) => () => {
  console.log('hello', who)
}

我们可以使用同样的技巧来定义事件处理函数,将组件的状态设置为一个给定的值。让我们对我们的代码做如下修改:

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = (newValue) => () => {    console.log('value now', newValue)  // print the new value to console    setValue(newValue)  }
  return (
    <div>
      {value}
      <button onClick={setToValue(1000)}>thousand</button>      <button onClick={setToValue(0)}>reset</button>      <button onClick={setToValue(value + 1)}>increment</button>    </div>
  )
}

当组件被渲染时,thousand按钮被创建:

<button onClick={setToValue(1000)}>thousand</button>

事件处理函数被设置为setToValue(1000)的返回值,也就是这个函数:

() => {
  console.log('value now', 1000)
  setValue(1000)
}

“加1”按钮的声明如下:

<button onClick={setToValue(value + 1)}>increment</button>

事件处理函数由函数调用setToValue(value + 1)创建,该函数接收状态变量value的当前值加一作为其参数。如果value的值是10,那么创建的事件处理函数将是这个函数:

() => {
  console.log('value now', 11)
  setValue(11)
}

实现这个功能不需要使用返回函数的函数。让我们把负责更新状态的setToValue函数,改回普通的函数:

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = (newValue) => {
    console.log('value now', newValue)
    setValue(newValue)
  }

  return (
    <div>
      {value}
      <button onClick={() => setToValue(1000)}>
        thousand
      </button>
      <button onClick={() => setToValue(0)}>
        reset
      </button>
      <button onClick={() => setToValue(value + 1)}>
        increment
      </button>
    </div>
  )
}

我们现在可以把事件处理函数定义为一个函数,该函数用一个适当的参数调用setToValue函数。重置应用状态的事件处理函数将是:

<button onClick={() => setToValue(0)}>reset</button>

在所介绍的两种定义事件处理函数的方式中,选择哪一种主要取决于自己的品味。

向子组件传递事件处理函数

让我们把按钮提取到它自己的组件中:

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

这个组件从onClick prop中获得事件处理函数,从text prop中获得按钮的文本。让我们使用这个新组件:

const App = (props) => {
  // ...
  return (
    <div>
      {value}
      <Button onClick={() => setToValue(1000)} text="thousand" />      <Button onClick={() => setToValue(0)} text="reset" />      <Button onClick={() => setToValue(value + 1)} text="increment" />    </div>
  )
}

使用Button组件很简单,尽管我们必须确保在向组件传递prop时使用正确的属性名称。

fullstack content

不要在组件中定义组件

让我们开始把显示应用值的功能放到它自己的Display组件中。

我们将通过在App组件内定义一个新的组件来改变应用。

// This is the right place to define a component
const Button = (props) => (
  <button onClick={props.onClick}>
    {props.text}
  </button>
)

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    console.log('value now', newValue)
    setValue(newValue)
  }

  // Do not define components inside another component
  const Display = props => <div>{props.value}</div>
  return (
    <div>
      <Display value={value} />      <Button onClick={() => setToValue(1000)} text="thousand" />
      <Button onClick={() => setToValue(0)} text="reset" />
      <Button onClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

应用似乎仍在运行,但不要这样实现组件!永远不要在其他组件中定义组件。这种方法没有任何好处,而且会导致许多不愉快的问题。最大的问题是由于React在每次渲染时都将定义在另一个组件内的组件视为一个新的组件。这使得React无法优化该组件。

让我们把Display组件函数移到它的正确位置,也就是App组件函数之外:

const Display = props => <div>{props.value}</div>

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

const App = () => {
  const [value, setValue] = useState(10)

  const setToValue = newValue => {
    console.log('value now', newValue)
    setValue(newValue)
  }

  return (
    <div>
      <Display value={value} />
      <Button onClick={() => setToValue(1000)} text="thousand" />
      <Button onClick={() => setToValue(0)} text="reset" />
      <Button onClick={() => setToValue(value + 1)} text="increment" />
    </div>
  )
}

阅读资料

互联网上有很多React相关的资料。然而,因为我们使用的是React的新风格,所以对于我们来说,网上发现的大部分材料已经过时。

你可能会发现下面的链接很有用:

  • React官方文档值得在某些时候查看,尽管它的大部分内容在课程的后期才会变得相关。另外,基于类的组件有关的一切都与我们无关;

  • Egghead.io上的一些课程,比如Start learning React的质量很高,最近更新的Beginner's Guide to React也比较好;两个课程介绍的概念也将在本课程的后面介绍。注意前者使用类式组件,后者使用新的函数式组件。

web开发者誓言

编程不易,因此我们要通过一切方法让它变得容易

  • 我会始终打开我的浏览器开发者控制台

  • 我小步前进

  • 我会写大量的console.log语句来确保我理解代码是怎么运行的,并借此准确找到问题

  • 如果我的代码出问题了,我不会写更多的代码。而是删除代码直到它能运行,或者直接回到之前代码能运行的状态

  • 当我在课程的Discord群或者其他地方寻求帮助时,我会准确表达我的问题,点了解如何寻求帮助。

利用大语言模型

大语言模型,比如ChatGPTClaudeGitHub Copilot非常有助于软件开发。

我个人常用的是GitHub Copilot,它现在原生集成进Visual Studio Code中

提醒一下,如果你是在校大学生的话,你可以通过GitHub Student Developer Pack来免费试用Copilot pro。

Copilot在许多场合下都有用。你可以在打开的文件里用文本描述你想要的功能,然后让Copilot编写代码:

fullstack content

如果代码看起来是对的,Copilot会将它加到文件里:

fullstack content

在我们的例子中,Copilot只是创建了一个按钮,事件处理函数handleResetClick没有定义。

事件处理函数也可以由Copilot生成。写完函数的第一行后,Copilot会提供生成的功能:

fullstack content

在Copilot的聊天窗口,还可以让Copilot解释选中的代码区域。

fullstack content

Copilot在处理错误上也很有用,将错误信息复制到Copilot的聊天窗口,你就能得到对问题的解释还有建议的修复方式:

fullstack content

Copilot的聊天窗口还能为我们创建更多功能

fullstack content

Copilot以及其他大语言模型提供的提示的有用程度各不相同。但也许大语言模型最大的问题是幻觉,它们有时会生成看起来完全有说服力的答案,但这些答案完全是错的。当然在编程中,幻觉代码只要运行不了就能查出来。更麻烦的是大语言模型生成的代码有时看起来能运行,但是隐藏了更难以发现的问题,或者诸如缺乏安全性的问题。

在软件开发中使用大语言模型还有另一个问题,语言模型很难“理解”大型项目,也难以生成比如需要修改多个文件的功能。目前大语言模型也无法很好地泛化代码,比如如果要实现的新功能只需要稍微修改项目中已有的函数或组件,语言模型不会利用这些已有的内容。这可能导致代码库变得越来越冗余,因为语言模型会生成大量重复代码,详见这里

使用语言模型时,责任始终在程序员自己。

语言模型的快速发展让编程学习者面临挑战:既然几乎所有内容都能从语言模型中获得现成的,是否还值得,以及是否还有必要深入学习编程呢?

此时,值得回忆一下Brian Kernighan,《C程序设计语言》作者之一的古老智慧:

fullstack content

每个人都知道,调试代码比从头写代码难两倍。所以如果你在编写时就足够聪明,那你将来又怎么调试呢?

换句话说,既然调试比编程难两倍,就不应该写那些自己都勉强能看懂的代码。如果编程都外包给了大语言模型,要调试的代码开发者自己都不理解,那又怎么可能调试呢?

目前为止,大语言模型和AI的发展还没有达到自给自足的阶段,最难的问题还是要靠人类来解决。因此,即使是初学者,也必须学好编程,以备不时之需。也许语言模型的发展,反而需要我们学得更深。人工智能可以处理简单的事情,但AI造成的最复杂的烂摊子还是要人类来收拾。GitHub Copilot这个名字非常贴切,它是副驾驶,只是在飞机上帮助主驾驶的驾驶员。程序员仍然是主驾驶,是机长,是扛起最终责任的人。

也许,在这门课程中默认关闭Copilot,只有在真正紧急情况时才依赖它,会对你更有利。