React Hooks 介绍

什么是Hooks
  • Hook 是 React 16.8 的新增特性。
  • 它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
  • 如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其转化为 class,现在你可以在现有的函数组件中使用 Hook

解决的问题
  • 主要解决了函数组件没有状态的问题。
  • 在组件之间复用状态逻辑很难,可能要用到 render props 和 高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
  • 难以理解的 class,包括难以捉摸的 this

注意事项
  • 在每次渲染的时候,hooks 数量不能变。
  • 所以只能在函数最外层调用 Hook,不要在循环、条件判断、子函数中调用
  • 只能在 React 的函数组件中调用 Hook,不要在其它 JavaScript 函数中调用

useState

let [value, setValue] = useState(initialState)

  • useState 就是一个Hook 。
  • 通过在函数组件中调用它来给组件添加一些内部 state ,React 会在重复渲染时保留这个 state 。
  • useState 唯一的参数就是初始 state。
  • useState 会返回一对值:当前状态 更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。
  • state
    • initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。
    • 在初始渲染期间,返回的状态(state)与传入的第一个参数(initialState)值相同
  • 更新 state 的函数(setState)
    • 该函数用于更新 state。它接收一个新的 state 值,并将组件的一次重新渲染加入队列。
    • 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。此函数只在初始渲染时被调用。
    • 该函数类似 class 组件的 this.setState , 但是它不会把新的 state 和旧的 state 进行合并更新。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果

例子:计数器

Hooks - 图1

  1. function App(){
  2. let [number, setNumber] = React.useState(0);
  3. let [number2, setNumber2] = React.useState(0);
  4. return (
  5. <>
  6. {/* setState 参数为 值 */}
  7. <button onClick={() => setNumber(number+1)}>number:{number}</button>
  8. {/* setState参数为 函数 */}
  9. <button onClick={() => setNumber2(n => n+1)}>number2:{number2}</button>
  10. </>
  11. )
  12. }
  13. ReactDOM.render(<App />, document.getElementById('root'));

hooks更新的比较算法是 Object.is

  • 调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

Hooks - 图2

  1. import React, { Component } from 'react';
  2. import ReactDOM from 'react-dom';
  3. function App(){
  4. const [counter,setCounter] = React.useState({name:'计数器',number:0});
  5. const number = counter.number + 1;
  6. console.log('render Counter');
  7. return (
  8. <>
  9. <p>{counter.name} -- {counter.number}</p>
  10. <button onClick={() => setCounter( { ...counter, number } )}>会更新</button>
  11. <button onClick={() => setCounter( Object.assign(counter, {number} ))}>不会更新</button>
  12. </>
  13. )
  14. }
  15. ReactDOM.render(<App />, document.getElementById('root'));

hooks的同步思维

  • 同步才是 hooks 的思维方式,异步获取不到最新状态
  • 每次渲染都是一个独立的闭包
  • 每次渲染都有它自己的 Props and State
  • 每次渲染都有它自己的事件处理函数
  • 函数内部会“捕获”我点击按钮时候的状态。
  • 我们的组件函数每次渲染都会被调用,但是每一次调用中number值都是常量,并且它被赋予了当前渲染中的状态值
  • 在单次渲染的范围内,props和state始终保持不变
  • making-setinterval-declarative-with-react-hooks

sdre.gif

  1. import React, { Component } from 'react';
  2. import ReactDOM from 'react-dom';
  3. function App(){
  4. const [number, setNumber] = React.useState(0);
  5. function delayAddNumber(){
  6. setTimeout(()=>{
  7. //这个number取的是点击时候的number变量值,并不是最新的number值
  8. setNumber(number+1);
  9. //如果想获取最新的值,参数可以传递函数
  10. //setNumber(number => number+1);
  11. },3000);
  12. }
  13. return (
  14. <>
  15. <p>{number}</p>
  16. <button onClick={()=>setNumber(number+1)}>加1</button>
  17. <button onClick={delayAddNumber}>3秒后加1</button>
  18. </>
  19. )
  20. }
  21. ReactDOM.render(<App />, document.getElementById('root'));

hooks的同步思维-简单示例说明

rewasf.gif

  1. <body>
  2. <button onclick="window.add()">点击</button>
  3. <button onclick="window.delayAdd()">延迟3秒触发</button>
  4. <script>
  5. let hookStates = []; // 这是一个数组,里面存放着我们所有的 hook 状态
  6. let hookIndex = 0; // 当前 hook 的索引
  7. // hook调度更新函数
  8. let scheduleUpdate = () => {
  9. hookIndex = 0; //每次重新渲染,hook索引重置
  10. render();
  11. };
  12. function useState(initialState){
  13. //把老的值取出来,如果没有,就使用默认值
  14. hookStates[hookIndex] = hookStates[hookIndex] ||
  15. (typeof initialState === 'function' ? initialState() : initialState);
  16. // 因为 hookIndex 一直在变,
  17. // 给每一个 useState 的 hook 定死一个不变的索引。用于 setState时 更新该hook的数据
  18. let currentIndex = hookIndex;
  19. function setState(newState){
  20. if(typeof newState === 'function') newState = newState(hookStates[currentIndex]);
  21. hookStates[currentIndex] = newState;
  22. scheduleUpdate(); //当状态改变后要重新更新应用
  23. }
  24. return [hookStates[hookIndex++], setState]; //注意,先取值,再++,把 hook 索引指向下一个 useState。
  25. }
  26. // 每次渲染都是一个闭包
  27. // 每次渲染都有它自己的 state
  28. function render(){
  29. let [count, setCount] = useState(1);
  30. console.log('最新的count =>', count);
  31. window.add = () => setCount(count+1);
  32. window.delayAdd = () => setTimeout(() => {
  33. console.log('delay里的 count =>', count); //这里的 count 的值就是取的点击时候的状态
  34. }, 3000)
  35. }
  36. render();
  37. </script>
  38. </body>

如何在异步中获取最新的值

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. function Counter(){
  4. const [count, setCount] = React.useState(0);
  5. let lastCount = React.useRef(count); //永远指向一个对象
  6. React.useEffect(() => {
  7. lastCount.current = count;
  8. setTimeout(() => {
  9. console.log(lastCount.current); //永远是最新的值
  10. }, 3000)
  11. })
  12. return (
  13. <div>
  14. <p>{count}</p>
  15. <button onClick={() => setCount(count+1)}>加</button>
  16. </div>
  17. )
  18. }
  19. ReactDOM.render(<Counter />, document.getElementById('root'));

useMemo、useCallback

  • React优化的最重要策略是减少组件刷新(即减少渲染次数),希望组件的属性不变的话就不要刷新
  • 类组件的话,可以用 PureComnent
  • 函数组件的话就是用 memo、useCallback、useMemo
  • 把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新
  • 把创建函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

sdresdg.gif

  1. import React, { Component } from 'react';
  2. import ReactDOM from 'react-dom';
  3. function Child({data, handleClick}){
  4. console.log('Child render');
  5. return <button onClick={handleClick}>按钮:{data.number}</button>
  6. }
  7. const MemoChild = React.memo(Child);
  8. function App(){
  9. console.log('App render');
  10. const [name, setName] = React.useState('jack');
  11. const [number, setNumber] = React.useState(0);
  12. // useMemo 用于缓存 对象
  13. // 第2个参数是 依赖数组。只要 number 不变,data 永远指向同一个对象
  14. const data = React.useMemo(() => ({number}), [number]);
  15. // useCallback 用于缓存 函数
  16. const handleClick = React.useCallback(() => setNumber(number+1), [number]);
  17. return (
  18. <div>
  19. <input type="text" value={name} onChange={e => setName(e.target.value)}/>
  20. <MemoChild data={data} handleClick={handleClick} />
  21. </div>
  22. )
  23. }
  24. ReactDOM.render(<App />, document.getElementById('root'));

useReducer

  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等

    useState 与 useReducer 的优劣
  • useState 用起来比较简单,不需要编写 reducer,但是不能实现复杂的状态算法。

  • useReducer 用起来麻烦一点,但是功能强大。
  • useState 算是 useReducer 的简化版,语法糖。

Hooks - 图6

  1. import React, { Component } from './react';
  2. import ReactDOM from './react-dom';
  3. const ADD = 'ADD', MINUS = 'MINUS';
  4. /** reducer 处理器(可以接收一个老状态,返回一个新状态)
  5. * 之所以看上去和 redux 很像,是因为 redux 的作者,后来被 facebook 招过去了,专门负责开发 hooks
  6. *
  7. * @param {*} state 上一次状态
  8. * @param {*} action 动作 {type: 'xx'}
  9. */
  10. function reducer(state, action){
  11. switch (action.type){
  12. case ADD:
  13. return {number: state.number + 1};
  14. case MINUS:
  15. return {number: state.number - 1};
  16. default:
  17. return state;
  18. }
  19. }
  20. function Counter(){
  21. const [state, dispatch] = React.useReducer(reducer, {number: 0});
  22. return (
  23. <div>
  24. <p>Counter: {state.number}</p>
  25. <button onClick={() => dispatch({type: ADD})}>加</button>
  26. <button onClick={() => dispatch({type: MINUS})}>减</button>
  27. </div>
  28. )
  29. }
  30. ReactDOM.render(<Counter />, document.getElementById('root'));

useContext

  • 接收一个 context 对象(React.createContext的返回值)并返回该 context 的当前值。
  • 当前的 context 值由上层组件中距离当前组件最近的 的 value prop 决定。
  • 当组件上层最近的 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的static contextType = MyContext或者
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 来为下层组件提供 context ```jsx import React, { Component } from ‘react’; import ReactDOM from ‘react-dom’;

const CounterContext = React.createContext(); //创建 const ADD = ‘ADD’, MINUS = ‘MINUS’;

function reducer(state, action){ switch (action.type){ case ADD: return {number: state.number + 1}; case MINUS: return {number: state.number - 1}; default: return state; } }

function Counter(){ let {state, dispatch} = React.useContext(CounterContext); //获取 return (

Counter: {state.number}

) }

function App(){ const [state, dispatch] = React.useReducer(reducer, {number: 0}); return ( {/提供者/} ) }

ReactDOM.render(, document.getElementById(‘root’));

  1. <a name="C082E"></a>
  2. # useEffect
  3. - `useEffect` 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。
  4. - 该 Hook 接收一个包含命令式、且可能有副作用代码的函数。`useEffect(callback, deps)`
  5. - 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
  6. - 赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
  7. - useEffect 函数是在组件 **挂载之后** 和 **更新之后** 执行的。 跟 class 组件中的componentDidMount、componentDidUpdate和componentWillUnmount具有相同的用途,只不过被合并成了一个 API。
  8. <a name="OhpIR"></a>
  9. ##### 性能优化
  10. - 默认情况下,每次我们重新渲染,都会生成新的 effect,替换掉之前的。
  11. - 某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。
  12. - 比如我们想“1秒加1(参考下边的例子)”,那么如何优化呢?
  13. - 方法1:依赖数组置空
  14. - 方法2:在开启新的定时器之前把老的定时器删除
  15. <a name="F3Hl2"></a>
  16. ## 示例:执行时机
  17. ![](https://cdn.nlark.com/yuque/0/2021/gif/139415/1618583755579-625ed79c-81a2-4f7d-99d6-2ad51e67ca5e.gif#clientId=u87ceb5ab-b172-4&from=drop&id=u984fdf37&margin=%5Bobject%20Object%5D&originHeight=305&originWidth=356&originalType=binary&ratio=1&size=40328&status=done&style=stroke&taskId=uddaeeb9b-cb9e-4420-b72d-dda68fd67d0)
  18. ```jsx
  19. import React, { Component } from 'react';
  20. import ReactDOM from 'react-dom';
  21. function Counter(props){
  22. React.useEffect(() => {
  23. console.log('Counter DidMount or DidUpdate');
  24. return () => {
  25. console.log('Counter WillUnMount');
  26. }
  27. })
  28. return (() => {
  29. console.log('Counter render');
  30. return <div>{props.number}</div>;
  31. })()
  32. }
  33. function App(){
  34. const [number, setNumber] = React.useState(0);
  35. return (
  36. <div>
  37. {number <= 2 ? <Counter number={number} /> : null}
  38. <button onClick={() => setNumber(number+1)}>plus</button>
  39. </div>
  40. )
  41. }
  42. ReactDOM.render(<App />, document.getElementById('root'));

依赖数组置空

  • 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行

示例:1秒加1(方法1:依赖数组置空)

如果依赖数组为空的话,那么 useEffect 只会执行一次。 因为只有 依赖数组 发生变化了,才会重新执行;如果依赖数组为空数,永远不会变化,那么就不会重新执行。

  1. import React, { Component } from 'react';
  2. import ReactDOM from 'react-dom';
  3. function Counter(){
  4. console.log('Counter render');
  5. const [number, setNumber] = React.useState(0);
  6. React.useEffect(() => {
  7. console.log('开启一个定时器');
  8. const $timer = setInterval(() => {
  9. setNumber(number => number+1)
  10. }, 1000);
  11. }, []);
  12. return (
  13. <div>
  14. <p>Counter:{number}</p>
  15. </div>
  16. )
  17. }
  18. ReactDOM.render(<Counter />, document.getElementById('root'));

清除副作用

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用。
  • 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除。(即在执行当前 effect 之前对上一个 effect 进行清除)
  • React 只会在浏览器绘制后运行effects。这使得你的应用更流畅因为大多数effects并不会阻塞屏幕的更新。Effect的清除同样被延迟了。上一次的effect会在重新渲染后被清除。

示例:1秒加1(方法2:在开启新的定时器之前把老的定时器删除)
  1. React.useEffect(() => {
  2. console.log('开启一个定时器');
  3. const $timer = setInterval(() => {
  4. setNumber(number => number+1)
  5. }, 1000);
  6. // 清除函数
  7. return () => {
  8. console.log('关闭一个定时器');
  9. clearInterval($timer);
  10. }
  11. });
  1. 14:31:01.768 index.js:6 Counter render
  2. 14:31:01.786 index.js:18 开启一个定时器
  3. 14:31:03.801 index.js:6 Counter render
  4. 14:31:03.801 index.js:26 关闭一个定时器
  5. 14:31:03.802 index.js:18 开启一个定时器

useLayoutEffect、useRef

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • useEffect 不会阻塞浏览器渲染,而 useLayoutEffect 会阻塞浏览器渲染
  • useEffect 会在浏览器渲染结束后执行, useLayoutEffect 则是在 DOM 更新完成后,浏览器绘制之前执行
  • 可以近似认为 useLayoutEffect 是一个微任务;useEffect 是一个宏任务。

事件循环

Hooks - 图7

示例

Hooks - 图8

  1. import React, { Component } from 'react';
  2. import ReactDOM from 'react-dom';
  3. function Animation (){
  4. const r1 = React.useRef();
  5. const r2 = React.useRef();
  6. // 因为 useEffect 是在浏览器绘制之后执行的,所以绘制的时候没有移动,位置还是 0
  7. // 然后绘制之后,再修改 translate 修改位置,所以就有动画效果了
  8. React.useEffect(() => {
  9. r1.current.style.transform = 'translateX(200px)';
  10. r1.current.style.transition = 'all 1s linear';
  11. })
  12. // 因为 useLayoutEffect 是在浏览器绘制之前执行的,所以绘制的时候,DOM已经更新成 200px 了。所以没有动画了
  13. React.useLayoutEffect(() => {
  14. r2.current.style.transform = 'translateX(200px)';
  15. r2.current.style.transition = 'all 1s linear';
  16. })
  17. let style = {width: '130px', height: '40px', backgroundColor: 'red', marginBottom: '10px' };
  18. return (
  19. <>
  20. <div style={style} ref={r1}>useEffect</div>
  21. <div style={style} ref={r2}>useLayoutEffect</div>
  22. </>
  23. )
  24. }
  25. ReactDOM.render(<Animation />, document.getElementById('root'));

forwardRef、useImperativeHandle

  • forwardRef 将 ref 从父组件中转发到子组件中的dom元素上,子组件接受 props 和 ref 作为参数
  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用 ```jsx import React, { Component } from ‘react’; import ReactDOM from ‘react-dom’;

function Child(props, ref){ return } const ForwardedChild = React.forwardRef(Child);

function Parent(Props){ let [count, setCount] = React.useState(0); let childRef = React.createRef(1);

let getFocus = () => { childRef.current.focus();

  1. // childRef.current.remove();
  2. // 这样子很危险,不安全。
  3. // 那么我们只想让父亲能有获得焦点的功能,不能干别的。怎么办?
  4. // 用 useImperativeHandle,见下例

}

return (

) }

ReactDOM.render(, document.getElementById(‘root’));

  1. <a name="SFnDw"></a>
  2. ##### 使用 `useImperativeHandle` 示例
  3. ```jsx
  4. function Child(props, ref){
  5. + let inputRef = React.createRef();
  6. + React.useImperativeHandle(ref, () => ({
  7. + focus(){
  8. + inputRef.current.focus();
  9. + }
  10. + }))
  11. + return <input ref={inputRef} />
  12. }
  13. const ForwardedChild = React.forwardRef(Child);
  14. function Parent(Props){
  15. let [count, setCount] = React.useState(0);
  16. let childRef = React.createRef(1);
  17. let getFocus = () => {
  18. // childRef.current 指向 React.useImperativeHandle 的第2个参数的返回值对象
  19. childRef.current.focus();
  20. // childRef.current.remove();
  21. // 因为没有remove方法,所以在调用 remove 的话就会报错
  22. // Uncaught TypeError: childRef.current.remove is not a function
  23. }
  24. return (
  25. <div>
  26. <ForwardedChild ref={childRef} />
  27. <button onClick={getFocus}>获得焦点</button>
  28. <button onClick={() => setCount(count+1)}>count加1:{count}</button>
  29. </div>
  30. )
  31. }
  32. ReactDOM.render(<Parent />, document.getElementById('root'));

使用自定义属性传递 ref 也是可以的
  1. function Child(props){
  2. return <input ref={props.childRef} />
  3. }
  4. const ForwardedChild = React.forwardRef(Child);
  5. function Parent(Props){
  6. let [count, setCount] = React.useState(0);
  7. let childRef = React.createRef(1);
  8. let getFocus = () => {
  9. childRef.current.focus();
  10. }
  11. return (
  12. <div>
  13. <Child childRef={childRef} />
  14. <button onClick={getFocus}>获得焦点</button>
  15. <button onClick={() => setCount(count+1)}>count1:{count}</button>
  16. </div>
  17. )
  18. }
  19. ReactDOM.render(<Parent />, document.getElementById('root'));

ref 总结

元素的 ref 属性
  • 给类组件添加ref。 ref.current = 类组件的实例
  • 给原生组件添加ref。 ref.current = 原生组件对应的真实DOM元素
  • 给 forward包裹后的函数组件添加ref。ref.current指向 = 取决于组件里面是把 ref 给了谁。

createRef 用来创建 REF 对象
  • React.createRef(); => {current: null}

React.forwardRef 用来转发 ref
  • 返回一个类组件,类组件会转发收到的 ref 属性给函数的第2个参数

自定义 hooks

  • 有时候我们会想要在组件之间重用一些状态逻辑
  • 自定义 Hook 可以让你在不增加组件的情况下达到同样的目的
  • Hook 是一种复用状态逻辑的方式,它不复用 state 本身
  • 事实上 Hook 的每次调用都有一个完全独立的 state
  • 自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook

tre.gif

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  4. // 自定义hooks。只要一个函数以use开头,并且里面调用了别的hooks,那么他就是hooks
  5. function useRequest(url){
  6. let limit = 5; //每页的条数
  7. let [offset, setOffset] = React.useState(0); //偏移量
  8. let [data, setData] = React.useState([]); //真实的用户列表数据
  9. function loadMore(){
  10. setData(null);
  11. fetch(`${url}?offset=${offset}&limit=${limit}`)
  12. .then(res => res.json())
  13. .then(pageData => {
  14. delay(1000).then(() => {
  15. setData([...data, ...pageData]);
  16. setOffset(offset + pageData.length);
  17. })
  18. })
  19. }
  20. // 第一次渲染的时候,先调用一次 loadMore 加载第一页。
  21. React.useEffect(loadMore, []);
  22. return [data, loadMore];
  23. }
  24. function App(){
  25. const [users, loadMore] = useRequest('http://localhost:2020/user/info');
  26. if (users === null){
  27. return <div>加载中...</div>
  28. } else {
  29. return (
  30. <div>
  31. <ul>
  32. {users.map((item, index) => <li key={index}>{item.id}:{item.name}</li>)}
  33. </ul>
  34. <button onClick={loadMore}>加载更多</button>
  35. </div>
  36. )
  37. }
  38. }
  39. ReactDOM.render(<App />, document.getElementById('root'));

接口
  1. // nodemon 1.js localhost 2020
  2. let express = require('express');
  3. let app = express();
  4. app.use((req, res, next) => {
  5. res.header('Access-Control-Allow-origin', '*');
  6. next();
  7. })
  8. app.get('/user/info', (req, res) => {
  9. console.log('get', req, res);
  10. let offset = parseInt(req.query.offset);
  11. let limit = parseInt(req.query.limit);
  12. let result = [];
  13. for (let i=offset; i<offset+limit; i++){
  14. result.push({
  15. id: i+1,
  16. name: `name-${i+1}`,
  17. })
  18. }
  19. res.json(result);
  20. })
  21. app.listen(2020);