React 类组件和函数组件
大纲链接 §
[toc]
1. 元素 v.s. 组件 ⇧
Elementv.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(React17)ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( <App /> );(React18+)
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过时的闭包问题
|
|
propsv.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,初始开销大,非惰性监听,无法直接在初始化后,再添加或删除的属性,需要另外的APIVue3使用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调用方法
- 全局环境下 不写
- 非严格模式
- 函数 显示调用
thisfn.call(obj) this -> objfn.apply(obj) this -> objfn.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的复习
ReactV.S.Vue
- 相同点
- 都是对视图的封装
React使用类和函数表示一个组件Vue通过构造选项构造一个组件(也可用compostion api)
- 通过编译,都提供了
createElement的XML简写React提供JSX/TSX语法Vue提供 模板语法(语法巨多)、装饰器类组件(实验或弃用)和JSX/TSX语法
- 都是对视图的封装
- 不同点
React是把HTML放在JS里写,即HTML in JSVue将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
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名