React 类组件和函数组件

大纲链接 §

[toc]


1. 元素 v.s. 组件

Element v.s. Component

  • 元素与组件
    • 这是一个 React 元素d小写):const div = React.createEkement('div', ...)
    • 这是一个 React 组件D大写):const Div = () => React.createElement('div', ...)
      • 使用时当做标签来使用 <Div /> 为了和原生标签区别
  • 这样的写法是约定俗成

什么是组件

  • 能跟其他物件 组合 起来的物件,就是组件
  • 组件并没有明确的定义,靠感觉理解就行
  • 就目前而言,一个返回 React 元素的 函数 就是组件
  • Vue 中,一个 构造选项 就可以表示一个组件

React中不同风格的两种组件:类组件 v.s. 函数组件

一、类组件

1
2
3
4
5
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>
  }
}
  • extends React.Component 是固定写法
  • 注意:使用外部数据 {this.props.name}
    • this 中获取 props
  • 使用组件 <Welcome name="xxx" />
    • 当做标签使用
    • 在标签中传属性,自动挂载到 props 上,以key: value的形式

二、函数组件 (更推荐)

1
2
3
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>
}
  • 注意:使用外部数据 {props.name}
    • 从函数的参数中获取 props
  • 使用组件 <Welcome name="xxx" />
    • 当做标签使用
    • 在标签中传属性,自动挂载到 props 上,以key: value的形式

两种写法等价,其中函数组件也可以写成箭头函数组件

三种风格的组件


2. 在React中写标签会被翻译为 React.createElement

React中写标签是在写什么?

  • 绝对不是在写 HTML,其实是将 XML 标签编译为 JS 代码
  • React 同时支持接受 字符串 和 函数 作为参数,即可自动根据参数的类型 做不同的操作
    • <div /> 会被翻译成 React.createElement('div') 原生 变成 标签名的字符串
    • <Welcome /> 会被翻译成 React.createElement(Welcome) 组件变成 函数名 传入
  • 可以用 babel online 查看直接翻译 结果来验证

React.createElement 的处理逻辑

如果传入一个 字符串 'div',则会创建一个 div标签

1
<div className="red" title="hi">xxx</div>

如果传入一个 ,则会在类前面加上 new操作符

  • 这会导致执行 constructor
  • 获取一个组件 对象
  • 然后调用对象的 render 方法获取其返回值,代替原来组件标签的位置
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Welcome extends React.Component {
  constructor() {
    super()
    this.state = {n: 0}
  }
  render() {
    return <div>hi</div>
  }
}

new Welcome()

<Welcome name="hi" />

如果传入一个 函数,则会调用该函数,并 获取其返回值,代替原来组件标签的位置

1
2
const Welcome = () => (<div>hi</div>)
<Welcome name="hi" />

3. 小试牛刀,动手尝试两种组件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 相同的开头 之后略
import React from 'react'
import ReactDOM from 'react-dom'
import './styles.css'

function App() { return ( <div> 爸爸 <Son /> </div> ) }

class Son extends ReactComponent {
  constructor() {}
  add() {}
  render() {}
}

const GrandSon = () => {
  return ()
}

代码示例:链接

  • 入口:AppReactDOM.render(<App />, rootElement))调用函数组件 AppReact 17)
    • ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <App /> );React 18+)
  • App 返回 根组件 <div></div>
    • 根组件 包含 Son 组件
      • Son 组件 使用类组件 class Son extends React.Component {}
      • Son 组件 实现显示数据 n 功能
        • constructor() {}
          • super() 继承属性,必写,不写报错
          • this.state = {n: 0} 声明内部数据 初始化数据
      • Son 组件 实现点击按钮 n + 1 功能
        • add() { this.setState({ n: this.state.n + 1 }); } 方法操作内部数据
        • render() {}
          • 儿子 n: {this.state.n}
          • <button onClick={() => this.add()}>+1</button>
    • Son 组件 包含 GrandSon 组件
      • GrandSon 组件 使用函数组件 const Grandson = () => {}
      • GrandSon 组件 实现显示数据 n 功能
        • const [n, setN] = React.useState(0) 解构出内部数据 和 数据更新方法 初始化
        • return ()
          • 孙子 n:{n}
      • GrandSon 组件 实现点击按钮 n + 1 功能
        • return ()
          • <button onClick={() => setN(n + 1)}>+1</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
class Son extends ReactComponent {
  constructor() {
    super()
    this.state = {n: 0}
  }
  add() {
    // this.state.n += 1 // 直接改为何不行
    this.setState( {n: this.state.n + 1 } )
  }
  render() {
    <div className="Son">
      儿子 n{ this.state.n }
      <button onClick={ () => this.add() }> +1 </button>
      <GrandSon />
    </div>
  }
}

const GrandSon = () => {
  const [n, setN] = React.useState(0)
  return (
    <div className="GrandSon">
      孙子 n{ n }
      <button onClick={ setN(n + 1) }> +1 </button>
    </div>
  )
}
  • 子组件,使用类组件
    • constructor
      • 继承属性 super()
      • 初始化内部数据 this.state = {n: 0}
    • 显示数据:儿子 n:{ this.state.n }
    • 变更数据:
      • this.state.n += 1 直接改内部数据为何不行
      • 必须使用 this.setState({ n: this.state.n + 1 })
    • 用户交互:点击按钮,执行加一操作 onClick={ () => this.add() }
  • 孙代组件:使用函数组件
    • 初始化内部数据 React.useState(0)
    • 解构出 内部数据 和 数据变更的方法 const [n, setN] = React.useState(0)
    • 显示数据:儿子 n:{ n }
    • 变更数据:onClick={ () => setN(n + 1) 得到一个新的n 而不是原有的n

4. 类组件 v.s. 函数组件使用 props

添加 props (外部数据) 父组件传值给子组件

  • 代码示例:链接
  • 父组件传递数据给子组件
    • <Son msgToSon="儿子你好"/>
    • <Son msgToSon={variable}/>
    • 可使用字符串或者表达式(变量)
  • 类组件 直接读取 属性 this.props.msgToSon
  • 函数组件 直接读取 参数 props.msgToSon
    • 接收参数(一般命名为props),将组件外部属性挂载到第一个参数对象上
  • 外部数据只关心 ,不可写(符合单向数据流原则)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Son extends React.Component {
  render() {
    return (
      <div calssName="Son">
        我是儿子 我爸对我说{ this.props.msgToSon }
        <Grandson msgToGrandson="孙子你好" />
      </div>
    )
  }
}

const GrandSon = props => {
  return (
    <div calssName="GrandSon">
      我是儿子 我爸对我说{ props.msgToGrandson }
    </div>
  )
}

5. 类组件 v.s. 函数组件使用 state

添加 state (内部数据类似于Vuedata

  • 代码示例:链接
  • 关注数据时的三个条件:
    • 初始化 内部数据
    • 读取 内部数据
    • 写入 内部数据
  • 类组件
    • constructor 方法中的 super()之后 初始化内部数据
      • this.state = { xxx: 0 }
    • 使用 this.state.xxx 来读
    • 使用 this.setState(???, fn) 来写,两种写法
      • 使用 this.setState({xxx: this.state.xxx + 1}, fn) 来写,可以再传回调函数作为第二个参数
      • 更推荐使用 this.setState( (state, props) => ({xxx: state.xxx + 1}), fn) 工厂函数 更清晰地表明改变后的数据
  • 函数组件 使用 useState 返回一个数组,第一项 ,第二项
    • 语法含义 const [读值, 写值方法] = React.useState(初始值)
    • 命名约定 const [x, setX] = useState('xxx')
    • 当调用了 setX 并不会改变 x 本身,且永远不会改变 x
    • 而是产生了一个新的 x 返回
  • 内部数据关心:
    • 初始化
 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
class Son extends React.Component {
  constructor() {
    super()
    this.state = {n: 0} // 初始化
  }
  add() {
    // this.state.n += 1 // 为什么不能更新视图?错误的写法
    // 因为 react 没有监听属性(属性劫持)

     // 写
    this.setState({ n: this.state.n + 1 }) // 不推荐的写法 易混淆
    this.setState( state => state.n + 1 ) // 推荐的写法 更好理解区分新旧 state,避免异步造成的误解
  }
  render() {
    return (
      <div className="Son">
        儿子 n{ this.state.n } {/* 读 */}
        <button onClick={() => this.add()}> +1 </button>
        <Grandson />
      </div>
    )
  }
}

const GrandSon = () => {
  const [n, setN] = React.useState(0)
  return (
    <div className="Grandson">
      孙子 n: {n}
      <button onClick={() => setN(n + 1)}>+1</button>
    </div>
  )
}

类组件中 不推荐 的 写入方法

  • this.state.n += 1
  • this.setState( this.state )
  • 虽然可以生效,但是一般推荐 重新产生一个新的对象,而不是在原有的对象中修改
  • 这遵循了 「数据不可变的原则」

更推荐向 setState() 中传 一个返回对象的工厂函数 作为 参数,代替 传新的对象

  • setState( (state, /* props */) => ({n: state.n + 1 }) )
  • 因为 setState 不会马上去改变 this.state.n 的值,会等一会儿更新,内部更新UI机制是异步的
  • 可以在 setSatate 之后验证,打印出 console.log(this.state.n)
    • 事实上,setSatate会在紧跟之后的同步代码结束后执行
  • 为了避免混淆新旧 this.state ,必须传工厂函数
  • 工厂函数能够明确显示 n 新的值,帮助更好地理解,防止误解出错
  • 这实际上是一个 Stale Closure 过时的闭包问题
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ...
  add() {
    this.setState({ n: this.state.n + 1 }) // 写
    console.log(this.state.n) // 视图改变 但打印的是 0 旧的n值
    // this.state.n 仍是旧的值

    this.setState( state => {
      const n = state.n + 1 // 可以很清楚地分辨 新旧 n 的值
      console.log(n)
      return {n}
    })
  }
// ...

props v.s. state 类组件 v.s.函数组件

  • 分别和Vue中的 propsdata 对应
  • 函数组件的写法更简洁、不用考虑this,数据不可变

6. 类组件 注意事项(很多坑)

this.state.n += 1 无效?

  • 其实 n 已经变了,只不过 UI 不会自动更新而已
  • 调用 setState 才会触发 UI 更新(异步更新)
  • 因为 React 没有像 Vue 监听 data 一样监听 state

setState 会异步更新 UI

  • setState 之后,state 不会马上改变,立马读 state 仍是旧值
  • 更推荐的方式是 setState(工厂函数)

this.setState(this.state) 不推荐?

  • React 希望我们不要修改旧 state(不可变数据)
  • 常用代码:setState( {n: state.n + 1 } )

小结

  • React 函数式是一种理念。不适就用 Vue

7. 函数组件 注意事项

与类组件相似的地方

  • 也要通过 setX(新值) 来更新 UI

和类组件不同的地方

  • 没有 this,一律用参数和变量

两种编程模型的浅层对比

两种编程模型的浅层对比

React 编程模型

  • 一个对象,对应一个虚拟 DOM
  • 另一个对象,对应另一个虚拟 DOM
  • 对比两个虚拟 DOM,找到不同处(DOM diff),最够局部更新 DOM

Vue 编程模型

  • 一个对象,对应一个虚拟 DOM
  • 当属性改变时,把属性相关 DOM 节点全部更新
  • 注:Vue 为了其他考量,也引入了虚拟 DOMDOM diff
  • Vue 劫持数据,修改数据会直接映射到UI上,更易理解

8. 复杂 state (多个数据/深层级)怎么处理

  • 公共代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import React from 'react'
import ReactDOM from 'react-dom'
import './styles.css'

function App() {
  return (
    <div className="app">
      爸爸
      <Son />
    </div>
  )
}

类组件里有 nm

 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
class Son extends React.Component {
  constructor() {
    super()
    this.state = {
      n: 0,
      m: 0
    }
  }

  addN() {
    this.setState( state => ( {n: state.n + 1}) )
    // m未传值 会被覆盖为 undefined 吗?
  }
  addM() {
    this.setState( state => ( {m: state.m + 1}) )
    // n未传值 会被覆盖为 undefined 吗?
  }

  render() {
    return (
      <div className="Son">
        儿子 n: {this.state.n}
        <button onClick={() => this.addN}>n + 1</button>
        m: {this.state.m}
        <button onClick={() => this.addM}>m + 1</button>
        <GrandSon />
      </div>
    )
  }

}
  • 类组件中setState,如果只对一部分进行修改,其他部分自动沿用上一次的值,而不会被undefined覆盖
    • 可以了理解为类组件中,会自动合并未修改的数据,而用手动写 this.setState( {...this.state, n: this.state.n + 1} )
  • 可是,这种合并只会合并第一层数据
  • 代码示例

函数组件里有 nm

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const GrandSon = () => {
  const [n, setN] = React.useState(0)
  const [m, setM] = React.useState(0)
  return (
    <div className="GrandSon">
      孙子 n: {n}
      <button onClick={ () => setN(n + 1)}>n + 1</button>
      m: {m}
      <button onClick={ () => setM(m + 1)}>m + 1</button>
    </div>
  )
}

函数组件另一种 不推荐的写法

你会发现 m 被置空

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const GrandSon = () => {
  const [state, setState] = React.useState( {n: 0, m: 0} )
  return (
    <div className="GrandSon">
      孙子 n: {state.n}
      <button onClick={ () => setState({n: state.n + 1})}>n + 1</button>
      m: {state.m}
      <button onClick={ () => setState({m: state.m + 1})}>m + 1</button>
    </div>
  )
}
  • 函数组件的 setState 不会自动合并没有更新设置的数据,会用 undefined 覆盖没有传入的数据属性
  • 要实现类组件同样的合并效果,需要使用 展开运算符 ...state 来拷贝原先的数据属性,手动合并:
    • <button onClick={ () => setState({...state, n: state.n + 1})}>n + 1</button>
    • <button onClick={ () => setState({...state, m: state.m + 1})}>m + 1</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
class Son extends React.Component {
  constructor() {
    super();
    this.state = {
      n: 0,
      m: 0,
      user: {
        name: "xxx",
        age: 18
      }
    };
  }
  changeUser() {
    this.setState({
      // m 和 n 不会被置空
      user: {
        name: "yyy"
        // 第二层的 age 被置空 undefined
      }
    });
  }
  render() {
    return (
      <div className="Son">
        儿子 n: {this.state.n}
        <button onClick={() => this.addN()}>n+1</button>
        m: {this.state.m}
        <button onClick={() => this.addM()}>m+1</button>
        <hr />
        <div>user.name: {this.state.user.name}</div>
        <div>user.age: {this.state.user.age}</div>
        <button onClick={() => this.changeUser()}>change user</button>
        <GrandSon />
      </div>
    );
  }
}

可以使用 Object.assign 拷贝一次第二层对象

 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
class Son extends React.Component {
  constructor() {
    super();
    this.state = {
      n: 0,
      m: 0,
      user: {
        name: "xxx",
        age: 18
      }
    };
  }
  addN() {
    this.setState({ n: this.state.n + 1 });
    // 对象第一层属性 m 不会被覆盖为 undefined
  }
  addM() {
    this.setState({ m: this.state.m + 1 });
    // 对象第一层属性 n 不会被覆盖为 undefined
  }
  changeUser() {
    const user = Object.assign({}, this.state.user); // 手动复制一遍之前的所有属性
    user.name = "jack";
    this.setState({
      // m 和 n 不会被置空
      user: user
    });
  }
  render() {
    return (
      <div className="Son">
        儿子 n: {this.state.n}
        <button onClick={() => this.addN()}>n+1</button>
        m: {this.state.m}
        <button onClick={() => this.addM()}>m+1</button>
        <hr />
        <div>user.name: {this.state.user.name}</div>
        <div>user.age: {this.state.user.age}</div>
        <button onClick={() => this.changeUser()}>change user</button>
        <Grandson />
      </div>
    );
  }
}


也可以使用 ... 展开运算符 拷贝一次第二层对象

 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
class Son extends React.Component {
  constructor() {
    super();
    this.state = {
      n: 0,
      m: 0,
      user: {
        name: "xxx",
        age: 18
      }
    };
  }
  addN() {
    this.setState({ n: this.state.n + 1 });
  }
  addM() {
    this.setState({ m: this.state.m + 1 });
  }
  changeUser() {
    this.setState({
      // 对象第一层属性 m 和 n 不会被置空
      user: {
        ...this.state.user, // 手动复制一遍之前的所有属性
        name: "jack"
        // 复制后第二层属性 age 不被置空
      }
    });
  }
  render() {
    return (
      <div className="Son">
        儿子 n: {this.state.n}
        <button onClick={() => this.addN()}>n+1</button>
        m: {this.state.m}
        <button onClick={() => this.addM()}>m+1</button>
        <hr />
        <div>user.name: {this.state.user.name}</div>
        <div>user.age: {this.state.user.age}</div>
        <button onClick={() => this.changeUser()}>change user</button>
        <Grandson />
      </div>
    );
  }
}

复杂 state 小结

  • 类组件的 setState 会自动合并第一层属性
    • 但是类组件不会合并第二层属性,举例
    • 可以使用 Object.assign举例
    • 可以使用 ... 展开运算符,举例
    • 更深层数据仍然不会自动合并,除非使用深拷贝
  • 函数组件的 setX 则完全不会合并

对比Vue

  • Vue对数据做了属性劫持操作
    • Vue2使用Object.defineProperty给所有嵌套属性添加gettersetter,初始开销大,非惰性监听,无法直接在初始化后,再添加或删除的属性,需要另外的API
    • Vue3使用Proxy代理原对象,转换所有嵌套属性,可以在真正用到深层数据的时候再做响应式(惰性响应式),改善 初始化性能 和 内存消耗
      • const state = reactive({ count: 0 })
  • Vue3 中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。
  • 只有代理对象是响应式的,更改原始对象不会触发更新,Vue仅使用你声明对象的代理版本

参考


9. React 事件绑定的各种写法

注意大小写 onClickonKeyPress

类组件事件绑定

  • React 事件绑定<button onClick={this.addN}> n + 1 </button>
  • 用户点击按钮,React会做什么
    • a. (×)会执行 this.addN()
    • b. (×)会执行 button.onClick
    • c. (√)会执行 button.onClick.call(null, event)
      • 其中 onClickthis === null
      • 在浏览器中这里的 this 会被 window 补位
      • 导致 <button onClick={this.addN}> n + 1 </button> 里的 this 指向 window
  • 所以 React 事件绑定 不该使用 <button onClick={this.addN}> n + 1 </button> 会让thiswindow
  • 参考 this 教程

对比四种写法

  1. <button onClick={ () => this.addN() }> n + 1 </button> 传一个箭头函数包裹方法,返回函数 (最稳妥的写法,没有任何问题)
  • 因为箭头函数无法改变内部 this 指向
  1. <button onClick={ this.addN }> n + 1 </button> 存在问题:这样会使 this.addN 方法里的 this 变成 windowundefiend(外部 this 仍是正常的)
  • addN() {this.setState({n: this.state.n + 1}) }里面的this指向丢失,变为window
  1. <button onClick={ this.addN.bind(this) }> n + 1 </button> 可以这样写,它返回一个 绑定了当前this 的新函数,但较麻烦
  2. 给箭头函数取个名字,写成 <button onClick={ this._addN }> n + 1 </button> 然后添加实例上的方法 this._addN = () => { this.addN() }(在constructor中声明)
  • 多了一个变量名,不够优雅
  1. 可合并写在constructor方法中,减少一个变量名
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//...
constructor() {
  super();
  this.addN = () => {
    this.setState( {n: this.state.n + 1} )
  }
}
render() {
  return (<button onClick={this.addN}> n + 1 </button>)
}
  • 但这样写不如直接在原型上声明 addN 为箭头函数 结构简洁清晰
  • React 会内部做判断处理,无论是否返回函数,都可以
    • this.addN = () => {this.setState( {n: this.state.n + 1} )}
    • this.addN = () => this.setState( {n: this.state.n + 1} )

类组件的事件绑定最终写法

最终写法代码示例

1
2
3
4
5
6
7
8
9
class Son extends React.Component {
  addN = () => {
    this.setState( {n: this.state.n + 1 } )
  }
  // addN = () => this.setState({n: this.state.n + 1}) // 也可
  render() {
    return (<button onClick={this.addN}> n + 1 </button>)
  }
}
  • class内部直接声明变量为箭头函数:addN = () => {} 相当于 constructor 中的 this.addN
  • 使用时直接写 <button onClick={this.addN}> n + 1 </button>

两种addN写法的区别

1
2
3
4
class Son extends React.Component {
  addN = () => {this.setState({n: this.state.n + 1})}
  // addN() {this.setState({n: this.state.n + 1})}
}

等价写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Son extends React.Component {
  // 等价写法一
  addN = () => this.setState( {n: this.state.n + 1 } ) // 语法糖
  constructor() {
    super()
    // ...
    this.addN = () => this.setState({n: this.state.n + 1})
  } // 两个写法完全等价

  // 等价写法二
  addN() {this.setState({n: this.state.n + 1})}
  addN = function() {this.setState({n: this.state.n + 1})}
  // 两个写法完全等价

  // 等价写法二 改变 this 使用时直接写this.addN会运行报错 Uncaught TypeError: Cannot read properties of undefined (reading 'setState')
}
  • 等价写法一 两种写法在实例对象上,等价写法二在原型上
  • 等价写法二中addNthis 变成 window;等价写法一中addNthis 不会改变

小结

  • 等价写法一是 实例对象 本身的属性,这意味着每个 Son 组件都有自己独立的 addN,如果有两个Son,就有两个addN
  • 等价写法二是 对象的共有属性(即原型上的属性),这意味着所有 Son 组件共用一个 addN
  • 实例上的方法,优先级高于原型上的类方法

代码证明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Person {
  constructor() {
    this.sayHi_instance1 = () => {}
    this.sayHi_instance2 = function() {}
  }
  sayHi_instance3 = () => {} // 实例化对象上的方法
  sayHi_instance4 = function() {} // 实例化对象上的方法

  sayHi_proto() {} // 原型上的方法 Person.prototype
}
const p = new Person()
console.dir(p)

为什么 this 会变/不会变

  • 所有普通函数的this都是参数,由调用决定,所以可变
  • 唯独箭头函数的this不变,因为箭头函数不接受this,由声明时的外部作用域this
  • React的特点:能不做的,我都不做
  • Vue的特点:能帮你做的,都帮你做了

结论

  • 直接使用写法一的addN即可
  • 或者使用函数组件,它完全不用this,以后的趋势也是优先采用函数组件
  • 但维护老项目时,必须看懂类组件事件绑定的正确写法

函数组件事件绑定


10. 复习 this,与两个面试题

this的指向有哪几种情况,简要回答:大概分四种(4 + 1种后面可以补充)

  • 严格模式下 全局环境 普通函数中 undefined
  • 对象中函数,即对象的方法 对象本身
  • call/apply/bind 显示绑定this指向
  • 构造函数 new 绑定生成实例对象
  • 箭头函数 声明时作用域链上一层 this

要判断一个运行中的函数的this绑定,就需要找到这个函数的直接调用位置,并按顺序应用下面这四条规则来判断this的绑定对象

  1. new调用?绑定到新创建的对象。
  2. call或者apply(或者bind)调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。
  • 箭头函数不适用this的四种标准规则,而是根据外层(函数或全局)作用域来决定this
  • this指向优先级:箭头函数 > new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

详细说明this的指向

  • 全局环境下分为
    • 两种宿主环境:
      • 浏览器环境 globalThis 指向window对象
      • node.js环境 globalThis 指向global对象
    • 两种模式:
      • 非严格模式
      • 严格模式( 模块环境class类内部 都是严格模式)
  • 全局环境下 普通函数 隐式调用 中的this
    • 非严格模式 fn this -> globalThiswindow 或者 global
    • 严格模式 fn this -> undefined
    • 函数 调用时 由调用主体决定,谁调用,this就指向谁
      • 全局环境下 不写window 声明变量自动挂载到window下,即window调用方法
  • 函数 显示调用 this
    • fn.call(obj) this -> obj
    • fn.apply(obj) this -> obj
    • fn.bind(obj) this -> obj
  • 构造函数this指向实例化对象instance = new Fn() this -> instance 实例对象
    • class类内部 严格模式 但使用类必须实例化 指向实例对象 this -> instance
  • 箭头函数不会创建自己this 函数内部的this -> 自己作用域链的上一层 this
    • 即取决于 声明时 该箭头函数作用域链的上一层 this
    • 声明在全局环境中 指向window/global
    • 声明在对象方法中 指向对象作用域的上一层 this
  • 对象中的this (所有模式 全局环境下)
    • 注意必须按照 对象.方法() 的形式调用 隐式绑定this为该对象
    • 而不是重新声明变量,引用赋值为 对象.方法,再执行该变量
    • obj.Fn 属性为 普通函数 内部 this -> obj
    • obj.Fn 属性为 箭头函数 内部 this -> 对象外部的this
    • 普通函数 取决于 运行时 的 this指向
      • 定时器回调 会丢失 this 而指向全局
      • 存在多层调用,就近原则:对象属性引用链只有一层或者说 最后一层 在调用位置中起作用
    • 箭头函数 保存下 声明时的 对象外部this指向
      • 定时器回调 会记录 this 仍指向声明时上一层作用域this
    • 隐式丢失this指向
      • 声明变量 指向对象的方法时,会丢失this
      • 传入普通函数作为参数时,会丢失this
        • 参数传递其实就是一种隐式赋值,传入函数时也会被隐式赋值
        • 异步方法通常需要传递回调函数,传普通函数,会丢失this指向
      • 除非使用箭头函数作为参数
  • 严谨的代码中会用call/apply/bind显示地指定this指向

this是什么?

  • thisfn.call/apply/bind的第一个参数
  • 调用fn()就是调用fn.call(this)
  • 所有函数调用fn()都可以改写为fn.call/apply(undefined)的形式,可以显式地看出this
    • 浏览器/Node.js (非严格模式或模块环境) 宿主环境下,函数调用中的this会被改成globalThis (分别指向windowglobal 对象)
  • 对象方法调用obj.move()可以改写成obj.move.call(obj)
    • 其中move只是对象的属性,键值,指向内存中一块存储函数的地址(此函数不属于对象)
    • 另一个例子obj.child.say()可改写成obj.child.say.call(obj.child)
    • 其它函数参数续写在后面
  • 开启严格模式,在函数内部的首行写'use strict',则this指向不会被更改,值为undefined

辨析题:为什么箭头函数不能用作构造函数

  • 没有自己的 this 上下文
  • 不绑定 arguments
  • 箭头函数不能用作构造器 和 new一起使用会抛出错误
  • 箭头函数没有prototype属性
  • 用作构造函数 直接报错

总结,设定在严格模式下

this \ 运行时条件 全局环境 上下文对象obj.fn调用 显示调用call/apply/bind 构造函数new
普通函数this undefined obj 指定的this 指向实例对象
箭头函数this *globaThis *globaThis *globaThis(忽略指定的this 报错
  • 箭头函数this为定义时确定,更加符合预期
    • *globaThis 声明时外部作用域中this
  • 普通函数this为运行时确定,会根据运行时改变

参考

第一题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var a = {
  name: "里面的name",
  sayName: function() {
    console.log(`this.name = ${this.name}`)
  }
}

var name = "外面的name"

function sayName() {
  var sss = a.sayName
  sss() // this.name
  a.sayName() // this.name
  ;(a.sayName)() // this.name
  ;(b = a.sayName)() // this.name
}

sayName()
  • 必须使用转换代码来改写程序
    • 将所有的函数调用改为 fn.call() 的形式

改写代码

  • 改顺序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function sayName() {
  var sss = a.sayName
  sss() // this.name
  a.sayName() // this.name
  ;(a.sayName)() // this.name
  ;(b = a.sayName)() // this.name
}
var a
var name
a = {
  name: "里面的name",
  sayName: function() {
    console.log(`this.name = ${this.name}`)
  }
}
name = "外面的name"
sayName()
  • 改成 .call 的形式
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function sayName() {
  var sss = a.sayName
  sss.call(window) // this.name
  a.sayName.call(a) // this.name
  ;(a.sayName).call(a) // this.name
  ;(b = a.sayName).call(window) // this.name
}
var a
var name
a = {
  name: "里面的name",
  sayName: function() {
    console.log(`this.name = ${this.name}`)
  }
}
name = "外面的name"
sayName.call(window)

第二题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var length = 10
function fn() {
  console.log(this.length)
}

var obj = {
  length: 5,
  method: function(fn) {
    fn()
    arguments[0]()
  }
}
obj.method(fn, 1)

改写代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var length
var obj
length = 10
obj = {
  length: 5,
  method: function(fn) {
    fn.call(window)
    // 错误 arguments[0].call(arguments[0])
    arguments[0].call(arguments) // arguments.length 即 [fn, 1].length
  }
}

function fn() {
  console.log(this.length)
}

// 错误 obj.method.call(obj.method, fn, 1) // arguments = [fn, 1]
obj.method.call(obj, fn, 1) // arguments = [fn, 1]

参考


11. 总结

关于React的知识点

  • 两种方式引入 ReactReactDOM
  • React.createElement('div' | 函数 | 类)
  • 类组件、函数组件如何获取外部数据props
  • 类组件、函数组件如何获取外部数据state
  • 类组件如何绑定事件,this很麻烦,记忆正确写法
  • 不想记忆this的写法,可以直接使用函数组件,没有this问题,一律用参数和变量
  • React特点:尽量使用JS本身语法特点,能不帮你做,就不做

其他知识点

  • Vue的特点:能帮你做的, 都帮你做
  • this的复习

React V.S. Vue

  • 相同点
    • 都是对视图的封装
      • React使用类和函数表示一个组件
      • Vue通过构造选项构造一个组件(也可用compostion api
    • 通过编译,都提供了createElementXML简写
      • React提供JSX/TSX语法
      • Vue提供 模板语法(语法巨多)、装饰器类组件(实验或弃用)和 JSX/TSX 语法
  • 不同点
    • React是把HTML放在JS里写,即 HTML in JS
    • VueJS 放在 HTML,即 JS in HTML

12. 课后习题:React 组件

用 React 类组件写出一个 n+1 的 demo,要求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";

class App extends React.Component {
  constructor() {
    super();
    this.state = { n: 0 };
  }
  add() {
    this.setState({ n: this.state.n + 1 });
  }
  render() {
    return (
      <div className="App">
        n: {this.state.n}
        <button onClick={() => this.add()}>+1</button>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

用 React 函数组件写出一个 n+1 的 demo,要求:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0)
  return (
    <div className="App">
      n: {n}
      <button onClick={()=>setN(n+1)}>+1</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Reactthis指向丢失的问题

首先搭建一个环境

1
npx create-react-app hello-react

index.js文件中编写一个基础代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class App extends React.Component {
  constructor(props) {
    super(props)
  }

  handleClick(e) {
    console.log('click', this) // this 值为 undefined
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click Me
      </button>
    )
  }
}

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementByid('root')
)
  • handleClick中的this指向预期为 App 组件的实例
  • 但实际指向 undefined,发生了this指向丢失

加断点来调试

 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
class App extends React.Component {
  constructor(props) {
    super(props)
  }

  handleClick(e) {
    // debugger
    console.log('click', this) // this 值为 undefined
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click Me
      </button>
    )
  }

  // JSX => VDom => diff => render抽象 => renderDom
  // 模拟最终产物
  renderDom() {
    const btn = document.createElement('button')
    const btn = document.createTextNode('Click Me Dom')
    function createCallback(fn) {
      const context = undefined
      return () => fn.apply(context) // 忽略参数
    }
    btn.addEventListener('click', createCallback(this.handleClick))
    document.getElementById('root2').appendChild(btn)
    return
  }

}
// ...
const app = new App()
app.renderDom
  • 绑定this指向的方法
    • .call(app)
    • app.handleClick
  • ReactDOMcallCallback() 中调用 func.apply(context, funcArgs)
    • context里的指向为空 null
  • btn.addEventListener('click', createCallback(this.handleClick))套了一层createCallback
    • () => fn.apply(context)

破解之道1:在构造函数上bind

  • bind改写constructor中实例的方法
  • 实例上的方法,优先级高 于原型上的类方法,将原型上的类方法绑定实例对象后返回新的函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class App extends React.Component {
  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this) // 实例方法,优先级高于原型上的类方法,将原型上的类方法绑定实例对象后返回新的函数
  }
  // 原型上的类方法
  handleClick(e) {
    // debugger
    console.log('click', this) // this 值为 undefined
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click Me
      </button>
    )
  }
  //...
}

破解之道2:在render方法上bind

  • 在使用handleClick时用bind绑定this
  • render方法的作用域也是指向实例对象的
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//...
render() {
  return (
    <button onClick={this.handleClick.bind(this)}>Click Me</button>
  )
}

// 模拟最终产物
renderDom() {
  const btn = document.createElement('button')
  const btn = document.createTextNode('Click Me Dom')
  function createCallback(fn) {
    const context = undefined
    return () => fn.apply(context) // 忽略参数
  }
  btn.addEventListener('click', createCallback(this.handleClick.bind(this)))
  document.getElementById('root2').appendChild(btn)
  return
}

破解之道3:handle函数使用箭头函数

  • 利用词法作用域,箭头函数
 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
// ...
// 实例上的方法
  handleClick = (e) => {
    console.log('click', this) // 外层的词法作用域,即指向app实例对象
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click Me
      </button>
    )
  }

// 模拟最终产物
renderDom() {
  const btn = document.createElement('button')
  const btn = document.createTextNode('Click Me Dom')
  function createCallback(fn) {
    const context = undefined
    return () => fn.apply(context) // 忽略参数
  }
  btn.addEventListener('click', createCallback(this.handleClick))
  document.getElementById('root2').appendChild(btn)
  return
}

破解之道4:在render方法中绑定事件函数使用箭头函数

 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
// ...
// 实例上的方法
  handleClick(e) {
    console.log('click', this) // 外层的词法作用域,即指向app实例对象
  }

  render() {
    return (
      <button onClick={() => this.handleClick()}>
        Click Me
      </button>
    )
  }

// 模拟最终产物
renderDom() {
  const btn = document.createElement('button')
  const btn = document.createTextNode('Click Me Dom')
  function createCallback(fn) {
    const context = undefined
    return () => fn.apply(context) // 忽略参数
  }
  btn.addEventListener('click', createCallback(() => this.handleClick()))
  document.getElementById('root2').appendChild(btn)
  return
}
  • 都可以
    • <button onClick={() => this.handleClick()}>
    • <button onClick={() => {this.handleClick()}}>

Reactthis丢失问题,丢失的原因和解决之道涉及了this指向



参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名