React
文档 http://www.zhufengpeixun.cn/2020/html/62.1.react-basic.html
React当中的元素就是普通的js对象,它描述了界面上元素的样子,react-dom会在render的时候根据对象的描述生成真实的dom结构。
react元素是构成react应用的最小单位。
react中的元素也就是虚拟dom。
react 源码 github.com => 搜索react => packages => react => src => ReactElement.js
react forceUpdate | react flush
e.preventDefault(); // 阻止默认表单提交事件
jsx
JavaScript+xml 是一种把js和html混合书写的一种语法。
JSX其实只是一种语法糖,最终会通过babeljs转译成createElement语法,以下代码等价。
1 | ReactDOM.render( |
this
事件的处理 关于this的处理
一般来说我们希望在回调函数里让this=当前组件
- 1、使用箭头函数 this就会永远指向类的实例
- 2、如果用的是普通函数,this = undefined
- 2.1 可以使用bind
- 2.2 可以使用匿名函数 ()=>funName()
解决this指针的三种方法
- this.add.bind(this); 把add方法里面的this指针绑定为组件实例
- 使用匿名函数 ()=>this.add();
1
2
3
4// 给类的实例增加一个add的属性,而这个属性的this绑死为组件的实例
add = () => {...}
...
onClick={this.add}
ref
ref的用法 + 受控组件、非受控组件
reference = 引用 如果我们希望在代码中获取到React元素渲染到页面上的真实DOM的话
ref用法
1 | this.a = React.createRef(); // {current: null} |
受控和非受控
非受控组件:指DOM元素的值存在于DOM元素内部,这个值跟react是相互独立的,不受react控制,这被称为非受控组件。
受控:设置 value={this….} onChange来改变value的值
生命周期
旧
父组件包裹子组件 父组件 Counter
子组件 SubCounter
当父组件改变数据时,触发时 父子组件生命周期顺序如下:
Counter
shouldComponentUpdateCounter
componentWillUpdateCounter
renderSubCounter
componentWillReceivePropsSubCounter
shouldComponentUpdateSubCounter
componentWillUpdateSubCounter
renderSubCounter
componentDidUpdateCounter
componentDidUpdate
新
如图 props、state、forceUpdate 改变时都会触发
- static getDerivedStateFromProps
1 | // 从属性对象中获取派发的状态,但返回的对象将会成为新的状态对象,如果不改变状态,则可以返回null |
getDerivedStateFromProps 的存在只有一个目的:让组件在props变化时更新state。
- shouldComponentUpdate
1 | shouldComponentUpdate(nextProps, nextState){ |
render
getSnapshotBeforeUpdate
1 | // 在更新前获取DOM的快照 |
更新DOM
componentDidUpdate
1 | componentDidUpdate(prevProps, prevState, prevXXXX){ |
状态
状态提升
多个组件需要共享同一个状态的话,就需要把他们的状态提升到他们共同的父组件中。
setState
1 | this.setState({}); |
修改状态;重新render
setState 可能是异步的
出于性能方面的考虑,React 可以将多次的 setState() 调用合并为一次
1 | constructor(){ |
源码
dirtyComponents
脏组件,就是组件的状态和界面显示不一致
1 | // 源码 |
1 | window.trigger = function(event,method){ |
实现原理和react一样,在react中会把所有的事件都委托给全局统一实现,通过事件源去区分,去调用对应的方法。
通过事件委托来实现的。
在react中,当你要开启一个事件的时候,当你要执行一个回调函数的时候,它会进入到批量更新状态。
1 | window.trigger = function(event,name){ |
上面line12 - line15 ,将多次传入的对象进行合并处理,以产生一个新的最终的 state 对象,这种合并类似于:
1 | const newState = Object.assign( |
然后再将得到的 “newState” 通过调用 setState 方法进行更新,所以,如果多次调用 setState 方法时传入的对象有相同的 key,那么最后一次调用时所传入的对象的那个 key 的值将成为最终的更新值,在最后一次调用前的值都将被覆盖。
优化window.trigger中的方法 事务
- 一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行
- 而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法一组 initialize 及 close 方法称为一个 wrapper
* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ *
1 | class Transaction { |
1、异步更新state,将短时间内的多个setState合并成一个
2、为了解决异步更新导致的问题,增加另一种形式的setState:接受一个函数作为参数,在函数中可以得到前一个状态并返回下一个状态
1 | add = () =>{ |
举例:
1 | class App extends Component { |
探讨前,我们先简单了解下react的事件机制:react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。
那么以上4种方式调用setState(),后面紧接着去取最新的state,按之前讲的异步原理,应该是取不到的。然而,setTimeout中调用以及原生事件中调用的话,是可以立马获取到最新的state的。根本原因在于,setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套。
在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout ,可以在钩子函数中 setTimeout ,也可以在原生事件setTimeout,但是不管是哪个场景下,基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。
小结:
1、setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
2、setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
3、setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。
小测试
1 | class App extends React.Component { |
0 0 2 3
【提问】line11 为什么在使用函数式 setState 进行状态更新后,在后一个里面还是不能通过 this.state.age 拿到最新的值?
【回答】源码这样解释,执行第二个 setState 里面的函数时,由第一个 setState 所产生的最新的 state 并没有合并到 this 对象上面去,所以此时通过 this 获取不到最新的状态,故而拿到的 this.state.val 的值为 0
1 | class App extends React.Component { |
0 0 3 4
1 | class App extends React.Component { |
0 1 3 4
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
1 | let Context = React.createContext(); // 返回一个context实例 |
https://zh-hans.reactjs.org/docs/context.html
从示例 http://react.html.cn/docs/context.html#%E5%8A%A8%E6%80%81-context 可以看出没有被包裹的改变同一个值,没有改变
注意下告诫
http://react.html.cn/docs/context.html#%E5%91%8A%E8%AF%AB
等学了react-redux的 childContextTypes
getChildContext
等
PureComponent
- 当一个组件的props或state变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM,当它们不相同时 React 会更新该 DOM。
- 如果渲染的组件非常多时可以通过覆盖生命周期方法
shouldComponentUpdate
来进行优化 - shouldComponentUpdate 方法会在重新渲染前被触发。其默认实现是返回 true,如果组件不需要更新,可以在shouldComponentUpdate中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作
- PureComponent通过prop和state的浅比较来实现shouldComponentUpdate
1 | import React, { Component } from 'react'; |
App
组件包含2个子组件 Title
组件 和 Counter
组件
当点击 + 时,三个组件都刷新,当父组件的值改变,子组件也render
1 | render App |
问题
- 当改变数字的时候 title 没有改变,
<Title>
组件不应该更新 - 当 + 0,实际数字没有改变,3个组件都不应该更新
优化
所有组件都继承 PureComponent
这个是React原生的 import React, { Component, PureComponent } from 'react'
测试
input中输入 0,3个组件都不render
input中输入 1
1 | render App |
手写PureComponent
浅比较方法
1 | // 浅比较 比较obj1 和 obj2 是否相等,相等返回true,不相等返回false,只比较第一层 |
改成深比较
1 | for(let key of keys1){ |
PureComponent 要注意的问题
当state中
1 | this.state = { |
时,改变state时这样写
1 | this.state.number.count = this.state.number.count + parseInt(this.inputRef.current.value); |
那么,改变input中的值3个组件都不render,因为
1 | if(!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]){ |
这里{count: ...}
这个对象指向的引用地址没有改变
可以这样改
1 | this.state.number.count = this.state.number.count + parseInt(this.inputRef.current.value); |
每次都创建新的number对象,所以能够更新
函数组件的PureComponent
当<Title>
组件变成函数组件时
1 | function Title(props){ |
可以看到每次改变input中的值,都会刷新 render Title Func
解决:
1 | function Title(props){ |
memo的原理
1 | function memo(Func){ |
额外补充
1 | let null1 = null; |
react组件通信
1、属性传递;2、context redux内部也是context实现的。
看下react中文文档 http://react.html.cn/docs/context.html 这里的例子可以看官网的 好好看看官方的这个例子
高阶组件
- 高阶组件就是一个函数,传给它一个组件,它返回一个新的组件
- 高阶组件的作用其实就是为了组件之间的代码复用
示例一
下面有2个逻辑相似的组件 如下
UserNameInput.js
1 | import React, { Component } from 'react'; |
EmailInput.js
1 | import React, { Component } from 'react'; |
这2个组件都是获取一个值然后展示,其中line3 ~ 13 的逻辑可以复用
将公共逻辑部分封装成一个高阶组件
withLocal.js
1 | import React from 'react'; |
那么原来的组件,还改变成如下:
UserNameInput.js
1 | import React, { Component } from 'react'; |
EmailInput.js
1 | import React, { Component } from 'react'; |
可以看到页面上还是能显示之前localStore设置的数据
示例二
升级,从本地获取到英文名字再从服务器端拿取数据显示对应的中文名字
withAjax.js
1 | import React from 'react'; |
withLocal.js
1 | import React from 'react'; |
UserNameInput.js
1 | import React, { Component } from 'react'; |
顺序:withLocal (携带本地的英文名)=> withAjax (从服务器请求数据传递)=> UserNameInput
高阶组件一般就嵌套一层,超过二层就太复杂了。之后学到的hooks能解决这个问题(高阶组件的嵌套问题)
高阶组件应用场景:比如 react-redux、react路由、路由权限
- react 高阶组件是一种react复用代码的封装、抽取方式,高阶组件是一个函数,接收组件并封装
1、基于属性代理:操作组件的props
2、基于反向继承:拦截生命周期、state、渲染过程 - 使用高阶组件
1、@修饰符
2、直接调用函数
https://blog.csdn.net/qq_29590623/article/details/88560805
render props
和高阶组件一样,都是解决逻辑复用问题,传值方式不同,本质没有区别。
一个组件 它的子组件是个函数。
Context.Consumer 实质上就是一个 render props
React.Fragment
片段(fragments) 可以让你将子元素列表添加到一个分组中,并且不会在DOM 中增加额外节点。
http://react.html.cn/docs/fragments.html
插槽Portal
Portals 提供了一种很好的方法,将子节点渲染到父组件 DOM 层次结构之外的 DOM 节点。
1 | ReactDOM.createPortal(child, container) |
第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 片段(fragment)
第二个参数(container)则是一个 DOM 元素
入口文件index.html 增加一个放置插槽的dom元素位置
1 | <div id="modal-root"></div> |
1 | import React,{Component} from 'react'; |
modal.css
1 | .modal{ |
这里的样式可以看一下,flex 、animation
React.lazy
https://zh-hans.reactjs.org/docs/code-splitting.html#reactlazy
错误边界
https://zh-hans.reactjs.org/docs/error-boundaries.html
长列表优化
未完待续
相关文章:
https://juejin.im/post/5b6f1800f265da282d45a79a
https://juejin.im/post/5aca20c96fb9a028d700e1ce
https://zh-hans.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html