0%

unqiueId([prefix=’’])

这个方法主要作用是,生成唯一ID。如果提供了prefix,会被添加到ID前缀上。

例子

1
2
3
4
5
_.uniqueId('contact_');
// => 'contact_104'

_.uniqueId();
// => '105'

这个方法源码很简单,我们直接上源码。解析直接以注释的方式呈现。

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
/*
创建一个全局的对象,用来生成唯一的id。
*/
const idCounter = {}

function uniqueId(prefix='$lodash$') { //如果prefix没有传的话,默认参数为$lodash$
/*
初始化,如果prefix在idCounter中不存在的话。
*/
if (!idCounter[prefix]) {
idCounter[prefix] = 0
}

const id =++idCounter[prefix] //每次调用的时候id+1,由于是++在前,所以id是从1开始。

/*
如果没有传prefix,最终返回id,如果传了,则拼接返回。
*/
if (prefix === '$lodash$') {
return `${id}`
}

return `${prefix}${id}`
}

export default uniqueId

转换成Number

type result
Undefined NaN
Null 0
Boolean 如果是true返回1,false返回0
Symbol 抛出TypeError

String转Number

只支持合法的字符串数字,数字和字母的组合就不行。如果数字前面有空格,或者空格,或者前后都有空格算是合法的。

1
2
3
4
Number('a') // NaN
Number('1a') // NaN
Number(' 1') // 1
Number(' 1 ') // 1

字符串到数字,也只支持十进制、二进制、八进制和十六进制。

1
2
3
Number('0b10') // 2
Number('0o10') // 8
Number('0x10') // 16

+Infinity和-Infinity也算合法Number型字符串。

1
2
Number('+Infinity') //Infinity
Number('-Infinity') //-Infinity

此外还支持科学计数法。用大写或者小写的e表示

1
2
Number('1e1') // 10
Number('1E1') // 10

转换String

type result
Undefined “undefined”
Null “null”
Boolean true转成”true”、false转成”false”
Symbol 抛出一个TypeError异常

Number转String

  1. NaN转为”NaN”
  2. +0或者-0转成”0”
  3. 比0小, 返回由”-“和 ToString(-Number)组成的字符串.
  4. 如果当Number绝对值较大或者较小的时候,字符串表示则是使用科学计数法表示的。

转成Boolean

type result
Undefined false
Null false
Number +0、-0、NaN返回false,其余的都返回true
String 空字符串返回false,其余的都返回true
Symbol true
BigInt 0n返回false,其余的都返回true
Object true

拆箱转换

对象到String和Number的转换都遵循”先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。
拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。
到 String 的拆箱转换会优先调用 toString。其实String的toString和valueOf是一样的。

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

1
2
3
4
5
6
7
8
9
var o = {
valueOf : () => {console.log("valueOf"); return {}},
toString : () => {console.log("toString"); return {}}
}
o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}

console.log(o + "")
// toPrimitive
// hello

装箱转换

每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

隐式装箱:

  1. 创建基本类型的一个实例;
  2. 在实例中调用制定的方法;
  3. 销毁这个实例。

在隐式装箱过程中会频繁创建临时对象。

显式装箱:

直接用new操作符进行创建实例。

强迫装箱:

Symbol和BigInt类型不能通过new来创建,这时可以定义一个函数,函数里面只有 return this,然后我们调用函数的 call 方法到一个 Symbol 类型的值上,这样就会产一个symbolObject。

1
2
3
4
5
6
var symbolObject = function() {
return this
}.call(Symbol("a"))
console.log(typeof symbolObject) //object
console.log(symbolObject instanceof Symbol) //true
console.log(symbolObject.constructor == Symbol) //true

八种类型

JavaScript语言规定了8种类型,每一个值都属于某一种数据类型。

  1. Undefined
  2. Null
  3. Boolean
  4. String
  5. Number
  6. Symbol (ES2015/ES6新增)
  7. BigInt (ES2020/ES11新增)
  8. Object

其中Symbol是ES2015中新添加的,我们就先从这这个最不熟悉的类型开始讲解。而ES2020中新添加的BigInt,我们放在Number类型之后讲。

Symbol

symbol代表唯一的标识。
Symbol()函数会返回一个symbol类型的值。创建时,我们可以给symbol一个描述,就像这样Symbol("description"),这主要的目的是用来debugging。你无需知道这个值具体是什么,你只用知道它与任何值都不相等就行,哪怕我们给他相同的描述,它也是不同的值。

1
2
3
4
let id1 = Symbol("id");
let id2 = Symbol("id");

alert(id1 == id2); // false

如果我们直接去console一个Symbol,它会报错TypeError: Cannot convert a Symbol value to a string,因为Symbol不能隐式转换成string,我们可以用toString()进行显示转换,也可以通过Symbol的description属性来去到他的描述

1
2
console.log(id1.toString());  // Symbol(id)
console.log(id1.description); // id

Symbol的作用

Symbol的作用只有一个,作为对象属性的唯一标识符。我们知道对象的key,只有可能是string和symbol,所以我们也可以说它是非字符串对象key的集合。
其最主要的目的就是防止对象属性发生冲突。

我们通过几个例子来理解一下:

1
2
3
4
5
6
7
8
const obj1 = {
a: 'aaa',
b: 'bbb',
}

const obj2 = {
a: 'a'
}

我们对上面两个对象进行合并的话,属性a就会被覆盖。就会变成这样。

1
2
3
4
{
a: 'a',
b: 'bbb'
}

如果我们不想在整合的时候因为冲突而产生覆盖,用Symbol就能解决这个问题。

1
2
3
4
5
6
7
8
const obj1 = {
[Symbol('a')]: 'aaa',
b: 'bbb'
}

const obj2 = {
[Symbol('a')]: 'a'
}

这样合并后的结果就是

1
2
3
4
5
{
Symbol(a): 'aaa',
Symbol(a): 'a',
b: 'bbb'
}

因为Symbol()返回值是唯一的,也就是: Symbol('a') === Symbol('a') 的返回值为false。

在举一个例子,在开发中我们经常会遇到这样一个需求。根据传入不同的参数去做不同的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

const obj = {
a: 'a',
b: 'b',
c: 'c'
}

function handler(type) {
if (type === obj.a) {
...
} else if (type === obj.b) {
...
} else if (type === obj.c {
...
}
}

我们在调用的时候传入obj.a或者obj.bobj.c,这样的话其实我们并不用关心obj对象中a、b、c具体的值,他们各自的值是多少无所谓,只要他们直接互不相同就行。这是Symbol就很好地满足了这个需求。我们就可以把obj对象写成这样

1
2
3
4
5
const obj = {
a: Symbol(),
b: Symbol(),
c: Symbol()
}

获取Symbol()对应属性的值

在symbol和对象使用的时候我们就需要[]了。

1
2
3
4
5
6
7
8
9
10
let s = symbol();

const obj = {
s = 1,
[s] = 2
}

console.log(obj.s) // 1
console.log(obj['s']) // 1
console.log(obj[s]) // 2

Symbol 与 for…in

Symbols在for…in中不可枚举,Object.keys也是枚举不到key的。如果想要枚举的话,可以借助Object.getOwnPropertySymbols(obj)这个方法。Reflect.ownKeys(obj) 这个方法会返回对象所有key的数组,使用这个相当于使用
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

Symbol 与 JSON.stringify()

当使用JSON.stringify()的时候,symbol作为key也会被忽略。

全局Symbol

我们知道,通常symbol是不同的。但是有时候我们希望相同名称的symbol的值是相同的。symbol提供了一个全局注册表。使用Symbol.for(key),它会从注册表里去读取,如果这个symbol不存在,它便会创建一个。它是全局可用的,甚至跨service worker、iframe。

1
2
3
4
5
6
7
8
// read from the global registry
let id = Symbol.for("id"); // if the symbol did not exist, it is created

// read it again (maybe from another part of the code)
let idAgain = Symbol.for("id");

// the same symbol
alert( id === idAgain ); // true

还有一个是与Symbol.for(key)相反的操作Symbol.keyFor(sym),它是传入全局symbol变量,返回它的name,注意的是它只适用于全局的Symbol,非全局的Symbol会返回undefined。

1
2
3
4
5
6
7
// get symbol by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");

// get name by symbol
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id

Undefined

表示未定义,任何变量在未赋值之前都是Undefined类型、值为undefined。它与null还是有一些区别,null表示定义了但是值为空,undefined表示根本就没有定义。这里有一个要注意的问题,undefined在JavaScript中不是一个关键词,它可能会被改写的。一般建议用void 0来获取undefined的值。因为 void 运算会把任意一个表达式变成 undefined,也就是说不管void后面跟什么返回的都是undefined。不少 JavaScript 压缩工具在压缩过程中,也是将 undefined 用 void 0 代替掉了。

Null

Null表示空值。前面也说过它代表着定义了但是值为空,它与undefined不同,null是JavaScript的关键词,可以放心的使用。

Boolean

这个很简单就两个值,true和false。

String

在JavaScript中最重要的一点就是,String是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。
ES7确定了字符串的最大长度为2^53-1。但是在老版本V8中字符串最大长度限制在256MB(2^28 - 16),而在新版本中32位平台还与之前一样,64位平台的最大长度将限制在2^(32-1)-1-24大约1GB。
但是我们要知道JavaScript中String的最大长度,并不是字符数。
要讲清楚这个概念,我们就不得不引入码点和代码单元的概念。
码点是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,码点采用十六进制书写,并加上前缀U+,例如U+0041 就是拉丁字母A 的码点。Unicode的码点可以分成17个级别。
第1个代码级别称为基本字符区域(BMP),(U+0000 - U+FFFF),在基本字符区域中,每个字符用16位表示,通常被称为代码单元,换句话说也就是在基本字符区域内一个码点需要一个代码单元,一个码点对应一个字符。
而其余的16个级别,码点从U+10000 到U+10FFFF , 其中包括一些辅助字符(supplementary character)。而辅助字符采用一对连续的代码单元进行编码。也就是需要两个代码单元。
总结一下:一个字符对应一个码点(且每个字符的码点都是唯一的),但一个码点可能需要一个代码单元或者两个代码单元才能表示。
而JavaScript字符串把每个UTF16单元当做一个字符来处理,所以处理非BMP的时候就需要注意。比如 “𝌷”的length就为2。
一个空的字符串的length为0.
如果你要是直接访问String.length返回的是1

1
console.log(String.length) // 1

这是因为String中的这个length其实继承于Function.prototype的。函数实例的length为声明的参数长度,String接受一个value参数,所以length为1。

Number

在JavaScript中Number这种类型使用 IEEE754 格式来表示
整数和浮点数值(浮点数值在某些语言中也被称为双精度数值)。接下来我们讲讲什么是IEEE754

IEEE754

我们先记住几个基础知识,十进制小数点向左移动1位相当于将该数除以10,向右移动1位相当于将该数乘以10。同理,二进制小数点向左移动1位相当于将该数除以2,向右移动1位相当于将该数乘以2。

我们可以根据上面的概念,引入科学计数法。一个很大或者很小的数可以表示为:

1
2
3
a * 10^n
|a|>=1 且 |a|<10
n为整数

如果用科学记数法来表示二进制数:

1
2
3
4
a * 2^n
指数基数为2
|a|>=1且|a|<2,也就是说a的范围包含两个区间(-2,-1]、[1,2)
n为整数

浮点数

在计算器科学中,浮点是一种对于实数的近似值数值表示法,由一个有效数字(即尾数)加上幂数来表示,通常是乘以某个基数的整数次指数得到。以这种表示法表示的数值,称为浮点数。

IEEE 754规定了四种表示浮点数值的方式

  • 单精度(32位)
  • 双精度(64位)
  • 延伸单精度(43比特以上,很少使用)
  • 延伸双精度(79比特以上,通常以80位实现)

JavaScript使用的是双精度浮点型。

二进制浮点数由三部分组成

  1. 符号位s(sign bit),0表示正数,1表示负数。
  2. 阶码e(exponent bias),规定为实际指数值加上一个偏移值。偏移值为2^(n-1)−1,其中的n为存储指数的比特位长度。
    由于科学记数法的指数可为正,可为负。因此,一个数转换为浮点数之后,符号位、阶码这两部分都是带符号的。
    如果我们要比较两个浮点数的大小,那么除了要判断比较数值本身的符号位,还需要再判断比较阶码的符号位,最后才是非符号部分的比较。显然,这会复杂化比较逻辑。
    在使用了偏移值之后,无论指数部分是正是负,都可以转换为非负数。将真值映射到正数域的数值(真值在数轴上正向平移一个偏移量),称为移码。使用移码来比较两个真值的大小比较简单,只要高位对齐后逐位比较即可,不用考虑符号位问题。
  3. 尾数f(fraction),用于存储“有效数字”的小数部分,使用原码表示。

在64位双精度浮点型:

  1. 符号位,占1bit
  2. 指数为,占11bit
  3. 尾数,占52bit

在32位单精度浮点型:

  1. 符号位,占1bit
  2. 指数为,占8bit
  3. 尾数,占23bit

IEEE 754计算

  1. 如果指数位全为1,且尾数不为0,就超过了最大值,这样的数就无法表示,表示为NaN,此时无视符号位。这就是为什么NaN != NaN。因为NaN有不同的可能。
  2. 如果指数位全为1,尾数为0的话,根据符号位得到两个特殊值,符号位为0就是正无穷Infinity,符号位为1的话就得负无穷-Infinity。
  3. 如果指数位和尾数位都为0的话,就会根据符号位表示正负0。
  4. 在正常的浮点数区间,那么对应的数字的计算公式为:(-1)^s x(2^(e-1023))x(1.f) 对于e-1023和1.f可能会有些疑惑。其中 1023 代表指数的偏移量,那么为什么会有这个偏移量呢,首先,作为11位的阶码,如果是无符号,那么表示的范围为[0-2047], 当处于我们第三种情况时,e的范围要去掉0和2047,变成[1,2046] ,如果要表示正负,则需要使用一个bit去作为符号位,范围为[-1022, 1023] ,如果没有偏移量,那么需要引入补码,计算更加复杂,为了简化运算,则使用无符号的阶码,引入了偏移量的概念,通过偏移将其转换成[1,2046] ,所以偏移量为 1023。 那么对于1.f来说,因为对于浮点数,在规范中采用科学计数法的方法,对于十进制12.34来说,用科学技术法表示为 1.234×10^2,不会表示为 0.1234×10^3,其首位肯定是一个不为0的数字,那么在二进制中,只有0和1,那么他的首位就只能是1(用科学计数法表示,首位不能为0)。因为对于所有的浮点数他的首位都是1,因此可以省略一位进行,不需要额外占用1位,所以f实际上的有效位数有53位(其中首位是1)。在计算时需要使用上这个隐含的1,这也是1.f的由来.
  5. 当指数为0且尾数不为0时,是非规范化的数字,用于表示那些非常接近0的数字。

从上面的分类我们得知,前两种是非正常的值,第一种含有 2^52 种情况(尾数有52位),第二种含有两种情况(正无穷和负无穷),那么根据排列组合,共有 2*1*2^52 = 2^53 种情况。这些非正常的值在JS中以 NaN、+Infinity、-Infinity表示,因此在Number中多了三种情况。那么总数就是 2^64-2^53+3

Number数值范围

Number的数值范围为-2^53 到 2^53。由上可知在双精度浮点型,尾数占52bit又由于整数位的第一位肯定为1,所以第一位可以省略掉,从而多了一位。

浮点数精度问题

因为十进制小数转成二进制会变成无限循环小数,它就变得不准确了。
从十进制数转为二进制数,小数的转换采用的乘2取整的过程。这必然就会有无限不循环的情况产生。这样用计算机存储产生截取是必然的,必定会有一定的精度损失!

遇到浮点数的比较,正确的方法应该是使用JavaScript提供的最小精度值:

1
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。

BigInt

这个是ES2020新增的,我们从上得知Number的最大整数是2^53-1。如果超过了这个数就会有问题。而BigInt的出现就再也不用担心这个问题。

创建BigInt的方式也很简单

  1. 直接在整数后面加n
    let a = 123n;
  2. 调用BigInt函数
    let a = BigInt(123);

BigInt运算

就像Number运算一样,只不过在做除法运算的时候会舍去小数部分。

1
2
1n + 2n  // 3n
1n / 2n // 0n

我们要注意BigInt类型和Number类型不能混用。
在BigInt和Number直接可以用BigInt()和Number()进行显示转换。但是如果BigInt超过了Number,会进行截断。
BigInt也不支持一元加法,我们知道可以用+”value”,将String类型的转为Number类型。这在BigInt中并不支持。

比较运算符,例如 < 和 >,使用它们来对 bigint 和 number 类型的数字进行比较没有问题。
由于 number 和 bigint 属于不同类型,它们可能在进行 == 比较时相等,但在进行 ===(严格相等)比较时不相等。

在进行布尔运算时与Number一样,BigInt 0nfalse,其他值为 true

Object

Object就是一组数据和功能的集合。对象通过new操作符后面跟着要创建的对象类型的名称来创建。

let o = new Object();  //如果不传参数的话,可以省略圆括号但是不推荐。

Object是所有类的基类。Object类所具有的任何属性和方法也同样存在于更具体的对象中。Object的每个实例都具有下列的属性和方法。

  1. constructor
    用于创建当前对象的函数,也就是构造函数。
  2. hasOwnProperty
    检测给定的属性是否在当前对象实例中,可以是String也可以是Symbol。
  3. isPrototypeOf
    检测一个对象是否是另一个对象的原型。或者说一个对象是否被包含在另一个对象的原型链中。就是说如果B继承与A,那么A就是B的原型。A.isPrototypeOf(B) === true
  4. propertyIsEnumerable
    此方法可以确定对象中指定的属性是否可以被 for…in 循环枚举,但是通过原型链继承的属性除外。如果对象没有指定的属性,则此方法返回 false。
  5. toLocaleString
    返回对象的字符串表示形式,针对当前语言环境进行了本地化。但只有Array、Date和Number重写了此方法。其他的只是默认调用toString方法。
  6. toString
    每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 “[object type]”,其中 type 是对象的类型。
  7. valueOf
    valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString()方法的返回值
    相同。

Number、String和Boolean,这三个构造方法,在new的时候产生与其基本类型对应的对象类型。而直接调用的时候表示强制类型转换。

Symbol、和BigInt比较特殊,直接用new的时候回报错。

我们发现有时候对象可以直接用在基本类型上,比如字符串字面量可以直接调用String类型的方法,这是因为
JavaScript运算符提供了装箱操作,他会根据基础类型构造一个临时对象,使我们能在基础类型上调用对于对象的方法。

什么是语义化?

语义化说白了,它是给机器看的,就是让机器能读懂你的内容,让机器知道你哪里是标题,哪里是段落。正因为如此。语义化最大的优点就是利于搜索引擎的抓取和建立索引也就是俗称SEO。除此之外,正确的使用语义化标签能够清晰的展示网页的结构,对开发者也更加友好,除此之外语义化还可以支持读屏软件使一些视障人士也可以无障碍的使用我们的网站。

语义化初步

我们来写一个最简单布局,这个布局就包括,头部,内容,和尾部从上到下依次排列。这也是最常见的网页布局。在没有语义化得时候,我们通常是使用div和给一个相对含义的className或者id的形式来布局

1
2
3
<div id="header></div>
<div class="section"></div>
<div id="footer"></div>

我们用语义化的形式来改写它。

1
2
3
<header></header>
<section></section>
<footer></footer>

这样看起来结构就清楚了很多。通过上面的例子我们也可以很容易看出,语义化可以使我们写出一致的代码。在不使用语义化标签的时候一个header就有很多种写法<div class="header"><div id="header">等等,这完全取决于每个开发者个人的风格。而使用语义化标签后,就让代码一致性变得更加的容易。

语义化标签介绍

<header> 和 <hgroup>

header标签很好理解,一般都在文档的最顶部。通常包含标题以及导航和搜索工具。它不一定非得是网页的头部,也可以是任意section或者article的头部部分。它也没有个数限制。
我们知道h1-h6是最基本的标题,表示文章中不同层级的标题。但有时候我们想让副标题在同一个层级中,这是我们就需要用到hgroup标签。hgroup标签实际就是一个标题组。

<nav>

nav 元素代表页面的导航链接区域。用于定义页面的主要导航部分。导航部分的常见示例是菜单,目录和索引。

1
2
3
4
5
6
7
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>

<section> 和 <article>

article它表示具有一定独立性质的文章,它的特点就是独立和可重用。每个article里面都可以有自己的header、section、footer。

1
2
3
4
5
6
7
<article>
<h1>你好,我是这边文章的标题</h1>
<p>你好,我是文章的内容</p>
<footer>
<p>最终解释权归XXX所有</p>
</footer>
</article>

section的主要作用是按主题分段。也就是说section中的内容跟大的主题是相关的。而且在section中会自动给标题h1-h6降级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<article>

<h1>web语义化</h1>
<p>什么是语义化?</p>

<section>
<h2>语义化详解</h2>
<p>语义化就是。。。</p>
</section>

<section>
<h2>语义化特点</h2>
<p>语义化特点就是。。。</p>
</section>

</article>

<aside>

aside一般是跟文章主题不相关的部分,可能包含导航或者广告之类的东西。一般在aside中的nav多数是关联页面,它与header中的nav有些许不同。

一般有header标签,也会有footer标签。通常他都是在文档的最后,通常都是放一些作者信息,版权信息,友情链接之类的。

<small>

<small>标签呈现的小号字体的效果,表示注释和附属细则,一般在footer中,包裹着版权信息。

1
<footer><small>&copy;Company A</small> Date</footer>

<time>

在出现日期的地方,我们可以加上

1
<time datetime="2020-11-08T11:21:00+08:00">2020年11月8日,星期二</time>

<figure> 和 <figcaption>

当图片不是独立的出现,当它与主文章相关,我们就可以使用figure标签。这种插入不限于图片,也可以是代码、表格等。figcaption表示内容的标题。

1
2
3
4
<figure>
<img src="#" />
<figcaption>title</figcaption>
</figure>

我们以一个Button组件为例。尝试从设计到实现一个React组件。

按钮的设计

轮廓线(按钮的边框)

主要的作用就是按钮与背景色相近的时候让按钮更加的明显。
成像效果不好的显示器,显示效果会有偏差。加上轮廓线确保显示。

类型检测

静态类型检测是写组件必不可少的。一方面是在开发时避免类型问题,它还可以通过IDE的智能提醒方便使用组件的开发者。将使用Typescript对组件进行类型检测声明。

Props声明

首先是Props声明,Props声明我们用组件名后面跟Props的形式来定义接口名称,并将它导出。

1
2
3
4
5
export interface BaseButtonProps {
type?: ButtonType;
children?: React.ReactNode;
...
}

在定义props的时候,我们要保持接口小,props数量要少。

组件声明

在组件使用中,应该无状态组件>有状态组件>Class组件。尤其是在hook出现后,函数组件被React大肆推广。

函数组件

我们用FC来声明函数组件,FC就是FunctionComponent的缩写,我们可以通过源码看到,它的props默认定义了children。

1
2
3
const ButtonBase:FC<BaseButtonProps> = props => {
return <div>{props.children}</div>
}

然后将这个组件导出

1
export default Button;

但是在之前FC类型来声明函数组件时并不能完美支持propsDefault(现在已经解决)。
一般用解构赋值的默认值来代替propsDefault

1
2
3
4
const BaseButton:FC<BaseButtonProps> = props => {
const { type = "default" } = props;
return <div>{props.children}</div>
}

也有直接使用普通函数来进行组件声明

1
2
3
4
const BaseButton = (props: BaseButtonProps): JSX.Element => {
const { type = "default" } = props;
return <div>{type}</div>
}

用普通函数来进行组件声明还有以下好处:

  1. React.FC隐式的提供了一个children props,如果你的组件不需要子组件,用普通函数组件也许是一个很好的选择
  2. 支持泛型。例如

    1
    2
    3
    4
    5
    6
    7
    type OptionValue = string | number;

    type Option<T extends OptionValue> = {
    value: T;
    };

    const App = <T extends OptionValue>(props: Option<T>): JSX.Element => <div>{props.value}</div>
  3. 在使用defaultProps不会出错。

【注意】在使用普通函数来进行组件声明的时候,defaultProps的类型和组件本身的props没有关联性,会使得defaultProps无法得到类型约束。比如

1
2
3
4
5
6
7
interface ButtonProps {
name: string;
}

const Button = (props: ButtonProps) => <div>{props.name}</div>

Button.defaultProps = { name: 123 } //这样是不会报错的

我们需要改写成这样

1
2
3
4
5
type Partial<T> = {
[P in keyof T]?: T[P];
};

Button.defaultProps = { name: 'Liam' } as Partial<ButtonProps>

概念

树的结构就很像生活中的树,一个枝干上分出很多的树杈。 在数据结构中,我们将树中的每个元素称为节点。没有父节点的叫做根节点。没有子节点的节点叫做叶子节点。如果多个节点的父节点是同一个节点,那么我们就将它们称为兄弟节点。
树还有 高度、深度、层。这三个概念

  • 节点的高度: 节点到叶子节点的最长路径,也就是边数
  • 节点的深度: 根节点到这个节点所经历的边的个数
  • 节点的层数: 节点的深度+1
  • 树的的高度: 根节点的高度

二叉树

每个节点上最多有两个叉。

满二叉树

除了叶子节点之外,每个节点都有左右两个子节点。

完全二叉树

最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大。

存储方式。

  1. 链式存储法。 每个节点有三个字段,一个存储数据,另外两个是指向左右节点的指针。
  2. 数组存储法。根节点在i=1的位置,左节点存储在2 i的位置,右节点存储在2 i + 1的位置。反过来 i/2存储的就是父节点的位置。 如果是完全二叉树,数组存储方式是最不占空间的。堆就是一种完全二叉树。

二叉树的遍历

  • 前序遍历是指,对于树中的任意节点来说,先打印这个节点,然后再打印它的左子树,最后打印它的右子树。
  • 中序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
  • 后序遍历是指,对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。

在之前的文章中,介绍过JavaScript的typeof运算符。在JavaScript中typeof主要是用于类型检测。
而在TypeScript中typeof不光能类型检测,还能用来获取类型。
具体是什么,得结合环境来看。
举一个简单地例子

1
2
3
let bar = { a: 0 }
let b = typeof bar // "object"
type c = typeof bar // "{a: number}"

我们可以看出来,如果typeof赋值给一个变量的话,那将用JavaScript运算符。 如果赋给一个type的话,就是获取它的类型。TypeScript将其对象中的a属性自动推断为number类型。

也可以获取类型通过一个字符数组

1
2
3
const data = ['text 1', 'text 2'] as const;
type Data = typeof data[number];
// type Data = "text 1" | "text 2"

或者是一个tuple

1
2
3
4
5
6
export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;

const animals = tuple('cat', 'dog', 'rabbit', 'snake');
type Animal = (typeof animals)[number];
//'cat' | 'dog' | 'rabbit' | 'snake'

还可以与keyof结合,将对象的key转为联合类型。

1
2
3
4
5
6
7
const datas = {
key1: 'value1',
key2: 'value2',
key3: 'value3',
}
type data = keyof typeof datas;
// type data = "key1" | "key2" | "key3"

旧的生命周期

constructor构造函数

构造函数的主要作用就是1、通过this.state来初始化内部的state。2、为事件函数绑定this。在组件挂载之前,会调用它的构造函数。

我们知道如果在为 React.Component 子类实现构造函数时,应在其他语句之前前调用 super(props)。否则,this.props 在构造函数中可能会出现未定义的 bug。我们可以来想想为什么每次都必须要写super,我们可以不写它么?

首先,super指的是父类的构造函数。在调用super之前,我们是不能使用this的。为什么这样做呢,我们试想一下,如果在调用super之前不禁止this,我们来看看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Father {
constructor(name) {
this.name = name
}
}

class Son extends Father {
constructor(name) {
this.greet();
super(name)
}
greet() {
console.log('hello my name is' + this.name);
}
}

我们可以看出在这个例子中,this.greet()在super()给this.name赋值之前就已经执行了。这时候this.name尚未定义。为了避免这种情况,强制在使用this之前先行调用super。
下面我们看看为什么要传入props么?

我们可以看看React内部

1
2
3
4
5
6
class Component {
constructor(props) {
this.props = props;
// ...
}
}

在React内部,的构造方法里我们一目了然,它直接将props赋值给this.props。
即便你调用super()的时候没有传入props,你依然能够在render函数或其他地方访问到this.props。这是因为,React 在调用构造函数后也立即将 props 赋值到了实例上

1
2
3
 // React 内部
const instance = new YourComponent(props);
instance.props = props;

尽管这样,我们也要避免使用super()而使用super(props),因为如果使用super(),会使得 this.props 在 super 调用一直到构造函数结束期间值为 undefined。

【注意】避免将props的值直接赋值给state。因为这样做毫无意义,并且props改变的时候,state并不改变。

componentWillMount

这是一个即将被废弃的方法。而且16.3已经将这个更名为UNSAFE_componentWillMount。它在挂载组件前调用,在componentWillMount中执行setState是毫无意义的。因为组件只挂在一次,componentWillMount也只执行一次,应该把这里的setState放到constructor中。
之前讨论的最多的就是异步请求api的时候,应该在componentWillMount里还是componentDidMount中,网上有一种说法是,放在componentWillMount中异步的请求数据,先会render一次空数据,等数据回来之后再走一遍render,所以应该放到componentDidMount中,但我觉得这种说法是不可靠的。因为在DidMount之前,同样也会render一遍空数据。早发起请求确实能早点获得结果,但是省的这几微妙根本微不足道。再加之WillMount即将被废弃。所以应该避免使用它。还有一点要注意,它是服务器渲染上调用的唯一方法。

render

render()是在componentWillMount()和componentWillReceive()之后调用。主要的作用是渲染组件,它是在class组件中唯一必须实现的方法。render应该为纯函数,意味着在state不变的情况下。每次调用返回的结果都应该相同。

componentDidMount

该方法会在render方法后立即执行,它跟componentWillMount一样也是永远只执行一次。在这里可以对DOM进行操作,因为这时组件已经加载完毕。之前我们也说了,网络请求数据的操作也应该放在这里。需要注意的是render函数结束之后,不会立即调用componentDidMount。是因为render函数本身并不往DOM树上渲染或者装载内容,它只是返回一个JSX表示的对象,然后由React库来根据返回对象决定如何渲染。而React库需要把所有组件返回的结果综合起来。才知道该如何产生对应的DOM修改。只有React库调用了全部render后,再依次调用各个组件的componentDidMount函数作为装载过程的收尾。

1
2
3
4
5
6
7
8
9
componentWillMount First
render First
componentWillMount Second
render Second
componentWillMount Third
render Third
componentDidMount First
componentDidMount Second
componentDidMount Third

componentWillReceiveProps

这个的用法主要是,需要根据新的prop改变状态,可以比较this.props和nextProps,并使用setState()执行state转换。
一般对这个方法有一个误区,就是它会通常被认为当props改变了它才会被调用,但事实是,只要是父组件的render函数被调用,在render函数里面被渲染的子组件就会经历更新过程,不管父组件传给子组件的props有没有改变,都会触发子组件的componentWillReceiveProps。我们唯一能保证的就是当props改变的时候,一定会触发componentWillReceiveProps,反之不然。注意在setState触发的更新过程是不会调用这个方法的。我们在使用的时候需要避免,在这个方法中调用父组件的回调函数修改父组件的state,这样父组件rerender会导致死循环。
同样这个方法在React 17中将被废弃。
想要使用应该写成UNSAFE_componentWillReceiveProps

shouldComponentUpdate

根据shouldComponentUpdate的返回值,判断React组件的输出是否受当前state或props更改的影响。意思就是判断组件是否重新渲染。默认行为state每次发生改变的时候组件都会重新渲染。我们要注意在首次渲染或使用forceUpdate()时不会调用该方法。在官方介绍中,说应该考虑使用PureComponent组件,而不是手动编写shouldComponentUpdate()。PureComponent会对props和state进行浅层比较。【注意】 返回 false 并不会阻止子组件在 state 更改时重新渲染。

componentWillUpdate

这个方法很简单,就是当组件收到新的props或state时,会在渲染之前调用这个方法。初始渲染不会调用它。
同样这个方法在React 17中将被废弃。
想要使用应该写成UNSAFE_componentWillUpdate

componentDidUpdate

componentDidUpdate会在更新后会被立即调用。首次渲染不会执行此方法。也可以在这个函数中直接调用setState(),但它必须包裹在一个条件语句中。要不然就会导致死循环。也不要将props镜像给state,应该直接使用props。

componentWillUnmount

componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等。
componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。

新的生命周期

新增了两个生命周期getDerivedStateFromPropsgetSnapshotBeforeUpdate
我们先来看看图
React新生命周期

getDerivedStateFromProps

组件每次被rerender的时候,都会触发这个生命周期函数,无论是props更新,还是setState,还是调用forceUpdate(),并且它在组件挂载和后续更新时都会被调用。它应该返回一个对象来更新state,如果返回null则不更新任何内容。注意,getDerivedStateFromProps是一个静态函数,所以函数体内不能访问this,输出完全由输入决定。

getSnapshotBeforeUpdate

它的触发时间实在update发生的时候,render之后,组件dom渲染之前。这个函数的返回值会作为componentDidUpdate的第三个参数。它可以来替代componentWillUpdate。

还有两个生命周期函数

getDerivedStateFromError

这个生命周期函数会在子组件抛出一个错误之后被调用。它会接收到这个throw出来的参数,然后去return一个值去更新state来处理这个错误。设置错误边界可以让代码在出错的情况下,也能将错误显示到页面中,而不是出现空白页面。
一般使用static getDerivedStateFromError() 来渲染一个提示错误的UI,使用componentDidCatch() 来记录一些error的详细信息,错误调用栈等等

componentDidCatch

这个也是在后代组件抛出错误后被调用。 它应该用于记录错误之类的情况。如果要是降级渲染UI,还是应该用getDerivedStateFromError来处理。

父子组件生命周期执行顺序总结

当子组件自身状态改变时,不会对父组件产生副作用的情况下,父组件不会更新,不会触发父组件的生命周期。
当父组件中状态发生变化(包括子组件的挂载以及)时,会触发自身对应的生命周期以及子组件的更新。
还是洋葱圈模型,render 以及 render 之前的生命周期,则 父组件 先执行。render 以及 render 之后的声明周期,则子组件先执行,并且是与父组件交替执行。

Classnames 简单介绍

Classnames是一个开发过程中经常能用到的类库。它的作用就是将多个className拼接到一块,还可以加入条件判断。

仓库地址:https://github.com/JedWatson/classnames

安装方式

1
2
3
4
5
6
7
8
# via npm
npm install classnames

# via Bower
bower install classnames

# or Yarn (note that it will automatically save the package to your `dependencies` in `package.json`)
yarn add classnames

我们可以先来简单地看一下它的用法。

  1. 最基础的两个className拼接,接收多个className返回拼接好的字符串

    1
    classNames('foo', 'bar'); // => 'foo bar'
  2. 也可以接收对象,对象的key作为要拼接的className,key所对应的value为一个布尔值,来表示是否拼接。true拼接,false不拼接。一般用于控制一个元素的显隐。

    1
    2
    classNames('foo', { bar: true }); // => 'foo bar'
    classNames('foo'. { bar: false }); // => 'foo'
  3. 也可以传一个数组,就像这样:

    1
    2
    var arr = ['b', { c: true, d: false }];
    classNames('a', arr); // => 'a b c'
  4. 与ES6的字符串模板配合,使用动态class name:

    1
    2
    let buttonType = 'primary';
    classNames({ [`btn-${buttonType}`]: true });

源码初探

源码很简单,大概30多行。我们可以先来简单的看一下整个源码。

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
var hasOwn = {}.hasOwnProperty;

function classNames() {
var classes = [];

for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;

var argType = typeof arg;

if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg)) {
if(arg.length) {
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
} else if (argType === 'object') {
if (arg.toString !== Object.prototype.toString) {
classes.push(arg.toString());
} else {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
}

return classes.join(' ');
}

接下来我们来详细分析一下。
var hasOwn = {}.hasOwnProperty;
这一行我们先不看。我们直接进入方法。

  1. 看第一个for循环:
    作用就是遍历这个方法传入的参数,然后判断参数,如果为falsy(如:null,undefined,false,’’)就直接返回。接下来就判断参数类型,根据类型分别进不同的判断条件。这里有两个知识点【arguments对象】和【JS的类型判断】。
  2. 第一个if条件很简单,如果传入参数为string类型或者number类型的话,就直接push到classes中,最后直接join(‘ ‘)即可。
  3. 第二个if,就是判断传入的参数是数组的时候。这里有一点,把判断为数组放在判断是否为对象之前因为type Array 返回的也是”object”,数组也是对象这很正常。这块主要就是递归调用这个方法。也是存在两个知识点【JS的数组判断】和【JS的递归实现
  4. 最后一个if就是当传入的参数为一个对象的时候。
    arg.toString !== Object.prototype.toString就是判断,是否是Object对象。如果不是的话就直接push toString()的结果。
    如果是Object对象的话。直接用for…in遍历它。这里有一个判断是否是对象它自身的属性。如果既是自身属性,属性值又为真的时候。就将这个属性名(也就是key)push到classes数组中。至此整个源码就分析完毕了。在这里if里涉及到的知识点有。【Object.prototype.toString() 与Object.toString()的区别】、【for…in陷阱】、【为什么不直接用hasOwnProperty判断】。

知识点讲解

arguments 对象

常用方法

arguments是一个数组对象,它的prototype并不是指向Array而是指向Object,代表传入function的参数列表。
一般通过数组下标的方式来访问每一个参数,如arguments[0]、arguments[1]]。
通过arguments.length 来获取传入了几个参数。

arguments转数组

在ES6之前,通常用Array.prototype.slice.call(arguments);或者更简单地方法[].slice.call(arguments)来将arguments对象转换为数组。

简单地讲一下,一般slice()方法,一般用于返回一个新的数组对象,这一对象是一个由传入的两个参数begin和end决定的原数组的浅拷贝(包括begin,不包括end)。原数组不会改变。
我们知道JS中,Array也不是真正的Array,也是一个类数组。于是只要我们构建一个类数组,将this绑定到该类数组上,不传入begin和end就可以实现该类数组转数组。 这块比较抽象。
ES6出来之后,我们就可以愉快地用Array.from()方法,来对类数组对象转为数组对象。

另外,有一个需要注意的地方就是,不能将函数的 arguments 泄露或者传递出去。如果将arguments对象泄漏出去了,最终的结果就是V8引擎将会跳过优化,导致相当大的性能损失。

修改arguments的值

这要分严格模式,和非严格模式。
在严格模式下,参数与arguments对象没有联系,修改一个值不会改变另一个值。而在非严格模式下两个会互相影响。
如果参数没有传入,失去绑定关系

用arguments模拟重载

1
2
3
4
5
6
7
8
9
10
11
function add(num1, num2, num3) {
if (arguments.length === 2) {
console.log("Result is " + (num1 + num2));
}
else if (arguments.length === 3) {
console.log("Result is " + (num1 + num2 + num3));
}
}

add(1, 2);
add(1, 2, 3)

caller 与 callee

讲到arguments,就顺带讲一下caller和callee

caller

返回调用指定函数的函数,如果是在全局作用内调用,则返回null。 如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数。
但是arguments.caller已经被废弃,被arguments.callee.caller所代替。

callee

callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。返回的就是fun函数本身。但在严格模式中不允许使用

我们还可以用它来验证传入的参数是否与形参相等

1
2
3
4
5
6
7
function fun(fun1, fun2){
if(arguments.length===arguments.callee.length){//function对象的length属性返回的是参数个数
console.log("参数正确");
} else{
console.log("参数不正确");
}
}

ES6 中的 arguments

  1. 扩展操作符

直接上栗子:

1
2
3
4
5
function func() {
console.log(...arguments);
}

func(1, 2, 3);

执行结果是:

1
1 2 3

简洁地讲,扩展操作符可以将 arguments 展开成独立的参数。

  1. Rest 参数

还是上栗子:

1
2
3
4
5
6
function func(firstArg, ...restArgs) {
console.log(Array.isArray(restArgs));
console.log(firstArg, restArgs);
}

func(1, 2, 3);

执行结果是:

1
2
true   
1 [2, 3]

从上面的结果可以看出,Rest 参数表示除了明确指定剩下的参数集合,类型是 Array。

  1. 默认参数

栗子:

1
2
3
4
5
6
function func(firstArg = 0, secondArg = 1) {
console.log(arguments[0], arguments[1]);
console.log(firstArg, secondArg);
}

func(99);

执行结果是:

1
2
99 undefined   
99 1

可见,默认参数对 arguments 没有影响,arguments 还是仅仅表示调用函数时所传入的所有参数。

JS类型判断

typeof

用typeof可以检测基本类型函数类型

6大原始类型Null、Undefined、String、Number、Boolean和Symbol。
【注意】 typeof null 返回的是 object。

我们来谈谈为什么typeof null会返回object,这是一个历史遗留问题。
js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息

  1. 1:整型(int)
  2. 000:引用类型(object)
  3. 010:双精度浮点型(double)
  4. 100:字符串(string)
  5. 110:布尔型(boolean
    另外还用两个特殊值:
  6. undefined,用整数−2^30(负2的30次方,不在整型的范围内)
  7. null,机器码空指针(C/C++ 宏定义),低三位也是000

所以,typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为0,因此直接被当做了对象来看待。

instanceof

基于原型链进行判断,一般用于判断对象类型。
instanceof左操作数,为一个对象 如果不是对象会直接返回false。右操作数为一个函数构造器。

instanceof可以来判断一个实例是否属于某种类型,也可以判断一个实例是否是其父类型或者祖先类型的实例。

instanceof 主要的实现原理就是只要右边变量的prototype在左边变量的原型链上即可。因此instanceof在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。

一般用于自定义对象实例判断。

1
2
3
4
5
6
7
8
let Person = function () {
}
let Programmer = function () {
}
Programmer.prototype = new Person()
let liam = new Programmer()
liam instanceof Person // true
liam instanceof Programmer // true

Object.prototype.toString

这是一个不错的判断类型的方法。但是要注意,在IE 6、7、8的时候, null和undefined会返回”[object Object]”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Object.prototype.toString.call(1) // "[object Number]"

Object.prototype.toString.call('hi') // "[object String]"

Object.prototype.toString.call({a:'hi'}) // "[object Object]"

Object.prototype.toString.call([1,'a']) // "[object Array]"

Object.prototype.toString.call(true) // "[object Boolean]"

Object.prototype.toString.call(() => {}) // "[object Function]"

Object.prototype.toString.call(null) // "[object Null]"

Object.prototype.toString.call(undefined) // "[object Undefined]"

Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"

constructor

constructor含义就是指向该对象的构造函数,每个对象都有构造函数,有可能是本身拥有也有可能是继承而来。所有函数和对象最终都是由Function构造函数得来。

1
2
const arr = []
console.log(arr.constructor); // Array

定义一个数组arr,然后我们取arr的constructor
它本身没有constructor属性。
就从arr.proto中查找
arr.proto又指向Array.prototype
有因为Array.prototype.constructor === Array, 所以arr.constructor为Array

但是由于我们可以对原型的指向进行修改。所以检测结果有时并不准确。

duck type

通过特性嗅探来进行类型验证,比如Array类型,就通过判断是否有.sort或者.slice等等数组特有的方法来判断是否为数组。

JS对数组的判断

这个一般会单独拿出来讲,数组中内置了一个数据类型判断函数isArray,但会有兼容性问题。所以一般的写法如下:

1
2
3
isArray = Array.isArray || function(array){
return Object.prototype.toString.call(array) === '[object Array]';
}

JS递归

我们知道JS中实现一个递归跟别的语言一样还是很容易的。

1
2
3
function fn() {
return fn();
}

有一些问题,函数是在定以后绑定到fn这个变量上,如果解释器先解析函数体,再把函数绑到变量上,在解析函数体的时候fn这个变量还处于未定义。

进阶:用匿名函数来实现递归,这时就需要借助callee

1
2
3
function (n) {
return arguments.callee(n - 1);
}

要注意的是,这个在严格模式下不能使用。

Object.toString和Object.prototype.toString之间的区别

我们要注意Object.proto === Function.prototype;因为proto指向的是构造该对象的构造函数的原型。
而构造函数除了是方法它也是对象,而函数的构造函数就是Function,所以Ojbect.proto指向的就是Function.prototype。 而Function.prototype又是对象,它的构造函数就是Object,因此Function.prototype.proto指向的就是Object.prototype。

我们知道,Object本身是没有toString方法的,它会去它的原型链上去找,也就是proto上,于是。就找到了Function.prototype.toString 而不是 Object.prototype.toString。

一般对象都会在原型上实现自己的toString方法。所以源码中利用toString方法来判断是否是一个真正的Object对象。

for in陷阱

我们先来看看for in的定义,以任意顺序遍历一个对象的除Symbol以外的可枚举属性。
首先我们可以知道,一、for in是以任意顺序遍历;二、它遍历可枚举的属性,要注意的是它也会遍历原型链上可枚举的属性。
我们可以使用对象的hasOwnProperty()方法来避免这个问题。而在使用hasOwnProperty()的时候就引出了下面的问题。

为什么不直接用hasOwnProperty判断?

这很简单,因为 JS 不保护属性名hasOwnProperty,你可以轻松地重写它,就像这样。

1
2
3
4
5
var foo = {
hasOwnProperty: function() {
return false;
},
};

所以我们在使用hasOwnProperty的时候这样来使用它。
({}).hasOwnProperty.call 或者 Object.prototype.hasOwnProperty.call

React简介

React是一个UI组件库,核心部分解决的就是声明式渲染和组件问题。

什么是声明式?

声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做。它表达逻辑而不显式地定义步骤也就是说它没有描述具体的步骤。声明式编程的例子有HTML、SQL等。声明式就好像你去饭店点菜,你只需要告诉服务员你吃什么,并不需要关心后厨是怎么做的。

什么是命令式?

命令式是与声明式相对的,声明式描述了应该做什么,而命令式是描述了如何做。它会具体的描述每一个步骤。命令式就像自己照着食谱做饭,先洗菜,切菜,倒油,炒它,加调料,装盘。精确地定义好每一步,然后去实施。

什么是函数式编程?

函数式是声明式的一部分,既然是声明式的一部分,函数式也更加强调程序执行的结果而非执行的过程。JavaScript中的函数是第一类公民,就意味着函数跟其它的数据类型一样处于平等的地位,可以赋值给其他变量,可以作为参数传入另一个函数,也可以作为别的函数的返回值(这就是高阶函数)。函数式编程的目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用并减少对状态的改变。
我们可以看一下函数式的几个重要的概念。

不可变数据

在函数式编程中,不能更改数据。如果要改变或者更改数据,则必须复制数据副本来更改。

纯函数,拒绝副作用

纯函数实际上就是,没有副作用的函数,相同的输入有相同的输出。

引用透明

函数的返回值只依赖于其输入值。

React走的就是函数式宗教,在React声明式渲染中,有一个公式就是UI = render(data)开发者只需要维护可变的数据,React会帮助我们处理具体的DOM操作。

JSX

JSX是学习React必须知道的一个概念,JSX是一个语法糖,它既不是字符串也不是HTML,它是JavaScript的语法扩展。它就像一个拥有JavaScript所有功能的模板引擎。
React UI与逻辑 高耦合。与传统的html、css、js三种语言分在三种不同的文件里面不同。React根据同一件事,把实现这个功能的所有代码集中在一个文件里。

JSX中的onClick事件与HTML的onclick的处理方式有很大的不同。

JSX最终会通过Babel转译成为一个名为React.createElement()的函数调用。这就是为什么只要使用JSX就必须import React。

JSX防止注入攻击
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

元素的渲染。

React 元素是创建开销极小的普通对象。React DOM会负责更新DOM来与React元素保持一致。React只更新它需要更新的部分,ReactDOM会将元素和它的子元素的状态进行比较,只会进行必要的更新。

render

想要把一个React元素渲染到根DOM节点中,只需要把它们一起传入ReactDOM.render();
ReactDOM.render(element, container[, callback])
在container里渲染一个React元素,callback是可选的。将在组件渲染或者更新之后被执行。

render会控制你传入容器节点里的内容。当首次调用时,容器节点里的所有DOM元素都会被替换点。但是不会修改容器节点。
会返回对根组件ReactComponent实例的引用。但是要避免使用返回的引用。因为之后React版本中,组件渲染在某些情况下可能是异步的。

unmountComponentAtNode()

ReactDOM.unmountComponentAtNode(container)
从DOM中卸载组件,会将其event handlers和state一并清除。如果组件被移除返回true,如果没有组件可被移除将会返回false。

createPortal

Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。
ReactDOM.createPortal(child, container)
第一个参数是任何可渲染的React子元素,第二个参数则是一个DOM元素。

函数组件与class组件。

组件是React中重要的概念。这样就允许将你的UI拆分为独立可复用的代码片段。之前我们介绍过React的核心就是UI=render(data), 它接收任意的props,返回用于展示内容的React元素。而React中定义组件的两种方式

  1. 函数组件

    1
    2
    3
    function Welcome(props) {
    return <h1>Hello, {props.name}</h1>
    }
  2. 用ES6的class来定义组件

    1
    2
    3
    4
    5
    class Welcome extends React.Component {
    render() {
    return <h1>Hello, {this.props.name}</h1>
    }
    }

Props

讲到组件就离不开props。props的定义是,当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称之为 “props”。
通过定义我们可以知道,通过props可以获得两个东西,一是组件的属性,二是子组件。

<Component name="name"> // props.name 来取得name属性上的值
    <SubComponent> // props.children 来获取子组件
</Component>

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

State

State是私有的,并且完全受控于当前组件。state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。他与props类似,它的更改会触发reRender。
在使用state的时候要注意以下几点。

  • 不要直接修改State
    直接修改State,代码不会重新渲染组件。
    应该使用setState();
  • State的更新可能是异步的
    这点很重要,因为this.props和this.state可能会异步更新,所以不要依赖他们的值来更新下一个状态。
    要是解决这个问题,可以让setState接收一个函数,而不是对象。这个函数第一个参数是上一个state,此次更新被应用时的props做为第二个参数。
  • State的更新会被合并
    简单地说就是,state中包含多个独立的变量,可以在setState中单独个更新它们,setState中只用写其中的一个变量。它会完整保留其他变量,并完全替换了你要单独改变的变量。