Vue2.0源码阅读笔记(十):指令

1,632 阅读13分钟

  指令是带有 v- 前缀的特殊特性,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
  Vue2.0 内置了形如v-bind、v-on等指令,如果需要对普通 DOM 元素进行底层操作还可以使用自定义指令。

一、自定义指令

  在 Vue2.0 中,可以通过自定义指令对普通 DOM 元素进行底层操作。一个指令定义对象可以提供如下几个钩子函数:

1、bind:指令第一次绑定到元素时调用,只调用一次。
2、inserted:被绑定元素插入父节点时调用。
3、update:所在组件的VNode更新时调用。
4、componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用。
5、unbind:指令与元素解绑时调用,只调用一次。

  通过如下示例来阐述源码中对自定义指令的处理过程:

<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>

1、全局注册与局部注册

  指令组件过滤器在Vue中称为资源。可以通过全局API进行全局注册,也可以通过具体选项进行局部注册。组件的全局注册和组件注册在《组件》一文中详细阐述过,指令的处理与之类似,这里简要说明。
  自定义指令全局注册的方法如下所示:

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 对象上。

2、模板编译

  带有自定义指令的标签在生成AST时,会调用 processElement 函数对自定义指令进行处理。

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"
          }
        ]
      }
    )
  ]
)

3、生成VNode

  调用 Vue.prototype._render 方法生成VNode,本质是通过调用渲染函数来完成的。渲染函数中的 _c() 是 createElement 的别称,在函数内部通过调用 _createElement 函数来生成VNode。

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

  根据示例的渲染函数生成的VNode如下所示:

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

4、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 钩子函数。

二、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>

三、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>

1、模板编译

  在模板编译的 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
)

2、生成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] }
        }
      }
      /* 省略其它属性 */  
    }
    /* 省略其它子节点 */
  ]
  /* 省略其它属性 */
}

3、原生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 上,因此处理方式与元素标签的情况一样。

4、自定义事件的处理

  在《选项合并》中,讲述实例初始化方法 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,其实现原理在下一篇文章详述。

四、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 指令可以处理的数据源类型有四种:数组、数字、可迭代对象与普通对象。

五、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 不同。

六、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 指令触发过渡效果的原理在《内置组件》一文中已经阐述过。

七、v-model

  v-model 指令用于在表单控件或者组件上创建双向绑定,所谓双向绑定是指除了数据驱动视图改变外,DOM视图的改变也会引起数据的改变。

1、用法

  v-model 指令可以在表单控件和组件上使用,表单控件包含有:<input>、<select>、<textarea>,可以在指令后添加修饰符:

.lazy:取代 input 监听 change 事件。
.number:输入字符串转为有效的数字。
.trim:输入首尾空格过滤。

  v-model 会忽略所有表单元素的 value、checked、selected 特性的初始值而总是将 Vue 实例的数据作为数据来源。v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:

1、text 和 textarea 元素使用 value 属性和 input 事件。
2、checkbox 和 radio 使用 checked 属性和 change 事件。
3、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。

2、表单元素

  这里借用官网的示例来阐述 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指令处理。

3、组件

  依旧借用官网实例来阐述 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 阶段转换的。

八、总结

  自定义指令用于对普通 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 阶段进行。

欢迎关注公众号:前端桃花源,互相交流学习!