Vue常用的内置指令的底层细节分析

660 阅读4分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

上一篇文章我们知道了指令的实现原理,接下来我们来研究下Vue提供的一些默认指令的实现原理。

v-text

使用案例
<div v-text="'value'"
实现逻辑
  • 先来看下render函数
const _hoisted_1 = ["textContent"]

function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("div", {
      textContent: _toDisplayString('value')
    }, null, 8 /* PROPS */, _hoisted_1))
  }
}
})

在创建VNode的时候传递了一个textContentpro

export function patchDOMProp(
  el: any,
  key: string,
  value: any,
  prevChildren: any,
  parentComponent: any,
  parentSuspense: any,
  unmountChildren: any
) {
  if (key === 'innerHTML' || key === 'textContent') {
    // 如果`textContent`直接更新元素的textContent
    el[key] = value == null ? '' : value
    return
  }
}

这个pro直接被用来作为元素的textContent

总结

v-text设置元素的textContent

v-html

我们通过上面的代码,估计你看到innerHTML应该就理解了v-html的实现逻辑。v-html是渲染函数生成VNode的时候传了一个innerHTMLpro, 这个pro直接被用来作为元素的 innerHTML

  • 验证下确实如此
const _hoisted_1 = ["innerHTML"]

function render(_ctx, _cache) {
  with (_ctx) {
    const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return (_openBlock(), _createElementBlock("div", { innerHTML: 'value' }, null, 8 /* PROPS */, _hoisted_1))
  }
}
总结

v-html设置元素的innerHTML

v-show

使用案例
<div v-show="true">div元素</div>
实现逻辑
  • 先来看下render函数
function render(_ctx, _cache) {
  with (_ctx) {
    const { vShow: _vShow, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

    return _withDirectives((_openBlock(), _createElementBlock("div", null, "div元素", 512 /* NEED_PATCH */)), [
      [_vShow, false]
    ])
  }
}

v-show实现逻辑:绑定了vShow指令在元素上

  • 我们来看下v-show这个内部指令
interface VShowElement extends HTMLElement {
  // _vod = vue original display
  _vod: string
}

export const vShow: ObjectDirective<VShowElement> = {
  beforeMount(el, { value }, { transition }) {
    el._vod = el.style.display === 'none' ? '' : el.style.display
    setDisplay(el, value)
  },
  updated(el, { value, oldValue }, { transition }) {
    setDisplay(el, value)
  },
  beforeUnmount(el, { value }) {
    setDisplay(el, value)
  }

}

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

v-show在内部就是切换元素的style.display。如果传入的值为false就将style.display设置为none不显示,如果传入的值为true,则是元素原本设置style.display值。

总结

v-show控制元素的style.display来切换显示和隐藏。

v-if && v-else-if && v-else

使用案例
<!-- 数据 -->
let condition = ref(1);

<!-- 模板 -->
<div v-if="condition == 1">状态1</div>
<div v-else-if="condition == 2">状态2</div>
<div v-else>其他状态</div>
实现逻辑
  • 来看下render函数
const _hoisted_1 = { key: 0 }
const _hoisted_2 = { key: 1 }
const _hoisted_3 = { key: 2 }

function render(_ctx, _cache) {
  with (_ctx) {
    const { openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue

    return (condition == 1)
      ? (_openBlock(), _createElementBlock("div", _hoisted_1, "状态1"))
      : (condition == 2)
        ? (_openBlock(), _createElementBlock("div", _hoisted_2, "状态2"))
        : (_openBlock(), _createElementBlock("div", _hoisted_3, "其他状态"))
  }
}

v-if && v-else-if && v-else实现逻辑:直接进行表达式的判断,然后不同的表达式渲染不同的DOM元素。

重要知识点

问题:v-if && v-else-if && v-else切换时会进行元素复用吗?

答案:不会。因为不同的元素赋予了不同的key, const _hoisted_1 = { key: 0 } const _hoisted_2 = { key: 1 } const _hoisted_3 = { key: 2 } 这样切换条件,会直接卸载旧的元素节点,挂载新的元素节点,不会进行复用。

v-for

使用案例
let items = [{
  id: 1,
  name: "张三"
}, 
{   id:2, 
  name: "李四"
}];

<div v-for="(item, index) in items" :key="item.id">{{ item.name }}</div>
实现逻辑
  • 来看下render函数
function render(_ctx, _cache) {
  with (_ctx) {
    const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, toDisplayString: _toDisplayString } = _Vue

    return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(items, (item, index) => {
      return (_openBlock(), _createElementBlock("div", { key: item.id }, _toDisplayString(item.name), 1 /* TEXT */))
    }), 128 /* KEYED_FRAGMENT */))
  }
}

渲染结果是Fragment中包含了每个Item对应的Element

重要知识点

问题:v-for 的遍历的数据只能是数组吗?

答案:不是

  1. 如果是数组,则遍历的是数组的每个元素;
  2. 如果是字符串,则遍历的是字符串的每个字符;
  3. 如果是数字,则遍历的是从 0 到 数据对应的那个值;
  4. 如果是实现了可迭代协议的数据,则是迭代遍历到的所有值;
  5. 如果是对象,则遍历的所有的key锁对应的值;

问题:v-ifv-for同时使用,会有优先使用哪个指令?

<div v-if="items.length > 0" v-for="(item, index) in items" :key="item.id">{{ item.name }}</div>
function render(_ctx, _cache) {
  with (_ctx) {
    const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, toDisplayString: _toDisplayString, createCommentVNode: _createCommentVNode } = _Vue

    return (items.length > 0)
      ? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(items, (item, index) => {
          return (_openBlock(), _createElementBlock("div", { key: item.id }, _toDisplayString(item.name), 1 /* TEXT */))
        }), 128 /* KEYED_FRAGMENT */))
      : _createCommentVNode("v-if", true)
  }
}

答案:优先判断v-if指令,如果条件成立,才会进行v-for遍历生成数组元素节点。

其他

还有一些其他常用的指令,例如v-model进行双向绑定,v-on进行事件绑定,v-slot进行插槽的设置。这几个指令由于相对复杂,我们将每个使用一个章节来介绍。本章节就到此为止。