React,用于构建用户界面的JavaScript库,只提供UI层面的解决方案
遵循组件设计模式,声明式编程范式和函数式编程概念,以使前端应用程序更高效
使虚拟 DOM
来有效的操作 DOM
,遵循从高阶到低阶组件的单向数据流;
帮助我们将界面分成各个独立的小块,每一个块就是组件,这些组件之间可以组合,嵌套,构成整体页面,
react
类组件使用一个名为render()
的方法或者函数组件 return
,接收输入的数据并返回需要展示的内容.
JSX语法
单向数据绑定
虚拟DOM
声明式编程
Component
声明式编程
声明式编程是一种编程范式,他关注的是你要做什么,而不是如何做
他表达逻辑而不显示的定义步骤,这意味着我们需要根据逻辑的计算来声明要显示的组件
如 实现一个标记的地图
通过命令是创建地图,创建标记,以及在地图上添加的标记的步骤如下
//创建地图 const map =new Map.map(document.getElementById('map')),{ zoom:4, center:{lat,lng} } //创建标记 const marker = new Map.marker({ position:{lat,lng}, title:'Hello Marker' }) //地图上添加标记 marker.setMap(map)
而用react
实现上述功能规则如下:
Component
在 react
中,一切皆为组件.通常将应用程序的整个逻辑分解为小的单个部分.我们将每个单独的部分称为组件
组件可以是一个函数或者是一个类,接收数据输入,处理它并返回在 UI
中呈现的 React
元素
一个组件该有的特点如下:
可组合:每个组件易于和其他组件一起使用,或者嵌套在另一个组件内部
可重用:每个组件都具有独立功能的,它可以被使用在多个UI场景
可维护:每个小的组件仅仅包含自身的逻辑,更容易被理解和维护
高效灵活
声明式的设计,简单使用
组件式开发,提高代码复用率
单向响应的数据流会比双向绑定的更安全,速度更快
Real DOM ,真实DOM,意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个节点都是一个真实 DOM
结构
Virttual DOM, 本质上是以 JavaScript
对象形式存在的对 DOM
的描述(本质上是JS对象,是真实DOM的抽象)
创建虚拟 DOM
目的就是为了更好将虚拟的节点渲染到页面视图中,虚拟 DOM
对象的节点与真实 DOM
的属性一一照应
在 react中, JSX
是其一大特征,可以让你在 JS
中通过使用 XML
的方式去直接声明界面的 DOM
结构
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(, document.getElementById('root') );
ReactDOM.render()
用于将你创建好的虚拟DOM 节点插入到某个真实节点上,并渲染到页面上
JSX
实际是一个语法糖,在使用过程中会被 Babel
进行编译转化成 JS
代码, JSX
就是为了简化直接调用 ReactDOM.render()
方法:
第一个参数是标签名
第二个参数是个对象,里面存着标签的一些属性
第三个参数是节点中的文本
JSX
通过 Babel
的方式转化成 ReactDOM.render()
执行,返回值是一个对象,也就是虚拟 DOM
两者的区别如下:
虚拟DOM 不会进行重排与重绘操作,而 真实DOM 会频繁重排与重绘
虚拟DOM 的总损耗是''虚拟DOM 增删改查+真实DOM 差异增删改+重排与重绘'',真实DOM 的总耗损是''真实DOM 完全增删改+重排与重绘''
真实DOM 的优势:
易用
缺点
效率低,解析速度慢,内存占用量过高
性能差:频繁操作真实DOM ,易于导致重排与重绘
虚拟DOM 的优势:
简单方便:如果使用手动操作真实DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
性能方面:使用虚拟DOM,能够有效避免真实DOM 树 频繁更新,减少多次引起重绘与回流,提高性能
跨平台:React 借助虚拟DOM,带来了跨平台的能力,一套代码多段运行
缺点:
在一些性能要求极高的应用中虚拟DOM 无法进行针对性的机制优化
首次渲染大量DOM时,由于多了一层虚拟DOM 的计算,速度比正常稍慢
一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态即是 state
,一般在 constructor
中初始化
当需要修改里面的值的状态需要通过调用 setState
来改变,从而达到更新组件内部数据的作用,并且重新调用组件 render
方法,
setState
还可以接收第二个参数,他是一个函数,会在 setState
调用完成并且组件开始重新渲染时 被调用,可以用来监听渲染是否完成
react
的核心思想就是组件思想,页面会被切分成一些独立的,可复用的组件,组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是 props
,所以可以把props
理解成从外部传入组件内部的数据
react
具有单项数据流的特性,所以他的主要作用是从父组件向子组件中传递数据
props
除了可以传字符串,数组,还可以传递对象,数组甚至是回调函数
在子组件中, props
在内部不可变的,如果想要改变它看,只能通过外部组件传入新的 props
来重新渲染子组件,否则子组件的 props
和展示形式不会改变
相同点:
两者都是JS对象
两者都是用于保存信息
props和state都能触发渲染更新
区别:
props 是外部传递给组件的,而state是在组件内组件自己管理的,一般在constructor中初始化
props在组件内部是不可修改的,不可变,而state在组件内部可以进行修改,可变
没有state的叫做无状态组件,有state的叫做有状态组件
多用props,少用state,也就是多写无状态组件
在 React
中,类组件基于 ES6,所以在constructor中必须使用super
在调用 super 过程,无论是否传入props, React
内部都会将props赋值给组件实例 props属性值
如果只调用super(), 那么 this.props 在super()和构造函数结束之间仍是undefined
在调用super() 方法之前,子类构造函数无法使用this引用,ES6子类也是如此.
将props作为参数传递给super() 调用的主要援用是在子构造函数能够this.props来获取传入的props
一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就是state
当需要修改里面的值的状态需要通过调用setState来改变,从而达到更新组件内部数据的作用
执行setState方法更新state状态,然后执行render 函数,从而导致页面的视图更新
注意: 直接修改state,不会重新渲染组件
setState 有两个参数,第一个参数可以是对象或者一个函数,第二个参数是一个回调函数,用于可以事实的获取到更新之后的数据.
setState在react中是异步的,为了自身性能
当调用setState(),react并不会马上修改state,
而是把这个对象放在一个更新队列里面
稍后才会从队列当中把新的状态提取出来合并到state当中,然后再触发组件更新
注意: 连续调用多次setState 修改同一数据,但是最终之一触发最后一次
setState的第二个参数,可以传递一个回调函数,这个回调函数会在数据更新完,DOM渲染完执行
setState()伪异步 是react刻意为之,为了节省性能 1.修改数据 2.更新视图 react的控制范围内,比如:事件处理函数,生命周期函数...都是异步的 setState如果在react的生命周期中或者是事件处理函数中,表现出来的是异步的 setState如果是在setTimeout/setInterval或者原生事件中,表示出现是同步的
总结:setState 是同步的方法,但是react为了性能优化,所以setState在react的事件中表现出来的是异步的
React
基于浏览器的事件机制自身实现了一套事件机制,包括事件注册,事件的合成,事件冒泡,事件派发等.在 React
中这套事件机制被称之为合成事件
合成事件(SyntheticEvent)
合成事件是React 模拟原生DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
根据 W3C
规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口
如果想要获取原生DOM 事件,可以通过 e.nativeEvent
属性
react事件和原生事件的区别
事件名称命名方式不同
事件处理函数书写不同
虽然 onClick
看似绑定到DOM 元素上,但实际并不会把事件处理代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听
这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数,但组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象
当事件发生时,首先被这个统一的事件监听器处理,然后再映射里找到真正的事件处理函数并调用,这样做简化了事件处理和回收机制,效果也有很大提升
原生事件:子元素DOM 事件监听
原生事件:父元素DOM事件监听
React事件:子元素事件监听
React事件:父元素事件监听
原生事件:document DOM 事件监听
结论:
React 所有事件都挂载在document对象上
当真实DOM 元素触发事件,会冒泡到document对象后,在处理React事件
所有会先执行原生事件,然后处理React事件
最后真正执行document上挂载的事件
所以想要组织不同时间段的冒泡行为,对应使用不同的方法,对应如下:
阻止合成事件间的冒泡,用e.stopPropagetion
阻止合成事件与外层docunment上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation()
阻止合成事件与最外层document上的原生事件上的冒泡,通过判断e.target来避免
document.body.addEventListener('click', e => { if (e.target && e.target.matches('div.code')) { return; } this.setState({ active: false, }); }); }
React 事件机制结论
React上注册的事件最终会绑定到document这个DOM上,而不是Reat组件对应的DOM(减少内存开销就是因为所有的事件都绑定在document上,其他节点上没有绑定事件)
React自身实现了一套事件冒泡机制,所有者也就是为什么我们 event.stopPropagetion()无效的原因
React通过队列的形式,从出发的组件想组件回溯,然后调用他们JSX中定义的callback
React有一套自己的合成事件SyntheicEvent
事件名首字母大写,{} 包裹
为了解决输出this的问题,常见的绑定方式:
将事件处理定义为内联的箭头函数
使用箭头函数来定义事件方法(优,避免多次渲染)
使用bind绑定this指向
使用带有Hooks的函数组件(优)
组件就是把图形,非图形的各种逻辑抽象为一个统一的概念(组件)来实现开发的模式
在react中,一个类,一个函数都可以视为一个组件
组件的优势:
降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历,事件,范围等组件作具体实现
调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移出组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所有逻辑会比分析这个系统要简单
提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
在 React
目前来讲,组件的创建主要分成三种方式
函数式创建
通过React.createClass方法创建
继承React.Component创建
由于 React.createClass
创建的方式过于冗杂,并不建议使用
而像函数式创建和类组件创建的区别主要在于需要创建的组件是否需要为有状态组件:
对于一些无状态的组件创建,建议使用函数式创建的方法
由于 React hook
的出现,函数式组件创建的组件通过使用hook方法也能使之称为有状态组件,再加上目前推崇函数式编程,所有这里建议都使用函数式的方式来创建组件
在考虑组件的选择原则上,能用无状态组件则用无状态组件
组件间通信即指组件通过某种方式来传递信息,访问组件之间的数据
父传子
子传父
兄弟组件通信(将共享状态提升到最近的公共组件中,由公共父组件管理这个状态)
跨组件通信(createContext, Provider提供状态,Consumer接收状态)
非关系组件(Redux)
由于 React
是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生改变时他们使用接收到的新值,而不是去修改已有的值
渲染列表的时候每一个族元素都需要一个唯一的key值
React和vue一样,也存在Diff算法,而元素key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染
因此 key
的值需要为每一个元素赋予一个确定的标识
key的作用主要是为了高效的更新虚拟DOM,提高渲染性能
key属性可以避免数据混乱的情况出现
React实现一套虚拟DOM,使我们不直接操作DOM元素只操作数据,就可以重新渲染页面,而隐藏在背后的原理就是高效的diff算法
当页面数据发生改变时候,Diff算法只会比较同一层级的节点,
如果节点类型不同,直接干掉前面的节点,在创建并插入新的节点,不会再比较这个节点后面的子节点
如果该节点类型相同,则会重新设置该节点属性,从而实现节点更新
使用key给每个节点做一个唯一标识,DIff算法就可以正确识别此节点,''就地更新''找到正确的位置插入新的节点
Refs 在计算机中被称为弹性文件系统
React 中的Refs 提供了一种方式,允许我们访问DOM节点或者在render方法中创建的React元素(组件)
本质为 ReactDOM.render() 返回的组件实例,如果是渲染组件则返回如果渲染dom则返回的是具体的dom节点
创建ref的形式
传入字符串,使用时通过this.refs.传入的字符串的格式获取对应的元素
传入对象,对象是通过React.createRef() 方式创建出来,使用获取到创建的对象中存在current属性就是对应的元素
传入函数,该函数会在DOM 被挂载时进行回调,这个函数会传入一个元素对象,可以自己保存,使用时直接拿到之前保存的元素对象即可
传入hook,hook是通过useRef()方式创建,使用时通过生成hook对象的current属性就是对应的元素
在某些情况下,我们会通过使用refs来更新组件,但这种方式并不推荐,更多情况我们是通过props与state的方式方式进行去重新渲染子元素
过多使用refs, 会使组件的实例或者是DOM 结构暴露,违反组件封装的原则
使用场景:
对DOM 元素的焦点控制,内容选择,控制
对DOM元素的内容设置及媒体播放
对DOM 元素的操作和对组件实例的操作
继承第三方DOM 库
通过使用ES6 类的编写形式去编写组件,该类必须继承React.Component
如果想要访问组件传递过来的参数,可通过this.props 的方式去访问
在组件中必须实现render 方法,在return中返回React对象
通过函数编写的形式去实现一个React组件,是React中定义组件最简单的方式
函数第一个参数为props 用于接收父组件传递过来的参数
针对两种React 组件,其区别主要分别以下几大方向
编写方式
状态管理
类组件通过setState来管理state
函数组件(hooks) 使用useState 生成state 以及管理state的方法
生命周期
类组件有生命周期,
函数组件使用useEffect代替生命周期的作用
调用方式
类组件需要进行实例化,然后调用实例对象的render方法
函数组件直接执行的函数返回结果,性能高
获取渲染的值
props在react中是不可变的所以他永远不会改变,但是this是可变的
类组件通过this.props 获取的是最新的数据
函数组件没有this,所有props中的内容还是之前的数据
函数组件相比如类组件没有this指向以及生命周期的问题
类组件使用的时候需要实例化,函数组件直接执行函数返回的结果,对比函数组件性能高
函数组件语法更短,更简单,这使得它更容易开发,理解,测试
受控组件就是react组件的状态来控制表单元素的值
需要提供两个属性:
绑定value
onChange事件
非受控组件是通过手动操作DOM的方式来控制
大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由 React
组件赋值处理
如果选择非受控组件的话,控制能力较弱,表单数据就由DOM 本身处理,但更加方便快捷,代码量少
高阶组件,至少满足下列一个条件的函数
接受一个或多个函数作为输入
输出一个函数
在React
中,高阶组件即接受一个或多个组件作为参数并且返回一个组件,本质也就是一个函数,并不是一个组件
高阶组件的这种实现方法,本质是一个装饰者设计模式
通过对传入的原始组件 做一些你想要的操作(比如操作props,提取state,给原始组件包裹其他元素等),从而加工出想要的组件
把通用的逻辑放在高阶组件中,对组件实现一致的结果,从而实现代码的复用
所以,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用
但在使用高阶组件的同时,一般遵循一些约定,如下:
props保持一致
你不能在函数式(无状态)组件上使用ref属性,因为它没有实例
不要以任何方式改变原始组件 WrappedComponent
透传不相关props属性给被包裹的组件WrappedComponet
不要再 render( ) 方法中使用高阶组件
使用compose组合高阶组件
这里需要注意的是,高阶组件可以传递所有的props,但是不能传递ref
如果向一个高阶组件添加refs引用,那么ref指向的是最外层容器组件实例的,而不是被包裹的组件,如果需要传递refs的话,则使用React.forwardRef
高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制,日志记录,数据校验,异常处理,统计上报等
Hooks 是React 16.8的新增特性. 它可以让你在不编写class的情况下使用 state 以及其他的React特性
为什么引入Hook,官方给出的动机是解决长时间使用和维护 React 过程中常遇到的问题,例如:
难以重用和共享组件中的与状态相关的逻辑
逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的local state时,每个生命周期函数中可能会包含各种互不相关的逻辑在里面
类组件中的this增加了学习成本,类组件在基于现有工具的优化上存在许多问题
由于业务变动,函数组件不得不该为类组件等等
在此之前函数组件也被称之为无状态组件,只负责渲染的一些工作
因此,现在的函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理
Hook 让我们的函数组件拥有了类组件的特性,例如组件内的状态,生命周期
最常见的hooks:
useState
useEffect
useRef
useContext
使用规则
React Hooks 只能出现在函数组件中和自定义hooks中使用
React Hooks 不能嵌套在if/for/其他函数中
React 是按照Hooks的调用顺序来识别每一个Hooks,如果每次调用的顺序不同,导致React 无法知道是哪一个Hooks
函数组件中通过useState 实现函数内部维护 state,参数为state默认的值,返回值是一个数组,第一个值是当前的state,第二个值为更新state的函数
函数组件和类组件状态的区别
state声明方式:在函数组件中通过useState直接获取,类组件通过constructor构造函数中设置
state读取方式:在函数组件中直接使用变量,类组件通过this.state.count的方式
state更新方式:在函数组件中通过setCount更新,类组件通过this.setState()
总来说useState使用起来更为简洁,减少了this指向不明确的情况
useState-回调参数
useState的参数可以有两种形式
useState(普通的数据)=>useState(0)
useState(回调函数)=>useState(()=>{return初始化})
回调函数的返回值就是状态的初始值
该回调函数只会触发一次
useEffect 可以让我们在函数组件中进行一些带有副作用的操作
主作用:根据数据(state/props)渲染UI
副作用: ajax请求,手动修改DOM,localStorage操作等
useEffect-一个参数
执行时机:该effect会在组件首次挂载完,以及组件更新完后执行
相当于:componentDidMount+componentDidUpdata
useEffect-两个参数,第二个参数为[]
执行时机:只在组件第一次渲染后执行
相当于:componentDidMount
使用场景:
事件绑定
发送ajax请求
useEffect-两个参数,第二个参数为[依赖项]
执行时机:在组件首次挂载完以及依赖项发生更新时候,都会执行
相当于:componentDidMount+依赖项改变时候的componentDidUpdata
推荐: 一个useEffect只处理一个功能,有多个功能时候,使用多次useEffect
注意: 不要对依赖项撒谎
useEffect- 清理副作用
需要清理的副作用:
开启定时器,延时器
注册事件
本地存储
执行时机:
清除函数会在组件卸载时以及下一次副作用回调函数调用的时候执行,用于清除上一次的副作用
如果依赖项为空数组,那么 会在组件卸载时会执行,相当于组件的componentWillUnmount
useEffect(()=>{ //useEffect 内部代码,再一次执行前,会调研+组件销毁时会调用 return ()=>{} },[])
是什么?
返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数( initialValue )。
返回的 ref 对象在组件的整个生命周期内保持不变
一般用于在react中需要定义一个全局的变量,例如定时器的timer
当更新 current 值时并不会 re-render ,这是与 useState 不同的地方
更新 useRef 是 side effect (副作用),所以一般写在 useEffect 或 event handler 里 useRef 类似于类组件的 this
使用场景:在React中进行DOM 操作时,用来获取DOM
//1.导入 import { useRef } from 'react' //2.创建 const aref=useRef(null) //3.绑定 ref={aref} //4.获取 aref.current
重点
需求:由于每次执行,都会重新初始化很多变量,有时,我们需要一个能够贯穿始终的一个变量
效果:一经修改,所有重新执行函数时,拿到的这个值,都是修改后最新的值
useRef=>可以得到一个可变的对象,利用current属性,存值=>获取dom useRef=>特质=>在整个函数渲染更新过程中,贯穿始终
hooks能够更容易解决状态相关的重用的问题
每调用useHook 一次都会生成一份独立的状态
通过自定义hooks 能够更好的封装我们的功能
编写hooks为函数式编程,每个功能都包裹在函数中,整体风格更清爽,更优雅
组件式开发选择合适的css解决方案尤为重要
通常会遵循一下规则:
可以编写局部css,不会随意污染其他组件内的原生
可以编写动态的css,可以获取当前组件的一些状态,根据状态的变化生成不同的css样式
支持所有的css特性:伪类,动画,媒体查询等
编写起来简洁方便,最好符合一贯的css风格特点
常见的css引入方式有以下:
在组件内直接使用
组件中引入.css文件
组件中引入.module.css文件
CSS in JS
在组件内直接使用css 该方式编写方便,容易能够根据状态修改属性,但是大量的演示编写容易导致代码混乱
组件中引入.css文件符合我们日常的编写习惯,但是作用域是全局的,样式之间会层叠
引入.module.scc文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编写
通过css in js 这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式浅谈,定义,修改状态等
当一个组件在显示与消失过程中存在过渡动画,可以很好的增加用户体验
在react 中实现过渡动画效果会有很多种选择,如react-transition-group, react-motion,Animated.以及原生的css都能完成切换动画
主要提供三个重要的组件:
CSSTransition:在前端开发中,结合 CSS 来完成过渡动画效果
SwitchTransition:两个组件显示和隐藏切换时,使用该组件
TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动画
redux就是一个实现组件内状态的集中管理的容器,遵循三大基本原则:
单一数据源
state是只读的
使用纯函数来执行修改
纯函数的特点:
只要有固定的输入,必然有固定的输出(避开 new Date(),Math.random())
纯函数不允许修改参数(引用类型数据,也不能直接修改)
不允许影响到外部作用域的内容 (不允许直接操作全局变量)
没有副作用 (操作dom,发送请求,localStrong)
redux要求我们把数据都放在store公共存储空间
组件需要获取一些数据,先告知store 需要获取数据,也就是action,然后store接收到之后去reducer中查,reducer 告诉store应该给组件对应的数据
三个核心
Action,Reducer,Store
action(动作):是一个JS对象,必须要有type属性,用于组件 dispatch帮忙分发action,action会传递给store
reducer(方案):是一个函数,有两个参数prevState,action
store(仓库):createStore可以帮助创建store,有三个参数: reducer,初始值(可省略),中间件
store.getState这个方法可以帮助获取store里面所有的数据内容
store.subscrible方法订阅store变化,只要store发生改变store.subscrible 这个函数接收的这个回调函数就会被执行
Provider 可以更好的监视store的变化
中间件是介于应用系统和系统软件之间的一类软件 Redux整个工作流程,当action发出之后,reducer立即算出state,整个过程是一个同步操作
那么如果需要支持异步操作,支持错误处理,日志监控,这个过程就可以用上中间件
redux中,中间件就是放在dispatch过程,在分发action进行拦截处理
中间件的本质是一个函数,对store.dispatch方法进行了改造,在发出action和执行reducer这两部之间,添加了其他功能
redux-thunk:用于异步操作
redux-logger:用于日志记录
中间件都需要通过applyMiddlewares进行注册,作用是将所有的中间件组成一个数组,一次执行
react-redux将组件分成:
容器组件:存在逻辑处理
UI组件:只负责显示和交互,内部不处理逻辑,状态由外部控制
通过redux将整个应用状态存储到store中,组件可以派发dispatch action给store
其他组件通过Provider上传递store 包裹住整个根组件,获取state
使用react-redux分成两大核心
Provider
connection
Provider
connection(旧)
connect 方法将store上的getState和dispatch包装成组件的props
useSlecter,useDispatch(新)
useSlecter,useDispatch,获store上的getState和dispatch
路由:组件和路径的映射关系
React-router主要分成几个不同的包
react-router:实现路由的核心功能
react-router-dom:基于react-router,加入了在浏览器运行环境下的一些功能
react-router-native:基于react-router,加入了react-native运行环境下的一些功能
react-router-config:用于配置静态路由的工具库
常用的Api
BrowserRouter、HashRouter
Route
Link、NavLink
Switch
Redirect
HashRouter :利用hash值 地址栏有#
BroserRouter : 利用html5浏览器端的history API实现=>推荐使用(React中路由传参只能在BroserRouter)
组件出口+规则
老版本 #新版本 React-Router v6版本中 } />
相同点:
本质是a标签
to属性,将来会渲染成a标签的href属性
不同点:
NavLink
组件,一个更特殊的Link
组件,可以用用于指定当前导航高亮
NavLink组件中有个activeClassName
:用于指定高亮的类名,默认active
activeStyle:活跃时(匹配时)的样式
exact:精确匹配
要将所有的Route 组件包裹
不管有多少个路由规则匹配到,都只会渲染第一个匹配的组件
新版本: switch==>Routes
用于路由的重定向,当这个组件出现时,就会执行跳转到对应的路径中,且redirect组件只能在switch包裹里才能生效,否则需要通过render 去渲染redirect
react-router还提供一些hooks
useHistory
useParams
useLocation
可以让函数组件内部直接访问history,无需通过props获取
import { useHistory } from "react-router-dom"; const history =useHistory()
返回URL参数的键值对的对象,用它来访问match.params当前
import { useParams } from "react-router-dom"; const { name } = useParams();
返回的location表示当前URL的对象,您可以将其视为每当URL,更改时useState返回一个新的
import { useLocation } from "react-router-dom"; const { pathname } = useLocation();
路由传递参数的三种形式
动态路由的方式
search传递参数
to传入对象
动态路由
path='/detail/:id'
获取传参的方法
const {id}=useParmas()
详情
search 传递参数
在跳转的路径中添加了一些query参数
详情2
push/replace/redirect组件to传参的三种方式
search:显示在地址栏,刷新不丢失
query:不显示在地址栏,刷新会丢失
state:不显示在地址栏,刷新不丢失(用的最多)=>这个state参数,只有BrowaweRouter支持
在单页应用中,一个web项目只有一个html页面,一旦页面加载完成之后,就不用因为用户的操作而进行页面的重新加载或者该跳转,其特性如下:
改变 url 且不让浏览器向服务器发送请求
在不刷新页面的前提下动态改变浏览器地址栏中的URL地址
其中主要成了两种模式
hash模式:在url后面加上#
history模式:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录
React Rouer 对应的hash 模式和 history 模式对应的组件为
HashRouter
BrowserRouter
路由就是组件和路径(URL
与 UI
之间)的映射关系,这种映射是单向的,即URL改变引起UI更新(无需刷新页面)
immutable, 不可改变的,在计算机中,即指一旦创建,就不能再被更改的数据
对 Immutable
对象的任何修改或者添加删除操作都会返回一个新的Immutable
对象
Immutable 实现的原理是 Persistent Data Structure
(持久化数据结构)
用一种数据结构来保存数据
当数据被修改时,会返回一个对象,但是新对象会可能利用之前的数据结构而不会对内存造成浪费
也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 deepCopy
把所有节点都赋值一遍带来的性能损耗
Immutable
使用了 Structural Sharing
(结构共享)
如果对象树中一个节点发生改变.只修改这个节点和受他影响的父节点,其他节点则进行共享
使用 Immutable
对象最主要的库是 immutable.js
immutable.js 是一个完全独立的库,无论基于什么框架都可以用它
其出现场景在于弥补 JavaScript 没有不可变数据结构的问题,通过Structural Sharing (结构共享)来解决的性能问题
List: 有序索引集,类似于JS中的Array
Map: 无需索引集,类似于JS中的Object
Set:没有重复值的集合
主要的方法如下
fromJS(): 讲一个js数据转换成 Immutable 类型的数据
const obj= Immutable.fromJS({})
toJS() : 将一个Immutable数据转换成 JS类型的数据
is(): 对两个对象进行比较
get(key): 对数据或对象取值
getIn([]): 将嵌套对象或数组取值,传参为数组,表示位置
let abs = Immutable.fromJS({a: {b:2}}); abs.getIn(['a', 'b']) // 2 abs.getIn(['a', 'c']) // 子级没有值 let arr = Immutable.fromJS([1 ,2, 3, {a: 5}]); arr.getIn([3, 'a']); // 5 arr.getIn([3, 'c']); // 子级没有值
使用 Immutable
可以给react 应用带来性能的优化,主要体现在减少渲染的次数
在做 react 性能优化的时候,为了避免重复渲染,我们会在 shouldComponentUpdata()中做对比,当返回 true执行render 方法
Immutable
通过is 方法 则可以完全对比,而无需想一样通过深度比较的方式比较
在使用 redux 过程中可以结合 Immutable
,不使用 Immutable
前修改一个数据需要做一个深拷贝
import '_' from 'lodash'; const Component = React.createClass({ getInitialState() { return { data: { times: 0 } } }, handleAdd() { let data = _.cloneDeep(this.state.data); data.times = data.times + 1; this.setState({ data: data }); } }
使用 Immutable 后:
getInitialState() { return { data: Map({ times: 0 }) } }, handleAdd() { this.setState({ data: this.state.data.update('times', v => v + 1) }); // 这时的 times 并不会改变 console.log(this.state.data.get('times')); }
同理,在redux
中也可以将数据进行fromJS
处理
import * as constants from './constants' import {fromJS} from 'immutable' const defaultState = fromJS({ //将数据转化成immutable数据 home:true, focused:false, mouseIn:false, list:[], page:1, totalPage:1 }) export default(state=defaultState,action)=>{ switch(action.type){ case constants.SEARCH_FOCUS: return state.set('focused',true) //更改immutable数据 case constants.CHANGE_HOME_ACTIVE: return state.set('home',action.value) case constants.SEARCH_BLUR: return state.set('focused',false) case constants.CHANGE_LIST: // return state.set('list',action.data).set('totalPage',action.totalPage) //merge效率更高,执行一次改变多个数据 return state.merge({ list:action.data, totalPage:action.totalPage }) case constants.MOUSE_ENTER: return state.set('mouseIn',true) case constants.MOUSE_LEAVE: return state.set('mouseIn',false) case constants.CHANGE_PAGE: return state.set('page',action.page) default: return state } }
首先,render函数在react中有两种形式
在类组件中,指的是render方法
在函数组件中,指的是函数组件本身
在render中,我们会编写jsx,jsx通过babel编译后就会转化成我们熟悉的js格式
在render 过程中,react 将新调用的render函数返回的树与旧版本的树进行比较,这一步是决定如何更新DOM 的必要步骤,然后进行diff比较,更新DOM
render 的执行时机主要分成了两部分
类组件
调用setState 修改状态
类组件props传值改变,重新渲染
函数组件
通过useState hook修改状态,只会首次触发render
render 函数里面可以编写JSX ,转化成 createElement这种形式,用于生成虚拟DOM, 最终转化成真实DOM
在 render中,类组件只要执行了 setState方法,就一定会触发render 函数执行,函数组件使用useState 更改状态不一定导致重新render
组件的props改变了,不一定触发render函数的执行,就会导致子组件的重新渲染
在这种情况下,父组件或者祖先组件的state 发生了改变,就会导致组件的重新渲染
所有一旦执行了setState就会执行render方法,useState会判断当前值会有无发生改变确定是否执行render方法,一旦父组件发生渲染,子组件也会渲染
react 基于虚拟DOM 和高效DIff算法 的完全配合,实现了对DOM 最小粒度的更新,大多数情况下,React对DOM 的渲染效率足以我们的业务日常
复杂业务场景下,性能问题依然会困扰我们,此时需要采取一些措施来提升运行性能,避免不必要的渲染是业务中场景的优化手段之一
具体实现的方法如下:
shouldComponentUpdate=>类组件
PureComponent+immutable =>类组件
React.memo=>函数组件
shouldComponentUpdata
通过shouldComponentUpdata 生命周期函数来比对state 和props,确定是否要重新渲染
默认情况下返回true 表示重新渲染,如果不希望组件重新渲染,返回false即可
PureComponent
跟 shouldComponentUpdate原理基本一致,通过对props和state的浅比较结果来实现shouldComponentUpdate
React.memo
React.memo 用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与PureComponent 十分类似.但不同的是,React.memo 只能用于函数组件
三.总结
在实际开发过程中,前端性能问题是一个必须考虑的问题,随着业务的赋值,遇到性能问题的概率也在增高
除此之外,建议将页面进行更小的颗粒化,如果一个过大,当状态发生改变的时候,就会导致整个大组件的渲染,而对组件进行拆分后,颗粒度变小了,也能减少子组件不必要的渲染
跟 vue一致,react通过引入Vritual DOM 的概念,极大地避免无效的DOM操作,是我们的页面的构建效率得到极大的提升,
而 diff算法就是更高效地通过对比新旧 Vritual DOM 来找到真正的DOM 变化之处
react 中diff算法主要遵循三个层级的策略
tree层级
conponent层级
element层级
tree 层级
DOM节点跨层级的操作不做优化,只会对相同层级的节点进行比较
conponent层级
如果是同一个类的组件,则会继续往下 diff运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的
element层级
对于比较同一层级的节点们,每个节点在对应的层级用唯一的key作为标识
通过key可以准确地发现新旧集合中的节点都是相同节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置
对于简单列表渲染而言,不使用key比使用key节省性能
由于dom节点的移动操作开销是比较昂贵的,没有key的情况下要比有key的性能更好
JS引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待
如果JS线程长时间占用了主线程,那么渲染层面的更新就不得不长时间的等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿
而这也正是React 15的Stack Reconciler 所面临的问题,当react在渲染组件时候,从开始到渲染完成整个成功是一气呵成的,无法中断
如果组件较大,那么js线程会一直执行,然后等到整颗VDOM 树计算完成后,才会交给渲染的线程
就会导致一些用户交互,动画等任务无法立即得到处理,导致卡顿的请求
react 16,主要做了以下操作
为每个增加了优先级,优先级高的任务可以中断低优先级的任务.然后再更新,注意是重新执行优先级低的任务
增加了异步任务,调用requesetldleCallback api ,浏览器空闲的时候执行
dom diff树变成链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行
从架构角度来看,Fiber是对React核心算法(即调和过程)的重写
从编码角度来看,Fiber是react内部所定义的一种数据结构,它是Fiber树结构的节点单位,也就是react 16新架构下的虚拟DOM
一个Fiber就是一个JS对象,包含了元素的信息,该元素更新操作队列,类型,其数据结构如下
type Fiber = { // 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等 tag: WorkTag, // ReactElement里面的key key: null | string, // ReactElement.type,调用`createElement`的第一个参数 elementType: any, // The resolved function/class/ associated with this fiber. // 表示当前代表的节点类型 type: any, // 表示当前FiberNode对应的element组件实例 stateNode: any, // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回 return: Fiber | null, // 指向自己的第一个子节点 child: Fiber | null, // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点 sibling: Fiber | null, index: number, ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject, // 当前处理过程中的组件props对象 pendingProps: any, // 上一次渲染完成之后的props memoizedProps: any, // 该Fiber对应的组件产生的Update会存放在这个队列里面 updateQueue: UpdateQueue| null, // 上一次渲染的时候的state memoizedState: any, // 一个列表,存放这个Fiber依赖的context firstContextDependency: ContextDependency | null, mode: TypeOfMode, // Effect // 用来记录Side Effect effectTag: SideEffectTag, // 单链表用来快速查找下一个side effect nextEffect: Fiber | null, // 子树中第一个side effect firstEffect: Fiber | null, // 子树中最后一个side effect lastEffect: Fiber | null, // 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes expirationTime: ExpirationTime, // 快速确定子树中是否有不在等待的变化 childExpirationTime: ExpirationTime, // fiber的版本池,即记录fiber更新过程,便于恢复 alternate: Fiber | null, }
JSX是JavaScript XML 的简写,表示在JS代码中编写HTML代码
JSX不是标准的JS语法,是JS的语法拓展
jsx本身不能浏览器读取,必须使用Babel和webpack等工具将其转换成传统的JS
Babel是一个工具链,主要将ES5+语法编写的代码转化成向下兼容的JS语法
JSX本质就是JS对象=>React.createElement
在react中,节点大盒子可以分成四大类别:
原生标签节点
文本节点
函数组件
类组件
这些类别最终都会转化成React.createElement这种形式
React.createElement 会根据传入的节点信息进行一个判断:
如果是原生标签节点,type是字符串,如div,span
如果是文本节点,type就没有,这里是TEXT
如果是函数组件,type就是函数名
如果是类组件,type就是类名
虚拟Dom会通过ReactDOM.render进行渲染成真实DOM
在react 源码中,虚拟DOM转化成真实DOM 整体流程如下
渲染流程如下:
使用React.createElement或JSX编写react组件,实际上所有的jsx代码最后都会转换成React.createElement(...),Babel帮助我们完成这个转换的过程
createElement函数对key 和ref等特殊的props进行处理,并获取defaultProps对默认props进行复制,并且对传入的孩子节点进行处理,最终构成一个虚拟DOM对象
ReactDOM.render将生成好的虚拟DOM 渲染到指定容器上,其中采用了批处理,事务等机制并且对特定浏览器进行了性能优化,最终转化成为真实DOM
React 凭借 virtual DOM 和 diff算法拥有高效的性能,但是在某些情况下,性能明显可以进一步提高
在React中避免不必要的render中有:shouldComponenetUpdate,PureComponent,React.memo
除此之外,常见性能优化常见的手段有如下:
避免使用内联函数
使用React Fragments避免额外标记=>可以使用幽灵组件
使用Immutable
懒加载组件
事件绑定方式
从性能方面考虑,在render方法中使用bind和render方法中使用箭头函数这两种形式,在每次组件render的时候都会生成新的方法实例=>尽量使用在render外创建一个函数,将事件绑定到函数本身
服务端渲染
服务端渲染需要起一个node服务,可以使用express,koa等
调用react的renderToString方法,将根组件渲染成字符串,在输出到响应中
为了解决出现的错误导致整个应用崩溃的问题,react16 引用了 错误边界 新的概念
错误边界是一种React组件,这种组件可以捕获发生子组件树任何位置的JS
错误,并打印这些错误,同时展示降级UI,而不会渲染哪些发生崩溃的子组件树
错误边界在渲染期间,生命周期方法和整个组件树的构造函数中捕获错误
形成错误边界组件的两个条件
使用了 static getDerivedStateFromError()==>渲染备用 UI
使用了componentDidCatch==>打印错误
下面这些情况无法捕获异常
事件处理
异常代码
服务端渲染
自身抛出来的错误
在SSR中,我们了解到Server-Side Rendering,简称SSR,意为服务端渲染
指由服务侧完成页面的HTML结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态和事件,称为完成可交互页面的过程
其解决的问题主要有两个
SEO,由于搜索引擎爬虫抓取工具可以直接查看完成渲染的页面
加速首屏加载,解决首屏白屏问题
在react中,实现SSR主要有两种形式
手动搭建一个SSR框架
使用成熟SSR框架,如Next.js
这里主要以手动搭建一个SSR框架进行实现
首先通过express 启动一个app.js,用于监听3000端口的请求,当请求根目录时,返回HTML
重构通俗讲就是一套React代码在服务器上运行一遍,到达浏览器又运行一遍:
服务端渲染完成页面结构
浏览器端渲染完成事件绑定
整体 react 服务端渲染并不复杂,具体如下:
node server 接收客户端请求,得到当前的请求url路径,然后再已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为props,context或者store形式传入组件
然后基于react 内置的服务器渲染方法 renderToString() 把组件渲染为html字符串在最终的html进行输出前需要将数据注入到浏览器端
浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务器输出的html节点,整个流程结束
创建时(挂载阶段)
constructor
初始化state
创建ref
render
渲染UI(注意:不要在render中调用setState,会死循环)
componentDidMount
发送请求+注册事件
操作DOM
更新时(更新阶段)
render
componentDidUpdate
setState() - 更改state,更新UI
forceUpdate() 强制更新
组件接收到新的props
卸载时(销毁阶段)
componentWillUnmount
执行清理工作(比如:清理定时器等)
-------------------------------------------------------------------------------Amy-YAN