- TS 和 JS 的区别是什么?有什么优势?
语法层面:TypeScript = JavaScript + Type(TS 是 JS 的超集)
执行环境层面:浏览器、Node.js 可以直接执行 JS,但不能执行 TS(Deno 可以执行 TS)
编译层面:TS 有编译阶段,JS 没有编译阶段(只有转译阶段「webpack打包 ES6->ES5」和link阶段「ESLink:提示代码写的不规范」)
编写层面:TS 更难写一些,但是类型更安全
文档层面:TS 的代码写出来就是文档,IDE 可以完美提示,JS 的提示主要靠 TS - any、unknown、never 的区别是什么?
any V.S. unknown
两者都是顶级类型(top type),任何类型的值都可以赋值给顶级类型变量(类型值可向上赋值):
let foo:any = 123; // 不报错
let bar:unknown = 123; // 不报错
但是 unknown 比 any 的类型检查更严格,any 什么检查都不做,unknown 要求先收窄类型:
const value:unknown = "Hello World";
const someString:string = value;
// 报错:Type 'unknown' is not assignable to type 'string'.(2322)
const value:unknown = "Hello World";
const someString:string = value as string; // 不报错 使用了类型收窄
如果改成 any,基本在哪都不报错。所以能用 unknown 就优先用 unknown,类型更安全一些
never
never 是底类型,表示不应该出现的类型,这里有一个尤雨溪给出的例子:
interface A {
type:'a'
}
interface B {
type:'b'
}
type All = A | B
function handleValue(val:All){
switch(val.type){
case 'a':
// 这里 value 被收窄为 A
break
case 'b':
// val 在这里是 B
break
default:
// val 在这里是 never
const exhaustiveCheck: never = val
break
}
}
- type 和 interface 的区别是什么?
组合方式:interface 使用 extends 来实现继承,type 使用 & 来实现联合类型
扩展方式:interface 可以重复声明用来扩展,type 一个类型只能声明一次
范围不同:type 适用于基本类型,interface 一般不行
命名方式:interface 会创建新的类型名,type 只是创建类型别名,并没有新创建类型 - TS 工具类型 Partial、Required、Readonly、Exclude、Extract、Omit、ReturnType 的作用和实现?
Partial 部分类型
interface User{
id:string;
name:string
}
const user:Partial{ // id 可不写
name:'Evelyn'
}
Required 必填类型
interface User{
id?:string;
name:string;
}
const user:Required= { // id为必填
id:'111';
name:'Evelyn'
}
Readonly 只读类型
interface User{
id?:string;
name:string;
}
const user:Readonly= { // id为必填
id:'111';
name:'Evelyn'
}
user.id = '222' //报错 Readonly 只读类型
Exclude 排除类型:后接基本类型
type Dir = '东'|'南'|'西'|'北'
type Dir2 = Exclude//排除 '北'
Extract 提取类型
type Dir = '东'|'南'|'西'|'北'
type Dir3 = Extract//只要 '东' '西'
Pick / Omit 排除 key 类型:Omit 后接对象类型
interface User{
id:string;
name:string;
age:number
}
type God = Pick// 只要 id name
type God1 = Omit//只是不要 age 属性
ReturnType 返回值类型
function f(a:number,b:number){
return 'a+b'
}
type A = ReturnType
注:Map 是不限制类型的 Record,Record 是限制类型的 Map
1. 虚拟 DOM 的原理是什么?
(1)是什么?虚拟 DOM 就是虚拟节点,React 用 JS 对象来模拟 DOM 节点,然后将其渲染成真实的 DOM 节点
(2)怎么做?
第一步是模拟:
用JSX语法写出来的 div 其实就是一个虚拟节点
hi
这代码会得到这样一个对象:用一个对象模拟节点
对象有三个属性:
- tag:表示什么标签
- props:表示标签上有哪些属性
- children:表示有哪些子标签 / 子文本
{
tag:'div', // 表示什么标签
props:{ // 表示标签上有哪些属性
id:'x'
},
children:[ // 表示有哪些子标签/子文本
{
tag:'span',
props:{
className:'red'
}
children:[
'hi'
]
}
]
}
能做到这一点是因为 JSX 语法会被转译为 createElement 函数调用(也叫 h 函数),通过 Babel 或 Webpack 的loader来实现的如下:
React.createElement("div",{id:"x"},
React.createElement("span",{className:"red"},"hi")
)
第二步是将虚拟节点渲染为真实节点
function render(vdom){
// 如果是字符串或者数字,创建一个文本节点
if(typeof vdom === 'string' || typeof vdom === 'number'){
return document.createTextNode(vdom)
}
const {tag,props,children} = vdom
// 创建真实 DOM(标签)
const element = document.createElement(tag)
// 设置属性
setProps(element,props)
// 遍历子节点,并获取创建真实 DOM,插入到当前节点
children.map(render)
.forEach(element.appendChild.bind(element))
// 虚拟 DOM 中缓存真实 DOM 节点,虚拟的和真实的建立关联
vdom.dom = element
// 返回 DOM 节点
return element
}
function setProps
function setProp
注意:如果节点发生变化,并不会直接把新虚拟节点渲染到真实节点,而是先经过 diff 算法得到一个 patch 再更新到真实节点上
(3)解决了什么问题
- DOM 操作性能问题,通过虚拟 DOM 和 diff 算法减少不必要的 DOM 操作,保证性能不太差
- DOM 操作不方便问题,以前各种 DOM API 要记,现在只有 setState
(4)优点
- 为 React 带来了跨平台能力,因为虚拟节点除了渲染为真实节点,还可以渲染为其他东西
- 让 DOM 操作的整体性能更好,能(通过 diff)减少不必要的 DOM 操作
(5)缺点
①性能要求极高的地方,还是得用真实 DOM 操作
②React 为虚拟 DOM 创造了合成事件,跟原生 DOM 事件不太一样,工作中要额外注意
- 所有 React 事件都绑定到根元素,自动实现事件委托
- 如果混用合成时间和原声 DOM 事件,有可能会出 bug
2. React 或 Vue 的 DOM diff 算法是怎样的?
(1)是什么
DOM diff 就是对比两颗虚拟 DOM 树的算法,当组件变化时,会 render 出一个新的虚拟 DOM,diff 算法对比新旧虚拟 DOM 之后,得到一个 patch,然后 React 用 patch 来更新真实的 DOM
(2)怎么做
首先,对比两颗树的根节点
①如果根节点的类型改变了,比如 div 变成了 p,那么直接认为整棵树都变了,不再对比子节点。此时直接删除对应的真实 DOM 树,创建新的真实 DOM 树
②如果根节点的类型没变,就看看属性变了没有
a. 如果没变,就保留对应的真实节点
b. 如果变了,就只更新该节点的属性,不重新创建节点
i. 更新 style 时,如果多个 CSS 属性只有一个改变了,那么 React 只更新改变的
然后同时遍历两棵树的子节点,每个节点的对比过程同上,不过存在如下两种情况:
①情况一
- A
- B
- A
- B
- C
React 一次对比 A-A、B-B、空-C,发现 C 是新增的,最终会创建真实 C 节点插入页面
②情况二
- B
- C
- A
- B
- C
React 对比 B-A,会删除 B 文本新建 A 文本;对比 C-B,会删除 C 文本,新建 B 文本;(注意:并不是边对比边删除新建,而是把操作汇总到 patch 里再进行 DOM 操作)对比 空-C,会新建 C 文本
由此发现其实只需要创建 A 文本,保留 B 和 C 即可,为什么 React 做不到呢?
因为 React 需要加 key 才能做到:
- B
- C
- A
- B
- C
React 先对比 key 发现 key 只新增了一个,于是保留 b 和 c,新建 a
以上是 React 的 diff 算法,Vue 的 diff 算法是「双端交叉对比」算法
假设有旧的 Vnode 数组和新的 Vnode数组这两个数组,有四个变量充当指针分别指向两个数组的头尾,重复下面的对比过程,知道两个数组中任一数组的头指针超过尾指针,循环结束
- 头头对比:对比两个数组的头部,如果找到,把新节点 patch 到旧节点,头指针后移
- 尾尾对比:对比两个数组的尾部,如果找到,把新节点 patch 到旧节点,尾指针前移
- 旧尾新头对比:交叉对比,旧尾新头,如果找到,把新节点 patch 到旧节点,旧尾针前移,新头指针后移
- 旧头新尾对比:交叉对比,旧头新尾,如果找到,把新节点 patch 到旧节点,新尾指针前移,旧头指针后移
- 利用 key 对比:用新指针对应节点的 key 去旧数组寻找对应的节点,这里分三种情况,当没有对应的 key,那么创建新的节点,如果有 key 并且是相同的节点,把新节点 patch 到旧节点,如果有 key 但是不是相同的节点,则创建新节点
循环结束后,两个数组中可能存在未遍历完的情况,所以循环结束后:
- 先对比旧数组的头尾指针,如果旧数组遍历完了(可能新数组没遍历完,有漏添加的问题),添加新数组中漏掉的节点
- 再对比新数组的头尾指针,如果新数组遍历完了(可能旧数组没遍历完,有漏删除的问题),删除旧数组中漏掉的节点
3. React DOM diff 和 Vue DOM diff 的区别?
① React 是从左向右遍历对比,Vue 是双端交叉对比
② React 至少需要维护三个变量(lastPlacedIndex、newIdx、nextOldFiber),Vue 则至少需要维护四个变量
③ Vue整体效率比 React 更高,举例说明:假设有 N 个子节点,我们只是把最后子节点移到第一个,那么
- React 需要进行借助 Map 进行 key 搜索找到匹配项,然后复用节点
- Vue 会发现移动,直接复用节点
4. React 有哪些生命周期钩子函数?数据请求放在哪个钩子里?
Mounting(挂载时):
- constructor() -- 类的构造函数
- static getDerivedStateFromProps()
- render()
- componentDidMount()
Updating(更新时):
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
Unmounting(卸载时):
- componentWillUnmount()
Error Handing(错误处理):
- static getDerivedStateFromError()
- componentDidCatch()
默认是 class 组件, hooks 函数组件是没有生命周期的
总的来说:
- 挂载时调用 constructor,更新时不调用
- 更新时调用shouldComponentUpdate 和 getSnapshotBeforeUpdate,挂载时不调用
- shouldComponentUpdate 在 render 之前调用,getSnapshotBeforeUpdate 在 render 后调用
- 请求放在 componentDidMount 里
请求不能放在 constructor 中因为它在 SSR 服务器端被调用,拿不到数据,更新阶段的 5 个生命周期会在每一次更新的时候都会调用,可能会触发多余的或无限调用,卸载时发送请求显然没什么意义,故数据请求只能放在 componentDidMount
5. React 如何实现组件间通信
(1)父子间通信:props + 函数(父亲给儿子传数据直接传 props,儿子要给父亲传东西就调用父亲传给他的函数)
(2)爷孙组件通信:两层父子通信或者使用 Context.Provider 和 Context.Consumer
(3)任意组件通信:其实就变成了状态管理了,那数据放到集中的地方
- Redux
- Mobx
- Recoil :局部
6. 你如何理解 Redux?
Redux 是一个状态管理库 / 状态容器
核心概念有:
- state:用来存放状态
- Action:对数据的改变 Action = type + payload荷载
- Reducer:是一个函数,传一个旧的 state ,(state 和 Action)产生一个新的 state
- Dispatch:后面接 Action,用来派发
- Middleware
Redux 和 ReactRedux 配合使用,关于 ReactRedux 有三个核心概念:
- connect:接受两次参数 connect()(Component) 其作用是把 Component 和 store 关联起来
- mapStateToProps
- mapDispatchToProps
常见的中间件:redux-thunk redux-promise
后续补充..........
7. 什么是高阶组件 HOC?
参数是组件,返回值也是组件的函数
- React.forwardRef
函数组件中不支持 ref,没办法去生命一个 ref 去引用button,函数组件没有生命周期,所以没办法持久化的保存对 button 的引用
function FancyButton(props) {
return (
);
}
FancyButton
使用React.forwardRef
来获取传递给它的ref
,然后转发到它渲染的 DOMbutton
:
函数组件外面使用 forwardRef(把组件传给函数)
const FancyButton = React.forwardRef((props, ref) => (
));
// 你可以直接获取 DOM button 的 ref:
const ref = React.createRef();
Click me! ;
- ReactRedux 的 connect
- ReactRouter 的 withRouter
8. React Hooks 如何模拟组件生命周期?
- 模拟 componentDidMount:使用 useEffect 时,第二个参数为空数组
- 模拟 componentDidUpdate:使用 useEffect 时,不加第二个参数,或者是在第二个参数的数组里加上要监听的变量
- 模拟 componentWillUnmount:在 did mount 后面写 return 函数
代码示例如下:代码运行
import { useEffect, useRef, useState } from "react";
import "./styles.css";
export default function App() {
const [visible, setNextVisible] = useState(true);
const onClick = () => {
setNextVisible(!visible);
};
return (
Hello CodeSandbox
{visible ? : null}
);
}
function Evelyn(props) {
const [n, setNextN] = useState(0);
const first = useRef(true);
// 模拟 componentDidUpdate,第二个参数为要监听的数据,监听 n 第二个参数是[n]
// 若想监听所有数据,直接第二个参数为空即可
useEffect(() => {
if (first.current === true) {
return;
}
console.log("did update"); //挂载的时候不调用
});
// 模拟 componentDidMount,第二个参数为空数组
useEffect(() => {
console.log("did mount");
first.current = false; //第一次调用为挂载,其值置为 false
// 模拟 componentWillUnmount,return 为当前组件消失的时候执行
return () => {
console.log("did unmount");
};
}, []);
const onClick = () => {
setNextN(n + 1);
};
return (
Evelyn
);
}