前端框架

选择使用框架而不是原生

框架的好处:

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

声明式编程 vs 命令式编程

声明式编程的编写方式描述了应该做什么,而命令式编程描述了如何做。在声明式编程中,让编译器决定如何做事情。声明性程序很容易推理,因为代码本身描述了它在做什么。
下面是一个例子,数组中的每个元素都乘以 2,我们使用声明式map函数,让编译器来完成其余的工作,而使用命令式,需要编写所有的流程步骤。

const numbers = [1,2,3,4,5];
// 声明式
const doubleWithDec = numbers.map(number => number * 2);
console.log(doubleWithDec)
// 命令式
const doubleWithImp = [];
for(let i=0; i<numbers.length; i++) {
    const numberdouble = numbers[i] * 2;
    doubleWithImp.push(numberdouble)
}
console.log(doubleWithImp)
1
2
3
4
5
6
7
8
9
10
11

函数式编程

函数式编程是声明式编程的一部分。javascript中的函数是第一类公民,这意味着函数是数据,你可以像保存变量一样在应用程序中保存、检索和传递这些函数。
函数式编程有些核心的概念,如下:

  • 不可变性(Immutability)
  • 纯函数(Pure Functions)
  • 数据转换(Data Transformations)
  • 高阶函数 (Higher-Order Functions)
  • 递归
  • 组合

不可变性(Immutability)
不可变性意味着不可改变。 在函数式编程中,你无法更改数据,也不能更改。 如果要改变或更改数据,则必须复制数据副本来更改。
例如,这是一个student对象和changeName函数,如果要更改学生的名称,则需要先复制 student 对象,然后返回新对象。
在javascript中,函数参数是对实际数据的引用,你不应该使用 student.firstName =“testing11”,这会改变实际的student 对象,应该使用Object.assign复制对象并返回新对象。

let student = {
    firstName: "testing",
    lastName: "testing",
    marks: 500
}
function changeName(student) {
    // student.firstName = "testing11" //should not do it
    let copiedStudent = Object.assign({}, student);
    copiedStudent.firstName = "testing11";
    return copiedStudent;
}
console.log(changeName(student));
console.log(student);
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14

纯函数
纯函数是始终接受一个或多个参数并计算参数并返回数据或函数的函数。 它没有副作用,例如设置全局状态,更改应用程序状态,它总是将参数视为不可变数据。
我想使用 appendAddress 的函数向student对象添加一个地址。 如果使用非纯函数,它没有参数,直接更改 student 对象来更改全局状态。
使用纯函数,它接受参数,基于参数计算,返回一个新对象而不修改参数。

let student = {
    firstName: "testing",
    lastName: "testing",
    marks: 500
}
// 非纯函数
function appendAddress() {
    student.address = {streetNumber:"0000", streetName: "first", city:"somecity"};
}
console.log(appendAddress());
// 纯函数
function appendAddress(student) {
    let copystudent = Object.assign({}, student);
    copystudent.address = {streetNumber:"0000", streetName: "first", city:"somecity"};
    return copystudent;
}
console.log(appendAddress(student));
console.log(student);
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

数据转换
我们讲了很多关于不可变性的内容,如果数据是不可变的,我们如何改变数据。如上所述,我们总是生成原始数据的转换副本,而不是直接更改原始数据。
再介绍一些 javascript内置函数,当然还有很多其他的函数,这里有一些例子。所有这些函数都不改变现有的数据,而是返回新的数组或对象。

let cities = ["irving", "lowell", "houston"];
// we can get the comma separated list
console.log(cities.join(','))
// irving,lowell,houston
// if we want to get cities start with i
const citiesI = cities.filter(city => city[0] === "i");
console.log(citiesI)
// [ 'irving' ]
// if we want to capitalize all the cities
const citiesC = cities.map(city => city.toUpperCase());
console.log(citiesC)
// [ 'IRVING', 'LOWELL', 'HOUSTON' ]
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13

高阶函数
高阶函数是将函数作为参数或返回函数的函数,或者有时它们都有。 这些高阶函数可以操纵其他函数。
Array.map,Array.filter和Array.reduce是高阶函数,因为它们将函数作为参数。

const numbers = [10,20,40,50,60,70,80]
const out1 = numbers.map(num => num * 100);
console.log(out1);
// [ 1000, 2000, 4000, 5000, 6000, 7000, 8000 ]
const out2 = numbers.filter(num => num > 50);
console.log(out2);
// [ 60, 70, 80 ]
const out3 = numbers.reduce((out,num) => out + num);
console.log(out3);
// 330
复制代码
1
2
3
4
5
6
7
8
9
10
11

下面是另一个名为isPersonOld的高阶函数示例,该函数接受另外两个函数,分别是 messageisYoung

const isYoung = age => age < 25;
const message = msg => "He is "+ msg;
function isPersonOld(age, isYoung, message) {
    const returnMessage = isYoung(age)?message("young"):message("old");
    return returnMessage;
}
// passing functions as an arguments
console.log(isPersonOld(13,isYoung,message))
// He is young
复制代码
1
2
3
4
5
6
7
8
9
10

递归
递归是一种函数在满足一定条件之前调用自身的技术。只要可能,最好使用递归而不是循环。你必须注意这一点,浏览器不能处理太多递归和抛出错误。
下面是一个演示递归的例子,在这个递归中,打印一个类似于楼梯的名称。我们也可以使用for循环,但只要可能,我们更喜欢递归。

function printMyName(name, count) {
    if(count <= name.length) {
        console.log(name.substring(0,count));
        printMyName(name, ++count);
    }
}
console.log(printMyName("Bhargav", 1));
/*
B
Bh
Bha
Bhar
Bharg
Bharga
Bhargav
*/
// withotu recursion
var name = "Bhargav"
var output = "";
for(let i=0; i<name.length; i++) {
    output = output + name[i];
    console.log(output);
}
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

组合
在React中,我们将功能划分为小型可重用的纯函数,我们必须将所有这些可重用的函数放在一起,最终使其成为产品。 将所有较小的函数组合成更大的函数,最终,得到一个应用程序,这称为组合
实现组合有许多不同方法。 我们从Javascript中了解到的一种常见方法是链接。 链接是一种使用表示法调用前一个函数的返回值的函数的方法。
这是一个例子。 我们有一个name,如果firstNamelastName大于5个单词的大写字母,刚返回,并且打印名称的名称和长度。

const name = "Bhargav Bachina";
const output = name.split(" ")
    .filter(name => name.length > 5)
    .map(val => {
    val = val.toUpperCase();
    console.log("Name:::::"+val);
    console.log("Count::::"+val.length);
    return val;
});
console.log(output)
/*
Name:::::BHARGAV
Count::::7
Name:::::BACHINA
Count::::7
[ 'BHARGAV', 'BACHINA' ]
*/
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在React中,我们使用了不同于链接的方法,因为如果有30个这样的函数,就很难进行链接。这里的目的是将所有更简单的函数组合起来生成一个更高阶的函数。

const name = compose(
    splitmyName,
    countEachName,
    comvertUpperCase,
    returnName
)
console.log(name);
1
2
3
4
5
6
7

作者:Fundebug
链接:https://juejin.im/post/5cf0733de51d4510803ce34e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

react虚拟dom

一、什么是虚拟DOM?

传统的 DOM 操作是直接在 DOM 上操作的,当需要修改一系列元素中的值时,就会直接对 DOM 进行操作。而采用 Virtual DOM 则会对需要修改的 DOM 进行比较(DIFF),从而只选择需要修改的部分。也因此对于不需要大量修改 DOM 的应用来说,采用 Virtual DOM 并不会有优势。开发者就可以创建出可交互的 UI。
在React中,render执行的结果得到的并不是真正的DOM节点,结果仅仅是轻量级的JavaScript对象,我们称之为virtual DOM。

虚拟DOM是react的一大亮点,具有batching(批处理)和高效的Diff算法。这让我们可以无需担心性能问题而”毫无顾忌”的随时“刷新”整个页面,由虚拟 DOM来确保只对界面上真正变化的部分进行实际的DOM操作。在实际开发中基本无需关心虚拟DOM是如何运作的,但是理解其运行机制不仅有助于更好的理解React组件的生命周期,而且对于进一步优化 React程序也会有很大帮助。

二 .为什么需要虚拟DOM

DOM是很慢的,其元素非常庞大,页面的性能问题鲜有由js引起的,大部分都是由DOM操作引起的。如果对前端工作进行抽象的话,主要就是维护状态和更新视图;而更新视图和维护状态都需要DOM操作。其实近年来,前端的框架主要发展方向就是解放DOM操作的复杂性。

在jQuery出现以前,我们直接操作DOM结构,这种方法复杂度高,兼容性也较差;有了jquery强大的选择器以及高度封装的API,我们可以更方便的操作DOM,jQuery帮我们处理兼容性问题,同时也使DOM操作变得简单;但是聪明的程序员不可能满足于此,各种MVVM框架应运而生,有AngularJS、avalon、vue.js等,MVVM使用数据双向绑定,使得我们完全不需要操作DOM了,更新了状态视图会自动更新,更新了视图数据状态也会自动更新,可以说MMVM使得前端的开发效率大幅提升,但是其大量的事件绑定使得其在复杂场景下的执行性能堪忧;有没有一种兼顾开发效率和执行效率的方案呢?ReactJS就是一种不错的方案,虽然其将JS代码和HTML代码混合在一起的设计有不少争议,但是其引入的Virtual DOM(虚拟DOM)却是得到大家的一致认同的。

三 、虚拟DOM 和 真实DOM的对比

DOM 完全不属于javascript (也不在Javascript 引擎中存在).。Javascript 其实是一个非常独立的引擎,DOM其实是浏览器引出的一组让Javascript操作HTML文档的API而已。在即时编译的时代,调用DOM的开销是很大的。而Virtual DOM的执行完全都在Javascript 引擎中,完全不会有这个开销。
优点:

  • 保证性能下限: 虚拟DOM可以经过diff找出最小差异,然后批量进行patch,这种操作虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多,因此虚拟DOM可以保证性能下限
  • 无需手动操作DOM: 虚拟DOM的diff和patch都是在一次更新中自动进行的,我们无需手动操作DOM,极大提高开发效率
  • 跨平台: 虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等

缺点:

  • 无法进行极致优化: 在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化

四.理解虚拟DOM

虚拟的DOM的核心思想是:对复杂的文档DOM结构,提供一种方便的工具,进行最小化地DOM操作。这句话,也许过于抽象,却基本概况了虚拟DOM的设计思想

DOM很慢,而javascript很快,用javascript对象可以很容易地表示DOM节点。DOM节点包括标签、属性和子节点,通过VElement表示如下。

//虚拟dom,参数分别为标签名、属性对象、子DOM列表
var VElement = function(tagName, props, children) {
    //保证只能通过如下方式调用:new VElement
    if (!(this instanceof VElement)) {
        return new VElement(tagName, props, children);
    }
    //可以通过只传递tagName和children参数
    if (util.isArray(props)) {
        children = props;
        props = {};
    }
    //设置虚拟dom的相关属性
    this.tagName = tagName;
    this.props = props || {};
    this.children = children || [];
    this.key = props ? props.key : void 666;
    var count = 0;
    util.each(this.children, function(child, i) {
        if (child instanceof VElement) {
            count += child.count;
        } else {
            children[i] = '' + child;
        }
        count++;
    });
    this.count = count;
}
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

通过VElement,我们可以很简单地用javascript表示DOM结构。比如

var vdom = velement('div', { 'id': 'container' }, [
    velement('h1', { style: 'color:red' }, ['simple virtual dom']),
    velement('p', ['hello world']),
    velement('ul', [velement('li', ['item #1']), velement('li', ['item #2'])]),
]);
1
2
3
4
5

上面的javascript代码可以表示如下DOM结构:

<div id="container">
    <h1 style="color:red">simple virtual dom</h1>
    <p>hello world</p>
    <ul>
        <li>item #1</li>
        <li>item #2</li>
    </ul>
</div>
1
2
3
4
5
6
7
8

既然我们可以用JS对象表示DOM结构,那么当数据状态发生变化而需要改变DOM结构时,我们先通过JS对象表示的虚拟DOM计算出实际DOM需要做的最小变动,然后再操作实际DOM,从而避免了粗放式的DOM操作带来的性能问题。
如下图所示,两个虚拟DOM之间的差异已经标红:

五、 虚拟DOM的原理

下图

虚拟DOM则是在DOM的基础上建立了一个抽象层,我们对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟DOM,最后再批量同步到DOM中。

虚拟DOM会使得App只关心数据和组件的执行结果,中间产生的DOM操作不需要App干预,而且通过虚拟DOM来生成DOM,会有一项非常可观收益——性能

props(properties 特性)是在调用时候被调用者设置的,只设置一次,一般没有额外变化
可以把任意类型的数据传递给组件,尽可能的把props当做数据源,不要在组件内部设置props ,0.15.x已经废弃了setProps的方法

1、this.props.children 2、this.props.xxx

state用来确定组件的状态,不同状态可以展示不同的视图(控制下拉菜单的隐藏显示)
可以通过setState方法来设置state //this.setState(obj|function(state){})

一旦props或者state发生改变,组件都会重新渲染
所有人都知道DOM慢,渲染一个空的DIV,浏览器需要为这个DIV生成几百个属性,而虚拟DOM只有6个。
所以说减少不必要的重排重绘以及DOM读写能够对页面渲染性能有大幅提升
那么我们来看看虚拟DOM是怎么做的。React会在内存中维护一个虚拟DOM树,当我们对这个树进行读或写的时候,实际上是对虚拟DOM进行的。当数据变化时,然后React会自动更新虚拟DOM,然后拿新的虚拟DOM和旧的虚拟DOM进行对比,找到有变更的部分,得出一个Patch,然后将这个Patch放到一个队列里,最终批量更新这些Patch到DOM中。
这样的机制可以保证即便是根节点数据的变化,最终表现在DOM上的修改也只是受这个数据影响的部分,这样可以保证非常高效的渲染。
但也是有一定的缺陷的——首次渲染大量DOM时因为多了一层虚拟DOM的计算,会比innerHTML插入方式慢。

这是一个简单单完整的React组件【类】,细节大家先不用太在意细节,了解机制就可以。
props 主要作用是提供数据来源,可以简单的理解为 props 就是构造函数的参数。
state 唯一的作用是控制组件的表现,用来存放会随着交互变化状态,比如开关状态等。
JSX 做的事情就是根据 state 和 props 中的值,结合一些视图层面的逻辑,输出对应的 DOM 结构

diff实现方式

虚拟DOM算法

DOM是很慢的,如果我们创建一个简单的div,然后把他的所有的属性都打印出来,你会看到:

var div = document.createElement('div'),
        str = '';
    for (var key in div) {
      str = str + ' ' + key;
    }
    console.log(str);
1
2
3
4
5
6

可以看到,这些属性还是非常惊人的,包括样式的修饰特性、一般的特性、方法等等,如果我们打印出其长度,可以得到惊人的227个。
而这仅仅是一层,真正的DOM元素是非常庞大的,这是因为标准就是这么设计的,而且操作他们的时候你要小心翼翼,轻微的触碰就有可能导致页面发生重排,这是杀死性能的罪魁祸首。

而相对于DOM对象,原生的JavaScript对象处理起来更快,而且更简单,DOM树上的结构信息我们都可以使用JavaScript对象很容易的表示出来。

var element = {
      tagName: 'ul',
      props: {
        id: 'list'
      },
      children: {
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['Item1']
        }, 
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['Item1']
        }, 
        {
          tagName: 'li',
          props: {
            class: 'item'
          },
          children: ['Item1']
        }
      }
    }
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

如上所示,对于一个元素,我们只需要一个JavaScript对象就可以很容易的表示出来,这个对象中有三个属性:

  1. tagName: 用来表示这个元素的标签名。
  2. props: 用来表示这元素所包含的属性。
  3. children: 用来表示这元素的children。

而上面的这个对象使用HTML表示就是:

<ul id='list'>
  <li class='item'>Item 1</li>
  <li class='item'>Item 2</li>
  <li class='item'>Item 3</li>
</ul>
1
2
3
4
5

OK! 既然原来的DOM信息可以使用JavaScript来表示,那么反过来,我们就可以用这个JavaScript对象来构建一个真正的DOM树。

所以之前所说的状态变更的时候会从新构建这个JavaScript对象,然后呢,用新渲染的对象和旧的对象去对比, 记录两棵树的差异,记录下来的就是我们需要改变的地方。 这就是所谓的虚拟DOM,包括下面的几个步骤:

用JavaScript对象来表示DOM树的结构; 然后用这个树构建一个真正的DOM树,插入到文档中。
当状态变更的时候,重新构造一个新的对象树,然后用这个新的树和旧的树作对比,记录两个树的差异。
把2所记录的差异应用在步骤一所构建的真正的DOM树上,视图就更新了。
Virtual DOM的本质就是在JS和DOM之间做一个缓存,可以类比CPU和硬盘,既然硬盘这么慢,我们就也在他们之间添加一个缓存; 既然DOM这么慢,我们就可以在JS和DOM之间添加一个缓存。 CPU(JS)只操作内存(虚拟DOM),最后的时候在把变更写入硬盘(DOM)。

diff算法实现

1、 用JavaScript对象模拟DOM树

用JavaScript对象来模拟一个DOM节点并不难,你只需要记录他的节点类型(tagName)、属性(props)、子节点(children)。
element.js

function Element(tagName, props, children) {
      this.tagName = tagName;
      this.props = props;
      this.children = children;
    }
   module.exports = function (tagName, props, children) {
       return new Element(tagName, props, children);
    }
1
2
3
4
5
6
7
8

通过这个构造函数,我们就可以传入标签名、属性以及子节点了,tagName可以在我们render的时候直接根据它来创建真实的元素,这里的props使用一个对象传入,可以方便我们遍历。

基本使用方法如下:

var el = require('./element');

    var ul = el('ul', {id: 'list'}, [
        el('li', {class: 'item'}, ['item1']),
        el('li', {class: 'item'}, ['item2']),
        el('li', {class: 'item'}, ['item3'])
      ]);
1
2
3
4
5
6
7

然而,现在的ul只是JavaScript表示的一个DOM结构,页面上并没有这个结构,所有我们可以根据ul构建一个真正的

Element.prototype.render = function () {
      // 根据tagName创建一个真实的元素
      var el = document.createElement(this.tagName);
      // 得到这个元素的属性对象,方便我们遍历。
      var props = this.props;

      for (var propName in props) {
        // 获取到这个元素值
        var propValue = props[propName];

        // 通过setAttribute设置元素属性。 
        el.setAttribute(propName, propValue);
      }

      // 注意: 这里的children,我们传入的是一个数组,所以,children不存在时我们用【】来替代。 
      var children = this.children || [];

      //遍历children
      children.forEach(function (child) {
        var childEl = (child instanceof Element)
                      ? child.render()
                      : document.createTextNode(child);
        // 无论childEl是元素还是文字节点,都需要添加到这个元素中。
        el.appendChild(childEl);
      });

      return el;
    }
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

所以,render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归的把自己的子节点也构建起来,所以只需要调用ul的render方法,通过document.body.appendChild就可以挂载到真实的页面了。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>div</title>
</head>
<body>
  <script>

    function Element(tagName, props, children) {
      this.tagName = tagName;
      this.props = props;
      this.children = children;
    }


    var ul = new Element('ul', {id: 'list'}, [
        new Element('li', {class: 'item'}, ['item1']),
        new Element('li', {class: 'item'}, ['item2']),
        new Element('li', {class: 'item'}, ['item3'])
      ]);

    Element.prototype.render = function () {
      // 根据tagName创建一个真实的元素
      var el = document.createElement(this.tagName);
      // 得到这个元素的属性对象,方便我们遍历。
      var props = this.props;

      for (var propName in props) {
        // 获取到这个元素值
        var propValue = props[propName];

        // 通过setAttribute设置元素属性。 
        el.setAttribute(propName, propValue);
      }

      // 注意: 这里的children,我们传入的是一个数组,所以,children不存在时我们用【】来替代。 
      var children = this.children || [];

      //遍历children
      children.forEach(function (child) {
        var childEl = (child instanceof Element)
                      ? child.render()
                      : document.createTextNode(child);
        // 无论childEl是元素还是文字节点,都需要添加到这个元素中。
        el.appendChild(childEl);
      });

      return el;
    }

    var ulRoot = ul.render();
    document.body.appendChild(ulRoot);
  </script>
</body>
</html>
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

上面的这段代码,就可以渲染出下面的结果了:

2.比较两颗虚拟DOM树的差异

比较两颗DOM数的差异是Virtual DOM算法中最为核心的部分,这也就是所谓的Virtual DOM的diff算法。 两个树的完全的diff算法是一个时间复杂度为 O(n3) 的问题。 但是在前端中,你会很少跨层地移动DOM元素,所以真实的DOM算法会对同一个层级的元素进行对比

上图中,div只会和同一层级的div对比,第二层级的只会和第二层级对比。 这样算法复杂度就可以达到O(n)。

(1)深度遍历优先,记录差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每一个节点就会有一个唯一的标记:

上面的这个遍历过程就是深度优先,即深度完全完成之后,再转移位置。 在深度优先遍历的时候,每遍历到一个节点就把该节点和新的树进行对比,如果有差异的话就记录到一个对象里面。

// diff函数,对比两颗树
    function diff(oldTree, newTree) {
      // 当前的节点的标志。因为在深度优先遍历的过程中,每个节点都有一个index。
      var index = 0;

      // 在遍历到每个节点的时候,都需要进行对比,找到差异,并记录在下面的对象中。
      var pathches = {};

      // 开始进行深度优先遍历
      dfsWalk(oldTree, newTree, index, pathches);

      // 最终diff算法返回的是一个两棵树的差异。
      return pathches;
    }

    // 对两棵树进行深度优先遍历。
    function dfsWalk(oldNode, newNode, index, pathches) {
      // 对比oldNode和newNode的不同,记录下来
      pathches[index] = [...];

      diffChildren(oldNode.children, newNode.children, index, pathches); 
    }

    // 遍历子节点
    function diffChildren(oldChildren, newChildren, index, pathches) {  
      var leftNode = null;
      var currentNodeIndex = index;
      oldChildren.forEach(function (child, i) {
        var newChild = newChildren[i];
        currentNodeIndex = (leftNode && leftNode.count)
        ? currentNodeIndex + leftNode.count + 1
        : currentNodeIndex + 1

        // 深度遍历子节点
        dfsWalk(child, newChild, currentNodeIndex, pathches);
        leftNode = child;
      });
    }
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
37
38

例如,上面的div和新的div有差异,当前的标记是0, 那么我们可以使用数组来存储新旧节点的不同:

patches[0] = [{difference}, {difference}, ...]
同理使用patches[1]来记录p,使用patches[3]来记录ul,以此类推。

(2)差异类型

上面说的节点的差异指的是什么呢? 对DOM操作可能会:

  1. 替换原来的节点,如把上面的div换成了section。
  2. 移动、删除、新增子节点, 例如上面div的子节点,把p和ul顺序互换。
  3. 修改了节点的属性。
    对于文本节点,文本内容可能会改变。 例如修改上面的文本内容2内容为Virtual DOM2.
      所以,我们可以定义下面的几种类型:
var REPLACE = 0;
    var REORDER = 1;
    var PROPS = 2;
    var TEXT = 3;
1
2
3
4

对于节点替换,很简单,判断新旧节点的tagName是不是一样的,如果不一样的说明需要替换掉。 如div换成了section,就记录下:

patches[0] = [{
  type: REPALCE,
  node: newNode // el('section', props, children)
}]
1
2
3
4

除此之外,如果给div新增了属性id为container,就记录下:

pathches[0] = [
      {
        type: REPLACE,
        node: newNode 
      }, 
      { 
        type: PROPS,
        props: {
          id: 'container'
        }
      }
    ]
1
2
3
4
5
6
7
8
9
10
11
12

如果是文本节点发生了变化,那么就记录下:

pathches[2] = [
      {
        type:  TEXT,
        content: 'virtual DOM2'
      }
    ]
1
2
3
4
5
6

那么如果我们把div的子节点重新排序了呢? 比如p、ul、div的顺序换成了div、p、ul,那么这个该怎么对比呢? 如果按照同级进行顺序对比的话,他们就会被替换掉,如p和div的tagName不同,p就会被div所代替,最终,三个节点就都会被替换,这样DOM开销就会非常大,而实际上是不需要替换节点的,只需要移动就可以了, 我们只需要知道怎么去移动。这里牵扯到了两个列表的对比算法,如下。

(3)列表对比算法
  假设现在可以英文字母唯一地标识每一个子节点:

旧的节点顺序:

a b c d e f g h i
1

现在对节点进行了删除、插入、移动的操作。新增j节点,删除e节点,移动h节点:

新的节点顺序:

a b c h d f g i j
1

现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的解决算法是 Levenshtein Distance,通过动态规划求解,时间复杂度为 O(M * N)。但是我们并不需要真的达到最小的操作,我们只需要优化一些比较常见的移动情况,牺牲一定DOM操作,让算法时间复杂度达到线性的(O(max(M, N))。具体算法细节比较多,这里不累述,有兴趣可以参考代码。

我们能够获取到某个父节点的子节点的操作,就可以记录下来:

patches[0] = [{
  type: REORDER,
  moves: [{remove or insert}, {remove or insert}, ...]
}]
1
2
3
4

但是要注意的是,因为tagName是可重复的,不能用这个来进行对比。所以需要给子节点加上唯一标识key,列表对比的时候,使用key进行对比,这样才能复用老的 DOM 树上的节点。

这样,我们就可以通过深度优先遍历两棵树,每层的节点进行对比,记录下每个节点的差异了。完整 diff 算法代码可见 diff.js。

3、把差异引用到真正的DOM树上
  因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) { // 深度遍历子节点
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches) // 对当前节点进行DOM操作
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:

function applyPatches (node, currentPatches) {
  currentPatches.forEach(function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        node.parentNode.replaceChild(currentPatch.node.render(), node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        node.textContent = currentPatch.content
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

5、结语
  virtual DOM算法主要实现上面步骤的三个函数: element、diff、patch,然后就可以实际的进行使用了。

// 1. 构建虚拟DOM
var tree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: blue'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li')])
])

// 2. 通过虚拟DOM构建真正的DOM
var root = tree.render()
document.body.appendChild(root)

// 3. 生成新的虚拟DOM
var newTree = el('div', {'id': 'container'}, [
    el('h1', {style: 'color: red'}, ['simple virtal dom']),
    el('p', ['Hello, virtual-dom']),
    el('ul', [el('li'), el('li')])
])

// 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree)

// 5. 在真正的DOM元素上应用变更
patch(root, patches)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

当然这是非常粗糙的实践,实际中还需要处理事件监听等;生成虚拟 DOM 的时候也可以加入 JSX 语法。这些事情都做了的话,就可以构造一个简单的ReactJS了。

转载:深入理解react中的虚拟DOM、diff算法

JSX

JSX是javascript的语法扩展。它就像一个拥有javascript全部功能的模板语言。它生成React元素,这些元素将在DOM中呈现。React建议在组件使用JSX。在JSX中,我们结合了javascript和HTML,并生成了可以在DOM中呈现的react元素。
下面是JSX的一个例子。我们可以看到如何将javascript和HTML结合起来。如果HTML中包含任何动态变量,我们应该使用表达式{}

import React from 'react';
export const Header = () => {
    const heading = 'TODO App'
    return(
        <div style={{backgroundColor:'orange'}}>
            <h1>{heading}</h1>
        </div>
    )
}
1
2
3
4
5
6
7
8
9

作者:Fundebug
链接:https://juejin.im/post/5cf0733de51d4510803ce34e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

react组件生命周期

react16前的生命周期可以总结为:

组件挂载阶段
  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount
组件更新阶段
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate
组件卸载阶段
  • componentWillUnmount

react16更新后的生命周期可以总结为:

组件挂载阶段
  • constructor
  • componentWillMount
  • render
  • componentDidMount
组件更新阶段
  • componentWillReceiveProps
  • shouldComponentUpdate
  • render
  • componentWillUpdate
  • componentDidUpdate
组件卸载阶段
  • componentWillUnmount

react的setState异步还是同步

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

  • 原因:在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。

简单来说就是当setState方法调用的时候React就会重新调用render方法来重新渲染组件;setState通过一个队列来更新state,当调用setState方法的时候会将需要更新的state放入这个状态队列中,这个队列会高效的批量更新state;

详解: 深入 setState 机制

  • setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
  • setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  • setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。

react的请求在哪个生命周期中

React的异步请求到底应该放在哪个生命周期里,有人认为在componentWillMount中可以提前进行异步请求,避免白屏,其实这个观点是有问题的.
由于JavaScript中异步事件的性质,当您启动API调用时,浏览器会在此期间返回执行其他工作。当React渲染一个组件时,它不会等待componentWillMount它完成任何事情 - React继续前进并继续render,没有办法“暂停”渲染以等待数据到达。
而且在componentWillMount请求会有一系列潜在的问题,首先,在服务器渲染时,如果在 componentWillMount 里获取数据,fetch data会执行两次,一次在服务端一次在客户端,这造成了多余的请求,其次,在React 16进行React Fiber重写后,componentWillMount可能在一次渲染中多次调用.
目前官方推荐的异步请求是在componentDidmount中进行.
如果有特殊需求需要提前请求,也可以在特殊情况下在constructor中请求:

react 17之后componentWillMount会被废弃,仅仅保留UNSAFE_componentWillMount

react组件内通信

React组件间通信方式:

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

react性能优化

React组件/逻辑复用

抛开已经被官方弃用的Mixin,组件抽象的技术目前有三种比较主流:

  • 高阶组件:
    • 属性代理
    • 反向继承
  • 渲染属性
  • react-hooks

mixin、hoc、render props、react-hooks的优劣

Mixin的缺陷:

  • 组件与 Mixin 之间存在隐式依赖(Mixin 经常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)
  • 多个 Mixin 之间可能产生冲突(比如定义了相同的state字段)
  • Mixin 倾向于增加更多状态,这降低了应用的可预测性(The more state in your application, the harder it is to reason about it.),导致复杂度剧增
  • 隐式依赖导致依赖关系不透明,维护成本和理解成本迅速攀升:
    • 难以快速理解组件行为,需要全盘了解所有依赖 Mixin 的扩展行为,及其之间的相互影响
    • 组价自身的方法和state字段不敢轻易删改,因为难以确定有没有 Mixin 依赖它
    • Mixin 也难以维护,因为 Mixin 逻辑最后会被打平合并到一起,很难搞清楚一个 Mixin 的输入输出

HOC相比Mixin的优势:

  • HOC通过外层组件通过 Props 影响内层组件的状态,而不是直接改变其 State不存在冲突和互相干扰,这就降低了耦合度
  • 不同于 Mixin 的打平+合并,HOC 具有天然的层级结构(组件树结构),这又降低了复杂度

HOC的缺陷:

  • 扩展性限制: HOC 无法从外部访问子组件的 State因此无法通过shouldComponentUpdate滤掉不必要的更新,React 在支持 ES6 Class 之后提供了React.PureComponent来解决这个问题
  • Ref 传递问题: Ref 被隔断,后来的React.forwardRef 来解决这个问题
  • Wrapper Hell: HOC可能出现多层包裹组件的情况,多层抽象同样增加了复杂度和理解成本
  • 命名冲突: 如果高阶组件多次嵌套,没有使用命名空间的话会产生冲突,然后覆盖老属性
  • 不可见性: HOC相当于在原有组件外层再包装一个组件,你压根不知道外层的包装是啥,对于你是黑盒

Render Props优点:

  • 上述HOC的缺点Render Props都可以解决

Render Props缺陷:

  • 使用繁琐: HOC使用只需要借助装饰器语法通常一行代码就可以进行复用,Render Props无法做到如此简单
  • 嵌套过深: Render Props虽然摆脱了组件多层嵌套的问题,但是转化为了函数回调的嵌套

React Hooks优点:

  • 简洁: React Hooks解决了HOC和Render Props的嵌套问题,更加简洁
  • 解耦: React Hooks可以更方便地把 UI 和状态分离,做到更彻底的解耦
  • 组合: Hooks 中可以引用另外的 Hooks形成新的Hooks,组合变化万千
  • 函数友好: React Hooks为函数组件而生,从而解决了类组件的几大问题:
    • this 指向容易错误
    • 分割在不同声明周期中的逻辑使得代码难以理解和维护
    • 代码复用成本高(高阶组件容易使代码量剧增)

React Hooks缺陷:

  • 额外的学习成本(Functional Component 与 Class Component 之间的困惑)
  • 写法上有限制(不能出现在条件、循环中),并且写法限制增加了重构成本
  • 破坏了PureComponent、React.memo浅比较的性能优化效果(为了取最新的props和state,每次render()都要重新创建事件处函数)
  • 在闭包场景可能会引用到旧的state、props值
  • 内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
  • React.memo并不能完全替代shouldComponentUpdate(因为拿不到 state change,只针对 props change)

Fiber 详解

React Fiber 是一种基于浏览器的单线程调度算法.
React 16之前 ,reconcilation 算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.
Fiber:一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。

Time Slice的理解

时间分片

  • React 在渲染(render)的时候,不会阻塞现在的线程
  • 如果你的设备足够快,你会感觉渲染是同步的
  • 如果你设备非常慢,你会感觉还算是灵敏的
  • 虽然是异步渲染,但是你将会看到完整的渲染,而不是一个组件一行行的渲染出来
  • 同样书写组件的方式

也就是说,这是React背后在做的事情,对于我们开发者来说,是透明的,具体是什么样的效果呢?
有图表三个图表,有一个输入框,以及上面的三种模式
这个组件非常的巨大,而且在输入框每次**输入东西的时候,就会进去一直在渲染。**为了更好的看到渲染的性能,Dan为我们做了一个表。
我们先看看,同步模式:

同步模式下,我们都知道,我们没输入一个字符,React就开始渲染,当React渲染一颗巨大的树的时候,是非常卡的,所以才会有shouldUpdate的出现,在这里Dan也展示了,这种卡!
我们再来看看第二种(Debounced模式):

Debounced模式简单的来说,就是延迟渲染,比如,当你输入完成以后,再开始渲染所有的变化。
这么做的坏处就是,至少不会阻塞用户的输入了,但是依然有非常严重的卡顿。
切换到异步模式:

异步渲染模式就是不阻塞当前线程,继续跑。在视频里可以看到所有的输入,表上都会是原谅色的。
时间分片正是基于可随时打断、重启的Fiber架构,可打断当前任务,优先处理紧急且重要的任务,保证页面的流畅运行.

redux的工作流程

首先,我们看下几个核心概念:

  • Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。
  • State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。
  • Action:State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。
  • Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。
  • Reducer:Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。
  • dispatch:是View发出Action的唯一方法。

然后我们过下整个工作流程:

  1. 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
  2. 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
  3. State一旦有变化,Store就会调用监听函数,来更新View。

到这儿为止,一次用户交互流程结束。可以看到,在整个流程中数据都是单向流动的,这种方式保证了流程的清晰。

react-redux如何工作

  • Provider: Provider的作用是从最外部封装了整个应用,并向connect模块传递store
  • connect: 负责连接React和Redux
    • 获取state: connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state
    • 包装原组件: 将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组件WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent
    • 监听store tree变化: connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用this.setState()方法触发Connect及其子组件的重新渲染

redux和mobx区别

两者对比:

  • redux将数据保存在单一的store中,mobx将数据保存在分散的多个store中
  • redux使用plain object保存数据,需要手动处理变化后的操作;mobx适用observable保存数据,数据变化后自动处理响应的操作
  • redux使用不可变状态,这意味着状态是只读的,不能直接去修改它,而是应该返回一个新的状态,同时使用纯函数;mobx中的状态是可变的,可以直接对其进行修改
  • mobx相对来说比较简单,在其中有很多的抽象,mobx更多的使用面向对象的编程思维;redux会比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用
  • mobx中有更多的抽象和封装,调试会比较困难,同时结果也难以预测;而redux提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变得更加的容易

场景辨析:
基于以上区别,我们可以简单得分析一下两者的不同使用场景.
mobx更适合数据不复杂的应用: mobx难以调试,很多状态无法回溯,面对复杂度高的应用时,往往力不从心.
redux适合有回溯需求的应用: 比如一个画板应用、一个表格应用,很多时候需要撤销、重做等操作,由于redux不可变的特性,天然支持这些操作.
mobx适合短平快的项目: mobx上手简单,样板代码少,可以很大程度上提高开发效率.
当然mobx和redux也并不一定是非此即彼的关系,你也可以在项目中用redux作为全局状态管理,用mobx作为组件局部状态管理器来用.

redux中如何进行异步操作?

当然,我们可以在componentDidmount中直接进行请求无须借助redux.
但是在一定规模的项目中,上述方法很难进行异步流的管理,通常情况下我们会借助redux的异步中间件进行异步处理.
redux异步流中间件其实有很多,但是当下主流的异步中间件只有两种redux-thunk、redux-saga,当然redux-observable可能也有资格占据一席之地,其余的异步中间件不管是社区活跃度还是npm下载量都比较差了.

redux异步中间件之间的优劣?

redux-thunk优点:

  • 体积小: redux-thunk的实现方式很简单,只有不到20行代码
  • 使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单

redux-thunk缺陷:

  • 样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的
  • 耦合严重: 异步操作与redux的action偶合在一起,不方便管理
  • 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装

redux-saga优点:

  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
  • action摆脱thunk function: dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function
  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
  • 功能强大: redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用
  • 灵活: redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow
  • 易测试,提供了各种case的测试方案,包括mock task,分支覆盖等等

redux-saga缺陷:

  • 额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想
  • 体积庞大: 体积略大,代码近2000行,min版25KB左右
  • 功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码
  • ts支持不友好: yield无法返回TS类型

redux-observable优点:

  • 功能最强: 由于背靠rxjs这个强大的响应式编程的库,借助rxjs的操作符,你可以几乎做任何你能想到的异步处理
  • 背靠rxjs: 由于有rxjs的加持,如果你已经学习了rxjs,redux-observable的学习成本并不高,而且随着rxjs的升级redux-observable也会变得更强大

redux-observable缺陷:

  • 学习成本奇高: 如果你不会rxjs,则需要额外学习两个复杂的库
  • 社区一般: redux-observable的下载量只有redux-saga的1/5,社区也不够活跃,在复杂异步流中间件这个层面redux-saga仍处于领导地位

React Router Dom 及其工作原理

react-router-dom是应用程序中路由的库。 React库中没有路由功能,需要单独安装react-router-dom
react-router-dom 提供两个路由器BrowserRouterHashRoauter。前者基于url的pathname段,后者基于hash段。

前者:http://127.0.0.1:3000/article/num1
 后者:http://127.0.0.1:3000/#/article/num1(不一定是这样,但#是少不了的)
复制代码
1
2
3

react-router-dom 组件

  • BrowserRouterHashRouter 是路由器。
  • Route 用于路由匹配。
  • Link 组件用于在应用程序中创建链接。 它将在HTML中渲染为锚标记。
  • NavLink是突出显示当前活动链接的特殊链接。
  • Switch 不是必需的,但在组合路由时很有用。
  • Redirect 用于强制路由重定向

下面是组件中的LinkNavLinkRedirect 的例子

// normal link
<Link to="/gotoA">Home</Link>
// link which highlights currentlu active route with the given class name
<NavLink to="/gotoB" activeClassName="active">
  React
</NavLink>
// you can redirect to this url
<Redirect to="/gotoC" />
复制代码
1
2
3
4
5
6
7
8
9

以下是 react router 组件的示例。 如果你查看下面的示例,我们将匹配路径并使用SwitchRoute呈现相应的组件。

import React from 'react'
// import react router DOM elements
import { Switch, Route, Redirect } from 'react-router-dom'
import ComponentA from '../common/compa'
import ComponentB from '../common/compb'
import ComponentC from '../common/compc'
import ComponentD from '../common/compd'
import ComponentE from '../common/compe'
const Layout = ({ match }) => {
    return(
        <div className="">
            <Switch>
                <Route exact path={`${match.path}/gotoA`} component={ComponentA} />
                <Route path={`${match.path}/gotoB`} component={ComponentB} />
                <Route path={`${match.path}/gotoC`} component={ComponentC} />
                <Route path={`${match.path}/gotoD`} component={ComponentD} />
                <Route path={`${match.path}/gotoE`} component={ComponentE} />
            </Switch>
        </div>
    )}
export default Layout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

作者:Fundebug
链接:https://juejin.im/post/5cf0733de51d4510803ce34e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

更新状态以及如何不更新

你不应该直接修改状态。可以在构造函数中定义状态值。直接使用状态不会触发重新渲染。React 使用this.setState()时合并状态。

//  错误方式
this.state.name = "some name"
//  正确方式
this.setState({name:"some name"})
复制代码
1
2
3
4
5

使用this.setState()的第二种形式总是更安全的,因为更新的props和状态是异步的。这里,我们根据这些 props 更新状态。

// 错误方式
this.setState({
    timesVisited: this.state.timesVisited + this.props.count
})
// 正确方式
this.setState((state, props) => {
    timesVisited: state.timesVisited + props.count
});
1
2
3
4
5
6
7
8

什么是错误边界

在 React 中,我们通常有一个组件树。如果任何一个组件发生错误,它将破坏整个组件树。没有办法捕捉这些错误,我们可以用错误边界优雅地处理这些错误。
错误边界有两个作用

  • 如果发生错误,显示回退UI
  • 记录错误

下面是ErrorBoundary类的一个例子。如果类实现了 getDerivedStateFromErrorcomponentDidCatch 这两个生命周期方法的任何一下,,那么这个类就会成为ErrorBoundary。前者返回{hasError: true}来呈现回退UI,后者用于记录错误。

import React from 'react'
export class ErrorBoundary extends React.Component {
    constructor(props) {
      super(props);
      this.state = { hasError: false };
    }
  
    static getDerivedStateFromError(error) {
      // Update state so the next render will show the fallback UI.
      return { hasError: true };
    }
  
    componentDidCatch(error, info) {
      // You can also log the error to an error reporting service
      console.log('Error::::', error);
    }
  
    render() {
      if (this.state.hasError) {
        // You can render any custom fallback UI
        return <h1>OOPS!. WE ARE LOOKING INTO IT.</h1>;
      }
  
      return this.props.children; 
    }
  }
复制代码
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

以下是我们如何在其中一个组件中使用ErrorBoundary。使用ErrorBoundary类包裹 ToDoFormToDoList。 如果这些组件中发生任何错误,我们会记录错误并显示回退UI。

import React from 'react';
import '../App.css';
import { ToDoForm } from './todoform';
import { ToDolist } from './todolist';
import { ErrorBoundary } from '../errorboundary';
export class Dashboard extends React.Component {
  render() {
    return (
      <div className="dashboard"> 
        <ErrorBoundary>
          <ToDoForm />
          <ToDolist />
        </ErrorBoundary>
      </div>
    );
  }
}
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

什么是 Fragments

在React中,我们需要有一个父元素,同时从组件返回React元素。有时在DOM中添加额外的节点会很烦人。使用 Fragments,我们不需要在DOM中添加额外的节点。我们只需要用 React.Fragment 或才简写 <> 来包裹内容就行了。如下 所示:

// Without Fragments   
return (
    <div>
       <CompoentA />
       <CompoentB />
       <CompoentC />
    </div>
)
// With Fragments   
  return (
    <React.Fragment>
       <CompoentA />
       <CompoentB />
       <CompoentC />
    </React.Fragment>
  )
  // shorthand notation Fragments   
  return (
    <>
       <CompoentA />
       <CompoentB />
       <CompoentC />
    </>
  )
复制代码
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

什么是传送门(Portals)

默认情况下,所有子组件都在UI上呈现,具体取决于组件层次结构。Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
这里有一个例子。默认情况下,父组件在DOM层次结构中有子组件。

我们可以将 children 组件移出parent 组件并将其附加 idsomeid 的 Dom 节点下。
首先,获取 id 为 someid,我们在constructor中创建一个元素div,将child附加到componentDidMount中的someRoot。 最后,我们在ReactDOM.createPortal(this.props.childen),domnode的帮助下将子节点传递给该特定DOM节点。
首先,先获取 id 为someid DOM元素,接着在构造函数中创建一个元素div,在 componentDidMount方法中将 someRoot 放到 div 中 。 最后,通过 ReactDOM.createPortal(this.props.childen), domnode)children 传递到对应的节点下。

const someRoot = document.getElementById('someid');
class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }
  componentDidMount() {
    someRoot.appendChild(this.el);
  }
  componentWillUnmount() {
    someRoot.removeChild(this.el);
  }
  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el,
    );
  }
}
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

什么是上下文

有时我们必须将props 传递给组件树,即使所有中间组件都不需要这些props 。上下文是一种传递props 的方法,而不用在每一层传递组件树。

什么是 Hooks

Hooks 是React版本16.8中的新功能。 请记住,我们不能在函数组件中使用state ,因为它们不是类组件。Hooks 让我们在函数组件中可以使用state 和其他功能。
目前没有重大变化,我们不必放弃类组件。
Hook 不会影响你对 React 概念的理解。 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后我们将看到,Hook 还提供了一种更强大的方式来组合他们。
我们可以使用一些钩子,例如useState,useEffect,useContext,useReducer等。
下面是 Hooks 的基本规则

  • Hooks 应该在外层使用,不应该在循环,条件或嵌套函数中使用
  • Hooks 应该只在函数组件中使用。

让我们看一个例子来理解 hooks。 这是一个函数组件,它采用props并在UI上显示这些props。 在useState钩子的帮助下,我们将这个函数组件转换为有状态组件。 首先,我们在第5行定义状态,这相当于

constructor(props) {
 super(props);
 this.state = {
     name:'myname', age:10, address:'0000 one street'
 }
}
复制代码
1
2
3
4
5
6
7

useState返回两个项,一个是user,另一个是setUser函数。 user 是一个可以在没有 this关键字的情况下直接使用的对象,setUser是一个可以用来设置用户点击第21行按钮的状态的函数,该函数等效于以下内容。

this.setState({name:'name changed'})
复制代码
1
2
import React, { useState } from "react";
export const UserDisplay = ({ name, address, age }) => {
    const [user, setUser] = useState({
        name: "myname",
        age: 10,
        address: "0000 onestreet"
    });
    return (
        <>
            <div>
                <div class="label">Name:</div>
                <div>{user.name}</div>
            </div>
            <div>
                <div class="label">Address:</div>
                <div>{user.address}</div>
            </div>
            <div>
                <div class="label">Age:</div>
                <div>{user.age}</div>
            </div>
            <button onClick={() => setUser({ name: "name changed" })}>
                Click me
            </button>
        </>
    );
};
复制代码
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

如何提高性能

我们可以通过多种方式提高应用性能,以下这些比较重要:

  • 适当地使用shouldComponentUpdate生命周期方法。 它避免了子组件的不必要的渲染。 如果树中有100个组件,则不重新渲染整个组件树来提高应用程序性能。
  • 使用create-react-app来构建项目,这会创建整个项目结构,并进行大量优化。
  • 不可变性是提高性能的关键。不要对数据进行修改,而是始终在现有集合的基础上创建新的集合,以保持尽可能少的复制,从而提高性能。
  • 在显示列表或表格时始终使用 Keys,这会让 React 的更新速度更快
  • 代码分离是将代码插入到单独的文件中,只加载模块或部分所需的文件的技术。

如何在重新加载页面时保留数据

单页应用程序首先在DOM中加载index.html,然后在用户浏览页面时加载内容,或者从同一index.html中的后端API获取任何数据。
如果通过点击浏览器中的重新加载按钮重新加载页面index.html,整个React应用程序将重新加载,我们将丢失应用程序的状态。 如何保留应用状态?
每当重新加载应用程序时,我们使用浏览器localstorage来保存应用程序的状态。我们将整个存储数据保存在localstorage中,每当有页面刷新或重新加载时,我们从localstorage加载状态。

如何在React进行API调用

我们使用redux-thunk在React中调用API。因为reduce是纯函数,所以没有副作用,比如调用API。
因此,我们必须使用redux-thunk从 action creators 那里进行 API 调用。Action creator 派发一个action,将来自API的数据放入action 的 payload 中。Reducers 接收我们在上面的redux循环中讨论的数据,其余的过程也是相同的。

redux-thunk是一个中间件。一旦它被引入到项目中,每次派发一个action时,都会通过thunk传递。如果它是一个函数,它只是等待函数处理并返回响应。如果它不是一个函数,它只是正常处理。
这里有一个例子。sendEmailAPI是从组件中调用的函数,它接受一个数据并返回一个函数,其中dispatch作为参数。我们使用redux-thunk调用API apiservice,并等待收到响应。一旦接收到响应,我们就使用payload 派发一个action

import apiservice from '../services/apiservice';
export function sendEmail(data) {
    return { type:"SEND_EMAIL", payload: data };
}
export function sendEmailAPI(email) {
    return function(dispatch) {
        return apiservice.callAPI(email).then(data => {
            dispatch(sendEmail(data));
        });
    }
}
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
最后更新时间: 10/8/2019, 7:41:17 PM