React遵循组件设计模式,使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据流。React 特性有很多,如:JSX 语法、单向数据绑定、虚拟 DOM、声明式编程、Component等。
1、声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做
2、Component:在 React 中,一切皆为组件。组件可以是一个函数或者是一个类,接受数据输入,处理它并返回在 UI 中呈现的 React 元素
3、JSX基础语法:定义虚拟DOM,不能使用’ ’
,标签中混入JS表达式的时候使用{},样式的类名指定不要使用class,使用className,不能有多个根标签,只能有一个根标签,标签必须闭合等
4、单项数据绑定:React 只支持把数据从 state 上传输到页面,但是无法自动实现数据从页面传输到 state 中进行保存。
我重点解释一些最新的生命周期,因为老版的已经弃用了。
生命周期,可以分成三个阶段:1、创建阶段 2、更新阶段 3、卸载阶段
创建阶段主要分成了以下几个生命周期方法:
1、constructor
:在该方法中,通常的操作为初始化state状态或者在this上挂载方法
2、getDerivedStateFromProps
:
如果state的值在任何时候都依赖于props时才使用此方法,让组件在props变化时来更新state
3、render
:类组件必须实现的方法,用于渲染DOM结构,可以访问组件state与prop属性
4、componentDidMount
:组件挂载到真实DOM节点后执行
更新阶段主要为如下方法:
1、getDerivedStateFromProps
:该方法介绍同上
2、shouldComponentUpdate
:当props或state发生变化时,该方法会在渲染之前被调用,该方法常用于性能优化,如果返回false则不重新渲染组件。
3、render
:介绍如上
4、getSnapshotBeforeUpdate
:该方法会在componentDidUpdate之前被调用,可以用来在组件发生变更前从DOM中捕获一些信息,返回的内容会作为参数传递给componentDidUpdate。
5、componentDidUpdate
:执行时机:组件更新结束后触发
卸载阶段为componentWillUnmount
此方法用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求等
数据状态就是 state,props 理解为从外部传入组件内部的数据。
区别:
1、props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化
2、props 在组件内部是不可修改的,但 state 在组件内部可以进行修改
3、state 是多变的、可以通过调用 setState 来修改
在 React 中,类组件是基于 ES6 的规范实现的,继承 React.Component,因此如果用到 constructor 就必须写 super() 才初始化 this。
这时候,在调用 super() 的时候,我们一般都需要传入 props 作为参数,其实如果不传进去,React 内部也会将porps赋值在组件实例属性中,所以是可以不写的,但在不传递 props 在 super 的情况下,constructor内部调用 this.props 为 undefined,因为在 React 会在类组件构造函数生成实例后再给 this.props 赋值。
需要修改state里面的值的状态需要通过调用setState来改变,从而达到更新组件内部数据的作用,在使用setState更新数据的时候,setState的更新类型分成:异步更新和同步更新。
异步更新:执行完setState之后无法立马拿到最新的state的结果,如果想要立刻获取更新后的值,在第二个参数的回调中更新后会执行。
(在组件生命周期或React合成事件中,setState是异步)
同步更新:在setTimeout或者原生dom事件中,setState是同步
1、React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
2、React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()
无效的原因。
3、React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
4、React 有一套自己的合成事件,如事件名称小驼峰命名,函数没有括号等。
使用一个类组件,在其中给某个组件/元素一个onClick
属性,它现在并会自定绑定其this到当前组件,又因开启严格模式没有自己的this所以显示为undefined
绑定方式有如下几种:
1、render方法中使用bind
在事件函数后使用.bind(this)将this绑定到当前组件中,这种方式在组件每次render渲染的时候,都会重新进行bind的操作,影响性能
2、render方法中使用箭头函数
通过ES6的上下文来将this的指向绑定给当前组件,同样再每一次render的时候都会生成新的方法,影响性能
3、constructor中bind
在constructor中预先bind当前组件,可以避免在render操作中重复绑定
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
4、定义阶段使用箭头函数绑定
跟上述方式3一样,能够避免在render操作中重复绑定,实现也非常的简单
综合上述,方式四是最优的事件绑定方式
在React目前来讲,组件的创建主要分成了两种方式:
1、函数式创建 2、继承 React.Component 创建
1、函数式创建
在React Hooks出来之前,函数式组件可以视为无状态组件,只负责根据传入的props来展示视图,不涉及对state状态的操作
2、继承 React.Component 创建
在react hooks出来之前,有状态的组件只能通过继承React.Component这种形式进行创建
在类创建的方式中通过this.state进行访问,当调用this.setState修改组件的状态时,组价会再次会调用render()方法进行重新渲染
区别:
1、对于一些无状态的组件创建,建议使用函数式创建的方式
2、由于react hooks的出现,函数式组件创建的组件通过使用hooks方法也能使之成为有状态组件,再加上目前推崇函数式编程,所以这里建议都使用函数式的方式来创建组件
1、父组件向子组件传递
React的数据流动为单向,父组件在调用子组件的时候,只需要在子组件标签内传递参数,子组件通过props属性就能接收父组件传递过来的参数
2、子组件向父组件传递
父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
3、兄弟组件之间的通信
兄弟组件之间的传递,父组件作为中间层来实现数据的互通,通过使用父组件传递
4、父组件向后代组件传递
使用context提供了组件之间通讯的一种方式,可以共享数据,其他数据都能读取对应的数据,通过使用React.createContext创建一个context,context创建成功后,其下存在Provider组件用于创建数据源,Consumer组件用于接收数据。
5、非关系组件传递:redux
React 中的 Refs允许我们访问 DOM节点或在 render方法中创建的 React组件实例,如果是渲染组件则返回的是组件实例,如果渲染dom则返回的是具体的dom节点
创建ref的形式有4种:
1、传入字符串:只需要在对应元素或组件中书写ref属性,使用时通过 this.refs.传入的字符串的格式获取对应的元素
2、传入对象:refs通过React.createRef()创建,然后将ref属性添加到React元素中,当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref .字段. current 属性中访问
3、传入函数:
当ref传入为一个函数的时候,在渲染过程中,回调函数参数会传入一个元素对象,然后通过实例将对象进行保存,获取ref对象只需要通过先前存储的对象即可
4、传入hook
通过useRef创建一个ref,整体使用方式与React.createRef一致,可以用在函数组件中
针对两种React组件,其区别主要分成以下几大方向:
1、编写形式:两者最明显的区别在于编写形式的不同,同一种功能的实现可以分别对应类组件和函数组件的编写形式
2、状态管理:在hooks出来之前,函数组件就是无状态组件,不能保管组件的状态,不像类组件中调用setState
。如果想要管理state状态,可以使用useState
3、生命周期:在函数组件中,并不存在生命周期,这是因为这些生命周期钩子都来自于继承的React.Component
。所以,如果用到生命周期,就只能使用类组件
但是函数组件使用useEffect也能够完成替代生命周期的作用
4、调用方式:如果是一个函数组件,调用则是执行函数即可,如果是一个类组件,则需要将组件进行实例化,然后调用实例对象的render方法
**受控组件:**简单来讲,就是受我们控制的组件,组件的状态全程响应外部数据,由state决定,受控组件我们一般需要初始状态和一个状态更新事件函数
**非受控组件:**简单来讲,就是不受我们控制的组件,一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态,当需要时,可以使用ref 查询 DOM并查找其当前值
应用场景:
大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由React组件负责处理
如果选择非受控组件的话,控制能力较弱,表单数据就由DOM本身处理,但更加方便快捷,代码量少如提交时一次性取值
在React中,高阶组件即接受一个或多个组件作为参数并且返回一个组件,本质也就是一个函数,并不是一个组件,高阶组件的这种实现方式,本质上是一个装饰者设计模式
通过对传入的原始组件做一些你想要的操作(比如操作 props,提取 state,给原始组件包裹其他元素等),从而加工出想要的组件t
把通用的逻辑放在高阶组件中,对组件实现一致的处理,从而实现代码的复用
Hook 是 React 16的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性,更方便维护与复用。
最常见的hooks有如下
1、useState:在函数组件中通过useState实现函数内部维护state,参数为state默认的值,返回值是一个数组,第一个值为当前的state,第二个值为更新state的函数
总的来讲,useState 使用起来更为简洁,减少了this指向不明确的情况
2、useEffect:useEffect可以让我们在函数组件中进行一些带有副作用的操作,类似生命周期函数
useEffect执行时机:可以把useEffect看做componentDidMount,componentDidUpdate, componentWillUnmount
这三个函数的组合
useEffect(() => {}) ---> componentDidMount, componentDidUpdate(只有函数)
useEffect(() => {}, []) ---> componentDidMount(第二个参数为数组)
useEffect(() => () => {}) ---> componentWillUnMount(函数套用)
3、useRef():获取DOM元素对象,
4、useCallback():性能优化,缓存函数,使用组件重新渲染时得到相同的函数实例。从而避免嵌套函数的再次渲染
5、useMemo(): 类似于Vue中的计算属性,可以监测某个值的变化,根据变化值计算新值,还可以缓存计算结果
6、useContext():在跨组件层级获取数据时简化获取数据的代码
1、在组件内直接使用
直接在组件中书写css样式,通过style属性直接引入
2、组件中引入css文件
将css单独写在一个css文件中,然后在组件中直接引入
3、组件中引入 .module.css 文件
将css文件作为一个模块引入,这个模块中的所有css,只作用于当前组件。不会影响当前组件的后代组件。这种方式是webpack特工的方案,只需要配置webpack配置文件中modules:true即可
我觉得Redux就类似于vue中的Vuex,redux要求我们把数据都放在 store公共存储空间,
一个组件改变了 store 里的数据内容,其他组件就能感知到 store的变化,再来取数据,从而间接的实现了这些数据传递的功能
过程就是React Components 需要获取一些数据, 然后它就告知 Store 需要获取数据,通过Action Creactor , Store 接收到之后去 Reducer 查一下, Reducer 会告诉 Store 应该给这个组件什么数据
使用过程
1、createStore可以帮助创建 store
2、store.dispatch 帮助派发 action , action 会传递给 store
3、store.getState 这个方法可以帮助获取 store 里边所有的数据内容
4、store.subscrible 方法订阅 store 的改变,只要 store 发生改变, store.subscrible 这个函数接收的这个回调函数就会被执行
中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的
Redux整个工作流程,当action发出之后,reducer立即算出state,整个过程是一个同步的操作。那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件。Redux中,中间件就是放在就是在dispatch过程,在分发action进行拦截处理
有很多优秀的redux中间件,如:
1、redux-thunk:用于异步操作
2、redux-logger:用于日志记录
上述的中间件都需要通过applyMiddlewares进行注册,作用是将所有的中间件组成一个数组,依次执行,然后作为第二个参数传入到createStore中
使用react-redux分成了两大核心:
1、Provider
2、connection
1、在redux中存在一个store用于存储state,如果将这个store存放在顶层元素中,其他组件都被包裹在顶层元素Provider之内,那么所有的组件都能够受到redux的控制,都能够获取到redux中的数据
2、connect方法将store上的getState和 dispatch包装成组件的props然后使用
可以传递两个参数:
react-router等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面,主要说的是react-router-dom的常用API,因为我只用过这个
1、BrowserRouter、HashRouter
Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件
BrowserRouter使用history对象,直接拼接路径,比较美观,适合部署在公网
HashRouter模式使用了location.hash、location.replace,在域名后,先拼接/#,再拼接路径,比较丑,适合部署在内网
2、Route
3、Link、NavLink
通常路径的跳转是使用Link组件,最终会被渲染成a元素,其中属性to代替a标题的href属性。NavLink是在Link基础之上增加了一些样式属性,例如组件被选中时,发生样式变化,则可以设置NavLink的一下属性:
4、redirect
用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中,如下例子
5、switch
swich组件的作用适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配
6、除了一些路由相关的组件之外,react-router还提供一些hooks,如下:
1、useHistory:可以让组件内部直接访问history,无须通过props获取
2、useParams:直接访问Params参数
3、useLocation:返回当前 URL的 location对象
7、参数传递
params参数需要占位,search传递参数则是在跳转的路径中添加了一些query参数,不需要占位,还可以通过在link标签内使用to来传递对象
我理解的动态路由有三种表现形式:
第一个是在路由表中path路径写成/detail/:id这种形式,这样的话符合前面的路径都可以匹配该路由,这是其一
第二种动态路由是通过在路由前置守卫中判断,然后利用router.addRoute实现动态路由添加
第三种是在导航栏那里v-if判断条件控制是否显示,之后前置守卫通过token来控制能否进行跳转,实现动态路由。
主要分成了两种模式:
1、hash 模式:在url后面加上#,如http://127.0.0.1:5500/home/#/page1
2、history 模式:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录
hash模式和history模式对应的组件为:HashRouter和BrowserRouter
实现原理
以hash模式为例子,改变hash值并不会导致浏览器向服务器发送请求,浏览器不发出请求,也就不会刷新页面
hash 值改变,触发全局 window 对象上的 hashchange 事件。所以 hash 模式路由就是利用 hashchange 事件监听 URL 的变化,从而进行 DOM 操作来模拟页面跳转
react-router也是基于这个特性实现路由的跳转
Immutable,不可改变的,在计算机中,即指一旦创建,就不能再被更改的数据,对 Immutable对象的任何修改或添加删除操作都会返回一个新的 Immutable对象,Immutable 实现的原理是 Persistent Data Structure(持久化数据结构):
也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享)
使用Immutable对象最主要的库是immutable.js
在React中应用
使用 Immutable可以给 React 应用带来性能的优化,主要体现在减少渲染的次数
在做react性能优化的时候,为了避免重复渲染,我们会在shouldComponentUpdate()
中做对比,当返回true执行render方法
Immutable通过is方法则可以完成对比,而无需像一样通过深度比较的方式比较
在使用redux过程中也可以结合Immutable,不使用Immutable前修改一个数据需要做一个深拷贝
使用 Immutable 后则不需要
render函数里面可以编写JSX,转化成createElement这种形式,用于生成虚拟DOM,最终转化成真实DOM
在render过程中,React 将新调用的 render函数返回的树与旧版本的树进行比较,这一步是决定如何更新 DOM 的必要步骤,然后进行 diff 比较,更新 DOM树
render的执行时机主要分成了两部分:
在React 中,类组件只要执行了 setState 方法,就一定会触发 render 函数执行,函数组件使用useState更改状态不一定导致重新render
组件的props 改变了,不一定触发 render 函数的执行,但是如果 props 的值来自于父组件或者祖先组件的 state,在这种情况下,父组件或者祖先组件的 state 发生了改变,就会导致子组件的重新渲染
所以,一旦执行了setState就会执行render方法,useState 会判断当前值有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染
react 基于虚拟 DOM 和高效 Diff算法的完美配合,实现了对 DOM最小粒度的更新,大多数情况下,React对 DOM的渲染效率足以我们的业务日常。
复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,避免不必要的渲染则是业务中常见的优化手段之一。
我们了解到render的触发时机,简单来讲就是类组件通过调用setState方法, 就会导致render,父组件一旦发生render渲染,子组件一定也会执行render渲染
从上面可以看到,父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下:
1、shouldComponentUpdate
通过shouldComponentUpdate生命周期函数来比对 state和 props,确定是否要重新渲染
默认情况下返回true表示重新渲染,如果不希望组件重新渲染,返回 false 即可
2、PureComponent
跟shouldComponentUpdate原理基本一致,通过对 props 和 state的浅比较结果来实现
3、React.memo
React.memo用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似。但不同的是, React.memo 只能用于函数组件
JavaScript引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待,如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿
React Fiber 是 Facebook 花费两年余时间对 React 做出的一个重大改变与优化
在react中,主要做了以下的操作:
1、为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务
2、增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行
3、dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber 是对 React核心算法(即调和过程)的重写
从编码角度来看,Fiber是 React内部所定义的一种数据结构,它是 Fiber树结构的节点单位,也就是 React 16 新架构下的虚拟DOM
一个 fiber就是一个 JavaScript对象,包含了元素的信息、该元素的更新操作队列、类型
其渲染流程如下所示:
1、使用React.createElement或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(…) ,Babel帮助我们完成了这个转换的过程。
2、createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象
3、ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM
避免不必要的render来性能优化,主要手段是通过shouldComponentUpdate、PureComponent、React.memo,这三种形式这里就不再复述
常见性能优化常见的手段有如下:
1、避免使用内联函数
如果我们使用内联函数,则每次调用render函数时都会创建一个新的函数实例,我们应该在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调用 render 时就不会创建单独的函数实例
2、使用 React Fragments 避免额外标记
fragement不会向组件引入任何额外标记,但它可以作为父级标签的作用
3、事件绑定方式
在事件绑定方式 (opens new window)中,我们了解到四种事假绑定的方式,在constructor中bind事件与定义阶段使用箭头函数绑定这两种形式只会生成一个方法实例,性能方面会有所改善
4、使用 Immutable
使用 Immutable可以给 React 应用带来性能的优化,主要体现在减少渲染的次数,Immutable通过is方法则可以完成对比,无需通过深度比较的方式比较
5、懒加载组件
从工程方面考虑,webpack存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小
而在react中使用到了Suspense和 lazy组件实现代码拆分功能
6、服务端渲染
采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面
react16引用了错误边界新的概念
错误边界是一种 React 组件,这种组件可以捕获发生在其子组件树任何位置的 JavaScript 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生崩溃的子组件树
错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误
形成错误边界组件的两个条件:
1、使用了 static getDerivedStateFromError()
2、使用了 componentDidCatch()
抛出错误后, 使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息
服务端渲染(SSR)
指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程
其解决的问题主要有两个:
react中,实现SSR主要有两种形式:
1、手动搭建一个 SSR 框架
2、使用成熟的SSR 框架,如 Next.JS
手动搭建:
1、首先通过express启动一个app.js文件,用于监听3000端口的请求,当请求根目录时,返回HTML
2、然后再服务器中编写react代码,在app.js中进行应引用
3、为了让服务器能够识别JSX,这里需要使用webpack对项目进行打包转换,创建一个配置文件webpack.server.js并进行相关配置
4、接着借助react-dom提供了服务端渲染的 renderToString方法,负责把React组件解析成html就可以了