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后删除
- 注意这个生命周期在
componentDidMountcomponentWillReceiveProps- 注意这个生命周期在
react17后删除
- 注意这个生命周期在
shouldComponentUpdatecomponentWillUpdate- 注意这个生命周期在
react17后删除
- 注意这个生命周期在
componentDidUpdatecomponentWillUnmount
因为在 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个节点的innerTextReact Fiber 的协调和提交做了哪些事情
协调阶段
- 根据
current fiber tree创建workInProgress fiber tree,并根据不同点打上effectFlag(这期间就要进行diff,在这期间还会更新state和props。最终根据diff的结果生成新的fiber tree)
- 根据
提交阶段
- 应用副作用
- 调用生命周期
- 清理以及
current fiber tree指向workInProgress fiber tree