React

coderljw 2024-10-13 React
  • React
  • React 事件机制
  • Fiber
  • Hooks
大约 15 分钟

# 1. 宏观包结构

# 2. 事件机制

  • 为了解决跨浏览器的兼容性问题,SyntheticEvent 实例将被传递给你的事件处理函数,SyntheticEvent 是 React 跨浏览器的浏览器原生事件包装器,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。

    1. 监听原生事件:对齐 DOM 元素和 fiber 元素。
    2. 收集 listeners:遍历 fiber 树,收集所有监听本事件 listener 函数。
    3. 派发合成事件:构造合成事件,遍历 listeners 进行派发。
  • React 实际上并不将事件附加到子节点本身。React 使用单个事件侦听器侦听顶层的所有事件。这对性能有好处,也意味着 React 在更新 DOM 时不需要跟踪事件监听器。

  • React 16 React 事件系统工作原理 - 网易云音乐大前端团队 (opens new window)

    1. React 将所有事件类型都注册到 document 上。
    2. 所有原生事件的 listener 都是 dispatchEvent 函数。
    3. 同一个类型的事件 React 只会绑定一次原生事件,例如无论我们写了多少个 onClick,最终反应在 DOM 事件上只会有一个 click listener(采用 Set 数据结构存储判断)。
    4. React 并没有将我们业务逻辑里的 listener 绑在原生事件上,也没有去维护一个类似 eventlistenermap 的东西存放我们的 listener。
  • React 16 及更早版本,合成事件对象的事件处理函数全部被调用之后,所有属性都会被置为 null。这时,如果我们需要在事件处理函数运行之后获取事件对象的属性,可以使用 React 提供的 e.persist() 方法,保留所有属性。

    React 17 不使用事件池,不会存在 “所有属性都会被置为 null” 的问题。

    const handleChange = e => {
      // e.persist()
      console.log(e.target) // button
      setTimeout(() => console.log(e.target), 100) // null
    }
    
    1
    2
    3
    4
    5
  • 类组件:事件触发过程中,dispatchEvent 调用了 invokeGuardedCallback 方法。回调函数是直接调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的 this 是 undefined。

  • 批量更新与同步更新。

    批量更新:

    // 更新调度
    export function scheduleUpdateOnFiber() {
      // ...
    
      if (lane === SyncLane) {
        // Legacy 模式
    
        if (omit) {
        } else {
          // 批量更新
          ensureRootIsScheduled(root, eventTime)
          schedulePendingInteractions(root, lane)
          // 执行上下文为空时进行同步更新
          if (executionContext === NoContext) {
            // 同步更新
            resetRenderTimer()
            flushSyncCallbackQueue()
          }
        }
      } else {
        // 并发模式(Blocking | Concurrent)
    
        // ...
    
        // 批量更新
        ensureRootIsScheduled(root, eventTime)
        schedulePendingInteractions(root, lane)
      }
      // ...
    }
    
    // 批处理函数
    export function batchedUpdates() {
      const prevExecutionContext = executionContext
      executionContext |= BatchedContext // 位掩码方式增加 BatchedContext
      try {
        // 此时,执行 scheduleUpdateOnFiber 的 executionContext !== NoContext
        return fn(a)
      } finally {
        executionContext = prevExecutionContext
        if (executionContext === NoContext) {
          // Flush the immediate callbacks that were scheduled during this batch
          resetRenderTimer()
          flushSyncCallbackQueue()
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47

    同步更新:

    • 在 React Legacy 模式中,非 React 管控的事件的回调函数(addEventListener、Promise、MessageChannel、setTimeout)不经过 batchedUpdates 函数执行,也就是同步更新。
  • React 会在派发事件时打开批量更新,此时 1、2 在合成事件内,只会触发一次 render。setTimeout 为异步任务,此时 3、4 是同步的,均会触发一次 render。

    类组件打印顺序:0 -> 0 -> render -> flushSync -> render -> 3 -> render -> 4 -> batched -> 4 -> 4 -> render

    import React, { Component } from 'react'
    import { unstable_batchedUpdates } from 'react-dom'
    
    class App extends Component {
      state = {
        num: 0,
      }
    
      handleChange = () => {
        const setState = this.setState.bind(this)
        setState({ num: 1 })
        console.log(this.state.num) // 0
        setState({ num: 2 })
        console.log(this.state.num) // 0
    
        setTimeout(() => {
          console.log('flushSync')
          setState({ num: 3 })
          console.log(this.state.num) // 3
          setState({ num: 4 })
          console.log(this.state.num) // 4
        })
    
        setTimeout(() => {
          unstable_batchedUpdates(() => {
            console.log('batched')
            setState({ num: 5 })
            console.log(this.state.num) // 4
            setState({ num: 6 })
            console.log(this.state.num) // 4
          })
        })
      }
    
      render() {
        console.log('render')
        return (
          <>
            <button onClick={this.handleChange}>button</button>
          </>
        )
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    • 函数组件打印顺序:0 -> 0 -> render -> flushSync -> render -> 0 -> render -> 0 -> batched -> 0 -> 0 -> render
    • Hooks 闭包原因,不能在 setTimeout 获取到 num 最新的值
    import React, { useState } from 'react'
    import { unstable_batchedUpdates } from 'react-dom'
    
    const App = () => {
      const [num, setNum] = useState(0)
    
      const handleChange = () => {
        setNum(1)
        console.log(num) // 0
        setNum(2)
        console.log(num) // 0
    
        setTimeout(() => {
          console.log('flushSync')
          setNum(3)
          console.log(num) // 0
          setNum(4)
          console.log(num) // 0
        })
    
        setTimeout(() => {
          unstable_batchedUpdates(() => {
            console.log('batched')
            setNum(5)
            console.log(num) // 0
            setNum(6)
            console.log(num) // 0
          })
        })
      }
    
      console.log('render')
    
      return (
        <>
          <button onClick={handleChange}>button</button>
        </>
      )
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
  • React 17 事件系统新特性。

    1. React 将不再向 document 附加事件处理器。而会将事件处理器附加到渲染 React 树的根 DOM 容器中ReactDOM.render(<App />, rootNode);。将顶层事件绑定在 rootNode 上而不是 document 上能够解决我们遇到的多版本共存问题,对微前端方案是个重大利好。
    2. 支持了原生捕获事件的支持,对齐了浏览器原生标准,同时 onScroll 事件不再进行事件冒泡。
    3. 取消事件复用。
  • 在 processDispatchQueueItemsInOrder 函数中,根据捕获(capture)或冒泡(bubble)的不同,采取了不同的遍历方式(packages/react-dom/src/events/DOMPluginEventSystem (opens new window))。

    • capture 事件:从上至下调(由外向里)用 fiber 树中绑定的回调函数,所以倒序遍历 dispatchListeners。
    • bubble 事件:从下至上调(由里向外)用 fiber 树中绑定的回调函数,所以顺序遍历 dispatchListeners。
  • 事件触发顺序。

17 版本之前 17 版本
html 捕获 父元素 React 事件捕获
父元素原生捕获 子元素 React 事件捕获
子元素原生捕获 root 捕获
子元素原生冒泡 父元素原生捕获
父元素原生冒泡 子元素原生捕获
html 冒泡 子元素原生冒泡
父元素 React 事件捕获 父元素原生冒泡
子元素 React 事件捕获 子元素 React 事件冒泡
子元素 React 事件冒泡 父元素 React 事件冒泡
父元素 React 事件冒泡 root 冒泡

# 3. Fiber 架构

  • Fiber 是对 React 核心算法的重构,facebook 团队使用两年多的时间去重构 React 的核心算法,在 React 16 以上的版本中引入了 Fiber 架构。

  • React 15 架构可以分为两层:

    • Reconciler(协调器)- 负责找出变化的组件
    • Renderer(渲染器)- 负责将变化的组件渲染到页面上
  • React 16 架构可以分为三层:

    • Scheduler(调度器)- 调度任务的优先级,高优任务优先进入 Reconciler
    • Reconciler(协调器)- 负责找出变化的组件
    • Renderer(渲染器)- 负责将变化的组件渲染到页面上
  • React 15 中 Reconciler 是递归处理虚拟 DOM 的,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。

  • React 16 使用双缓存 Fiber 树(链表结构)处理虚拟 DOM,调度的过程可以被中断,将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复调度。让 React 的更新过程变得可控,避免了之前一竿子递归到底影响性能的做法(渲染可中断需开启 Concurrent 模式)。

    1. 为每个任务增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新执行优先级低的任务。
    2. 增加了异步任务,调用 requestIdleCallback(React 使用自我实现的 requestIdleCallbackpolyfill —— Scheduler),浏览器空闲的时候执行。
    3. dom diff 树变成了链表,一个 dom 对应两个 fiber,对应两个队列,是为找到被中断的任务,重新执行。
  • 任务优先级:为了避免任务被饿死,可以设置一个超时时间。这个超时时间不是死的,低优先级的可以慢慢等待,高优先级的任务应该率先被执行。目前 React 预定义了 5 个优先级。

    1. Immediate(-1)这个优先级的任务会同步执行,或者说要马上执行且不能中断。
    2. UserBlocking(250ms)这些任务一般是用户交互的结果,需要即时得到反馈。
    3. Normal(5s)应对哪些不需要立即感受到的任务,例如网络请求。
    4. Low(10s)这些任务可以放后,但是最终应该得到执行。例如分析通知。
    5. Idle(没有超时时间)一些没有必要做的任务(e.g. 比如隐藏的内容),可能会被饿死。
  • Fiber 两阶段:

    协调阶段(fiber 树构造):可以认为是 Diff 阶段,这个阶段可以被中断,会找出所有节点变更,例如节点新增、删除、属性变更等等,这些变更 React 称之为 副作用(Effect)。以下生命周期钩子会在协调阶段被调用:

    1. constructor
    2. componentWillMount(废弃)
    3. componentWillReceiveProps(废弃)
    4. static getDerivedStateFromProps
    5. shouldComponentUpdate
    6. componentWillUpdate(废弃)
    7. render

    提交阶段(fiber 树渲染):将上一个阶段计算出来的需要处理的 副作用(Effects) 一次性执行了。Legacy 模式下这个阶段必须同步执行,不能被打断。这些生命周期钩子在提交阶段被执行:

    1. getSnapshotBeforeUpdate(严格来说,这个是在进入 commit 阶段前调用)
    2. componentDidMount
    3. componentDidUpdate
    4. componentWillUnmount
  • 最通俗的 React Fiber(时间分片) 打开方式 - 荒山 (opens new window)

  • React 技术揭秘 - 卡颂 (opens new window)

  • 图解 React - 图解 React (opens new window)

# 4. Hooks

  • 代数效应是函数式编程中的一个概念,用于将副作用从函数调用中分离。不需要关注 Hooks 怎么存储状态,只需按正常结果编写业务逻辑。

    // 假设语法
    function getName(user) {
      let name = user.name;
      if (name === null) {
        name = perform 'ask_name';
      }
      return name;
    }
    
    function makeFriends(user1, user2) {
      user1.friendNames.add(getName(user2));
      user2.friendNames.add(getName(user1));
    }
    
    const arya = { name: null };
    const gendry = { name: 'Gendry' };
    try {
      makeFriends(arya, gendry);
    } handle (effect) {
      if (effect === 'ask_name') {
        setTimeout(() => {
          resume with 'Arya Stark';
        }, 1000);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  • 极简 Hooks 实现 - React 技术揭秘 (opens new window)

    let workInProgressHook // 当前工作的 Hook
    let isMount = true // true 代表首次挂载
    
    // App 组件对应的 fiber
    const fiber = {
      memoizedState: null, // Hooks 链表
      stateNode: App, // render
    }
    
    // 调度更新
    function schedule() {
      // 更新前将 workInProgressHook 重置为第一个 Hook
      workInProgressHook = fiber.memoizedState
      // render
      const app = fiber.stateNode()
      isMount = false
      return app
    }
    
    // 更新 Hook(updateNum)
    function dispatchAction(queue, action) {
      // 更新队列:环状单向链表(方便取第一个元素和添加元素)
      const update = {
        action, // updateNum 回调
        next: null, // 下个 update
      }
      // 创建 update 单向环状链表
      // queue.pending 为最后一个添加的元素,queue.pending.next 为第一个添加的元素
      if (queue.pending === null) {
        update.next = update
      } else {
        update.next = queue.pending.next
        queue.pending.next = update
      }
      queue.pending = update
    
      schedule()
    }
    
    function useState(initialState) {
      let hook
    
      if (isMount) {
        // 首次挂载
        hook = {
          // 更新队列
          queue: {
            pending: null,
          },
          memoizedState: initialState, // Hook 当前状态(num)
          next: null, // 下个 Hook
        }
        // 创建 Hooks 单向链表
        if (!fiber.memoizedState) {
          fiber.memoizedState = hook
        } else {
          workInProgressHook.next = hook
        }
        workInProgressHook = hook
      } else {
        // 更新
        hook = workInProgressHook
        workInProgressHook = workInProgressHook.next // 移动到下一个 Hook
      }
    
      let baseState = hook.memoizedState // 保存更新前状态
      // 判断当前 Hook 更新队列是否为空
      if (hook.queue.pending) {
        let firstUpdate = hook.queue.pending.next
    
        // 执行更新队列函数,获取最后的 Hook 状态
        do {
          const action = firstUpdate.action
          baseState = action(baseState)
          firstUpdate = firstUpdate.next
        } while (firstUpdate !== hook.queue.pending)
    
        // 清空更新队列
        hook.queue.pending = null
      }
      // 更新 Hook 当前状态
      hook.memoizedState = baseState
    
      return [baseState, dispatchAction.bind(null, hook.queue)]
    }
    
    function App() {
      const [num, updateNum] = useState(0)
    
      console.log(`${isMount ? 'mount' : 'update'} num: `, num)
    
      return {
        click() {
          updateNum(num => num + 1)
        },
      }
    }
    
    window.app = schedule()
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99

# 5. 启动模式

  • Legacy 模式:不支持 Concurrent(并发),不可中断渲染。

    ReactDOM.render(<App />, document.getElementById('root'), dom => {}) // 支持 callback 回调,参数是一个 dom 对象
    
    1
  • Blocking 模式:开启部分 Concurrent 功能,可中断渲染。

    ReactDOM.createBlockingRoot(document.getElementById('root')).render(<App />) // 不支持回调
    
    1
  • Concurrent 模式:开启所有 Concurrent 功能,可中断渲染。

    ReactDOM.createRoot(document.getElementById('root')).render(<App />) // 不支持回调
    
    1

# 6. 优先级管理

  • 3 种类型优先级。

    1. fiber 优先级(LanePriority):位于 react-reconciler 包,也就是 Lane(车道模型)。
    2. 调度优先级(SchedulerPriority):位于 scheduler 包。
    3. 优先级等级(ReactPriorityLevel):位于 react-reconciler 包中的 SchedulerWithReactIntegration.js,负责上述 2 套优先级体系的转换。
  • Lane(车道模型):占有低位比特位的 Lane 变量对应的优先级越高。

    • 最高优先级为 SyncLanePriority 对应的车道为 SyncLane = 0b0000000000000000000000000000001。
    • 最低优先级为 OffscreenLanePriority 对应的车道为 OffscreenLane = 0b1000000000000000000000000000000。
    const SyncLane = 0b0000000000000000000000000000001
    const SyncBatchedLane = 0b0000000000000000000000000000010
    const InputDiscreteHydrationLane = 0b0000000000000000000000000000100
    // ...
    
    const task1 = SyncLane,
      task2 = SyncBatchedLane,
      task3 = InputDiscreteHydrationLane
    
    let batchOfTasks = task1 | task2 // =>  0b0000000000000000000000000000011
    
    // 删除单个 task
    batchOfTasks &= task1 // => 0b0000000000000000000000000000010
    // 增加单个 task
    batchOfTasks |= task1 // => 0b0000000000000000000000000000011
    // 比较 task1 是否在 group 中
    ;(task1 & batchOfTasks) !== 0 // => true (task1 & batchOfTasks:0b0000000000000000000000000000011)
    // 比较 task3 是否在 group 中
    ;(task3 & batchOfTasks) !== 0 // => false (task3 & batchOfTasks:0b0000000000000000000000000000000)
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  • 图解 React / 优先级管理 - 图解 React (opens new window)

# 7. 调度原理

  • requestHostCallback 请求回调后,通过 MessageChannel 发消息的方式触发 performWorkUntilDeadline 函数,最后执行回调 scheduledHostCallback。

    微任务将在页面更新前全部执行完,达不到将主线程还给浏览器的目的,递归的 setTimeout() 调用会使调用间隔变为 4ms,导致浪费了 4ms(事件循环(Event Loop))。所以首选 MessageChannel ,setTimeout 作为 MessageChannel 降级处理。

let mnCount = 0
function mn() {
  const mc = new MessageChannel()
  mc.port1.onmessage = ({ data }) => {
    console.log(++mnCount, Date.now() - data) // 大部分输出0
    if (mnCount < 50) mn()
  }
  mc.port2.postMessage(Date.now())
}
mn()

let tnCount = 0
function tn() {
  const startTime = Date.now()
  setTimeout(_ => {
    console.log(++tnCount, Date.now() - startTime) // 前4次输出约为1,第5次开始输出约为5
    if (tnCount < 50) tn()
  }, 0)
}
tn()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 8. Fiber 树构造

  • 双缓冲技术:把 ReactElement 转换成 fiber 树的过程中,内存里会同时存在 2 棵 fiber 树。

    1. 代表当前界面的 fiber 树(已经被展示出来,挂载到 fiberRoot.current 上)。如果是初次构造(初始化渲染),页面还没有渲染,此时界面对应的 fiber 树为空(fiberRoot.current = null)。
    2. 正在构造的 fiber 树(即将展示出来,挂载到 HostRootFiber.alternate 上,正在构造的节点称为 workInProgress)。当构造完成之后,重新渲染页面,最后切换 fiberRoot.current = workInProgress,使得 fiberRoot.current 重新指向代表当前界面的 fiber 树。
  • fiber 树构造循环负责构造新的 fiber 树,构造过程中同时标记 fiber.flags,最终把所有被标记的 fiber 节点收集到一个副作用队列中,这个副作用队列被挂载到根节点上(HostRootFiber.alternate.firstEffect)。此时的 fiber 树和与之对应的 DOM 节点都还在内存当中,等待 commitRoot 阶段进行渲染。

  • fiber 树更新阶段跟踪了创建过程中内存引用的变化情况。与初次构造最大的不同在于 fiber 节点是否可以复用,其中 bailout 逻辑是 fiber 子树能否复用的判断依据。

  • 图解 React / fiber 树构造 - 图解 React (opens new window)

# 9. Fiber 树渲染

从渲染前、渲染、渲染后三个方面分解了 commitRootImpl 函数。其中最核心的渲染逻辑又分为了 3 个函数,这 3 个函数共同处理了有副作用 fiber 节点,并通过渲染器 react-dom 把最新的 DOM 对象渲染到界面上。

  1. commitBeforeMutationEffects

    dom 变更之前、处理副作用队列中带有 Snapshot(根节点与类组件)、Passive(使用 hook 的函数组件) 标记的 fiber 节点。

  2. commitMutationEffects

    dom 变更、界面得到更新。处理副作用队列中带有 Ref(清空 Ref)、Placement(新增)、Update(更新)、Deletion(删除)、Hydrating 标记的 fiber 节点。

  3. commitLayoutEffects

    dom 变更后,处理副作用队列中带有 Update、Callback(如:this.setState({}, callback))、Ref(重新设置 Ref) 标记的 fiber 节点。对于 HostComponent 节点,如有 Update 标记,需要设置一些原生状态(focus 等)。

# 10. 状态与副作用

  • useState 在 fiber 树构造阶段(render)执行,可以修改 Hook.memoizedState。

  • useLayoutEffect 和 useEffect 均在 fiber 树渲染阶段(commitRoot)执行。

    • useLayoutEffect 是同步执行的,与 componentDidMount、componentDidUpdate 的调用阶段是一样的。
    • useEffect 经过了调度中心,所以 useEffect 中的函数是异步执行的(调度中心首选 MessageChannel,setTimeout 降级处理)。
  • 输出顺序:1 -> 5 -> 3 -> 7 -> 2 -> 4 -> 6。

    function App() {
      console.log(1)
    
      // 先于 useEffect 注册
      const { port1, port2 } = new MessageChannel()
      port1.onmessage = ({ data }) => console.log(data)
      port2.postMessage(2)
    
      useLayoutEffect(() => {
        // commitRoot 阶段同步执行
        console.log(3)
      })
    
      useEffect(() => {
        // commitRoot 阶段,通过调度中心执行(MessageChannel || setTimeout)
        console.log(4)
      })
    
      setTimeout(_ => console.log(6))
    
      Promise.resolve(7).then(console.log)
    
      console.log(5)
    
      return <></>
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
  • 输出顺序:1 -> 2 -> 5 -> num: 0 -> 1 -> 5 -> num: 1 -> 3 -> 6 -> 6 -> 4。

    render 执行两次,commitRoot 执行一次。

    function App() {
      console.log(1)
      const [num, setNum] = useState(0)
    
      useState(_ => {
        console.log(2)
        setNum(num + 1) // 初次执行 render 后,再次进入 reconciler 流程
      })
    
      useLayoutEffect(() => {
        console.log(3)
      })
    
      useEffect(() => {
        console.log(4)
      })
    
      // 两次 render 即注册两次
      Promise.resolve().then(_ => console.log(6))
    
      console.log(5)
    
      return <>{console.log('num:', num)}</>
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
  • 图解 React / 状态与副作用 - 图解 React (opens new window)

# 11. 类组件与函数组件区别

类组件 函数组件
面向对象编程(OOP) 函数式编程(FP),更方便单元测试,未来趋势
需要创建实例,会占用一定内存 不需要创建实例,可以节约内存
烦人的 this 好用的 Hooks(小心闭包)
拥有完整的生命周期 可通过 Hooks 实现一些生命周期
主要通过高阶组件(HOC)实现逻辑复用(还可以用继承) 主要通过 Hooks 实现逻辑复用(也可以用 HOC)
babel 转换后代码更多 babel 转换后代码更少
以父之名
周杰伦.mp3