0%

全局状态管理插件设计的核心思想都可以概括成:不能随意的去修改状态(state)。因此我们通常需要一个 action 来统一 modify 我们的 state。
MobX 也是基于这种思想设计的,它具有:

  • 定义状态并使其可观察 (observable)
  • 创建视图以响应状态的变化(observer、computed)
  • 更改状态(action)

image.png

Function 组件里使用

由于 React Hooks 的出现,以及注解的不稳定性。在 6.x 中,MobX 升级了它的使用方法。
这里我们以 makeAutoObservable 的 Store 设计为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export class Store6_makeAutoObservable {
 readonly base = 5;
 MCount = 0;

 constructor() {
   makeAutoObservable(this);
}

 setMCount() {
   this.MCount++;
}

 get total() {
   return this.MCount * this.base;
}
}

export default new Store6;

然后在函数组件里,我们需要使用 observer 函数来替代注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { observer } from "mobx-react";
import store6 from "@/store";

export const MobXFunc: React.FC = observer(() => {
return (
<div>
     
<button
onClick={() => {
store6.setMCount();
}}
>
       count++      
</button>
     <span> Count {store6.MCount} </span>     
<span> Base {store6.base} </span>     <span> Total {store6.total} </span> 
 
</div>
);
});

MobX + Hooks 写法

为了更好的结合 Hooks 语法,Mobx 在 6.x 中也提供了 2 个新的 API:

useLocalStore(Hooks 环境下的 observable)

1
const store = useLocalStore(() => ({ key: "value" }));

等价于

1
const [store] = useState(() => observable({ key: "value" }));

为 Hooks 解决了 依赖传递 和 缓存雪崩 的问题。

useObserver

Mobx 使组件响应数据状态的变化主要有以下三种方式:

  • @observer
    • 给类组件提供 pure component 的能力,将组件的 props 和 state 转换为 observable 态,响应数据变化
    • 不推荐在 Hooks 中使用
  • observer 方法
  • Component:Observer(Mobx 6 中已经基于 useObserver 来实现了)
  • Hooks:useObserver

还是以 makeAutoObservable 的 Store 设计为例,这里就不展示了。

我们在函数组件里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import React from "react";
import { Observer, useLocalObservable, useObserver } from "mobx-react";
import { store6_auto as store6 } from "@/store";

export const MobXHook: React.FC = () => {
const store = useLocalObservable(() => store6);
return useObserver(() => (
<div>
     
<button
onClick={() => {
store.setMCount();
}}
>
       count++      
</button>
     <span> Count {store.MCount} </span>     
<span> Base {store.base} </span>     <span> Total {store.total} </span>   
</div>
));
};

// 或者
export const MobXHook: React.FC = () => {
const store = useLocalObservable(() => store6);
return (
<Observer>
   {" "}
{() => (
<div>
         
<button
onClick={() => {
store.setMCount();
}}
>
           count++          
</button>
         <span> Count {store.MCount} </span>         
<span> Base {store.base} </span>         <span>
{" "}
Total {store.total}{" "}
</span>       
</div>
)}
   
</Observer>
);
};

同时,useLocalObservable 也可以用来创建一个新的 observable,并在组件的整个生命周期内将其保留在组件中(可以理解为组件级别的 observer)。

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
const MouseEventListenerMobx: React.FC = (props) => {
const state = useLocalStore(
(target) => ({
x: 0,
y: 0,
handler(e) {
const nx = e.xxx;
const ny = e.xxx;
if (
Math.abs(nx - state.x) >= target.size ||
Math.abs(ny - state.y) >= target.size
) {
state.x = nx;
state.y = ny;
}
},
}),
props
);

useEffect(() => {
document.addEventListener("mousemove", state.handler);
return () => document.removeEventListener("mousemove", state.handler);
}, []);

return useObserver(() => props.children(state.x, state.y));
};

最终推荐方案

综上所述,在结尾给出最终推荐方案。
在 store 设计上,建议采用 makeAutoObservable 的写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export class Store6_makeAutoObservable {
readonly base = 5;
MCount = 0;

constructor() {
makeAutoObservable(this);
}

setMCount() {
this.MCount++;
}

get total() {
return this.MCount * this.base;
}
}

export default new Store6();

在 store 使用上,建议采用 observer 函数包裹组件。

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 from "react";
import { observer } from "mobx-react";
import { store6_auto as store6 } from "@/store";

const MobXHook: React.FC = () => {
const store = store6;
return (
<div>
     
<button
onClick={() => {
store.setMCount();
}}
>
       count++      
</button>
     <span> Count {store.MCount} </span>     
<span> Base {store.base} </span>     <span> Total {store.total} </span>   
</div>
);
};

export default observer(MobXHook);

FC 就是 Function Component.也就是函数式组件.

不变的

函数式组件每一次渲染都会存储当前的状态,他是不变的.
每一次渲染都有自己的 pops 和 state,也有自己的事件处理函数.

1
<p>you click {count} times</p>

当我们更新状态时,React 会重新渲染组件.每一次渲染都能拿到独立的 count 状态,这个状态是函数中的一个常量.
任意一次渲染中的 count 常量都不会随着时间改变.渲染输出会变是因为我们的组件被一次次调用.
而每次调用引起的渲染中,它包含的 count 值独立于其他渲染.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function App() {
const [count, setCount] = useState(0);

const showMessage = () => {
setTimeout(() => {
alert("Now count: " + count);
}, 3000);
};

return (
<div>
     <p>You clicked {count} times</p>     <button
onClick={() => setCount(count + 1)}
>
Click me
</button>     <button onClick={showMessage}>Print</button>   
</div>
);
}

点击 Print 后,再点击 click,打印的是点击 Print 时的 count 值,因为 count 是独立的,拿到的是那时候的.

useEffect 的依赖项

依赖项里进行的比较是浅比较。

无限循环

什么可能会造成无限循环呢?

  1. 没有依赖项
  2. 依赖项是函数,对象,数组
  3. 依赖项不正确

当依赖项是函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function App() {
const [count, setCount] = useState(0);

function logResult() {
return 2 + 2;
}
useEffect(() => {
setCount((count) => count + 1);
}, [logResult]); // 函数作为依赖项
return (
<div className="App">
<p> value of count: {count} </p>
</div>
);
}

看上去,应该是只在第一次渲染时增加 count。实际上会报错。
因为,依赖项进行的是浅比较,每次 render 都会重新定义函数的引用。就会触发 useEffect 的更新,进入无限循环。

解决

使用 useCallback。

1
2
3
4
5
6
const logResult = useCallback(() => {
return 2 + 2;
}, []);
useEffect(() => {
setCount((count) => count + 1);
}, [logResult]);

当依赖项是对象

1
2
3
4
5
6
7
8
9
10
11
12
const [count, setCount] = useState(0);
const person = { name: "Rue", age: 17 }; //创建一个对象
useEffect(() => {
// 每次增加count的值
// person的值发生了变化
setCount((count) => count + 1);
}, [person]); // 依赖项数组包含一个对象作为参数
return (
<div className="App">
<p> Value of {count} </p>
</div>
);

同样和依赖项是函数一样,依赖项是对象也因为浅比较,每次 render 都重新得到一个新的对象,出现无限循环的问题。

解决

使用 useMemo,确保对象在每次渲染期间不会改变;

1
2
3
4
const person = useMemo(() => ({ name: "Tom", age: 10 }), []);
useEffect(() => {
setCount((count) => count + 1);
}, [count]);

疑问: 如果是 useState 保存的对象作为依赖,该如何处理?

当依赖项是数组

1
2
3
4
5
6
const [count, setCount] = useState(0); //初始值为0。
const myArray = ["one", "two", "three"];

useEffect(() => {
setCount((count) => count + 1); // 和前面一样,增加Count的值
}, [myArray]); // 将数组变量传递给依赖项

无限循环的原因与上面无异

解决

使用 useRef。

1
2
3
4
5
6
const [count, setCount] = useState(0);
const { current: myArrray } = useRef(["one", "two", "three"]);

useEffect(() => {
setCount((count) => count + 1);
}, [myArray]);

注意

无限循环问题是在 useEffect 中 setState 了新值导致的,在其他地方是不会无限循环的。

Option 1 - 依赖于对象属性

在这个例子中,当前组件会从 props 获取一个对象,并在 useEffect 的依赖数组中使用这个对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ team }) => {
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers);
}
}, [team]);

return <Players team={team} players={players} />;
};

理想情况下,props 传入的 team 的内容是一样的话,其内存地址也会相同。但这其实是无法保证的。

所以,为了解决这个问题,我们可以只使用 team 对象里的一些属性,而不是使用整个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ team }) => {
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers);
}
}, [team.id, team.active]);

return <Players team={team} players={players} />;
};

假设 team 对象中的 id 和 active 属性都是基本数据类型,effect 就只会在 id 或者 active 属性发生变化的时候执行(注意是或,不是且)。

team 对象如果是在组件内被创建的话也能够起到作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
// construct the object from props and/or state
const team = { id, name, active };
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team.id).then(setPlayers);
}
}, [team.id, team.active]);

return <Players team={team} players={players} />;
};

所以,即使 team 对象在每次渲染过程中都重新被创建,也不会导致 effect 每次渲染都会执行,因为 useEffect 只依赖于 id 和 active 属性。

需要注意的是,这个方案不适用于依赖元素为数组的情况。

Option2 - 在内部创建对象

在上一个例子中,如果 effect 不是使用对象里的元素,而是以整个对象作为依赖会发生什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
// construct the object from props/state
const team = { id, name, active };
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [team]);

return <Players team={team} players={players} />;
};

team 对象是在每次渲染下重新创建的,所以 useEffect 在每次重新渲染时都会执行。

幸运的是,react-hooks/exhaustive-depsESLint rule 提示道:

The 'team' object makes the dependencies of useEffect Hook change on every render. To fix this, wrap the initialization of 'team' in its own useMemo() Hook.

在我们使用 ESLint 推荐的使用 useMemo Hook 之前,我们可以尝试更加简单的操作。我们可以尝试两次创建 team 对象,一个用于传递给 Player 子组件,一个用于 useEffect 内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect } from "react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
const [players, setPlayers] = useState([]);

useEffect(() => {
// recreate the `team` object within `useEffect`
// from props/state
const team = { id, name, active };

if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [id, name, active]);

const team = { id, name, active };

return <Players team={team} players={players} />;
};

现在 team 对象是在 useEffect 内部被创建,并且只在 effect 要被执行的时候被创建。id、name、active 作为依赖,只有当这些值发生变化的时候 effect 才会执行。

创建对象相对来说开销是比较小的,所以在 useEffect 中重新创建一个 team 对象是可以接受的。
优化 useEffect 所带来的性能提升远远大于创建两个对象所带来的性能损耗。

Option3 - 记忆对象

然而,如果创建对象或数组的开销是昂贵的,那重复创建对象就会比执行多次 effect 更糟糕。在这种情况下,我们需要“缓存”创建的对象或数组,这样当其中的数据没有发生变化时,这个对象或数组就不会在渲染过程中发生变化。这个过程称为“记忆”(memoization),我们通过 useMemo 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { useState, useEffect, useMemo } from "react";
import { createTeam } from "../utils";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ id, name, active }) => {
// memoize calling `createTeam` because it's
// an expensive operation
const team = useMemo(
() => createTeam({ id, name, active }),
[id, name, active]
);
const [players, setPlayers] = useState([]);

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [team]);

return <Players team={team} players={players} />;
};

假设这个 createTeam()方法开销昂贵,那我们自然会希望它执行的次数越少越好。useMemo Hook 可以实现只有在 id、name 或者 active 在渲染过程中发生变化时,才会再次创建 team。但如果在 Team 组件重新渲染过程中,以上属性没有一个发生变化,team 对象就会是同一个对象。因为是同一个对象,我们就可以放心地使用 useEffect,不用担心会执行不必要的次数。

Option 4 - 自己创建

如果以上方案都无法解决问题怎么办?比如从 props 中传入了对象或者数组,这个对象或数组会成为 useEffect 的依赖数组元素,而且我们并不知道创建一个新的 team 对象所需的属性。但我们还是想要实现在组件渲染过程中“缓存”这个对象的值。

在这种情况下,我们可以使用 useRef Hook 替代 useMemo Hook。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState, useEffect, useRef } from "react";
import isDeepEqual from "fast-deep-equal/react";
import { getPlayers } from "../api";
import Players from "../components/Players";

const Team = ({ team }) => {
const [players, setPlayers] = useState([]);
const teamRef = useRef(team);

if (!isDeepEqual(teamRef.current, team)) {
teamRef.current = team;
}

useEffect(() => {
if (team.active) {
getPlayers(team).then(setPlayers);
}
}, [teamRef.current]);

return <Players team={team} players={players} />;
};

这里使用了 isDeepEqual 来判断 teamRef.current 和 team 的值是否一致,而不是比较两者的内存地址。所以,即使在每次渲染过程中 team 对象是一个新的对象,如果它的内容是一致的,isDeepEqual()也会返回 true。

所以当两者在做深比较的时候,如果内容一致,isDeepEqual 会返回 true,teamRef.current 会继续指向原本 team 对象的内存地址。那依赖数组里的元素 teamRef.current 就没有发生变化,useEffect 也不会再执行。如果 isDeepEqual 返回 false,teamRef.current 就会被赋予新的 team 值,effect 就会执行。

如果你发现自己遇到了 Option4 这样的情况,我建议安装 react-usenpm 包,使用其中的 useDeepCompareEffect Hook 来解决问题,还能够避免 react-hooks/exhaustive-depslint rule 报错。

总结

大多数情况下,我能用 Option1 来解决问题。如果 Option1 无法解决,我就会使用 react-use 包里的 helper Hook 来解决问题。

多次调用同一个 setState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function App() {
const [num, setNum] = React.useState(0); // 声明一个变量 num

const add = () => {
setNum(num + 1);
console.log(num);
setNum(num + 2);
console.log(num);
};

return (
<div>
<div>{num}</div>
<button onClick={add}>加1</button>
</div>
);
}

这样只会计算最后一个 setNum。
如果想依赖上一次的可以使用函数写法。

1
2
3
4
5
const add = () => {
setNum((n) => n + 1);
// 一些操作
setNum((n) => n + 2);
};

这种写法是传入的就不是直接的值,而是一个 function,它接受前面的 state,我们可以根据这个 stat 来做一些处理以后,再返回一个 state。
所以我们这里调用完以后,页面上就会成功显示加 3 后的结果了。(但是一定要记住我说的:生效的只会是最后一个setState,如果你在最后仍写的是setNum(x),最后都会以 x 的值作为渲染)

React 面试专题

React.js 是 MVVM 框架吗?

React 就是 Facebook 的一个开源 JS 框架,专注的层面为 View 层,不包括数据访问层或者那种 Hash 路由(不过 React 有插件支持),与 Angularjs,Emberjs 等大而全的框架不同,React 专注的中心是 Component,即组件。React 认为一切页面元素都可以抽象成组件,比如一个表单,或者表单中的某一项。

React 可以作为 MVVM 中第二个 V,也就是 View,但是并不是 MVVM 框架。MVVM 一个最显著的特征:双向绑定。React 没有这个,它是单向数据绑定的。React 是一个单向数据流的库,状态驱动视图。react 整体是函数式的思想,把组件设计成纯组件,状态和逻辑通过参数传入,所以在 react 中,是单向数据流,推崇结合 immutable 来实现数据不可变。

hooks 用过吗?聊聊 react 中 class 组件和函数组件的区别

类组件是使用 ES6 的 class  来定义的组件。 函数组件是接收一个单一的  props  对象并返回一个 React 元素。

关于 React 的两套 API(类(class)API 和基于函数的钩子(hooks) API)。官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较”轻”,而类比较”重”。而且,钩子是函数,更符合 React 函数式的本质。

函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据函数这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。函数的返回结果只依赖于它的参数。不改变函数体外部数据、函数执行过程里面没有副作用。

类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。

类组件的缺点 :

大型组件很难拆分和重构,也很难测试。
业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
组件类引入了复杂的编程模式,比如 render props 和高阶组件。
难以理解的 class,理解 JavaScript 中  this  的工作方式。

区别

函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。

1.状态的有无
hooks 出现之前,函数组件没有实例没有生命周期没有state没有this,所以我们称函数组件为无状态组件。 hooks 出现之前,react 中的函数组件通常只考虑负责 UI 的渲染,没有自身的状态没有业务逻辑代码,是一个纯函数。它的输出只由参数 props 决定,不受其他任何因素影响。

2.调用方式的不同
函数组件重新渲染,将重新调用组件方法返回新的 react 元素。类组件重新渲染将 new 一个新的组件实例,然后调用 render 类方法返回 react 元素,这也说明为什么类组件中 this 是可变的。

3.因为调用方式不同,在函数组件使用中会出现问题
在操作中改变状态值,类组件可以获取最新的状态值,而函数组件则会按照顺序返回状态值

React Hooks(钩子的作用)

Hook  是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

React Hooks 的几个常用钩子:

  1. useState() //状态钩子
  2. useContext() //共享状态钩子
  3. useReducer() //action 钩子
  4. useEffect() //副作用钩子

还有几个不常见的大概的说下,后续会专门写篇文章描述下

  • 1.useCallback 记忆函数
    一般把函数式组件理解为 class 组件 render 函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。而有了 useCallback 就不一样了,你可以通过 useCallback 获得一个记忆后的函数。
1
2
3
4
5
6
function App() {
const memoizedHandleClick = useCallback(() => {
console.log("Click happened");
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}

第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback 就会重新返回一个新的记忆函数提供给后面进行渲染。

  • 2.useMemo 记忆组件
    useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。
    唯一的区别是:useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你
    所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。
  • 3.useRef 保存引用值

useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用。useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问组件或真实的 DOM 节点,重点是组件也是可以访问到的,从而可以对 DOM 进行一些操作,比如监听事件等等。

  • 4.useImperativeHandle 穿透 Ref

通过 useImperativeHandle 用于让父组件获取子组件内的索引

  • 5.useLayoutEffect 同步执行副作用

大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。

useEffect 和 useLayoutEffect 有什么区别:简单来说就是调用时机不同,useLayoutEffect 和原来 componentDidMount&componentDidUpdate 一致,在 react 完成 DOM 更新后马上同步调用的代码,会阻塞页面渲染。而 useEffect 是会在整个页面渲染完才会调用的代码。官方建议优先使用useEffect

React 组件通信方式

react 组件间通信常见的几种情况:

    1. 父组件向子组件通信
    1. 子组件向父组件通信
    1. 跨级组件通信
    1. 非嵌套关系的组件通信

1)父组件向子组件通信

父组件通过 props 向子组件传递需要的信息。父传子是在父组件中直接绑定一个正常的属性,这个属性就是指具体的值,在子组件中,用 props 就可以获取到这个值

1
2
3
4
5
6
7
8
9
// 子组件: Child
const Child = (props) => {
return <p>{props.name}</p>;
};

// 父组件 Parent
const Parent = () => {
return <Child name="京程一灯"></Child>;
};

2)子组件向父组件通信

props+回调的方式,使用公共组件进行状态提升。子传父是先在父组件上绑定属性设置为一个函数,当子组件需要给父组件传值的时候,则通过 props 调用该函数将参数传入到该函数当中,此时就可以在父组件中的函数中接收到该参数了,这个参数则为子组件传过来的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 子组件: Child
const Child = (props) => {
const cb = (msg) => {
return () => {
props.callback(msg);
};
};
return <button onClick={cb("京程一灯欢迎你!")}>京程一灯欢迎你</button>;
};

// 父组件 Parent
class Parent extends Component {
callback(msg) {
console.log(msg);
}
render() {
return <Child callback={this.callback.bind(this)}></Child>;
}
}

3)跨级组件通信

即父组件向子组件的子组件通信,向更深层子组件通信。

  • 使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了复杂度,并且这些 props 并不是中间组件自己需要的。
  • 使用 context,context 相当于一个大容器,我们可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。
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
// context方式实现跨级组件通信
// Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据

const BatteryContext = createContext();

// 子组件的子组件
class GrandChild extends Component {
render() {
return (
<BatteryContext.Consumer>
{(color) => <h1 style={{ color: color }}>我是红色的:{color}</h1>}
</BatteryContext.Consumer>
);
}
}

// 子组件
const Child = () => {
return <GrandChild />;
};
// 父组件
class Parent extends Component {
state = {
color: "red",
};
render() {
const { color } = this.state;
return (
<BatteryContext.Provider value={color}>
<Child></Child>
</BatteryContext.Provider>
);
}
}

4)非嵌套关系的组件通信

即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。

    1. 可以使用自定义事件通信(发布订阅模式),使用 pubsub-js
    1. 可以通过 redux 等进行全局状态管理
    1. 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点, 结合父子间通信方式进行通信。
    1. 也可以 new 一个 Vue 的 EventBus,进行事件监听,一边执行监听,一边执行新增 VUE 的 eventBus 就是发布订阅模式,是可以在 React 中使用的;

setState 既存在异步情况也存在同步情况

1.异步情况 在React事件当中是异步操作

2.同步情况 如果是在setTimeout事件或者自定义的dom事件中,都是同步的

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
//setTimeout事件
import React, { Component } from "react";
class Count extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}

render() {
return (
<>
<p>count:{this.state.count}</p>
<button onClick={this.btnAction}>增加</button>
</>
);
}

btnAction = () => {
//不能直接修改state,需要通过setState进行修改
//同步
setTimeout(() => {
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
});
};
}

export default Count;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//自定义dom事件
import React, { Component } from "react";
class Count extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}

render() {
return (
<>
<p>count:{this.state.count}</p>
<button id="btn">绑定点击事件</button>
</>
);
}

componentDidMount() {
//自定义dom事件,也是同步修改
document.querySelector("#btn").addEventListener("click", () => {
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
});
}
}

export default Count;

生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
安装
当组件的实例被创建并插入到 DOM 中时,这些方法按以下顺序调用:

constructor()
static getDerivedStateFromProps()
render()
componentDidMount()

更新中
更新可能由道具或状态的更改引起。当重新渲染组件时,这些方法按以下顺序调用:

static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()

卸载
当组件从 DOM 中移除时调用此方法:

componentWillUnmount()

说一下 react-fiber

1)背景

react-fiber 产生的根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

2)实现原理

  • react 内部运转分三层:
    • Virtual DOM 层,描述页面长什么样。
    • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
    • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示

1
2
3
4
5
6
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
  • 为了实现不卡顿,就需要有一个调度器 (Scheduler) 来进行任务分配。优先级高的任务(如键盘输入)可以打断优先级低的任务(如 Diff)的执行,从而更快的生效。任务的优先级有六种:
    • synchronous,与之前的 Stack Reconciler 操作一样,同步执行
    • task,在 next tick 之前执行
    • animation,下一帧之前执行
    • high,在不久的将来立即执行
    • low,稍微延迟执行也没关系
    • offscreen,下一次 render 时或 scroll 时才执行
  • Fiber Reconciler(react )执行过程分为 2 个阶段:
    • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
    • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
  • Fiber 树:React 在 render 第一次渲染时,会通过 React.createElement 创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。Fiber Tree 一个重要的特点是链表结构,将递归遍历编程循环遍历,然后配合 requestIdleCallback API, 实现任务拆分、中断与恢复。

从 Stack Reconciler 到 Fiber Reconciler,源码层面其实就是干了一件递归改循环的事情

传送门 ☞# 深入了解 Fiber

Portals

Portals 提供了一种一流的方式来将子组件渲染到存在于父组件的 DOM 层次结构之外的 DOM 节点中。结构不受外界的控制的情况下就可以使用 portals 进行创建
例如 模态框, 弹出的 loading

何时要使用异步组件?如和使用异步组件

  • 加载大组件的时候
  • 路由异步加载的时候

react 中要配合 Suspense 使用

1
2
3
4
// 异步懒加载
const Box = lazy(() => import("./components/Box"));
// 使用组件的时候要用suspense进行包裹
<Suspense fallback={<div>loading...</div>}>{show && <Box />}</Suspense>;

React 事件绑定原理

React 并不是将 click 事件绑在该 div 的真实 DOM 上,而是在document处监听所有支持的事件,当事件发生并冒泡至 document 处时,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault

React.lazy() 实现的原理

React 的懒加载示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Suspense } from "react";

const OtherComponent = React.lazy(() => import("./OtherComponent"));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

React.lazy 原理

以下 React 源码基于 16.8.0 版本

React.lazy 的源码实现如下:

1
2
3
4
5
6
7
8
9
10
11
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
let lazyType = {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null,
};

return lazyType;
}

可以看到其返回了一个 LazyComponent 对象。

而对于 LazyComponent 对象的解析:

1
2
3
4
5
6
7
8
9
10
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
1
2
3
4
5
6
7
8
9
10
11
function mountLazyComponent(
_current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
) {
...
let Component = readLazyComponentType(elementType);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Pending = 0, Resolved = 1, Rejected = 2
export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
const status = lazyComponent._status;
const result = lazyComponent._result;
switch (status) {
case Resolved: {
const Component: T = result;
return Component;
}
case Rejected: {
const error: mixed = result;
throw error;
}
case Pending: {
const thenable: Thenable<T, mixed> = result;
throw thenable;
}
default: {
// lazyComponent 首次被渲染
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
thenable.then(
(moduleObject) => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
},
(error) => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
}
);
// Handle synchronous thenables.
switch (lazyComponent._status) {
case Resolved:
return lazyComponent._result;
case Rejected:
throw lazyComponent._result;
}
lazyComponent._result = thenable;
throw thenable;
}
}
}

注:如果 readLazyComponentType 函数多次处理同一个 lazyComponent,则可能进入 Pending、Rejected 等 case 中。

从上述代码中可以看出,对于最初 React.lazy() 所返回的 LazyComponent 对象,其 _status 默认是 -1,所以首次渲染时,会进入 readLazyComponentType 函数中的 default 的逻辑,这里才会真正异步执行 import(url)操作,由于并未等待,随后会检查模块是否 Resolved,如果已经 Resolved 了(已经加载完毕)则直接返回 moduleObject.default(动态加载的模块的默认导出),否则将通过 throw 将 thenable 抛出到上层。

为什么要 throw 它?这就要涉及到 Suspense 的工作原理,我们接着往下分析。

Suspense 原理

由于 React 捕获异常并处理的代码逻辑比较多,这里就不贴源码,感兴趣可以去看 throwException 中的逻辑,其中就包含了如何处理捕获的异常。简单描述一下处理过程,React 捕获到异常之后,会判断异常是不是一个 thenable,如果是则会找到 SuspenseComponent ,如果 thenable 处于 pending 状态,则会将其 children 都渲染成 fallback 的值,一旦 thenable 被 resolve 则 SuspenseComponent 的子组件会重新渲染一次。

为了便于理解,我们也可以用 componentDidCatch 实现一个自己的 Suspense 组件,如下:

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
class Suspense extends React.Component {
state = {
promise: null,
};

componentDidCatch(err) {
// 判断 err 是否是 thenable
if (
err !== null &&
typeof err === "object" &&
typeof err.then === "function"
) {
this.setState({ promise: err }, () => {
err.then(() => {
this.setState({
promise: null,
});
});
});
}
}

render() {
const { fallback, children } = this.props;
const { promise } = this.state;
return <>{promise ? fallback : children}</>;
}
}

至此,我们分析完了 React 的懒加载原理。简单来说,React 利用 React.lazy 与 import()实现了渲染时的动态加载 ,并利用 Suspense 来处理异步加载资源时页面应该如何显示的问题。

参考传送门 ☞ React Lazy 的实现原理

1、什么是 TypeScript?

TypeScript 是 JavaScript 的加强版,它给 JavaScript 添加了可选的静态类型和基于类的面向对象编程,它拓展了 JavaScript 的语法。

而且 TypeScript 不存在跟浏览器不兼容的问题,因为在编译时,它产生的都是 JavaScript 代码。

2、TypeScript 和 JavaScript 的区别是什么?

Typescript 是 JavaScript 的超集,可以被编译成 JavaScript 代码。 用 JavaScript 编写的合法代码,在 TypeScript 中依然有效。Typescript 是纯面向对象的编程语言,包含类和接口的概念。 程序员可以用它来编写面向对象的服务端或客户端程序,并将它们编译成 JavaScript 代码。

TypeScript 和 JavaScript 的关系

TypeScript 引入了很多面向对象程序设计的特征,包括:

interfaces   接口
classes   类
enumerated types 枚举类型
generics 泛型
modules 模块

主要不同点如下:

TS 是一种面向对象编程语言,而 JS 是一种脚本语言(尽管 JS 是基于对象的)。
TS 支持可选参数, JS 则不支持该特性。
TS 支持静态类型,JS 不支持。
TS 支持接口,JS 不支持接口。

3. 为什么要用 TypeScript ?

TS 在开发时就能给出编译错误, 而 JS 错误则需要在运行时才能暴露。
作为强类型语言,你可以明确知道数据的类型。代码可读性极强,几乎每个人都能理解。
TS 非常流行,被很多业界大佬使用。像 Asana、Circle CI 和 Slack 这些公司都在用 TS。

4、TypeScript 和 JavaScript 哪个更好?

由于 TS 的先天优势,TS 越来越受欢迎。但是 TS 最终不可能取代 JS,因为 JS 是 TS 的核心。

选择 TypeScript 还是 JavaScript 要由开发者自己去做决定。如果你喜欢类型安全的语言,那么推荐你选择 TS。 如果你已经用 JS 好久了,你可以选择走出舒适区学习 TS,也可以选择坚持自己的强项,继续使用 JS。

5、什么是泛型?

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,使用时再去指定类型的一种特性。
可以把泛型理解为代表类型的参数
// 我们希望传入的值是什么类型,返回的值就是什么类型
// 传入的值可以是任意的类型,这时候就可以用到 泛型

// 如果使用 any 的话,就失去了类型检查的意义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
function createArray1(length: any, value: any): Array<any> {
let result: any = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

let result = createArray1(3, "x");
console.log(result);

// 最傻的写法:每种类型都得定义一种函数
function createArray2(length: number, value: string): Array<string> {
let result: Array<string> = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}

function createArray3(length: number, value: number): Array<number> {
let result: Array<number> = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
// 或者使用函数重载,写法有点麻烦
function createArray4(length: number, value: number): Array<number>;
function createArray4(length: number, value: string): Array<string>;
function createArray4(length: number, value: any): Array<any> {
let result: Array<number> = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray4(6, "666");
//使用泛型
// 有关联的地方都改成 <T>
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
// 使用的时候再指定类型
let result = createArray<string>(3, "x");
// 也可以不指定类型,TS 会自动类型推导
let result2 = createArray(3, "x");
console.log(result);

6、TS 中的类

TypeScript 是面向对象的 JavaScript。而其中的类描述了所创建的对象共同的属性和方法。

传统的 JavaScript 程序使用函数和基于原型的继承来创建可重用的组件,但这对于熟悉使用面向对象方式的程序员来说有些棘手,因为他们用的是基于类的继承并且对象是从类构建出来的。

从 ECMAScript 2015,也就是 ECMAScript 6,JavaScript 程序将可以使用这种基于类的面向对象方法。在 TypeScript 里允许开发者现在就使用这些特性,并且编译后的 JavaScript 可以在所有主流浏览器和平台上运行,

7、什么是构造函数,构造函数作用是什么?

构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与 new 运算符一起使用在创建对象的语句中。而 TypeScript 的构造函数用关键字 constructor 来实现。可以通过 this(和 java/C#一样代表对象实例的成员访问)关键字来访问当前类体中的属性和方法。

8、实例化是什么?

一般情况下,创建一个类后并不能直接的对属性和方法进行引用,必须对类进行实例化,即创建一个对象。TypeScript 中用 new 关键字创建对象。实例化后通过“.”来访问属性和方法

9、方法重写是什么?

子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写

重写的作用在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。

10、什么是可索引类型接口?

一般用来约束数组和对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 数字索引——约束数组
// index 是随便取的名字,可以任意取名
// 只要 index 的类型是 number,那么值的类型必须是 string
interface StringArray {
// key 的类型为 number ,一般都代表是数组
// 限制 value 的类型为 string
[index: number]: string;
}
let arr: StringArray = ["aaa", "bbb"];
console.log(arr);

// 字符串索引——约束对象
// 只要 index 的类型是 string,那么值的类型必须是 string
interface StringObject {
// key 的类型为 string ,一般都代表是对象
// 限制 value 的类型为 string
[index: string]: string;
}
let obj: StringObject = { name: "ccc" };

11、什么是函数类型接口?

对方法传入的参数和返回值进行约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 注意区别

// 普通的接口
interface discount1 {
getNum: (price: number) => number;
}

// 函数类型接口
interface discount2 {
// 注意:
// “:” 前面的是函数的签名,用来约束函数的参数
// ":" 后面的用来约束函数的返回值
(price: number): number;
}
let cost: discount2 = function (price: number): number {
return price * 0.8;
};

// 也可以使用类型别名
type Add = (x: number, y: number) => number;
let add: Add = (a: number, b: number) => a + b;

12、什么是类类型接口?

如果接口用于一个类的话,那么接口会表示“行为的抽象”
对类的约束,让类去实现接口,类可以实现多个接口
接口只能约束类的公有成员(实例属性/方法),无法约束私有成员、构造函数、静态属性/方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接口可以在面向对象编程中表示为行为的抽象
interface Speakable {
name: string;

// ":" 前面的是函数签名,用来约束函数的参数
// ":" 后面的用来约束函数的返回值
speak(words: string): void;
}

interface Speakable2 {
age: number;
}

class Dog implements Speakable, Speakable2 {
name!: string;
age = 18;

speak(words: string) {
console.log(words);
}
}

let dog = new Dog();
dog.speak("汪汪汪");

13、什么是混合类型接口?

一个对象可以同时做为函数和对象使用

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
interface FnType {
(getName: string): string;
}

interface MixedType extends FnType {
name: string;
age: number;
}
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

function getCounter(): Counter {
let counter = <Counter>function (start: number) {};
counter.interval = 123;
counter.reset = function () {};
return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

14、never 和 void 的区别?

void 表示没有任何类型(可以被赋值为 null 和 undefined)。
never 表示一个不包含值的类型,即表示永远不存在的值。
拥有 void 返回值类型的函数能正常运行。拥有 never 返回值类型的函数无法正常返回,无法终止,或会抛出异常。

15、TS 的学前基础?

因为 TypeScript 是对 JavaScript 的扩展,更准确的说是 ECMAScript。所以,我们学习我们这套 TypeScript 的课程,需要具备 ECMAScript 语言的基础:

熟悉语法基础(变量、语句、函数等基础概念)
掌握内置对象(Array、Date 等)的使用
面向对象基本概念(构造函数、原型、继承)

JS 中的 8 种数据类型及区别

包括值类型(基本对象类型)和引用类型(复杂对象类型)
基本类型(值类型): Number(数字),String(字符串),Boolean(布尔),Symbol(符号),null(空),undefined(未定义)在内存中占据固定大小,保存在栈内存中
引用类型(复杂数据类型): Object(对象)、Function(函数)。其他还有 Array(数组)、Date(日期)、RegExp(正则表达式)、特殊的基本包装类型(String、Number、Boolean) 以及单体内置对象(Global、Math)等 引用类型的值是对象 保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。
使用场景:
Symbol:使用 Symbol 来作为对象属性名(key)   利用该特性,把一些不需要对外操作和访问的属性使用 Symbol 来定义
BigInt:由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于 253 时使用 BigInt 类型,并且不在两种类型之间进行相互转换。
传送门 ☞# JavaScript 数据类型之 Symbol、BigInt

JS 中的数据类型检测方案

1.typeof

1
2
3
4
5
6
7
8
9
10
console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof "mc"); // string
console.log(typeof Symbol); // function
console.log(typeof function () {}); // function
console.log(typeof console.log()); // function
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
console.log(typeof undefined); // undefined

优点:能够快速区分基本数据类型

缺点:不能将 Object、Array 和 Null 区分,都返回 object

2.instanceof

1
2
3
4
5
6
console.log(1 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log("str" instanceof String); // false
console.log([] instanceof Array); // true
console.log(function () {} instanceof Function); // true
console.log({} instanceof Object); // true

优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象

缺点:Number,Boolean,String 基本数据类型不能判断

3.Object.prototype.toString.call()

1
2
3
4
5
6
7
8
9
var toString = Object.prototype.toString;
console.log(toString.call(1)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call("mc")); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call({})); //[object Object]
console.log(toString.call(function () {})); //[object Function]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]

优点:精准判断数据类型

缺点:写法繁琐不容易记,推荐进行封装后使用

instanceof 的作用

用于判断一个引用类型是否属于某构造函数;

还可以在继承关系中用来判断一个实例是否属于它的父类型。

instanceof 和 typeof 的区别:

typeof 在对值类型 number、string、boolean 、null 、 undefined、 以及引用类型的 function 的反应是精准的;但是,对于对象{ } 、数组[ ] 、null 都会返回 object

为了弥补这一点,instanceof 从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。

var && let && const

ES6 之前创建变量用的是 var,之后创建变量用的是 let/const

三者区别

  1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
    let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
    const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改。
  2. var 可以先使用,后声明,因为存在变量提升;let 必须先声明后使用。
  3. var 是允许在相同作用域内重复声明同一个变量的,而 let 与 const 不允许这一现象。
  4. 在全局上下文中,基于 let 声明的全局变量和全局对象 GO(window)没有任何关系 ;
    var 声明的变量会和 GO 有映射关系;
  5. 会产生暂时性死区

暂时性死区是浏览器的 bug:检测一个未被声明的变量类型时,不会报错,会返回 undefined
如:console.log(typeof a) //undefined
而:console.log(typeof a)//未声明之前不能使用
let a

  1. let /const/function 会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于 let 的块作用特征即可解决

作用域和作用域链

创建函数的时候,已经声明了当前函数的作用域==>当前创建函数所处的上下文。如果是在全局下创建的函数就是[[scope]]:EC(G),函数执行的时候,形成一个全新的私有上下文EC(FN),供字符串代码执行(进栈执行)

定义:简单来说作用域就是变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成 1.全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。 2.函数作用域:在固定的代码片段才能被访问

作用:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

作用域链参考链接一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

闭包的两大作用:保存/保护

  • 闭包的概念
    函数执行时形成的私有上下文 EC(FN),正常情况下,代码执行完会出栈后释放;但是特殊情况下,如果当前私有上下文中的某个东西被上下文以外的事物占用了,则上下文不会出栈释放,从而形成不销毁的上下文。 函数执行函数执行过程中,会形成一个全新的私有上下文,可能会被释放,可能不会被释放,不论释放与否,他的作用是:

(1)保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);

(2)保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;

我们把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包

闭包是指有权访问另一个函数作用域中的变量的函数–《JavaScript 高级程序设计》

稍全面的回答: 在 js 中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包函数是建立在函数内部的子函数, 由于其可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁, 这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。

  • 闭包的特性
    • 1、内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。
      1.1.闭包是密闭的容器,,类似于 set、map 容器,存储数据的
      1.2.闭包是一个对象,存放数据的格式为 key-value 形式
    • 2、函数嵌套函数
    • 3、本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除
  • 闭包形成的条件
    1. 函数的嵌套
    2. 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期
  • 闭包的用途
    1. 模仿块级作用域
    2. 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
    3. 封装私有化变量
    4. 创建模块
  • 闭包应用场景
    闭包的两个场景,闭包的两大作用:保存/保护。 在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送 ajax 请求成功|失败的回调;setTimeout 的延时回调;或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。
  • 闭包的优点:延长局部变量的生命周期
  • 闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

JS 中 this 的情况

  1. 普通函数调用:通过函数名()直接调用:this指向全局对象window(注意 let 定义的变量不是 window 属性,只有 window.xxx 定义的才是。即 let a =’aaa’; this.a 是 undefined)
  2. 构造函数调用:函数作为构造函数,用 new 关键字调用时:this指向新new出的对象
  3. 对象函数调用:通过对象.函数名()调用的:this指向这个对象
  4. 箭头函数调用:箭头函数里面没有 this ,所以永远是上层作用域this(上下文)
  5. apply 和 call 调用:函数体内 this 的指向的是 call/apply 方法第一个参数,若为空默认是指向全局对象 window。
  6. 函数作为数组的一个元素,通过数组下标调用的:this 指向这个数组
  7. 函数作为 window 内置函数的回调函数调用:this 指向 window(如 setInterval setTimeout 等)

call/apply/bind 的区别

相同:

1、都是用来改变函数的 this 对象的指向的。
2、第一个参数都是 this 要指向的对象。
3、都可以利用后续参数传参。

不同:

apply 和 call 传入的参数列表形式不同。apply 接收 arguments,call 接收一串参数列表

1
2
fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);

bind:语法和 call 一模一样,区别在于立即执行还是等待执行,bind 不兼容 IE6~8
bind 主要就是将函数绑定到某个对象,bind()会创建一个函数,返回对应函数便于稍后调用;而 apply、call 则是立即调用。

总结:基于 Function.prototype 上的 apply 、 call 和 bind调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply接收参数的是数组,call接受参数列表,`` bind方法通过传入一个对象,返回一个this绑定了传入对象的新函数。这个函数的this指向除了使用new `时会被改变,其他情况下都不会改变。若为空默认是指向全局对象 window。

参考:☞ call、apply、bind 三者的用法和区别

箭头函数的特性

  1. 箭头函数没有自己的this,会捕获其所在的上下文的 this 值,作为自己的 this 值
  2. 箭头函数没有constructor,是匿名函数,不能作为构造函数,不能通过 new 调用;
  3. 没有new.target 属性。在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是 undefined
  4. 箭头函数不绑定Arguments 对象。取而代之用 rest 参数…解决。由于 箭头函数没有自己的 this 指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定 this),他们的第一个参数会被忽略。(这种现象对于 bind 方法同样成立)
  5. 箭头函数通过 call()   或   apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响。
  6. 箭头函数没有原型属性 Fn.prototype 值为 undefined
  7. 箭头函数不能当做 Generator 函数,不能使用 yield 关键字

参考:箭头函数与普通函数的区别

原型 && 原型链

原型关系:

  • 每个 class 都有显示原型 prototype
  • 每个实例都有隐式原型 __proto__
  • 实例的 __proto__ 指向对应 class 的 prototype

原型:   在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype  属性,这个属性指向函数的原型对象

原型链:函数的原型链对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针proto,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用proto一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.** proto** = null 表示原型链顶端。如此形成了 js 的原型链继承。同时所有的 js 对象都有 Object 的基本防范

特点:  JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。

new 运算符的实现机制

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的prototype对象。
  3. 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

EventLoop 事件循环

JS是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,Promise.thenMutationObserver,宏任务的话就是setImmediate setTimeout setInterval

JS 运行的环境。一般为浏览器或者 Node。 在浏览器环境中,有 JS 引擎线程和渲染线程,且两个线程互斥。 Node 环境中,只有 JS 线程。 不同环境执行机制有差异,不同任务进入不同 Event Queue 队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。

浏览器中的事件环(Event Loop)

事件环的运行机制是,先会执行栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,然后在取宏任务清微任务这样不停的循环。

  • eventLoop 是由 JS 的宿主环境(浏览器)来实现的;
  • 事件循环可以简单的描述为以下四个步骤:
    1. 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs,接着执行同步任务,直到 Stack 为空;
    2. 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
    3. 执行栈为空时,Event Loop 把微任务队列执行清空;
    4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复 4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务。

  • 浏览器中的任务源(task):
    • 宏任务(macrotask)
      宿主环境提供的,比如浏览器
      ajax、setTimeout、setInterval、setTmmediate(只兼容 ie)、script、requestAnimationFrame、messageChannel、UI 渲染、一些浏览器 api
    • 微任务(microtask)
      语言本身提供的,比如 promise.then
      then、queueMicrotask(基于 then)、mutationObserver(浏览器提供)、messageChannel 、mutationObersve

传送门 ☞ # 宏任务和微任务

Node 环境中的事件环(Event Loop)

Node是基于 V8 引擎的运行在服务端的JavaScript运行环境,在处理高并发、I/O 密集(文件操作、网络操作、数据库操作等)场景有明显的优势。虽然用到也是 V8 引擎,但由于服务目的和环境不同,导致了它的 API 与原生 JS 有些区别,其 Event Loop 还要处理一些 I/O,比如新的网络连接等,所以 Node 的 Event Loop(事件环机制)与浏览器的是不太一样。

执行顺序如下:

  • timers: 计时器,执行 setTimeout 和 setInterval 的回调
  • pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  • idle, prepare: 队列的移动,仅系统内部使用
  • poll轮询: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  • check: 执行setImmediate回调,setImmediate 在这里执行
  • close callbacks: 执行close事件的callback,一些关闭的回调函数,如:socket.on(‘close’, …)

setTimeout、Promise、Async/Await 的区别

  1. setTimeout
    settimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行。
  2. Promise
    Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
console.log("script start");
let promise1 = new Promise(function (resolve) {
console.log("promise1");
resolve();
console.log("promise1 end");
}).then(function () {
console.log("promise2");
});
setTimeout(function () {
console.log("settimeout");
});
console.log("script end");
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
  1. async/await
    async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}

console.log("script start");
async1();
console.log("script end");

// 输出顺序:script start->async1 start->async2->script end->async1 end

传送门 ☞ # JavaScript Promise 专题

Async/Await 如何通过同步的方式实现异步

Async/Await 就是一个自执行的 generate 函数。利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象.

介绍节流防抖原理、区别以及应用

节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。

防抖:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行!

使用场景
节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。

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
/**
* 节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
* @param fn要被节流的函数
* @param delay规定的时间
*/
function throttle(fn, delay) {
//记录上一次函数触发的时间
var lastTime = 0;
return function () {
//记录当前函数触发的时间
var nowTime = Date.now();
if (nowTime - lastTime > delay) {
//修正this指向问题
fn.call(this);
//同步执行结束时间
lastTime = nowTime;
}
};
}

document.onscroll = throttle(function () {
console.log("scllor事件被触发了" + Date.now());
}, 200);

/**
* 防抖函数 一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
* @param fn要被节流的函数
* @param delay规定的时间
*/
function debounce(fn, delay) {
//记录上一次的延时器
var timer = null;
return function () {
//清除上一次的演示器
clearTimeout(timer);
//重新设置新的延时器
timer = setTimeout(function () {
//修正this指向问题
fn.apply(this);
}, delay);
};
}
document.getElementById("btn").onclick = debounce(function () {
console.log("按钮被点击了" + Date.now());
}, 1000);

axios

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import axios from 'axios'

export const request = createAxiosInstance()

function createAxiosInstance () {
const instance = axios.create({
baseURL: process.env.REACT_APP_BASE_URL,
timeout: 5000,
headers: {
// 可定义统一的请求头部
post: {
'Content-Type': 'application/json'
}
...
}
})
// 添加请求拦截器(在发送请求之前做些什么)
instance.interceptors.request.use((config) => {
//可添加开启loading效果的函数
loading.open()
//token 存在就添加到请求头里
token && (config.headers.Authorization = token)
// 过滤请求参数中的 null undefined ''的函数
cleanObject()
return config
})
// 添加响应拦截器(对响应数据做点什么)
instance.interceptors.response.use((response) => {
//可添加关闭loading效果的函数
loading.close()
//解构出返回结果的数据
const res = response.data
//对自定义code码进行判断,将成功的数据返回出去
const validateStatus = /^(2|3)\d{2}$/ //code为2或3开头的视作请求成功
if (validateStatus.test(res.code)) {
return res.data
}
//判断失败的code码并作出提示等操作
if (res.code === 401) {
message.error(res.msg)
} else {
message.warning(res.msg)
}
return Promise.reject(res)
},
(error) => {
loading.close() //可添加关闭loading效果的函数
if (error.response.status === 401) {
message.error('token失效,请重新登录!')
removeStorageToken()
setTimeout(() => {
window.location.href = '/login'
}, 2000)
} else {
if (!window.navigator.onLine) {
message.warning('网络异常,请检查网络是否正常连接')
} else if (error.code === 'ECONNABORTED') {
message.warning('请求超时')
} else {
message.warning('服务器异常,请联系管理员')
}
}
return Promise.reject(error) // 将错误继续返回给到具体页面
}
)

return instance
}

axios 默认配置

全局 axios 默认值

1
2
3
4
axios.defaults.baseURL = "https://api.example.com";
axios.defaults.headers.common["Authorization"] = AUTH_TOKEN;
axios.defaults.headers.post["Content-Type"] =
"application/x-www-form-urlencoded";

自定义实例默认值

1
2
3
4
5
6
7
// 创建实例时配置默认值
const instance = axios.create({
baseURL: "https://api.example.com",
});

// 创建实例后修改默认值
instance.defaults.headers.common["Authorization"] = AUTH_TOKEN;

优先级

1
2
3
4
5
6
7
8
9
10
11
12
// 使用库提供的默认配置创建实例
// 此时超时配置的默认值是 `0`
const instance = axios.create();

// 重写库的超时默认值
// 现在,所有使用此实例的请求都将等待2.5秒,然后才会超时
instance.defaults.timeout = 2500;

// 重写此请求的超时时间,因为该请求需要很长时间
instance.get("/longRequest", {
timeout: 5000,
});

请求配置

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
// `params` 是与请求一起发送的 URL 参数
// 必须是一个简单对象或 URLSearchParams 对象
params: {
ID: 12345
},

// `paramsSerializer`是可选方法,主要用于序列化`params`
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function (params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},

// `data` 是作为请求体被发送的数据
// 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
// 在没有设置 `transformRequest` 时,则必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属: FormData, File, Blob
// - Node 专属: Stream, Buffer
data: {
firstName: 'Fred'
},

// 发送请求体数据的可选语法
// 请求方式 post
// 只有 value 会被发送,key 则不会
data: 'Country=Brasil&City=Belo Horizonte',

Authorization

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
// 如果写在请求实例中
const accessToken = localStorage.getItem("accessToken");

this.instance = axios.create(config);
// 就加上common
this.instance.defaults.headers.common["Authorization"] = accessToken
? `Bearer ${accessToken}`
: null;

// 写在请求拦截器中不用加common

//* 请求拦截器
this.instance.interceptors.request.use(
(config) => {
//* token
config.headers["Authorization"] = accessToken
? `Bearer ${accessToken}`
: null;

return config;
},
(error) => {
Promise.reject(error);
}
);

竞态问题

无法保证异步操作会按照开始时的顺序执行。比如快速切换分页,但是网络刷新较慢,导致第 2 页请求到了,第 1 页才到。

如何解决

  1. 取消请求
  2. 忽略过期请求

从输入 URL 到页面加载的全过程

iShot2022-12-09 17.29.44.png

流程

  1. 首先在浏览器中输入 URL
  2. 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。
  • 浏览器缓存:浏览器会记录 DNS 一段时间,因此,只是第一个地方解析 DNS 请求;
  • 操作系统缓存:如果在浏览器缓存中不包含这个记录,则会使系统调用操作系统, 获取操作系统的记录(保存最近的 DNS 查询缓存);
  • 路由器缓存:如果上述两个步骤均不能成功获取 DNS 记录,继续搜索路由器缓存;
  • ISP 缓存:若上述均失败,继续向 ISP 搜索。
  1. DNS 域名解析:浏览器向 DNS 服务器发起请求,解析该 URL 中的域名对应的 IP 地址。DNS服务器是基于UDP的,因此会用到UDP协议
  2. 建立 TCP 连接:解析出 IP 地址后,根据 IP 地址和默认 80 端口,和服务器建立 TCP 连接
  3. 发起 HTTP 请求:浏览器发起读取文件的 HTTP 请求,,该请求报文作为 TCP 三次握手的第三次数据发送给服务器
  4. 服务器响应请求并返回结果:服务器对浏览器请求做出响应,并把对应的 html 文件发送给浏览器
  5. 关闭 TCP 连接:通过四次挥手释放 TCP 连接
  6. 浏览器渲染:客户端(浏览器)解析 HTML 内容并渲染出来,浏览器接收到数据包后的解析流程为:
  • 构建 DOM 树:词法分析然后解析成 DOM 树(dom tree),是由 dom 元素及属性节点组成,树的根是 document 对象
  • 构建 CSS 规则树:生成 CSS 规则树(CSS Rule Tree)
  • 构建 render 树:Web 浏览器将 DOM 和 CSSOM 结合,并构建出渲染树(render tree)
  • 布局(Layout):计算出每个节点在屏幕中的位置
  • 绘制(Painting):即遍历 render 树,并使用 UI 后端层绘制每个节点。

iShot2022-12-09 17.31.04.png
浏览器渲染流程图

  1. JS 引擎解析过程:调用 JS 引擎执行 JS 代码(JS 的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)
  • 创建 window 对象:window 对象也叫全局执行环境,当页面产生时就被创建,所有的全局变量和函数都属于 window 的属性和方法,而 DOM Tree 也会映射在 window 的 doucment 对象上。当关闭网页或者关闭浏览器时,全局执行环境会被销毁。
  • 加载文件:完成 js 引擎分析它的语法与词法是否合法,如果合法进入预编译
  • 预编译:在预编译的过程中,浏览器会寻找全局变量声明,把它作为 window 的属性加入到 window 对象中,并给变量赋值为’undefined’;寻找全局函数声明,把它作为 window 的方法加入到 window 对象中,并将函数体赋值给他(匿名函数是不参与预编译的,因为它是变量)。而变量提升作为不合理的地方在 ES6 中已经解决了,函数提升还存在。
  • 解释执行:执行到变量就赋值,如果变量没有被定义,也就没有被预编译直接赋值,在 ES5 非严格模式下这个变量会成为 window 的一个属性,也就是成为全局变量。string、int 这样的值就是直接把值放在变量的存储空间里,object 对象就是把指针指向变量的存储空间。函数执行,就将函数的环境推入一个环境的栈中,执行完成后再弹出,控制权交还给之前的环境。JS 作用域其实就是这样的执行流机制实现的。

传送门 ☞ # DNS 域名解析过程# 浏览器的工作原理

在浏览器中输入 URL 到显示页面经历哪些过程,涉及到哪些协议?

浏览器要将 URL 解析为 IP 地址,解析域名就要用到 DNS 协议,首先主机会查询 DNS 的缓存,如果没有就给本地 DNS 发送查询请求。DNS 查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的 DNS 服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的 IP 地址。DNS服务器是基于UDP的,因此会用到UDP协议。

得到 IP 地址后,浏览器就要与服务器建立一个 http 连接。因此要用到 http 协议。http 生成一个 get 请求报文,将该报文传给 TCP 层处理,所以还会用到 TCP 协议。如果采用 https 还会使用 https 协议先对 http 数据进行加密。TCP 层如果有需要先将 HTTP 数据包分片,分片依据路径 MTU 和 MSS。TCP 的数据包然后会发送给 IP 层,用到 IP 协议。IP 层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如 PPP,SLIP),以太网协议需要直到目的 IP 地址的物理地址,有需要 ARP 协议。

其中:

1、DNS协议,http协议,https协议属于应用层

应用层是体系结构中的最高层。应用层确定进程之间通信的性质以满足用户的需要。这里的进程就是指正在运行的程序。应用层不仅要提供应用进程所需要的信息交换和远地操作,而且还要作为互相作用的应用进程的用户代理,来完成一些为进行语义上有意义的信息交换所必须的功能。应用层直接为用户的应用进程提供服务。

2、TCP/UDP属于传输层

传输层的任务就是负责主机中两个进程之间的通信。因特网的传输层可使用两种不同协议:即面向连接的传输控制协议 TCP,和无连接的用户数据报协议 UDP。面向连接的服务能够提供可靠的交付,但无连接服务则不保证提供可靠的交付,它只是“尽最大努力交付”。这两种服务方式都很有用,备有其优缺点。在分组交换网内的各个交换结点机都没有传输层。

3、IP协议,ARP协议属于网络层

网络层负责为分组交换网上的不同主机提供通信。在发送数据时,网络层将运输层产生的报文段或用户数据报封装成分组或包进行传送。在 TCP/IP 体系中,分组也叫作 IP 数据报,或简称为数据报。网络层的另一个任务就是要选择合适的路由,使源主机运输层所传下来的分组能够交付到目的主机。

4、数据链路层

当发送数据时,数据链路层的任务是将在网络层交下来的 IP 数据报组装成帧,在两个相邻结点间的链路上传送以帧为单位的数据。每一帧包括数据和必要的控制信息(如同步信息、地址信息、差错控制、以及流量控制信息等)。控制信息使接收端能够知道—个帧从哪个比特开始和到哪个比特结束。控制信息还使接收端能够检测到所收到的帧中有无差错。

5、物理层

物理层的任务就是透明地传送比特流。在物理层上所传数据的单位是比特。传递信息所利用的一些物理媒体,如双绞线、同轴电缆、光缆等,并不在物理层之内而是在物理层的下面。因此也有人把物理媒体当做第 0 层。

浏览器的主要功能

浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展示您选择的网络资源。这里所说的资源一般是指 HTML 文档,也可以是 PDF、图片或其他的类型。资源的位置由用户使用 URI(统一资源标示符)指定。

浏览器的工作原理

渲染引擎一开始会从网络层获取请求文档的内容,内容的大小一般限制在 8000 个块以内。

然后进行如下所示的基本流程:

2019-06-22-11-48-00

图:渲染引擎的基本流程。

渲染引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的  DOM  节点。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于创建另一个树结构:[渲染树](https://link.juejin.cn?target=https%3A%2F%2Fwww.html5rocks.com%2Fzh%2Ftutorials%2Finternals%2Fhowbrowserswork%2F%23Render_tree_construction)

渲染树包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。

渲染树构建完毕之后,进入“布局”处理阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标。下一个阶段是绘制 - 渲染引擎会遍历渲染树,由用户界面后端层将每个节点绘制出来。

需要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,渲染引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其余内容的同时,渲染引擎会将部分内容解析并显示出来。

浏览器的主要组成部分是什么?

  1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  2. 浏览器引擎 - 在用户界面和呈现引擎之间传送指令。
  3. 呈现引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  4. 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  5. 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  6. JavaScript 解释器。用于解析和执行 JavaScript 代码。
  7. 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。


图:浏览器的主要组件。

值得注意的是,和大多数浏览器不同,Chrome 浏览器的每个标签页都分别对应一个呈现引擎实例。每个标签页都是一个独立的进程。

浏览器是如何渲染 UI 的?

  1. 浏览器获取 HTML 文件,然后对文件进行解析,形成 DOM Tree
  2. 与此同时,进行 CSS 解析,生成 Style Rules
  3. 接着将 DOM Tree 与 Style Rules 合成为 Render Tree
  4. 接着进入布局(Layout)阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标
  5. 随后调用 GPU 进行绘制(Paint),遍历 Render Tree 的节点,并将元素呈现出来

image.png

DOM Tree 是如何构建的?

  1. 转码: 浏览器将接收到的二进制数据按照指定编码格式转化为 HTML 字符串
  2. 生成 Tokens: 之后开始 parser,浏览器会将 HTML 字符串解析成 Tokens
  3. 构建 Nodes: 对 Node 添加特定的属性,通过指针确定 Node 的父、子、兄弟关系和所属 treeScope
  4. 生成 DOM Tree: 通过 node 包含的指针确定的关系构建出 DOM
    Tree

iShot2022-12-09 17.33.11.png
2019-06-22-11-48-00

浏览器重绘与重排的区别?

  • 重排/回流(Reflow):当DOM的变化影响了元素的几何信息,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。表现为重新生成布局,重新排列元素。
  • 重绘(Repaint): 当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。表现为某些元素的外观被改变

单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分

重排和重绘代价是高昂的,它们会破坏用户体验,并且让 UI 展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。

『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。

如何触发重排和重绘?

任何改变用来构建渲染树的信息都会导致一次重排或重绘:

  • 添加、删除、更新 DOM 节点
  • 通过 display: none 隐藏一个 DOM 节点-触发重排和重绘
  • 通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化
  • 移动或者给页面中的 DOM 节点添加动画
  • 添加一个样式表,调整样式属性
  • 用户行为,例如调整窗口大小,改变字号,或者滚动。

如何避免重绘或者重排?

  1. 集中改变样式,不要一条一条地修改 DOM 的样式。
  2. 不要把 DOM 结点的属性值放在循环里当成循环里的变量。
  3. 为动画的 HTML 元件使用 fixedabsoultposition,那么修改他们的 CSS 是不会 reflow 的。
  4. 不使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。
  5. 尽量只修改position:absolutefixed元素,对其他元素影响不大
  6. 动画开始GPU加速,translate使用3D变化
  7. 提升为合成层
    将元素提升为合成层有以下优点:
  • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
  • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
  • 对于 transform 和 opacity 效果,不会触发 layout 和 paint

提升合成层的最好方式是使用 CSS 的 will-change 属性:

1
2
3
#target {
will-change: transform;
}

关于合成层的详解请移步无线性能优化:Composite

介绍下 304 过程

  • a. 浏览器请求资源时首先命中资源的 Expires 和 Cache-Control,Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效,可以通过 Cache-control: max-age 指定最大生命周期,状态仍然返回 200,但不会请求数据,在浏览器中能明显看到 from cache 字样。
  • b. 强缓存失效,进入协商缓存阶段,首先验证 ETagETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据客户端上送的 If-None-Match 值来判断是否命中缓存。
  • c. 协商缓存 Last-Modify/If-Modify-Since 阶段,客户端第一次请求资源时,服务服返回的 header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间。再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存。

浏览器的缓存机制 强制缓存 && 协商缓存

浏览器与服务器通信的方式为应答模式,即是:浏览器发起 HTTP 请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中 HTTP 头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下图:

image.png
图片

由上图我们可以知道:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

以上两点结论就是浏览器缓存机制的关键,他确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了。为了方便理解,这里根据是否需要向服务器重新发起 HTTP 请求将缓存过程分为两个部分,分别是强制缓存协商缓存

  • 强制缓存
    强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。当浏览器向服务器发起请求时,服务器会将缓存规则放入 HTTP 响应报文的 HTTP 头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是 ExpiresCache-Control,其中 Cache-Control 优先级比 Expires 高。
    强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:
    1. 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)。
    2. 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存。
    3. 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果
  • 协商缓存
    协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,同样,协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-SinceEtag / If-None-Match,其中 Etag / If-None-Match 的优先级比 Last-Modified / If-Modified-Since 高。协商缓存主要有以下两种情况:
    1. 协商缓存生效,返回 304
    2. 协商缓存失效,返回 200 和请求结果结果

传送门 ☞ # 彻底理解浏览器的缓存机制

Cookie、sessionStorage、localStorage 的区别

相同点

  • 存储在客户端

不同点

  • cookie 数据大小不能超过 4k;sessionStorage 和 localStorage 的存储比 cookie 大得多,可以达到 5M+
  • cookie 设置的过期时间之前一直有效;localStorage 永久存储,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage 数据在当前浏览器窗口关闭后自动删除
  • cookie 的数据会自动的传递到服务器;sessionStorage 和 localStorage 数据保存在本地

说下进程、线程和协程

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。

线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程 ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

协程,英文 Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。

进程和线程的区别与联系

【区别】:

调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;

并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;

拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。

系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

【联系】:

一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;

资源分配给进程,同一进程的所有线程共享该进程的所有资源;

处理机分给线程,即真正在处理机上运行的是线程;

线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

传送门 ☞ # 一文搞懂进程、线程、协程及 JS 协程的发展
☞ 了解更多

关于浏览器传送门 ☞# 深入了解现代 Web 浏览器

进程间的通信方式

进程通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 A 把数据从用户空间拷到内核缓冲区,进程 B 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

进程间的通信方式:管道、有名管道、信号、消息队列、共享内存、信号量、socket

匿名管道( pipe ): 管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系

高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。

有名管道 (named pipe): 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

套接字( socket ) 通信: 套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信

浏览器样式兼容

一、CSS 初始化

每个浏览器的 css 默认样式不尽相同,所以最简单有效的方式就是对其进行初始化(覆盖默认样式)

常见 :  *{ margin: 0; padding: 0;}

库:normalize.css

二、浏览器私有属性

常用的前缀有:

firefox 浏览器 :-moz-

chrome、safari :-webkit-

opera :-o- / -xv-

IE 浏览器 :-ms-(目前只有 IE 8+支持)

三、CSS hack(条件 hack、属性级 hack、选择符级 hack)

JS 垃圾回收机制

  1. 项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。
  2. 浏览器垃圾回收机制/内存回收机制:

    浏览器的Javascript具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

标记清除:在js中,最常用的垃圾回收机制是标记清除:当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。垃圾回收器会销毁那些带标记的值并回收它们所占用的内存空间。
谷歌浏览器:“查找引用”,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。
IE 浏览器:“引用计数法”,当前内存被占用一次,计数累加 1 次,移除占用就减 1,减到 0 时,浏览器就回收它。

  1. 优化手段:内存优化 ; 手动释放:取消内存的占用即可。
    (1)堆内存:fn = null 【null:空指针对象】
    (2)栈内存:把上下文中,被外部占用的堆的占用取消即可。
  2. 内存泄漏
    在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器

1. HTTP 和 HTTPS

1.http 和 https 的基本概念

http: 是一个客户端和服务器端请求和应答的标准(TCP),用于从 WWW 服务器传输超文本到本地浏览器的超文本传输协议。
https:是以安全为目标的 HTTP 通道,即 HTTP 下 加入 SSL 层进行加密。其作用是:建立一个信息安全通道,来确保数据的传输,确保网站的真实性。

2.http 和 https 的区别及优缺点?

  • http 是超文本传输协议,信息是明文传输,HTTPS 协议要比 http 协议安全,https 是具有安全性的 ssl 加密传输协议,可防止数据在传输过程中被窃取、改变,确保数据的完整性(当然这种安全性并非绝对的,对于更深入的 Web 安全问题,此处暂且不表)。
  • http 协议的默认端口为 80,https 的默认端口为 443。
  • http 的连接很简单,是无状态的。https 握手阶段比较费时,会使页面加载时间延长 50%,增加 10%~20%的耗电。
  • https 缓存不如 http 高效,会增加数据开销。
  • Https 协议需要 ca 证书,费用较高,功能越强大的证书费用越高。
  • SSL 证书需要绑定 IP,不能再同一个 IP 上绑定多个域名,IPV4 资源支持不了这种消耗。

3.https 协议的工作原理

客户端在使用 HTTPS 方式与 Web 服务器通信时有以下几个步骤:

  1. 客户端使用 https url 访问服务器,则要求 web 服务器建立 ssl 链接。
  2. web 服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),传输给客户端。
  3. 客户端和 web 服务器端开始协商 SSL 链接的安全等级,也就是加密等级。
  4. 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站。
  5. web 服务器通过自己的私钥解密出会话密钥。
  6. web 服务器通过会话密钥加密与客户端之间的通信。

传送门 ☞ # 解读 HTTP1/HTTP2/HTTP3

TCP 三次握手

  1. 第一次握手:建立连接时,客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SENT 状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
  2. 第二次握手:服务器收到 syn 包并确认客户的 SYN(ack=j+1),同时也发送一个自己的 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;
  3. 第三次握手:客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED(TCP 连接成功)状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。

TCP 四次 挥手

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为 seq=u(等于前面已经传送过来的数据的最后一个字节的序号加 1),此时,客户端进入 FIN-WAIT-1(终止等待 1)状态。 TCP 规定,FIN 报文段即使不携带数据,也要消耗一个序号。
    2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号 seq=v,此时,服务端就进入了 CLOSE-WAIT(关闭等待)状态。TCP 服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个 CLOSE-WAIT 状态持续的时间。
    3)客户端收到服务器的确认请求后,此时,客户端就进入 FIN-WAIT-2(终止等待 2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最 后的数据)。
    4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为 seq=w,此时,服务器就进入了 LAST-ACK(最后确认)状态,等待客户端的确认。
    5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是 seq=u+1,此时,客户端就进入了 TIME-WAIT(时间等待)状态。注意此时 TCP 连接还没有释放,必须经过 2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的 TCB 后,才进入 CLOSED 状态。
    6)服务器只要收到了客户端发出的确认,立即进入 CLOSED 状态。同样,撤销 TCB 后,就结束了这次的 TCP 连接。可以看到,服务器结束 TCP 连接的时间要比客户端早一些。

TCP/IP / 如何保证数据包传输的有序可靠?

对字节流分段并进行编号然后通过 ACK 回复和超时重发这两个机制来保证。
(1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;
(2)并为每个已发送的数据包启动一个超时定时器;
(3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;
(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。
(5)接收方收到数据包后,先进行 CRC 校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。

TCP 和 UDP 的区别

  1. TCP 是面向连接的,而 UDP 是面向无连接的。
  2. TCP 仅支持单播传输,UDP 提供了单播,多播,广播的功能。
  3. TCP 的三次握手保证了连接的可靠性; UDP 是无连接的、不可靠的一种数据传输协议,首先不可靠性体现在无连接上,通信都不需要建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收。
  4. UDP 的头部开销比 TCP 的更小,数据传输速率更高,实时性更好。

传送门 ☞ # 深度剖析 TCP 与 UDP 的区别

HTTP 请求跨域问题

  1. 跨域的原理

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的。
同源策略,是浏览器对 JavaScript 实施的安全限制,只要协议、域名、端口有任何一个不同,都被当作是不同的域。
跨域原理,即是通过各种方式,避开浏览器的安全限制。

  1. 解决方案最初做项目的时候,使用的是 jsonp,但存在一些问题,使用 get 请求不安全,携带数据较小,后来也用过 iframe,但只有主域相同才行,也是存在些问题,后来通过了解和学习发现使用代理和 proxy 代理配合起来使用比较方便,就引导后台按这种方式做下服务器配置,在开发中使用 proxy,在服务器上使用 nginx 代理,这样开发过程中彼此都方便,效率也高;现在 h5 新特性还有 windows.postMessage()
    • JSONP
      ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链接却可以访问跨域的 js 脚本,利用这个特性,服务端不再返回 JSON 格式的数据,而是 返回一段调用某个函数的 js 代码,在 src 中进行了调用,这样实现了跨域。步骤: 1. 去创建一个 script 标签 2. script 的 src 属性设置接口地址 3. 接口参数,必须要带一个自定义函数名,要不然后台无法返回数据 4. 通过定义函数名去接受返回的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
//动态创建 script
var script = document.createElement("script");

// 设置回调函数
function getData(data) {
console.log(data);
}

//设置 script 的 src 属性,并设置请求地址
script.src = "http://localhost:3000/?callback=getData";

// 让 script 生效
document.body.appendChild(script);
  - **JSONP 的缺点**:

JSON 只支持 get,因为 script 标签只能使用 get 请求; JSONP 需要后端配合返回指定格式的数据。 - document.domain 基础域名相同 子域名不同 - window.name 利用在一个浏览器窗口内,载入所有的域名都是共享一个 window.name

  • CORS CORS(Cross-origin resource sharing)跨域资源共享 服务器设置对 CORS 的支持原理:服务器设置Access-Control-Allow-Origin HTTP 响应头之后,浏览器将会允许跨域请求
  • proxy 代理 目前常用方式,通过服务器设置代理,nginx 中设置 location /api { proxy_pass xxx:3000 }
  • window.postMessage() 利用 h5 新特性 window.postMessage()

跨域传送门 ☞ # 跨域,不可不知的基础概念

Cookie、sessionStorage、localStorage 的区别

相同点

  • 存储在客户端

不同点

  • cookie 数据大小不能超过 4k;sessionStorage 和 localStorage 的存储比 cookie 大得多,可以达到 5M+
  • cookie 设置的过期时间之前一直有效;localStorage 永久存储,浏览器关闭后数据不丢失除非主动删除数据;sessionStorage 数据在当前浏览器窗口关闭后自动删除
  • cookie 的数据会自动的传递到服务器;sessionStorage 和 localStorage 数据保存在本地

粘包问题分析与对策

TCP 粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
粘包出现原因
简单得说,在流传输中出现,UDP 不会出现粘包,因为它有消息边界
粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。
为了避免粘包现象,可采取以下几种措施:
(1)对于发送方引起的粘包现象,用户可通过编程设置来避免,TCP 提供了强制数据立即传送的操作指令 push,TCP 软件收到该操作指令后,就立即将本段数据发送出去,而不必等待发送缓冲区满;
(2)对于接收方引起的粘包,则可通过优化程序设计、精简接收进程工作量、提高接收进程优先级等措施,使其及时接收数据,从而尽量避免出现粘包现象;
(3)由接收方控制,将一包数据按结构字段,人为控制分多次接收,然后合并,通过这种手段来避免粘包。分包多发。
以上提到的三种措施,都有其不足之处。
(1)第一种编程设置方法虽然可以避免发送方引起的粘包,但它关闭了优化算法,降低了网络发送效率,影响应用程序的性能,一般不建议使用。
(2)第二种方法只能减少出现粘包的可能性,但并不能完全避免粘包,当发送频率较高时,或由于网络突发可能使某个时间段数据包到达接收方较快,接收方还是有可能来不及接收,从而导致粘包。
(3)第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。

一种比较周全的对策是:接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。实验证明这种方法是高效可行的。

浏览器

从输入 URL 到页面加载的全过程

  1. 首先在浏览器中输入 URL
  2. 查找缓存:浏览器先查看浏览器缓存-系统缓存-路由缓存中是否有该地址页面,如果有则显示页面内容。如果没有则进行下一步。
    • 浏览器缓存:浏览器会记录 DNS 一段时间,因此,只是第一个地方解析 DNS 请求;
    • 操作系统缓存:如果在浏览器缓存中不包含这个记录,则会使系统调用操作系统, 获取操作系统的记录(保存最近的 DNS 查询缓存);
    • 路由器缓存:如果上述两个步骤均不能成功获取 DNS 记录,继续搜索路由器缓存;
    • ISP 缓存:若上述均失败,继续向 ISP 搜索。
  3. DNS 域名解析:浏览器向 DNS 服务器发起请求,解析该 URL 中的域名对应的 IP 地址。DNS 服务器是基于 UDP 的,因此会用到 UDP 协议。
  4. 建立 TCP 连接:解析出 IP 地址后,根据 IP 地址和默认 80 端口,和服务器建立 TCP 连接
  5. 发起 HTTP 请求:浏览器发起读取文件的 HTTP 请求,,该请求报文作为 TCP 三次握手的第三次数据发送给服务器
  6. 服务器响应请求并返回结果:服务器对浏览器请求做出响应,并把对应的 html 文件发送给浏览器
  7. 关闭 TCP 连接:通过四次挥手释放 TCP 连接
  8. 浏览器渲染:客户端(浏览器)解析 HTML 内容并渲染出来,浏览器接收到数据包后的解析流程为:
    • 构建 DOM 树:词法分析然后解析成 DOM 树(dom tree),是由 dom 元素及属性节点组成,树的根是 document 对象
    • 构建 CSS 规则树:生成 CSS 规则树(CSS Rule Tree)
    • 构建 render 树:Web 浏览器将 DOM 和 CSSOM 结合,并构建出渲染树(render tree)
    • 布局(Layout):计算出每个节点在屏幕中的位置
    • 绘制(Painting):即遍历 render 树,并使用 UI 后端层绘制每个节点。
  9. JS 引擎解析过程:调用 JS 引擎执行 JS 代码(JS 的解释阶段,预处理阶段,执行阶段生成执行上下文,VO,作用域链、回收机制等等)
    • 创建 window 对象:window 对象也叫全局执行环境,当页面产生时就被创建,所有的全局变量和函数都属于 window 的属性和方法,而 DOM Tree 也会映射在 window 的 doucment 对象上。当关闭网页或者关闭浏览器时,全局执行环境会被销毁。
    • 加载文件:完成 js 引擎分析它的语法与词法是否合法,如果合法进入预编译
    • 预编译:在预编译的过程中,浏览器会寻找全局变量声明,把它作为 window 的属性加入到 window 对象中,并给变量赋值为’undefined’;寻找全局函数声明,把它作为 window 的方法加入到 window 对象中,并将函数体赋值给他(匿名函数是不参与预编译的,因为它是变量)。而变量提升作为不合理的地方在 ES6 中已经解决了,函数提升还存在。
    • 解释执行:执行到变量就赋值,如果变量没有被定义,也就没有被预编译直接赋值,在 ES5 非严格模式下这个变量会成为 window 的一个属性,也就是成为全局变量。string、int 这样的值就是直接把值放在变量的存储空间里,object 对象就是把指针指向变量的存储空间。函数执行,就将函数的环境推入一个环境的栈中,执行完成后再弹出,控制权交还给之前的环境。JS 作用域其实就是这样的执行流机制实现的。

传送门 ☞ # DNS 域名解析过程# 浏览器的工作原理

浏览器重绘与重排的区别?

  • 重排/回流(Reflow):当 DOM 的变化影响了元素的几何信息,浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。表现为重新生成布局,重新排列元素。
  • 重绘(Repaint): 当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。表现为某些元素的外观被改变

单单改变元素的外观,肯定不会引起网页重新生成布局,但当浏览器完成重排之后,将会重新绘制受到此次重排影响的部分
重排和重绘代价是高昂的,它们会破坏用户体验,并且让 UI 展示非常迟缓,而相比之下重排的性能影响更大,在两者无法避免的情况下,一般我们宁可选择代价更小的重绘。
『重绘』不一定会出现『重排』,『重排』必然会出现『重绘』。

如何触发重排和重绘?

任何改变用来构建渲染树的信息都会导致一次重排或重绘:

  • 添加、删除、更新 DOM 节点
  • 通过 display: none 隐藏一个 DOM 节点-触发重排和重绘
  • 通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化
  • 移动或者给页面中的 DOM 节点添加动画
  • 添加一个样式表,调整样式属性
  • 用户行为,例如调整窗口大小,改变字号,或者滚动。

如何避免重绘或者重排?

  1. 集中改变样式,不要一条一条地修改 DOM 的样式。
  2. 不要把 DOM 结点的属性值放在循环里当成循环里的变量。
  3. 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
  4. 不使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。
  5. 尽量只修改 position:absolute 或 fixed 元素,对其他元素影响不大
  6. 动画开始 GPU 加速,translate 使用 3D 变化
  7. 提升为合成层将元素提升为合成层有以下优点:提升合成层的最好方式是使用 CSS 的 will-change 属性:#target { will-change: transform; } 复制代码关于合成层的详解请移步无线性能优化:Composite
    • 合成层的位图,会交由 GPU 合成,比 CPU 处理要快
    • 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层
    • 对于 transform 和 opacity 效果,不会触发 layout 和 paint

提升合成层的最好方式是使用 CSS 的 will-change 属性:

1
2
3
#target {
will-change: transform;
}

介绍下 304 过程

  • a. 浏览器请求资源时首先命中资源的 Expires 和 Cache-Control,Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效,可以通过 Cache-control: max-age 指定最大生命周期,状态仍然返回 200,但不会请求数据,在浏览器中能明显看到 from cache 字样。
  • b. 强缓存失效,进入协商缓存阶段,首先验证 ETag. ETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据客户端上送的 If-None-Match 值来判断是否命中缓存。
  • c. 协商缓存 Last-Modify/If-Modify-Since 阶段,客户端第一次请求资源时,服务服返回的 header 中会加上 Last-Modify,Last-modify 是一个时间标识该资源的最后修改时间。再次请求该资源时,request 的请求头中会包含 If-Modify-Since,该值为缓存之前返回的 Last-Modify。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存。

浏览器的缓存机制 强制缓存 && 协商缓存

浏览器与服务器通信的方式为应答模式,即是:浏览器发起 HTTP 请求 – 服务器响应该请求。那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中 HTTP 头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下图:

由上图我们可以知道:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

以上两点结论就是浏览器缓存机制的关键,他确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了。为了方便理解,这里根据是否需要向服务器重新发起 HTTP 请求将缓存过程分为两个部分,分别是强制缓存和协商缓存。

  • 强制缓存

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程。当浏览器向服务器发起请求时,服务器会将缓存规则放入 HTTP 响应报文的 HTTP 头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是 Expires 和 Cache-Control,其中 Cache-Control 优先级比 Expires 高。

强制缓存的情况主要有三种(暂不分析协商缓存过程),如下:

  1. 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)。
  2. 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存。
  3. 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果
  • 协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,同样,协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:Last-Modified / If-Modified-Since 和 Etag / If-None-Match,其中 Etag / If-None-Match 的优先级比 Last-Modified / If-Modified-Since 高。协商缓存主要有以下两种情况:

  1. 协商缓存生效,返回 304
  2. 协商缓存失效,返回 200 和请求结果结果

传送门 ☞ # 彻底理解浏览器的缓存机制

说下进程、线程和协程

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。
线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程 ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
协程,英文 Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。
进程和线程的区别与联系
【区别】:
调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;
并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;
拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
【联系】: 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;
资源分配给进程,同一进程的所有线程共享该进程的所有资源;
处理机分给线程,即真正在处理机上运行的是线程;
线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
传送门 ☞ # 一文搞懂进程、线程、协程及 JS 协程的发展☞ 了解更多
关于浏览器传送门 ☞# 深入了解现代 Web 浏览器

HTML && CSS

HTML5 新特性、语义化

  1. 概念:HTML5 的语义化指的是合理正确的使用语义化的标签来创建页面结构。【正确的标签做正确的事】
  2. 语义化标签:header nav main article section aside footer
  3. 语义化的优点:
    • 在没 CSS 样式的情况下,页面整体也会呈现很好的结构效果
    • 代码结构清晰,易于阅读,
    • 利于开发和维护 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
    • 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重

CSS 选择器及优先级

选择器

  • id 选择器(#myid)
  • 类选择器(.myclass)
  • 属性选择器(a[rel=”external”])
  • 伪类选择器(a:hover, li:nth-child)
  • 标签选择器(div, h1,p)
  • 相邻选择器(h1 + p)
  • 子选择器(ul > li)
  • 后代选择器(li a)
  • 通配符选择器(*)

优先级:

  • !important
  • 内联样式(1000)
  • ID 选择器(0100)
  • 类选择器/属性选择器/伪类选择器(0010)
  • 元素选择器/伪元素选择器(0001)
  • 关系选择器/通配符选择器(0000)

带!important 标记的样式属性优先级最高; 样式表的来源相同时:!important > 行内样式>ID 选择器 > 类选择器 > 标签 > 通配符 > 继承 > 浏览器默认属性

position 属性的值有哪些及其区别

固定定位 fixed: 元素的位置相对于浏览器窗口是固定位置,即使窗口是滚动的它也不会移动。Fixed 定 位使元素的位置与文档流无关,因此不占据空间。 Fixed 定位的元素和其他元素重叠。
相对定位 relative: 如果对一个元素进行相对定位,它将出现在它所在的位置上。然后,可以通过设置垂直 或水平位置,让这个元素“相对于”它的起点进行移动。 在使用相对定位时,无论是 否进行移动,元素仍然占据原来的空间。因此,移动元素会导致它覆盖其它框。
绝对定位 absolute: 绝对定位的元素的位置相对于最近的已定位父元素,如果元素没有已定位的父元素,那 么它的位置相对于。absolute 定位使元素的位置与文档流无关,因此不占据空间。 absolute 定位的元素和其他元素重叠。
粘性定位 sticky: 元素先按照普通文档流定位,然后相对于该元素在流中的 flow root(BFC)和 containing block(最近的块级祖先元素)定位。而后,元素定位表现为在跨越特定阈值前为相对定 位,之后为固定定位。
默认定位 Static: 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声 明)。 inherit: 规定应该从父元素继承 position 属性的值。

box-sizing 属性

box-sizing 规定两个并排的带边框的框,语法为 box-sizing:content-box/border-box/inherit
content-box:宽度和高度分别应用到元素的内容框,在宽度和高度之外绘制元素的内边距和边框。【标准盒子模型】
border-box:为元素设定的宽度和高度决定了元素的边框盒。【IE 盒子模型】
inherit:继承父元素的 box-sizing 值。

CSS 盒子模型

CSS 盒模型本质上是一个盒子,它包括:边距,边框,填充和实际内容。CSS 中的盒子模型包括 IE 盒子模型和标准的 W3C 盒子模型。
在标准的盒子模型中,width 指 content 部分的宽度。
在 IE 盒子模型中,width 表示 content+padding+border 这三个部分的宽度。
故在计算盒子的宽度时存在差异:
标准盒模型: 一个块的总宽度 = width+margin(左右)+padding(左右)+border(左右)
IE 盒模型: 一个块的总宽度 = width+margin(左右)(既 width 已经包含了 padding 和 border 值)

BFC(块级格式上下文)

BFC 的概念
BFC 是 Block Formatting Context 的缩写,即块级格式化上下文。BFC 是 CSS 布局的一个概念,是一个独立的渲染区域,规定了内部 box 如何布局, 并且这个区域的子元素不会影响到外面的元素,其中比较重要的布局规则有内部 box 垂直放置,计算 BFC 的高度的时候,浮动元素也参与计算。
BFC 的原理布局规则

  • 内部的 Box 会在垂直方向,一个接一个地放置
  • Box 垂直方向的距离由 margin 决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
  • 每个元素的 margin box 的左边, 与包含块 border box 的左边相接触(对于从左往右的格式化,否则相反
  • BFC 的区域不会与 float box 重叠
  • BFC 是一个独立容器,容器里面的子元素不会影响到外面的元素
  • 计算 BFC 的高度时,浮动元素也参与计算高度
  • 元素的类型和 display 属性,决定了这个 Box 的类型。不同类型的 Box 会参与不同的 Formatting Context。

如何创建 BFC?

  • 根元素,即 HTML 元素
  • float 的值不为 none
  • position 为 absolute 或 fixed
  • display 的值为 inline-block、table-cell、table-caption
  • overflow 的值不为 visible

BFC 的使用场景

  • 去除边距重叠现象
  • 清除浮动(让父元素的高度包含子浮动元素)
  • 避免某元素被浮动元素覆盖
  • 避免多列布局由于宽度计算四舍五入而自动换行

让一个元素水平垂直居中

  • 水平居中
    • 对于 行内元素 : text-align: center;
    • 对于确定宽度的块级元素:
      • (1)width 和 margin 实现。margin: 0 auto;
      • (2)绝对定位和 margin-left: (父 width - 子 width)/2, 前提是父元素 position: relative
    • 对于宽度未知的块级元素
      • (1)table 标签配合 margin 左右 auto 实现水平居中。使用 table 标签(或直接将块级元素设值为 display:table),再通过给该标签添加左右 margin 为 auto
      • (2)inline-block 实现水平居中方法。display:inline-block 和 text-align:center 实现水平居中。
      • (3)绝对定位+transform,translateX 可以移动本身元素的 50%。
      • (4)flex 布局使用 justify-content:center
  • 垂直居中
    1. 利用 line-height 实现居中,这种方法适合纯文字类
    2. 通过设置父容器 相对定位 ,子级设置 绝对定位,标签通过 margin 实现自适应居中
    3. 弹性布局 flex :父级设置 display: flex; 子级设置 margin 为 auto 实现自适应居中
    4. 父级设置相对定位,子级设置绝对定位,并且通过位移 transform 实现
    5. table 布局,父级通过转换成表格形式,然后子级设置 vertical-align 实现。(需要注意的是:vertical-align: middle 使用的前提条件是内联元素以及 display 值为 table-cell 的元素)。

传送门 ☞ # 图解 CSS 水平垂直居中常见面试方法

隐藏页面中某个元素的方法

1.opacity:0,该元素隐藏起来了,但不会改变页面布局,并且,如果该元素已经绑定 一些事件,如 click 事件,那么点击该区域,也能触发点击事件的
2.visibility:hidden,该元素隐藏起来了,但不会改变页面布局,但是不会触发该元素已 经绑定的事件 ,隐藏对应元素,在文档布局中仍保留原来的空间(重绘)
3.display:none,把元素隐藏起来,并且会改变页面布局,可以理解成在页面中把该元素。 不显示对应的元素,在文档布局中不再分配空间(回流+重绘)

该问题会引出 回流和重绘

用 CSS 实现三角符号

1
2
3
4
5
6
7
8
9
10
11
/*记忆口诀:盒子宽高均为零,三面边框皆透明。 */
div:after {
position: absolute;
width: 0px;
height: 0px;
content: " ";
border-right: 100px solid transparent;
border-top: 100px solid #ff0;
border-left: 100px solid transparent;
border-bottom: 100px solid transparent;
}

页面布局

1.Flex 布局

布局的传统解决方案,基于盒状模型,依赖 display 属性 + position 属性 + float 属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。
Flex 是 Flexible Box 的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。指定容器 display: flex 即可。 简单的分为容器属性和元素属性。
容器的属性:

  • flex-direction:决定主轴的方向(即子 item 的排列方法)flex-direction: row | row-reverse | column | column-reverse;
  • flex-wrap:决定换行规则 flex-wrap: nowrap | wrap | wrap-reverse;
  • flex-flow: .box { flex-flow: || ; }
  • justify-content:对其方式,水平主轴对齐方式
  • align-items:对齐方式,竖直轴线方向
  • align-content

项目的属性(元素的属性):

  • order 属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为 0
  • flex-grow 属性:定义项目的放大比例,即使存在空间,也不会放大
  • flex-shrink 属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小,如果 定义个 item 的 flow-shrink 为 0,则为不缩小
  • flex-basis 属性:定义了在分配多余的空间,项目占据的空间。
  • flex:是 flex-grow 和 flex-shrink、flex-basis 的简写,默认值为 0 1 auto。
  • align-self:允许单个项目与其他项目不一样的对齐方式,可以覆盖
  • align-items,默认属 性为 auto,表示继承父元素的 align-items 比如说,用 flex 实现圣杯布局

2.Rem 布局

首先 Rem 相对于根(html)的 font-size 大小来计算。简单的说它就是一个相对单例 如:font-size:10px;,那么(1rem = 10px)了解计算原理后首先解决怎么在不同设备上设置 html 的 font-size 大小。其实 rem 布局的本质是等比缩放,一般是基于宽度。
优点:可以快速适用移动端布局,字体,图片高度
缺点
① 目前 ie 不支持,对 pc 页面来讲使用次数不多;
② 数据量大:所有的图片,盒子都需要我们去给一个准确的值;才能保证不同机型的适配;
③ 在响应式布局中,必须通过 js 来动态控制根元素 font-size 的大小。也就是说 css 样式和 js 代码有一定的耦合性。且必须将改变 font-size 的代码放在 css 样式之前。

3.百分比布局

通过百分比单位 “ % “ 来实现响应式的效果。通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。 直观的理解,我们可能会认为子元素的百分比完全相对于直接父元素,height 百分比相 对于 height,width 百分比相对于 width。 padding、border、margin 等等不论是垂直方向还是水平方向,都相对于直接父元素的 width。 除了 border-radius 外,还有比如 translate、background-size 等都是相对于自身的。
缺点
(1)计算困难
(2)各个属性中如果使用百分比,相对父元素的属性并不是唯一的。造成我们使用百分比单位容易使布局问题变得复杂。

4.浮动布局

浮动布局:当元素浮动以后可以向左或向右移动,直到它的外边缘碰到包含它的框或者另外一个浮动元素的边框为止。元素浮动以后会脱离正常的文档流,所以文档的普通流中的框就变的好像浮动元素不存在一样。
优点
这样做的优点就是在图文混排的时候可以很好的使文字环绕在图片周围。另外当元素浮动了起来之后,它有着块级元素的一些性质例如可以设置宽高等,但它与 inline-block 还是有一些区别的,第一个就是关于横向排序的时候,float 可以设置方向而 inline-block 方向是固定的;还有一个就是 inline-block 在使用时有时会有空白间隙的问题
缺点
最明显的缺点就是浮动元素一旦脱离了文档流,就无法撑起父元素,会造成父级元素高度塌陷。

如何使用 rem 或 viewport 进行移动端适配

rem 适配原理:
改变了一个元素在不同设备上占据的 css 像素的个数
rem 适配的优缺点

  • 优点:没有破坏完美视口
  • 缺点:px 值转换 rem 太过于复杂(下面我们使用 less 来解决这个问题)

viewport 适配的原理
viewport 适配方案中,每一个元素在不同设备上占据的 css 像素的个数是一样的。但是 css 像素和物理像素的比例是不一样的,等比的
viewport 适配的优缺点

  • 在我们设计图上所量取的大小即为我们可以设置的像素大小,即所量即所设
  • 缺点破坏完美视口

清除浮动的方式

  • 添加额外标签
1
2
3
4
5
<div class="parent">
//添加额外标签并且添加clear属性
<div style="clear:both"></div>
//也可以加一个br标签
</div>
  • 父级添加 overflow 属性,或者设置高度
  • 建立伪类选择器清除浮动
1
2
3
4
5
6
7
8
9
10
11
12
13
// 在css中添加:after伪元素
.parent:after {
/* 设置添加子元素的内容是空 */
content: "";
/* 设置添加子元素为块级元素 */
display: block;
/* 设置添加的子元素的高度0 */
height: 0;
/* 设置添加子元素看不见 */
visibility: hidden;
/* 设置clear:both */
clear: both;
}

JS、TS、ES6

JS 中的 8 种数据类型及区别

包括值类型(基本对象类型)和引用类型(复杂对象类型)
基本类型(值类型): Number(数字),String(字符串),Boolean(布尔),Symbol(符号),null(空),undefined(未定义)在内存中占据固定大小,保存在栈内存中
引用类型(复杂数据类型): Object(对象)、Function(函数)。其他还有 Array(数组)、Date(日期)、RegExp(正则表达式)、特殊的基本包装类型(String、Number、Boolean) 以及单体内置对象(Global、Math)等 引用类型的值是对象 保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。
传送门 ☞# JavaScript 数据类型之 Symbol、BigInt

JS 中的数据类型检测方案

1.typeof

1
2
3
4
5
6
7
8
9
10
console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof "mc"); // string
console.log(typeof Symbol); // function
console.log(typeof function () {}); // function
console.log(typeof console.log()); // undefined
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
console.log(typeof undefined); // undefined

优点:能够快速区分基本数据类型
缺点:不能将 Object、Array 和 Null 区分,都返回 object

2.instanceof

1
2
3
4
5
6
console.log(1 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log("str" instanceof String); // false
console.log([] instanceof Array); // true
console.log(function () {} instanceof Function); // true
console.log({} instanceof Object); // true

优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String 基本数据类型不能判断

3.Object.prototype.toString.call()

1
2
3
4
5
6
7
8
9
var toString = Object.prototype.toString;
console.log(toString.call(1)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call("mc")); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call({})); //[object Object]
console.log(toString.call(function () {})); //[object Function]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]

优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用

var && let && const

ES6 之前创建变量用的是 var,之后创建变量用的是 let/const
三者区别

  1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问。
    let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问。
    const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改。
  2. var 可以先使用,后声明,因为存在变量提升;let 必须先声明后使用。
  3. var 是允许在相同作用域内重复声明同一个变量的,而 let 与 const 不允许这一现象。
  4. 在全局上下文中,基于 let 声明的全局变量和全局对象 GO(window)没有任何关系 ;
    var 声明的变量会和 GO 有映射关系;
  5. 会产生暂时性死区:

暂时性死区是浏览器的 bug:检测一个未被声明的变量类型时,不会报错,会返回 undefined

1
2
3
如:console.log(typeof a) //undefined
而:console.log(typeof a)//未声明之前不能使用
let a
  1. let /const/function 会把当前所在的大括号(除函数之外)作为一个全新的块级上下文,应用这个机制,在开发项目的时候,遇到循环事件绑定等类似的需求,无需再自己构建闭包来存储,只要基于 let 的块作用特征即可解决

JS 垃圾回收机制

  1. 项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。我们尽可能减少使用闭包,因为它会消耗内存。
  2. 浏览器垃圾回收机制/内存回收机制:

浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

标记清除:在 js 中,最常用的垃圾回收机制是标记清除:当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。垃圾回收器会销毁那些带标记的值并回收它们所占用的内存空间。
谷歌浏览器:“查找引用”,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它;如果被占用,就不能回收。
IE 浏览器:“引用计数法”,当前内存被占用一次,计数累加 1 次,移除占用就减 1,减到 0 时,浏览器就回收它。

  1. 优化手段:内存优化 ; 手动释放:取消内存的占用即可。(1)堆内存:fn = null 【null:空指针对象】(2)栈内存:把上下文中,被外部占用的堆的占用取消即可。
  2. 内存泄漏在 JS 中,常见的内存泄露主要有 4 种,全局变量、闭包、DOM 元素的引用、定时器

作用域和作用域链

创建函数的时候,已经声明了当前函数的作用域==>当前创建函数所处的上下文。如果是在全局下创建的函数就是[[scope]]:EC(G),函数执行的时候,形成一个全新的私有上下文 EC(FN),供字符串代码执行(进栈执行)
定义:简单来说作用域就是变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成 1.全局作用域:代码在程序的任何地方都能被访问,window 对象的内置属性都拥有全局作用域。 2.函数作用域:在固定的代码片段才能被访问
作用:作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
作用域链参考链接一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

闭包的两大作用:保存/保护

  • 闭包的概念函数执行时形成的私有上下文 EC(FN),正常情况下,代码执行完会出栈后释放;但是特殊情况下,如果当前私有上下文中的某个东西被上下文以外的事物占用了,则上下文不会出栈释放,从而形成不销毁的上下文。 函数执行函数执行过程中,会形成一个全新的私有上下文,可能会被释放,可能不会被释放,不论释放与否,他的作用是:

(1)保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);
(2)保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;

我们把函数执行形成私有上下文,来保护和保存私有变量机制称为闭包。
闭包是指有权访问另一个函数作用域中的变量的函数--《JavaScript高级程序设计》 **稍全面的回答**: 在js中变量的作用域属于函数作用域, 在函数执行完后,作用域就会被清理,内存也会随之被回收,但是由于闭包函数是建立在函数内部的子函数, 由于其可访问上级作用域,即使上级函数执行完, 作用域也不会随之销毁, 这时的子函数(也就是闭包),便拥有了访问上级作用域中变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。
  • 闭包的特性
    • 1、内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。1.1.闭包是密闭的容器,,类似于 set、map 容器,存储数据的 1.2.闭包是一个对象,存放数据的格式为 key-value 形式
    • 2、函数嵌套函数
    • 3、本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除
  • 闭包形成的条件
    1. 函数的嵌套
    2. 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期
  • 闭包的用途
    1. 模仿块级作用域
    2. 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
    3. 封装私有化变量
    4. 创建模块
  • 闭包应用场景闭包的两个场景,闭包的两大作用:保存/保护。 在开发中, 其实我们随处可见闭包的身影, 大部分前端 JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送 ajax 请求成功|失败的回调;setTimeout 的延时回调;或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。
  • 闭包的优点:延长局部变量的生命周期
  • 闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

JS 中 this 的五种情况

  1. 作为普通函数执行时,this 指向 window。
  2. 当函数作为对象的方法被调用时,this 就会指向该对象。
  3. 构造器调用,this 指向返回的这个对象。
  4. 箭头函数 箭头函数的 this 绑定看的是 this 所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则 this 绑定到最近的一层对象上。
  5. 基于 Function.prototype 上的 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply 接收参数的是数组,call 接受参数列表,``bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用new`时会被改变,其他情况下都不会改变。若为空默认是指向全局对象 window。

原型 && 原型链

原型关系:

  • 每个 class 都有显示原型 prototype
  • 每个实例都有隐式原型 _ proto_
  • 实例的* proto*指向对应 class 的 prototype

原型: 在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个 prototype 属性,这个属性指向函数的原型对象。
原型链:函数的原型链对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针proto,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用proto一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.** proto** = null 表示原型链顶端。如此形成了 js 的原型链继承。同时所有的 js 对象都有 Object 的基本防范
特点: JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。

new 运算符的实现机制

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的 prototype 对象。
  3. 让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

EventLoop 事件循环

JS 是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,Promise.then,MutationObserver,宏任务的话就是 setImmediate setTimeout setInterval
JS 运行的环境。一般为浏览器或者 Node。 在浏览器环境中,有 JS 引擎线程和渲染线程,且两个线程互斥。 Node 环境中,只有 JS 线程。 不同环境执行机制有差异,不同任务进入不同 Event Queue 队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。

浏览器中的事件环(Event Loop)

事件环的运行机制是,先会执行栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,然后在取宏任务清微任务这样不停的循环。

  • eventLoop 是由 JS 的宿主环境(浏览器)来实现的;
  • 事件循环可以简单的描述为以下四个步骤:
    1. 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs,接着执行同步任务,直到 Stack 为空;
    2. 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
    3. 执行栈为空时,Event Loop 把微任务队列执行清空;
    4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack(栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复 4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务。
  • 浏览器中的任务源(task):
    • 宏任务(macrotask):
      宿主环境提供的,比如浏览器
      ajax、setTimeout、setInterval、setTmmediate(只兼容 ie)、script、requestAnimationFrame、messageChannel、UI 渲染、一些浏览器 api
    • 微任务(microtask):
      语言本身提供的,比如 promise.then
      then、queueMicrotask(基于 then)、mutationObserver(浏览器提供)、messageChannel 、mutationObersve

传送门 ☞ # 宏任务和微任务

Node 环境中的事件环(Event Loop)

Node 是基于 V8 引擎的运行在服务端的 JavaScript 运行环境,在处理高并发、I/O 密集(文件操作、网络操作、数据库操作等)场景有明显的优势。虽然用到也是 V8 引擎,但由于服务目的和环境不同,导致了它的 API 与原生 JS 有些区别,其 Event Loop 还要处理一些 I/O,比如新的网络连接等,所以 Node 的 Event Loop(事件环机制)与浏览器的是不太一样。
执行顺序如下:

  • timers: 计时器,执行 setTimeout 和 setInterval 的回调
  • pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  • idle, prepare: 队列的移动,仅系统内部使用
  • poll 轮询: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  • check: 执行 setImmediate 回调,setImmediate 在这里执行
  • close callbacks: 执行 close 事件的 callback,一些关闭的回调函数,如:socket.on(‘close’, …)

setTimeout、Promise、Async/Await 的区别

  1. setTimeout

settimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行。

  1. Promise

Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log("script start");
let promise1 = new Promise(function (resolve) {
console.log("promise1");
resolve();
console.log("promise1 end");
}).then(function () {
console.log("promise2");
});
setTimeout(function () {
console.log("settimeout");
});
console.log("script end");
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
  1. async/await

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}

console.log("script start");
async1();
console.log("script end");

// 输出顺序:script start->async1 start->async2->script end->async1 end

传送门 ☞ # JavaScript Promise 专题

Async/Await 如何通过同步的方式实现异步

Async/Await 就是一个自执行的 generate 函数。利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象.

介绍节流防抖原理、区别以及应用

节流:事件触发后,规定时间内,事件处理函数不能再次被调用。也就是说在规定的时间内,函数只能被调用一次,且是最先被触发调用的那次。
防抖:多次触发事件,事件处理函数只能执行一次,并且是在触发操作结束时执行。也就是说,当一个事件被触发准备执行事件函数前,会等待一定的时间(这时间是码农自己去定义的,比如 1 秒),如果没有再次被触发,那么就执行,如果被触发了,那就本次作废,重新从新触发的时间开始计算,并再次等待 1 秒,直到能最终执行!
使用场景:
节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。

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
/**
* 节流函数 一个函数执行一次后,只有大于设定的执行周期才会执行第二次。有个需要频繁触发的函数,出于优化性能的角度,在规定时间内,只让函数触发的第一次生效,后面的不生效。
* @param fn要被节流的函数
* @param delay规定的时间
*/
function throttle(fn, delay) {
//记录上一次函数触发的时间
var lastTime = 0;
return function () {
//记录当前函数触发的时间
var nowTime = Date.now();
if (nowTime - lastTime > delay) {
//修正this指向问题
fn.call(this);
//同步执行结束时间
lastTime = nowTime;
}
};
}

document.onscroll = throttle(function () {
console.log("scllor事件被触发了" + Date.now());
}, 200);

/**
* 防抖函数 一个需要频繁触发的函数,在规定时间内,只让最后一次生效,前面的不生效
* @param fn要被节流的函数
* @param delay规定的时间
*/
function debounce(fn, delay) {
//记录上一次的延时器
var timer = null;
return function () {
//清除上一次的演示器
clearTimeout(timer);
//重新设置新的延时器
timer = setTimeout(() => {
//修正this指向问题
fn.apply(this);
}, delay);
};
}
document.getElementById("btn").onclick = debounce(function () {
console.log("按钮被点击了" + Date.now());
}, 1000);

Vue

简述 MVVM

什么是 MVVM?
视图模型双向绑定,是 Model-View-ViewModel 的缩写,也就是把 MVC 中的 Controller 演变成 ViewModel。Model 层代表数据模型,View 代表 UI 组件,ViewModel 是 View 和 Model 层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中,视图变化的时候会通知 viewModel 层更新数据。以前是操作 DOM 结构更新视图,现在是数据驱动视图。
MVVM 的优点: 1.低耦合。视图(View)可以独立于 Model 变化和修改,一个 Model 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变化,当 Model 变化的时候 View 也可以不变; 2.可重用性。你可以把一些视图逻辑放在一个 Model 里面,让很多 View 重用这段视图逻辑。 3.独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。 4.可测试。

Vue2 和 Vue3 的区别

双向数据绑定的差别

Vue2 是使用 Object.defineProperty,将 Vue 实例上的属性添加 getter,setter 方法,并进行依赖收集.等数据发生变化,通知 vue 重新渲染.
缺点是无法对数组进行观测.

Vue3 是使用 Proxy 和 Reflect 替代了上面的方法,这是 Es6 推出的新特性.
Proxy 可以对对象进行代理,内置有陷阱函数.

1
2
3
4
5
6
7
8
const obj = {a:1}
let proxy = new Proxy(obj, handler)

const handler = function(target, key, receiver) {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
}
}
  1. 选项式 api 和组合式 API
    setup(){}将方法和属性包裹返回,setup 语法糖也不再写 return
  2. 响应式数据
    由之前的 data()函数,改为 ref,reactive.
    ref 定义基本类型,reactive 定义复杂类型,ref 获取加上 value
  3. 生命周期的差异
    移除 beforeCreate,created,因为 setup 就是围绕这两个生命周期运行.
    其他的生命周期在写法上加上了 on,使用了 hook 函数引用.destroyed 变成了 unMounted.
1
onMounted(() => {});
  1. watch 变化
1
watch('监听值', (new, old) => {})

新增 watchEffect 副作用监听函数,传入立即执行函数,响应式追踪依赖.
watchEffect 是 watch 的依赖项和回调函数的合并,当响应式依赖改变就重新执行, 5. 组件通信

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
// props // 组合式和选项式一致.都是props:[], setup语法糖不一致 //父组件

<template>
<div>
<Child :msg="parentMsg" />
</div>
</template>
<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
const parentMsg = ref("父组件信息");
</script>

//子组件

<template>
<div>
{{ parentMsg }}
</div>
</template>
<script setup>
import { toRef, defineProps } from "vue";
const props = defineProps(["msg"]);
let parentMsg = toRef(props, "msg");
</script>

props 中的数据流是单向的,子组件不能改变父组件的值.
在子组件中用其他变量接收 props 的值,需要使用 toRef 将 props 中的属性转为响应式.

emit 传递信息
vue3 中使用 emits:[‘注册的事件’],

1
2
3
4
5
6
7
8
export default defineComponent {
emits: ['msg'],
setup(props, ctx) {
const sendMsg = () => {
ctx.emit('msg', '子组件数据')
}
}
}
1
2
3
4
5
// 语法糖
<script setup>
import {defineEmits} from 'vue' const emits = defineEmits(['msg']) const
sendMsg = () => {emits("msg", "子组件数据")}
</script>

provide/inject
在 provide 中的属性值在所有后代的 inject 中都可以获取.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 父组件
<script setup>
import { ref, provide } from 'vue';
const msg1 = ref('子组件msg1')
const msg2 = ref('子组件msg2')
provide('msg1', msg1)
</script>

// 子组件
<script setup>
import {inject} from 'vue';
console.log(inject('msg1').value)
</script>

EventBus 替换为 Mitt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import mitt from 'mitt';
const Mitt = mitt()
export default Mitt;
// 发送端
<script setup>
import Mitt from './mitt.js'
const sendMsg = () => {
Mitt.emit('sendMsg', '发送值')
}
</script>
// 接收端
<script setup>
import Mitt from './mitt.js'
const getMsg = (val) => {
console.log(val)
}
Mitt.on('sendMsg', getMsg)
onUnmounted(()=>{
Mitt.off('sendMsg',getMsg)
})
</script>

v-model

1
2
v-bind:value = 'val'
v-on:input = 'val = $event.target.value'

.sync 用法
相对于直接在 template 上监听

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<Child :changeVal.sync="msg" />
// 相当于
<Child @update:changeVal="msg=$event" />
</div>
</template>
// 子组件
changeVal(){
this.$emit('update:changeVal', '子组件')
}

vue3

1
2
3
4
5
6
7
8
9
10
11
<template>
<Child v-model:changeVal="msg">
</template>
// 子组件
<script setup>
import {defineEmits} from 'vue'
const emits = defineEmits(['msg'])
const change = () => {
emits('update:changeVal', '改变值')
}
</script>

路由

路由传参和跳转的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default {
beforeRouteEnter(to, from, next) {
next();
},
methods: {
toPage() {
// 路由跳转
this.router.push(xxx);
},
},
created() {
// 获取路由参数
this.$route.params;
this.$route.query;
},
};

setup 语法糖写法不能写 beforeRouteEnter,setup 时组件已经创建,而 beforeRouteEnter 是在进入路由前触发,也就是组件还没创建.

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
<script>
export default {
beforeRouteEnter(to,from, next){
next()
}
}
</script>
<script setup>
import { useRoute, useRouter, onBeforeRouteUpdate, onBeforeRouteLeave} from 'vue-router';
const router = useRouter()
const route = useRoute()
const toPage = () => {
router.push(xxx)
}
// 获取参数
route.params

route.query
// 路由守卫
onBeforeRouteUpdate((to,from,next){
next()
})
onBeforeRouteLeave((to,from, next){
next()
})

</script>

路由守卫

分为三种,全局,单个路由独享,组件的.

全局

相当于程序的页面生命周期.
全局守卫 3 个,beforeEach, beforeResolve, afterEach
分别表示进入路由前,解析前,离开路由时.
beforeEach: 路由跳转前触发,参数to,form,next,主要用于登录验证.
beforeResolve: 和 beforeEach 类似,在 beforeEach 和 beforeRouteEnter 之后,afterEach 之前.
afterEach: 在路由跳转后触发.参数to,from.

next()函数的作用是回调验证导航.
next(): 什么都不传,表示继续执行.多个 next()则可能不会解析或报错.
next(false): 中断当前导航.
next(‘/‘): 跳转到该地址.
next(error): 钩子中止将错误传递给 router.onerror()回调.

1
2
const router = new VueRouter();
router.beforeEach((to, from, next) => {});

路由独享守卫

beforeEnter: 只在进入路由时触发.不会在params,query,hash改变时触发.

1
2
3
4
5
6
7
8
9
const router = new VueRouter({
routes: [
{
path: "./foo",
component: Foo,
beforeEnter: (to, from, next) => {},
},
],
});

组件内守卫

有三个,beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave.
分别代表,渲染该组件对应的路由被验证前调用,路由复用同一个组件,离开当前路由

1
2
3
4
5
6
7
8
9
const Foo = {
template: "...",
beforeRouteEnter(to, from) {},
beforeRouteUpdate(to, from) {
// 比如动态参数/users/:id/,在user/1和user/2之间切换,那就复用同一个组件,
// 就会被调用,而且可以使用this,其他两个不行,因为组件没有了
},
beforeRouteLeave(to, from) {},
};

Vue 底层实现原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter 和 getter,在数据变动时发布消息给订阅者,触发相应的监听回调
Vue 是一个典型的 MVVM 框架,模型(Model)只是普通的 javascript 对象,修改它则试图(View)会自动更新。这种设计让状态管理变得非常简单而直观
Observer(数据监听器) : Observer 的核心是通过 Object.defineProprtty()来监听数据的变动,这个函数内部可以定义 setter 和 getter,每当数据发生变化,就会触发 setter。这时候 Observer 就要通知订阅者,订阅者就是 Watcher
Watcher(订阅者) : Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

  1. 在自身实例化时往属性订阅器(dep)里面添加自己
  2. 自身必须有一个 update()方法
  3. 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调

Compile(指令解析器) : Compile 主要做的事情是解析模板指令,将模板中变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加鉴定数据的订阅者,一旦数据有变动,收到通知,更新试图

谈谈对 vue 生命周期的理解?

每个 Vue 实例在创建时都会经过一系列的初始化过程,vue 的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

  • create 阶段:vue 实例被创建
    beforeCreate: 创建前,此时 data 和 methods 中的数据都还没有初始化
    created: 创建完毕,data 中有值,未挂载
  • mount 阶段: vue 实例被挂载到真实 DOM 节点
    beforeMount:可以发起服务端请求,去数据
    mounted: 此时可以操作 DOM
  • update 阶段:当 vue 实例里面的 data 数据变化时,触发组件的重新渲染
    beforeUpdate :更新前
    updated:更新后
  • destroy 阶段:vue 实例被销毁
    beforeDestroy:实例被销毁前,此时可以手动销毁一些方法
    destroyed:销毁后

组件生命周期

生命周期(父子组件) 父组件 beforeCreate –> 父组件 created –> 父组件 beforeMount –> 子组件 beforeCreate –> 子组件 created –> 子组件 beforeMount –> 子组件 mounted –> 父组件 mounted –>父组件 beforeUpdate –>子组件 beforeDestroy–> 子组件 destroyed –> 父组件 updated
加载渲染过程 父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
挂载阶段 父 created->子 created->子 mounted->父 mounted
父组件更新阶段 父 beforeUpdate->父 updated
子组件更新阶段 父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
销毁阶段 父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

computed 与 watch

通俗来讲,既能用 computed 实现又可以用 watch 监听来实现的功能,推荐用 computed, 重点在于 computed 的缓存功能 computed 计算属性是用来声明式的描述一个值依赖了其它的值,当所依赖的值或者变量 改变时,计算属性也会跟着改变; watch 监听的是已经在 data 中定义的变量,当该变量变化时,会触发 watch 中的方法。
watch 属性监听
是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用
computed 计算属性
属性的结果会被缓存,当 computed 中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用 computed 中的函数必须用 return 返回最终的结果 computed 更高效,优先使用。data 不改变,computed 不更新。
使用场景
computed:当一个属性受多个属性影响的时候使用,例:购物车商品结算功能
watch:当一条数据影响多条数据的时候使用,例:搜索数据

组件中的 data 为什么是一个函数?

1.一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。 2.如果 data 是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间 data 不冲突,data 必须是一个函数。

为什么 v-for 和 v-if 不建议用在一起

1.当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费(Vue2.x) 2.这种场景建议使用 computed,先对数据进行过滤
注意:3.x 版本中 v-if 总是优先于 v-for 生效。由于语法上存在歧义,建议避免在同一元素上同时使用两者。比起在模板层面管理相关逻辑,更好的办法是通过创建计算属性筛选出列表,并以此创建可见元素。
解惑传送门 ☞ # v-if 与 v-for 的优先级对比非兼容

React/Vue 项目中 key 的作用

  • key 的作用是为了在 diff 算法执行时更快的找到对应的节点,提高 diff 速度,更高效的更新虚拟 DOM;

vue 和 react 都是采用 diff 算法来对比新旧虚拟节点,从而更新节点。在 vue 的 diff 函数中,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点。如果没找到就认为是一个新增节点。而如果没有 key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个 map 映射,另一种是遍历查找。相比而言。map 映射的速度更快。

  • 为了在数据变化时强制更新组件,以避免“就地复用”带来的副作用。

当 Vue.js 用 v-for 更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。重复的 key 会造成渲染错误。

vue 组件的通信方式

  • props/$emit 父子组件通信

父->子 props,子->父 $on、$emit 获取父子组件实例 parent、childrenRef 获取实例的方式调用组件的属性或者方法 父->子孙 Provide、inject 官方不推荐使用,但是写组件库时很常用

  • $emit/$on 自定义事件 兄弟组件通信

Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue() 自定义事件

  • vuex 跨级组件通信

Vuex、$attrs、$listenersProvide、inject

nextTick 的实现

  1. nextTick 是 Vue 提供的一个全局 API,是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用$nextTick,则可以在回调中获取更新后的 DOM;
  2. Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启 1 个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中-次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick 方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用;
  3. 比如,我在干什么的时候就会使用 nextTick,传一个回调函数进去,在里面执行 dom 操作即可;
  4. 我也有简单了解 nextTick 实现,它会在 callbacks 里面加入我们传入的函数,然后用 timerFunc 异步方式调用它们,首选的异步方式会是 Promise。这让我明白了为什么可以在 nextTick 中看到 dom 操作结果。

nextTick 的实现原理是什么?

在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。 nextTick 主要使用了宏任务和微任务。 根据执行环境分别尝试采用 Promise、MutationObserver、setImmediate,如果以上都不行则采用 setTimeout 定义了一个异步方法,多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。

使用过插槽么?用的是具名插槽还是匿名插槽或作用域插槽

vue 中的插槽是一个非常好用的东西 slot 说白了就是一个占位的 在 vue 当中插槽包含三种一种是默认插槽(匿名)一种是具名插槽还有一种就是作用域插槽 匿名插槽就是没有名字的只要默认的都填到这里具名插槽指的是具有名字的

keep-alive 的实现

作用:实现组件缓存,保持这些组件的状态,以避免反复渲染导致的性能问题。 需要缓存组件 频繁切换,不需要重复渲染
场景:tabs 标签页 后台导航,vue 性能优化
原理:Vue.js 内部将 DOM 节点抽象成了一个个的 VNode 节点,keep-alive 组件的缓存也是基于 VNode 节点的而不是直接存储 DOM 结构。它将满足条件(pruneCache 与 pruneCache)的组件在 cache 对象中缓存起来,在需要重新渲染的时候再将 vnode 节点从 cache 对象中取出并渲染。

mixin

mixin 项目变得复杂的时候,多个组件间有重复的逻辑就会用到 mixin
多个组件有相同的逻辑,抽离出来
mixin 并不是完美的解决方案,会有一些问题
vue3 提出的 Composition API 旨在解决这些问题【追求完美是要消耗一定的成本的,如开发成本】
场景:PC 端新闻列表和详情页一样的右侧栏目,可以使用 mixin 进行混合
劣势:1.变量来源不明确,不利于阅读 2.多 mixin 可能会造成命名冲突 3.mixin 和组件可能出现多对多的关系,使得项目复杂度变高

Vuex 的理解及使用场景

Vuex 是一个专为 Vue 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。

  1. Vuex 的状态存储是响应式的;当 Vue 组件从 store 中读取状态的时候,

若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新 2. 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation, 这样使得我们可以方便地跟踪每一个状态的变化 Vuex 主要包括以下几个核心模块:

  1. State:定义了应用的状态数据
  2. Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),

就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算 3. Mutation:是唯一更改 store 中状态的方法,且必须是同步函数 4. Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作 5. Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中

hooks 用过吗?聊聊 react 中 class 组件和函数组件的区别

类组件是使用 ES6 的 class 来定义的组件。
函数组件是接收一个单一的 props 对象并返回一个 React 元素。

关于 React 的两套 API(类(class)API 和基于函数的钩子(hooks) API)。官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较”轻”,而类比较”重”。而且,钩子是函数,更符合 React 函数式的本质。
函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据函数这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。函数的返回结果只依赖于它的参数。不改变函数体外部数据、函数执行过程里面没有副作用。
类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。
类组件的缺点 :
大型组件很难拆分和重构,也很难测试。
业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。
组件类引入了复杂的编程模式,比如 render props 和高阶组件。
难以理解的 class,理解 JavaScript 中 this 的工作方式。
区别
函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。 1.状态的有无
hooks 出现之前,函数组件没有实例,没有生命周期,没有 state,没有 this,所以我们称函数组件为无状态组件。 hooks 出现之前,react 中的函数组件通常只考虑负责 UI 的渲染,没有自身的状态没有业务逻辑代码,是一个纯函数。它的输出只由参数 props 决定,不受其他任何因素影响。 2.调用方式的不同
函数组件重新渲染,将重新调用组件方法返回新的 react 元素。类组件重新渲染将 new 一个新的组件实例,然后调用 render 类方法返回 react 元素,这也说明为什么类组件中 this 是可变的。 3.因为调用方式不同,在函数组件使用中会出现问题
在操作中改变状态值,类组件可以获取最新的状态值,而函数组件则会按照顺序返回状态值

React Hooks(钩子的作用)
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
React Hooks 的几个常用钩子:

  1. useState() //状态钩子
  2. useContext() //共享状态钩子
  3. useReducer() //action 钩子
  4. useEffect() //副作用钩子

还有几个不常见的大概的说下,后续会专门写篇文章描述下

  • 1.useCallback 记忆函数 一般把函数式组件理解为 class 组件 render 函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。而有了 useCallback 就不一样了,你可以通过 useCallback 获得一个记忆后的函数。
1
2
3
4
5
6
function App() {
const memoizedHandleClick = useCallback(() => {
console.log("Click happened");
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}

第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback 就会重新返回一个新的记忆函数提供给后面进行渲染。

  • 2.useMemo 记忆组件 useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。 唯一的区别是:useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你
    所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。
  • 3.useRef 保存引用值 useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用。useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问组件或真实的 DOM 节点,重点是组件也是可以访问到的,从而可以对 DOM 进行一些操作,比如监听事件等等。
  • 4.useImperativeHandle 穿透 Ref 通过 useImperativeHandle 用于让父组件获取子组件内的索引
  • 5.useLayoutEffect 同步执行副作用大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。useEffect 和 useLayoutEffect 有什么区别:简单来说就是调用时机不同,useLayoutEffect 和原来 componentDidMount&componentDidUpdate 一致,在 react 完成 DOM 更新后马上同步调用的代码,会阻塞页面渲染。而 useEffect 是会在整个页面渲染完才会调用的代码。官方建议优先使用 useEffect

React 组件通信方式

react 组件间通信常见的几种情况:

  1. 父组件向子组件通信
  2. 子组件向父组件通信
  3. 跨级组件通信
  4. 非嵌套关系的组件通信

1)父组件向子组件通信

父组件通过 props 向子组件传递需要的信息。父传子是在父组件中直接绑定一个正常的属性,这个属性就是指具体的值,在子组件中,用 props 就可以获取到这个值

1
2
3
4
5
6
7
8
9
// 子组件: Child
const Child = (props) => {
return <p>{props.name}</p>;
};

// 父组件 Parent
const Parent = () => {
return <Child name="京程一灯"></Child>;
};

2)子组件向父组件通信

props+回调的方式,使用公共组件进行状态提升。子传父是先在父组件上绑定属性设置为一个函数,当子组件需要给父组件传值的时候,则通过 props 调用该函数将参数传入到该函数当中,此时就可以在父组件中的函数中接收到该参数了,这个参数则为子组件传过来的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 子组件: Child
const Child = (props) => {
const cb = (msg) => {
return () => {
props.callback(msg);
};
};
return <button onClick={cb("京程一灯欢迎你!")}>京程一灯欢迎你</button>;
};

// 父组件 Parent
class Parent extends Component {
callback(msg) {
console.log(msg);
}
render() {
return <Child callback={this.callback.bind(this)}></Child>;
}
}

3)跨级组件通信

即父组件向子组件的子组件通信,向更深层子组件通信。

  • 使用 props,利用中间组件层层传递,但是如果父组件结构较深,那么中间每一层组件都要去传递 props,增加了复杂度,并且这些 props 并不是中间组件自己需要的。
  • 使用 context,context 相当于一个大容器,我们可以把要通信的内容放在这个容器中,这样不管嵌套多深,都可以随意取用,对于跨越多层的全局数据可以使用 context 实现。
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
// context方式实现跨级组件通信
// Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据

const BatteryContext = createContext();

// 子组件的子组件
class GrandChild extends Component {
render() {
return (
<BatteryContext.Consumer>
{(color) => <h1 style={{ color: color }}>我是红色的:{color}</h1>}
</BatteryContext.Consumer>
);
}
}

// 子组件
const Child = () => {
return <GrandChild />;
};
// 父组件
class Parent extends Component {
state = {
color: "red",
};
render() {
const { color } = this.state;
return (
<BatteryContext.Provider value={color}>
<Child></Child>
</BatteryContext.Provider>
);
}
}

4)非嵌套关系的组件通信

即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。

  1. 可以使用自定义事件通信(发布订阅模式),使用 pubsub-js
  2. 可以通过 redux 等进行全局状态管理
  3. 如果是兄弟组件通信,可以找到这两个兄弟节点共同的父节点, 结合父子间通信方式进行通信。
  4. 也可以 new 一个 Vue 的 EventBus,进行事件监听,一边执行监听,一边执行新增 VUE 的 eventBus 就是发布订阅模式,是可以在 React 中使用的;

setState 既存在异步情况也存在同步情况

1.异步情况 在 React 事件当中是异步操作 2.同步情况 如果是在 setTimeout 事件或者自定义的 dom 事件中,都是同步的

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
//setTimeout事件
import React, { Component } from "react";
class Count extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}

render() {
return (
<>
<p>count:{this.state.count}</p>
<button onClick={this.btnAction}>增加</button>
</>
);
}

btnAction = () => {
//不能直接修改state,需要通过setState进行修改
//同步
setTimeout(() => {
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
});
};
}

export default Count;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//自定义dom事件
import React, { Component } from "react";
class Count extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}

render() {
return (
<>
<p>count:{this.state.count}</p>
<button id="btn">绑定点击事件</button>
</>
);
}

componentDidMount() {
//自定义dom事件,也是同步修改
document.querySelector("#btn").addEventListener("click", () => {
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
});
}
}

export default Count;

生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//安装
//当组件的实例被创建并插入到 DOM 中时,这些方法按以下顺序调用:

constructor()
static getDerivedStateFromProps()
render()
componentDidMount()

//更新中
//更新可能由道具或状态的更改引起。当重新渲染组件时,这些方法按以下顺序调用:

static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()

//卸载
//当组件从 DOM 中移除时调用此方法:

componentWillUnmount()

说一下 react-fiber

1)背景

react-fiber 产生的根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用 setState 更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

2)实现原理

  • react 内部运转分三层:
    • Virtual DOM 层,描述页面长什么样。
    • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
    • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:

1
2
3
4
5
6
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
}
  • 为了实现不卡顿,就需要有一个调度器 (Scheduler) 来进行任务分配。优先级高的任务(如键盘输入)可以打断优先级低的任务(如 Diff)的执行,从而更快的生效。任务的优先级有六种:
    • synchronous,与之前的 Stack Reconciler 操作一样,同步执行
    • task,在 next tick 之前执行
    • animation,下一帧之前执行
    • high,在不久的将来立即执行
    • low,稍微延迟执行也没关系
    • offscreen,下一次 render 时或 scroll 时才执行
  • Fiber Reconciler(react )执行过程分为 2 个阶段:
    • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
    • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。
  • Fiber 树:React 在 render 第一次渲染时,会通过 React.createElement 创建一颗 Element 树,可以称之为 Virtual DOM Tree,由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node,将 Fiber Node 链接起来的结构成为 Fiber Tree。Fiber Tree 一个重要的特点是链表结构,将递归遍历编程循环遍历,然后配合 requestIdleCallback API, 实现任务拆分、中断与恢复。

从 Stack Reconciler 到 Fiber Reconciler,源码层面其实就是干了一件递归改循环的事情
传送门 ☞# 深入了解 Fiber

Portals

Portals 提供了一种一流的方式来将子组件渲染到存在于父组件的 DOM 层次结构之外的 DOM 节点中。结构不受外界的控制的情况下就可以使用 portals 进行创建

何时要使用异步组件?如和使用异步组件

  • 加载大组件的时候
  • 路由异步加载的时候

react 中要配合 Suspense 使用

1
2
3
4
// 异步懒加载
const Box = lazy(() => import("./components/Box"));
// 使用组件的时候要用suspense进行包裹
<Suspense fallback={<div>loading...</div>}>{show && <Box />}</Suspense>;

React 事件绑定原理

React 并不是将 click 事件绑在该 div 的真实 DOM 上,而是在 document 处监听所有支持的事件,当事件发生并冒泡至 document 处时,React 将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。

webpack

webpack 做过哪些优化,开发效率方面、打包策略方面等等

1)优化 Webpack 的构建速度

  • 使用高版本的 Webpack (使用 webpack4)
  • 多线程/多实例构建:HappyPack(不维护了)、thread-loader
  • 缩小打包作用域:
    • exclude/include (确定 loader 规则范围)
    • resolve.modules 指明第三方模块的绝对路径 (减少不必要的查找)
    • resolve.extensions 尽可能减少后缀尝试的可能性
    • noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
    • IgnorePlugin (完全排除模块)
    • 合理使用 alias
  • 充分利用缓存提升二次构建速度:
    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin
      注意:thread-loader 和 cache-loader 兩個要一起使用的話,請先放 cache-loader 接著是 thread-loader 最後才是 heavy-loader
  • DLL:
    • 使用 DllPlugin 进行分包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间。 2)使用 webpack4-优化原因
  • (a)V8 带来的优化(for of 替代 forEach、Map 和 Set 替代 Object、includes 替代 indexOf)
  • (b)默认使用更快的 md4 hash 算法
  • (c)webpacks AST 可以直接从 loader 传递给 AST,减少解析时间
  • (d)使用字符串方法替代正则表达式 ①noParse
  • 不去解析某个库内部的依赖关系
  • 比如 jquery 这个库是独立的, 则不去解析这个库内部依赖的其他的东西
  • 在独立库的时候可以使用
1
2
3
4
5
6
module.exports = {
module: {
noParse: /jquery/,
rules: [],
},
};

②IgnorePlugin

  • 忽略掉某些内容 不去解析依赖库内部引用的某些内容
  • 从 moment 中引用 ./locol 则忽略掉
  • 如果要用 local 的话 则必须在项目中必须手动引入
1
2
3
4
5
6
import 'moment/locale/zh-cn'
module.exports = {
plugins: [
new Webpack.IgnorePlugin(/./local/, /moment/),
]
}

③dillPlugin

  • 不会多次打包, 优化打包时间
  • 先把依赖的不变的库打包
  • 生成 manifest.json 文件
  • 然后在 webpack.config 中引入
  • webpack.DllPlugin Webpack.DllReferencePlugin ④happypack -> thread-loader
  • 大项目的时候开启多线程打包
  • 影响前端发布速度的有两个方面,一个是构建,一个就是压缩,把这两个东西优化起来,可以减少很多发布的时间。 ⑤thread-loader
    thread-loader 会将您的 loader 放置在一个 worker 池里面运行,以达到多线程构建。
    把这个 loader 放置在其他 loader 之前(如下图 example 的位置), 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.js$/,
include: path.resolve("src"),
use: [
"thread-loader",
// 你的高开销的loader放置在此 (e.g babel-loader)
],
},
],
},
};

每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。请在高开销的 loader 中使用,否则效果不佳
⑥ 压缩加速——开启多线程压缩

  • 不推荐使用 webpack-paralle-uglify-plugin,项目基本处于没人维护的阶段,issue 没人处理,pr 没人合并。
    Webpack 4.0 以前:uglifyjs-webpack-plugin,parallel 参数
1
2
3
4
5
6
7
8
9
module.exports = {
optimization: {
minimizer: [
new UglifyJsPlugin({
parallel: true,
}),
],
},
};
  • 推荐使用 terser-webpack-plugin
1
2
3
4
5
6
7
module.exports = {
optimization: {
minimizer: [new TerserPlugin(
parallel: true // 多线程
)],
},
};

2)优化 Webpack 的打包体积

  • 压缩代码
  • 提取页面公共资源:
  • Tree shaking
  • Scope hoisting
  • 图片压缩
  • 动态 Polyfill

3)speed-measure-webpack-plugin
简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
开发阶段
开启多核压缩 插件:** terser-webpack-plugin **

1
2
3
4
5
6
7
8
9
10
11
12
13
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
ecma: 6,
},
}),
],
},
};

传送门 ☞# 工程化专题

Babel

简单描述一下 Babel 的编译过程

Babel 是一个 JavaScript 编译器,是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。
Babel 本质上就是在操作 AST 来完成代码的转译。AST 是抽象语法树(Abstract Syntax Tree, AST)
如果想要了解更多,可以阅读和尝试:

Babel 的功能很纯粹,它只是一个编译器。大多数编译器的工作过程可以分为三部分:

  1. 解析(Parse) :将源代码转换成更加抽象的表示方法(例如抽象语法树)。包括词法分析和语法分析。词法分析主要把字符流源代码(Char Stream)转换成令牌流( Token Stream),语法分析主要是将令牌流转换成抽象语法树(Abstract Syntax Tree,AST)。
  2. 转换(Transform) :通过 Babel 的插件能力,对(抽象语法树)做一些特殊处理,将高版本语法的 AST 转换成支持低版本语法的 AST。让它符合编译器的期望,当然在此过程中也可以对 AST 的 Node 节点进行优化操作,比如添加、更新以及移除节点等。
  3. 生成(Generate) :将 AST 转换成字符串形式的低版本代码,同时也能创建 Source Map 映射。

经过这三个阶段,代码就被 Babel 转译成功了。

Git

Git 常用命令

查看分支:git branch
创建分支:git branch
切换分支:git checkout
创建+切换分支:git checkout -b
合并某分支到当前分支:git merge
删除分支:git branch -d

如何使用 Git 管理项目


实际开发中,一个仓库(一般只放一个项目)主要存在两条主分支:master 与 develop 分支。这个两个分支的生命周期是整个项目周期。
我们可能使用的不同类型的分支对项目进行管理是:

  • 功能分支 功能分支(或有时称为主题分支)用于为即将发布或遥远的未来版本开发新功能。在开始开发某个功能时,将包含该功能的目标版本在那时很可能是未知的。功能分支的本质在于,只要该功能处于开发阶段,它就存在,但最终会被合并回 develop(明确将新功能添加到即将发布的版本中)或丢弃。功能分支通常只存在于开发者仓库中,而不存在于 origin。
  • 发布分支 发布分支支持准备新的生产版本。它们允许在最后一刻打点 i 和交叉 t。此外,它们允许修复小错误并为发布准备元数据(版本号、构建日期等)。通过在发布分支上完成所有这些工作,该 develop 分支被清除以接收下一个大版本的功能。
    • 从 develop 分支拉取,且必须合并回 develop 和 master
    • 分支命名约定:release-*
  • 修补程序分支 Hotfix 分支与发布分支非常相似,因为它们也旨在为新的生产版本做准备,尽管是计划外的。它们产生于需要立即对现场制作版本的不良状态采取行动。当必须立即解决生产版本中的关键错误时,可以从标记生产版本的主分支上的相应标记中分支出一个修补程序分支。

master:这个分支最为稳定,这个分支表明项目处于可发布的状态。
develop:做为开发的分支,平行于 master 分支。
Feature branches:这种分支和咱们程序员平常开发最为密切,称做功能分支。必须从 develop 分支建立,完成后合并回 develop 分支。
Release branches:这个分支用来分布新版本。从 develop 分支建立,完成后合并回 develop 与 master 分支。这个分支上能够作一些很是小的 bug 修复,固然,你也能够禁止在这个分支作任何 bug 的修复工做,而只作版本发布的相关操做,例如设置版本号等操做,那样的话那些发现的小 bug 就必须放到下一个版本修复了。若是在这个分支上发现了大 bug,那么也绝对不能在这个分支上改,须要 Featrue 分支上改,走正常的流程。
Hotfix branches:这个分支主要为修复线上特别紧急的 bug 准备的。必须从 master 分支建立,完成后合并回 develop 与 master 分支。这个分支主要是解决线上版本的紧急 bug 修复的,例如忽然版本 V0.1 上有一个致命 bug,必须修复。那么咱们就能够从 master 分支上发布这个版本那个时间点 例如 tag v0.1(通常代码发布后会及时在 master 上打 tag),来建立一个 hotfix-v0.1.1 的分支,而后在这个分支上改 bug,而后发布新的版本。最后将代码合并回 develop 与 master 分支。
更多请参考

项目优化

移除生产环境的控制台打印。方案很多,esling+pre-commit、使用插件自动去除,插件包括 babel-plugin-transform-remove-console、uglifyjs-webpack-plugin、terser-webpack-plugin。最后选择了 terser-webpack-plugin,脚手架 vue-cli 用这个插件来开启缓存和多线程打包,无需安装额外的插件,仅需在 configureWebpack 中设置 terser 插件的 drop_console 为 true 即可。最好还是养成良好的代码习惯,在开发基本完成后去掉无用的 console,vscode 中的 turbo console 就蛮好的。
第三方库的按需加载。echarts,官方文档里是使用配置文件指定使用的模块,另一种使用 babel-plugin-equire 实现按需加载。element-ui 使用 babel-plugin-component 实现按需引入。
前后端数据交换方面,推动项目组使用蓝湖、接口文档,与后端同学协商,规范后台数据返回。
雅虎军规提到的,避免 css 表达式、滤镜,较少 DOM 操作,优化图片、精灵图,避免图片空链接等。
性能问题:页面加载性能、动画性能、操作性能。Performance API,记录性能数据。
winter 重学前端 优化技术方案:
缓存:客户端控制的强缓存策略。
降低请求成本:DNS 由客户端控制,隔一段时间主动请求获取域名 IP,不走系统 DNS(完全看不懂)。TCP/TLS 连接复用,服务器升级到 HTTP2,尽量合并域名。
减少请求数:JS、CSS 打包到 HTML。JS 控制图片异步加载、懒加载。小型图片使用 data-uri。
较少传输体积:尽量使用 SVG\gradient 代替图片。根据机型和网络状况控制图片清晰度。对低清晰度图片使用锐化来提升体验。设计上避免大型背景图。
使用 CDN 加速,内容分发网络,是建立再承载网基础上的虚拟分布式网络,能够将源站内容缓存到全国或全球的节点服务器上。用户就近获取内容,提高了资源的访问速度,分担源站压力。

配置 nginx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80;
server_name localhost;
# 访问 /, 返回静态资源
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
# api转发,(请求后端api,转发到7001端口)
lacation /api/v1 {
proxy_pass http://127.0.0.1:7001;
}
# 报错页面
error_page 500 502 503 504 50x.html
location = /50x.html {
root /usr/share/nginx/html;
}
}

配置 webpack

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
const path = require("path");

module.exports = {
mode: "none",
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
publicPath: "",
},
devServer: {
hotOnly: true,
proxy: {
"/api": {
target: "api.example.com",
changeOrigin: true,
pathRewrite: {
"^api": "",
},
},
},
},
modules: {
rules: [
{
test: /.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin(),
new CopyWebpackPlugin(),
],
};

配置 rollup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import json form '@rollup/plugin-json';
import {nodeResolve} from '@rollup/plugin-node-resolve';

export default {
input: 'src/index.js',
output: {
// 如果是多入口或者分包就不能使用file,需要使用dir
file: 'dist/bundle.js',
// dir: 'dist',
// 多入口和分包也不能使用iife,可以使用amd.iife会将文件放在一个函数内
format: 'iife'
// format: 'amd'
},
// 由于roolup默认只支持esm,如果要加载cjs模块需要插件
plugins: [
// 加载json文件
json(),
// 加载cjs文件
nodeResolve()
]
}

React 问题

React.memo useMemo useCallback 都是什么,区别?应用场景?方法?

  • React.memo 是高阶组件,是对整个组件进行包裹.作用类似于 class 组件中的shouldComponentUpdate,对 props 是否变化做浅层比较.
    变化才会重新渲染.如果想深层比较,可以使用它的第二个参数.和shouldComponentUpdate不同的地方是第二个参数的返回值,props 如果相等是返回 true,而shouldComponentUpdate返回的是 false.
1
export default React.memo(MyComponent, areEqual); // 第二个参数areEqual
  • React.memo 是对整个组件进行包裹,useMemo 和 useCallback 是对更细粒度的值或函数进行优化.
  • useMemo 和 useCallback 差异在于返回值,一个是返回的 memorize 值,一个是返回 memorize 函数.
  • useMemo 和 useCallback 需要配合 React.memo,如果不使用 memo,那么每次 prop 整体都是一个新的对象/
  • Vue 会自动去做这件事,React 需要手动去做.

Hook 为啥不能变.不能在前面加判断语句?

要保证 hooks 是在最顶层调用.是为了保证多个 hooks 调用的顺序是一致.
Hooks 最终是构建了一个单项链表,每次执行都是按照同样的顺序被调用,如果放在循环,条件语句或者嵌套函数中肯定会破坏它的顺序型导致问题.

Fiber 算法

Fiber 是一个执行单元,一种数据结构.因为 React16 之前是将虚拟 DOM 树看出一个任务执行,递归处理,任务庞大且不能中断.
React16 之后,将整个任务分成一个个小的任务在空余时间里处理,主要是通过 requestIdleCallback 这个浏览器的 API 去获取浏览器的空余时间,如果有,就执行,没有就让出主线程.

useLayoutEffect,和 useEffect 区别?在哪个阶段执行? beginWork,commit Work

二者的函数签名是相同的,不同点在于触发的时机不同.
useLayoutEffect是同步处理副作用.useEffect 是异步处理.
useLayoutEffect在 DOM 更新后,浏览器渲染之前,useLayoutEffect内部的更新将会同步刷新.
useEffect是在浏览器渲染后.
useLayoutEffect是在commitWork阶段.

Redux 是双向数据绑定吗?

不是,是单向数据流.

redux 和 react-redux 区别

  1. redux 和组件进行对接的时候是直接在组件中进行创建。react-redux 是运用 Provider 将组件和 store 对接,使在 Provider 里的所有组件都能共享 store 里的数据,还要使用 connect 将组件和 react 连接.
  2. redux 获取 state 是直接通过 store.getState()。
    react-redux 获取 state 是通过 mapStateToProps 函数,只要 state 数据变化就能获取最新数据
  3. redux 是使用 dispatch 直接触发,来操作 store 的数据。
    react-redux 是使用 mapDispatchToProps 函数然后在调用 dispatch 进行触发

React vs Vue 区别

  1. 从写法上,react 推荐的是 jsx+内联样式,即all in js.Vue 是 template 的单文件格式,即 html,js,css 写在同一个文件中.
  2. 虚拟 DOM 的区别,主要是 diff 算法的区别
  3. 数据驱动视图.
  • Vue 是类似 MVVM 框架,数据响应式 => 转化 data 属性为 getter/setter =>watcher 实例对象进行依赖收集与监听 =>数据 setter 被调用时,watcher 对比前后两个值是否发生变化 => 通知视图是否进行渲染.
  • React 通过setState实现数据驱动视图. setState 将接收的第一个参数 state 存储在pending队列中,判断当前 React 是否处在批量更新状态,是的话就将需要更新的 state 组件添加到dirtyComponent中,不是的话,就遍历dirtyComponent中的所有组件,调用updateComponent方法更新每个dirty组件.

diff 算法区别

  1. dom 更新策略不同
    react 会自顶而下全 diff,vue 会跟踪每个组件的依赖关系,不需要重新渲染整个组件树.
    react 当状态发生改变会重新 render 页面,生成新的虚拟 dom 树,新旧 dom 树进行比较,进行打补丁的方式,局部更新 dom.
    所以需要手动的对不需要重新渲染的组件或者值进行 memo 优化.
    vue 是通过数据响应式的方式,通过Object.defineProperty把 data 的属性转化为getter/setter.同时进行依赖收集与监听,setter 被调用时进行组件更新.
  2. diff 算法源码实现不同
    vue 采用双端对比的算法,同时从新旧 children 的两端进行比较,借助 key 值找到可复用的节点,再进行相关操作.

react 为什么没有采用双指针?

因为目前 Fiber 上没有设置反向链表,单向链表无法使用双指针,所以无法对算法进行双指针优化.

react 异步组件

原理: 用 componentDidCatch 捕获异步请求,如果有异步请求渲染 fallback,等到异步请求执行完毕,渲染真实组件,借此整个异步流程完毕.

1
2
3
4
5
6
7
8
9
const LazyComponent = React.lazy(() => import("./test.js"));

export default function Index() {
return (
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
);
}

setState 是异步还是同步

本质上是批量执行或非批量执行的问题.而且只在 React18 以前存在.
在 react17 中,setState 是批量执行的,因为执行前会设置 executionContext。但如果在 setTimeout、事件监听器等函数里,就不会设置 executionContext 了,这时候 setState 会同步执行。可以在外面包一层 batchUpdates 函数,手动设置下 executionContext 来切换成异步批量执行
在 react18 里面,如果用 createRoot 的 api,就不会有这种问题了,因为所有的 setState 都是异步批量执行了。

webpack

webpack plugin 原理

利用钩子机制,在生命周期的钩子上挂载函数实现扩展.

webpack 中 publicPath 作用?

静态资源的基础路径,一般在生成模式下使用.

tree shaking

作用是移除未引入的代码.
webpack 的 tree-shaking 在 production 下是默认开启的.
tree-shaking 需要依赖 ESModule 模块才能起作用.
在开发环境下可以通过设置optimization: { useExports: true; minimize: true}开启.
这个和babel-loaderpreset-env互斥,但是新版本的已经改为自动判断是否开启.所以不用担心.

Webpack  => sideEffects

可以在optimization中开启标识副作用,在package.json中标识是否有副作用.
也可以在package.json中标记哪些有副作用,不过在生产环境是默认开启的.

1
2
//package.json
sideEffects: ["./src/extends.js", "*.css"];

直接导出构造函数会被 tree-shaking 吗

看导出的构造函数有没有被使用了

webpack 打包体积如何减小?

chunk 分包,import动态导入,提取公共模块.

webpack 优化

在加快构建时间方面,作用最大的是配置 cache,可大大加快二次构建速度。

在减小打包体积方面,作用最大的是压缩代码、分离重复代码、Tree Shaking,可最大幅度减小打包体积。

在加快加载速度方面,按需加载、浏览器缓存、CDN 效果都很显著。

配置持久化 cache

1
2
3
cache: {
type: "filesystem"
}

压缩代码:

  • 压缩 js 代码:在 production 模式默认开启压缩.可以在 optimization 的 minimizer 中进行配置
  • 压缩 html 代码: 在 HtmlWebpackPlugin 中 minify 开启压缩.

打包的代码加上 hash,实现缓存文件
开启 tree Shaking,移除未引用代码(production 默认开启)
开启方法: 在 package.json 配置 sideEffects: false(表示都没有副作用,可以清除所有未引用代码),如果有,写个数组[‘*.css’],一般都是 css 文件
按照路由拆分代码,实现按需加载

多页应用实现多入口打包,设置多个 entry 入口,htmlWebpackPlugin 也是多个
提取公共模块,也就是分离代码,在 optimization 中设置 splitChunks,

图片压缩

  • 使用雪碧图,字体图标, 5k 一下使用 base64 编码
    使用 cdn

使用预加载
script 标签中 rel=”preload”

  • DNS 预解析 dns-prefetch
  • 资源预加载 preload
  • 预渲染 prerender

减少重排重绘

  • 不使用 table 布局
  • css 属性读写分离
  • 样式批量修改
  • 减少 dom 深度
  • 将没用的元素设置为不可见 visibility: hidden

polyfill 按需加载 babel 典型配置

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
presets: [
[
"@babel/preset-env",
{
target: {}, // 适配哪些浏览器,可以写在.browserlistrc
corejs: 3, // corejs版本
useBuiltIns: "usage", // 按需加载usage,完全加载entry,
modules: false, // 是否将es6转为其他
},
],
],
};

postcss 什么作用?

处理 css 兼容性的工具,一般有多种工具,比如autoprefixer处理不同浏览器的前缀.

sass-loader 用了什么方法

1
2
3
4
5
6
7
8
9
10
const nodeSass = require("node-sass");
const path = require("path");

let result = nodeSass.renderSync({
file: path.resolve(__dirname, "../src/scss/index.scss"),
outputStyle: "expanded",
});
module.exports = function () {
return result.css.toString();
};

可以看到,sass-loader 使用了 node-sass 解析 sass 文件导出为 css 文件.输出为字符串交给下一个 loader.

Esm 是什么?

es6 module 是一种模块化开发的规范.设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系和导入导出的变量.
主要功能由两个命令构成: export, import.

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import 之后是不可修改的 例如,在使用 CommonJS 时,必须导入完整的工具 (tool) 或库 (library) 对象,且可带有条件判断来决定是否导入。

Umi 了解吗

Element-ant 之类的是怎么实现响应式布局?

参照了 Bootstrap 的 响应式设计,预设了五个响应尺寸:xs、sm、md、lg 和 xl。
也就是通过类名.
在 less 或者 scss 文件中设置变量.媒体查询通过断点设置 css.

Vue

Vue 对数组是怎么处理的

  1. 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
  2. 对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
  3. 把需要被拦截的 Array 类型的数据原型指向改造后原型。
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
import { def } from "../util/index";

const arrayProto = Array.prototype; // 这是个空数组继承了Array的所有方法
export const arrayMethods = Object.create(arrayProto); // 这是个对象继承了所有Array的方法

const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];

methodsToPatch.forEach(function (method) {
// 缓存原来的方法
const original = arrayProto[method]; // 原来Array 的方法
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args); // 调用还是用原来Array的方法调用
const ob = this.__ob__;
let inserted;
// 判断传入的方法,进行修改args,push,unshift只接收一种参数,就把args赋值给inserted
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
// splice是接收3个参数,实际要处理的参数就是第3个往后的
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});

$set 为什么可以检测数组变化?

如果 target 是一个数组且索引有效,就设置 length 的属性。
通过 splice 方法把 value 设置到 target 数组指定位置。
设置的时候,vue 会拦截到 target 发生变化,然后把新增的 value 也变成响应式
最后返回 value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function set(target, key, val) {
//...
// target是传入数组或对象,key是索引,val是修改的值
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
//...
defineReactive$$1(ob.value, key, val);
ob.dep.notify();
return val;
}

$nextTick 的作用和原理

Vue 中 DOM 更新是异步的,批量的.在修改 DOM 后立即获取,是获取不到的,需要等待 dom 更新后才能获取.
tis.$nextTick 接收一个函数,函数会在下一次循环 DOM 渲染完毕后执行,在这里也就得到了最新的值.

mutationObserver 了解吗

属于微任务,观察 dom 元素,并在检测到更改时触发回调.

1
2
3
4
5
6
// 通过new创建实例,接收一个回调
// 回调的两个参数,一个是包含了MutationRecord 的数组,一个是观察者对象本身
const observer = new MutationObserver((mutationRecords, observer) => {});
// target是观察目标,options 监听哪方面的内容
observer.observe(target, options);
// 主要是在回调中处理,当观察元素或者子元素变化,就做出相应处理

父子组件的生命周期执行顺序?

执行顺序:父组件先创建,然后子组件创建;子组件先挂载,然后父组件挂载,
否则父组件先挂载就是结束了,子组件没地方挂载了.
大体流程: 先父后字,再先子后父.界限在挂载那里改为先子后父,前面的都是先父后子.

渲染阶段
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
卸载阶段
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

Http

axios 和 fetch 区别

  1. 传递数据方式.axios 放在 data 中,fetch 放在 body 中并且是 string 形式.
  2. 设置超时.axios 设置 timeout 即可,fetch 需要使用AbortController属性设置.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const url = "https://jsonplaceholder.typicode.com/todos";

const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 4000);

fetch(url, {
signal: signal,
})
.then((response) => response.json())
.then(console.log)
.catch((err) => {
console.error(err.message);
});
  1. 数据转化. 对于返回类型,axios 可以自动转化,默认是 json 类型.fetch 需要清楚后端传过来是什么类型,并手动转化.
  2. 拦截器.axios 自带,fetch 需要手动封装.
1
2
3
4
5
6
// axios
axios.interceptors.request.use((response) => {
console.log(response.data);
});
// fetch手动封装
fetch = () => {};
  1. 并发请求. 都支持.
1
2
3
4
5
6
7
8
9
10
axios.all([axios.get("..."), axios.get("...")]).then(
axios.spread((obj1, obj2) => {
//...
})
);
// fetch
Promise.all([fetch("..."), fetch("...")]).then(async ([res1, res2]) => {
const a = await res1.json();
const b = await res2.json();
});
  1. 使用 fetch 封装,注意和 axios 的区别,无法捕获非网络异常导致的 error.必须手动 Promise.reject()抛出.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const url = "https://jsonplaceholder.typicode.com/todos";

fetch(url)
.then((response) => {
if (!response.ok) {
throw new Error(
`This is an HTTP error: The status is ${response.status}`
);
}
return response.json();
})
.then(console.log)
.catch((err) => {
console.log(err.message);
});

interceptor 原理

1、当使用 axios 时,Axios 被实例化,并内部分别实例化 request、response 拦截器
2、当使用 use 添加拦截器时,触发 InterceptorManager.prototype.use 方法,将传过来的 resolve、reject 回调,添加在 this.handlers[]中
3、使用 axios.get(‘https://xxxx‘) 时,触发 Axios.prototype.request ,内部触发 InterceptorManager.prototype.forEach
3.1 request 拦截器 和 response 拦截器 触发 Axios.prototype.request 内方法不一样 分别是:this.interceptors.request.forEach、this.interceptors.response.forEach 二者主要区别在于想队列 chain[] 中添加的位置不同,request 拦截器从头开始添加 和 response 拦截器从尾开始添加,而 chain[]本身就含有 xhr 的 promise 调用返回
3.2 从而实现:先触发 request 拦截器=>发请求,请求返回=>触发 response 拦截器

  1. Axios.prototype.request 最后 return promise,就是我们最后使用的 axios.get(‘’)这个方法的原样,所有 axios.get(‘’).then(res)会再被调用,res 就是请求返回的数据被 response 拦截器处理后的最终结果了

fetch 的 interceptor 原理

拦截器的原理很简单,原生 fetch,接受两个参数,一个是请求路径 resource,resource 可以是 url 也可以是 request 对象,另外一个参数是 config,可以配置 method,header 等请求参数。而拦截器则是通过替换 window.fetch,在其外面再套一层处理起 request 以及 response。

1
2
3
4
5
6
7
8
9
10
function initFetchInterceptor() {
const { fetch: originalFetch } = window;

window.fetch = async (...args) => {
const [resource, config] = args;
const response = await originalFetch(resource, config);

return response;
};
}

request 拦截比较简单,就是直接获取 resource 以及 config 进行处理,替换为自己想要的参数再发起请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// request拦截
function initFetchInterceptor() {
const { fetch: originalFetch } = window;

window.fetch = async (...args) => {
// resource => https://api.github.com?name=meadery
const [resource, config] = args;
// 把resouce替换
const _reource = "https://api.github.com?name=meadery";
const response = await originalFetch(_reource, config);

return response;
};
}

response 拦截

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
const btn = document.getElementById("btn");

function initFetchInterceptor() {
const { fetch: originalFetch } = window;

window.fetch = async (...args) => {
const [resource, config] = args;
console.log("resource: ", resource);
console.log("config: ", config);

const response = await originalFetch(resource, config);
console.log("response: ", response);
const json = () =>
response
.clone()
.json()
.then(() => "hello interceptor");
response.json = json;
return response;
};
}

initFetchInterceptor();

btn.addEventListener("click", async function () {
const response = await fetch("https://api.github.com", {
method: "GET",
});
response.json().then((data) => {
console.log("data: ", data);
});
console.log("response: ", response);
});

CORS 的请求头

CORS 是跨域资源共享.
当浏览器发现域是不同的,会向服务端发送一个 options 请求,检查请求是否允许.
前端可以在该方法中添加Access-Control-Request-Methods,表头中提供一些信息,真实请求何时到来,数据类型是什么.
服务端响应发回Access-Control-Allow-Origin,返回允许的地址.

ES6

Map,set, weakMap,weakSet 区别?

Set 是类数组的数据结构,内部存储的值并不重复.
weakSet 的成员只能是对象类型,而且是弱引用,就是如果没有对象引用内部的对象,那就可能被 GC,所以不能遍历.
Map 是键值对的组合,和 Object 的区别是 key 可以是任意类型,而对象只能是 string 和 symbol.
weakMap 类似于 Map,不过只接受对象做 key.
弱引用是所引用的对象的其他引用被清除,垃圾回收就会自动释放该对象占用的内存.(就是没被人用了,就清除了)

Proxy 和 Reflect?

Proxy 和 Reflect 是 ES6 推出的新特性.
Prxoy 可以代理对象的属性,内部有陷阱函数可以对代理的对象进行操作.通过 Rflect 可以将代理对象的方法返回.

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: 'Tom'
}

const handler = () => {
get(target, key, receiver) {
console.log(key,'key')
return Reflect.get(target, key, receiver)
}
}

const proxy = new Proxy(obj, handler)

receiver 表示代理对象或者继承对象

Promise 并发控制

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
//自定义请求函数
var request = (url) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`任务${url}完成`);
}, 1000);
}).then((res) => {
console.log("外部逻辑", res);
});
};
// 执行任务
async function fn() {
let urls = [
"bytedance.com",
"tencent.com",
"alibaba.com",
"microsoft.com",
"apple.com",
"hulu.com",
"amazon.com",
]; // 请求地址
let pool = []; //并发池
let max = 3; //最大并发量
for (let i = 0; i < urls.length; i++) {
let url = urls[i];
let task = request(url);
task.then((data) => {
//每当并发池跑完一个任务,从并发池删除个任务
pool.splice(pool.indexOf(task), 1);
console.log(`${url} 结束,当前并发数:${pool.length}`);
});
// 异步队列等待这个同步任务把pool塞满
pool.push(task);
// 当pool满后,停止循环,执行异步队列中的task,继续循环
if (pool.length === max) {
//利用Promise.race方法来获得并发池中某任务完成的信号
//跟await结合当有任务完成才让程序继续执行,让循环把并发池塞满
console.log("停止循环");
await Promise.race(pool);
}
}
}
fn();

Node

node 写过 cli 吗

简历项目

在项目中发现首屏加载时间 xx 秒,有很大优化空间,经过研究采用 xx 方案优化到了 xx 秒.提升了 60%性能.

借鉴 xx 项目中优秀的方案,讲解

竟可能多说解决方案

看了好多 Docker 部署的文章,除了列举相同的主要步骤,具体到底怎么部署的,愣是没搞明白.也许到了这个部署 node 的阶段的前端都已经是大神了,不需要讲那么清楚,那我就只好自己摸索记录一下.

也不需要讲什么具体概念,直接就冲方法,先把成就感拿到,回头你再反复咀嚼,看看别人文章里的概念细节,品位品位.

如果讲的不对的地方有大佬指正,将不胜感激.

0. 前提

本项目是 egg.js+mongoDB 的 youtubeclone 的后端项目.

修改项目

在 egg 项目的 package.json 文件中,在 start 启动项中,–daemon 是后台启动。如果使用 docker 容器,需要去除 –daemon .

大体流程

1. 买服务器

首先,你得有个服务器.没有自己想办法白嫖,什么阿里腾讯的学生机,什么 AWS 的一年免费小水管.
买了服务器,肯定要装系统,建议就是 Linux 的,什么 CentOS,Debian 之类的.

2. 安装 Docker

文章目的就是冲它,肯定要先装它.

1
2
3
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
// 或者
curl -sSL https://get.daocloud.io/docker | sh

测试安装是否成功

1
docker -v
1
2
3
4
// 启动docker
sudo systemctl start docker
// 设置开机自启动
sudo systemctl enable docker

3. 安装 Docker-compose

1
2
3
4
// 国内
curl -L https://get.daocloud.io/docker/compose/releases/download/v2.12.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
// 国外
curl -L "https://github.com/docker/compose/releases/download/2.12.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

添加可执行权限

1
chmod +x /usr/local/bin/docker-compose

查看版本

1
docker-compose --version

4. 创建 Dockerfile

通过 Dockfile 创建镜像.之后使用.

自动生成

安装 vscode 插件,搜索 docker.安装后 F1,输入 docker add.按照提示一步一步生成 Dockerfile.

手动写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用node镜像
FROM node:14.21.0-alpine
# 在容器中新建目录文件夹 egg
RUN mkdir -p /egg
# 将 /egg 设置为默认工作目录
WORKDIR /egg
# 环境变量
ENV NODE_ENV=production
# 修改时区
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone
# 将 package.json 复制默认工作目录
COPY package.json /egg/package.json
# 安装依赖
RUN npm config set register https://registry.npm.taobao.org
# 只安装dependencies的包
RUN npm --production
# 再copy代码至容器
COPY ./ /egg
# 7001端口
EXPOSE 7001
#等容器启动之后执行脚本
CMD npm start

新建dockerignore

排除一些不需要打包的文件.

5. 上传代码

不管是上传到 github,gitlab,还是 gitee.因为需要在服务器上下载.
当然如果想手动上传压缩也行,不过后期要配置自动化 CI/CD 的,那样比较麻烦.

6.下载代码到服务器

安装 git

记得安装 git,
不知道服务端怎么安装点这里
或者搜符合自己服务器版本的.

1
git clone https://github.com/xx-coding/youtube-clone-backend

7. 构建镜像

1
2
cd youtube-clone-backend
docker build -t youtube-clone-backend .

查看镜像

1
2
# 查看所有镜像
docker images

8. 创建容器

1
docker run -itd --name youtube-clone-container -p 7001:7001 youtube-clone-backend

测试是否连通

1
curl localhost:7001

注意服务器的安全组中端口开放.

查看容器

1
2
# 查看所有容器
docker ps -a

此时,还不能启动这个容器,因为我的需要连接数据库.

9. 部署 mongoDB

1
2
3
4
# 拉取镜像
docker pull mongo
# 创建mongo容器
docker run -p 27017:27017 -v ~/docker/mongo:/data/db --name docker_mongodb -d mongo
  • -v: 将宿主机~/docker/mongo 目录挂载到容器中的 /data/db 中,此目录为 MongoDB 数据存储位置.
  • -d:设置容器以守护进程方式运行
  • -p:宿主机 27017 端口映射到容器 27017 端口

创建用户

不创建好像连接后访问不了.

1
2
3
4
5
6
7
8
9
10
11
# 进入mongoDb容器
docker exec -it docker_mongodb mongosh

# 进入mongoDb
> use admin

# 创建用户名为admin, 密码为admin, 角色为root的用户;
> db.createUser({user:"admin",pwd:"admin",roles:[{role:"root",db:"admin"}]});

# 出现创建成功提示就对了,退出管理员
# > exit;

用管理员账户创建普通用户管理你的数据库.

1
2
3
4
5
6
7
# 用刚创建的管理用户登录
# mongo --port 27017 -u admin -p admin --authenticationDatabase admin
# 再创建一个普通用户
> use youtube-clone

# 普通用户账户: staff, 密码: staff, 有读写权限;
> db.createUser({user:"staff",pwd:"staff",roles:[{role:"readWrite",db:"youtube-clone"}]});

10. 将数据库容器与后端容器关联

修改后端项目中 MongoDB 的配置

1
2
3
// config.default.js
// 注意xxxxx是你的服务器IP名称,如果不想暴露可以写成环境变量
url: 'mongodb://staff:staff@xxxxxxx:27017/youtube-clone?authSource=staff',

创建自定义网络

1
docker network create youtube-net

将容器连接进网络

1
2
# 服务端容器连接网络
docker network connect youtube-net youtube-clone-container
1
2
# MongoDB连接网络
docker network connect youtube-net docker_mongodb

两个都连接到自定义网络就可以通信了.
至此.就应该可以通过 ip+端口号进行后端的接口访问了.

查看问题

1
2
# 查看执行日志
docker logs -f containerID

更简单的方法 docker-compose

其实就是把上面的指令写到文件中执行