lihongxun945/myblog

vue1.0源码解析四:实现Compile和Directive

lihongxun945 opened this issue · 0 comments

这一篇,我们要实现一个事件绑定的功能:

<div @click=“sayHello”></div>

那么为了实现这个功能,我们需要三步:

  1. 实现 compileDirectives 方法, 可以从attrs中读取directive的配置,这里称之为 descriptor
  2. 实现Directive类
  3. 实现一个自定指令: v-on

v-on 为例,如果碰到这样一个属性 v-on:click=“hello” 指令的初始化流程如下?

  1. 遍历DOM
  2. 对其中每一个Element,获取所有的attributes
  3. 遍历 attributes,如果name以 v-on 开头,那么就是一个绑定事件的指令,我们把相关的配置都存在 descriptor 中
  4. 遍历DOM结束,得到一个 descriptors 列表,其中存放了我们创建directive时需要的所有参数,比如name, value, el 等
  5. 在 bind 阶段,遍历 descriptors
  6. 对每一个descriptor,创建一个Directive
  7. bind 结束,directive 初始化完成。

关于watcher的实现以及如何在directive中监听数据的变动,我们放到下一章来讲。

compileDirectives

在vue v1.0 版本及以前,是通过DOM API直接获取attributes的,并通过对name的匹配来判断是哪一类directive。在vue2中显然是通过virtual DOM的API来做的。

那么我们按照上面的步骤来说,compileDirectives 负责其中 1-4 步,即从attributes中生成一个 descriptors 列表:

const onRE = /^v-on:|^@/
const modelRE = /^v-model/
const textRE = /^v-text/
const dirAttrRE = /^v-([^:]+)(?:$|:(.*)$)/

export const compileDirectives = function (el, attrs) {
  if (!attrs) return undefined
  const dirs = []

  let i = attrs.length

  while (i--) {
    const attr = attrs[i]
    const name = attr.name
    const value = attr.value
    let arg = name
    if (name.match(dirAttrRE)) {
               if (onRE.test(name)) {
        arg = name.replace(onRE, '')
                    pushDir('on', dirOn)
               } else if (modelRE.test(name)) {
        arg = name.replace(modelRE, '')
                    pushDir('model', dirModel)
               } else if (textRE.test(name)) {
        arg = name.replace(textRE, '')
                    pushDir('text', dirText)
               }
    }

    function pushDir(dirName, def) {
      dirs.push({
        el: el,
        name: dirName,
        rawName: name,
        def: def,
        arg: arg,
        value: value,
        rawValue: value,
        expression: value
      })
    }
  }
  if (dirs.length) return makeNodeLinkFn(dirs)
}

上面这些代码都在 compile.js

获取了 descriptors 之后,我们就可以逐个创建directive了,那么显然,我们需要实现一个 Directive类,他会接收这些 descriptor 并生成一个 directive 实例。

这里需要重点说明一下,vuejs对每一个指令都会生成一个 directive 的实例,那么既然有 Directive 类了,那么 directives 下面的那么多指令是怎么回事呢?

我们可以把 Directive 类当做一个父类,他定义了所有指令通用的方法:_bind, _update 以及生成 watcher。我们自己定义的比如 v-on 指令的实现,其实可以看做是他的一个子类,其中的 bind 和 update 分别会被 _bind_update 调用。当然,在语法上其实并不是父类和子类的关系,语法上来说,Directive会调用我们自定义的回调函数(update/bind) 仅此而已。

Directive 类需要实现这几个功能:

  • constructor 从 descriptor 中抽取所需的参数,比如 el, value, expression 等
  • _bind 阶段调用 bind
    _ 生成一个 _update 函数,负责调用 update
  • 创建 this._watcher

我们直接贴上相关代码:

export default function Directive (descriptor, vm, el) {
  this.descriptor = descriptor
  this.vm = vm
  this.el = el
  this.expression = descriptor.expression
}

Directive.prototype._bind = function () {

  var def = this.descriptor.def
  if (typeof def === 'function') {
    this.update = def
  } else {
    extend(this, def)
  }

  if (this.bind) this.bind()
  if (this.update) this.update()

  if (this.update) {
    const dir = this
    this._update = function (val, oldVal) {
      dir.update(val, oldVal)
    }
  } else {
    this._update = function () {}
  }

  var watcher = this._watcher = new Watcher(
    this.vm,
    this.expression,
    this._update
  )
  // v-model with inital inline value need to sync back to
  // model instead of update to DOM on init. They would
  // set the afterBind hook to indicate that.
  if (this.update) {
    this.update(watcher.value)
  }   
}

这里注意创建watcher的代码,其中第三个参数 this._update 是watcher发现所观察的对象有更新的时候会触发的回调。

因为我们还未实现 Watcher 类,所以这里可以先把 Watcher 相关的代码注释掉。让我们实现一个自己的指令 v-on 吧。

这个指令最精简的实现非常简单:在 bind 的时候调用 addEventListener 绑定一个回调即可。

export default {
  bind () {
    const el = this.descriptor.el
    if (this.descriptor.arg === 'click') {
      el.addEventListener('click', this.vm[this.descriptor.value].bind(this.vm))
    }
  }
}

显然这么简单的处理是很不妥的,比如addEventListener不支持怎么办?如果用户更新了回调函数怎么办? 什么时候应该注销?,没关系,我们这里只是最精简版,暂时不用考虑这么全面。

调用 compile 以及new Directive 其实都是在 lifecycle 中完成的,代码很简单大家直接去源码中看吧。
赶紧绑定一个事件试试吧。