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

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

Open
lihongxun945 opened this issue Jul 15, 2018 · 0 comments
Open

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

lihongxun945 opened this issue Jul 15, 2018 · 0 comments

Comments

@lihongxun945
Copy link
Owner

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

<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 中完成的,代码很简单大家直接去源码中看吧。
赶紧绑定一个事件试试吧。

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