Skip to content

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

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

Open
pf12345 opened this issue Mar 6, 2018 · 0 comments
Open

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

pf12345 opened this issue Mar 6, 2018 · 0 comments

Comments

@pf12345
Copy link
Owner

pf12345 commented Mar 6, 2018

在上面主要谈了下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'

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant