# vite 基本原理

// index.html
<div id="app"></div>
<script type="module">
  import { createApp } from 'vue'
  import Main from './Main.vue'

  createApp(Main).mount('#app')
</script>
1
2
3
4
5
6
7
8

Vite 会在本地帮你启动一个服务器,当浏览器读取到这个 html 文件之后,会在执行到 import 的时候才去向服务端发送 Main.vue 模块的请求,Vite 此时在利用内部的一系列黑魔法,包括 Vue 的 template 解析,代码的编译等等,解析成浏览器可以执行的 js 文件返回到浏览器端。

这就保证了只有在真正使用到这个模块的时候,浏览器才会请求并且解析这个模块,最大程度的做到了按需加载。

# 依赖预编译

依赖预编译,其实是 Vite 2.0 在为用户启动开发服务器之前,先用 esbuild 把检测到的依赖预先构建了一遍。

当你用 import { debounce } from 'lodash' 导入一个命名函数的时候,可能你理想中的场景就是浏览器去下载只包含这个函数的文件。但其实没那么理想,debounce 函数的模块内部又依赖了很多其他函数,形成了一个依赖图。

当浏览器请求 debounce 的模块时,又会发现内部有 2 个 import,再这样延伸下去,这个函数内部竟然带来了 600 次请求,耗时会在 1s 左右。

这当然是不可接受的,于是尤老师想了个折中的办法,正好利用 Esbuild (opens new window) 接近无敌的构建速度,让你在没有感知的情况下在启动的时候预先帮你把 debounce 所用到的所有内部模块全部打包成一个传统的 js bundle

Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

大致过程:在 httpServer.listen 启动开发服务器之前,会进行依赖预构建步骤,根据本次运行的入口文件来扫描其中的依赖,之后再根据分析出来的依赖,使用 ESbuild 把它们打包成单文件的 bunble,之后在浏览器请求相关模块时,返回这个预构建好的模块

在预构建这个步骤中,还会对 CommonJS 模块进行分析,方便后面需要统一处理成浏览器可以执行的 ES Module

Vite Server 启动阶段,在 server.listen 的回调中执行 runOptimize 逻辑,进入预构建阶段。

runOptimize 中调用 optimizeDeps,内部调用 esbuild 进行构建, 并往 esbuild 里面传入自定义的 scan 插件,esbuild 构建过程中进行依赖分析,并将依赖项赋给 deps

拿到 deps 后打印出上述的终端 log,第一次预构建结束。

函数调用流程如下:

startServer -> runOptimize -> optimizeDeps -> scanImports -> esbuild.build
1

Vite 预构建并不只有在服务启动的时候进行,在请求进入的时候也有可能触发预构建,也就是说,预构建的行为不只是在最开始触发一次,在浏览器访问项目的时候有可能再次触发,甚至是多次触发

简单来说,当浏览器发起请求时,请求进入 Vite 服务器中,首先是执行一系列的插件,其中就会在比较靠前的位置走到 resolvePlugin,这个插件中分析项目中的依赖关系,如果发现了有依赖没有被预构建,那么会执行 _registerMissingImport 将这个依赖进行预构建,并重启 Vite Server。

_registerMissingImport 调用之后会进行二次预构建,但不是立即执行,相当于每隔 100 ms 批量收集一次然后一起构建,实际上有一个节流的过程,这样一来不用对每个依赖都调用 optimizeDeps ,也能提高预构建的效率,属于 Vite 里面细节性的优化,和 Vue 里面的nextTick 批量更新有异曲同工之妙。

当通过动态 import 的依赖多了之后,会非常影响构建性能,这种场景下可以用 vite-plugin-optimize-persist 这个插件进行自动优化

# vue3 如何减少源码体积?

首先,移除一些冷门的 feature(比如 filter、inline-template 等);

其次,引入 tree-shaking 的技术,减少打包体积。

# Tree shaking?

Treeshaking 是一个术语,通常用于描述移除JavaScript (opens new window)上下文中的未引用代码(dead-code),就像一棵大树,将那些无用的叶子都摇掉。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。

# Tree shaking 的原理

tree-shaking 依赖 ES2015 模块语法的静态结构(即 import 和 export),通过编译阶段的静态分析,找到没有引入的模块并打上标记

举个例子,一个 math 模块定义了 2 个方法 square(x) 和 cube(x) :

export function square(x) {
  return x * x
}
export function cube(x) {
  return x * x * x
}
1
2
3
4
5
6

我们在这个模块外面只引入了 cube 方法:

import { cube } from './math.js'
// do something with cube
1
2

最终 math 模块会被 webpack 打包生成如下代码:

/* 1 */
/***/ ;(function(module, __webpack_exports__, __webpack_require__) {
  'use strict'
  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__['a'] = cube
  function square(x) {
    return x * x
  }
  function cube(x) {
    return x * x * x
  }
})
1
2
3
4
5
6
7
8
9
10
11
12

可以看到,未被引入的 square 模块被标记了, 然后压缩阶段会利用例如 uglify-js、terser 等压缩工具真正地删除这些没有用到的代码

也就是说,利用 tree-shaking 技术,如果你在项目中没有引入 Transition、KeepAlive 等组件,那么它们对应的代码就不会打包,这样也就间接达到了减少项目引入的 Vue.js 包体积的目的。

# Tree shaking 与 按需引入的区别?

按需引入是在 babel 编译过程中,其本质是在 babel 编译阶段将部分代码做了替换

Tree shaking 是在 webpack 打包阶段,移除 JavaScript 上下文中的未引用代码

# vue3 使用 Proxy 代替 Object.defineProperty 的好处

vue2 中 Object.defineProperty 的缺陷:

  1. 无法原生监听数组的变化,需要特殊处理
  2. 必须遍历对象的每个属性(当示例初始化的时,Object.definePropety 是从 data 的根节点遍历到末节点。一次性遍历全部)
  3. 无法监听属性的新增删除操作(vue 提供 Vue.set Vue.delete API,原因就是因为 Object.definePropety 无法监听新增删除操作)

vue3 中 proxy 的优点

proxy 劫持的是整个对象,对于对象的属性的增加和删除都能检测到。

但要注意的是,Proxy API 并不能监听到内部深层次的对象变化,因此 Vue.js 3.0 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归,这样无疑也在很大程度上提升了性能.

到目前为止,各大主流移动端浏览器,PC 端浏览器,包括新一代 window 产品内置 Microsoft Edge 都满足对于 Vue3 的支持。其次,若真的需要满足支持 IE 及极少数低版本浏览器,还可以使用垫片工具(es6-proxy-polyfill)。

# vue3 的编译优化

我们知道通过数据劫持和依赖收集,Vue.js 2.x 的数据更新并触发重新渲染的粒度是组件级的,虽然 Vue 能保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vnode 树,举个例子,比如我们要更新这个组件:

<template>
  <div id="content">
    <p class="text">static text</p>
    <p class="text">static text</p>
    <p class="text">{{ message }}</p>
    <p class="text">static text</p>
    <p class="text">static text</p>
  </div>
</template>
1
2
3
4
5
6
7
8
9

整个 diff 过程如图所示

image-20220305182133114

可以看到,因为这段代码中只有一个动态节点,所以这里有很多 diff 和遍历其实都是不需要的,这就会导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。

而对于上述例子,理想状态只需要 diff 这个绑定 message 动态节点的 p 标签即可。

Vue.js 3.0 做到了,它通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破

除此之外,Vue.js 3.0 在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法

# 组合式 API(Composition API)好处

  • 优化逻辑组织

    • vue2 中 options API,逻辑关注点比较分散,代码比较多时难以阅读
    • vue3 中通过 composition API,可以将每个逻辑关注点写在同一个函数中,解决了 options API 的缺陷
  • 优化逻辑复用

    • vue2 中逻辑复用可以用 mixins 实现,当 mixins 比较多时,容易造成命名冲突和数据来源不清晰
    • vue3 可以将复用的逻辑封装成一个 hook 函数,需要使用就引入,这样命名不会冲突而且数据来源也很清晰
  • 更好的类型支持

    • 因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this
  • 对 tree-shaking 友好,代码也更容易压缩

如果组件足够简单,还是可以使用 Options API 的

# vnode 到真实 DOM 是如何转变的

// 在 Vue.js 3.0 中,初始化一个应用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
1
2
3
4
5
  • createApp 内部做了两件事,返回 app 对象

    • ensureRenderer().createApp() 来创建 app 对象

      用 ensureRenderer() 来延时创建渲染器,这样做的好处是当用户只依赖响应式包的时候,就不会创建渲染器,因此可以通过 tree-shaking 的方式移除核心渲染逻辑相关的代码

    • 重写 app.mount 方法

      为什么重写?因为 Vue.js 不仅仅是为 Web 平台服务,它的目标是支持跨平台渲染,而 createApp 函数内部的 app.mount 方法是一个标准的可跨平台的组件渲染流程(先创建 vnode,再渲染 vnode),也就是说这些代码的执行逻辑都是与平台无关的。因此我们需要在外部重写这个方法,来完善 Web 平台下的渲染逻辑。

  • app.mount 内部标准化容器之后进行标准的渲染流程

    • 首先是通过 normalizeContainer 标准化容器(这里可以传字符串选择器或者 DOM 对象,但如果是字符串选择器,就需要把它转成 DOM 对象,作为最终挂载的容器)
    • if 判断,如果组件对象没有定义 render 函数和 template 模板,则取容器的 innerHTML 作为组件模板内容
    • 在挂载前清空容器内容,最终再调用 mount 方法走标准的组件渲染流程
  • 标准渲染流程内部先创建 vnode,然后渲染 vnode

    • 创建 vnode

      • 调用 createVNode 函数,对 props 做标准化处理、对 vnode 的类型信息编码、创建 vnode 对象,标准化子节点 children
    • 渲染 vnode

      • 调用 render 函数,如果它的第一个参数 vnode 为空,则执行销毁组件的逻辑,否则执行创建或者更新组件的逻辑

      • render 内部调用了 patch 函数,这个函数有两个功能,一个是根据 vnode 挂载 DOM,一个是根据新旧 vnode 更新 DOM

      • patch 内部根据节点类型执行对应的渲染逻辑

        • 组件渲染逻辑

          • 执行 processComponent,该函数的逻辑很简单,如果 参数 1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。
          • processComponent 内部调用 mountComponent 函数挂载组件,这个函数主要做三件事情:创建组件实例、设置组件实例、设置并运行带副作用的渲染函数。
            • 创建组件实例:内部通过对象的方式去创建了当前渲染的组件实例
            • 设置组件实例,instance 保留了很多组件相关的数据,维护了组件的上下文,包括对 props、插槽,以及其他实例的属性的初始化处理
            • 运行带副作用的渲染函数 setupRenderEffect,该函数利用响应式库的 effect 函数创建了一个副作用渲染函数 componentEffect副作用,这里你可以简单地理解为,当组件的数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的
              • 渲染函数内部也会判断这是一次初始渲染还是组件更新
              • 初始渲染主要做两件事情:渲染组件生成 subTree、把 subTree 挂载到 container 中。
              • 每个组件都会有对应的 render 函数,即使你写 template,也会编译成 render 函数,而 renderComponentRoot 函数就是去执行 render 函数创建整个组件树内部的 vnode,把这个 vnode 再经过内部一层标准化,就得到了该函数的返回结果:子树 vnode(也就是 subTree)。
              • 渲染生成子树 vnode 后,接下来就是继续调用 patch 函数把子树 vnode 挂载到 container 中了。
              • 那么我们又再次回到了 patch 函数,会继续对这个子树 vnode 类型进行判断,如果对应的子树 vnode 是一个普通元素 vnode,那么就执行普通元素渲染逻辑
        • 普通元素渲染逻辑

          • 执行 processElement,该函数的逻辑很简单,如果 参数 1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。

          • processElement 内部调用 mountElement 函数挂载元素,这个函数主要做四件事:创建 DOM 元素节点、处理 props、处理 children、挂载 DOM 元素到 container 上

            • 创建 DOM 元素节点,通过 hostCreateElement 方法创建,这个方法跟平台相关,在 web 平台下,它本质是调用底层的 DOM API document.createElement 创建元素,这些平台相关的方法是在创建渲染器阶段作为参数传入的

            • 处理 props,如果有 props 的话,给这个 DOM 节点添加相关的 class、style、event 等属性,并做相关的处理,这些逻辑都是在 hostPatchProp 函数内部做的

            • 处理 children,处理纯文本和数组的情况

              • 如果子节点是纯文本,则执行 hostSetElementText 方法,它在 Web 环境下通过设置 DOM 元素的 textContent 属性设置文本

              • 如果子节点是数组,则执行 mountChildren 方法,函数内部会遍历 children 获取到每一个 child,然后递归执行 patch 方法挂载每一个 child 。注意,这里有对 child 做预处理的情况,属于编译优化的内容

                为什么是 patch?不是 mountElement?因为子节点可能有其他类型的 vnode,比如组件 vnode。

            • 挂载 DOM 元素到 container 上,调用 hostInsert 方法,在 web 平台下,该函数内部会做一个 if 判断,如果有参考元素 anchor,就执行 parent.insertBefore ,否则执行 parent.appendChild 来把 child 添加到 parent 下,完成节点的挂载

        • TELEPORT 组件

        • SUSPENSE 组件

大致流程:

image-20220305192359860

主要函数调用大致流程:

createApp => mount => normalizeContainer => createVNode => render => patch =>

  • processComponent => mountComponent => setupRenderEffect => patch
  • processElement => mountElement => mountChilden => patch

# 完整的 DOM diff 流程是怎样的

回顾带副作用渲染函数 setupRenderEffect创建的componentEffect函数,组件数据变化的时候会触发componentEffect,函数内部通过instance.isMounted判断是渲染组件还是更新组件,由于我们关注的是更新组件,这里关注更新组件的流程,更新组件主要做三件事情:更新组件 vnode 节点、渲染新的子树 vnode、根据新旧子树 vnode 执行 patch 逻辑

  • 更新组件 vnode 节点,这里会有一个条件判断,判断组件实例中是否有新的组件 vnode(用 next 表示),有则调用updateComponentPreRender去更新组件 vnode 节点信息,包括更改组件实例的 vnode 指针、更新 props 和更新插槽等一系列操作,没有 则 next 指向之前的组件 vnode

  • 渲染新的子树 vnode,调用renderComponentRoot函数去渲染新的子树 vnode,因为数据发生了变化,模板又和数据相关,所以渲染生成的子树 vnode 也会发生相应的变化

  • 执行 patch 逻辑,用来找出新旧子树 vnode 的不同,并找到一种合适的方式更新 DOM

    • 首先判断新旧节点是否是相同的 vnode 类型,如果不同,销毁旧节点,挂载新节点,如果是相同的 vnode 类型,就需要走 diff 更新流程了

      节点的 type 和 key 都相同,才是相同节点

    • 挂载新节点和走 diff 流程都是通过调用processElementprocessComponent函数

      挂载组件调用processComponent(n1 = null,n2)

      更新组件调用processComponent(n1,n2)

      挂载普通元素调用processElement(n1 = null,n2)

      更新普通元素调用processElement(n1,n2)

    • 如果是更新组件,走processComponent,更新组件的过程主要做两件事情:判断是否需要更新子组件,触发子组件的更新函数

      • processComponent函数内部执行 updateComponent 函数来更新子组件,
      • updateComponent 函数在更新子组件的时候,会先执行 shouldUpdateComponent 函数,根据新旧子组件 vnode 来判断是否需要更新子组件
      • 在 shouldUpdateComponent 函数的内部,主要是通过检测和对比组件 vnode 中的 props、chidren、dirs、transiton 等属性,来决定子组件是否需要更新
      • 如果 shouldUpdateComponent 返回 true ,先执行 invalidateJob(instance.update)避免子组件由于自身数据变化导致的重复更新
      • 然后又执行了子组件的副作用渲染函数 instance.update 来主动触发子组件的更新
    • 如果是更新普通元素,走processElement,更新元素的过程主要做两件事情:更新 props 和更新子节点

      • processElement函数内部通过调用patchProps来更新props,接着调用patchChildren更新子节点
      • patchProps 函数就是在更新 DOM 节点的 class、style、event 以及其它的一些 DOM 属性
      • patchChildren函数内部根据子节点的情况去更新
      • 如果旧子节点是纯文本,新子节点是纯文本,新旧文本替换
      • 如果旧子节点是纯文本,新子节点是空,删除旧子节点
      • 如果旧子节点是纯文本,新子节点是数组,清空旧文本,添加新子节点
      • 如果旧子节点是空,新子节点是纯文本,添加新文本
      • 如果旧子节点是空,新子节点是空,什么都不需要做
      • 如果旧子节点是空,新子节点是数组,添加新字节点
      • 如果旧子节点是数组,新子节点是纯文本,删除旧子节点,添加新文本
      • 如果旧子节点是数组,新子节点是空,删除旧子节点
      • 如果旧子节点是数组,新子节点是数组,diff 算法

# diff 算法流程

  1. 同步头部索引i
  2. 同步头部节点就是从头部开始,依次对比新节点和旧节点,如果它们相同的则执行 patch 更新节点;如果不同或者索引 i 大于索引 e1 或者 e2,则同步过程结束。
  3. 同步旧子节点数组的尾部索引e1,新子节点数组的尾部索引e2
  4. 同步尾部节点就是从尾部开始,依次对比新节点和旧节点,如果相同的则执行 patch 更新节点;如果不同或者索引 i 大于索引 e1 或者 e2,则同步过程结束。
  5. 同步完成有三种情况
  6. 如果i > e1i <= e2,说明需要挂载剩余的新节点
  7. 如果i > e2i <= e1,说明需要删除多余的旧节点
  8. 都不满足的话,根据 key 建立新子序列的索引图keyToNewIndexMapkeyToNewIndexMap 存储的就是新子序列中每个节点在新子序列中的索引
  9. 正序遍历旧子序列,根据前面建立的 keyToNewIndexMap 查找旧子序列中的节点在新子序列中的索引,如果找不到就说明新子序列中没有该节点,就删除它;如果找得到则将它在旧子序列中的索引更新到 newIndexToOldIndexMap数组
  10. 注意这里索引加了长度为 1 的偏移,是为了应对 i 为 0 的特殊情况,如果不这样处理就会影响后续求解最长递增子序列。
  11. 遍历过程中,用变量 maxNewIndexSoFar 跟踪判断节点是否移动,maxNewIndexSoFar 始终存储的是上次求值的 newIndex,一旦本次求值的 newIndex 小于 maxNewIndexSoFar,这说明顺序遍历旧子序列的节点在新子序列中的索引并不是一直递增的,也就说明存在移动的情况。
  12. 除此之外,这个过程中也会更新新旧子序列中匹配的节点,另外如果所有新的子序列节点都已经更新,而对旧子序列遍历还未结束,说明剩余的节点就是多余的,删除即可。
  13. 至此,完成了新旧子序列节点的更新多余旧节点的删除,并且建立了一个 newIndexToOldIndexMap 存储新子序列节点的索引和旧子序列节点的索引之间的映射关系,并确定是否有移动
  14. 如果 moved 为 true 就通过 getSequence(newIndexToOldIndexMap) 计算最长递增子序列
  15. 接着采用倒序的方式遍历新子序列,因为倒序遍历可以方便使用最后更新的节点作为锚点。在倒序的过程中,锚点指向上一个更新的节点,然后判断 newIndexToOldIndexMap[i] 是否为 0,如果是则表示这是新节点,就需要挂载它;接着判断是否存在节点移动的情况,如果存在的话则看节点的索引是不是在最长递增子序列中,如果在则倒序最长递增子序列,否则把它移动到锚点的前面。

# setup 组件渲染前的初始化流程是怎样的

初始化组件,创建组件实例,设置组件实例,初始化 props,初始化 slots,设置有状态的组件实例,创建渲染上下文代理,创建 setup 函数上下文,执行 setup 函数获取结果,处理 setup 函数的执行结果,完成组件实例设置,初始化模板或者渲染函数,兼容 optionsAPI

# 为什么需要创建渲染上下文代理

在 Vue.js 2.x 中,也有类似的数据代理逻辑,比如 props 求值后的数据,实际上存储在 this._props 上,而 data 中定义的数据存储在 this._data 上。在初始化组件的时候,data 中定义的 属性 在组件内部是存储在 this._data 上的,而模板渲染的时候访问 this.xxx,实际上访问的是 this._data.xxx,这是因为 Vue.js 2.x 在初始化 data 的时候,做了一层 proxy 代理。

到了 Vue.js 3.0,为了方便维护,把组件中不同状态的数据存储到不同的属性中,比如存储到 setupState、ctx、data、props 中。我们在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以我们也要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupState、ctx、data、props 中的数据的访问和修改

# 研究 PublicInstanceProxyHandlers

PublicInstanceProxyHandlers 是创建渲染上下文代理的时候用到的,作为传入 new Proxy 的 handler 对象,定义了 get 和 set

# 渲染上下文中的访问优先级

PublicInstanceProxyHandlers 的 get 函数中首先判断 key 是否以$开头

不以$开头的 key 可能是访问 setupState、data、props、ctx

判断优先级为 setupState > data > props > ctx

也就是说,当上述对象 key 值相同的时候,会优先访问 setup 中定义的 key,其次是 data,接着是 props,最后是 ctx

$开头的 key 可能是访问 vue 内部公开的$xxx属性或者方法,vue-loader 编译注入的 css 模块内部的 key,用户自定义以$开头的 key,全局属性

判断优先级为 vue 内部公开的$xxx属性或者方法 > vue-loader 编译注入的 css 模块内部的 key > 用户自定义以$开头的 key > 全局属性

如果都不满足,就剩两种情况了,即在非生产环境下就会报两种类型的警告,第一种是在 data 中定义的数据以 $ 开头的警告,因为 $是保留字符,不会做代理;第二种是在模板中使用的变量没有定义的警告。

set 函数在对 instance.ctx 中的属性修改时触发,触发判断顺序跟 get 一样,先判断 setupState,再判断 data,接着是 prop,最后是 ctx

# vnode 有什么优势呢?为什么一定要设计 vnode 这样的数据结构呢?

首先是抽象,引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力也得到提升。

其次是跨平台,因为 patch vnode 的过程不同平台可以有自己的实现,基于 vnode 再做服务端渲染、Weex 平台、小程序平台的渲染都变得容易了很多。

不过这里要特别注意,使用 vnode 并不意味着不用操作 DOM 了,很多同学会误以为 vnode 的性能一定比手动操作原生 DOM 好,这个其实是不一定的。

因为,首先这种基于 vnode 实现的 MVVM 框架,在每次 render to vnode 的过程中,渲染组件会有一定的 JavaScript 耗时,特别是大组件,比如一个 1000 _ 10 的 Table 组件,render to vnode 的过程会遍历 1000 _ 10 次去创建内部 cell vnode,整个耗时就会变得比较长,加上 patch vnode 的过程也会有一定的耗时,当我们去更新组件的时候,用户会感觉到明显的卡顿。虽然 diff 算法在减少 DOM 操作方面足够优秀,但最终还是免不了操作 DOM,所以说性能并不是 vnode 的优势。

# 平时开发页面就是把页面拆成一个个组件,那么组件的拆分粒度是越细越好吗?为什么呢?

并不是拆分粒度越小越好。

原因:

1、在我的日常开发中,有两种情况会去拆分组件,第一种是根据页面的布局或功能,将整个页面拆分成不同的模块组件,最后将这些模块组件拼起来形成页面;第二种是在实现第一部拆分出来的这些模块组件的时候,发现其中有一些模块组件具有相同或相似的功能点,将这些相似的功能点抽离出来写成公共组件,然后在各个模块中引用。无论是模块组件还是公共组件,拆分组件的出发点都和组件的大小粒度无关。可维护性和复用性才是拆分组件的出发点。

2、对于组件的渲染,会先通过renderComponentRoot去生成组件的子树vnode,再递归 patch 去处理这个子树vnode。也就是说,对于同样一个 div,如果将其封装成组件的话,会比直接渲染一个 div 要多执行一次生成组件的子树vnode的过程。并且还要设置并运行带副作用的渲染函数。也就是说渲染组件比直接渲染元素要耗费更多的性能。如果组件过多,这些对应的过程就越多。如果按照组件粒度大小去划分组件的话会多出很多没有意义的渲染子树和设置并运行副作用函数的过程。

综上所述,并不是拆分粒度越小越好,只要按照可维护性和复用性去划分组件就好。

# vue-devtools 打开 editor 原理

利用nodejs中的child_process,执行了类似code path/to/file命令,于是对应编辑器就打开了相应的文件,而对应的编辑器则是通过在进程中执行ps xWindow则用Get-Process)命令来查找的,当然也可以自己指定编辑器

最近更新: 4 小时前