关于前端:字节前端必会面试题持续更新中

5次阅读

共计 25857 个字符,预计需要花费 65 分钟才能阅读完成。

事件流传机制(事件流)

冒泡和捕捉

谈一谈 HTTP 数据传输

大略遇到的状况就分为 定长数据 不定长数据 的解决吧。

定长数据

对于定长的数据包而言,发送端在发送数据的过程中,须要设置Content-Length, 来指明发送数据的长度。

当然了如果采纳了 Gzip 压缩的话,Content-Length 设置的就是压缩后的传输长度。

咱们还须要晓得的是👇

  • Content-Length如果存在并且无效的话,则必须和音讯内容的传输长度完全一致,也就是说,如果过短就会截断,过长的话,就会导致超时。
  • 如果采纳短链接的话,间接能够通过服务器敞开连贯来确定音讯的传输长度。
  • 那么在 HTTP/1.0 之前的版本中,Content-Length 字段可有可无, 因为一旦服务器敞开连贯,咱们就能够获取到传输数据的长度了。
  • 在 HTTP/1.1 版本中,如果是 Keep-alive 的话,chunked 优先级高于Content-Length, 若是非 Keep-alive,跟后面状况一样,Content-Length 可有可无。

那怎么来设置Content-Length

举个例子来看看👇

const server = require('http').createServer();
server.on('request', (req, res) => {if(req.url === '/index') {
      // 设置数据类型
    res.setHeader('Content-Type', 'text/plain');
    res.setHeader('Content-Length', 10);
    res.write("你好,应用的是 Content-Length 设置传输数据模式");
  }
})

server.listen(3000, () => {console.log("胜利启动 --TinaTian");
})

不定长数据

当初采纳最多的就是 HTTP/1.1 版本,来实现传输数据,在保留 Keep-alive 状态下,当数据是不定长的时候,咱们须要设置新的头部字段👇

Transfer-Encoding: chunked

通过 chunked 机制,能够实现对不定长数据的解决,当然了,你须要晓得的是

  • 如果头部信息中有Transfer-Encoding, 优先采纳 Transfer-Encoding 外面的办法来找到对应的长度。
  • 如果设置了 Transfer-Encoding,那么 Content-Length 将被忽视。
  • 应用长连贯的话,会继续的推送动静内容。

那咱们来模仿一下吧👇

const server = require('http').createServer();
server.on('request', (req, res) => {if(req.url === '/index') {
      // 设置数据类型
    res.setHeader('Content-Type', 'text/html; charset=utf8');
    res.setHeader('Content-Length', 10);
    res.setHeader('Transfer-Encoding', 'chunked');

    res.write("你好,应用的是 Transfer-Encoding 设置传输数据模式");
    setTimeout(() => {res.write("第一次传输数据给您 <br/>");
    }, 1000);
    res.write("骚等一下");
    setTimeout(() => {res.write("第一次传输数据给您");
      res.end()}, 3000);
  }
})

server.listen(3000, () => {console.log("胜利启动 --TinaTian");
})

下面应用的是 nodejs 中 http 模块,有趣味的小伙伴能够去试一试,以上就是 HTTP 对 定长数据 不定长数据 传输过程中的解决伎俩。

transition 和 animation 的区别

  • transition 是适度属性,强调适度,它的实现须要触发一个事件(比方鼠标挪动下来,焦点,点击等)才执行动画。它相似于 flash 的补间动画,设置一个开始关键帧,一个完结关键帧。
  • animation 是动画属性,它的实现不须要触发事件,设定好工夫之后能够本人执行,且能够循环一个动画。它也相似于 flash 的补间动画,然而它能够设置多个关键帧(用 @keyframe 定义)实现动画。

两栏布局的实现

个别两栏布局指的是 右边一栏宽度固定,左边一栏宽度自适应,两栏布局的具体实现:

  • 利用浮动,将右边元素宽度设置为 200px,并且设置向左浮动。将左边元素的 margin-left 设置为 200px,宽度设置为 auto(默认为 auto,撑满整个父元素)。
.outer {height: 100px;}
.left {
  float: left;
  width: 200px;
  background: tomato;
}
.right {
  margin-left: 200px;
  width: auto;
  background: gold;
}
  • 利用浮动,左侧元素设置固定大小,并左浮动,右侧元素设置 overflow: hidden; 这样左边就触发了 BFC,BFC 的区域不会与浮动元素产生重叠,所以两侧就不会产生重叠。
.left{
     width: 100px;
     height: 200px;
     background: red;
     float: left;
 }
 .right{
     height: 300px;
     background: blue;
     overflow: hidden;
 }
  • 利用 flex 布局,将右边元素设置为固定宽度 200px,将左边的元素设置为 flex:1。
.outer {
  display: flex;
  height: 100px;
}
.left {
  width: 200px;
  background: tomato;
}
.right {
  flex: 1;
  background: gold;
}
  • 利用相对定位,将父级元素设置为绝对定位。右边元素设置为 absolute 定位,并且宽度设置为 200px。将左边元素的 margin-left 的值设置为 200px。
.outer {
  position: relative;
  height: 100px;
}
.left {
  position: absolute;
  width: 200px;
  height: 100px;
  background: tomato;
}
.right {
  margin-left: 200px;
  background: gold;
}
  • 利用相对定位,将父级元素设置为绝对定位。右边元素宽度设置为 200px,左边元素设置为相对定位,右边定位为 200px,其余方向定位为 0。
.outer {
  position: relative;
  height: 100px;
}
.left {
  width: 200px;
  background: tomato;
}
.right {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 200px;
  background: gold;
}

应用 clear 属性革除浮动的原理?

应用 clear 属性革除浮动,其语法如下:

clear:none|left|right|both

如果单看字面意思,clear:left 是“革除左浮动”,clear:right 是“革除右浮动”,实际上,这种解释是有问题的,因为浮动始终还在,并没有革除。

官网对 clear 属性解释:“元素盒子的边不能和后面的浮动元素相邻”,对元素设置 clear 属性是为了防止浮动元素对该元素的影响,而不是革除掉浮动。

还须要留神 clear 属性指的是元素盒子的边不能和后面的浮动元素相邻,留神这里“后面的”3 个字,也就是 clear 属性对“前面的”浮动元素是充耳不闻的。思考到 float 属性要么是 left,要么是 right,不可能同时存在,同时因为 clear 属性对“前面的”浮动元素充耳不闻,因而,当 clear:left 无效的时候,clear:right 必然有效,也就是此时 clear:left 等同于设置 clear:both;同样地,clear:right 如果无效也是等同于设置 clear:both。由此可见,clear:left 和 clear:right 这两个申明就没有任何应用的价值,至多在 CSS 世界中是如此,间接应用 clear:both 吧。

个别应用伪元素的形式革除浮动:

.clear::after{content:'';  display: block;   clear:both;}

clear 属性只有块级元素才无效的,而::after 等伪元素默认都是内联程度,这就是借助伪元素革除浮动影响时须要设置 display 属性值的起因。

画一条 0.5px 的线

  • 采纳 transform: scale()的形式,该办法用来定义元素的 2D 缩放转换:
transform: scale(0.5,0.5);
  • 采纳 meta viewport 的形式
<meta name="viewport" content="width=device-width, initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5"/>

这样就能缩放到原来的 0.5 倍,如果是 1px 那么就会变成 0.5px。viewport 只针对于挪动端,只在挪动端上能力看到成果

参考 前端进阶面试题具体解答

数组的遍历办法有哪些

办法 是否扭转原数组 特点
forEach() 数组办法,不扭转原数组,没有返回值
map() 数组办法,不扭转原数组,有返回值,可链式调用
filter() 数组办法,过滤数组,返回蕴含符合条件的元素的数组,可链式调用
for…of for…of 遍历具备 Iterator 迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历一般的 obj 对象,将异步循环变成同步循环
every() 和 some() 数组办法,some()只有有一个是 true,便返回 true;而 every()只有有一个是 false,便返回 false.
find() 和 findIndex() 数组办法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值
reduce() 和 reduceRight() 数组办法,reduce()对数组正序操作;reduceRight()对数组逆序操作

new 操作符的实现原理

new 操作符的执行过程:

(1)首先创立了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象增加属性)

(4)判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象。

具体实现:

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回后果
  return flag ? result : newObject;
}
// 应用办法
objectFactory(构造函数, 初始化参数);

程度垂直居中的实现

  • 利用相对定位,先将元素的左上角通过 top:50% 和 left:50% 定位到页面的核心,而后再通过 translate 来调整元素的中心点到页面的核心。该办法须要 思考浏览器兼容问题。
.parent {position: relative;} .child {position: absolute;    left: 50%;    top: 50%;    transform: translate(-50%,-50%);}
  • 利用相对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,因为宽高固定,因而对应方向实现平分,能够实现程度和垂直方向上的居中。该办法实用于 盒子有宽高 的状况:
.parent {position: relative;}

.child {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
}
  • 利用相对定位,先将元素的左上角通过 top:50% 和 left:50% 定位到页面的核心,而后再通过 margin 负值来调整元素的中心点到页面的核心。该办法实用于 盒子宽高已知 的状况
.parent {position: relative;}

.child {
    position: absolute;
    top: 50%;
    left: 50%;
    margin-top: -50px;     /* 本身 height 的一半 */
    margin-left: -50px;    /* 本身 width 的一半 */
}
  • 应用 flex 布局,通过 align-items:center 和 justify-content:center 设置容器的垂直和程度方向上为居中对齐,而后它的子元素也能够实现垂直和程度的居中。该办法要 思考兼容的问题,该办法在挪动端用的较多:
.parent {
    display: flex;
    justify-content:center;
    align-items:center;
}

常见的位运算符有哪些?其计算规定是什么?

古代计算机中数据都是以二进制的模式存储的,即 0、1 两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,行将符号位独特参加运算的运算。

常见的位运算有以下几种:

运算符 形容 运算规定
& 两个位都为 1 时,后果才为 1
` ` 两个位都为 0 时,后果才为 0
^ 异或 两个位雷同为 0,相异为 1
~ 取反 0 变 1,1 变 0
<< 左移 各二进制位全副左移若干位,高位抛弃,低位补 0
>> 右移 各二进制位全副右移若干位,负数左补 0,正数左补 1,左边抛弃

1. 按位与运算符(&)

定义: 加入运算的两个数据 按二进制位 进行“与”运算。运算规定:

0 & 0 = 0  
0 & 1 = 0  
1 & 0 = 0  
1 & 1 = 1

总结:两位同时为 1,后果才为 1,否则后果为 0。
例如:3&5 即:

0000 0011 
   0000 0101 
 = 0000 0001

因而 3&5 的值为 1。
留神:正数按补码模式加入按位与运算。

用处:

(1)判断奇偶

只有依据最未位是 0 还是 1 来决定,为 0 就是偶数,为 1 就是奇数。因而能够用 if ((i & 1) == 0) 代替 if (i % 2 == 0) 来判断 a 是不是偶数。

(2)清零

如果想将一个单元清零,即便其全副二进制位为 0,只有与一个各位都为零的数值相与,后果为零。

2. 按位或运算符(|)

定义: 加入运算的两个对象按二进制位进行“或”运算。

运算规定:

0 | 0 = 0
0 | 1 = 1  
1 | 0 = 1  
1 | 1 = 1

总结:加入运算的两个对象只有有一个为 1,其值为 1。
例如:3| 5 即:

0000 0011
  0000 0101 
= 0000 0111

因而,3| 5 的值为 7。
留神:正数按补码模式加入按位或运算。

3. 异或运算符(^)

定义: 加入运算的两个数据按二进制位进行“异或”运算。

运算规定:

0 ^ 0 = 0  
0 ^ 1 = 1  
1 ^ 0 = 1  
1 ^ 1 = 0

总结:加入运算的两个对象,如果两个相应位雷同为 0,相异为 1。
例如:3| 5 即:

0000 0011
  0000 0101 
= 0000 0110

因而,3^5 的值为 6。
异或运算的性质:

  • 交换律:(a^b)^c == a^(b^c)
  • 结合律:(a + b)^c == a^b + b^c
  • 对于任何数 x,都有 x^x=0,x^0=x
  • 自反性: a^b^b=a^0=a;

4. 取反运算符 (~)

定义: 加入运算的一个数据按二进制进行“取反”运算。

运算规定:

~ 1 = 0~ 0 = 1

总结:对一个二进制数按位取反,行将 0 变 1,1 变 0。
例如:~6 即:

0000 0110= 1111 1001

在计算机中,负数用原码示意,正数应用补码存储,首先看最高位,最高位 1 示意正数,0 示意负数。此计算机二进制码为正数,最高位为符号位。
当发现按位取反为正数时,就 间接取其补码,变为十进制:

0000 0110   = 1111 1001 反码:1000 0110 补码:1000 0111

因而,~6 的值为 -7。

5. 左移运算符(<<)

定义: 将一个运算对象的各二进制位全副左移若干位,右边的二进制位抛弃,左边补 0。
设 a=1010 1110,a = a<< 2 将 a 的二进制位左移 2 位、右补 0,即得 a =1011 1000。
若左移时舍弃的高位不蕴含 1,则每左移一位,相当于该数乘以 2。

6. 右移运算符(>>)

定义: 将一个数的各二进制位全副右移若干位,负数左补 0,正数左补 1,左边抛弃。
例如:a=a>>2 将 a 的二进制位右移 2 位,左补 0 或者 左补 1 得看被移数是正还是负。
操作数每右移一位,相当于该数除以 2。

7. 原码、补码、反码

下面提到了补码、反码等常识,这里就补充一下。
计算机中的 有符号数 有三种示意办法,即原码、反码和补码。三种示意办法均有符号位和数值位两局部,符号位都是用 0 示意“正”,用 1 示意“负”,而数值位,三种示意办法各不相同。

(1)原码

原码就是一个数的二进制数。例如:10 的原码为 0000 1010

(2)反码

  • 负数的反码与原码雷同,如:10 反码为 0000 1010
  • 正数的反码为除符号位,按位取反,即 0 变 1,1 变 0。

例如:-10

原码:1000 1010
反码:1111 0101

(3)补码

  • 负数的补码与原码雷同,如:10 补码为 0000 1010
  • 正数的补码是原码除符号位外的所有位取反即 0 变 1,1 变 0,而后加 1,也就是反码加 1。

例如:-10

原码:1000 1010
反码:1111 0101
补码:1111 0110

JavaScript 为什么要进行变量晋升,它导致了什么问题?

变量晋升的体现是,无论在函数中何处地位申明的变量,如同都被晋升到了函数的首部,能够在变量申明前拜访到而不会报错。

造成变量申明晋升的 实质起因 是 js 引擎在代码执行前有一个解析的过程,创立了执行上下文,初始化了一些代码执行时须要用到的对象。当拜访一个变量时,会到以后执行上下文中的作用域链中去查找,而作用域链的首端指向的是以后执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它蕴含了函数的形参、所有的函数和变量申明,这个对象的是在代码解析的时候创立的。

首先要晓得,JS 在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。

  • 在解析阶段,JS 会查看语法,并对函数进行预编译。解析的时候会先创立一个全局执行上下文环境,先把代码中行将执行的变量、函数申明都拿进去,变量先赋值为 undefined,函数先申明好可应用。在一个函数执行之前,也会创立一个函数执行上下文环境,跟全局执行上下文相似,不过函数执行上下文会多出 this、arguments 和函数的参数。

    • 全局上下文:变量定义,函数申明
    • 函数上下文:变量定义,函数申明,this,arguments
  • 在执行阶段,就是依照代码的程序顺次执行。

那为什么会进行变量晋升呢?次要有以下两个起因:

  • 进步性能
  • 容错性更好

(1)进步性能 在 JS 代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了进步性能,如果没有这一步,那么每次执行代码前都必须从新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会扭转,解析一遍就够了。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计申明了哪些变量、创立了哪些函数,并对函数的代码进行压缩,去除正文、不必要的空白等。这样做的益处就是每次执行函数时都能够间接为该函数调配栈空间(不须要再解析一遍去获取代码中申明了哪些变量,创立了哪些函数),并且因为代码压缩的起因,代码执行也更快了。

(2)容错性更好

变量晋升能够在肯定水平上进步 JS 的容错性,看上面的代码:

a = 1;var a;console.log(a);

如果没有变量晋升,这两行代码就会报错,然而因为有了变量晋升,这段代码就能够失常执行。

尽管,在能够开发过程中,能够完全避免这样写,然而有时代码很简单的时候。可能因为忽略而先应用后定义了,这样也不会影响失常应用。因为变量晋升的存在,而会失常运行。

总结:

  • 解析和预编译过程中的申明晋升能够进步性能,让函数能够在执行时事后为变量调配栈空间
  • 申明晋升还能够进步 JS 代码的容错性,使一些不标准的代码也能够失常执行

变量晋升尽管有一些长处,然而他也会造成肯定的问题,在 ES6 中提出了 let、const 来定义变量,它们就没有变量晋升的机制。上面看一下变量晋升可能会导致的问题:

var tmp = new Date();

function fn(){console.log(tmp);
    if(false){var tmp = 'hello world';}
}

fn();  // undefined

在这个函数中,本来是要打印出外层的 tmp 变量,然而因为变量晋升的问题,内层定义的 tmp 被提到函数外部的最顶部,相当于笼罩了外层的 tmp,所以打印后果为 undefined。

var tmp = 'hello world';

for (var i = 0; i < tmp.length; i++) {console.log(tmp[i]);
}

console.log(i); // 11

因为遍历时定义的 i 会变量晋升成为一个全局变量,在函数完结之后不会被销毁,所以打印进去 11。

对象继承的形式有哪些?

(1)第一种是以原型链的形式来实现继承,然而这种实现形式存在的毛病是,在蕴含有援用类型的数据时,会被所有的实例对象所共享,容易造成批改的凌乱。还有就是在创立子类型的时候不能向超类型传递参数。

(2)第二种形式是应用借用构造函数的形式,这种形式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种办法解决了不能向超类型传递参数的毛病,然而它存在的一个问题就是无奈实现函数办法的复用,并且超类型原型定义的办法子类型也没有方法拜访到。

(3)第三种形式是组合继承,组合继承是将原型链和借用构造函数组合起来应用的一种形式。通过借用构造函数的形式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现办法的继承。这种形式解决了下面的两种模式独自应用时的问题,然而因为咱们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。

(4)第四种形式是原型式继承,原型式继承的次要思路就是基于已有的对象来创立新的对象,实现的原理是,向函数中传入一个对象,而后返回一个以这个对象为原型的对象。这种继承的思路次要不是为了实现发明一种新的类型,只是对某个对象实现一种简略继承,ES5 中定义的 Object.create() 办法就是原型式继承的实现。毛病与原型链形式雷同。

(5)第五种形式是寄生式继承,寄生式继承的思路是创立一个用于封装继承过程的函数,通过传入一个对象,而后复制一个对象的正本,而后对象进行扩大,最初返回这个对象。这个扩大的过程就能够了解是一种继承。这种继承的长处就是对一个简略对象实现继承,如果这个对象不是自定义类型时。毛病是没有方法实现函数的复用。

(6)第六种形式是寄生式组合继承,组合继承的毛病就是应用超类型的实例做为子类型的原型,导致增加了不必要的原型属性。寄生式组合继承的形式是应用超类型的原型的副原本作为子类型的原型,这样就防止了创立不必要的属性。

HTTP 和 HTTPS 协定的区别

HTTP 和 HTTPS 协定的次要区别如下:

  • HTTPS 协定须要 CA 证书,费用较高;而 HTTP 协定不须要;
  • HTTP 协定是超文本传输协定,信息是明文传输的,HTTPS 则是具备安全性的 SSL 加密传输协定;
  • 应用不同的连贯形式,端口也不同,HTTP 协定端口是 80,HTTPS 协定端口是 443;
  • HTTP 协定连贯很简略,是无状态的;HTTPS 协定是有 SSL 和 HTTP 协定构建的可进行加密传输、身份认证的网络协议,比 HTTP 更加平安。

Promise.all 和 Promise.race 的区别的应用场景

(1)Promise.all Promise.all能够将多个 Promise 实例包装成一个新的 Promise 实例。同时,胜利和失败的返回值是不同的,胜利的时候返回的是 一个后果数组 ,而失败的时候则返回 最先被 reject 失败状态的值

Promise.all 中传入的是数组,返回的也是是数组,并且会将进行映射,传入的 promise 对象返回的值是依照程序在数组中排列的,然而留神的是他们执行的程序并不是依照程序的,除非可迭代对象为空。

须要留神,Promise.all 取得的胜利后果的数组外面的数据程序和 Promise.all 接管到的数组程序是统一的,这样当遇到发送多个申请并依据申请程序获取和应用数据的场景,就能够应用 Promise.all 来解决。

(2)Promise.race

顾名思义,Promse.race 就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])外面哪个后果取得的快,就返回那个后果,不论后果自身是胜利状态还是失败状态。当要做一件事,超过多长时间就不做了,能够用这个办法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

POST 和 PUT 申请的区别

  • PUT 申请是向服务器端发送数据,从而批改数据的内容,然而不会减少数据的品种等,也就是说无论进行多少次 PUT 操作,其后果并没有不同。(能够了解为时 更新数据
  • POST 申请是向服务器端发送数据,该申请会扭转数据的品种等资源,它会创立新的内容。(能够了解为是 创立数据

Compositon api

Composition API也叫组合式 API,是 Vue3.x 的新个性。

通过创立 Vue 组件,咱们能够将接口的可重复部分及其性能提取到可重用的代码段中。仅此一项就能够使咱们的应用程序在可维护性和灵活性方面走得更远。然而,咱们的教训曾经证实,光靠这一点可能是不够的,尤其是当你的应用程序变得十分大的时候——想想几百个组件。在解决如此大的应用程序时,共享和重用代码变得尤为重要

  • Vue2.0 中,随着性能的减少,组件变得越来越简单,越来越难保护,而难以保护的根本原因是 Vue 的 API 设计迫使开发者应用 watch,computed,methods 选项组织代码,而不是理论的业务逻辑。
  • 另外 Vue2.0 短少一种较为简洁的低成本的机制来实现逻辑复用,尽管能够 minxis 实现逻辑复用,然而当 mixin 变多的时候,会使得难以找到对应的 data、computed 或者 method 来源于哪个mixin,使得类型推断难以进行。
  • 所以 Composition API 的呈现,次要是也是为了解决 Option API 带来的问题,第一个是代码组织问题,Compostion API能够让开发者依据业务逻辑组织本人的代码,让代码具备更好的可读性和可扩展性,也就是说当下一个开发者接触这一段不是他本人写的代码时,他能够更好的利用代码的组织反推出理论的业务逻辑,或者依据业务逻辑更好的了解代码。
  • 第二个是实现代码的逻辑提取与复用,当然 mixin 也能够实现逻辑提取与复用,然而像后面所说的,多个 mixin 作用在同一个组件时,很难看出 property 是来源于哪个 mixin,起源不分明,另外,多个mixinproperty存在变量命名抵触的危险。而 Composition API 刚好解决了这两个问题。

艰深的讲:

没有 Composition API 之前 vue 相干业务的代码须要配置到 option 的特定的区域,中小型我的项目是没有问题的,然而在大型项目中会导致前期的维护性比较复杂,同时代码可复用性不高。Vue3.x 中的 composition-api 就是为了解决这个问题而生的

compositon api 提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

都说 Composition API 与 React Hook 很像,说说区别

从 React Hook 的实现角度看,React Hook 是依据 useState 调用的程序来确定下一次重渲染时的 state 是来源于哪个 useState,所以呈现了以下限度

  • 不能在循环、条件、嵌套函数中调用 Hook
  • 必须确保总是在你的 React 函数的顶层调用 Hook
  • useEffect、useMemo等函数必须手动确定依赖关系

而 Composition API 是基于 Vue 的响应式零碎实现的,与 React Hook 的相比

  • 申明在 setup 函数内,一次组件实例化只调用一次setup,而 React Hook 每次重渲染都须要调用 Hook,使得 React 的 GC 比 Vue 更有压力,性能也绝对于 Vue 来说也较慢
  • Compositon API的调用不须要顾虑调用程序,也能够在循环、条件、嵌套函数中应用
  • 响应式零碎主动实现了依赖收集,进而组件的局部的性能优化由 Vue 外部本人实现,而 React Hook 须要手动传入依赖,而且必须必须保障依赖的程序,让 useEffectuseMemo 等函数正确的捕捉依赖变量,否则会因为依赖不正确使得组件性能降落。

尽管 Compositon API 看起来比 React Hook 好用,然而其设计思维也是借鉴 React Hook 的。

watch 的了解

watch没有缓存性,更多的是察看的作用,能够监听某些数据执行回调。当咱们须要 深度监听对象中 的属性时,能够关上 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话能够应用字符串模式监听

留神:Watcher : 观察者对象 , 实例分为 渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

React Fiber 架构

最次要的思维就是将工作拆分

  • DOM 须要渲染时暂停,闲暇时复原。
  • window.requestIdleCallback
  • React 外部实现的机制

React 谋求的是“疾速响应”,那么,“疾速响应“的制约因素都有什么呢

  • CPU的瓶颈:当我的项目变得宏大、组件数量繁多、遇到大计算量的操作或者设施性能有余使得页面掉帧,导致卡顿。
  • IO的瓶颈:发送网络申请后,因为须要期待数据返回能力进一步操作导致不能疾速响应。

fiber 架构次要就是用来解决 CPU 和网络的问题,这两个问题始终也是最影响前端开发体验的中央,一个会造成卡顿,一个会造成白屏。为此 react 为前端引入了两个新概念:Time Slicing 工夫分片 Suspense

1. React 都做过哪些优化

  • React 渲染页面的两个阶段

    • 调度阶段(reconciliation):在这个阶段 React 会更新数据生成新的 Virtual DOM,而后通过 Diff 算法,疾速找出须要更新的元素,放到更新队列中去,失去新的更新队列。
    • 渲染阶段(commit):这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到 DOM 上
  • React 15 架构

    • React15 架构能够分为两层

      • Reconciler(协调器)—— 负责找出变动的组件;
      • Renderer(渲染器)—— 负责将变动的组件渲染到页面上;
  • 在 React15 及以前,Reconciler 采纳递归的形式创立虚构 DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多工夫,递归更新工夫超过了 16ms,用户交互就会卡顿。
  • 为了解决这个问题,React16 将递归的无奈中断的更新重构为异步的可中断更新,因为已经用于递归的虚构 DOM 数据结构曾经无奈满足需要。于是,全新的 Fiber 架构应运而生。
  • React 16 架构

    • 为了解决同步更新长时间占用线程导致页面卡顿的问题,也为了摸索运行时优化的更多可能,React 开始重构并始终继续至今。重构的指标是实现 Concurrent Mode(并发模式)。
    • 从 v15 到 v16,React 团队花了两年工夫将源码架构中的 Stack Reconciler 重构为 Fiber Reconciler
    • React16 架构能够分为三层

      • Scheduler(调度器)—— 调度工作的优先级,高优工作优先进入 Reconciler;
      • Reconciler(协调器)—— 负责找出变动的组件:更新工作从递归变成了能够中断的循环过程。Reconciler 外部采纳了 Fiber 的架构;
      • Renderer(渲染器)—— 负责将变动的组件渲染到页面上。
  • React 17 优化

    • 应用 Lane 来治理工作的优先级。Lane 用二进制位示意工作的优先级,不便优先级的计算(位运算),不同优先级占用不同地位的“赛道”,而且存在批的概念,优先级越低,“赛道”越多。高优先级打断低优先级,新建的工作须要赋予什么优先级等问题都是 Lane 所要解决的问题。
    • Concurrent Mode 的目标是实现一套可中断 / 复原的更新机制。其由两局部组成:

      • 一套协程架构:Fiber Reconciler
      • 基于协程架构的启发式更新算法:管制协程架构工作形式的算法

2. 浏览器一帧都会干些什么以及 requestIdleCallback 的启发

咱们都晓得,页面的内容都是一帧一帧绘制进去的,浏览器刷新率代表浏览器一秒绘制多少帧。原则上说 1s 内绘制的帧数也多,画面体现就也细腻。目前浏览器大多是 60Hz(60 帧 /s),每一帧耗时也就是在 16.6ms 左右。那么在这一帧的(16.6ms)过程中浏览器又干了些什么呢

通过下面这张图能够分明的晓得,浏览器一帧会通过上面这几个过程:

  1. 承受输出事件
  2. 执行事件回调
  3. 开始一帧
  4. 执行 RAF (RequestAnimationFrame)
  5. 页面布局,款式计算
  6. 绘制渲染
  7. 执行 RIC (RequestIdelCallback)

第七步的 RIC 事件不是每一帧完结都会执行,只有在一帧的 16.6ms 中做完了后面 6 件事儿且还有剩余时间,才会执行。如果一帧执行完结后还有工夫执行 RIC 事件,那么下一帧须要在事件执行完结能力持续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面呈现卡顿和事件响应不及时。

requestIdleCallback 的启发:咱们以浏览器是否有剩余时间作微工作中断的规范,那么咱们须要一种机制,当浏览器有剩余时间时告诉咱们。

requestIdleCallback((deadline) => {
// deadline 有两个参数
  // timeRemaining(): 以后帧还剩下多少工夫
  // didTimeout: 是否超时
// 另外 requestIdleCallback 后如果跟上第二个参数 {timeout: ...} 则会强制浏览器在以后帧执行完后执行。if (deadline.timeRemaining() > 0) {// TODO} else {requestIdleCallback(otherTasks);
 }
});
// 用法示例
var tasksNum = 10000

requestIdleCallback(unImportWork)

function unImportWork(deadline) {while (deadline.timeRemaining() && tasksNum > 0) {console.log(` 执行了 ${10000 - tasksNum + 1}个工作 `)
    tasksNum--
  }
  if (tasksNum > 0) { // 在将来的帧中继续执行
    requestIdleCallback(unImportWork)
  }
}

其实局部浏览器曾经实现了这个 API,这就是 requestIdleCallback。然而因为以下因素,Facebook 摈弃了 requestIdleCallback的原生 API:

  • 浏览器兼容性;
  • 触发频率不稳固,受很多因素影响。比方当咱们的浏览器切换 tab 后,之前 tab 注册的 requestIdleCallback 触发的频率会变得很低。

基于以上起因,在 React 中实现了性能更齐备的requestIdleCallbackpolyfill,这就是Scheduler。除了在闲暇时触发回调的性能外,Scheduler 还提供了多种调度优先级供工作设置

3. React Fiber 是什么

React Fiber是对外围算法的一次从新实现。React Fiber把更新过程碎片化,把一个耗时长的工作分成很多小片,每一个小片的运行工夫很短,尽管总工夫仍然很长,然而在每个小片执行完之后,都给其余工作一个执行的机会,这样惟一的线程就不会被独占,其余工作仍然有运行的机会

  1. React Fiber 中,一次更新过程会分成多个分片实现,所以齐全有可能一个更新工作还没有实现,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新工作会优先解决完,而低优先级更新工作所做的工作则会齐全作废,而后期待机会重头再来
  2. 因为一个更新过程可能被打断,所以 React Fiber 一个更新过程被分为两个阶段 (Phase):第一个阶段Reconciliation Phase 和第二阶段Commit Phase
  3. 在第一阶段 Reconciliation PhaseReact Fiber 会找出须要更新哪些 DOM,这个阶段是能够被打断的;然而到了第二阶段Commit Phase,那就一鼓作气把DOM 更新完,绝不会被打断
  4. 这两个阶段大部分工作都是 React Fiber 做,和咱们相干的也就是生命周期函数

React Fiber扭转了之前 react 的组件渲染机制,新的架构使原来同步渲染的组件当初能够异步化,可中途中断渲染,执行更高优先级的工作。开释浏览器主线程

要害个性

  • 增量渲染(把渲染工作拆分成块,匀到多帧)
  • 更新时可能暂停,终止,复用渲染工作
  • 给不同类型的更新赋予优先级
  • 并发方面新的根底能力

增量渲染用来解决掉帧的问题,渲染工作拆分之后,每次只做一小段,做完一段就把工夫控制权交还给主线程,而不像之前长时间占用

4. 组件的渲染程序

如果有 A,B,C,D 组件,层级构造为:

咱们晓得组件的生命周期为:

挂载阶段

  • constructor()
  • componentWillMount()
  • render()
  • componentDidMount()

更新阶段为

  • componentWillReceiveProps()
  • shouldComponentUpdate()
  • componentWillUpdate()
  • render()
  • componentDidUpdate

那么在挂载阶段,A,B,C,D的生命周期渲染程序是如何的呢?

那么在挂载阶段,A,B,C,D 的生命周期渲染程序是如何的呢?

render() 函数为分界线。从顶层组件开始,始终往下,直至最底层子组件。而后再往上

组件 update 阶段同理

后面是 react16 以前的组建渲染形式。这就存在一个问题

如果这是一个很大,层级很深的组件,react渲染它须要几十甚至几百毫秒,在这期间,react会始终占用浏览器主线程,任何其余的操作(包含用户的点击,鼠标挪动等操作)都无奈执行

Fiber 架构就是为了解决这个问题

看一下 fiber 架构 组建的渲染程序

退出 fiberreact将组件更新分为两个期间

这两个期间以 render 为分界

  • render前的生命周期为phase1,
  • render后的生命周期为phase2
  • phase1的生命周期是能够被打断的,每隔一段时间它会跳出以后渲染过程,去确定是否有其余更重要的工作。此过程,ReactworkingProgressTree(并不是实在的virtualDomTree)上复用 current 上的 Fiber 数据结构来一步地(通过requestIdleCallback)来构建新的 tree,标记处须要更新的节点,放入队列中
  • phase2的生命周期是不可被打断的,React 将其所有的变更一次性更新到 DOM

这里最重要的是 phase1 这是期间所做的事。因而咱们须要具体理解 phase1 的机制

  • 如果不被打断,那么 phase1 执行完会间接进入 render 函数,构建实在的virtualDomTree
  • 如果组件再 phase1 过程中被打断,即以后组件只渲染到一半(兴许是在 willMount, 兴许是willUpdate~ 反正是在 render 之前的生命周期),那么react 会怎么干呢?react会放弃以后组件所有干到一半的事件,去做更高优先级更重要的工作(当然,也可能是用户鼠标挪动,或者其余 react 监听之外的工作),当所有高优先级工作执行完之后,react通过 callback 回到之前渲染到一半的组件,从头开始渲染。(看起来放弃曾经渲染完的生命周期,会有点不合理,反而会减少渲染时长,然而 react 的确是这么干的)

所有 phase1 的生命周期函数都可能被执行屡次,因为可能会被打断重来

这样的话,就和 react16 版本之前有很大区别了,因为可能会被执行屡次,那么咱们最好就得保障 phase1 的生命周期每一次执行的后果都是一样的,否则就会有问题,因而,最好都是纯函数

  • 如果高优先级的工作始终存在,那么低优先级的工作则永远无奈进行,组件永远无奈持续渲染。这个问题 facebook 目前如同还没解决
  • 所以,facebook 在 react16 减少 fiber 构造,其实并不是为了缩小组件的渲染工夫,事实上也并不会缩小,最重要的是当初能够使得一些更高优先级的工作,如用户的操作可能优先执行,进步用户的体验,至多用户不会感觉到卡顿

5 React Fiber 架构总结

React Fiber 如何性能优化

  • 更新的两个阶段

    • 调度算法阶段 - 执行 diff 算法,纯 js 计算
    • Commit 阶段 - 将 diff 后果渲染 dom
  • 可能会有性能问题

    • JS 是单线程的,且和 DOM 渲染专用一个线程
    • 当组件足够简单,组件更新时计算和渲染压力都大
    • 同时再有 DOM 操作需要(动画、鼠标拖拽等),将卡顿
  • 解决方案 fiber

    • 将调度算法阶段阶段工作拆分(Commit 无奈拆分)
    • DOM 须要渲染时暂停,闲暇时复原
    • 扩散执行: 工作宰割后,就能够把小工作单元扩散到浏览器的闲暇期间去排队执行,而实现的要害是两个新 API: requestIdleCallbackrequestAnimationFrame

      • 低优先级的工作交给 requestIdleCallback 解决,这是个浏览器提供的事件循环闲暇期的回调函数,须要 pollyfill,而且领有 deadline 参数,限度执行事件,以持续切分工作;
      • 高优先级的工作交给 requestAnimationFrame 解决;

React 的外围流程能够分为两个局部:

  • reconciliation (调度算法,也可称为 render)

    • 更新 stateprops
    • 调用生命周期钩子;
    • 生成 virtual dom

      • 这里应该称为 Fiber Tree 更为合乎;
    • 通过新旧 vdom 进行 diff 算法,获取 vdom change
    • 确定是否须要从新渲染
  • commit

    • 如须要,则操作 dom 节点更新

要理解 Fiber,咱们首先来看为什么须要它

  • 问题 : 随着利用变得越来越宏大,整个更新渲染的过程开始变得吃力,大量的组件渲染会导致主过程长时间被占用,导致一些动画或高频操作呈现卡顿和掉帧的状况。而关键点,便是 同步阻塞。在之前的调度算法中,React 须要实例化每个类组件,生成一颗组件树,应用 同步递归 的形式进行遍历渲染,而这个过程最大的问题就是无奈 暂停和复原。
  • 解决方 案: 解决同步阻塞的办法,通常有两种: 异步 与 工作宰割。而 React Fiber 便是为了实现工作宰割而诞生的
  • 简述

    • React V16 将调度算法进行了重构,将之前的 stack reconciler 重形成新版的 fiber reconciler,变成了具备链表和指针的 单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步与下一步,从而使遍历变得能够被暂停和重启
    • 这里我了解为是一种 工作宰割调度算法,次要是 将原先同步更新渲染的工作宰割成一个个独立的 小工作单位,依据不同的优先级,将小工作扩散到浏览器的闲暇工夫执行,充分利用主过程的事件循环机制
  • 外围

    • Fiber 这里能够具象为一个 数据结构
class Fiber {constructor(instance) {
        this.instance = instance
        // 指向第一个 child 节点
        this.child = child
        // 指向父节点
        this.return = parent
        // 指向第一个兄弟节点
        this.sibling = previous
    }    
}
  • 链表树遍历算法 : 通过 节点保留与映射,便可能随时地进行 进行和重启,这样便能达到实现工作宰割的基本前提

    • 首先通过一直遍历子节点,到树开端;
    • 开始通过 sibling 遍历兄弟节点;
    • return 返回父节点,继续执行 2;
    • 直到 root 节点后,跳出遍历;
  • 工作宰割,React 中的渲染更新能够分成两个阶段

    • reconciliation 阶段 : vdom 的数据比照,是个适宜拆分的阶段,比方比照一部分树后,先暂停执行个动画调用,待实现后再回来持续比对
    • Commit 阶段 : 将 change list 更新到 dom 上,并不适宜拆分,能力保持数据与 UI 的同步。否则可能因为阻塞 UI 更新,而导致数据更新和 UI 不统一的状况
  • 扩散执行: 工作宰割后,就能够把小工作单元扩散到浏览器的闲暇期间去排队执行,而实现的要害是两个新 API: requestIdleCallbackrequestAnimationFrame

    • 低优先级的工作交给 requestIdleCallback 解决,这是个浏览器提供的事件循环闲暇期的回调函数,须要 pollyfill,而且领有 deadline 参数,限度执行事件,以持续切分工作;
    • 高优先级的工作交给 requestAnimationFrame 解决;
// 相似于这样的形式
requestIdleCallback((deadline) => {
    // 当有闲暇工夫时,咱们执行一个组件渲染;// 把工作塞到一个个碎片工夫中去;while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {nextComponent = performWork(nextComponent);
    }
});
  • 优先级策略: 文本框输出 > 本次调度完结需实现的工作 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防未来会显示的工作
  • Fiber 其实能够算是一种编程思维,在其它语言中也有许多利用(Ruby Fiber)。
  • 核心思想是 工作拆分和协同,被动把执行权交给主线程,使主线程有工夫空挡解决其余高优先级工作。
  • 当遇到过程阻塞的问题时,工作宰割、异步调用 和 缓存策略 是三个显著的解决思路。

OSI 七层模型

ISO为了更好的使网络应用更为遍及,推出了 OSI 参考模型。

(1)应用层

OSI参考模型中最靠近用户的一层,是为计算机用户提供利用接口,也为用户间接提供各种网络服务。咱们常见应用层的网络服务协定有:HTTPHTTPSFTPPOP3SMTP等。

  • 在客户端与服务器中常常会有数据的申请,这个时候就是会用到 http(hyper text transfer protocol)(超文本传输协定) 或者https. 在后端设计数据接口时,咱们经常应用到这个协定。
  • FTP是文件传输协定,在开发过程中,集体并没有波及到,然而我想,在一些资源网站,比方 百度网盘` 迅雷 ` 应该是基于此协定的。
  • SMTPsimple mail transfer protocol(简略邮件传输协定)。在一个我的项目中,在用户邮箱验证码登录的性能时,应用到了这个协定。

(2)表示层

表示层提供各种用于应用层数据的编码和转换性能, 确保一个零碎的应用层发送的数据能被另一个零碎的应用层辨认。如果必要,该层可提供一种规范示意模式,用于将计算机外部的多种数据格式转换成通信中采纳的规范示意模式。数据压缩和加密也是表示层可提供的转换性能之一。

在我的项目开发中,为了不便数据传输,能够应用 base64 对数据进行编解码。如果按性能来划分,base64应该是工作在表示层。

(3)会话层

会话层就是负责建设、治理和终止表示层实体之间的通信会话。该层的通信由不同设施中的应用程序之间的服务申请和响应组成。

(4)传输层

传输层建设了主机端到端的链接,传输层的作用是为下层协定提供端到端的牢靠和通明的数据传输服务,包含解决差错控制和流量管制等问题。该层向高层屏蔽了上层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户管制和设定的、牢靠的数据通路。咱们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。

(5)网络层

本层通过 IP 寻址来建设两个节点之间的连贯,为源端的运输层送来的分组,抉择适合的路由和替换节点,正确无误地依照地址传送给目标端的运输层。就是通常说的 IP 层。这一层就是咱们常常说的 IP 协定层。IP协定是 Internet 的根底。咱们能够这样了解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。

(6)数据链路层

将比特组合成字节, 再将字节组合成帧, 应用链路层地址 (以太网应用 MAC 地址)来拜访介质, 并进行过错检测。
网络层与数据链路层的比照,通过下面的形容,咱们或者能够这样了解,网络层是布局了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还减少了差错控制的性能。

(7)物理层

理论最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。罕用设施有(各种物理设施)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。

OSI 七层模型通信特点:对等通信 对等通信,为了使数据分组从源传送到目的地,源端 OSI 模型的每一层都必须与目标端的对等层进行通信,这种通信形式称为对等层通信。在每一层通信过程中,应用本层本人协定进行通信。

JSX 语法糖实质

JSX 是语法糖,通过 babel 转成 React.createElement 函数,在 babel 官网上能够在线把 JSX 转成 React 的 JS 语法

  • 首先解析进去的话,就是一个 createElement 函数
  • 而后这个函数执行完后,会返回一个vnode
  • 通过 vdom 的 patch 或者是其余的一个办法,最初渲染一个页面

script 标签中不增加 text/babel 解析 jsx 语法的状况下

<script>
  const ele = React.createElement("h2", null, "Hello React!");
  ReactDOM.render(ele, document.getElementById("app"));
</script>

JSX 的实质是 React.createElement()函数

createElement函数返回的对象是 ReactEelement 对象。

createElement的写法如下

class App extends React.Component {constructor() {super()
    this.state = {}}

  render() {
    return React.createElement("div", null,
        /* 第一个子元素,header*/
        React.createElement("div", { className: "header"},
                            React.createElement("h1", { title: "\u6807\u9898"}, "\u6211\u662F\u6807\u9898")
                          ),
        /* 第二个子元素,content*/
        React.createElement("div", { className: "content"},
                            React.createElement("h2", null, "\u6211\u662F\u9875\u9762\u7684\u5185\u5BB9"),
                            React.createElement("button", null, "\u6309\u94AE"),
                            React.createElement("button", null, "+1"),
                            React.createElement("a", { href: "http://www.baidu.com"},
                                                "\u767E\u5EA6\u4E00\u4E0B")
                          ),
        /* 第三个子元素,footer*/
        React.createElement("div", { className: "footer"},
                            React.createElement("p", null, "\u6211\u662F\u5C3E\u90E8\u7684\u5185\u5BB9")
                          )
      );
  }
}

ReactDOM.render(<App />, document.getElementById("app"));

理论开发中不会应用 createElement 来创立 ReactElement 的,个别都是应用 JSX 的模式开发。

ReactElement在程序中打印一下

render() {
  let ele = (
    <div>
      <div className="header">
        <h1 title="题目"> 我是题目 </h1>
      </div>
      <div className="content">
        <h2> 我是页面的内容 </h2>
        <button> 按钮 </button>
        <button>+1</button>
        <a href="http://www.baidu.com"> 百度一下 </a>
      </div>
      <div className="footer">
        <p> 我是尾部的内容 </p>
      </div>
    </div>
  )
  console.log(ele);
  return ele;
}

react 通过 babel 把 JSX 转成 createElement 函数,生成 ReactElement 对象,而后通过 ReactDOM.render 函 数把 ReactElement 渲染成实在的 DOM 元素

为什么 React 应用 JSX

  • 在答复问题之前,我首先解释下什么是 JSX 吧。JSX 是一个 JavaScript 的语法扩大,构造相似 XML。
  • JSX 次要用于申明 React 元素,但 React 中并不强制应用 JSX。即便应用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖
  • 接下来与 JSX 以外的三种技术计划进行比照

    • 首先是模板,React 团队认为模板不应该是开发过程中的关注点,因为引入了模板语法、模板指令等概念,是一种不佳的实现计划
    • 其次是模板字符串,模板字符串编写的构造会造成屡次外部嵌套,使整个构造变得复杂,并且优化代码提醒也会变得困难重重
    • 所以 React 最初选用了 JSX,因为 JSX 与其设计思维贴合,不须要引入过多新的概念,对编辑器的代码提醒也极为敌对。

Babel 插件如何实现 JSX 到 JS 的编译?在 React 面试中,这个问题很容易被诘问,也常常被要求手写。

它的实现原理是这样的。Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就能够将 JSX 的构造转换为 React.createElement 的函数。如下代码所示:

module.exports = function (babel) {
  var t = babel.types;
  return {
    name: "custom-jsx-plugin",
    visitor: {JSXElement(path) {
        var openingElement = path.node.openingElement;
        var tagName = openingElement.name.name;
        var args = []; 
        args.push(t.stringLiteral(tagName)); 
        var attribs = t.nullLiteral(); 
        args.push(attribs); 
        var reactIdentifier = t.identifier("React"); //object
        var createElementIdentifier = t.identifier("createElement"); 
        var callee = t.memberExpression(reactIdentifier, createElementIdentifier)
        var callExpression = t.callExpression(callee, args);
        callExpression.arguments = callExpression.arguments.concat(path.node.children);
        path.replaceWith(callExpression, path.node); 
      },
    },
  };
};

React.createElement 源码剖析

/**
 101. React 的创立元素办法
 */
export function createElement(type, config, children) {
  // propName 变量用于贮存前面须要用到的元素属性
  let propName; 
  // props 变量用于贮存元素属性的键值对汇合
  const props = {}; 
  // key、ref、self、source 均为 React 元素的属性,此处不用深究
  let key = null;
  let ref = null; 
  let self = null; 
  let source = null; 

  // config 对象中存储的是元素的属性
  if (config != null) { 
    // 进来之后做的第一件事,是顺次对 ref、key、self 和 source 属性赋值
    if (hasValidRef(config)) {ref = config.ref;}
    // 此处将 key 值字符串化
    if (hasValidKey(config)) {key = '' + config.key;}
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 接着就是要把 config 外面的属性都一个一个挪到 props 这个之前申明好的对象外面
    for (propName in config) {
      if (
        // 筛选出能够提进 props 对象里的属性
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName) 
      ) {props[propName] = config[propName]; 
      }
    }
  }
  // childrenLength 指的是以后元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
  const childrenLength = arguments.length - 2; 
  // 如果抛去 type 和 config,就只剩下一个参数,个别意味着文本节点呈现了
  if (childrenLength === 1) { 
    // 间接把这个参数的值赋给 props.children
    props.children = children; 
    // 解决嵌套多个子元素的状况
  } else if (childrenLength > 1) { 
    // 申明一个子元素数组
    const childArray = Array(childrenLength); 
    // 把子元素推动数组里
    for (let i = 0; i < childrenLength; i++) {childArray[i] = arguments[i + 2];
    }
    // 最初把这个数组赋值给 props.children
    props.children = childArray; 
  } 

  // 解决 defaultProps
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {if (props[propName] === undefined) {props[propName] = defaultProps[propName];
      }
    }
  }

  // 最初返回一个调用 ReactElement 执行办法,并传入方才解决过的参数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

入参解读:发明一个元素须要晓得哪些信息

export function createElement(type, config, children)

createElement 有 3 个入参,这 3 个入参囊括了 React 创立一个元素所须要晓得的全副信息。

  • type:用于标识节点的类型。它能够是相似“h1”“div”这样的规范 HTML 标签字符串,也能够是 React 组件类型或 React fragment 类型。
  • config:以对象模式传入,组件所有的属性都会以键值对的模式存储在 config 对象中。
  • children:以对象模式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”
React.createElement("ul", {
  // 传入属性键值对
  className: "list"
   // 从第三个入参开始往后,传入的参数都是 children
}, React.createElement("li", {key: "1"}, "1"), React.createElement("li", {key: "2"}, "2"));

这个调用对应的 DOM 构造如下:

<ul className="list">
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

createElement 函数体拆解

createElement 中并没有十分复杂的波及算法或实在 DOM 的逻辑,它的每一个步骤简直都是在格式化数据。

当初看来,createElement 原来只是个“参数中介”。此时咱们的注意力自然而然地就聚焦在了 ReactElement

出参解读:初识虚构 DOM

createElement 执行到最初会 return 一个针对 ReactElement 的调用。这里对于 ReactElement,我仍然先给出源码 + 正文模式的解析

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE 是一个常量,用来标识该对象是一个 ReactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 记录发明该元素的组件
    _owner: owner,
  };

  // 
  if (__DEV__) {// 这里是一些针对 __DEV__ 环境下的解决,对于大家了解次要逻辑意义不大,此处我间接省略掉,免得混淆视听}

  return element;
};

ReactElement 其实只做了一件事件,那就是“创立”,说得更准确一点,是“组装”:ReactElement 把传入的参数依照肯定的标准,“组装”进了 element 对象里,并把它返回给了 eact.createElement,最终 React.createElement 又把它交回到了开发者手中

const AppJSX = (<div className="App">
  <h1 className="title">I am the title</h1>
  <p className="content">I am the content</p>
</div>)

console.log(AppJSX)

你会发现它的确是一个规范的 ReactElement 对象实例

这个 ReactElement 对象实例,实质上是以 JavaScript 对象模式存在的对 DOM 的形容,也就是陈词滥调的“虚构 DOM”(精确地说,是虚构 DOM 中的一个节点)

正文完
 0