首发于Think In Vue
开发一个简单指令理解 Vue 的 v-model 语法糖

开发一个简单指令理解 Vue 的 v-model 语法糖

题图来自网络,侵删

Vue 中有若干个“语法糖”:

  • v-model 语法糖
  • {{ }} 插值语法糖
  • 组件注册语法糖
  • arr.$set 语法糖



后三种语法糖即使不深入理解,也可以直接应用,然而如果没有充分理解第一种语法糖,那么就可能遇到一些奇怪的问题。


考虑如下需求:

编写一个自定义指令,使得在文本输入框中输入的敏感词(如:f..k)自动删除,并更新通过 v-model 指令所绑定的 Vue 实例数据。


自定义指令定义对象包含若干钩子函数,我们在 update 钩子函数里实现相关功能:

Vue.directive('exclude', {
  update: function (el, {value}) {
    try
      el.value = el.value.replace(new RegExp(value, 'gi'), '')
    } catch (e) {
    }
  }
})

其中 {value} 是 ES6 的解构语法,可以在函数体内通过 value 直接获取参数对象的 value 属性。为避免修改指令参数对应的 Vue 实例数据时导致 new RegExp 出错,这里的赋值语句使用 try ... catch 进行错误捕获。

对应模板如下:

<div class="app">
  To be excluded: <input v-model="excluded"><br>
  <textarea v-model="content" v-exclude="excluded"></textarea>
  <div>
    {{content}}
  </div>
</div>

其中的 excluded 和 content 均为 Vue 实例数据。

然而在实际使用时,却发现,textarea 中输入的 fork 虽然被删除了,但 Vue 实例数据中的数据仍然包含了对应的单词。

这是什么原因导致的呢?这涉及到 v-model 是一个语法糖这样一个事实,<input v-model="something"> 等价于 <input v-bind:value="something" v-on:input="something = $event.target.value">,因此在自定义指令中,通过 update 钩子函数在用户输入变化时修改 DOM 元素的 value 还不够,还需要触发 DOM 元素的 input 事件,使得通过 v-model 指令绑定的 Vue 实例数据得到更新,自定义指令修改如下:

Vue.directive('exclude', {
  update: function (el, {value}) {
    try
      el.value = el.value.replace(new RegExp(value, 'gi'), '')
      el.dispatchEvent(new Event('input'))
    } catch (e) {
    }
  }
})


此外,我们还需要考虑用户可能遗漏 v-model 的情况,以及 v-model 支持 lazy 修饰符的情况。

通过 bind hook 可以检查遗漏 v-model 的情况,并进行警告。

当 v-model 使用 lazy 修饰符时,v-model 的数据更新在 change 事件中触发,我们还需要能够针对不同的情况触发不同的事件。

修改后的完整代码如下:

Vue.directive('exclude', {
  bind (el, binding, vnode) {
    const modelDirective = vnode.data.directives.find((directive) => directive.name === 'model')
    if (!modelDirective) {
      console.warn('v-model required')
    }
  },
  update: function(el, {value}, vnode) {
    const modelDirective = vnode.data.directives.find((directive) => directive.name === 'model')
    
    try {
      el.value = el.value.replace(new RegExp(value, 'gi'), '')
      if (modelDirective) {
        modelDirective.modifiers.lazy) ? el.dispatchEvent(new Event('change')) : el.dispatchEvent(new Event('input'))
      }
    } catch (e) {
    }
  }
})

其中还要考虑 new Event 的兼容性问题,有兴趣的同学可以试着完善一下。

基于对 v-model 语法糖的理解,在创建如 date-picker 之类的自定义组件时,在组件中添加 value 的 prop 用于数据的传入,并在用户交互时通过 $emit 方法使用新的数据为参数触发一个 input 事件(若 v-model 使用 lazy 修饰符,则为 change 事件),就可以在标签中通过 v-model 双向绑定数据到子组件中了。

编辑于 2019-06-11 14:29