Vue2.0源碼閱讀筆記(十):指令

  指令是帶有 v- 前綴的特殊特性,當表達式的值改變時,將其產生的連帶影響,響應式地做用於 DOM。
  Vue2.0 內置了形如v-bind、v-on等指令,若是須要對普通 DOM 元素進行底層操做還可使用自定義指令。
javascript

1、自定義指令

  在 Vue2.0 中,能夠經過自定義指令對普通 DOM 元素進行底層操做。一個指令定義對象能夠提供以下幾個鉤子函數:
html

一、bind:指令第一次綁定到元素時調用,只調用一次。
二、inserted:被綁定元素插入父節點時調用。
三、update:所在組件的VNode更新時調用。
四、componentUpdated:指令所在組件的VNode及其子VNode所有更新後調用。
五、unbind:指令與元素解綁時調用,只調用一次。
前端

  經過以下示例來闡述源碼中對自定義指令的處理過程:
vue

<body>
  <div id="app"></div>
</body>
<script> let vm = new Vue({ el: '#app', template: '<div>' + '<input v-focus>' + '</div>', directives: { focus: { inserted: function (el) { el.focus() } } } }) </script>
複製代碼

一、全局註冊與局部註冊

  指令組件過濾器在Vue中稱爲資源。能夠經過全局API進行全局註冊,也能夠經過具體選項進行局部註冊。組件的全局註冊和組件註冊在《組件》一文中詳細闡述過,指令的處理與之相似,這裏簡要說明。
  自定義指令全局註冊的方法以下所示:
java

Vue.directive = function (id, definition) {
  if (!definition) {
    return this.options.directives[id]
  } else {
    if (typeof definition === 'function') {
      definition = { bind: definition, update: definition };
    }
    Vue.options.directives[id] = definition;
    return definition
  }
}
複製代碼

  Vue.directive 方法功能比較簡單:將自定義指令的名字與配置對象轉化成 Vue.options.directives 對象上的鍵值對。當配置對象爲函數時,將該函數當成 bind 與 update 的鉤子函數內容來處理,這是由於Vue提供了這種函數簡寫的方式,在《選項合併》中有過詳細闡述。
  使用 directives 選項來註冊指令,會將自定義指令信息存儲在當前組件實例的 $options.directives 對象上。
node

二、模板編譯

  帶有自定義指令的標籤在生成AST時,會調用 processElement 函數對自定義指令進行處理。
express

function processElement (element,options) {
  /* ... */
  processAttrs(element);
  return element
}
複製代碼

  processElement 函數會將標籤上的屬性解析到元素對象的 attrsList 與 attrsMap 屬性中,而後調用 processAttrs 函數處理標籤上的屬性。若是有指令屬性,則將其放入到元素對象的 directives 數組屬性中。
  模板編譯的 codegen 階段,在執行 genData 時會根據 el.directives 將指令信息存入到 el.data 字符串中。
  渲染函數最終根據標籤名稱el.tag、標籤數據el.data、子節點children共同生成。實例中的模板通過編譯後生成的渲染函數以下所示:
數組

_c(
  'div',
  [
    _c(
      'input',
      {
        directives:[
          {
            name:"focus",
            rawName:"v-focus"
          }
        ]
      }
    )
  ]
)
複製代碼

三、生成VNode

  調用 Vue.prototype._render 方法生成VNode,本質是經過調用渲染函數來完成的。渲染函數中的 _c() 是 createElement 的別稱,在函數內部經過調用 _createElement 函數來生成VNode。
app

function _createElement (context,tag,data,children,normalizationType){
  /* ... */
  if (config.isReservedTag(tag)) {
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,undefined, undefined, context
    );
  }
  /* ... */    
}
複製代碼

  根據示例的渲染函數生成的VNode以下所示:
dom

vnode = {
  tag: "div",
  children: [
    {
      tag: "input",
      data: {
        directives: [
          {
            name: "focus",
            rawName: "v-focus"
          }
        ]
      }
      /* 省略其它屬性 */
    }
  ]
  /* 省略其它屬性 */
}
複製代碼

四、patch

  在 patch 過程當中,會調用 createElm 函數來生成真實DOM並插入到DOM樹中。

function createElm (/* ... */){
    /* 省略... */
    createChildren(vnode, children, insertedVnodeQueue);
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
    insert(parentElm, vnode.elm, refElm);
    /* 省略... */
}

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (var i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode);
  }
  i = vnode.data.hook;
  if (isDef(i)) {
    if (isDef(i.create)) { i.create(emptyNode, vnode); }
    if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
  }
}
複製代碼

  關於 cbs 中各階段的鉤子函數的詳細闡述可參看《Virtual DOM》

cbs = {
  create: [
    /* 省略... */
    function updateDirectives (oldVnode, vnode) {/*省略具體代碼*/}
  ]
  /* 省略... */
}
複製代碼

  在 updateDirectives 方法中,若是虛擬DOM的 data.directives 屬性存在,會調用內部方法 _update 。該方法比較很重要,自定義指令提供的鉤子都在該函數中進行處理,下面分步詳細解讀該函數:

function _update (oldVnode, vnode) {
  var isCreate = oldVnode === emptyNode;
  var isDestroy = vnode === emptyNode;
  var oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context);
  var newDirs = normalizeDirectives(vnode.data.directives, vnode.context);

  var dirsWithInsert = [];
  var dirsWithPostpatch = [];
  /* 省略... */
}
複製代碼

  函數首先定義一些變量,變量的具體含義以下所示:

isCreate:指令所在的元素節點是否被建立。
isDestroy:指令所在的元素節點是否被銷燬。
oldDirs:舊元素節點上的指令。
newDirs:新元素節點上的指令。
dirsWithInsert:擁有 inserted 鉤子函數的指令。
dirsWithPostpatch:擁有 componentUpdated 鉤子函數的指令。

  在 _update 函數中,會調用 callHook 來調用具體的鉤子函數。

function callHook (dir, hook, vnode, oldVnode, isDestroy) {
  var fn = dir.def && dir.def[hook];
  if (fn) {
    try {
      fn(vnode.elm, dir, vnode, oldVnode, isDestroy);
    } catch (e) {
      handleError(e, vnode.context, ("directive " + (dir.name) + " " + hook + " hook"));
    }
  }
}
複製代碼

  接着說 _update 函數,在定義變量以後處理新VNode存在的狀況。代碼以下所示:

function _update (oldVnode, vnode) {
  /* 省略... */
  var key, oldDir, dir;
  for (key in newDirs) {
    oldDir = oldDirs[key];
    dir = newDirs[key];
    if (!oldDir) {
      callHook(dir, 'bind', vnode, oldVnode);
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir);
      }
    } else {
      dir.oldValue = oldDir.value;
      dir.oldArg = oldDir.arg;
      callHook(dir, 'update', vnode, oldVnode);
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir);
      }
    }
  }
  /* 省略... */
}
複製代碼

  當新VNode存在而舊VNode不存在時,說明新VNode是新建立的,未與自定義指令綁定,此時第一次綁定調用 bind 鉤子函數,如有 inserted 鉤子函數,則將指令存入 dirsWithInsert 數組。
  當新VNode和舊VNode都存在時,說明是在進行VNode更新。此時調用 update 鉤子函數,如有 componentUpdated 鉤子函數,則將指令存入 dirsWithPostpatch 數組。
  而後是對 inserted 與 componentUpdated 鉤子函數的處理:

function _update (oldVnode, vnode) {
  /* 省略... */
  if (dirsWithInsert.length) {
    var callInsert = function () {
      for (var i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode);
      }
    };
    if (isCreate) {
       mergeVNodeHook(vnode, 'insert', callInsert);
    } else {
      callInsert();
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch', function () {
      for (var i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
      }
    });
  }
  /* 省略... */
}
複製代碼

  mergeVNodeHook 函數接收三個參數:def、 hookKey、hook。若是第一個參數 def 是VNode類型,則會替換成 def.data.hook。mergeVNodeHook 的功能是:若是def[hookKey] 不存在,則直接調用hook,若是存在則將hook合併存儲起來,在後續合適時機調用。
  由代碼能夠看出,對指令 inserted 鉤子函數的處理是:若VNode是新建立的,則會把 dirsWithInsert 數組中的函數追加到 vnode.data.hook.insert 中執行。若是是更新VNode,則直接執行鉤子函數。
  對指令 componentUpdated 鉤子函數的處理是:使用 mergeVNodeHook 函數進行處理,等待後面子組件所有更新完成後調用。

function _update (oldVnode, vnode) {
  /* 省略... */
  if (!isCreate) {
    for (key in oldDirs) {
      if (!newDirs[key]) {
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
      }
    }
  }
}
複製代碼

  _update 函數的最後是對 unbind 鉤子函數的處理,在舊VNode存在而新VNode不存在時,即指令與元素解綁時調用 unbind 鉤子函數。

2、v-bind

  使用 v-bind 指令能夠動態地綁定一個或多個特性,或一個組件 prop 到表達式,v-bind 指令能夠簡寫爲 。由於字符串拼接麻煩且易錯,在將 v-bind 用於 class 和 style 時,Vue 作了專門的加強。因此 v-bind 指令的使用分爲三種狀況:普通屬性、class、style。
  示例代碼以下所示:

<body>
  <div id="app"></div>
</body>
<script> let vm = new Vue({ el: '#app', template: `<div> <div v-bind:id="id" :class="{ red: isRed }" :style="{ fontSize: size + 'px' }" >666</div> </div>`, data () { return { id: 123, size: 24, isRed: true } } }) </script>
複製代碼

  在模板編譯的 parse 階段會調用 processElement 函數,在該函數的最後分別調用 transforms 數組中的函數來解析 v-bind 綁定的 class 和 style,最後用 processAttrs 函數來解析 v-bind 綁定的普通屬性。

function processElement (element,options) {
    /* 省略... */
    for (var i = 0; i < transforms.length; i++) {
      element = transforms[i](element, options) || element;
    }
    processAttrs(element);
    return element
}

transforms = [
  function transformNode (el, options) {
    /* ... */
    if(staticClass){el.staticClass=JSON.stringify(staticClass);}
    var classBinding = getBindingAttr(el, 'class', false);
    if(classBinding){el.classBinding = classBinding;}
  },
  function transformNode (el, options) {
    /* ... */
    var styleBinding = getBindingAttr(el, 'style', false);
    if (styleBinding) {el.styleBinding = styleBinding;}
  }
]
複製代碼

  通過 processElement 函數處理後,v-bind 綁定的普通屬性會存入元素節點的 attrs 屬性中,class 與 style 會分別存入 classBinding 與 styleBinding 中。

ast = {
  tag: "div",
  children: [
    {
      tag: "div",
      hasBindings: true,
      attrs: [{/* 省略屬性id詳情 */}],
      attrsList: [{/* 省略屬性id詳情 */}],
      attrsMap: {
        :class: "{ red: isRed }",
        :style: "{ fontSize: size + 'px' }",
        v-bind:id: "id"
      },
      rawAttrsMap: {
        /* 省略屬性v-bind:id、:class、:style詳情 */
      }
      styleBinding: "{ fontSize: size + 'px' }",
      classBinding: "{ red: isRed }"
      /* 省略其它屬性... */
    }
  ]
  /* 省略其它屬性... */
}
複製代碼

  在模板編譯的 codegen 階段會調用 genElement 函數,並在該函數中調用 genData 函數來將 v-bind 綁定的普通屬性、class 與 style 合併到 data 中。示例最終生成的渲染函數以下所示:

_c(
  'div',
  [
    _c(
      'div',
      {
        class:{ red: isRed },
        style:({ fontSize: size + 'px' }),
        attrs:{"id":id}
      },
      [_v("666")]
    )
  ]
)
複製代碼

  渲染函數通過 Vue.prototype._render 函數處理後生成 VNode,_c() 函數的第二個參數會處理成元素標籤 VNode 的 data 屬性。

vnode = {
  tag: "div",
  children: [
    {
      tag: "div",
      data: {
        attrs: {id: 123}
        class: {red: true}
        style: {fontSize: "24px"}
      }
      /* 省略其它屬性... */
    }
  ]
  /* 省略其它屬性... */
}
複製代碼

  在 patch 階段,會的調用 createElm 函數生成真實 DOM,在createElm 函數中生成真實 DOM 後會調用 invokeCreateHooks 來對 data 中的數據進行處理。

function createElm (/*...*/){
  /*...*/
  var data = vnode.data;
  if (isDef(data)) {
    invokeCreateHooks(vnode, insertedVnodeQueue);
  }
  /*...*/
}

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (var i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, vnode);
  }
  i = vnode.data.hook;
  if (isDef(i)) {
    if(isDef(i.create)){i.create(emptyNode, vnode);}
    if(isDef(i.insert)){insertedVnodeQueue.push(vnode);}
  }
}
複製代碼

  關於 cbs 的具體組成能夠查看《Virtual DOM》 一文,跟 v-bind 相關的部分以下所示:

cbs = {
  create: [
    function updateAttrs (oldVnode, vnode) {/*省略具體代碼*/},
    function updateClass (oldVnode, vnode) {/*省略具體代碼*/},
    function updateStyle (oldVnode, vnode) {/*省略具體代碼*/},
    /* updateDOMProps函數做用是更新一些特殊的屬性: 不能經過 setAttribute 設置, 而是應該直接經過 DOM 元素設置的屬性。 好比:value、checked等 */
    function updateDOMProps (oldVnode, vnode) {/*省略具體代碼*/},
    /* 省略其它函數 */
  ]
  /* 省略其它屬性 */
複製代碼

   使用 v-bind 指令修飾符 .prop 綁定的屬性會放入 vnode.data.domProps 中,使用 updateDOMProps 進行處理,這裏省略具體處理邏輯。
   對普通屬性的處理函數 updateAttrs 邏輯比較簡單:對比新舊VNode,來決定增長仍是刪除屬性,增長屬性調用原生DOM的 setAttribute 方法,刪除屬性調用原生DOM的 removeAttribute 方法。在該函數中有對 IE 的兼容處理。

function updateAttrs (oldVnode, vnode) {
  /* 省略... */
  var oldAttrs = oldVnode.data.attrs || {};
  var attrs = vnode.data.attrs || {};

  for (key in attrs) {
    cur = attrs[key];
    old = oldAttrs[key];
    if (old !== cur) {
      setAttr(elm, key, cur);
    }
  }

  for (key in oldAttrs) {
    if (isUndef(attrs[key])) {
      if (isXlink(key)) {
        elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
      } else if (!isEnumeratedAttr(key)) {
        elm.removeAttribute(key);
      }
    }
  }
  /* 省略... */
}
複製代碼

  對 class 處理的函數 updateClass 邏輯是:將Vue中使用的靜態類staticClass 與使用響應式數據相關的 dynamicClass 統一塊兒來,而後和普通屬性同樣調用 DOM 原生方法 setAttribute 添加類名。

function updateClass (oldVnode, vnode) {
  /* 省略.... */
  var cls = genClassForVnode(vnode);
  var transitionClass = el._transitionClasses;
  if (isDef(transitionClass)) {
    cls = concat(cls, stringifyClass(transitionClass));
  }
  if (cls !== el._prevClass) {
    el.setAttribute('class', cls);
    el._prevClass = cls;
  }
}
複製代碼

  對 style 處理的函數 updateStyle 有一點比較特殊,設置 style 屬性時是調用 dom.style.setProperty 方法。
  v-bind 指令中還有一點須要注意:能夠添加 .sync 修飾符對一個 prop 進行「雙向綁定」。.sync 修飾符實質上是語法糖,會擴展成一個更新父組件綁定值的 v-on 偵聽器,v-on 指令將在下一節詳細介紹。

// 語法糖
<Child v-bind:val.sync = parentVal></Child>

// 至關於下面代碼
<Child v-bind:val = parentVal
       @updateVal = "parentVal.a=$event">
</Child>
複製代碼

3、v-on

  在 Vue 中用 v-on 指令監聽 DOM 事件,並在觸發時運行一些 JavaScript 代碼。由於事件指令使用較多,Vue提供了簡寫形式:@。
  示例代碼以下所示:

<body>
    <div id="app"></div>
</body>
<script> let Child = { template: `<div @click="changeVal">點擊</div>`, props: ['val'], methods: { changeVal () { this.$emit('updateVal', ++this.val.a) } } } let vm = new Vue({ el: '#app', template: `<div> <Child v-bind:val = parentVal @updateVal = "parentVal.a=$event" @mouseover.native = "printMsg" ></Child> <div v-on:mouseover = "showMsg" @mouseout.stop = "hideMsg"> {{parentVal.a}} </div> <div>{{message}}</div> </div>`, data() { return { parentVal: { a: 1 }, message: '離開' } }, methods: { printMsg (){console.log(this.message)}, showMsg () { this.message = '進入' }, hideMsg () { this.message = '離開' }, }, components: { Child } }) </script>
複製代碼

一、模板編譯

  在模板編譯的 parse 階段,會調用 processAttrs 處理事件屬性。通過 processAttrs 函數處理後會爲不帶 native 修飾符的節點添加 events 屬性,events 對象包含各個事件信息,其中修飾符存儲在事件對象的 modifiers 屬性中。對於在組件上帶 native 修飾符的DOM事件則存儲在 nativeEvents 屬性中。
  通過 parse 生成的 AST 以下所示:

ast = {
  tag: "div",
  children: [
    {
      tag: "Child",
      attrs: {
        name: "val",
        value: "parentVal"
        /* 省略其它屬性 */
      },
      events: {
        updateVal: {
          value: "parentVal.a=$event"
          /* 省略其它屬性 */
        }
      },
      nativeEvents: {
        mouseover: {/* 省略具體屬性 */}
      }
    },
    {
      tag: "div",
      events: {
        mouseout: {
          value: "hideMsg"
          modifiers: {stop: true}
          /* 省略其它屬性 */
        },
        mouseover: {
          value: "showMsg"
          /* 省略其它屬性 */
        }
      }
    }
    /* 省略其它子節點 */
  ]
  /* 省略其它屬性 */
}
複製代碼

  在 codegen 階段調用 genElement 生成渲染函數字符串時,會調用 genData 方法將普通事件信息存儲到 data.on 屬性中,將組件上的DOM原生事件存儲到 data.nativeOn 屬性中。最終生成以下渲染函數:

_c(
  'div',
  [
    _c(
      'Child',
      {
        attrs:{"val":parentVal},
        on:{
          "updateVal":function($event){
            parentVal.a=$event
          }
        },
        nativeOn:{
          "mouseover":function($event){
            return printMsg($event)
          }
        }
      }
    ),
    _c(
      'div',
      {
        on:{
          "mouseover":showMsg,
          "mouseout":function($event){
            $event.stopPropagation();
            return hideMsg($event)
          }
        }
      },
      /* 省略子節點渲染函數 */
    )
    /* 省略其它子節點渲染函數 */
  ],
  1
)
複製代碼

二、生成VNode

  在生成VNode的過程當中會調用 _createElement 函數生成元素 VNode,具體實現是經過調用 new VNode() 完成的。VNode() 構造函數會根據類型的不一樣而作出不一樣處理,若是是元素VNode則將事件信息直接放到 data 屬性上,若是是組件VNode則將其放到 componentOptions.listeners 上,對於組件上的原生DOM屬性,則將其從 data.nativeOn 複製到 data.on 上。
  根據渲染函數生成的VNode以下所示:

vnode = {
  tag: "div",
  children: [
    {
      tag: "vue-component-1-Child",
      data: {
        attrs: {},
        on: {
          mouseover: function($event){
            return printMsg($event)
          }
        },
        nativeOn: {
          mouseover: function($event){
            return printMsg($event)
          }
        },
        hook: {
          destroy: function(){},
          init: function(){},
          insert: function(){},
          prepatch: function(){}
        }
      },
      componentOptions: {
        tag: "Child",
        listeners: {
          updateVal: function($event){parentVal.a=$event}
        }
        /* 省略其它屬性 */
      }
      /* 省略其它屬性 */
    },
    {
      tag: "div",
      data: {
        on: {
          mouseout: function($event){
            $event.stopPropagation();
            return hideMsg($event)
          },
          mouseover: function () { [native code] }
        }
      }
      /* 省略其它屬性 */  
    }
    /* 省略其它子節點 */
  ]
  /* 省略其它屬性 */
}
複製代碼

三、原生DOM事件的處理

  在 patch 階段對元素標籤上 v-on 的處理跟前面提到的自定義指令、v-bind相似,最終會調用 cbs.create.updateDOMListeners 來處理事件。

function updateDOMListeners (oldVnode, vnode) {
  /* ... */
  var on = vnode.data.on || {};
  var oldOn = oldVnode.data.on || {};
  target = vnode.elm;
  normalizeEvents(on);
  updateListeners(on,oldOn,add,remove,createOnceHandler,vnode.context);
  target = undefined;
}

function updateListeners(on,oldOn,add,remove,createOnceHandler,vm){
    var name, def, cur, old, event;
    for (name in on) {
      def = cur = on[name];
      old = oldOn[name];
      event = normalizeEvent(name);
      if (isUndef(cur)) {
        /* ... */
      } else if (isUndef(old)) {
        if (isUndef(cur.fns)) {
          cur = on[name] = createFnInvoker(cur, vm);
        }
        if (isTrue(event.once)) {
          cur = on[name] = createOnceHandler(event.name, cur, event.capture);
        }
        add(event.name, cur, event.capture, event.passive, event.params);
      } else if (cur !== old) {
        old.fns = cur;
        on[name] = old;
      }
    }
    for (name in oldOn) {
      if (isUndef(on[name])) {
        event = normalizeEvent(name);
        remove(event.name, oldOn[name], event.capture);
      }
    }
  }
複製代碼

  updateDOMListeners 函數做用是提取出新舊VNode的 on 屬性,規範化新節點的 on 屬性後調用 updateListeners 函數來處理。
  updateListeners 主要功能是對比新舊VNode,若是新節點須要添加事件就調用 add 方法,本質是調用原生DOM的 addEventListener 方法爲元素添加事件監聽。若是新節點須要移除事件,就調用 remove,本質是調用原生DOM的 removeEventListener 方法刪除元素上的事件監聽。另外,updateListeners 函數中也有對各類修飾符的處理。
  在 patch 階段對組件上 v-on 的處理分爲兩種:對原生DOM事件的處理、自定義事件的處理。原生DOM事件存儲在 data.on 上,所以處理方式與元素標籤的狀況同樣。

四、自定義事件的處理

  在《選項合併》中,講述實例初始化方法 Vue.prototype._init 時跳過了處理組件的代碼:

Vue.prototype._init = function (options) {
  /* 省略.... */
  if (options && options._isComponent) {
    initInternalComponent(vm, options);
  }
  /* 省略.... */
}

function initInternalComponent (vm, options) {
  var opts = vm.$options = Object.create(vm.constructor.options);
  var parentVnode = options._parentVnode;
  /* 省略.... */
  var vnodeComponentOptions = parentVnode.componentOptions;
  opts._parentListeners = vnodeComponentOptions.listeners;
  /* 省略.... */
}
複製代碼

  通過 initInternalComponent 函數處理後會將父組件的 componentOptions.listeners 賦值給子組件的 _parentListeners 屬性。在子組件調用初始化事件函數 initEvents 時會處理 listeners。

function initEvents (vm) {
  vm._events = Object.create(null);
  vm._hasHookEvent = false;

  var listeners = vm.$options._parentListeners;
   if (listeners) {
    updateComponentListeners(vm, listeners);
  }
}

function updateComponentListeners(vm,listeners,oldListeners){
  target = vm;
  updateListeners(listeners,oldListeners||{},add,remove,createOnceHandler,vm);
  target = undefined;
}
複製代碼

  從上述代碼中能夠看出,自定義事件的處理最終是經過 updateListeners 函數來完成的:

function updateListeners(on,oldOn,add,remove,createOnceHandler,vm){
    var name, def, cur, old, event;
    for (name in on) {
      def = cur = on[name];
      old = oldOn[name];
      event = normalizeEvent(name);
      if (isUndef(cur)) {
        /* 省略... */
      } else if (isUndef(old)) {
        if (isUndef(cur.fns)) {
          cur = on[name] = createFnInvoker(cur, vm);
        }
        if (isTrue(event.once)) {
          cur = on[name] = createOnceHandler(event.name, cur, event.capture);
        }
        add(event.name,cur,event.capture,event.passive,event.params);
      } else if (cur !== old) {
        old.fns = cur;
        on[name] = old;
      }
    }
    for (name in oldOn) {
      if (isUndef(on[name])) {
        event = normalizeEvent(name);
        remove(event.name,oldOn[name],event.capture);
      }
    }
  }
複製代碼

  自定義事件與原生DOM事件處理的最大不一樣就是調用的添加事件函數 add() 與刪除事件函數 remove()不一樣。

function add (event, fn) {
  target.$on(event, fn);
}

function remove (event, fn) {
  target.$off(event, fn);
}
複製代碼

  自定義事件的添加與刪除最終是調用了實例方法 $on 與 $off 來完成的,自定義事件的觸發是調用實例方法 $emit 來完成,這些實例方法都是暴露出來的API,其實現原理在下一篇文章詳述。

4、v-for

  Vue 使用 v-for 指令完成基於源數據屢次渲染元素或模板塊的功能,循環渲染的數據源能夠是數組或者對象,2.6版本以後數據源也能夠是可迭代的值,好比原生的 Map 和 Set。
  示例代碼以下所示:

<body>
  <div id="app"></div>
</body>
<script> let vm = new Vue({ el: '#app', template: `<div> <div v-for="(item,index) in colors" :key="index"> {{index}}:{{item}} </div><div v-for="(item,name,index) in object" :key="name"> {{index}}:{{name}}:{{item}} </div> </div>`, data() { return { colors: ['red','blue','green'], object: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } } }) </script>
複製代碼

  通過模板編譯處理後,生成以下渲染函數:

_c(
  'div',
  [
    _l(
      (colors),
      function(item,index){
        return _c(
          'div',
          {key:index},
          [_v("\n"+_s(index)+":"+_s(item)+"\n")]
        )
      }
    ),
    _l(
      (object),
      function(item,name,index){
        return _c(
          'div',
          {key:name},
          [_v("\n "+_s(index)+":"+_s(name)+":"+_s(item)+"\n")]
        )
      }
    )
  ],
  2
)
複製代碼

  能夠看到,v-for 所在的模板最終會轉化成 _l() 函數,_l() 函數是 renderList 的別稱。

function renderList (val, render) {
  var ret, i, l, keys, key;
  if (Array.isArray(val) || typeof val === 'string') {
    ret = new Array(val.length);
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i);
    }
  } else if (typeof val === 'number') {
    ret = new Array(val);
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i);
    }
  } else if (isObject(val)) {
    if (hasSymbol && val[Symbol.iterator]) {
      ret = [];
      var iterator = val[Symbol.iterator]();
      var result = iterator.next();
      while (!result.done) {
        ret.push(render(result.value, ret.length));
        result = iterator.next();
      }
    } else {
      keys = Object.keys(val);
      ret = new Array(keys.length);
      for (i = 0, l = keys.length; i < l; i++) {
        key = keys[i];
        ret[i] = render(val[key], key, i);
      }
      
  }
  if (!isDef(ret)) {
    ret = [];
  }
  (ret)._isVList = true;
  return ret
}
複製代碼

  renderList 函數主要功能是生成 VNode 數組,其中具體 VNode 的生成依然是經過 _c() 函數來完成的。
  由上述代碼能夠看出,v-for 指令能夠處理的數據源類型有四種:數組、數字、可迭代對象與普通對象。

5、v-if、v-else

  v-if 指令根據表達式的值的真假條件渲染元素。若是元素是 <template> ,將提出它的內容做爲條件塊。v-else 指令來表示 v-if 的「else 塊」,v-else-if 指令充當 v-if 的「else-if 塊」,能夠連續使用。
  示例代碼以下所示:

<body>
  <div id="app"></div>
</body>
<script> let vm = new Vue({ el: '#app', template: `<div> <div v-if="type === 'A'">A</div> <div v-else-if="type === 'B'">B</div> <div v-else>C</div> <div v-if="color === 'blue'">blue</div> </div>`, data() { return { type: 'A', color: 'blue' } } }) </script>
複製代碼

  在模板編譯的 parse 階段,會使用 processIfConditions 函數處理條件渲染指令的內容。

function processIfConditions (el, parent) {
  var prev = findPrevElement(parent.children);
  if (prev && prev.if) {
    addIfCondition(prev, {
      exp: el.elseif,
      block: el
    });
  } else {
    /* 省略警告信息 */
  }
}
複製代碼

  生成的 AST 以下所示:

ast = {
  tag: "div",
  type: 1,
  children: [
    {
      type: 1,
      tag: "div",
      if: "type === 'A'",
      ifProcessed: true,
      ifConditions: [
        {
          exp: "type === 'A'",
          block: {/* 省略具體 */}
        },
        {
          exp: "type === 'B'",
          block: {/* 省略具體 */}
        },
        {
          exp: undefined,
          block: {/* 省略具體 */}
        }
      ]
    },
    {
      type: 1,
      tag: "div",
      if: "color === 'blue'",
      ifProcessed: true,
      ifConditions: [
        {
          exp: "color === 'blue'",
          block: {/* 省略具體 */}
        }
      ]
    }
  ]
  /* 省略其它屬性 */
}
複製代碼

  在模板編譯的 codegen 階段,會調用 genIf 函數處理 v-if 所在的標籤:

function genIf(el,state,altGen,altEmpty){
  el.ifProcessed = true;
  return genIfConditions(el.ifConditions.slice(),state,altGen,altEmpty)
}

function genIfConditions(conditions,state,altGen,altEmpty){
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  var condition = conditions.shift();
  if (condition.exp) {
    return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
  } else {
    return ("" + (genTernaryExp(condition.block)))
  }

  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}
複製代碼

  從代碼中能夠看出,v-if 指令會轉化成三目運算符的形式,最終生成的渲染函數以下所示:

_c(
  'div',
  [
    (type === 'A')?_c('div',[_v("A")]):(type === 'B')?_c('div',[_v("B")]):_c('div',[_v("C")]),
    _v(" "),
    (color === 'blue')?_c('div',[_v("blue")]):_e()
  ]
)
複製代碼

  帶有 v-if 指令的模板會編譯成根據數據源真假值來調用具體輔助方法的渲染函數,v-if 會根據數據源真假值來決定是否渲染該節點,這一點與 v-show 不一樣。

6、v-show

  v-show 指令根據表達式之真假值,切換元素的 display CSS 屬性。當條件變化時該指令觸發過渡效果。
  v-if 是「真正」的條件渲染,由於它會確保在切換過程當中條件塊內的事件監聽器和子組件適當地被銷燬和重建。v-show 就簡單得多:無論初始條件是什麼,元素老是會被渲染,而且只是簡單地基於 CSS 進行切換。
  示例代碼以下所示:

<body>
  <div id="app"></div>
</body>

<script> let vm = new Vue({ el: '#app', template: `<div> <h1 v-show="hello">Hello</h1> <h1 v-show="world">World</h1> </div>`, data() { return { hello: true, world: false } } }) </script>
複製代碼

  在模板編譯和生成VNode的過程當中,v-show指令與自定義指令的過程同樣,示例生成的渲染函數以下所示:

_c(
  'div',
  [
    _c(
      'h1',
      {
        directives:[
          {
            name:"show",
            rawName:"v-show",
            value:(hello),
            expression:"hello"
          }
        ]
      },
      [_v("Hello")]
    ),
    _v(" "),
    _c(
      'h1',
      {
        directives:[
          {
            name:"show",
            rawName:"v-show",
            value:(world),
            expression:"world"
          }
        ]
      },
      [_v("World")]
    )
  ]
)
複製代碼

  在調用處理指令的鉤子函數 updateDirectives 時,v-show 指令有所不一樣,至關於 v-show 內部實現了自定義指令的 bind、update、unbind 三個階段的鉤子函數。

export default {
  bind (el, { value }, vnode) {
    vnode = locateNode(vnode)
    const transition = vnode.data && vnode.data.transition
    const originalDisplay = el.__vOriginalDisplay =
      el.style.display === 'none' ? '' : el.style.display
    if (value && transition) {
      vnode.data.show = true
      enter(vnode, () => {
        el.style.display = originalDisplay
      })
    } else {
      el.style.display = value ? originalDisplay : 'none'
    }
  },

  update (el, { value, oldValue }, vnode) {
    if (!value === !oldValue) return
    vnode = locateNode(vnode)
    const transition = vnode.data && vnode.data.transition
    if (transition) {
      vnode.data.show = true
      if (value) {
        enter(vnode, () => {
          el.style.display = el.__vOriginalDisplay
        })
      } else {
        leave(vnode, () => {
          el.style.display = 'none'
        })
      }
    } else {
      el.style.display = value ? el.__vOriginalDisplay : 'none'
    }
  },

  unbind (el,binding,vnode,oldVnode,isDestroy){
    if (!isDestroy) {
      el.style.display = el.__vOriginalDisplay
    }
  }
}
複製代碼

  從上述代碼能夠看到,v-show 指令僅僅是經過調用 DOM.style.display 的值來顯示和隱藏DOM元素。關於 v-show 指令觸發過渡效果的原理在《內置組件》一文中已經闡述過。

7、v-model

  v-model 指令用於在表單控件或者組件上建立雙向綁定,所謂雙向綁定是指除了數據驅動視圖改變外,DOM視圖的改變也會引發數據的改變。

一、用法

  v-model 指令能夠在表單控件和組件上使用,表單控件包含有:<input>、<select>、<textarea>,能夠在指令後添加修飾符:

.lazy:取代 input 監聽 change 事件。
.number:輸入字符串轉爲有效的數字。
.trim:輸入首尾空格過濾。

  v-model 會忽略全部表單元素的 value、checked、selected 特性的初始值而老是將 Vue 實例的數據做爲數據來源。v-model 在內部爲不一樣的輸入元素使用不一樣的屬性並拋出不一樣的事件:

一、text 和 textarea 元素使用 value 屬性和 input 事件。
二、checkbox 和 radio 使用 checked 屬性和 change 事件。
三、select 字段將 value 做爲 prop 並將 change 做爲事件。

  v-model 指令表單元素上的使用示例以下所示:

<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>

<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
複製代碼

  在組件上使用 v-model 默認會利用名爲 value 的 prop 和名爲 input 的事件,可是像單選框、複選框等類型的輸入控件可能會將 value 特性用於不一樣的目的。model 選項能夠用來避免這樣的衝突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: ` <input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)"> `
})

<base-checkbox v-model="lovingVue"></base-checkbox> 複製代碼

  這裏的 lovingVue 的值將會傳入這個名爲 checked 的 prop。同時當 <base-checkbox> 觸發一個 change 事件並附帶一個新的值的時候,這個 lovingVue 的屬性將會被更新。
  儘管 model 選項中已經聲明瞭 prop 屬性,可是仍須要在組件的 props 選項裏聲明 checked 這個 prop。

二、表單元素

  這裏借用官網的示例來闡述 v-model 指令在表單元素:

<body>
  <div id="app"></div>
</body>

<script> let vm = new Vue({ el: '#app', template: `<div> <input v-model="message" placeholder="edit me"> <p>Message is: {{ message }}</p> </div>`, data() { return { message: '' } } }) </script>
複製代碼

  在模板編譯的 parse 階段,v-model 與前面講的指令同樣,會被 processAttrs 函數將其放入到元素對象的 directives 數組屬性中。而後在 codegen 階段調用 genDirectives 函數來處理指令:

function genDirectives (el, state) {
  var dirs = el.directives;
  if (!dirs) { return }
  var res = 'directives:[';
  var hasRuntime = false;
  var i, l, dir, needRuntime;
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i];
    needRuntime = true;
    var gen = state.directives[dir.name];
    if (gen) {
      needRuntime = !!gen(el, dir, state.warn);
    }
    if (needRuntime) {
      hasRuntime = true;
      res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}
複製代碼

  v-model 指令比較特殊的地方在於 state.directives.model 函數是真實存在的,也就是說 gen 的值爲 true。

var gen = state.directives.model;
複製代碼

  state.directives.model 函數以下所示:

function model (el,dir,_warn) {
  warn = _warn;
  var value = dir.value;
  var modifiers = dir.modifiers;
  var tag = el.tag;
  var type = el.attrsMap.type;

  /* 省略警告信息 */
  if (el.component) {
    genComponentModel(el, value, modifiers);
    return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers);
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers);
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers);
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers);
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers);
    return false
  } else {
    /* 省略警告信息 */
  }

  return true
}
複製代碼

  示例代碼 tag 值爲 input,所以會調用 genDefaultModel 方法:

function genDefaultModel (el,value,modifiers) {
  var type = el.attrsMap.type;
  /* 省略v-bind與v-model值有衝突的警告信息 */
  var ref = modifiers || {};
  var lazy = ref.lazy;
  var number = ref.number;
  var trim = ref.trim;
  var needCompositionGuard = !lazy && type !== 'range';
  var event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input';

  var valueExpression = '$event.target.value';
  if (trim) {
    valueExpression = "$event.target.value.trim()";
  }
  if (number) {
    valueExpression = "_n(" + valueExpression + ")";
  }

  var code = genAssignmentCode(value, valueExpression);
  if (needCompositionGuard) {
    code = "if($event.target.composing)return;" + code;
  }

  addProp(el, 'value', ("(" + value + ")"));
  addHandler(el, event, code, null, true);
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()');
  }
}
複製代碼

  genDefaultModel 函數會根據指令的修飾符來進行分別處理,該函數的核心代碼以下所示:

addProp(el, 'value', ("(" + value + ")"));
addHandler(el, event, code, null, true);
複製代碼

  其實不只僅是 genDefaultModel 函數,model函數中處理其他幾種狀況的函數本質也是調用,addProp 與 addHandler 函數,這兩個函數分別將 v-model 綁定的值放入 el.props 與 el.events 中,進而轉化成對 v-bind 與 v-on 指令的處理。示例代碼生成的渲染函數以下所示:

_c(
  'div',
  [
    _c(
      'input',
      {
        directives:[
          {
            name:"model",
            rawName:"v-model",
            value:(message),
            expression:"message"
          }
        ],
        attrs:{
          "placeholder":"edit me"
        },
        domProps:{
          "value":(message)
        },
        on:{
          "input":function($event){
            if($event.target.composing)return;
            message=$event.target.value
          }
        }
      }
    ),
    _v(" "),
    _c('p',[_v("Message is: "+_s(message))])
  ]
)
複製代碼

  由此能夠看出:v-model 本質上一個語法糖,在模板編譯的階段會被拆分,分別被當作v-bind與v-on指令處理。

三、組件

  依舊借用官網實例來闡述 v-model 指令在組件上的使用狀況:

<body>
  <div id="app"></div>
</body>
<script> Vue.component('base-checkbox', { model: { prop: 'checked', event: 'change' }, props: { checked: Boolean }, template: ` <input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)" > ` }) let vm = new Vue({ el: '#app', template: `<div> <base-checkbox v-model="lovingVue"></base-checkbox> <div>{{lovingVue}}</div> </div>`, data() { return { lovingVue: false } } }) </script>
複製代碼

  在模板編譯的 codegen 階段依舊是調用 genDirectives 函數,與在表單元素上狀況不一樣的在 model 中最終會調用 genComponentModel 方法:

function genComponentModel(el,value,modifiers) {
  var ref = modifiers || {};
  var number = ref.number;
  var trim = ref.trim;

  var baseValueExpression = '$$v';
  var valueExpression = baseValueExpression;
  if (trim) {
    valueExpression =
      "(typeof " + baseValueExpression + " === 'string'" +
      "? " + baseValueExpression + ".trim()" +
      ": " + baseValueExpression + ")";
  }
  if (number) {
    valueExpression = "_n(" + valueExpression + ")";
  }
  var assignment = genAssignmentCode(value, valueExpression);

  el.model = {
    value: ("(" + value + ")"),
    expression: JSON.stringify(value),
    callback: ("function (" + baseValueExpression + ") {" + assignment + "}")
  };
}
複製代碼

  通過 genComponentModel 函數處理後父組件節點上會添加 model 屬性。在 parse 後續階段會調用 genData 函數,其中有對節點含有 model 屬性狀況的處理:

function genData (el, state) {
  var data = '{';
  /* 省略... */
  if (el.model) {
    data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
  }
  /* 省略... */
  return data
}
複製代碼

  示例代碼最終生成的渲染函數以下所示:

_c(
  'div',
  [
    _c(
      'base-checkbox',
      {
        model:{
          value:(lovingVue),
          callback:function ($$v) {
            lovingVue=$$v
          },
          expression:"lovingVue"
        }
      }
    ),
    _v(" "),
    _c('div',[_v(_s(lovingVue))])
  ],
  1
)
複製代碼

  在根據渲染函數生成 VNode 的過程當中,會調用 createComponent 函數生成組件類型VNode:

function createComponent(Ctor,data,context,children,tag){
  /* 省略... */
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }
  /* 省略... */
}
複製代碼

  若組件渲染函數第二個參數對象上有 model 屬性時會調用 transformModel 函數進行處理:

function transformModel (options, data) {
  var prop = (options.model && options.model.prop) || 'value';
  var event = (options.model && options.model.event) || 'input'
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value;
  var on = data.on || (data.on = {});
  var existing = on[event];
  var callback = data.model.callback;
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing);
    }
  } else {
    on[event] = callback;
  }
}
複製代碼

  transformModel 函數將 data.model.value 賦值給 data.props、將 data.model.callback 賦值給 data.on。data.props 與 data.on 中的屬性名是由 model 選項的值來決定,若是不傳該選項則默認 prop 爲 value,event 爲 input。
  由上可知,v-model 在組件上使用時最終也會轉化成 v-bind 與 v-on 狀況,只是與 v-model 在表單元素上使用時在模板編譯階段轉化不一樣,在組件上使用時是在生成 VNode 階段轉換的。

8、總結

  自定義指令用於對普通 DOM 元素進行底層操做,全局註冊會將自定義指令信息存儲在 Vue.options.directives 對象上,局部註冊會將信息存儲在組件實例的 $options.directives 對象上。在根據 VNode 生成真實DOM過程當中,會在合適的時機調用不一樣的鉤子函數。
  v-bind指令的使用分爲三種狀況:普通屬性、class、style。普通屬性與class是經過原生DOM的 setAttribute 與 removeAttribute方法添加和移除的;而設置 style 屬性時是調用原生DOM的 style.setProperty 方法。
  v-on指令用於綁定事件監聽器,原生DOM事件主要經過原生的 addEventListener 與 removeEventListener 方法來添加和刪除的。自定義事件是利用 Vue 定義的事件中心來實現的。
  v-for指令基於源數據屢次渲染元素或模板塊,其實現思路是在渲染函數生成VNode時,根據循環條件來生成多個 VNode。
  v-if指令根據表達式的值的真假條件渲染元素,v-if 指令生成的渲染函數是三目運算符的形式,會根據數據的真假條件來生成對應的VNode。
  v-show指令只是簡單地切換元素的 CSS 屬性 display,其內部實現至關於實現了 bind、update、unbind 鉤子函數的自定義指令。若在 Transition 組件上使用則調用 enter 與 leave 函數完成過渡效果。
  v-model指令用於在表單控件或者組件上建立雙向綁定,其本質是一個語法糖,會轉換成 v-bind 與 v-on 指令處理。在表單元素上使用時,這種轉化在模板編譯階段進行;在組件上使用時,是在根據渲染函數生成 VNode 階段進行。

歡迎關注公衆號:前端桃花源,互相交流學習!

相關文章
相關標籤/搜索