天天看點

别問我[vue],VNode、elm、context、el是個啥?

vue越來越熟練,但是一些基礎的概念卻漸漸模糊。模糊中又會變得自我懷疑,然後重新梳理,重新認識、深入了解 ~ VNode、elm、context、el ~他們是個啥,擔任着什麼樣子的角色 ?那就得從指令鈎子函數開始

研究準備階段

vnode源碼位址

/* @flow */
export default class VNode {
  tag: string | void; /** 目前标簽如:div */
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void; /** 存在于VNode中 即div.class名所代表一個Node對象,可直接操作dom */
  ns: string | void;
  context: Component | void; /** 上下文環境,即目前VNode所在的上下文環境。也即是目前VNode的父虛拟節點上下文環境 */
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*目前節點的标簽名*/
    this.tag = tag
    /*目前節點對應的對象,包含了具體的一些資料資訊,是一個VNodeData類型,可以參考VNodeData類型中的資料資訊*/
    this.data = data
    /*目前節點的子節點,是一個數組*/
    this.children = children
    /*目前節點的文本*/
    this.text = text
    /*目前虛拟節點對應的真實dom節點*/
    this.elm = elm
    /*目前節點的命名空間*/
    this.ns = undefined
    /*上下文作用域:VueComponent 即我們知道的 this */
    this.context = context
    /*函數化元件作用域*/
    this.functionalContext = undefined
    /*節點的key屬性,被當作節點的标志,用以優化*/
    this.key = data && data.key
    /*元件的option選項*/
    this.componentOptions = componentOptions
    /*目前節點對應的元件的執行個體*/
    this.componentInstance = undefined
    /*目前節點的父節點*/
    this.parent = undefined
    /*簡而言之就是是否為原生HTML或隻是普通文本,innerHTML的時候為true,textContent的時候為false*/
    this.raw = false
    /*靜态節點标志*/
    this.isStatic = false
    /*是否作為根節點插入*/
    this.isRootInsert = true
    /*是否為注釋節點*/
    this.isComment = false
    /*是否為克隆節點*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    // #7975
    // clone children array to avoid mutating original in case of cloning
    // a child.
    vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}
           

上面則展示了VNode源碼,且标注了部分注釋。但即使通讀了上面源碼,也隻是模模糊糊、并不深刻。因為隻有放入項目即使用,才能身臨其境的深刻了解。

接下來 , 将對

VNode、elm、context、el

相關程式檔案放入項目中,進行窺探 ~

element-ui

源碼中的一個js檔案

./package/src/utils/clickoutside.js

放到Vue項目中,并在main.js中進行全局引入

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Clickoutside from '@/views/clickoutside';
Vue.use(Clickoutside)

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

           

在引入的clickoutside.js檔案中,對關鍵位置作了日志log的列印 :

/**
 * package/src/utils/clickoutside.js
 * v-clickoutside
 * @desc 點選元素外面才會觸發的事件
 * @example
 * ```vue
 * <div v-element-clickoutside="handleClose">
 * ```
 */
const clickoutsideContext = '@@clickoutsideContext';

export default {
  bind(el, binding, vnode) {
    const documentHandler = function(e) {
      console.log('directives vnode =>', vnode)
      console.log('directives vnode.context =>', vnode.context)
      console.log('directives e.target =>', e.target)
      console.log('directives vnode.context[el[clickoutsideContext].methodName] =>', vnode.context[el[clickoutsideContext].methodName])
      if (vnode.context && !el.contains(e.target)) {
        vnode.context[el[clickoutsideContext].methodName]();
      }
    };
    console.log('directives el是什麼類型 =>', (typeof el))
    el[clickoutsideContext] = {
      documentHandler,
      methodName: binding.expression,
      arg: binding.arg || 'click'
    };
    console.log('directives el =>', el)
    console.log('directives el[clickoutsideContext].methodName =>', el[clickoutsideContext].methodName)
    console.log('directives el[clickoutsideContext].arg =>', el[clickoutsideContext].arg)
    console.log('directives el[clickoutsideContext] =>', el[clickoutsideContext])
    document.addEventListener(el[clickoutsideContext].arg, documentHandler);
  },

  update(el, binding) {
    el[clickoutsideContext].methodName = binding.expression;
  },

  unbind(el) {
    document.removeEventListener(
      el[clickoutsideContext].arg,
      el[clickoutsideContext].documentHandler);
  },

  install(Vue) {
    Vue.directive('clickoutside', {
      bind: this.bind,
      unbind: this.unbind
    });
  }
};

           

項目中使用看代碼

<templete/>

<template >
  <div class="acc-transfer-container" ref="template">
    <div class="header">
      <div class="img-back">
        <span class="img"></span>
      </div>
      <div class="span">
        <span>賬戶轉賬c</span>
      </div>
    </div>
    <div class="transfer-list">
      <li-acc-click mType="收款賬戶" :account="payee" ></li-acc-click>
      <li-show mType="币種" :allBalance="currency" ></li-show>
      <li-acc-input :callbackInput="callbackInput"></li-acc-input>
      <li-acc-click mType="付款賬戶" :account="payAcc"></li-acc-click>
      <li-show :allBalance="allBalance"></li-show>
    </div>
    <!-- 引入自定義指令 v-clickoutside ->
    <div class="outside-click" v-clickoutside="clickoutsideMehthod">點選 有 觸動</div>
    <el-button type="danger" round size="medium" class="btn_next" @click="sayHello($event)">下一步</el-button>
  </div>
</template>
           

從代碼中看,下截圖中的按鈕

點選 有 觸動

則是引入自定義指令的按鈕

别問我[vue],VNode、elm、context、el是個啥?

以上是為研究 VNode、elm、context、el的準備過程,ok!接下來進入日志的研究過程。

研究開始階段

注意: 這些

VNode、elm、context、el

的研究是出自指令鈎子函數。即從指令函數參數vnode 和 el出發,對自定義指令所用到的标簽,進行研究。一定要明确這裡介紹和研究的基礎環境條件。

研究從指令鈎子函數出發,研究第一項

el

經過上述準備,執行運作該vue項目工程,首次進入該vue頁面時,有日志 輸出。

别問我[vue],VNode、elm、context、el是個啥?

研究從指令鈎子函數出發,在vue官網對指令鈎子函數的參數,給以解釋

别問我[vue],VNode、elm、context、el是個啥?

結合日志輸出截圖、clickoutside.js以及vue程式,來看

指令鈎子函數中的參數el 是一個object的對象,且該對象

指向(僅指向)

<div class="outside-click">點選 有 觸動</div>

所代表的對象。vue官網說el是指令所綁定元素,可直接用來操作dom,顯然跟

<templete>

中的

不期而遇。

提問:這裡是對指令函數參數中el的解釋,那麼與VNode中的elm有什麼差別?與VNode中的context中的el又有什麼差別? 帶着問題接着向下看呗 ~

研究從指令鈎子函數出發,研究第二項

vnode

點選使用指令

v-clickoutside

的标簽按鈕"點選有觸動"外邊,有日志 輸出

别問我[vue],VNode、elm、context、el是個啥?

根據該日志輸出來看VNode,該VNode是來自指令鈎子函數參數

bind(el, binding, vnode)

即VNode表示

<div class="outside-click" v-clickoutside="clickoutsideMehthod">點選 有 觸動</div>

的虛拟節點

别問我[vue],VNode、elm、context、el是個啥?

接下來對VNode截圖内的關鍵字段進行逐個檢視、解釋

elm:[div.outside-click] = 是VNode中的一個字段,但是可以看到它的類型是

div.outside-click

對象。可直接用來操作dom,指令函數參數中el和vnode中的elm是同一個!! 為證明這個結論,在bind指令鈎子函數中新增了日志輸出 ,

别問我[vue],VNode、elm、context、el是個啥?

最終的驗證輸出結果是,符合預期

/** console.log('directives vnode.elm == el =>', vnode.elm ===el, vnode.elm == el) */
directives vnode.elm == el => true true
           

children:[VNode] = 是VNode中的一個字段,類型是VNode,是

<div class="outside-click" v-clickoutside="clickoutsideMehthod">點選 有 觸動</div>

的數組子節點

别問我[vue],VNode、elm、context、el是個啥?

進入該數組子節點詳情看,該數組中隻有一個元素,VNode虛拟節點。對應的标簽對象類型是

text

,内容是

點選 有 觸動

data:{directives:Array(1), staticClass: “outside-click”} = 是VNode中的一個字段,類型是對象。

tag:“div” = 是VNode中的一個字段,類型是div字元串。

context:VueComponent = 是VNode中的一個字段,類型是VueComponent,是目前虛拟節點所在的上下文環境。

别問我[vue],VNode、elm、context、el是個啥?

從該context内容詳情中我們是不是看到了熟悉的内容。

研究VNode中的context

别問我[vue],VNode、elm、context、el是個啥?

從截圖中context:VueComponent,是

$el:div.acc-transfer-container

,說明該上下文環境是使用了自定義指令

v-clickoutside

的标簽,所在的上下文環境。即

<template >

中的

<div class="acc-transfer-container" ref="template">

所創造的環境!創造的-環境!

從該context内容詳情中我們是不是看到了熟悉的内容。哪裡熟悉?即我們在vue開發中,經常在下截圖中使用

this.$el、this.$data、this.$refs

等,還記得不?

别問我[vue],VNode、elm、context、el是個啥?

說明了什麼?說明我們上截圖中的上下文環境context即我們使用的this,this指向context。而context上下文環境的類型也是VueComponent。明白了吧?!

[

注意:目前結論是以目前案例的層次結構為例子來說明

]

别問我[vue],VNode、elm、context、el是個啥?

從這個模闆嵌套層次看,

<.script>

中使用的this就是上下文環境,且上下文環境類型是VueComponent。

context中的

$el

:是 指建立目前上下文環境的标簽對象。與指令函數中的el和vnode中的

$elm

不同。context中的

$el

是在指令函數中

el

和vnode中的

$elm

的更外層标簽。而指令函數中的el和vnode中的

$elm

是相同的。

讀懂了以上内容就能清楚以上兩個問題了

1,VNode、elm、context、el ~ 他們是個啥,擔任着什麼樣子的角色 ?

2,指令函數參數中el的解釋,那麼與VNode中的elm有什麼差別?與VNode中的context中的el又有什麼差別?

參考文獻:

https://github.com/answershuto/learnVue/blob/master/docs/VNode

https://cn.vuejs.org