Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue2.x源码解析系列十一:指令 #34

Open
lihongxun945 opened this issue Aug 2, 2018 · 0 comments
Open

Vue2.x源码解析系列十一:指令 #34

lihongxun945 opened this issue Aug 2, 2018 · 0 comments

Comments

@lihongxun945
Copy link
Owner

lihongxun945 commented Aug 2, 2018

在前面的章节中我们其实多次提到了 directives 指令相关内容,不过都是分散在各个生命周期中的。因此大家对指令的整个工作原理还没有很明白,这一篇文章,让我们以内置指令 v-model 为例,来看看一个指令的完整生命周期内的工作原理

初始化

在前面《组件的初始化过程》一章中我们其实讲到过,内置指令其实在Vue类的创建过程中就被创建出来,并存储在 Vue.options.directives 中,作为默认存在的指令。代码如下:

platforms/web/runtime/index.js

// 省略
// 注册指令和组件,这里的 directives 和 components 也是web平台上的,是内置的指令和组件,其实很少
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives) // 内置的directives只有两个,`v-show` 和 `v-model`

假设我们有如下组件:

    var app = new Vue({
      el: '#app',
      template:
      '<div class="hello">' +
        '<input v-model="message" />'+
        '<p>{{message}}</p>'+
      '</div>'
      ,
      data: {
        message: 'Hello Vue!'
      }
    })

当我们通过 new Vue(options) 创建实例的时候,会调用 _init 进行初始化,在 _init 函数内部,会把 optionsVue.options 进行合并,因此我们的组件$options 中就有了默认的 directives:

core/instance/init.js

 Vue.prototype._init = function (options?: Object) {
  const vm: Component = this

  // a flag to avoid this being observed
  vm._isVue = true
  // merge options
  if (options && options._isComponent) {
    // 省略
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
}

上面的代码 mergeOptions 会进行合并,最终 vm.$options 包含了我们创建实例是传入的自定义指令和系统自带的 v-model 指令。

而,我们生成的render 函数是这样的:

(function anonymous() {
    with (this) {
        return _c('div', {
            staticClass: "hello"
        }, [_c('input', {
            directives: [{
                name: "model",
                rawName: "v-model",
                value: (message),
                expression: "message"
            }],
            domProps: {
                "value": (message)
            },
            on: {
                "input": function($event) {
                    if ($event.target.composing)
                        return;
                    message = $event.target.value
                }
            }
        }), _c('p', [_v(_s(message))])])
    }
}
)

在生成 render 函数的时候,通过对模板的语法解析,已经知道我们用到了 v-model 这个指令,并解析出了他的参数。这样我们创建出来的 vnode 就存在一个 vnode.data.directives 保存了 v-model 指令。
大家可能注意到了,这里面还生成了一个 input 事件,在我们输入的时候会修改 message。那么我们的 v-model 其实在 render 函数中变成了两部分:

  • v-model 指令的配置
  • input 事件

这样我们执行 render之后,得到的 vnodedata 字段是这样的:

{
  data: {
    directives: [
      {
        expression:"message",
        name:"model",
        rawName:"v-model",
        value:"Hello Vue!"
    ],
    on: {
      input: function () {}
    }
  }
}

输入框如何更新 message

patch 阶段,在 createElm 函数中会通过 invokeCreateHooks 来绑定 input 事件。具体代码这里不贴出来,感兴趣的可以去 core/vdom/patch.js 中看 invokeCreateHooks 相关的代码。

patch 完成之后,最后一段代码是 invokeInsertedHook ,会调用 v-modelinserted 方法,我们看看这个方法:

platform/web/runtime/directives/model.js

const directive = {
  inserted (el, binding, vnode, oldVnode) {
    if (vnode.tag === 'select') {
      // #6903
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', () => {
          directive.componentUpdated(el, binding, vnode)
        })
      } else {
        setSelected(el, binding, vnode.context)
      }
      el._vOptions = [].map.call(el.options, getValue)
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers
      if (!binding.modifiers.lazy) {
        el.addEventListener('compositionstart', onCompositionStart)
        el.addEventListener('compositionend', onCompositionEnd)
        // Safari < 10.2 & UIWebView doesn't fire compositionend when
        // switching focus before confirming composition choice
        // this also fixes the issue where some browsers e.g. iOS Chrome
        // fires "change" instead of "input" on autocomplete.
        el.addEventListener('change', onCompositionEnd)
        /* istanbul ignore if */
        if (isIE9) {
          el.vmodel = true
        }
      }
    }
  },

  componentUpdated (el, binding, vnode) {
    // 省略
  }
}

可以发现 inserted 主要就是调用DOM的API添加了几个事件监听,主要是这三行代码:

el.addEventListener('compositionstart', onCompositionStart)
el.addEventListener('compositionend', onCompositionEnd)
el.addEventListener('change', onCompositionEnd)

如果你以为这几个回调函数比如 onCompositionEnd 里面会去修改 message,那你就错了。其实这几个回调完全不是为了做数据修改,而是为了为了生成一个 input 事件。没错,这几行代码只是为了处理兼容性,在各种浏览器环境中都能生成正确的 input 事件。

而当我们输入框的内容发生变化的时候,其实是由 input 事件触发的。input函数正是我们render函数生成的:

on: {
  "input": function($event) {
    if ($event.target.composing)
      return;
    message = $event.target.value
  }
}

如果 $event.target.composingtrue ,说明是在组合事件,因此不用管。等组合事件结束了,会触发 inpunt 事件,此时只要去更新一下值就行了。这样我们就理解了我们输入的时候,是如何更新 message 的值的。

我画了一个图来表示 input 是如何更新 message 值的,其实就是这么简单:

那么,当 message 被其他地方更新的时候,输入框的值是怎么更新的呢?

message 被更新时,输入框如何一起更新

可能有人第一反应是 v-model 会监听 this.message 的更新,然后通过 input.value 来设置输入框的值。实际上这种想法是完全错误的,因为 v-model 指令根本不用处理这种情况,这是交给 patch 来做的。当 message 被更新的时候, 会触发 vm._update 来更新组件,最终会把这个更新patch到真实的 DOM 上。所以,如果我们画一个流程图,其实是这样的:

记住这一点:在 v-model 插件中,如果我们要更新DOM,只需要修改vm的状态,它自己会进行patch来更新。关于 patch 的机制请参阅之前的文章。

v-show

那么可能大家会想,是不是在 vue2.x 中由于vdom的存在,我们的指令就不会操作真实DOM呢?其实并不是,这里简单看下内置指令 v-show 的代码:

platform/web/runtime/directives/show.js

var show = {
  bind: function bind (el, ref, vnode) {
    var value = ref.value;

    //省略动画相关
    {
      el.style.display = value ? originalDisplay : 'none';
    }
  },

  update: function update (el, ref, vnode) {
    var value = ref.value;
    var oldValue = ref.oldValue;

    // 省略动画相关
    {
      el.style.display = value ? el.__vOriginalDisplay : 'none';
    }
  }

主要的代码都是对动画的处理,这里我们不考虑动画部分,那么其实在 v-show 指令中,真的是对原生DOM进行操作的:

el.style.display = xxxx

我们写指令的时候,在生命周期的阶段都可以直接访问原生DOM,而且指令本来作用就是拓展 DOM 能里的,因此指令中操作原生DOM是很常见的操作。因此我们也可以知道,这些需要操作原生DOM的指令,和平台是相关的,他们的代码在 platforms 里而不是 core里面。

Vue 官方对指令生命周期的定义如下:

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

在生命周期的各个阶段,第一个参数都是 el.

@lihongxun945 lihongxun945 changed the title Vue2.x源码解析系列十一:插件系统 Vue2.x源码解析系列十一:指令 Aug 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant