0%

Virtual DOM是什么?diff算法是什么?diff算法是怎样让Virtual DOM快速实现页面渲染的?

一. Virtual DOM(虚拟DOM)

Virtual DOM(虚拟DOM):是用JS对象来描述真实的DOM结构。当调用setState时会创建新的虚拟DOM树,新旧虚拟DOM树通过diff算法快速找出差异,将差异更新到真实的DOM树上。

二. diff算法

1. 传统diff算法

​ 传统diff算法通过循环递归对节点进行一次对比,效率很低,算法复杂度达到O(n^3),n是树中节点的总数。

2. React中的diff算法

​ React 通过制定策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。

​ diff 策略:

​ (1). Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

​ (2). 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。

​ (3). 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证 明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

三. tree diff

React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。

四. component diff

因为react通过组件化开发,在对比组件差异上也采用上述算法。即,同一层只要出现不是同一类型的组件,就替换该组件的所有子节点。对于同一类型的组件,则通过shouldComponentUpdate去判断是否需要通过diff进行分析。shouldComponentUpdate默认为true。

五. element diff

element diff主要是根据mountIndexlastIndex进行比较,在确定是否移动 ,mountIndex是A节点在旧节点结合中的位置,lastIndex指访问过的节点,在旧集合中最右的位置,每次遍历都有可能会更新。

算法描述

  1. 遍历新节点集合

  2. 如果出现旧节点集合中有与当前指针所指新节点A相同的节点,则通过对比节点位置进行判断操作,对比mountIndexlastIndex

    如果mountIndex>=lastIndex:不做移动操作。并把lastIndex更新为mountIndex

    如果mountIndex<lastIndex:移动。

  3. 如果新节点集合中有旧节点集合中不存在的节点,添加,更新lastIndex

  4. 最后遍历旧节点集合,如果存在新节点集合上不存在的点,则将其删除。

至于为什么要比较mountIndexlastIndex,是因为要保证当前要进行移动操作的节点一定要比lastIndex小,一是为了节约性能,二是为了使节点排序更有条理,如果不进行比较,看见有相同的节点就移动,整个队列就乱了套了。

六. 总结

  • React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
  • React 通过分层求异的策略,对 tree diff 进行算法优化;
  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
  • React 通过设置唯一 key的策略,对 element diff 进行算法优化;
  • 建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
  • 建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

一、在Javascript中定义一个函数的四种方式:

  • 函数声明
  • 函数表达式
  • ES6中的箭头函数
  • new Function()

二、函数声明

函数声明定义一个具有指定参数的函数。

语法:

1
2
3
function name([param,[, param,[..., param]]]) {
[statements]
}

一个被函数声明创建的函数是一个 Function 对象,具有 Function 对象的所有属性、方法和行为。查看 Function 以获取 function 的详细信息。

默认情况下,函数是返回 undefined 的。想要返回一个其他的值,函数必须通过一个 return 语句指定返回值。

注意:使用函数声明创建的函数可以被提升,即在函数声明之前使用该函数。

三、函数表达式

function 关键字可以用来在一个表达式中定义一个函数。

语法:

1
2
3
let function_expression = function [name]([param1[, param2[, ..., paramN]]]) {
statements
};

也可以使用箭头函数,即:

1
2
3
let function_expression = ([param1[, param2[, ..., paramN]]]) => {
statements
};

注意:

  • 函数表达式中的函数名(name)可以被省略。
  • 函数表达式定义的函数不会被提升,必须先声明在使用。

函数声明与函数表达式的区别

  1. 函数声明必须带有标识符(函数名称),函数表达式则可以省略
    • 表达式里的名字不能在函数外面用,只能在函数内部用
    • 函数有一个name属性,指向紧跟在function关键字之后的那个函数名。如果函数表达式没有名字,那name属性指向变量名
  2. 函数声明会被预解析,函数表达式不会

为了提高组件的复用率,可测试性,就要保证组件功能单一性;但若要满足复杂需求就要扩展功能单一的组件,在React里就有了HOC(Higher-Order-Components)的概念。

定义:高阶组件是接受组件作为参数并且返回值为新组件的函数。简单来说高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, {Component} from 'react';

//高阶组件
const HOC = (wrappedComponent)=>
class WrapperComponent extends Component {
render() {
const newProps = {
name: "HOC"
}
return(
<wrappedComponent {...this.props} {...newProps}/>
);
}
}
//普通组件
class WrappedComponent extends Component {
render() {
return (
<div> hello world! </div>
)
}
}

export default HOC(WrappedComponent)

React是单向数据流,子组件对父组件透明,如果父组件想要获取子组件的状态,那么就需要用到ref。

ref的创建需用调用React.createRef(),然后将返回的对象作为子组件的props以ref传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ButtonComponent extends React.Component{
render(){
return <button>{this.props.children}</button>
}
}
class App extends React.Component{
ref = React.createRef();
render(){
return(
<ButtonComponent ref={this.ref}>Click Me</FancyButton>
)
}
componentDidMount() {
console.log(this.ref.current);
}
}
总结:

高阶组件就是一个函数,传给它一个组件,它返回一个新的组件。新的组件使用传入的组件作为子组件。

高阶组件的作用是用于代码复用,可以把组件之间可复用的代码、逻辑抽离到高阶组件当中。新的组件和传入的组件通过 props 传递信息。

高阶组件有助于提高我们代码的灵活性,逻辑的复用性。灵活和熟练地掌握高阶组件的用法需要经验的积累还有长时间的思考和练习。

我们能看到HTTPS的网站,在浏览器的地址栏会出现一个带锁的标记,鼠标点击小锁提示连接是安全的,证书有效等信息。HTTPS是怎样保证连接安全的呢?

HTTP存在的问题:

  1. 窃听风险:通信使用明文(不加密),内容可能会被窃听(第三方可能获知通信内容)

  2. 冒充风险:不验证通信方的身份,因此有可能遭遇伪装

  3. 篡改风险:无法证明报文的完整性,所以有可能已遭篡改

HTTPS:

HTTPS并非是应用层一个新的协议,通常 HTTP 直接和 TCP 通信,HTTPS则先和安全层(SSL/TLS)通信,然后安全层再和 TCP 层通信。

SSL/TLS协议就是为了解决上面提到的HTTP存在的问题而生的,下面我们来看一下它是怎么解决的:

  1. 所有的信息都是加密传输的,第三方无法窃听。

  2. 配备身份验证,防止身份被冒充。

  3. 具有校验机制,一旦被篡改,通信双方会立刻发现。

加密:

对称加密

加密和解密同用一个秘钥的方式称为 共享秘钥加密,也被叫做对称秘钥加密。

对称加密的实现步骤:

  • 浏览器发送给服务端 client_random 和一系列加密方法

  • 服务端发送给浏览器 server_random和加密方法

  • 现有浏览器和服务器有了三个相同的凭证:client_randomserver_random和加密方法 用加密方法把 client_randomserver_random 两个随机数混合起来,生成秘钥,这个密钥就是浏览器和服务端通信的暗号。

存在的问题:

​ 第三方可以在中间获取到client_randomserver_random和加密方法,由于这个加密方法同时可以解密,所以中间人可以成功对暗号进行解密,拿到数据,很容易就将这种加密方式破解了。

非对称加密

  • 浏览器发送给服务端 一系列加密方法
  • 服务端发送给浏览器 加密方法以及公钥
  • 然后浏览器通过公钥将数据加密传输给服务端,服务端收到数据使用私钥进行解密。服务端给浏览器发送数据,则使用私钥进行加密,浏览器收到服务端发送过来的数据,使用公钥进行解密。

存在的问题:

  1. 非对称加密效率太低, 这会严重影响加解密的速度,进而影响到用户打开页面的速度。
  2. 无法保证服务器发送给浏览器的数据安全, 服务器的数据只能用私钥进行加密(因为如果它用公钥那么浏览器也没法解密啦),中间人一旦拿到公钥,那么就可以对服务端传来的数据进行解密了,就这样又被破解了。

HTTPS使用对称加密和非对称加密结合

传输数据阶段依然使用对称加密,但是对称加密的秘钥我们采用非对称加密传输。

  • 浏览器向服务器发送client_random和加密方法列表。
  • 服务器接收到,返回server_random、加密方法以及公钥。
  • 浏览器接收,接着生成另一个随机数pre_master, 并且用公钥加密,传给服务器。(重点操作!)
  • 服务器用私钥解密这个被加密后的pre_master。
  • 到此为止,服务器和浏览器就有了相同的 client_randomserver_randompre_master, 然后服务器和浏览器会使用这三组随机数生成对称秘钥。有了对称秘钥之后,双方就可以使用对称加密的方式来传输数据了。

CA (数字证书)

使用对称和非对称混合的方式,实现了数据的加密传输。但是这种仍然存在一个问题,服务器可能是被黑客冒充的。这样,浏览器访问的就是黑客的服务器,黑客可以在自己的服务器上实现公钥和私钥,而对浏览器来说,它并不完全知道现在访问的是这个是黑客的站点。

服务器需要证明自己的身份,需要使用权威机构颁发的证书,这个权威机构就是 CA(Certificate Authority), 颁发的证书就称为数字证书 (Digital Certificate)。

对于浏览器来说,数字证书有两个作用:

  1. 通过数字证书向浏览器证明服务器的身份
  2. 数字证书里面包含了服务器公钥

相对于不含数字证书的HTTPS请求流程,主要以下两点改动

  1. 服务器没有直接返回公钥给浏览器,而是返回了数字证书,而公钥正是包含数字证书中的;
  2. 在浏览器端多了一个证书验证的操作,验证了证书之后,才继续后序流程。

React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。

1
2
3
4
5
6
7
8
9
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}

可以使用一种新的,且更简短的语法来声明 Fragments,像空标签:

1
2
3
4
5
6
7
8
9
10
class Columns extends React.Component {
render() {
return (
<>
<td>Hello</td>
<td>World</td>
</>
);
}
}

可以像使用任何其他元素一样使用 <> </>,但是它不支持 key 属性。若需使用key 属性可以使用<React.Fragment></React.Fragment>,key 是唯一可以传递给 Fragment 的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// 没有`key`,React 会发出一个关键警告
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

使用props进行数据传递如下:

1
2
3
4
5
6
//App.js
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
1
2
3
4
5
6
7
8
9
10
11
//Toolbar.js
function Toolbar(props) {
// Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
// 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
// 因为必须将这个值层层传递所有组件。
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
1
2
3
4
5
6
//ThemedButton.js
class ThemedButton extends React.Component {
render() {
return <Button theme={this.props.theme} />;
}
}

使用 context进行数据传递如下:

1
2
3
4
5
//ThemeContext.js
//使用createContext为theme 创建一个context。
const ThemeContext = React.createContext({
theme:"light"
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//App.js
import ThemeContext from "ThemeContext.js";

class App extends React.Component {
render() {
// 使用Provider将当前的theme的值传递给组件树,且通过value来改变theme的值。
//接下来组件树接受的theme值为dark。
return (
<ThemeContext.Provider value={{theme: "dark"}}>
<Toolbar />
</ThemeContext.Provider>
);
}
}
1
2
3
4
5
6
7
8
9
//Toolbar.js
//中间的组件不用向下传递theme了。
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
1
2
3
4
5
6
7
8
9
10
11
//ThemedButton.js
import ThemeContext from "ThemeContext.js";

function ThemedButton() {
// 使用Consumer获取theme的值,此时theme的值为dark。
return (
<ThemeContext.Consumer>
{value=> <Button theme={value.theme} /> }
</ThemeContext.Consumer>
)
}

使用v16.8.0版本新增的Hooks中提出的useContext来更加便利的使用context:

1
2
3
4
5
6
7
8
9
10
11
//ThemedButton.js
import React from 'react';
import ThemeContext from "ThemeContext.js";

function ThemedButton() {
// 使用useContext获取theme的值。
const value = React.useContext(ThemeContext);
return (
<Button theme={value.theme} />
)
}

Hooks是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其它的 React 特性。第一次接触Hooks是因为新项目要求使用函数组件和hooks,就开始了hooks的学习和使用。

学习hooks前的注意

  1. 完全可选的。无需重写任何已有代码就可以在一些组件中使用Hooks。若是用不到hooks,也不用去学习和使用它。
  2. 100%向后兼容的。Hook 不包含任何破坏性改动。
  3. 现在可用。 Hook 已发布于 v16.8.0。
  4. 没有计划从React中移除class。
  5. Hooks 不会影响你对React概念的影响。Hooks 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。

组件类的缺点

React 的核心是组件。v16.8 版本之前,组件的标准写法是类(class)。一个很简单的按钮组件类就有很多行代码,代码看起来很重。真实的 React App 由多个类按照层级,一层层构成,复杂度成倍增长。再加入 Redux,就变得更复杂。

Redux 的作者 Dan Abramov 总结了组件类的几个缺点。

  • 大型组件很难拆分和重构,也很难测试。
  • 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
  • 组件类引入了复杂的编程模式,比如 render props 和高阶组件。

函数组件

React 团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可。 组件的最佳写法应该是函数,而不是类。

React 早就支持函数组件

但是,函数组件有重大限制,必须是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。

React Hooks 的设计目的,就是加强版函数组件,完全不使用”类”,就能写出一个全功能的组件。

Hooks 的含义

Hook 这个单词的意思是”钩子”。

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码”钩”进来。 React Hooks 就是那些钩子。

你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。

所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于识别。

useState():状态钩子

useState()用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。

1
2
3
4
5
6
7
8
9
10
11
import React from "react";

const Button = () => {
const [buttonText, setButtonText] = React.useState("Click me, please");

const onChangeButtonText = () => {
setButtonText("Thanks, been clicked!");
}

return <button onClick={onChangeButtonText}>{buttonText}</button>
}

Button 组件是一个函数,内部使用useState()钩子引入状态。

useState()这个函数接受状态的初始值,作为参数,上例的初始值为按钮的文字。该函数返回一个数组,数组的第一个成员是一个变量(上例是buttonText),指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是set前缀加上状态的变量名(上例是setButtonText)。

useEffect():副作用钩子

useEffect()用来引入具有副作用(对环境的改变)的操作,最常见的就是向服务器请求数据。

作为componentDidMount()使用,第二个参数使用[]。

作为componentDidUpdate()使用,可在第二个参数数组中指定依赖。

作为componentWillUnMount()使用,通过return函数使用,组件要卸载的时候执行。

当多个useEffect存在时,按照顺序执行。

放在componentDidMount里面的代码,现在可以放在useEffect()

1
2
3
useEffect(()  =>  {
// Async Action
}, [dependencies])

useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Person = ({ personId }) => {
const [loading, setLoading] = React.useState(true);
const [person, setPerson] = React.useState({});

React.useEffect(() => {
setLoading(true);
fetch(`/people/${personId}/`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId])

if (loading === true) {
return <p>Loading ...</p>
}

return <div>
<p>You're viewing: {person.name}</p>
<p>Height: {person.height}</p>
<p>Mass: {person.mass}</p>
</div>
}

上面代码中,每当组件参数personId发生变化,useEffect()就会执行。组件第一次渲染时,useEffect()也会执行。

useContext():共享状态钩子

如果需要在组件之间共享状态,可以使用useContext()

第一步就是使用 React Context API,在组件外部建立一个 Context

//context.jsx

1
const AppContext = React.createContext({});

//app.jsx

1
2
3
4
5
6
7
8
<AppContext.Provider value={{
username: 'superawesome'
}}>
<div className="App">
<Navbar/>
<Messages/>
</div>
</AppContext.Provider>

//navbar.jsx

1
2
3
4
5
6
7
8
9
const Navbar = () => {
const { username } = React.useContext(AppContext);
return (
<div className="navbar">
<p>AwesomeSite</p>
<p>{username}</p>
</div>
);
}

//message.jsx

1
2
3
4
5
6
7
8
9
10
11
const Messages = () => {
const { username } = React.useContext(AppContext)

return (
<div className="messages">
<h1>Messages</h1>
<p>1 message for {username}</p>
<p className="message">useContext is awesome!</p>
</div>
)
}

useRef(): 在组件render时不变的值

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

  • useState/useReducer 每次render的时候state都会变。
  • useMemo/useCallback 每次render的时候,依赖变了,state就会变。
  • useRef, 每次render的时候都不会变。

React 16.3 发布React.createRef(),并且是现在类组件中推荐使用的。

Refs 是使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。在构造组件时,通常将 Refs 分配给实例属性,以便可以在整个组件中引用它们。当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问。

React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref会在 componentDidMountcomponentDidUpdate 生命周期钩子触发前更新。

createRef 和 useRef 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";
function Search() {
const inputRef = React.createRef();
const onSearch = () => {
const searchValue = inputRef.current.value
//to search
console.log(searchValue);
};
return (
<div>
<input type="text" placeholder="Search" ref={inputRef} />
<button onClick={onSearch}>Search</button>
</div>
);
};

因为函数组件没有实例,如果想用ref获取子组件的实例,子组件组要写成类组件。

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
import React, { Component, useEffect, useRef } from 'react';
function App() {
const childRef = useRef();
useEffect(() => {
console.log('useRef')
console.log(childRef.current)
childRef.current.handleLog();
}, [])
return (
<div>
<h1>Hello World!</h1>
<Child ref={childRef} count="1"/>
</div>
)
}
// 因为函数组件没有实例,如果想用ref获取子组件的实例,子组件组要写成类组件
class Child extends Component {
handleLog = () => {
console.log('Child Component');
}
render() {
const { count } = this.props;
return <h2>count: { count }</h2>
}
}
export default App;

useReducer():action 钩子

React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux。

Redux 的核心概念是,组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState

useReducers()钩子用来引入 Reducer 功能。

1
const [state, dispatch] = useReducer(reducer, initialState);

上面是useReducer()的基本用法,它接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的dispatch函数。

下面是一个计数器的例子。用于计算状态的 Reducer 函数如下。

1
2
3
4
5
6
7
8
9
10
11
const myReducer = (state, action) => {
switch(action.type) {
case('countUp'):
return {
...state,
count: state.count + 1
}
default:
return state;
}
}

组件代码如下。

1
2
3
4
5
6
7
8
9
10
11
function App() {
const [state, dispatch] = useReducer(myReducer, { count: 0 });
return (
<div className="App">
<button onClick={() => dispatch({ type: 'countUp' })}>
+1
</button>
<p>Count: {state.count}</p>
</div>
);
}

由于 Hooks 可以提供共享状态和 Reducer 函数,所以它在这些方面可以取代 Redux。但是,它没法提供中间件(middleware)和时间旅行(time travel),如果你需要这两个功能,还是要用 Redux。

创建自己的 Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const usePerson = (personId) => {
const [loading, setLoading] = React.useState(true);
const [person, setPerson] = React.useState({});
useEffect(() => {
setLoading(true);
fetch(`/people/${personId}/`)
.then(response => response.json())
.then(data => {
setPerson(data);
setLoading(false);
});
}, [personId]);
return [loading, person];
};

上面代码中,usePerson()就是一个自定义的 Hook。

Person 组件就改用这个新的钩子,引入封装的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Person = ({ personId }) => {
const [loading, person] = usePerson(personId);

if (loading === true) {
return <p>Loading ...</p>;
}

return (
<div>
<p>You're viewing: {person.name}</p>
<p>Height: {person.height}</p>
<p>Mass: {person.mass}</p>
</div>
);
};

React自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,虽然和原生的是两码事,但也是基于浏览器的事件机制下完成的。

React 的所有事件并没有绑定到具体的dom节点上而是绑定在了document 上,然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),所有节点的事件都会在 document 上触发。

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
import React from "react";
class Test extends React.Component{
componentDidMount(){
document.getElementById('btn-reactandnative').addEventListener('click', (e) => {
console.log('原生+react 事件:原生事件执行');
});
}

handleNativeAndReact = (e) => {
console.log('原生+react 事件:当前执行react事件');
}

handleClick=(e)=>{
console.log('button click');
}
render(){
return(
<div className="pageIndex">
<p>react event!!!</p>
<button id="btn-confirm" onClick={this.handleClick}>react事件</button>
<button id="btn-reactandnative" onClick={this.handleNativeAndReact}>原生 + react 事件</button>
</div>
)
}
}

btn#btn-confirm 绑定了合成事件,btn#btn-reactandnative绑定了原生的事件。

只有原生的事件才会被绑到button上,合成事件在document上,由 dispatchEvent 统一去处理。

React中所有事件均注册到了元素的最顶层-document 上 ,节点的事件由统一的入口处理。优点如下:

  1. 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次
  2. 统一规范,解决 ie 事件兼容问题,简化事件逻辑
  3. 对开发者友好

React v17 中,React 不会再将事件处理添加到 document 上,而是将事件处理添加到渲染 React 树的根 DOM 容器中:

1
2
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

在 React 16 及之前版本中,React 会对大多数事件进行 document.addEventListener() 操作。React v17 开始会通过调用 rootNode.addEventListener() 来代替。

CSS雪碧 即CSS Sprite,也有人叫它CSS精灵,是一种CSS图像合并技术。该方法是将小图标和背景图像合并到一张图片上,然后利用css的背景定位来显示需要显示的图片部分,该图片使用CSS background和background-position属性渲染。

优点:

  1. 减少图片的字节;
  2. 减少了网页的http请求;
  3. 解决了网页设计师在图片命名的困扰,不需要对每张图进行命名;
  4. 更换风格方便,只需要在一张上修改图片的颜色样式。

缺点:

  1. 耗费内存,影响浏览器的缩放功能,拼图维护比较麻烦,使CSS的编写变得困难。
1
2
3
4
5
6
7
div {
width: 42px;
height: 34px;
background-image: url(amazon-sprite.png);
background-repeat: no-repeat;
background-position: -8px -335px //雪碧图中的某个图标的位置
}

在面对自学前端三大主流框架(React、Vue、Angular)选择时,相信不少人选择了Vue。

从事前端工作三年有余,大四实习的时候选择了Vue框架,仅仅是因为Vue学习成本较低和入门相对容易些。毕业后进入现在的公司用的是React,就开始了React的学习探索直到现在。

React和Vue的使用比较:

​ 相比Vue来说,React提供的API的确较少,比如Vue中的v-if,v-for之类的指令在React中需要自行用js实现,这也让React能够更大程度的发挥js的灵活性,能更自由的组合js,可以用js的if实现v-if,js的for实现v-for。React的概念简洁(自由度高),Vue的使用便利(提供现成的范式),React相对Vue规矩的多,这是因为其目标并非写更少的代码,而是追求条理更好理解,这种极高的代码规范在大型项目上是非常可贵的,可以减少不稳定因素的影响,很适合团队开发。

​ React的一大优势是把用户界面抽象成一个个组件,如按钮组件Button,对话框组件Dialog,日期组件Calendar。开发者通过组合这些组件,最终得到功能丰富、可交互的页面。通过引入JSX语法,使得编写组件简单快速,同时保证组件结构清晰。

React

React起源于Facebook的内部项目,用来架设Instagram网站(Instagram在中国大陆简称ins,是Facebook公司旗下一款免费提供在线图片及视频分享的社交网站),并于 2013年 5 月开源。

React是用于构建用户界面的JS框架。因此react只负责解决view层的渲染。

React拥有较高的性能,代码逻辑简单,越来越多的人开始关注和使用。

React做了什么?

  1. Virtual Dom模型

  2. 生命周期管理

  3. setState机制

  4. diff算法

  5. React patch、事件系统

  6. React的 Virtual Dom模型(virtual dom 实际上是对实际Dom的一个抽象,是一个js对象。react所有的表层操作实际上是在操作virtual dom。)

  7. diff算法用于计算出两个virtual dom的差异,是react中开销最大的地方。

    传统diff算法通过循环递归对比差异,算法复杂度为O(n3)。

    react diff算法制定了三条策略,将算法复杂度从 O(n3)降低到O(n)。

    WebUI中DOM节点跨节点的操作特别少,可以忽略不计。
    拥有相同类的组件会拥有相似的DOM结构。拥有不同类的组件会生成不同的DOM结构。
    同一层级的子节点,可以根据唯一的ID来区分。
    针对这三个策略,react diff实施的具体策略是:

    diff对树进行分层比较,只对比两棵树同级别的节点。跨层级移动节点,将会导致节点删除,重新插入,无法复用。
    diff对组件进行类比较,类相同的递归diff子节点,不同的直接销毁重建。diff对同一层级的子节点进行处理时,会根据key进行简要的复用。两棵树中存在相同key的节点时,只会移动节点。
    另外,在对比同一层级的子节点时:

    diff算法会以新树的第一个子节点作为起点遍历新树,寻找旧树中与之相同的节点。

    如果节点存在,则移动位置。如果不存在,则新建一个节点。

    在这过程中,维护了一个字段lastIndex,这个字段表示已遍历的所有新树子节点在旧树中最大的index。
    在移动操作时,只有旧index小于lastIndex的才会移动。

    这个顺序优化方案实际上是基于一个假设,大部分的列表操作应该是保证列表基本有序的。
    可以推倒倒序的情况下,子节点列表diff的算法复杂度为O(n2)

特点:

  1. 声明式设计:React采用声明范式,可以轻松描述应用。
  2. 高效:React通过对DOM的模拟,最大限度的减少与DOM的交互。
  3. 灵活:React可以与已知的库或框架(个人觉得框架是要求你按照它提供的规则去写代码, 而库是多个工具函数的集合。)很好的配合。

优点:

  1. 速度快:在UI渲染过程中,React通过在虚拟DOM中的微操作来实现对实际DOM的局部更新。

  2. 跨浏览器兼容:虚拟DOM帮助我们解决了跨浏览器问题,它为我们提供了标准化的API,甚至在IE8中都是没问题的。

  3. 模块化:为你程序编写独立的模块化UI组件,这样当某个或某些组件出现问题是,可以方便地进行隔离。

  4. 单向数据流:Flux是一个用于在JavaScript应用中创建单向数据层的架构,它随着React视图库的开发而被Facebook概念化。

  5. 同构、纯粹的javascript:因为搜索引擎的爬虫程序依赖的是服务端响应而不是JavaScript的执行,预渲染你的应用有助于搜索引擎优化。

  6. 兼容性好:比如使用RequireJS来加载和打包,而Browserify和Webpack适用于构建大型应用。它们使得那些艰难的任务不再让人望而生畏。

缺点:

React本身只是一个V(view)而已,并不是一个完整的框架,所以如果是大型项目想要一套完整的框架的话,基本都需要加上ReactRouter和Flux才能写大型应用。

React性能优化方案

由于react中性能主要耗费在于update阶段的diff算法,因此性能优化也主要针对diff算法。

  1. 减少diff算法触发次数
    减少diff算法触发次数实际上就是减少update流程的次数。
    正常进入update流程有三种方式:
  2. setState
    setState机制在正常运行时,由于批更新策略,已经降低了update过程的触发次数。
    因此,setState优化主要在于非批更新阶段中(timeout/Promise回调),减少setState的触发次数。
    常见的业务场景即处理接口回调时,无论数据处理多么复杂,保证最后只调用一次setState。
  3. 父组件render
    父组件的render必然会触发子组件进入update阶段(无论props是否更新)。此时最常用的优化方案即为shouldComponentUpdate方法。
    最常见的方式为进行this.props和this.state的浅比较来判断组件是否需要更新。或者直接使用PureComponent,原理一致。
    需要注意的是,父组件的render函数如果写的不规范,将会导致上述的策略失效。
  4. 正确使用diff算法
    不使用跨层级移动节点的操作。
    对于条件渲染多个节点时,尽量采用隐藏等方式切换节点,而不是替换节点。
    尽量避免将后面的子节点移动到前面的操作,当节点数量较多时,会产生一定的性能问题。

Vue

Vue是尤雨溪编写的一个构建数据驱动的Web界面的库,准确来说不是一个框架,它聚焦在V(view)视图层。

基本原理:

  1. 建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据、v-model等规则进行compile;
  2. 通过Object.defineProperty()进行数据变化拦截;
  3. 截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
  4. 通过改变虚拟DOM元素值,从而改变最后渲染dom树的值,完成双向绑定。完成数据的双向绑定在于Object.defineProperty()

特点:

  1. 轻量级的框架

  2. 双向数据绑定

  3. 指令

  4. 插件化

优点:

  1. 简单:官方文档很清晰,比Angular简单易学。

  2. 快速:异步批处理方式更新DOM。

  3. 组合:用解耦的、可复用的组件组合你的应用程序。

  4. 紧凑:~18kbmin+gzip,且无依赖。

  5. 强大:表达式无需声明依赖的可推导属性(computedproperties)。

  6. 对模块友好:可以通过NPM、Bower或Duo安装,不强迫你所有的代码都遵循Angular的各种规定,使用场景更加灵活。

缺点:

  1. 新生儿:Vue.js是一个新的项目,没有angular那么成熟。

  2. 影响度不是很大:google了一下,有关于Vue.js多样性或者说丰富性少于其他一些有名的库

  3. 不支持IE8。

Angular

Angular是一款优秀的前端JS框架,已经被用于Google的多款产品当中。

特点:

  1. 良好的应用程序结构

  2. 双向数据绑定

  3. 指令

  4. HTML模板

  5. 可嵌入、注入和测试

优点:

  1. 模板功能强大丰富,自带了极其丰富的angular指令。

  2. 是一个比较完善的前端框架,包含服务,模板,数据双向绑定,模块化,路由,过滤器,依赖注入等所有功能;3.自定义指令,自定义指令后可以在项目中多次使用。

  3. ng模块化比较大胆的引入了Java的一些东西(依赖注入),能够很容易的写出可复用的代码,对于敏捷开发的团队来说非常有帮助。

  4. angularjs是互联网巨人谷歌开发,这也意味着他有一个坚实的基础和社区支持。

缺点:

  1. angular入门很容易但深入后概念很多,学习中较难理解。

  2. 文档例子非常少,官方的文档基本只写了api,一个例子都没有,很多时候具体怎么用都是google来的,或直接问misko,angular的作者。

  3. 对IE6/7兼容不算特别好,就是可以用jQuery自己手写代码解决一些。

  4. 指令的应用的最佳实践教程少,angular其实很灵活,如果不看一些作者的使用原则,很容易写出四不像的代码,例如js中还是像jQuery的思想有很多dom操作。

  5. DI依赖注入如果代码压缩需要显示声明。