写点什么

字节前端必会面试题(持续更新中)

作者:Geek_02d948
  • 2023-02-27
    浙江
  • 本文字数:21096 字

    阅读完需:约 69 分钟

事件传播机制(事件流)

冒泡和捕获

谈一谈 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 只针对于移动端,只在移动端上才能看到效果


参考 前端进阶面试题详细解答

数组的遍历方法有哪些

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 = 00 | 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 中的一个节点)


用户头像

Geek_02d948

关注

还未添加个人签名 2022-09-08 加入

还未添加个人简介

评论

发布
暂无评论
字节前端必会面试题(持续更新中)_JavaScript_Geek_02d948_InfoQ写作社区