备注: 2020-10-24补充新的理解

事前准备

  • git 克隆 git@github.com:vuejs/vue.git
  • 执行编译: npx rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev 得到 dist/vue.js

问题

  • v-if v-for 哪个优先级高?如何优化?
  • Vue组件里data为何是函数,根实例直接写就可以?
  • vue的key的作用和工作原理
  • Vue.use 插件机制 和 beforeCreate

v-if v-for 哪个优先级高?如何优化?

这个问题可以惊艳一把,需要搞清楚是 vue2.x 还是 vue3 ,两者结论不同,因为vue3的list文档是我翻译的哈哈哈,记忆犹新。

结论是 vue2 中 for 优先级高。vue3中 if 优先级高。

vue2 官方文档提到了这一点: v-for v-if 一起使用

v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中

代码中大概是这样子描述的,渲染 render函数是这样书写的:

  1. _l(循环数据,function(){return (if条件)})

进一步地,源码中是如何实现的? src/compiler/codegen/index.js line 64 搜索 el.for

这是一锤定音:

  1. if (el.staticRoot && !el.staticProcessed) {
  2. return genStatic(el, state)
  3. } else if (el.once && !el.onceProcessed) {
  4. return genOnce(el, state)
  5. } else if (el.for && !el.forProcessed) { // 这里先对for进行生成,然后是if
  6. return genFor(el, state)
  7. } else if (el.if && !el.ifProcessed) {
  8. return genIf(el, state)
  9. } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
  10. return genChildren(el, state) || 'void 0'
  11. } else if (el.tag === 'slot') {
  12. return genSlot(el, state)
  13. } else {}

如何优化?识别意图,分两种。计算或者前置,略。


Vue组件里data为何是函数,根实例直接写就可以?

结论:

在编写代码过程中,vue组件强制要求函数return对象,根实例无此要求。

这里也涉及到一个单例模式、多例模式的概念,因为组件可能存在多个实例,函数会返回能避免污染:通过创建多个实例避免数据污染。

根组件兼容两种写法,如果是函数会自动执行调用获得返回结果,并且只有一个实例,不需要关心。

源码中存在一个 mergeDataOrFn 方法,根实例执行时候会忽略检查。组件会进入判断。

源码有进一步的指明,根组件无所谓函数或者对象:

从 initState 进入,定位到 src/core/instance/state.js 的 #initData()

代码大致如下:

  1. function initData (vm: Component) {
  2. let data = vm.$options.data // 拿到根节点挂载的data
  3. // 如果 挂载的data是函数就执行 getData 否则就直接挂载
  4. data = vm._data = typeof data === 'function'
  5. ? getData(data, vm)
  6. : data || {}
  7. // 如果不是一个对象,就要告警
  8. if (!isPlainObject(data)) {
  9. data = {}
  10. process.env.NODE_ENV !== 'production' && warn(
  11. 'data functions should return an object:\n' +
  12. 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
  13. vm
  14. )
  15. }
  16. }

组件级别的源码如何判断?

合并 MergeOption vm会被判断,看是否会跳过。

  1. // src/core/instance/init.js#initMixin
  2. // merge options 合并选项
  3. if (options && options._isComponent) { // 此时是一个组件
  4. // optimize internal component instantiation 优化内部组件初始化
  5. // since dynamic options merging is pretty slow, and none of the 动态选项合并很慢
  6. // internal component options needs special treatment. 内部组件选项都不用特别关注
  7. initInternalComponent(vm, options)
  8. } else { // 核心 mergeOptions
  9. vm.$options = mergeOptions(
  10. resolveConstructorOptions(vm.constructor),
  11. options || {},
  12. vm
  13. )
  14. }

当我们对其打断点会发现,全局组件先执行,根组件后。

观察data策略合并,会定位到 sr/core/util/options.js#mergeDataOrFn 中的 states.data

  1. strats.data = function (
  2. parentVal: any,
  3. childVal: any,
  4. vm?: Component // 第三个参数可选,是否是组件
  5. ): ?Function {
  6. if (!vm) { // 如果没有传递组件,需要把 parent child 进行合并
  7. if (childVal && typeof childVal !== 'function') {
  8. // 这里就定义了必须是一个函数
  9. process.env.NODE_ENV !== 'production' && warn(
  10. 'The "data" option should be a function ' +
  11. 'that returns a per-instance value in component ' +
  12. 'definitions.',
  13. vm
  14. )
  15. return parentVal
  16. }
  17. return mergeDataOrFn(parentVal, childVal)
  18. }
  19. // 如果传递了组件,需要对 vm 实例额外合并。这里并没有走进去warn
  20. return mergeDataOrFn(parentVal, childVal, vm)
  21. }

vue的key的作用和工作原理

结论: 唯一确定元素,diff更高效。

假定存在下面的demo

  1. <li v-for="item of arr" :key="item.id"></li>
  1. var arr=[a,b,c,d,e]
  2. // do sth
  3. var arr = arr.splice(2,0,'f') // 修改了值

也就变成了:

  1. a b c d e
  2. a b f c d e
  • 无 key 的情况下,

a b c d e
a b f c d e
从左向右一次覆盖。c会替换成f,后续覆盖,e单独创建。会执行 5次更新+1次创建

  • 有key的情况下,遇到 c 和 f 不同,会进行尾部判断,从右向左。

e d c 都会发现对应。最终创建 f 插入。5次更新+1次创建。

更新次数不变,但不同的是 是否真正会更新

  • 不加key,执行更新3次+1次创建
  • 加key执行0次更新+1次创建。

因为后续会进行 key判断(默认undefined)、 tag类型、内容类型的判断,如果大的方向是一致,会认为是同一个节点,会执行更新,会频繁更新操作。

面对下面的情况:

  1. c d e
  2. f c d e

会判断 c e,e e ,发现后面相同。

patch.js 算法 sameVnode


Vue的 diff 算法

diff算法很多,我们关注的是 vue2.x 的diff 算法。

结论:
diff 是自然而然产生的,涉及到 虚拟DOM 必然的产物。一个组件对应一个watcher,颗粒度适中。整体上:深度优先、同层比较。

观察 path.js # patchVnode 是diff 发生的地方。整体上:深度优先、同层比较。

观察是否有child,先进入子类,递归。

同层比较,文本节点比对。新增子节点、比较相同节点。

高效的 updateChildren

image.png


Vue组件化的理解

定义、有点、场景、注意事项、优化。

我们通过书写一个 sfc 单文件通过 vue-loader 渲染为 render函数,导出的本质还是配置对象,框架会基于 vue拓展VueComponent,基于此生成相关构造函数。

从 lifecycle.js#mountCompoent 中看到 Watcher的创建。合理区分颗粒度。

独立可复用是核心,降低复杂度,方便维护和测试。


$nextTick

对异步更新队列和策略有关。

定义、场景、实现

全局api,回调。在下次DOM更新循环结束之后执行。有些数据DOM频繁修改,不会立刻生效,需要使用 nextTick

Vue更新DOM是异步的,会合并,微任务的方式在同步代码执行结束之后更新watcher。

使用 Promise.then 回到函数,其他 mutation 等,最晚是宏任务 settimeout


Vue.use 插件机制 和 beforeCreate

vue插件是如何做到全局注册的。

观察 router源码:

vue-router/src/index.js — init 判断 installed — VueRouter.install

install.js install Vue.mixin beforeCreate

在使用 vue-router 时候 为何 vue.use(xxx) 就可以

Vue.use 源码很简单 https://github.com/vuejs/vue/blob/dev/src/core/global-api/use.js

use 本身是function,会自动判断 install