Vue 系列
为什么用 Proxy 代替 defineProperty
Proxy
可以直接监听数组长度的变化、对象属性的添加、删除等。对于数组来说Proxy
比defineProperty
性能好Proxy
是对整个对象进行代理,而defineProperty
是对属性进行监听,在数据量大且嵌套深
的数据中会有性能问题Proxy
可以对Map/Set/WeakMap/WeakSet
等数据结构进行劫持Proxy
可以进行懒代理
,在没有用到的属性不添加observe
劫持Proxy
可以直接监听数组的变化
Vue 组件挂载流程
Vue2
:- 创建流程: 在创建元素时会调用
patch
方法,传入oldvnode
和vnode
。但是此时的oldvnode
是null
,所以就会走createEle
就会去尝试看创建的元素是否为 组件否则就走创建普通节点的过程。createComponent
会去执行init
这个hook
,在这个hook
中会继承父组件原型链的方法,并实例化一个新组件。然后调用新组件的$mount
方法挂载组件。这个$mount
的方法是将组件转化成真实节点的过程,其中会调用mountComponent
创建watcher
实例,创建组件更新流程。
jsfunction mountedComponent(vm, el) { vm.$el = el; callHook(vm, "beforeMount"); const updateComponent = () => { vm._update(vm._render()); }; // 创建一个渲染watcher,并在实例化的时候会执行一次 updateComponent 的方法,收集依赖 // _update 方法会调用 patch 方法,传入 oldvnode 和 vnode 的到真实节点,并插入到dom中 const watcher = new Watcher( vm, updateComponent, () => { // callHook(vm, "updated"); }, true ); callHook(vm, "Mounted"); }
- 更新流程: 在实例化渲染
watcher
的时候会调用组件的render
方法此时数据的Dep
就会收集这个渲染实例,当数据更新时就会将自身Dep
里面的所有watcher
实例都调用update
方法,实际上就是调用上面的updateComponent
方法重新调用render
进行patch
更新真实节点。
- 创建流程: 在创建元素时会调用
Vue3
:跟Vue2
的流程类似只不过没有了watcher
的概念而是转换成effect
收集渲染effect
Vue3 做了哪些优化
- 编译时
- 静态节点提升,将 静态节点提升至 render 函数之外,减少渲染次数
- 增加
patch flag
标记,标记处节点中有哪些地方是动态的。 - 事件函数缓存
- 运行时
diff
算法优化,在对比孩子的过程中,使用最长递增子序列
代替双指针暴力比较
- 利用编译时的
patch flag
,diff
时只对特定动态attrs
进行比较;比如只有class
是动态的那将不会进入style/text
的比较
Vue2 和 Vue3 对响应式数组处理有何不同
Vue2
中没有直接使用defineProperty
对数组进行拦截(原因是 性能差)- 重写了数组的方法,比如
push
、pop
、shift
、unshift
、splice
- 重写后的方法会触发
dep.notify()
通知watcher
更新
- 重写了数组的方法,比如
Vue3
中使用Proxy
对数组的长度
和索引
以及对能够修改数组方法
进行拦截
双向绑定原理
Vue
中实现双向绑定主要体现在 v-model
这个指令上。v-model
是一个语法糖本质上还是对 value
和 input
事件的监听和处理。但是因为我们绑定的数据已经是响应式的,所以当数据发生变化时,视图会自动更新。
Vue2 和 Vue3 响应式区别
Vue2
: 采用Object.defineProperty
对数据的每一个属性的get/set
进行劫持,在渲染时用到响应式数据会触发这个属性的get
从而将渲染watcher
放入Dep
中。在数据变化的时候触发set
并通知该属性Dep
中每一个watcher
进行更新
。 使视图发生变化Vue3
: 采用Proxy
进行数据代理,并引用effect
来保存当前的渲染函数
。触发get
是将当前的将当前的effect
与数据建立一个依赖图谱。在赋值时通过遍历这个数据的依赖图谱
重新执行渲染函数
来达到视图更新的目的。
Vue2 和 Vue3 的 diff 算法区别
- 在比较孩子时
Vue2
使用双指针双端算法
,Vue3
使用最长递增子序列
减少不必要的Dom
操作 Vue3
新增patch flag
标记能够针对性比较动态的属性Vue3
只会diff
动态节点
nextTick 原理
Vue2
: 通过降级Promise
、MutationObserve
、setImmediate
、setTimeout
实现Vue3
: 通过Promise
实现
keepAlive 原理
通过缓存组件的 vnode
实现。并通过 LRU
算法实现最大缓存数量以及失效
LRU 算法
原理是最近使用的优先插入到后面,而超出则会从头部删除
function LRU(max = 10) {
let cache = new Set();
function push(vnode) {
pop();
// 这里有个细节,就是Set中如果新加一个重复元素,会先降旧的删除,再将新元素插入到队尾
cache.add(vnode);
}
function pop() {
if (cache.size > max) {
cache.delete(keys.values().next().value);
}
}
return {
push,
pop,
};
}
讲讲 Teleport
Teleport
是一个内置组件也是一个新特性。在 Vue2
中我们一般挂载的组件都在组件内。如果想在 body
等位置挂载需要做一些额外处理。
const instance = new Com({});
instance.$mount(document.body);
它出现后就相当于一个portal
,可以让我们将组件挂载到任何地方。
讲讲 Suspense
Suspense
是一个内置组件,它允许我们定义 异步依赖
,并且可以渲染 loading
组件。在请求异步组件的时候由于要发生网络请求可能短暂无响应如果加一个loading
组件可以提升用户体验。
React
引入 fiber 是什么原因,解决了什么问题
fiber
是 react16
引入的一种概念。因为在 react
中的没法像 vue
一样做到 精确更新
,它是从 根节点
开始然后一层层比较下来。在处理大型组件树
时由于通过 递归
的方式进行这可能会出现一些性能问题 主线程占中时间过长等
fiber
通过 链表
的方式将渲染任务分割成一个个细小
的任务,并通过自行实现 requestIdleCallback
的方式来判断主线程是否繁忙,避免卡顿
。
有以下好处
- 增量渲染
- 优先级调度
- 可中断与恢复
setState 是同步还是异步
在 react18
之前只有在 事件函数回调、生命周期(componentShouldUpdate除外)
中 是异步的,其他情况下都是同步的
在 react18
之后在使用 createRoot
创建的应用都是异步处理
setState 函数做了哪些事情
- 先比较新状态和老状态是否相同
- 将新状态创建一个
update
对象,加入到fiber
中的updateQueue
队列中 - 判断当前是否需要批量更新,如果是则使用
queueMicrotask
将更新任务加入到 微任务中,在这期间如果有重复的更新任务进入并会加入到更新队列但不会再进行调度,否则则立刻执行更新任务 - 更新任务需要重新计算组件新的状态并且重新执行 组件的
render
函数获取vnode
,在通过diff
更新页面
在 React 类组件中,为什么修改状态要使用 setState 而不是用 this.state.xxx = xxx
因为 react
的更新需要将状态放入更新队列中,而 this.state.xxx = xxx
并不会将状态放入更新队列。所以导致更新无效
useState 的原理是什么,背后怎么执行的,它怎么保证一个组件中写多个 useState 不会串
- 挂载阶段
- 执行
HooksDispatcherOnMount.mountState
方法创建一个hook
对象,将这个hook
添加到当前fiber.memoizedState
单向链表的末尾tstype Hook = { memoizedState: any; // 当前 hook 的状态 baseState: any; // 当前 hook 的状态 baseUpdate: Update<any> | null; // 当前 hook 的更新 queue: UpdateQueue<any>; // 当前 hook 的更新队列 next: Hook | null; // 指向下一个 hook };
- 初始化
hook
的值,也就是传入进来的initState
的值 - 创建更新队列
- 绑定
dispatcher
函数
- 执行
- 更新阶段
- 执行
HooksDispatcherOnUpdate.updateState
方法从current fiber
中拿到memoizedState
也就是hook
链表,然后重新对照旧链表节点重新创建一个新hook
。 - 执行
新hook
中的queue
更新action
得到最新的状态 - 通知调度进行更新
- 执行
不会串是因为每执行一次 useState
都会创建一个新的 Hook
对象并插入到 fiber
的 memoizedState
链表中。 而更新时创建新链表也是一个个按照顺序取。
dispatcher
函数只有一个作用就是将 action
放入 queue
更新队列中,然后通知调度进行更新
React-Hook 为什么不能放到条件语句中
因为在 React 中 hook 会因调用顺序存放在fiber
的单向链表中,在更新时如果因为放在条件语句中导致hook
的顺序被打乱,从而取值也会错误。
React18 新特性
- 推出使用
createRoot
创建应用并默认开启并发更新 - state 更新时默认进行合并
- 推出新的
hook
函数
React class 组件生命周期
componentWillMount
- 注意这个生命周期在
react17
后删除
- 注意这个生命周期在
componentDidMount
componentWillReceiveProps
- 注意这个生命周期在
react17
后删除
- 注意这个生命周期在
shouldComponentUpdate
componentWillUpdate
- 注意这个生命周期在
react17
后删除
- 注意这个生命周期在
componentDidUpdate
componentWillUnmount
因为在 react17
后会开启并发渲染的模式,所以一个组件组件可能会开始挂载|更新
过程,但在完成之前被打断。等待优先级高的任务执行完毕后在重新执行挂载或更新
。这意味这些生命周期可能会被调用多次
。
类组件和函数组件区别
- 类中可以使用
this
访问实例,函数中不能使用this
- 类组件中可以访问
this.state
函数组件中不能访问this.state
- 在没有
hooks
之前函数组件没有自己的状态
React 合成事件
React
里的事件,例如 onClick
等,并不是原生事件,而是由原生事件合成的 React
事件。主要是为了跨平台兼容,抹平不同浏览器的差异
有可能会衍生出以下几个问题
我们写的事件是绑定在
dom
上么,如果不是绑定在哪里?v16 绑定在
document
,v17 在rootNode
为什么我们的事件不能绑定给组件?
为什么我们的事件手动绑定
this
(不是箭头函数的情况)因为 jsx 会转化成
jsReact.createElement("button", { onClick: this.handleClick }, "click me"); //此时 this 会指向进行默认绑定,一般会指向 window, //而 class 中会指向 undefined,所以需要绑定。
为什么不能用
return false
来阻止事件的默认行为?实际上
react
的大多数事件都是通过window.addEventListener
来进行监听。所以需要使用e.preventDefault
来阻止默认行为react
怎么通过dom
元素,找到与之对应的fiber
对象的?React
在创建真实DOM
的时候会通过一个key
将fiber
绑定在DOM
上并且fiber
对象用stateNode
指向了当前的dom
元素onClick
是在冒泡阶段绑定的? 那么onClickCapture
就是在事件捕获阶段绑定的吗?这个捕获阶段并不是类似
dom
中的捕获,只是在合成事件中React
从发出事件的Dom
开始,向hostComponent
的元素收集这类事件。并把Capture
放在事件队列的头部,普通事件放在尾部。
React 优化
- 对于一些不影响页面的数据,但是需要实时获取的数据可以使用
useRef
代替useState
- 合理使用
React.memo/shouldComponentUpdate
对组件进行缓存 - 合理使用
React.lazy/Suspense
进行异步组件加载
useRef 和 useImperativeHandle 区别
如果要绑定获取组件或者元素实例,需要使用 React.forwardRef()
包裹,这时候配合 forward
就可以拿到对应的 ref
内容。 但是如果内部元素不想将整个实例暴露给用户或者是函数组件因为没有示例
可以使用 useImperativeHandle
进行选择暴露
useLayoutEffect 和 useEffect 的区别
useLayoutEffect
会在DOM 更新后,浏览器绘制前执行
,这时候可以对 DOM 进行操作来避免屏幕闪烁useEffect
会在DOM 更新后浏览器绘制后执行
,通常可以在这个时机发起异步请求,或者是跟 DOM 不相关的操作
React.memo / PureComponent / useMemo 的区别
React.memo
作用于函数组件,可以自定义规则让组件是否进行缓存PureComponent
作用于类组件,React.PureComponent
内部已经实现了shouldComponentUpdate
方法,自动进行props
和state
的浅层比较useMemo
作用于函数组件,主要目的是为了返回一个值
React DOM diff
React DOM diff 主要采取三种策略
tree
层级- 在比较时不做跨层级比较,只比较同层起,复杂度从
O(n3)
降低到O(n)
- 在比较时不做跨层级比较,只比较同层起,复杂度从
component
层级- 如果是同一类型的组件,直接进行内部 diff 比较
- 如果不是同一类型的组件,直接进行替换
element
层级- 首先会从头开始找出是否能够复用节点,可以复用则进行复用,不能复用则终止遍历
- 遍历旧节点以
key 或者 index
作为索引,建立一个Map
- 循环新节点通过
key 或者 index
查找旧节点Map
是否有匹配的元素,如果有记录其下标lastIndex
,没有则创建这个节点
- 如果能够复用的旧节点下标比
lastIndex
下标小,则将旧节点进行移动操作
,反之则更新lastIndex
- 最后将
旧节点Map
中的节点进行删除
,diff
结束
// 1.加key
<div key='1'>1</div> <div key='1'>1</div>
<div key='2'>2</div> <div key='3'>3</div>
<div key='3'>3</div> ========> <div key='2'>2</div>
<div key='4'>4</div> <div key='5'>5</div>
<div key='5'>5</div> <div key='6'>6</div>
// 操作:节点2移动至下标为2的位置,新增节点6至下标为4的位置,删除节点4。
// 2.不加key
<div>1</div> <div>1</div>
<div>2</div> <div>3</div>
<div>3</div> ========> <div>2</div>
<div>4</div> <div>5</div>
<div>5</div> <div>6</div>
// 操作:修改第1个到第5个节点的innerText
React Fiber 的协调和提交做了哪些事情
协调阶段
- 根据
current fiber tree
创建workInProgress fiber tree
,并根据不同点打上effectFlag
(这期间就要进行diff
,在这期间还会更新state
和props
。最终根据diff
的结果生成新的fiber tree
)
- 根据
提交阶段
- 应用副作用
- 调用生命周期
- 清理以及
current fiber tree
指向workInProgress fiber tree