关于源码分析:Vue-3-中-vif-和-vshow-指令实现的原理源码分析

97次阅读

共计 8234 个字符,预计需要花费 21 分钟才能阅读完成。

前言

又回到了经典的一句话:“知其然,而后使其然 ”。置信大家对 Vue 提供 v-ifv-show 指令的应用以及对应场景应该都 滚瓜烂熟 了。然而,我想依然会有很多同学对于 v-ifv-show 指令实现的原理存在常识空白。

所以,明天就让咱们来一起理解一番 v-ifv-show 指令实现的原理~

v-if

在之前【Vue3 源码解读】从编译过程,了解动态节点晋升 一文中,我给大家介绍了 Vue 3 的编译过程,即一个模版会经验 baseParsetransformgenerate 这三个过程,最初由 generate 生成能够执行的代码(render 函数)。

这里,咱们就不从编译过程开始解说 v-if 指令的 render 函数生成过程了,有趣味理解这个过程的同学,能够看我之前的文章从编译过程,了解动态节点晋升

咱们能够间接在 Vue3 Template Explore 输出一个应用 v-if 指令的栗子:

<div v-if="visible"></div>

而后,由它编译生成的 render 函数会是这样:

render(_ctx, _cache, $props, $setup, $data, $options) {return (_ctx.visible)
    ? (_openBlock(), _createBlock("div", { key: 0}))
    : _createCommentVNode("v-if", true)
}

能够看到,一个简略的应用 v-if 指令的模版编译生成的 render 函数最终会返回一个 三目运算表达式。首先,让咱们先来认识一下其中几个变量和函数的意义:

  • _ctx 以后组件实例的上下文,即 this
  • _openBlock()_createBlock() 用于结构 Block TreeBlock VNode,它们次要用于靶向更新过程
  • _createCommentVNode() 创立正文节点的函数,通常用于占位

显然,如果当 visiblefalse 的时候,会在以后模版中创立一个 正文节点(也可称为占位节点),反之则创立一个实在节点(即它本人)。例如当 visiblefalse 时渲染到页面上会是这样:

<div align=”center”>

<img width="400" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa3d336210f34fff8f68d1b8cab83443~tplv-k3u1fbpfcp-zoom-1.image"/>

</div>

在 Vue 中很多中央都使用了正文节点来作为 占位节点 ,其目标是在不展现该元素的时候,标识其 在页面中的地位,以便在 patch 的时候将该元素放回该地位。

那么,这个时候我想大家就会抛出一个疑难:当 visible 动静切换 truefalse 的这个过程(派发更新)到底产生了什么?

派发更新时 patch,更新节点

如果不理解 Vue 3 派发更新和依赖收集过程的同学,能够看我之前的文章 4k+ 字剖析 Vue 3.0 响应式原理(依赖收集和派发更新)

在 Vue 3 中总共有四种指令:v-onv-modelv-showv-if。然而,实际上在源码中,只针对后面三者 进行了非凡解决,这能够在 packages/runtime-dom/src/directives 目录下的文件看出:

// packages/runtime-dom/src/directives
|-- driectives
    |-- vModel.ts       ## v-model 指令相干
    |-- vOn.ts          ## v-on 指令相干
    |-- vShow.ts        ## v-show 指令相干

而针对 v-if 指令是间接走派发更新过程时 patch 的逻辑。因为 v-if 指令订阅了 visible 变量,所以当 visible 变动的时候,则会触发 派发更新,即 Proxy 对象的 set 逻辑,最初会命中 componentEffect 的逻辑。

当然,咱们也能够称这个过程为组件的更新过程

这里,咱们来看一下 componentEffect 的定义(伪代码):

function componentEffect() {if (!instance.isMounted) {....} else {
          ...
        const nextTree = renderComponentRoot(instance)
        const prevTree = instance.subTree
        instance.subTree = nextTree
        patch(
          prevTree,
          nextTree,
          hostParentNode(prevTree.el!)!,
          getNextHostNode(prevTree),
          instance,
          parentSuspense,
          isSVG
        )
        ...
      }
  }
}

能够看到,当 组件还没挂载时,即第一次触发派发更新会命中 !instance.isMounted 的逻辑。而对于咱们这个栗子,则会命中 else 的逻辑,即组件更新,次要会做三件事:

  • 获取以后组件对应的组件树 nextTree 和之前的组件树 prevTree
  • 更新以后组件实例 instance 的组件树 subTreenextTree
  • patch 新旧组件树 prevTreenextTree,如果存在 dynamicChildren,即 Block Tree,则会命中靶向更新的逻辑,显然咱们此时满足条件

注:组件树则指的是该组件对应的 VNode Tree。

小结

总体来看,v-if 指令的实现较为简单,基于 数据驱动 的理念,当 v-if 指令对应的 valuefalse 的时候会 事后创立一个正文节 点在该地位,而后在 value 发生变化时,命中派发更新的逻辑,对新旧组件树进行 patch,从而实现应用 v-if 指令元素的动态显示暗藏。

<div align=”center”>

<img width="700" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fd36f8e0870340eeb0d2fbcf56fec40a~tplv-k3u1fbpfcp-zoom-1.image"/>

</div>

上面,咱们来看一下 v-show 指令的实现~

v-show

同样地,对于 v-show 指令,咱们在 Vue 3 在线模版编译平台输出这样一个栗子:

<div v-show="visible"></div>

那么,由它编译生成的 render 函数:

render(_ctx, _cache, $props, $setup, $data, $options) {return _withDirectives((_openBlock(), _createBlock("div", null, null, 512 /* NEED_PATCH */)), 
  [[_vShow, _ctx.visible]
  ])
}

此时,这个栗子渲染到页面上的 HTML:
<div align=”center”>

<img width="400" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e16e74b15c044a469baf368c96ddb6ae~tplv-k3u1fbpfcp-zoom-1.image"/>

</div>

从下面的 render 函数能够看出,不同于 v-if 的三目运算符表达式,v-showrender 函数返回的是 _withDirectives() 函数的执行。

后面,咱们曾经简略介绍了 _openBlock()_createBlock() 函数。那么,除开这两者,接下来咱们逐点剖析一下这个 render 函数,首当其冲的是 vShow

vShow 在生命周期中扭转 display 属性

_vShow 在源码中则对应着 vShow,它被定义在 packages/runtime-dom/src/directives/vShow。它的职责是对 v-show 指令进行 非凡解决,次要体现在 beforeMountmountedupdatedbeforeUnMount 这四个生命周期中:

// packages/runtime-dom/src/directives/vShow
export const vShow: ObjectDirective<VShowElement> = {beforeMount(el, { value}, {transition}) {
    el._vod = el.style.display === 'none' ? '' : el.style.display
    if (transition && value) {
      // 解决 tansition 逻辑
      ...
    } else {setDisplay(el, value)
    }
  },
  mounted(el, { value}, {transition}) {if (transition && value) {
      // 解决 tansition 逻辑
      ...
    }
  },
  updated(el, { value, oldValue}, {transition}) {if (!value === !oldValue) return
    if (transition) {
      // 解决 tansition 逻辑
      ...
    } else {setDisplay(el, value)
    }
  },
  beforeUnmount(el, { value}) {setDisplay(el, value)
  }
}

对于 v-show 指令会解决两个逻辑:一般 v-showtransition 时的 v-show 状况。通常状况下咱们只是应用 v-show 指令,命中的就是前者

这里咱们只对一般 v-show 状况开展剖析。

一般 v-show 状况,都是调用的 setDisplay() 函数,以及会传入两个变量:

  • el 以后应用 v-show 指令的 实在元素
  • v-show 指令对应的 value 的值

接着,咱们来看一下 setDisplay() 函数的定义:

function setDisplay(el: VShowElement, value: unknown): void {el.style.display = value ? el._vod : 'none'}

setDisplay() 函数正如它自身 命名的语意 一样,是通过扭转该元素的 CSS 属性 display 的值来动静的管制 v-show 绑定的元素的 显示 或暗藏。

并且,我想大家可能留神到了,当 valuetrue 的时候,display 是等于的 el.vod,而 el.vod 则等于这个实在元素的 CSS display 属性(默认状况下为空)。所以,当 v-show 对应的 valuetrue 的时候,元素显示与否是取决于它自身 的 CSS display 属性。

其实,到这里 v-show 指令的实质在源码中的体现曾经进去了。然而,依然会留有一些疑难,例如 withDirectives 做了什么?vShow 在生命周期中对 v-show 指令的解决又是如何使用的?

withDirectives 在 VNode 上减少 dir 属性

withDirectives() 顾名思义和指令相干,即在 Vue 3 中和指令相干的元素,最初生成的 render 函数都会调用 withDirectives() 解决指令相干的逻辑,vShow 的逻辑作为 dir 属性增加 VNode 上。

withDirectives() 函数的定义:

// packages/runtime-core/directives
export function withDirectives<T extends VNode>(
  vnode: T,
  directives: DirectiveArguments
): T {
  const internalInstance = currentRenderingInstance
  if (internalInstance === null) {__DEV__ && warn(`withDirectives can only be used inside render functions.`)
    return vnode
  }
  const instance = internalInstance.proxy
  const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = [])
  for (let i = 0; i < directives.length; i++) {let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i]
    if (isFunction(dir)) {...}
    bindings.push({
      dir,
      instance,
      value,
      oldValue: void 0,
      arg,
      modifiers
    })
  }
  return vnode
}

首先,withDirectives() 会获取以后渲染实例解决 边缘条件,即如果在 render 函数里面应用 withDirectives() 则会抛出异样:

“withDirectives can only be used inside render functions.”

而后,在 vnode 上绑定 dirs 属性,并且遍历传入的 directives 数组,而对于咱们这个栗子 directives 就是:

[[_vShow, _ctx.visible]
]

显然此时只会 迭代一次(数组长度为 1)。并且从 render 传入的 参数能够晓得,从 directives 上解构出的 dir 指的是 _vShow,即咱们下面介绍的 vShow。因为 vShow 是一个对象,所以会从新结构(bindings.push())一个 dirVNode.dir

VNode.dir 的作用体现在 vShow 在生命周期扭转元素的 CSS display 属性,而这些 生命周期会作为派发更新的完结回调被调用

接下来,咱们一起来看看其中的调用细节~

派发更新时 patch,注册 postRenderEffect 事件

置信大家应该都晓得 Vue 3 提出了 patchFlag 的概念,其用来针对不同的场景来执行对应的 patch 逻辑。那么,对于下面这个栗子,咱们会命中 patchElement 的逻辑。

而对于 v-show 之类的指令来说,因为 Vnode.dir 上绑定了解决元素 CSS display 属性的相干逻辑(vShow 定义好的生命周期解决)。所以,此时 patchElement 中会为注册一个 postRenderEffect 事件。

const patchElement = (
    n1: VNode,
    n2: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    ...
    // 此时 dirs 是存在的
    if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
      // 注册 postRenderEffect 事件
      queuePostRenderEffect(() => {vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
        dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
      }, parentSuspense)
    }
    ...
  }

这里咱们简略剖析一下 queuePostRenderEffectinvokeDirectiveHook

  • queuePostRenderEffectpostRenderEffect 事件注册是通过 queuePostRenderEffect 实现的,因为 effect 都是保护在一个队列中(为了放弃 effect 的有序),这里是 pendingPostFlushCbs,所以对于 postRenderEffect 也是一样的会被 进队
  • invokeDirectiveHook,因为 vShow 封装了对元素 CSS display 属性的解决,所以 invokeDirective 的本职是调用指令相干的生命周期解决。并且,须要留神的是此时是 更新逻辑 ,所以 只会调用 vShow 中定义好的 update 生命周期

flushJobs 的完结(finally)调用 postRenderEffect

到这里,咱们曾经围绕 v-Show 介绍完了 vShowwithDirectivespostRenderEffect 等概念。然而,万事具备只欠东风,还短少一个 调用 postRenderEffect 事件的机会,即解决 pendingPostFlushCbs 队列的机会.

在 Vue 3 中 effect 相当于 Vue 2.x 的 watch。尽管变了个命名,然而依然放弃着一样的调用形式,都是调用的 run() 函数,而后由 flushJobs() 执行 effect 队列。而调用 postRenderEffect 事件的机会 则是在执行队列的完结

flushJobs 函数的定义:

// packages/runtime-core/scheduler.ts
function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  if (__DEV__) {seen = seen || new Map()
  }
  flushPreFlushCbs(seen)
  // 对 effect 进行排序
  queue.sort((a, b) => getId(a!) - getId(b!))
  try {for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      // 执行渲染 effect
      const job = queue[flushIndex]
      if (job) {...}
    }
  } finally {
    ...
    // postRenderEffect 事件的执行机会
    flushPostFlushCbs(seen)
    ...
  }
}

flushJobs() 函数中会执行三种 effect 队列,别离是 preRenderEffectrenderEffectpostRenderEffect,它们各自对应 flushPreFlushCbs()queueflushPostFlushCbs

那么,显然 postRenderEffect 事件的 调用机会 是在 flushPostFlushCbs()。而 flushPostFlushCbs() 外部则会遍历 pendingPostFlushCbs 队列,即执行之前在 patchElement 时注册的 postRenderEffect 事件,实质上就是执行

updated(el, { value, oldValue}, {transition}) {if (!value === !oldValue) return
  if (transition) {...} else {
    // 扭转元素的 CSS display 属性
    setDisplay(el, value)
  }
},

小结

相比拟 v-if 简略罗唆地通过 patch 间接更新元素,v-show 的解决就略显简单。这里咱们从新梳理一下整个过程:

  • 首先,由 widthDirectives 来生成最终的 VNode。它会给 VNode 上绑定 dir 属性,即 vShow 定义的在生命周期中对元素 CSS display 属性的解决
  • 其次,在 patchElement 的阶段,会注册 postRenderEffect 事件,用于调用 vShow 定义的 update 生命周期解决 CSS display 属性的逻辑
  • 最初,在派发更新的完结,调用 postRenderEffect 事件,即执行 vShow 定义的 update 生命周期,更改元素的 CSS display 属性

<div align=”center”>
<img src=”https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2179d0b950e4de7b319372fb87c52b0~tplv-k3u1fbpfcp-zoom-1.image” />
</div>

结语

v-ifv-show 原理,你能够用一两句话概括,也能够用一大堆话概括。如果牵扯到面试场景下,我更观赏后者,因为这阐明你 钻研的够深 以及 理解能力够强。并且,当你理解一个指令的处理过程后,对于其余指令 v-onv-model 的解决,置信也能够很容易的得出结论。最初,如果文中存在表白不当或谬误的中央,欢送各位同学提 Issue~

我是五柳,喜爱翻新、捣鼓源码,专一于 Vue3 源码、Vite 源码、前端工程化等技术分享,欢送关注我的 微信公众号:Code center

正文完
 0