Skip to content
On this page

Vue 系列

为什么用 Proxy 代替 defineProperty

  1. Proxy 可以直接监听数组长度的变化、对象属性的添加、删除等。对于数组来说 ProxydefineProperty 性能好
  2. Proxy 是对整个对象进行代理,而 defineProperty 是对属性进行监听,在数据量大且嵌套深的数据中会有性能问题
  3. Proxy 可以对 Map/Set/WeakMap/WeakSet 等数据结构进行劫持
  4. Proxy 可以进行 懒代理,在没有用到的属性不添加 observe 劫持
  5. Proxy 可以直接监听数组的变化

Vue 组件挂载流程

  • Vue2:
    1. 创建流程: 在创建元素时会调用 patch 方法,传入oldvnodevnode。但是此时的 oldvnodenull,所以就会走 createEle 就会去尝试看创建的元素是否为 组件否则就走创建普通节点的过程。createComponent会去执行 init 这个 hook ,在这个 hook 中会继承父组件原型链的方法,并实例化一个新组件。然后调用新组件的 $mount 方法挂载组件。这个 $mount 的方法是将组件转化成真实节点的过程,其中会调用 mountComponent创建 watcher 实例,创建组件更新流程。
    js
    function 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");
    }
    1. 更新流程: 在实例化渲染 watcher 的时候会调用组件的 render 方法此时数据的 Dep 就会收集这个渲染实例,当数据更新时就会将自身 Dep 里面的所有 watcher 实例都调用 update 方法,实际上就是调用上面的 updateComponent 方法重新调用 render 进行 patch 更新真实节点。
  • Vue3:跟 Vue2 的流程类似只不过没有了 watcher 的概念而是转换成 effect 收集渲染effect

Vue3 做了哪些优化

  • 编译时
    1. 静态节点提升,将 静态节点提升至 render 函数之外,减少渲染次数
    2. 增加 patch flag 标记,标记处节点中有哪些地方是动态的。
    3. 事件函数缓存
  • 运行时
    1. diff 算法优化,在对比孩子的过程中,使用 最长递增子序列 代替 双指针暴力比较
    2. 利用编译时的 patch flag ,diff时只对特定动态 attrs 进行比较;比如只有class是动态的那将不会进入 style/text 的比较

Vue2 和 Vue3 对响应式数组处理有何不同

  • Vue2 中没有直接使用 defineProperty 对数组进行拦截(原因是 性能差)
    1. 重写了数组的方法,比如 pushpopshiftunshiftsplice
    2. 重写后的方法会触发 dep.notify() 通知 watcher 更新
  • Vue3 中使用 Proxy 对数组的 长度索引以及对能够修改数组方法进行拦截

双向绑定原理

Vue中实现双向绑定主要体现在 v-model 这个指令上。v-model是一个语法糖本质上还是对 valueinput 事件的监听和处理。但是因为我们绑定的数据已经是响应式的,所以当数据发生变化时,视图会自动更新。

Vue2 和 Vue3 响应式区别

  • Vue2: 采用 Object.defineProperty 对数据的每一个属性的 get/set 进行劫持,在渲染时用到响应式数据会触发这个属性的 get 从而将 渲染watcher 放入 Dep 中。在数据变化的时候触发 set 并通知该属性 Dep 中每一个 watcher 进行 更新。 使视图发生变化
  • Vue3: 采用 Proxy 进行数据代理,并引用 effect 来保存当前的 渲染函数。触发 get 是将当前的将当前的 effect 与数据建立一个依赖图谱。在赋值时通过遍历这个数据的 依赖图谱 重新执行渲染函数来达到视图更新的目的。

Vue2 和 Vue3 的 diff 算法区别

  1. 在比较孩子时 Vue2 使用双指针双端算法,Vue3 使用最长递增子序列减少不必要的Dom操作
  2. Vue3 新增 patch flag 标记能够针对性比较动态的属性
  3. Vue3 只会 diff 动态节点

nextTick 原理

  • Vue2: 通过降级 PromiseMutationObservesetImmediatesetTimeout实现
  • Vue3: 通过 Promise 实现

keepAlive 原理

通过缓存组件的 vnode实现。并通过 LRU算法实现最大缓存数量以及失效

LRU 算法

原理是最近使用的优先插入到后面,而超出则会从头部删除

js
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 等位置挂载需要做一些额外处理。

js
const instance = new Com({});
instance.$mount(document.body);

它出现后就相当于一个portal,可以让我们将组件挂载到任何地方。

讲讲 Suspense

Suspense 是一个内置组件,它允许我们定义 异步依赖,并且可以渲染 loading 组件。在请求异步组件的时候由于要发生网络请求可能短暂无响应如果加一个loading组件可以提升用户体验。

React

引入 fiber 是什么原因,解决了什么问题

fiberreact16 引入的一种概念。因为在 react 中的没法像 vue 一样做到 精确更新,它是从 根节点 开始然后一层层比较下来。在处理大型组件树 时由于通过 递归 的方式进行这可能会出现一些性能问题 主线程占中时间过长等

fiber 通过 链表 的方式将渲染任务分割成一个个细小的任务,并通过自行实现 requestIdleCallback 的方式来判断主线程是否繁忙,避免卡顿

有以下好处

  1. 增量渲染
  2. 优先级调度
  3. 可中断与恢复

setState 是同步还是异步

react18 之前只有在 事件函数回调、生命周期(componentShouldUpdate除外)中 是异步的,其他情况下都是同步的

react18 之后在使用 createRoot 创建的应用都是异步处理

setState 函数做了哪些事情

  1. 先比较新状态和老状态是否相同
  2. 将新状态创建一个update对象,加入到 fiber 中的 updateQueue 队列中
  3. 判断当前是否需要批量更新,如果是则使用 queueMicrotask 将更新任务加入到 微任务中,在这期间如果有重复的更新任务进入并会加入到更新队列但不会再进行调度,否则则立刻执行更新任务
  4. 更新任务需要重新计算组件新的状态并且重新执行 组件的 render 函数获取 vnode ,在通过 diff 更新页面

在 React 类组件中,为什么修改状态要使用 setState 而不是用 this.state.xxx = xxx

因为 react 的更新需要将状态放入更新队列中,而 this.state.xxx = xxx 并不会将状态放入更新队列。所以导致更新无效

useState 的原理是什么,背后怎么执行的,它怎么保证一个组件中写多个 useState 不会串

  • 挂载阶段
    1. 执行 HooksDispatcherOnMount.mountState 方法创建一个 hook 对象,将这个 hook 添加到当前 fiber.memoizedState 单向链表的末尾
      ts
      type Hook = {
        memoizedState: any; // 当前 hook 的状态
        baseState: any; // 当前 hook 的状态
        baseUpdate: Update<any> | null; // 当前 hook 的更新
        queue: UpdateQueue<any>; // 当前 hook 的更新队列
        next: Hook | null; // 指向下一个 hook
      };
    2. 初始化 hook 的值,也就是传入进来的 initState 的值
    3. 创建更新队列
    4. 绑定 dispatcher 函数
  • 更新阶段
    1. 执行 HooksDispatcherOnUpdate.updateState 方法从 current fiber 中拿到 memoizedState 也就是 hook 链表,然后重新对照旧链表节点重新创建一个新hook
    2. 执行新hook 中的queue 更新 action 得到最新的状态
    3. 通知调度进行更新

不会串是因为每执行一次 useState 都会创建一个新的 Hook 对象并插入到 fibermemoizedState 链表中。 而更新时创建新链表也是一个个按照顺序取。

dispatcher 函数只有一个作用就是将 action 放入 queue 更新队列中,然后通知调度进行更新

React-Hook 为什么不能放到条件语句中

因为在 React 中 hook 会因调用顺序存放在fiber 的单向链表中,在更新时如果因为放在条件语句中导致hook 的顺序被打乱,从而取值也会错误。

React18 新特性

  1. 推出使用 createRoot 创建应用并默认开启并发更新
  2. state 更新时默认进行合并
  3. 推出新的 hook 函数

React class 组件生命周期

  • componentWillMount
    • 注意这个生命周期在 react17 后删除
  • componentDidMount
  • componentWillReceiveProps
    • 注意这个生命周期在 react17 后删除
  • shouldComponentUpdate
  • componentWillUpdate
    • 注意这个生命周期在 react17 后删除
  • componentDidUpdate
  • componentWillUnmount

因为在 react17 后会开启并发渲染的模式,所以一个组件组件可能会开始挂载|更新过程,但在完成之前被打断。等待优先级高的任务执行完毕后在重新执行挂载或更新。这意味这些生命周期可能会被调用多次

类组件和函数组件区别

  1. 类中可以使用 this 访问实例,函数中不能使用 this
  2. 类组件中可以访问 this.state 函数组件中不能访问 this.state
  3. 在没有 hooks 之前函数组件没有自己的状态

React 合成事件

React 里的事件,例如 onClick 等,并不是原生事件,而是由原生事件合成的 React 事件。主要是为了跨平台兼容,抹平不同浏览器的差异

有可能会衍生出以下几个问题

  1. 我们写的事件是绑定在 dom 上么,如果不是绑定在哪里?

    v16 绑定在 document,v17 在 rootNode

  2. 为什么我们的事件不能绑定给组件?

  3. 为什么我们的事件手动绑定 this(不是箭头函数的情况)

    因为 jsx 会转化成

    js
    React.createElement("button", { onClick: this.handleClick }, "click me");
    //此时 this 会指向进行默认绑定,一般会指向 window,
    //而 class 中会指向 undefined,所以需要绑定。
  4. 为什么不能用 return false 来阻止事件的默认行为?

    实际上 react 的大多数事件都是通过 window.addEventListener来进行监听。所以需要使用 e.preventDefault来阻止默认行为

  5. react 怎么通过 dom 元素,找到与之对应的 fiber 对象的?

    React 在创建真实 DOM 的时候会通过一个 keyfiber 绑定在 DOM 上并且 fiber 对象用 stateNode 指向了当前的 dom 元素

  6. onClick 是在冒泡阶段绑定的? 那么 onClickCapture 就是在事件捕获阶段绑定的吗?

    这个捕获阶段并不是类似 dom 中的捕获,只是在合成事件中 React 从发出事件的 Dom 开始,向 hostComponent 的元素收集这类事件。并把Capture 放在事件队列的头部,普通事件放在尾部。

React 优化

  1. 对于一些不影响页面的数据,但是需要实时获取的数据可以使用 useRef 代替 useState
  2. 合理使用 React.memo/shouldComponentUpdate 对组件进行缓存
  3. 合理使用 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 方法,自动进行 propsstate 的浅层比较
  • useMemo 作用于函数组件,主要目的是为了返回一个值

React DOM diff

React DOM diff 主要采取三种策略

  1. tree 层级

    • 在比较时不做跨层级比较,只比较同层起,复杂度从 O(n3) 降低到 O(n)
  2. component 层级

    • 如果是同一类型的组件,直接进行内部 diff 比较
    • 如果不是同一类型的组件,直接进行替换
  3. element 层级

    • 首先会从头开始找出是否能够复用节点,可以复用则进行复用,不能复用则终止遍历
    • 遍历旧节点以 key 或者 index 作为索引,建立一个 Map
    • 循环新节点通过 key 或者 index 查找 旧节点Map 是否有匹配的元素,如果有记录其下标lastIndex,没有则创建这个节点
    • 如果能够复用的旧节点下标比 lastIndex 下标小,则将旧节点进行移动操作,反之则更新 lastIndex
    • 最后将 旧节点Map 中的节点进行删除, diff 结束
js
// 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 的协调和提交做了哪些事情

  1. 协调阶段

    • 根据 current fiber tree 创建 workInProgress fiber tree,并根据不同点打上 effectFlag (这期间就要进行 diff,在这期间还会更新 stateprops。最终根据 diff 的结果生成新的 fiber tree)
  2. 提交阶段

    • 应用副作用
    • 调用生命周期
    • 清理以及 current fiber tree 指向 workInProgress fiber tree