Web全栈开发学习笔记—Part1 React入门—d.深入React 应用调试

目录

Complex state

Handling arrays

Conditional rendering

Debugging React applications

Rules of Hooks

Event Handling Revisited

Function that returns a function

Passing Event Handlers to Child Components

Do Not Define Components Within Components

Complex state

【复杂状态】

之前的示例,应用状态很简单,因为它仅由单个整数组成。 如果需要一个更复杂的状态,最简单和最好的方法是多次使用 useState 函数来创建单独的状态“片段”。

在下面的代码中,我们为应用创建了两个名为 left 和 right 的初始值为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>
  )
}

组件获得对 setLeft 和 setRight 函数的访问权,可以使用这两个函数更新这两个状态。

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

{
  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 属性的值相同。

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

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。

在上面的例子中,下面代码:

{ ...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 中状态不可直接修改的原则,因为它会导致意想不到的副作用。 必须始终通过将状态设置为新对象来更改状态。 如果之前的状态没有变化,属性仅仅需要简单地复制,就是通过将这些属性复制到新的对象中,并将其设置为新状态。

对于这个特定的应用来说,将所有状态存储在单个状态对象中是一个糟糕的选择; 没有明显的好处,还会导致产生的应用要复杂得多。 在这种情况下,将点击计数器存储到单独的状态块中是一个更合适的选择。

Handling arrays

【处理数组】

向应用添加一个状态,该状态包含一个数组 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 的单独的状态单元中,这个状态单元被初始化为一个空数组:

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)
}

但是,不要这样做。 如前所述,React 组件(如 allClicks )的状态不能直接更改。 即使改变状态在某些情况下可以工作,也可能导致很难调试的问题。

点击历史看看是如何渲染在页面上的:

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

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

我们为 allClicks 数组调用join方法,该数组将所有项目连接到一个字符串中,由作为函数参数传递的字符串分隔,这里字符串是一个空格。

Conditional rendering

【条件渲染】

修改应用,使得单击历史的渲染由一个新的 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 还提供了许多其他的方法来实现条件渲染。 

对应用进行最后一次修改,重构它,用上前面定义的 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>
  )
}

Debugging React applications

【调试React应用】

典型的开发大部分时间都花在调试和读取现有代码上。 时不时会写一两行新代码,但是大部分时间都花在试图弄明白为什么有些东西坏了,或者某些东西是如何工作的上面。 出于这个原因,良好的调试实践和工具非常重要。

在调试方面来说,React 对开发者是非常友好的。

The first rule of web development web开发第一原则

Keep the browser's developer console open at all times.

始终打开浏览器的开发控制台

The Console tab in particular should always be open, unless there is a specific reason to view another tab. 尤其是Console 选项卡应该始终处于打开状态,除非有特定的原因需要查看另一个选项卡。

保持代码和网页同时打开,一直同时打开。

如果代码编译失败,浏览器就会像圣诞树一样亮起来:

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 的方式组合对象。 即不要写:

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

而应用逗号分隔需要打印到控制台的内容:

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

如果使用类似于 java 的方式将一个字符串与一个对象结合起来,最终会得到一个相当无用的日志消息:

props value is [Object object]

而用逗号分隔的项目都可以在浏览器控制台中进行进一步检查。

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

一旦到达调试器命令执行的地方,执行就会暂停:

fullstack content

通过访问Console 选项卡,可以很容易地检查变量的当前状态:

fullstack content

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

debugger 还允许我们使用在Sources 选项卡右侧找到控件一行一行地执行代码。

通过在Sources 选项卡中添加断点,还可以在不使用 debugger 命令的情况下访问调试器。 检查组件变量的值可以在 Scope-部分 中完成:

fullstack content

强烈建议在 Chrome 中添加 React developer tools扩展。 它为开发工具增加了一个新的 Components 选项卡。新的开发者工具页可以用来检查不同的React 元素,以及它的属性和状态:

fullstack content fullstack content

App 组件的状态定义如下:

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

开发工具按照定义顺序显示hook的状态:

fullstack content

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

Rules of Hooks

【Hook的规则】

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

不能从循环、条件表达式或任何不是定义组件的函数的地方调用 useState (同样的还有 useEffect 函数)。 这样做是为了确保Hook总是以相同的顺序调用,如果不是这样,应用的行为就会不规则。

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

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 (
    //...
  )
}

Event Handling Revisited

【复习事件处理】

假设我们正在开发这个简单的应用:

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

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

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

希望单击按钮来重置存储在 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,它只返回操作的结果。 在控制台中会友好地警告我们:

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>

当组件渲染出来时,消息会被打印到控制台一次,但是当我们点击按钮时什么也没有发生。 

这里的问题是,事件处理被定义为function call,这意味着事件处理程序实际上被分配了函数返回的值,而 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>
  )
}

Function that returns a function

【返回函数的函数】

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

对代码进行如下修改:

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
}

函数的返回值 是分配给处理程序变量的另一个函数。

当 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')创建,函数 call 返回函数:

() => {
  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) => () => {
    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)的返回值,该返回值是如下函数:

() => {
  setValue(1000)
}

为 increase 按钮的代码行如下:

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

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

() => {
  setValue(11)
}

使用返回函数的函数不是实现此功能所必需的。 将负责更新状态的 setToValue 函数返回到一个普通函数:

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

  const setToValue = (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>

Passing Event Handlers to Child Components

【将事件处理传递给子组件】

将按钮提取到它自己的组件中:

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

该组件从 handleClick 属性获取事件处理函数,从text 属性获取按钮的文本。

使用Button 组件很简单,确保在向组件传递props时使用正确的属性名。

fullstack content

Do Not Define Components Within Components

【不要在组件中定义组件】

将应用的值显示到它自己的Display 组件中。

通过在App-组件中定义一个新组件来更改应用。

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

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

  const setToValue = newValue => {
    setValue(newValue)
  }

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

应用看起来仍然可以工作,但是 不要像这样实现组件!不要在其他组件内部定义组件。 这种方法最大的问题是,React 在每次渲染时,会将内部的组件当作一个新的组件。这回导致React 无法去优化组件。

Display 组件函数移动到正确的位置,这个位置在App 组件函数之外:

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

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

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

  const setToValue = newValue => {
    setValue(newValue)
  }

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

猜你喜欢

转载自blog.csdn.net/qq_39389123/article/details/111878792