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. 函数组件 ⇧
一、类组件
|
|
extends React.Component
是固定写法- 注意:使用外部数据
{this.props.name}
- 从
this
中获取props
- 从
- 使用组件
<Welcome name="xxx" />
- 当做标签使用
- 在标签中传属性,自动挂载到
props
上,以key: value
的形式
二、函数组件 (更推荐)
|
|
- 注意:使用外部数据
{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
标签 ⇧
|
|
如果传入一个 类,则会在类前面加上
new
操作符 ⇧
- 这会导致执行
constructor
- 获取一个组件 对象
- 然后调用对象的
render
方法,获取其返回值,代替原来组件标签的位置
|
|
如果传入一个 函数,则会调用该函数,并 获取其返回值,代替原来组件标签的位置 ⇧
|
|
3. 小试牛刀,动手尝试两种组件 ⇧
|
|
代码示例:链接
- 入口:
App
(ReactDOM.render(<App />, rootElement)
)调用函数组件App
(React
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>
- 根组件 包含
|
|
- 子组件,使用类组件
- 在
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
),将组件外部属性挂载到第一个参数对象上
- 接收参数(一般命名为
- 外部数据只关心 读,不可写(符合单向数据流原则)
|
|
5. 类组件 v.s. 函数组件使用 state
⇧
添加
state
(内部数据类似于Vue
的data
) ⇧
- 代码示例:链接
- 关注数据时的三个条件:
- 初始化 内部数据
- 读取 内部数据
- 写入 内部数据
- 类组件
- 在
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
返回
- 语法含义
- 内部数据关心:
- 初始化
- 读
- 写
|
|
类组件中 不推荐 的 写入方法
- 先
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
过时的闭包问题
|
|
props
v.s.state
类组件 v.s.函数组件
- 分别和
Vue
中的props
与data
对应 - 函数组件的写法更简洁、不用考虑
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
为了其他考量,也引入了虚拟DOM
和DOM diff
Vue
劫持数据,修改数据会直接映射到UI上,更易理解
8. 复杂 state
(多个数据/深层级)怎么处理 ⇧
- 公共代码
|
|
类组件里有 n
和 m
⇧
|
|
- 类组件中
setState
,如果只对一部分进行修改,其他部分自动沿用上一次的值,而不会被undefined
覆盖- 可以了理解为类组件中,会自动合并未修改的数据,而用手动写
this.setState( {...this.state, n: this.state.n + 1} )
- 可以了理解为类组件中,会自动合并未修改的数据,而用手动写
- 可是,这种合并只会合并第一层数据
- 代码示例
函数组件里有 n
和 m
⇧
|
|
函数组件另一种 不推荐的写法 ⇧
你会发现
m
被置空 ⇧
|
|
- 函数组件的
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>
- 代码示例
类组件不会合并第二层属性 ⇧
|
|
可以使用 Object.assign
拷贝一次第二层对象 ⇧
|
|
也可以使用 ...
展开运算符 拷贝一次第二层对象 ⇧
|
|
复杂 state
小结
- 类组件的
setState
会自动合并第一层属性 - 函数组件的
setX
则完全不会合并
对比
Vue
Vue
对数据做了属性劫持操作Vue2
使用Object.defineProperty
给所有嵌套属性添加getter
和setter
,初始开销大,非惰性监听,无法直接在初始化后,再添加或删除的属性,需要另外的API
Vue3
使用Proxy
代理原对象,转换所有嵌套属性,可以在真正用到深层数据的时候再做响应式(惰性响应式),改善 初始化性能 和 内存消耗const state = reactive({ count: 0 })
- 在
Vue3
中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。 - 只有代理对象是响应式的,更改原始对象不会触发更新,
Vue
仅使用你声明对象的代理版本
参考
9. React
事件绑定的各种写法 ⇧
注意大小写
onClick
、onKeyPress
类组件事件绑定 ⇧
React
事件绑定<button onClick={this.addN}> n + 1 </button>
- 用户点击按钮,
React
会做什么- a.
(×)
会执行this.addN()
- b.
(×)
会执行button.onClick
- c.
(√)
会执行button.onClick.call(null, event)
- 其中
onClick
的this === null
- 在浏览器中这里的
this
会被window
补位 - 导致
<button onClick={this.addN}> n + 1 </button>
里的this
指向window
- 其中
- a.
- 所以
React
事件绑定 不该使用<button onClick={this.addN}> n + 1 </button>
会让this
变window
- 参考 this 教程
对比四种写法
<button onClick={ () => this.addN() }> n + 1 </button>
传一个箭头函数包裹方法,返回函数 (最稳妥的写法,没有任何问题)
- 因为箭头函数无法改变内部
this
指向
<button onClick={ this.addN }> n + 1 </button>
存在问题:这样会使this.addN
方法里的this
变成window
或undefiend
(外部this
仍是正常的)
addN() {this.setState({n: this.state.n + 1}) }
里面的this
指向丢失,变为window
<button onClick={ this.addN.bind(this) }> n + 1 </button>
可以这样写,它返回一个 绑定了当前this
的新函数,但较麻烦- 给箭头函数取个名字,写成
<button onClick={ this._addN }> n + 1 </button>
然后添加实例上的方法this._addN = () => { this.addN() }
(在constructor
中声明)
- 多了一个变量名,不够优雅
- 可合并写在
constructor
方法中,减少一个变量名
|
|
- 但这样写不如直接在原型上声明
addN
为箭头函数 结构简洁清晰 React
会内部做判断处理,无论是否返回函数,都可以this.addN = () => {this.setState( {n: this.state.n + 1} )}
this.addN = () => this.setState( {n: this.state.n + 1} )
类组件的事件绑定最终写法 ⇧
|
|
- 在
class
内部直接声明变量为箭头函数:addN = () => {}
相当于constructor
中的this.addN
- 使用时直接写
<button onClick={this.addN}> n + 1 </button>
两种
addN
写法的区别 ⇧
|
|
等价写法
|
|
- 等价写法一 两种写法在实例对象上,等价写法二在原型上
- 等价写法二中
addN
的this
变成window
;等价写法一中addN
的this
不会改变
小结 ⇧
- 等价写法一是 实例对象 本身的属性,这意味着每个
Son
组件都有自己独立的addN
,如果有两个Son
,就有两个addN
- 等价写法二是 对象的共有属性(即原型上的属性),这意味着所有
Son
组件共用一个addN
- 实例上的方法,优先级高于原型上的类方法
代码证明:
|
|
为什么
this
会变/不会变
- 所有普通函数的
this
都是参数,由调用决定,所以可变 - 唯独箭头函数的
this
不变,因为箭头函数不接受this
,由声明时的外部作用域this
React
的特点:能不做的,我都不做Vue
的特点:能帮你做的,都帮你做了
结论
- 直接使用写法一的
addN
即可 - 或者使用函数组件,它完全不用
this
,以后的趋势也是优先采用函数组件 - 但维护老项目时,必须看懂类组件事件绑定的正确写法
函数组件事件绑定 ⇧
|
|
10. 复习 this
,与两个面试题 ⇧
this的指向有哪几种情况,简要回答:大概分四种(
4 + 1
种后面可以补充) ⇧
- 严格模式下 全局环境 普通函数中
undefined
- 对象中函数,即对象的方法 对象本身
- 用
call/apply/bind
显示绑定this
指向 - 构造函数
new
绑定生成实例对象 - 箭头函数 声明时作用域链上一层
this
要判断一个运行中的函数的this绑定,就需要找到这个函数的直接调用位置,并按顺序应用下面这四条规则来判断this的绑定对象 ⇧
- 由
new
调用?绑定到新创建的对象。 - 由
call
或者apply
(或者bind
)调用?绑定到指定的对象。 - 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到
undefined
,否则绑定到全局对象。
- 箭头函数不适用this的四种标准规则,而是根据外层(函数或全局)作用域来决定this
this
指向优先级:箭头函数 > new绑定 > 显示绑定 > 隐式绑定 > 默认绑定
详细说明
this
的指向 ⇧
- 全局环境下分为
- 两种宿主环境:
- 浏览器环境
globalThis
指向window对象 node.js
环境globalThis
指向global对象
- 浏览器环境
- 两种模式:
- 非严格模式
- 严格模式( 模块环境 和 class类内部 都是严格模式)
- 两种宿主环境:
- 全局环境下 普通函数 隐式调用 中的
this
- 非严格模式
fn this -> globalThis
为window
或者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 -> objobj.Fn
属性为 箭头函数 内部 this -> 对象外部的this- 普通函数 取决于 运行时 的 this指向
- 定时器回调 会丢失 this 而指向全局
- 存在多层调用,就近原则:对象属性引用链只有一层或者说 最后一层 在调用位置中起作用
- 箭头函数 保存下 声明时的 对象外部this指向
- 定时器回调 会记录 this 仍指向声明时上一层作用域this
- 隐式丢失
this
指向- 声明变量 指向对象的方法时,会丢失this
- 传入普通函数作为参数时,会丢失this
- 参数传递其实就是一种隐式赋值,传入函数时也会被隐式赋值
- 异步方法通常需要传递回调函数,传普通函数,会丢失this指向
- 除非使用箭头函数作为参数
- 注意必须按照
- 严谨的代码中会用
call/apply/bind
显示地指定this指向
this是什么? ⇧
this
是fn.call/apply/bind
的第一个参数- 调用
fn()
就是调用fn.call(this)
- 所有函数调用
fn()
都可以改写为fn.call/apply(undefined)
的形式,可以显式地看出this
- 在 浏览器/Node.js (非严格模式或模块环境) 宿主环境下,函数调用中的
this
会被改成globalThis
(分别指向window
和global
对象)
- 在 浏览器/Node.js (非严格模式或模块环境) 宿主环境下,函数调用中的
- 对象方法调用
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
为运行时确定,会根据运行时改变
参考
- 彻底理解JavaScript中的this
- Javascript.info 对象方法,“this”
- MDN 箭头函数
- Javascript.info深入理解箭头函数
- javascript必知必会之this关键字及scope
第一题
|
|
- 必须使用转换代码来改写程序
- 将所有的函数调用改为
fn.call()
的形式
- 将所有的函数调用改为
改写代码
- 改顺序
|
|
- 改成
.call
的形式
|
|
第二题
|
|
改写代码
|
|
参考
- this 的值到底是什么?一次说清楚 给this定性
- 你怎么还没搞懂 this? 如何确定this
- JS 里为什么会有 this 为什么要设计this
11. 总结 ⇧
关于
React
的知识点
- 两种方式引入
React
和ReactDOM
React.createElement('div' | 函数 | 类)
- 类组件、函数组件如何获取外部数据
props
- 类组件、函数组件如何获取外部数据
state
- 类组件如何绑定事件,
this
很麻烦,记忆正确写法 - 不想记忆
this
的写法,可以直接使用函数组件,没有this
问题,一律用参数和变量 React
特点:尽量使用JS
本身语法特点,能不帮你做,就不做
其他知识点
Vue
的特点:能帮你做的, 都帮你做this
的复习
React
V.S.Vue
- 相同点
- 都是对视图的封装
React
使用类和函数表示一个组件Vue
通过构造选项构造一个组件(也可用compostion api
)
- 通过编译,都提供了
createElement
的XML
简写React
提供JSX/TSX
语法Vue
提供 模板语法(语法巨多)、装饰器类组件(实验或弃用)和JSX/TSX
语法
- 都是对视图的封装
- 不同点
React
是把HTML
放在JS
里写,即HTML in JS
Vue
将JS
放在HTML
,即JS in HTML
12. 课后习题:React
组件 ⇧
用 React 类组件写出一个 n+1 的 demo,要求:
|
|
用 React 函数组件写出一个 n+1 的 demo,要求:
|
|
React
与this
指向丢失的问题 ⇧
首先搭建一个环境
|
|
在
index.js
文件中编写一个基础代码
|
|
handleClick
中的this
指向预期为App
组件的实例- 但实际指向
undefined
,发生了this
指向丢失
加断点来调试
|
|
- 绑定
this
指向的方法.call(app)
app.handleClick
ReactDOM
的callCallback()
中调用func.apply(context, funcArgs)
context
里的指向为空null
btn.addEventListener('click', createCallback(this.handleClick))
套了一层createCallback
() => fn.apply(context)
破解之道1:在构造函数上
bind
- 用
bind
改写constructor
中实例的方法 - 实例上的方法,优先级高 于原型上的类方法,将原型上的类方法绑定实例对象后返回新的函数
|
|
破解之道2:在
render
方法上bind
- 在使用
handleClick
时用bind
绑定this
render
方法的作用域也是指向实例对象的
|
|
破解之道3:
handle
函数使用箭头函数
- 利用词法作用域,箭头函数
|
|
破解之道4:在
render
方法中绑定事件函数使用箭头函数
|
|
- 都可以
<button onClick={() => this.handleClick()}>
<button onClick={() => {this.handleClick()}}>
React
的this
丢失问题,丢失的原因和解决之道涉及了this
指向
参考文章 ⇧
相关文章 ⇧
- React18 createRoot
React 18
,不再支持render
绑定根元素节点,否则降版本到react 17
- React 18 用 createRoot 替换 render,保留 Legacy Root API 平滑升级
- React18 为什么要用 createRoot 取代 render
- React18 我帮一朋友重构了点代码,他直呼牛批,但基操勿六
- 全站然叔 2022年前端面试题汇总
React
与this
指向丢失的问题
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名