Skip to content

Vue源码解析(四):指令-directive #4

Open
@pf12345

Description

@pf12345

在上面主要谈了下vue整个compile编译过程,其实最主要任务就是提取节点属性、根据属性创建成对应的指令directive实例并保存到this.directives数组中,并在执行生成的linker的时候,将this.directives中新的指令进行初始化绑定_bind;那这儿主要谈下directive相关的知识;

在前面说过,自定义组件的渲染其实也是通过指令的方式完成的,那这儿就以组件渲染过程来进行说明,如下组件:

// hello.vue
<template>
  <div class="hello">
    <h1>hello, welcome here</h1>
  </div>
</template>

// app.vue
<hello @click.stop="hello" style="color: red" class="hello1" :class="{'selected': true}"></hello>

对于自定义组件的整个编译过程,在前面已经说过了,在这就不说了,主要说下如何通过指令将真正的html添加到对应的文档中;

首先,new directive其实主要是对指令进行初始化配置,就不多谈;

主要说下其中this._bind方法,它是指令初始化后绑定到对应元素的方法;

// remove attribute
  if (
    // 只要不是cloak指令那就从dom的attribute里移除
    // 是cloak指令但是已经编译和link完成了的话,那也还是可以移除的
    // 如移出":class"、":style"等
    (name !== 'cloak' || this.vm._isCompiled) &&
    this.el && this.el.removeAttribute
  ) {
    var attr = descriptor.attr || ('v-' + name)
    this.el.removeAttribute(attr)
  }

这儿主要移出元素上添加的自定义指令,如v-ifv-show等;所以当我们使用控制台去查看dom元素时,其实是看不到写在代码中的自定义指令属性;但是不包括v-cloak,因为这个在css中需要使用;

// html
<div v-cloak>
  {{ message }}
</div>

// css
[v-cloak] {
  display: none;
}
  // copy def properties
  // 不采用原型链继承,而是直接extend定义对象到this上,来扩展Directive实例
  // 将不同指令一些特殊的函数或熟悉合并到实例化的directive里
  var def = descriptor.def
  if (typeof def === 'function') {
    this.update = def
  } else {
    extend(this, def)
  }

这儿主要说下extend(this, def),descriptor主要是指令的一些描述信息:

指令描述对象,以v-bind:href.literal="mylink"为例:
      {
        arg:"href",
        attr:"v-bind:href.literal",
        def:Object,// v-bind指令的定义
        expression:"mylink", // 表达式,如果是插值的话,那主要用到的是下面的interp字段
        filters:undefined
        hasOneTime:undefined
        interp:undefined,// 存放插值token
        modifiers:Object, // literal修饰符的定义
        name:"bind" //指令类型
        raw:"mylink"  //未处理前的原始属性值
      }

而,def其实就是指令对应的配置信息;也就是我们在写指令时配置的数据,如下指令:

<template>
  <div class="hello">
    <h1 v-demo="demo">hello {{ msg }} welcome here</h1>
    <!-- <h3 v-if="show" >this is v-if</h3> -->
  </div>
</template>

<script>
export default {
  created() {
    setInterval(()=> {
      this.demo += 1;
    }, 1000)
  },
  data () {
    return {
      msg: 'Hello World!',
      show: false,
      demo: 1
    }
  },
  directives: {
    demo: {
      bind: function() {
        this.el.setAttribute('style', 'color: green');
      },
      update: function(value) {
        if(value % 2) {
          this.el.setAttribute('style', 'color: green');
        } else {
          this.el.setAttribute('style', 'color: red');
        }
      }
    }
  }
}
</script>

它对应的descriptor就是:

descriptor = {
    arg: undefined,
    attr: "v-demo",
    def: {
        bind: function() {}, // 上面定义的bind
        update: function() {} // 上面定义的update
    },
    expression:"demo",
    filters: undefined,
    modifiers: {},
    name: 'demo'
}

接着上面的,使用extend(this, def)就将def中定义的方法或属性就复制到实例化指令对象上面;好供后面使用;

// initial bind
  if (this.bind) {
    this.bind()
  }

这就是执行上面刚刚保存的bind方法;当执行此方法时,上面就会执行

this.el.setAttribute('style', 'color: green');

将字体颜色改为绿色;

  // 下面这些判断是因为许多指令比如slot component之类的并不是响应式的,
  // 他们只需要在bind里处理好dom的分发和编译/link即可然后他们的使命就结束了,生成watcher和收集依赖等步骤根本没有
  // 所以根本不用执行下面的处理
if (this.literal) {

} else if (
    (this.expression || this.modifiers) &&
    (this.update || this.twoWay) &&
    !this._checkStatement()
) {

var watcher = this._watcher = new Watcher(
      this.vm,
      this.expression,
      this._update, // callback
      {
        filters: this.filters,
        twoWay: this.twoWay,
        deep: this.deep,
        preProcess: preProcess,
        postProcess: postProcess,
        scope: this._scope
      }
    )
}

而这儿就是对需要添加双向绑定的指令添加watcher;对应watcher后面再进行详细说明; 可以从上看出,传入了this._update方法,其实也就是当数据变化时,就会执行this._update方法,而:

var dir = this
if (this.update) {
      // 处理一下原本的update函数,加入lock判断
      this._update = function (val, oldVal) {
        if (!dir._locked) {
          dir.update(val, oldVal)
        }
      }
} else {
      this._update = function() {}
}

其实也就是执行上面的descriptor.def.update方法,所以当值变化时,会触发我们自定义指令时定义的update方法,而发生颜色变化;

这是指令最主要的代码部分;其他的如下:

// 获取指令的参数, 对于一些指令, 指令的元素上可能存在其他的attr来作为指令运行的参数
  // 比如v-for指令,那么元素上的attr: track-by="..." 就是参数
  // 比如组件指令,那么元素上可能写了transition-mode="out-in", 诸如此类
this._setupParams();

// 当一个指令需要销毁时,对其进行销毁处理;此时,如果定义了unbind方法,也会在此刻调用
this._teardown();

而对于每个指令的处理原理,可以看其对应源码;如v-show源码:

// src/directives/public/show.js

import { getAttr, inDoc } from '../../util/index'
import { applyTransition } from '../../transition/index'

export default {

  bind () {
    // check else block
    var next = this.el.nextElementSibling
    if (next && getAttr(next, 'v-else') !== null) {
      this.elseEl = next
    }
  },

  update (value) {
    this.apply(this.el, value)
    if (this.elseEl) {
      this.apply(this.elseEl, !value)
    }
  },

  apply (el, value) {
    if (inDoc(el)) {
      applyTransition(el, value ? 1 : -1, toggle, this.vm)
    } else {
      toggle()
    }
    function toggle () {
      el.style.display = value ? '' : 'none'
    }
  }
}

可以从上面看出在初始化页面绑定时,主要获取后面兄弟元素是否使用v-else; 如果使用,将元素保存到this.elseEl中,而当值变化执行update时,主要执行了this.apply;而最终只是执行了下面代码:

el.style.display = value ? '' : 'none'

从而达到隐藏或者展示元素的效果;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions