框架好处

  1. 组件化: 其中以 React 的组件化最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以是我们的工程易于维护、易于组合拓展
  2. 天然分层: JQuery 时代的代码大部分情况下是面条代码,耦合严重,现代框架不管是 MVC、MVP还是MVVM 模式都能帮助我们进行分层,代码解耦更易于读写。
  3. 生态: 现在主流前端框架都自带生态,不管是数据流管理架构还是 UI 库都有成熟的解决方案。
  4. 开发效率: 现代前端框架都默认自动更新DOM,而非我们手动操作,解放了开发者的手动DOM成本,提高开发效率,从根本上解决了UI 与状态同步问题。

常见框架:Angular React Vue Svelte

React vs Vue

React 与 Vue 存在很多共同点,例如他们都是 JavaScript 的 UI 框架,专注于创造前端的富应用。

不同于早期的 JavaScript 框架“功能齐全”,Reat 与 Vue 只有框架的骨架,其他的功能如路由、状态管理等是框架分离的组件。

  • React组件倾向于使用jsx语法,all in js,将html与css全都融入javaScript,jsx语法相对来说更加灵活。

  • vue仍然是拥抱经典的**html(结构)+css(表现)+js(行为)**的形式,vue鼓励开发者使用template模板,并提供指令供开发者使用如v-if、v-show、v-for等指令,因此在开发vue应用的时候会有一种在写经典web应用(结构、表现、行为分离)的感觉。

  • React 整体是函数式的思想,在 React 中是单向数据流,推崇结合 immutable 来实现数据不可变

  • 而 Vue 的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立 Watcher 来监听,当属性变化的时候,响应式的更新对应的虚拟 DOM。

  • 如上,所以 React 的性能优化需要手动去做。

React优势

  • 灵活的结构和可扩展性
  • 丰富的JavaScript库
  • 发展: React得到了Facebook专业开发人员的支持,他们不断寻找改进方法。
  • Web或移动平台: React提供React Native平台,可通过相同的React组件模型为iOSAndroid开发本机呈现的应用程序。

react在中后台项目中由于在处理复杂的业务逻辑或组件的复用问题vue优雅而被人认可,但这种优雅是要有成本代价的,它更需要团队技术整体比较给力,领头大佬的设计与把关能力要更优秀,因此开发成本更大。

vue更友好更易上手的写法著称,渐进式的框架、更友好的api、更亲民的设计让开发成本大大下降而效率大大提升。

vuereact在发展长河中越发成熟,深思熟虑后觉得两者不管在移动端或大型中后台都是非常可行的,其实框架本无好坏之分,我们更应该思考的是团队想要用什么技术栈、自己喜欢与擅长什么技术栈。

JSX

JSX 是JavaScript XML 的简写。是 React 使用的一种文件,它利用 JavaScript 的表现力和类似 HTML 的模板语法。这使得 HTML 文件非常容易理解。此文件能使应用非常可靠,并能够提高其性能。

React不强制要求使用 JSX,但是大多数人发现,在 JavaScript 代码中将 JSX 和 UI 放在一起时,会在视觉上有辅助作用。它还可以使 React 显示更多有用的错误和警告消息

实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。

虚拟DOM

操作DOM会引起页面的回流或者重绘。相比起来,通过多一些预先计算来减少DOM的操作要划算的多。

但是,“使用虚拟DOM会更快”这句话并不一定适用于所有场景。例如:一个页面就有一个按钮,点击一下,数字加一,那肯定是直接操作DOM更快。使用虚拟DOM无非白白增加了计算量和代码量。即使是复杂情况,浏览器也会对我们的DOM操作进行优化,大部分浏览器会根据我们操作的时间和次数进行批量处理,所以直接操作DOM也未必很慢。

使用虚拟DOM可以提高代码的性能下限,并极大的优化大量操作DOM时产生的性能损耗。同时这些框架也保证了,即使在少数虚拟DOM不太给力的场景下,性能也在我们接受的范围内。

UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调

这种方式赋予了 React 声明式的 API:您告诉 React 希望让 UI 是什么状态,React 就确保 DOM 匹配该状态。这使您可以从属性操作、事件处理和手动 DOM 更新这些在构建应用程序时必要的操作中解放出来。

实现原理

Virtual DOM 是一个轻量级的 JavaScript 对象,它最初只是 real DOM 的副本。它是一个节点树,它将元素、它们的属性和内容作为对象及其属性。 React 的渲染函数从 React 组件中创建一个节点树。然后它响应数据模型中的变化来更新该树,该变化是由用户或系统完成的各种动作引起的。

  • 每当底层数据发生改变时,整个 UI 都将在 Virtual DOM 描述中重新渲染。
  • 然后计算之前 DOM 表示与新表示的之间的差异。
  • 完成计算后,将只用实际更改的内容更新 real DOM。

React Fiber

Fiber 是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM 可以进行增量式渲染

React Fiber的思想: React 渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染

diff 算法/协调

在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 stateprops 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何高效的更新 UI,以保证当前 UI 与最新的树保持同步。

此算法有一些通用的解决方案,即将一棵树转换成另一棵树的最小操作次数。然而,即使使用最优的算法,该算法的复杂程度仍为 O(n^3 ),其中 n 是树中元素的数量。

于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:

  1. 两个不同类型的元素会产生出不同的树
  2. 开发者可以通过设置 key 属性,来告知渲染哪些子元素在不同的渲染下可以保存不变

Keys

当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素,从而减少不必要的元素重新渲染。

虚拟DOM中key的作用:

  • 简单的说: key是虚拟DOM对象的标识, 在更新显示时key起着极其重要的作用。

  • 详细的说: 当状态中的数据发生变化时,react会根据新数据生成新的虚拟DOM, 随后React进行新虚拟DOM旧虚拟DOM的diff比较,比较规则如下:

    • 旧虚拟DOM中找到了与新虚拟DOM相同的key:

      • 若虚拟DOM中内容没变, 直接使用之前的真实DOM
      • 若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM
    • 旧虚拟DOM中未找到与新虚拟DOM相同的key

      • 根据数据创建新的真实DOM,随后渲染到到页面

用index作为key可能会引发的问题:

  • 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低

  • 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题

  • 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

开发中如何选择key?:

  • 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。

  • 如果确定只是简单的展示数据,用index也是可以的

两种组件

所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。

在不违反上述规则的情况下,state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。

函数式

1
2
3
4
5
6
7
//1.创建函数式组件
function MyComponent(){
console.log(this); //此处的this是undefined,因为babel编译后开启了严格模式
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent/>,document.getElementById('test'))

执行了ReactDOM.render(<MyComponent/>.......)之后,发生了什么?

  • React解析组件标签,找到了MyComponent组件。
  • 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现在页面中。

类式

1
2
3
4
5
6
7
8
9
10
11
//1.创建类式组件
class MyComponent extends React.Component {
render(){
//render是放在哪里的?—— MyComponent的原型对象上,供实例使用。
//render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。
console.log('render中的this:',this);
return <h2>我是用类定义的组件</h2>
}
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent/>,document.getElementById('test'))

执行了ReactDOM.render(<MyComponent/>.......)之后,发生了什么?

  • React解析组件标签,找到了MyComponent组件。

  • 发现组件是使用类定义的,随后new出来该类的实例,并通过该实例调用到原型上的render方法

  • 将render返回的虚拟DOM转为真实DOM,随后呈现在页面中。

类的方法默认开启了局部严格模式

组件中的render方法中的this为组件实例对象

但组件自定义方法中this为undefined,如何解决?

a) 强制绑定this:通过函数对象的bind()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//1.创建组件
class Weather extends React.Component{
//构造器调用几次? ———— 1次
constructor(props){
console.log('constructor');
super(props)
//初始化状态
this.state = {isHot:false,wind:'微风'}
//解决changeWeather中this指向问题,也可以在调用处直接使用
this.changeWeather = this.changeWeather.bind(this)
}
//render调用几次? ———— 1+n次 1是初始化的那次 n是状态更新的次数
render(){
console.log('render');
//读取状态
const {isHot,wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
}
//changeWeather调用几次? ———— 点几次调几次
changeWeather(){
//changeWeather放在哪里? ———— Weather的原型对象上,供实例使用
//由于changeWeather是作为onClick的回调,所以不是通过实例调用的,是直接调用
//类中的方法默认开启了局部的严格模式,所以changeWeather中的this为undefined
console.log('changeWeather');
//获取原来的isHot值
const isHot = this.state.isHot
//严重注意:状态必须通过setState进行更新,且更新是一种合并,不是替换。
this.setState({isHot:!isHot})
console.log(this);

//严重注意:状态(state)不可直接更改,下面这行就是直接更改!!!
//this.state.isHot = !isHot //这是错误的写法
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>,document.getElementById('test'))

b) 箭头函数推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.创建组件
class Weather extends React.Component{
//初始化状态
//类中可以直接写赋值语句,如下代码的含义是:给Weather的实例对象添加一个属性 state
state = {isHot:false,wind:'微风'}

render(){
const {isHot,wind} = this.state
return <h1 onClick={this.changeWeather}>今天天气很{isHot ? '炎热' : '凉爽'},{wind}</h1>
}

//自定义方法————要用赋值语句的形式+箭头函数
changeWeather = ()=>{
const isHot = this.state.isHot
this.setState({isHot:!isHot})
}
}
//2.渲染组件到页面
ReactDOM.render(<Weather/>,document.getElementById('test'))

事件处理

  • 通过onXxx属性指定事件处理函数(注意大小写)
    • React使用的是自定义(合成)事件,而不是使用的原生DOM事件—-为了更好的兼容性
    • React中的事件是通过事件委托的方式处理的(委托给组件最外层的元素)—-为了更高效
  • 通过event.target得到发生事件的DOM元素对象 —–不要过度使用ref
  • React的所有事件都挂载在document中,当真实dom触发后冒泡到document后才会对react事件进行处理
  • 所以原生的事件会先执行,然后执行react合成事件,最后执行真正在document上挂载的事件

State

关于 setState() 你应该了解三件事:

不要直接修改 State

而是应该使用 setState():

State更新可能异步

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.propsthis.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

1
2
3
4
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));

上面使用了箭头函数,不过使用普通的函数也同样可以:

1
2
3
4
5
6
// Correct
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});

setState更新状态的2种写法

  • setState(stateChange, [callback])——对象式的setState

    • stateChange为状态改变对象(该对象可以体现出状态的更改)
    • callback是可选的回调函数, 它在状态更新完毕(状态更新是异步的)、界面也更新后(render调用后)才被调用
  • setState(updater, [callback])——函数式的setState

    • updater为返回stateChange对象的函数
    • updater可以接收到state和props。
    • callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。

总结:

  • 对象式的setState是函数式的setState的简写方式(语法糖)
  • 使用原则:
    • 如果新状态不依赖于原状态 => 使用对象方式
    • 如果新状态依赖于原状态 => 使用函数方式
    • 如果需要在setState()执行后获取最新的状态数据, 要在第二个callback函数中读取

setState只在合成事件和 hook() 中是“异步”的,在 原生事件和 setTimeout 中都是同步的。

State更新会合并

当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。

这里的合并是浅合并

Refs

在典型的React数据流理念中,父组件跟子组件的交互都是通过传递props实现的。

如果父组件需要修改子组件,只需要将新的属性传递给子组件,由子组件来实现具体的绘制逻辑。

在特殊的情况下,如果你需要命令式(imperatively)的修改子组件,React也提供了应急的处理办法–Ref。

Ref 既支持修改DOM元素,也支持修改自定义的组件

声明式编程

React有2个基石设计理念:一个是声明式编程,一个是函数式编程。

声明式编程的特点体现在2方面:

  • 组件定义的时候,所有的实现逻辑都封装在组件的内部,通过state管理,对外只暴露属性
  • 组件使用的时候,组件调用者通过传入不同属性的值来达到展现不同内容的效果。一切效果都是事先定义好的,至于效果是怎么实现的,组件调用者不需要关心。

因此,在使用React的时候,一般很少需要用到Ref

Ref使用场景

简单理解就是,控制一些DOM原生的效果,如输入框的聚焦效果和选中效果等;触发一些命令式的动画;集成第三方的DOM库。最后还补了一句:如果要实现的功能可以通过声明式的方式实现,就不要借助Ref。

refs是React组件中非常特殊的props, 可以附加到任何一个组件上,从字面意思上看,ref即reference,组件被调用时会创建一个该组件的实例,而ref就会指向这个实例

Ref用法

如果作用在原生的DOM元素上,通过Ref获取的是DOM元素,可以直接操作DOM的API

如果作用在自定义组件,Ref获取的是组件的实例,可以直接操作组件内的任意方法

创建 Refs

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。

在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。

1
2
3
4
5
6
7
8
9
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}

访问 Refs

当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

1
const node = this.myRef.current;

ref 的值根据节点的类型而有所不同:

  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 ref 属性,因为他们没有实例

Refs 与函数组件

默认情况下,你不能在函数组件上使用 ref 属性,因为它们没有实例。

如果要在函数组件中使用 ref,你可以使用 forwardRef(可与 useImperativeHandle结合使用)。

但你可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件

React组件通信如何实现?

由于React是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值。因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中。

React组件间通信方式:

  • 父组件子组件通讯: 父组件可以向子组件通过传 props 的方式,向子组件进行通讯
  • 子组件父组件通讯: props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中
  • 兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
  • 跨层级通信: Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context通信再适合不过。
  • 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信。
  • 全局状态管理工具: 借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态

比较好的搭配方式

  • 父子组件:props
  • 兄弟组件:消息订阅-发布、集中式管理
  • 祖孙组件(跨级组件):消息订阅-发布、集中式管理、Context(开发用的少,封装插件用的多)

React.memo

React.PureComponent

React.Component 是使用ES6 classes方式定义 React 组件的基类。

React.PureComponentReact.Component很相似。

两者的区别在于 React.Component并未实现 shouldComponentUpdate(),而 React.PureComponent 中以浅层对比 propstate 的方式来实现了该函数。

如果赋予 React 组件相同的 props 和 state,render() 函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent 可提高性能。

React.memo

1
2
3
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
});

React.memo 为高阶组件。

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。

这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更

如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReduceruseContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。