梦想家罗西的博客

但守陋巷,陈说平生

  1. javascript ["1", "2", "3"].map(parseInt)
    解析:
    1
    2
    3
    ["1","2","3"].map(parseInt("1",0)) //1
    ["1","2","3"].map(parseInt("2",1)) //NaN
    ["1","2","3"].map(parseInt("3",2)) //NaN
    答案为:[1, NaN, NaN]
  2. javascript [typeof null, null instanceof Object]
    This is actually a long-standing bug in JavaScript, but it has not been fixed. null is not an object. But the result of typeof null is “object”.
    null is not an instance of Object
    答案为:['object' false]
  3. javascript [ [3,2,1].reduce(Math.pow), [].reduce(Math.pow)]
    解析:
    1
    2
    3
    [3,2,1].reduce((acc,cur)=>Match.pow(3,2)) //9
    [3,2,1].reduce((acc,cur)=>Match.pow(9,1))//9
    [].reduce(Math.pow)//报错,由于 reduce 没有初始值
    答案为:error
  4. javascript var val = 'smtg'; console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');
    解析:
    由于 + 的运算符优先级高于三元运算符,所以会先执行'Value is ' + (val === 'smtg')// Value is true后执行'Value is true'?'Something' : 'Nothing' 所以最终结果为:’Something’
    答案为:Something
  5. code:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var name = 'World!';
    (function () {
    if (typeof name === 'undefined') {
    var name = 'Jack';
    console.log('Goodbye ' + name);
    } else {
    console.log('Hello ' + name);
    }
    })();
    解析:
    以上自执行函数形成了一个闭包,闭包会形成一个独立的作用域环境,val 声明的变量存在变量提升,所以typeof name === 'undefined'返回结果为 true 最终输出结果为:'Goodbye Jack'
    答案为:’Goodbye Jack’
  6. code:
    1
    2
    3
    4
    5
    6
    7
    var END = Math.pow(2, 53);
    var START = END - 100;
    var count = 0;
    for (var i = START; i <= END; i++) {
    count++;
    }
    console.log(count);
    解析:
    变量 STARTEND和差值为 100,也就是 for 循环会循环 100次,最终结果为: 100
    答案为:100
  7. code:
    1
    2
    3
    var ary = [0,1,2];
    ary[10] = 10;
    ary.filter(function(x) { return x === undefined;});
    答案为:[]
  8. code:
    1
    2
    3
    4
    5
    var two   = 0.2;
    var one = 0.1;
    var eight = 0.8;
    var six = 0.6;
    [two - one == one, eight - six == two]
    解析:
    由于浮点数的精度问题,所以two - one == one返回结果为 trueeight - six == two 0.8 - 0.6 equals 0.20000000000000007 返回结果为 false
    答案为:[true false]
  9. code:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function showCase(value) {
    switch(value) {
    case 'A':
    console.log('Case A');
    break;
    case 'B':
    console.log('Case B');
    break;
    case undefined:
    console.log('undefined');
    break;
    default:
    console.log('Do not know!');
    }
    }
    showCase(new String('A'));
    解析:
    由于 new String('A') 返回的是一个对象 String{‘A’},所以 switch 语句会返回 Do not know!
    答案为:’Do not know!’
  10. code:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      function showCase2(value) {
    switch(value) {
    case 'A':
    console.log('Case A');
    break;
    case 'B':
    console.log('Case B');
    break;
    case undefined:
    console.log('undefined');
    break;
    default:
    console.log('Do not know!');
    }
    }
    showCase2(String('A'));
    解析:
    由于 String('A') 返回的是一个字符串 ‘A’,所以 switch 语句会返回 Case A
    答案为:’Case A’
  11. code:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function isOdd(num) {
    return num % 2 == 1;
    }
    function isEven(num) {
    return num % 2 == 0;
    }
    function isSane(num) {
    return isEven(num) || isOdd(num);
    }
    var values = [7, 4, '13', -9, Infinity];
    values.map(isSane);
    解析:
    由于
    1
    2
    3
    4
    5
    7%2==0 || 7%2 == 1 //true
    4%2==0 || 4%2 == 1 //true
    '13'%2==0 || '13'%2 == 1 //true
    -9%2==0 || -9%2 == 1 //false
    Infinity%2==0 || Infinity%2 == 1 //false
    答案为:[true, true, true, false, false]
  12. code
    1
    2
    3
    parseInt(3, 8)
    parseInt(3, 2)
    parseInt(3, 0)
    parseInt(3, 8) -> 3, 3 是一个有效的八进制数,结果为:3
    parseInt(3, 2) -> NaN, 3 不是一个有效的二进制数,结果为:NaN
    parseInt(3, 0) -> 3, 0 代表十进制,结果为:3
    答案为: 3 NaN 3
  13. code:
    1
    Array.isArray( Array.prototype )
    答案为:true
  14. code:
    1
    2
    3
    4
    5
    6
    var a = [0];
    if ([0]) {
    console.log(a == true);
    } else {
    console.log("wut");
    }
    答题解析:
    在 if() 语句中,所有值都会被转换成 boolean 类型,所以 [0] 会被转换成 true,所以会进入console.log(a == true);
    当比较数组和布尔值时,两边都会被转换为数字,[0].toString() -> ‘0’转换为数字 -> 0, 0 == true -> false, 所以最终结果为:false
    答案为:false
  15. code:
    1
    []==[]
    答案为:false
  16. code:
    1
    2
     '5' + 3  
    '5' - 3
    答案为:'53' 2
  17. code:
    1
    1 + - + + + - + 1 
    解析:
    1+(- + + + - + 1) -> 1 + - + + + - (+ 1) -> 1 + - + + + (- + 1)->1 + - (+ + + - + 1)->1 + - -1 -> 1 + (- - 1)->1 + 1 -> 2
    答案为:2
  18. code:
    1
    2
    3
     var ary = Array(3);
    ary[0]=2
    ary.map(function(elem) { return '1'; });
    数组长度为 3,但是只有一个元素,所以最终结果为:['1',,]
    答案为:[‘1’,,]
  19. code:
    1
    2
    3
    4
    5
    6
    7
    8
    9
     function sidEffecting(ary) { 
    ary[0] = ary[2];
    }
    function bar(a,b,c) {
    c = 10
    sidEffecting(arguments);
    return a + b + c;
    }
    bar(1,1,1)
    解析:
    在非严格模式下sidEffecting(arguments) 会改变 arguments 的值,所以最终结果为:21
    答案为:21
  20. code:
    1
    2
    3
      var a = 111111111111111110000,
    b = 1111;
    a + b;
    因为 a 是一个 超过 2^53 的大整数,导致 a 的精度丢失,所以最终结果为:111111111111111110000
    答案为:111111111111111110000
  21. code:
    1
    Number.MIN_VALUE > 0
    Number.MIN_VALUE 是 JavaScript 中最小的正数,所以最终结果为:true
    答案为:true
  22. code:
    1
    [1 < 2 < 3, 3 < 2 < 1]
    解析:
    由于 < 1 < 2 < 3->true < 3 -> true, 3 < 2 < 1 -> false < 1 -> true 所以结果为:[true true]
    答案为:[true true]
  23. code:
    1
    2 == [[[2]]]
    答题解析:
    [[[2]]].toString()->’2’ 2 == ‘2’ -> true 所以最终结果为:true
    答案为:true
  24. code:
    1
    2
    3
    3.toString()
    3..toString()
    3...toString()
    解析:
    点运算符会被优先识别为数字常量的一部分,然后才是对象属性访问符:
    3.toString()会被解析为:(3.)toString(),所以会报错
    3..toString()会被解析为(3.).toString(),结果为:’3’
    3...toString()会被解析为(3.)..toString(),会报错
    答案为:’报错,’3’,报错’
  25. code:
    1
    2
    3
    4
    5
     (function(){
    var x = y = 1;
    })();
    console.log(y);
    console.log(x);
    解析:
    由于 var x = y = 1 会被解析为 y = 1; var x = y; 所以 y 会被定义为全局变量,x 会被定义为局部变量,所以最终结果为:1 undefined
    答案为:’1 undefined’
  26. code:
    1
    2
    3
     var a = /123/, b = /123/;
    a == b
    a === b
    解析:
    由于正则表达式是对象,所以 a == ba === b 会比较两个对象的引用,所以最终结果为:false false
    答案为:false false
  27. code:
    1
    2
    3
    4
    5
    6
    7
     var a = [1, 2, 3],
    b = [1, 2, 3],
    c = [1, 2, 4]
    a == b
    a === b
    a > c
    a < c
    解析:
    由于数组是对象,所以 a == ba === b 会比较两个对象的引用,所以a == b 和 a === b 结果为:false false
    由于数组会被转换为字符串,所以 a > ca < c 会比较两个数组的字符串形式,a -> ‘1,2,3’ c -> ‘1,2,4’,两个字符串比较如果首字母相等,会继续比较下一个字符,直到比较完成 所以最终结果为:false true
    答案为:false true
  28. code:
    1
    2
     var a = {}, b = Object.prototype;
    [a.prototype === b, Object.getPrototypeOf(a) === b]
    解析:
    由于对象没有 prototype 属性,所以 a.prototype 会返回 undefined,所以最终结果为:[false true]
    答案为:[false true]
  29. code:
    1
    2
    3
     function f() {}
    var a = f.prototype, b = Object.getPrototypeOf(f);
    a === b
    解析:
    由于函数的 prototype 属性是一个对象,而Object.getPrototypeOf(f) 是函数,所以最终结果为:false
    答案为:false
  30. code:
    1
    2
    3
    4
    function foo() { }
    var oldName = foo.name;
    foo.name = "bar";
    [oldName, foo.name]
    解析:
    The result shows ["foo", "foo"]. This demonstrates that:
    The initial foo.name is “foo” (which gets stored in oldName)
    Even though we try to change foo.name to “bar”, it remains “foo”
    This is because the name property of functions in JavaScript is read-only by default (it’s a non-writable property). In strict mode, attempting to change it would throw an error. In non-strict mode, the assignment is silently ignored.
    Object.getOwnPropertyDescriptor(foo, 'name').writable // false
    答案为:["foo", "foo"]
  31. code:
    1
    "1 2 3".replace(/\d/g, parseInt)
    解析:
    First digit: parseInt(“1”, 0) → 1 (radix 0 is treated as 10)
    Second digit: parseInt(“2”, 2) → NaN (invalid binary digit)
    Third digit: parseInt(“3”, 4) → 3 (valid in base 4)
    So the result is: “1 NaN 3”
    答案为:"1 NaN 3"
  32. code:
    1
    2
    3
    4
    5
    6
     function f() {}
    var parent = Object.getPrototypeOf(f);
    f.name // ?
    parent.name // ?
    typeof eval(f.name) // ?
    typeof eval(parent.name) // ?
    解析:
    f.name -> “f”
    parent.name -> ‘’
    typeof eval(f.name) -> “function”
    typeof eval(parent.name) -> “undefined”
  33. code:
    1
    2
    var lowerCaseOnly =  /^[a-z]+$/;
    [lowerCaseOnly.test(null), lowerCaseOnly.test()]
    解析:
    The result is [true, true]. This demonstrates that:test在检测时会隐性将内容转为字符串,其实等同于:’[lowerCaseOnly.test(‘null’), lowerCaseOnly.test(‘undefined’)]’
    答案为:[true, true]
  34. code:
    1
    [,,,].join(", ")
    解析:
    The result is ", , ". This demonstrates that:
    Empty array slots are treated as undefined
    When .join() is called with a separator, it places the separator between each element
    In this case, there are three slots, so two separators are inserted
    So the output is literally: , ,
    答案为:, ,
  35. code:
    1
    2
    var a = {class: "Animal", name: 'Fido'};
    a.class
    解析:
    The result is "Animal". This demonstrates that:This creates an object a with two properties:
    class with the value “Animal”
    name with the value “Fido” 答案为:“Animal”`

1. 有效括号匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* @return {Boolean}
* @param {string} s
*/
const isValid = function (s) {
if (s.length % 2 === 1) return false;
let stack = [];
const type = new Map([
[')', '('],
['}', '{'],
[']', '[']
]);
for (let char of s) {
if (type.has(char)) {
if (stack.length === 0 || stack[stack.length - 1] !== type.get(char)) {
return false;
}
stack.pop();
} else {
stack.push(char);
}
}
return stack.length === 0;
};

2. 给你一棵二叉树,想象你站在他的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值,示例:输入:[1,2,3,null,5,null,4],输出:[1,3,4]

二叉树数组表示的映射关系是这样的:
img

所以示例的二叉线表示出来是:

img

下面看查看右侧视图的代码实现吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* @return {Array}
* @param {string} input
* 输入:input = [1,2,3,null,5,null,4]
* 输出:[1,3,4]
*/
const rightSideView = (input) => {
// 定义二叉树节点
function TreeNode(val, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
function BFS(_root){
if (!root) return [];

const queue = [root];
const result = [];

while (queue.length > 0) {
const levelSize = queue.length;

for (let i = 0; i < levelSize; i++) {
const node = queue.shift();

// 如果是当前层的最后一个节点,添加到结果中
if (i === levelSize - 1) {
result.push(node.val);
}

// 把左子节点和右子节点按顺序加入队列
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return result;
}
// 示例二叉树 [1,2,3,null,5,null,4]
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.right = new TreeNode(5);
root.right.right = new TreeNode(4);
return BFS(root);
}

3 给指定的 key 属性给数组对象去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* @return {Array}
* @param {Array} arr
* @param {string} key
* 实例:
* 输入:arr = const arr = [
* { id: 1, name: 'Alice' },
* { id: 2, name: 'Bob' },
* { id: 1, name: 'Alice' },
* { id: 3, name: 'Charlie' }
* ];
* key='id'
* 输出:[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]
*/
//1. 使用 set+filter
function uniqueArr(arr,key){
return arr.filter((item, index, self) => index === self.findIndex(t => t[key] === item[key]));
}
//2. 使用 Map
function uniqueArr2(arr=[],key){
return Array.from(new Map(arr.map(item=>[item[key],item])).values());
}
/**
* 注意:方式 1 和方式 2 的区别:
* 1. 方式 1 去重后的结果里面重复元素取的是第一条而方式 2 是取的最后一条
* 2. 方式 1 简洁,适合数组较小的场景,更加高效,适合较大数组的去重
*/
//3. 使用reduce,此方式灵活性较高,可以处理更复杂的场景
function uniqueArr3(arr=[],key){
return arr.reduce((prev,cur)=>{
const x = prev.find(item => item[key] === cur[key]);
if(!x){
prev.push(cur);
}
return prev;
},[]);
}

浏览器进程

最新的 Chrome 浏览器包括:1 个浏览器(Browser)主进程、1 个 GPU 进程、1 个网络(NetWork)进程、多个渲染进程和多个插件进程,下面我们来逐个分析下这几个进程的功能。

  • 浏览器进程:负责管理和协调其他进程。它主要处理用户界面、标签页管理、窗口管理、地址栏、书签、导航、网络请求的调度、子进程管理,同时提供存储等功能。
  • 渲染进程: 核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • GPU 进程:Chrome 使用 GPU 进程来加速图形的渲染。它处理 3D CSS、WebGL、视频解码、以及图形相关的硬件加速任务。
  • 网络进程:负责管理所有的网络请求和数据传输,包括 HTTP、HTTPS、WebSocket 等。这个进程负责处理网络资源加载和缓存管理
  • 插件进程:专门用于运行浏览器插件(例如 Flash)。每个插件可以在一个独立的进程中运行。
  • 扩展进程:Chrome 扩展(如广告拦截器、密码管理器)通常在单独的进程中运行。这些进程执行扩展的后台任务、内容脚本、以及与网页和浏览器之间的交互。
  • 实用程序进程:用于处理浏览器的其他任务,如音视频解码、PDF 渲染、文件类型解析等。这些任务被分配给独立的实用程序进程,以确保浏览器主进程的稳定性和性能。
  • 服务工作进程:处理与 Service Workers 相关的任务,包括缓存管理、消息推送、后台同步等功能。这些进程允许 Web 应用在用户不直接与页面交互时仍能执行后台任务。
  • 内存管理进程:监控和管理 Chrome 的内存使用情况。它有助于防止内存泄漏、优化内存分配,并在需要时触发垃圾回收。
  • 其他进程:Chrome 有时会启动其他专门的进程以处理特定任务或实验性功能。例如,实验性功能可能会在不同的进程中进行测试,以确保不会影响核心浏览器的稳定性。

浏览器的渲染流程

  1. 浏览器接收 HTML ,javascript和 CSS 代码

    当用户访问一个网页时,浏览器会通过网络请求获取 HTML、CSS 和 JavaScript 文件。这些文件通过网络协议(例如 HTTP/HTTPS)传输到浏览器。浏览器开始下载这些资源并开始解析。
  2. 构建 DOM 树 (Document Object Model)

    浏览器开始逐行解析 HTML 文件,生成一个内部的结构化树,称为 DOM 树。这棵树代表了 HTML 文档的结构,其中每个节点对应一个 HTML 元素(如 body、div、h1 等)
  3. 构建 CSSOM 树 (CSS Object Model)

    浏览器同时解析所有与页面关联的 CSS 文件,包括 style 标签内的样式和外部的样式表链接。
  4. 合并 DOM 和 CSSOM 树为渲染树 (Render Tree)

    浏览器将 DOM 树和 CSSOM 树合并,生成渲染树。渲染树只包含可见元素(例如,display: none 的元素不会在渲染树中)。
  5. 布局 (Layout)

    计算元素位置和大小:在这个阶段,浏览器根据渲染树来计算每个元素在屏幕上的确切位置和大小,这个过程也称为 回流(reflow) 或 布局(layout)。
  6. 分层 (Layering) 和 分块 (Tiling)
    • 分层:一些复杂的元素(如包含 3D 变换、动画、固定定位等)会被提升为单独的图层。这有助于提高渲染性能,因为它允许特定的图层单独重绘而不影响其他图层。
    • 分块:对于大的图层,浏览器会将其分成多个小块(tiles)。这使得在滚动或动画过程中,只需重绘特定的块,而不是整个图层。
  7. 绘制 (Painting)
    • 绘制指令生成:浏览器将渲染树节点转换为绘制指令,指令会详细描述应该如何绘制每个节点的内容,例如颜色、边框、文字等。
    • 逐层绘制:每个图层都按照从背景到前景的顺序进行绘制,这个过程生成了最终要呈现的图像。
  8. 合成 (Compositing)

    浏览器的合成器进程会将所有图层组合在一起,形成一个最终的图像输出。这一步可以在 GPU 中完成,以加速渲染过程
  9. 显示 (Display)

    合成后的图像最终通过显示器输出,呈现给用户。这个过程可能包含了多次更新以适应动态变化,如用户滚动、JavaScript 动画、用户交互等。
  10. 持续的渲染与更新

重排”“重绘”和“合成”

  • 重排:更新了元素的几何属性

    通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的
  • 重绘:更新元素的绘制属性

    如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
  • 合成

    使用 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

    除此之外以下 CSS 属性通常不会触发重排和重绘,只会影响复合阶段(在 GPU 上操作),从而避免了性能开销:
    • transform:例如 transform: translateX(100px);
    • opacity:例如 opacity: 0.5;
    • filter:例如 filter: blur(5px);
    • will-change:例如 will-change: transform;(明确告诉浏览器即将变化的属性,可以优化复合性能)
    • backface-visibility:例如 backface-visibility: hidden;

javascript是单线程的,所以是通过事件循环机制在 JS 运行时处理异步操作的,在浏览器环境和nodejs环境它们的实现细节和优先级处理有所不同,我们先看浏览器环境下的事件循环机制,整体来说浏览器下的事件循环机制相对简单,他由四部分组成:

调用栈(Call Stack)

调用栈是一个 LIFO(Last In, First Out)后进先出的结构,用于存储在代码执行过程中创建的函数调用。每当一个函数被调用时,它会被添加到调用栈顶部。当函数执行完成后,它会从调用栈顶部弹出。

1
2
3
4
5
6
7
8
9
10
function first() {
console.log('First');
}

function second() {
first(); // 调用 first(),first() 被推入调用栈
console.log('Second');
}

second(); // 调用 second(),second() 被推入调用栈
  1. second() 被调用并推入调用栈。
  2. second() 调用了 first(),first() 被推入调用栈。
  3. first() 完成后,从调用栈弹出。
  4. second() 完成后,从调用栈弹出。

img

消息队列(Task Queue)

消息队列是一个 FIFO(先进先出)结构,用于存储待处理的异步任务(如事件处理、回调函数)。常见的任务包括:

  • 用户交互事件(点击、键盘输入等)的回调函数。

  • setTimeout、setInterval 的回调函数。

  • 网络请求(如 fetch、XHR)的回调

    虽然 fetch 返回一个 Promise,但实际的网络请求是通过浏览器内部机制异步处理的。这意味着网络请求的启动和响应的处理都是在宏任务的上下文中完成的

  • requestAnimationFrame

  • DOM 事件

注意:消息队列和宏任务(Macro Task)的区别

  • 包含关系:消息队列是存储宏任务的容器,宏任务是消息队列中的一个个任务。可以说消息队列由一系列宏任务组成。
  • 执行顺序:事件循环机制中,主线程执行完当前的同步代码后,会先检查并执行所有的微任务队列(Microtask Queue)。只有在微任务队列为空的情况下,事件循环才会从消息队列中取出下一个宏任务并执行。
    • 处理机制:
      • 宏任务:每次事件循环只执行一个宏任务。执行完宏任务后,立即处理微任务队列中的所有微任务,然后再从消息队列中取出下一个宏任务。
      • 消息队列:消息队列中可以包含多个宏任务,但每次事件循环只处理一个宏任务。只有在当前宏任务完成且微任务执行完毕后,才会取下一个宏任务。
  • 优先级:微任务的优先级高于宏任务。每次宏任务执行完后,事件循环会先检查并执行所有微任务,然后再继续下一个宏任务。消息队列本身没有优先级的概念,而是所有存储的任务都是按先进先出(FIFO)顺序执行。

微任务(Microtask Queue)

微任务队列用于处理高优先级的异步任务。微任务通常包括:

  • Promise 的回调(如 .then()、.catch())。

  • MutationObserver 的回调

    MutationObserver 是一种用于监听和响应 DOM 树中更改的 Web API。当 DOM 元素的结构、属性或文本内容发生变化时,MutationObserver 可以检测到这些变化,并执行相应的回调函数

  • queueMicrotask() 明确调度的微任务。

事件循环(Event Loop)

事件循环是一个持续运行的过程,其主要职责是查看执行栈是否为空,如果为空则检查消息队列中是否有任务待处理。如果有,则将队列中的第一个任务移入执行栈并开始执行。事件循环的每一次循环迭代称为“tick”。

事件循环的工作步骤

  1. 执行栈处理同步代码:当 JavaScript 引擎开始运行时,所有的同步任务(函数调用)都被推入执行栈中并立即执行。执行栈会持续运行直到栈为空。
  2. 执行微任务:执行栈清空后,事件循环检查微任务队列。它会一次性执行完所有在微任务队列中的微任务,直到队列为空。
  3. 执行宏任务:在微任务执行完毕后,事件循环会从消息队列中取出第一个任务(宏任务),将其推入执行栈并执行。
  4. 渲染更新:在每个宏任务之间,浏览器有机会更新渲染。如果 DOM 发生了更改(例如插入元素、修改样式等),浏览器会重新渲染 UI。
  5. 重复步骤 1-4:事件循环不断重复这些步骤,处理同步任务、微任务、宏任务、并更新渲染。
1
2
3
4
5
6
7
8
9
10
11
console.log('Start');  // 同步任务

setTimeout(() => {
console.log('Timeout'); // 宏任务
}, 0);

Promise.resolve().then(() => {
console.log('Promise'); // 微任务
});

console.log('End'); // 同步任务

执行顺序如下:
1. 同步代码先执行:console.log(‘Start’) 和 console.log(‘End’) 是同步任务,依次推入执行栈并执行,输出:

1
2
Start
End
  1. 微任务执行:Promise.resolve().then(…) 是微任务,它的回调函数被推入微任务队列。由于微任务优先于宏任务执行,所以 console.log(‘Promise’) 先输出:
    1
    Promise
  2. 宏任务执行:setTimeout 的回调函数被推入消息队列,在所有微任务完成后执行。最终输出:
    1
    Timeout

最终输出顺序:

1
2
3
4
Start
End
Promise
Timeout

img

总结

浏览器中的事件循环机制是管理 JavaScript 异步行为的核心,通过执行栈、消息队列、微任务队列的协同工作,实现了非阻塞、响应迅速的编程模型。了解事件循环有助于优化代码性能,避免阻塞主线程,提高用户体验。

npm, yarn查看源和换源:

1
npm config get registry 

设置npm镜像源为淘宝镜像

1
npm config set registry https://registry.npm.taobao.org/ 

查看yarn当前镜像源

1
yarn config get registry

设置yarn镜像源为淘宝镜像

1
yarn config set registry https://registry.npm.taobao.org/

镜像源地址部分如下:

1
2
3
4
5
6
7
npm --- https://registry.npmjs.org/
cnpm --- https://r.cnpmjs.org/
taobao --- https://registry.npm.taobao.org/
nj --- https://registry.nodejitsu.com/
rednpm --- https://registry.mirror.cqupt.edu.cn/
npmMirror --- https://skimdb.npmjs.com/registry/
deunpm --- http://registry.enpmjs.org/

如yarn 或者npm install的时候报错如下:

1
2
3
4
info There appears to be trouble with your network connection. Retrying...
info There appears to be trouble with your network connection. Retrying...
info There appears to be trouble with your network connection. Retrying...
error An unexpected error occurred: "https://registry.npmjs.org/eslint-plugin-react: tunneling socket could not be established, cause=connect ECONNREFUSED 127.0.0.1:1087".

问题是代理连接不上,可通过一下命令查看代理情况:

1
yarn config list 

修改代理为正确的即可,也可以使用淘宝镜像源解决

1
2
yarn config set proxy  http://username:password@server:port
yarn config set https-proxy http://username:password@server:port

token即标志、记号的意思,在IT领域也叫作令牌。在计算机身份认证中是令牌(临时)的意思,在词法分析中是标记的意思,一般作为邀请、登录系统使用

Token可以解决那些问题

  1. 服务端不用存储用户信息,也就没有分布式存储问题,也不用为了认证上Redis集群
  2. 前端存储token可以灵活掌控,也可以支持跨域访问
  3. 更适用于移动端(Android,iOS,小程序等等),像这种原生平台不支持cookie
  4. 不依赖cookie,可以避免CSRF跨站伪造攻击

Token认证流程


token 的流程是这样的:

  • 用户登录,服务端校验账号密码,获得用户信息
  • 把用户信息、token 配置编码成 token,通过 cookie set 到浏览器
  • 此后用户请求业务接口,通过 cookie 携带 token
  • 接口校验token有效性,进行正常业务接口处理

Token安全

  • 使用HTTPS传输
  • node 端的 cookie-session - npm 库来编码token
  • token涉及敏感权限,使用token 加数字签名的方式来避免token被篡改
  • 使用JWT生成和认证token

Token客户端存储

  • SessionStorage安全性较高,能比较好防御CSRF,但是对XSS无用,且局限较大
  • LocalStorage和IndexDB受同源保护,但是一旦被注入脚本也很危险
  • USB Key安全性较高,属于物理级别的防御,但是有一定的成本
  • Cookie最方便,但是默认状态下最不安全,需要将http-only,same-site,secure等开启才能有较高的安全性

JWT

JWT生成token原理


https://jwt.io
jwt生成的token是由三段字符串组成,并且用.连接起来

  1. 第一段字符串header,内部包含算法和token类型,如:{ “alg”: “HS256”,”typ”: “JWT”},将json转换为字符串后用Base64URL加密得到
  2. 第二段字符串payload,自定义值,一般存储用户信息,token超时时间等 如:{“sub”: “1234567890”,”name”: “John Doe”,”iat”: 1516239022},将json转换成字符串后用Base64URL加密得到
  3. 第三段字符串对前两部分签名,防止数据篡改
    首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
    HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)

jwt认证

  1. 获取客户端传过来的token,并用 “.” 切割得到三段字符串
  2. 对第二段字符串Base64URL解密,获取payload信息,检查token是否过期
  3. 将第一段密文和第二段密文拼接起来,使用生成token的Header里面的签名算法加密+生成token的密钥(secret)
  4. 将第三段密文和第3步得到的密文字符串对比

jwt被盗了会发生什么

简而言之:它很糟糕,真的很糟糕

  • 攻击者能一直使用您的JWT 访问该服务,直到它过期;能做的是生成jwt的时候让他的有效期短一些,但是如果攻击者通过中间人连接或者直接连接客户端或者你的服务器,这时候即使最短寿命的jwt也无济于事
  • jwt被盗的风险比丢失用户名和密码风险更大,用户名和密码登录往往还有其他的认证保护
  • 你很难知道你的用户jwt被盗了

jwt了被盗了怎么做

  1. 立即撤销受损的令牌。如果您在服务器上使用撤销列表来使令牌无效,则撤消令牌可立即将攻击者从系统中启动,直到他们获得新令牌为止。虽然这是一个临时解决方案,但它会让攻击者的生活变得更加困难。
  2. 强制您的客户立即更改密码。在Web或移动应用程序的上下文中,强制您的用户立即重置其密码,最好通过某种多因素身份验证流程,如Okta提供的那样。如果攻击者试图使用受感染的令牌修改用户登录凭据,则强制用户更改其密码可能会使攻击者远离其帐户。通过要求多因素身份验证,您可以更自信地重置其凭据的用户是他们所声称的人而不是攻击者。
  3. 检查客户的环境。用户的手机是否被盗,以便攻击者可以访问预先认证的移动应用程序?客户端是否从受感染的设备(如移动电话或受感染的计算机)访问您的服务?发现攻击者如何获得令牌是完全理解错误的唯一方法。
  4. 检查您的服务器端环境。攻击者是否能够从您的角色中妥协令牌?如果是这样,这可能需要更多的工作来修复,但越早开始就越好。

HTTP 的缓存机制旨在提高 Web 性能,通过减少不必要的网络请求、降低延迟和节省带宽。它利用缓存来存储服务器响应的副本,供后续请求使用。以下是 HTTP 缓存机制的基本工作原理和关键组件:

  1. 缓存的基本概念:

    • 缓存 (Cache) 是存储在客户端(如浏览器)、代理服务器或其他中间设备上的一份资源的副本。缓存机制可以避免重复请求相同的资源,减少服务器负载和网络流量。
  2. 缓存位置:

    • 浏览器缓存 (Client-Side Cache): 由浏览器存储的缓存,用于快速加载用户先前访问过的网页和资源。
    • 代理缓存 (Proxy Cache): 位于网络中间层的缓存服务器,可能会为多个用户存储常见资源。
    • 服务器缓存 (Server-Side Cache): 服务器端缓存,可能在多个请求间重复利用资源。
  3. 缓存控制:

    • HTTP 头 (HTTP Headers): HTTP 协议通过头部字段来控制缓存行为,常见的字段有 Cache-Control、Expires、ETag、Last-Modified 等。
  4. 缓存相关的 HTTP 头字段:

    • Cache-Control:
      • 控制缓存的主要指令。它可以指定资源是否可以缓存、缓存的最大时长、缓存的公共性等。
      • 常见指令:
        • no-cache: 每次使用资源前必须向服务器进行重新验证。
        • no-store: 不允许缓存,不存储响应数据。
        • max-age=: 指定资源可以被缓存的最长时间(以秒为单位)。
        • public: 允许任何缓存保存响应(如浏览器、代理服务器)。
        • private: 只允许特定用户的缓存保存响应(通常是浏览器)。
    • Expires:
      用于指定资源的过期时间(绝对时间)。一旦超过该时间,缓存将被视为过期,必须重新从服务器获取资源。通常不推荐使用,因为它依赖于客户端时间设置,容易出错。Cache-Control: max-age 更常用。
    • ETag (Entity Tag):
      由服务器生成的资源唯一标识符,用于验证资源是否发生改变。客户端请求时会携带这个标识符,服务器比较后确定资源是否需要重新发送。如果资源未变动,服务器会返回 304 Not Modified,指示客户端可以使用缓存版本。
    • Last-Modified:
      指示资源最后一次修改的时间。客户端请求时可以使用 If-Modified-Since 头部来询问服务器资源是否自该时间后被修改,如果没有修改,服务器返回 304 Not Modified。
    • Vary:
      指定缓存服务器如何判断不同的请求是否应该使用同一缓存响应。比如 Vary: User-Agent 意味着服务器会基于用户代理来区分缓存的响应。
  5. 缓存流程:
    img

    1. 首次请求:
      • 当用户首次请求某个资源时,浏览器将请求发送给服务器,服务器响应并将资源存储在缓存中,供未来请求使用。
    2. 后续请求:
      • 在后续请求中,浏览器首先检查缓存中是否有已存储的资源副本。
      • 如果缓存副本未过期(根据 Cache-Control 或 Expires),直接使用缓存资源。
      • 如果缓存副本过期或标识需要重新验证(no-cache),浏览器会向服务器发送条件性请求(携带 ETag 或 Last-Modified 信息)。如果资源未改变,服务器返回 304 Not Modified,浏览器使用缓存资源;否则服务器发送新的资源内容。
    3. 条件请求:
      • 条件请求是指客户端通过 If-None-Match(携带 ETag)或 If-Modified-Since(携带最后修改时间)请求资源,服务器会根据这些条件判断资源是否需要重新传输。
  6. 缓存的优缺点:

    • 优点:
      • 提高网站加载速度,减少带宽消耗。
      • 减轻服务器负载。
    • 缺点:
      • 缓存失效控制复杂,可能导致用户看到旧版本内容。
      • 不适用于动态变化频繁的内容。

总结

HTTP 缓存机制通过 Cache-Control、ETag、Last-Modified 等 HTTP 头字段,灵活地管理资源的缓存行为。它可以显著提升 Web 性能,减少网络请求量,但需要精细的配置以确保缓存的正确性和有效性。

作为一名前端开发工程师,难免和各种端打交道,那么这些端他们使用浏览器是什么,内核是什么,用于解释javascript的引擎什么,渲染引擎又是什么呢?

想知道程序运行使用的那个浏览器可以使用:http://www.ip33.com/browser.html查看

浏览器内核 javascript引擎 渲染引擎
ios小程序 WebKit javascriptCore(也称为 Nitro) WKWebView
安卓小程序 Mobile Chromium 内核 V8(旧版本是X5 JSCore) Mobile Chrome 53 内核
Chrome WebKit v8 Blink
Firefox 浏览器 Gecko SpiderMonkey Gecko
Safari 浏览器 WebKit JavaScriptCore (也称为 Nitro) WebKit
Microsoft IE 浏览器 基于 Chromium 的 EdgeHTML Chakra 基于 Chromium 的 Blink 引擎

特别注意:

在 iOS 和 iPadOS 上,Apple 强制所有浏览器使用 WebKit 作为渲染引擎和 JavaScriptCore 作为 JavaScript 引擎。这意味着即使你在 iOS 上使用 Chrome 或 Firefox,它们实际上也是在使用 WebKit 和 JavaScriptCore,而不是它们在其他平台上通常使用的引擎(如 Blink 和 V8)

项目是通过web-view内嵌在小程序里的vue单页应用.然而前几天发现明明发布了代码,在小程序入口进去看到的还是旧页面,尝试以下操作:

  • 手动退出小程序,再次进入
  • 删除小程序,发现-小程序,重新进入
  • 关闭微信,杀掉进程,重新进入
  • 配置Cache-Control:在服务器中,给入口html页面的访问添加响应头,如在nginx中配置 Cache-Control 为 no-store, no-cache,这样浏览器访问页面时,就不会缓存该页面。
    img
  • 安卓手机清除微信浏览器缓存
    1. debugx5.qq.com 手动清除安卓微信浏览器缓存
    2. 使用工具debugtbs, 在微信上打开http://debugtbs.qq.com, 然后将所有清除的按钮点击一遍,下次再进去就可以了
  • iOS手机利用微信自带清除缓存功能
    打开微信,找到:我–设置–通用–存储空间–清理微信缓存,确定即可完成。

    img
  • 入口html文件设置meta标签
    1
    2
    3
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
  • 给页面访问地址增加时间戳参数
    1
    2
    3
    const src = `https://XXX.com?timestamp=${new Date().getTime()}`;

    <web-view src='{{src}}'></web-view>
  • 在webpack打包的时候加上hash配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    module.exports = {
    optimization: {
    chunkIds: 'deterministic',
    },
    output: {
    entry: './src/index.js',
    mode: 'none',
    module: moduleConfig,
    output: {
    filename: '[name].[contenthash].js',
    // chunkFilename: '[name].[contenthash].chunk.js',
    path: path.resolve(__dirname, 'dist/contenthash'),
    clean: true
    },
    plugins: [
    new MiniCssExtractPlugin({
    // css 单独输出成文件
    filename: '[name].[contenthash].css'
    })
    ]
    },
    };
    build后的产物就类似于这种:/dist/main.103a1483.js 浏览器缓存策略识别到这是一个新的文件会去服务端从新请求资源,自然解决了缓存问题
    • 最后终极解决方案就是:在页面做版本管理
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      const getCurrentHash = (html) => {
      const curHashSrc = (html || document).getElementsByTagName('head')[0]
      .getElementsByTagName('script')[2].src.split('/');
      return curHashSrc[curHashSrc.length - 1].split('.')[0]?.split('-')?.[1];
      };
      const fetchNewHash = async () => {
      // 在 js 中请求首页地址不会更新页面
      const timestamp = new Date().getTime();
      const response = await fetch(`${window.location.origin}?time=${timestamp}`);
      if (!response.ok) {
      throw new Error('Network response was not ok ' + response.statusText);
      return;
      }
      // 返回的是字符串,需要转换为 html
      const el = document.createElement('html');
      el.innerHTML = await response.text();
      // 拿到 hash
      const newHash = getCurrentHash(el)
      const currentHash = getCurrentHash();
      console.log('%cnewHash: %c%s %ccurrentHash: %c%s',
      'color: #000;',
      'color: red;',
      newHash,
      'color: #000;',
      'color: green;',
      currentHash
      );
      if (newHash && newHash !== currentHash) {
      // 版本更新,弹出提示
      console.log('%c有新版本更新', 'color: red; font-size: 16px;');
      window.location.reload();
      } else if (newHash && newHash === currentHash) {
      //没有版本更新
      console.log('%c没有新版本更新', 'color: green; font-size: 16px;');
      }
      }
      export default fetchNewHash;

  1. React 基础知识

    1. React的核心概念是什么?
      1. 组件(Components)
      2. JSX(JavaScript XML)
      3. 虚拟 DOM(Virtual DOM)
      4. 单向数据流(One-way Data Flow)
      5. 状态(State)
      6. 属性(Props)
      7. 生命周期方法(Lifecycle Methods)
      8. Hooks
    2. 什么是 JSX?与普通的 JavaScript 不同?
      • JSX 是一种强大的语法工具,让你可以在 JavaScript 中直接编写类似 HTML 的代码。与普通 JavaScript 相比,JSX 提高了代码的可读性和维护性,特别适合用于定义复杂的用户界面结构。
      • 在 JSX 中,嵌入的表达式会自动进行转义处理,防止跨站脚本攻击(XSS)。
  2. 组件和生命周期

    1. useEffect是什么,他有什么作用?
      useEffect 是 React 中的一个 Hook,用于在函数组件中执行副作用(side effects),副作用包括数据获取、订阅、手动更改 DOM,以及其他无法在纯函数中完成的操作

      1. 默认情况下,useEffect 在每次渲染后运行
      2. 如果传递一个依赖数组,useEffect 只会在数组中的某个依赖项发生变化时执行
      3. 可以在 useEffect 中返回一个函数,这个函数会在组件卸载时、或在执行下一次副作用之前被调用,这里在执行下一次副作用前被调用很关键,不太好理解
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import React, { useState, useEffect } from 'react';

      function ResourceLoader({ resource }) {
      const [data, setData] = useState(null);

      useEffect(() => {
      const fetchData = async () => {
      const response = await fetch(`https://api.example.com/${resource}`);
      const result = await response.json();
      setData(result);
      };

      fetchData();

      // 清理函数:在依赖项更新时重置数据
      return () => {
      setData(null);
      console.log('Previous data cleared');
      };
      }, [resource]); // 当 `resource` 改变时,清理上一次的数据并重新加载

      return <div>Data: {JSON.stringify(data)}</div>;
      }

      在这个例子中,每次 resource 改变时,useEffect 中的清理函数会先被调用,清除上一次的 data,然后再次发起新的数据请求。

    2. 如何使用 useEffect 模拟生命周期方法?

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      import React, { useState, useEffect } from 'react';

      function MyComponent() {
      const [count, setCount] = useState(0);

      useEffect(() => {
      // 相当于 componentDidMount
      console.log('Component mounted');
      // 相当于 componentDidUpdate
      console.log(`Component updated: count is ${count}`);
      // 卸载时的清理函数
      return () => {
      // 相当于 componentWillUnmount
      console.log('Component will unmount');
      };
      }, [count]); // count 作为依赖项,只要 count 变化,useEffect 就会执行。
      }
    3. useLayoutEffect 与 useEffect 的区别?

      • useEffect:
        • useEffect 是异步执行的,它会在浏览器绘制完成之后触发,因此它不会阻塞页面的渲染。
        • 常用于执行副作用,如数据获取、订阅事件、修改 DOM 等。
      • useLayoutEffect:
        • useLayoutEffect 是同步执行的,在 DOM 更新后、浏览器绘制之前触发。因此它适合需要直接读取或修改 DOM 布局的操作。
        • 例如,你需要在浏览器绘制前测量 DOM 元素的大小或位置,或者强制同步地执行一些 DOM 操作。
    • 使用场景
      1. 测量 DOM 元素:如果你需要在组件渲染后立即测量 DOM 元素的尺寸或位置,并在测量结果的基础上进行一些操作,比如调整布局,那么 useLayoutEffect 是理想的选择。
      2. 强制 DOM 更新:当需要立即更新 DOM,而不是等待浏览器绘制后再进行更新时,useLayoutEffect 可以确保 DOM 的变更在绘制之前完成。
      3. 与动画相关的操作:在动画开始前设置 DOM 状态以避免闪烁或抖动。
  3. React Hooks

    1. 什么是 React Hooks?它们为何被引入?
      1. 简化组件逻辑:
        • 在类组件中,状态管理和生命周期方法往往分散在不同的部分,导致逻辑难以组织和维护。Hooks 允许将相关逻辑更紧密地组合在一起,即使它们涉及到状态或副作用。
        • 例如,处理状态变化和副作用的逻辑可以在同一个 useEffect 中完成,而不是分散在 componentDidMount、componentDidUpdate 和 componentWillUnmount 之中。
      2. 避免类组件的复杂性:
        • 类组件有时候会引入一些复杂性,特别是对初学者来说。你需要了解 this 的用法、生命周期方法的顺序、继承等。
        • Hooks让你通过函数的方式处理组件逻辑,避免了使用类的许多复杂性。
      3. 复用状态逻辑:在类组件中,复用状态逻辑通常需要使用高阶组件(HOC)或渲染属性(render props),这些模式可能会导致组件嵌套过深,代码难以理解。
        • Hooks 提供了一种新的状态逻辑复用方式。通过创建自定义 Hooks,可以提取和共享逻辑,而不会改变组件的结构。
      4. 更好的代码组织:
        • Hooks 使得在一个函数组件中更自然地组合状态、事件处理、数据获取等逻辑,不必分散在多个生命周期方法中。
        • 例如,通过 useState 和 useEffect,你可以将状态管理和副作用处理逻辑整合在一起,使代码更直观。
      5. 向后兼容:
        • Hooks 与现有的类组件完全兼容,开发者可以逐步引入 Hooks 而不必完全重写现有的代码库。
    2. 常见的 React Hooks 有哪些?它们的用途是什么?
      1. useState,useEffect,useContext,useReducer,useRef,useMemo,useCallback,useLayoutEffect,useImperativeHandle,
    3. 如何自定义 Hook?有什么场景需要自定义 Hook?
  4. 高级主题
    什么是 Context API?如何使用它?
    如何处理 React 中的性能问题?
    什么是 React 的高阶组件 (Higher-Order Components)?
    什么是 Render Props 模式?与高阶组件相比有哪些优缺点?

  5. 状态管理
    如何在 React 中进行全局状态管理?
    Redux 和 Context API 的区别是什么?
    什么是 Redux Saga 或 Redux Thunk?它们的区别是什么?

  6. 路由和异步操作
    如何在 React 中进行路由管理?
    如何在 React 中处理异步数据?

  7. 架构与设计

    1. 如何设计一个可复用的 React 组件?
    2. 如何处理组件之间的通信?
    3. 如何在 React 中处理表单和表单验证?
    4. 如何管理大型 React 应用中的组件结构?
    5. 如何处理 React 应用中的依赖注入?
  8. React 中的性能问题

    1. React 中的 Reconciliation 过程是怎样的?
    2. React 组件的 key 属性在列表渲染中有何作用?
    3. React 中的性能优化有哪些策略?
    4. 什么是虚拟 DOM?它如何提高性能?
    5. 如何优化 React 应用的加载性能?
    6. 如何优化 React 中的大量列表渲染?
    7. 如何使用 React Profiler 工具进行性能调优?
    8. 如何处理 React 中的慢渲染问题?
      讨论如何优化组件结构,减少不必要的渲染,使用 memoization 技术,以及如何处理大规模数据渲染。
  9. React 实践问题

    1. 如何处理 React 中的错误边界?
    2. 如何在 React 应用中实现国际化 (i18n)?
    3. React 18 中有哪些新的特性?
    4. 如何处理 React 中的访问控制 (access control)?
    5. React Fiber 是什么?它如何改进 React 的性能?
    6. 什么是 Suspense 和 Concurrent Mode?它们如何改善用户体验?
    7. 如何在 React 中实现 SSR(服务端渲染)?
    8. 如何在 React 中实现 CSS 作用域?
    9. 什么是 React Portals?它的应用场景有哪些
    10. 如何在 React 中处理长时间运行的异步任务?
    11. 如何在 React 中处理路由切换时的数据预加载?
    12. 如何在 React 应用中处理环境配置和敏感信息?
      1. dotenv工具加载配置
    13. 如何在 React 项目中实施模块热替换(HMR)
    14. 如何在 React 中实现实时数据更新?
      1. 长轮讯:保持连接直到有新数据可用,减少网络流量和负载和服务器负载,但存在通信延迟
      2. webSockets: 提供全双工通信,适用于低延迟和高频率的更新的应用,但实现较为复杂
      3. SSE(Server-Sent Events):单向通信,适用于实时更新客户端而无需客户端发送的数据场景
      4. WebTransport: 基于HTTP/3和QUIC协议,提供高效的数据传输,但支撑尚不广泛
      5. WebRTC:支持浏览器和移动应用点对点通信,适用于流媒体音频,视频和数据交换,主要用于客户端间的交互
0%