Vue 1.0.28 源码剖析

团体概览

Vue源码终究是向外部抛出一个Vue的组织函数,见源码

function Vue (options) {
  this._init(options)
}

在源码最最先,经由历程installGlobalAPI要领(见源码)向Vue组织函数增加全局要领,如Vue.extend、Vue.nextTick、Vue.delete等,主要初始化Vue一些全局运用的要领、变量和设置;

export default function (Vue){
    Vue.options = {
          .....
    }
    Vue.extend = function (extendOptions){
           ......
    }
    Vue.use = function (plugin){
           ......
    }
    Vue.mixin = function (mixin){
           ......
    }
    Vue.extend = function (extendOptions){
           ......
    }
}

实例化Vue

当运用vue时,最基本运用体式格局以下:

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

此时,会挪用组织函数实例化一个vue对象,而在组织函数中只需这句代码this.init(options);而在init中(源码),主要举行一些变量的初始化、option重组、种种状况、事宜初始化;以下:

Vue.prototype._init = function (options) {
    options = options || {}
    this.$el = null
    this.$parent = options.parent
    this.$root = this.$parent
      ? this.$parent.$root
      : this
    this.$children = []
    this.$refs = {}       // child vm references
    this.$els = {}        // element references
    this._watchers = []   // all watchers as an array
    this._directives = [] // all directives

    ...... // 更多见源码

    options = this.$options = mergeOptions(
      this.constructor.options,
      options,
      this
    )

    // set ref
    this._updateRef()

    // initialize data as empty object.
    // it will be filled up in _initData().
    this._data = {}

    // call init hook
    this._callHook('init')

    // initialize data observation and scope inheritance.
    this._initState()

    // setup event system and option events.
    this._initEvents()

    // call created hook
    this._callHook('created')

    // if `el` option is passed, start compilation.
    if (options.el) {
      this.$mount(options.el)
    }
}

在个中经由历程mergeOptions要领,将全局this.constructor.options与传入的options及实例化的对象举行兼并;而this.constructor.options则是上面初始化vue时举行设置的,个中主要包含一些全局运用的指令、过滤器,如常常运用的”v-if”、”v-for”、”v-show”、”currency”:

this.constructor.options = {
        directives: {
          bind: {}, // v-bind
          cloak: {}, // v-cloak
          el: {}, // v-el
          for: {}, // v-for
          html: {}, // v-html
          if: {}, // v-if
          for: {}, // v-for
          text: {}, // v-text
          model: {}, // v-model
          on: {}, // v-on
          show: {} // v-show
        },
        elementDirectives: {
          partial: {}, // <partial></partial> api: https://v1.vuejs.org/api/#partial
          slot: {} // <slot></slot>
        },
        filters: {  // api: https://v1.vuejs.org/api/#Filters
          capitalize: function() {}, // {{ msg | capitalize }}  ‘abc’ => ‘Abc’
          currency: funnction() {},
          debounce: function() {},
          filterBy: function() {},
          json: function() {},
          limitBy: function() {},
          lowercase: function() {},
          orderBy: function() {},
          pluralize: function() {},
          uppercase: function() {}
        }
}

然后,会触发初始化一些状况、事宜、触发init、create钩子;然后随后,会触发this.$mount(options.el);举行实例挂载,将dom增加到页面;而this.$mount()要领则包含了绝大部份页面衬着的代码量,包含模板的嵌入、编译、link、指令和watcher的天生、批处置惩罚的实行等等,后续会细致举行申明;

_compile函数之transclude

在上面说了下,在Vue.prototype.$mount完成了大部份事情,而在$mount要领内里,最主要的事情量由this._compile(el)负担;其主要包含transclude(嵌入)、compileRoot(根节点编译)、compile(页面其他的编译);而在这儿主要申明transclude要领;

经由历程对transclude举行网络翻译结果是”嵌入”;其主要目标是将页面中自定义的节点转化为实在的html节点;如一个组件<hello></hello>其现实dom为<div><h1>hello {{message}}</h1></div>源码; 当我们运用时<div><hello></hello></div>; 会经由历程transclude将其转化为<div><div><h1>hello {{message}}</h1></div></div>,见源码解释;

那transclude详细干了什么呢,我们先看它的源码:

export function transclude (el, options) {
  // extract container attributes to pass them down
  // to compiler, because they need to be compiled in
  // parent scope. we are mutating the options object here
  // assuming the same object will be used for compile
  // right after this.
  if (options) {
    // 把el(假造节点,如<hello></hello>)元素上的统统attributes抽掏出来寄存在了选项对象的_containerAttrs属性上
    // 运用el.attributes 要领猎取el上面,并运用toArray要领,将类数组转换为实在数组
    options._containerAttrs = extractAttrs(el)
  }
  // for template tags, what we want is its content as
  // a documentFragment (for fragment instances)
  // 推断是不是为 template 标签
  if (isTemplate(el)) {
    // 获得一段寄存在documentFragment里的实在dom
    el = parseTemplate(el)
  }
  if (options) {
    if (options._asComponent && !options.template) {
      options.template = '<slot></slot>'
    }
    if (options.template) {
      // 将el的内容(子元素和文本节点)抽掏出来
      options._content = extractContent(el)
      // 运用options.template 将假造节点转化为实在html, <hello></hello> => <div><h1>hello {{ msg }}</h1></div>
      // 但不包含未绑定数据, 则上面转化为 => <div><h1>hello</h1></div>
      el = transcludeTemplate(el, options)
    }
  }
  // isFragment: node is a DocumentFragment
  // 运用nodeType 为 11 举行推断是非为文档片断
  if (isFragment(el)) {
    // anchors for fragment instance
    // passing in `persist: true` to avoid them being
    // discarded by IE during template cloning
    prepend(createAnchor('v-start', true), el)
    el.appendChild(createAnchor('v-end', true))
  }
  return el
}

起首先看以下代码:

if (options) {
    // 把el(假造节点,如<hello></hello>)元素上的统统attributes抽掏出来寄存在了选项对象的_containerAttrs属性上
    // 运用el.attributes 要领猎取el上面,并运用toArray要领,将类数组转换为实在数组
    options._containerAttrs = extractAttrs(el)
  }

而extractAttrs要领以下,其主要依据元素nodeType去推断是不是为元素节点,假如为元素节点,且元素有相干属性,则将属性值掏出以后,再转为属性数组;末了将属性数组放到options._containerAttrs中,为何要这么做呢?由于如今的el能够不是实在的元素,而是诸如<hello class="test"></hello>,在背面编译历程,须要将其替代为实在的html节点,所以,它上面的属性值都邑先掏出来预存起来,背面兼并到实在html根节点的属性上面;

function extractAttrs (el) {
  // 只查找元素节点及有属性
  if (el.nodeType === 1 && el.hasAttributes()) {
    // attributes 属性返回指定节点的属性鸠合,即 NamedNodeMap, 类数组
    return toArray(el.attributes)
  }
}

下一句,依据元素nodeName是不是为“template”去推断是不是为<template></template>元素;假如是,则走parseTemplate(el)要领,并掩盖当前el对象

if (isTemplate(el)) {
    // 获得一段寄存在documentFragment里的实在dom
    el = parseTemplate(el)
  }

function isTemplate (el) {
  return el.tagName &&
    el.tagName.toLowerCase() === 'template'
}

parseTemplate则重如果将传入内容天生一段寄存在documentFragment里的实在dom;进入函数,起首推断传入是不是已是一个文档片断,假如已是,则直接返回;不然,推断传入是不是为字符串,假如为字符串, 先推断是不是是”#test”这类选择器范例,假如是,经由历程document.getElementById要领掏出元素,假如文档中有此元素,将经由历程nodeToFragment体式格局,将其放入一个新的节点片断中并赋给frag,末了返回到表面;假如不是选择器范例字符串,则运用stringToFragment将其天生一个新的节点片断,并返回;假如传入非字符串而是节点(不论是什么节点,可所以元素节点、文本节点、以至Comment节点等);则直接经由历程nodeToFragment天生节点片断并返回;

export function parseTemplate (template, shouldClone, raw) {
  var node, frag

  // if the template is already a document fragment,
  // do nothing
  // 是不是为文档片断, nodetype是不是为11
  // https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment
 // 推断传入是不是已是一个文档片断,假如已是,则直接返回
  if (isFragment(template)) {
    trimNode(template)
    return shouldClone
      ? cloneNode(template)
      : template
  }
  // 推断传入是不是为字符串
  if (typeof template === 'string') {
    // id selector
    if (!raw && template.charAt(0) === '#') {
      // id selector can be cached too
      frag = idSelectorCache.get(template)
      if (!frag) {
        node = document.getElementById(template.slice(1))
        if (node) {
          frag = nodeToFragment(node)
          // save selector to cache
          idSelectorCache.put(template, frag)
        }
      }
    } else {
      // normal string template
      frag = stringToFragment(template, raw)
    }
  } else if (template.nodeType) {
    // a direct node
    frag = nodeToFragment(template)
  }

  return frag && shouldClone
    ? cloneNode(frag)
    : frag
}

从上面可见,在parseTemplate内里最主要的是nodeToFragment和stringToFragment;那末,它们又是怎样将传入内容转化为新的文档片断呢?起首看nodeToFragment:

function nodeToFragment (node) {
  // if its a template tag and the browser supports it,
  // its content is already a document fragment. However, iOS Safari has
  // bug when using directly cloned template content with touch
  // events and can cause crashes when the nodes are removed from DOM, so we
  // have to treat template elements as string templates. (#2805)
  /* istanbul ignore if */
  // 是template元素或许documentFragment,运用stringToFragment转化并保留节点内容
  if (isRealTemplate(node)) {
    return stringToFragment(node.innerHTML)
  }
  // script template
  if (node.tagName === 'SCRIPT') {
    return stringToFragment(node.textContent)
  }
  // normal node, clone it to avoid mutating the original
  var clonedNode = cloneNode(node)
  var frag = document.createDocumentFragment()
  var child
  /* eslint-disable no-cond-assign */
  while (child = clonedNode.firstChild) {
  /* eslint-enable no-cond-assign */
    frag.appendChild(child)
  }
  trimNode(frag)
  return frag
}

实在看源码,很轻易明白,起首推断传入内容是不是为template元素或许documentFragment或许script标签,假如是,都直接走stringToFragment;背面就是先运用document.createDocumentFragment竖立一个文档片断,然后将节点举行轮回appendChild到竖立的文档片断中,并返回新的片断;
那末,stringToFragment呢?这个就相对庞杂一点了,以下:

function stringToFragment (templateString, raw) {
  // try a cache hit first
  var cacheKey = raw
    ? templateString
    : templateString.trim() //trim() 要领会从一个字符串的两头删除空缺字符
  var hit = templateCache.get(cacheKey)
  if (hit) {
    return hit
  }
  // 竖立一个文档片断
  var frag = document.createDocumentFragment()
  // tagRE: /<([\w:-]+)/
  // 婚配标签
  // '<test v-if="ok"></test>'.match(/<([\w:-]+)/) => ["<test", "test", index: 0, input: "<test v-if="ok"></test>"]
  var tagMatch = templateString.match(tagRE)
  // entityRE: /&#?\w+?;/
  var entityMatch = entityRE.test(templateString)
  // commentRE: /<!--/ 
  // 婚配解释
  var commentMatch = commentRE.test(templateString) 

  if (!tagMatch && !entityMatch && !commentMatch) {
    // text only, return a single text node.
    // 假如都没婚配到,竖立一个文本节点增加到文档片断
    frag.appendChild(
      document.createTextNode(templateString)
    )
  } else {
    var tag = tagMatch && tagMatch[1]
    // map, 对标签举行修正;如是td标签,则返回"<table><tbody><tr>" + templateString +  "</tr></tbody></table>";
    // map['td'] = [3, "<table><tbody><tr>", "</tr></tbody></table>"]
    var wrap = map[tag] || map.efault
    var depth = wrap[0]
    var prefix = wrap[1]
    var suffix = wrap[2]
    var node = document.createElement('div')

    node.innerHTML = prefix + templateString + suffix

    while (depth--) {
      node = node.lastChild
    }

    var child
    document.body.appendChild(node);
    /* eslint-disable no-cond-assign */
    while (child = node.firstChild) {
    /* eslint-enable no-cond-assign */
      frag.appendChild(child)
    }
  }
  if (!raw) {
    // 移除文档中空文本节点及解释节点
    trimNode(frag)
  }
  templateCache.put(cacheKey, frag)
  return frag
}

起首去缓存检察是不是已有,假如有,则直接取缓存数据,削减顺序运转;然后,经由历程正则推断是不是为元素文本,假如不是,则申明为一般的笔墨文本,直接竖立文本节点,并放入新建的DocumentFragment中再放入缓存中,并返回终究天生的DocumentFragment;假如是节点文本,则起首对文本举行修正;比方假如传入的是<td></td>则须要在其外层增加tr、tbody、table后才直接运用appendChild将节点增加到文档碎片中,而没法直接增加td元素到div元素中;在末了返回一个DocumentFragment;

以上就是parseTemplate及其内里nodeToFragment、stringToFragment的详细完成;然后我们继续回到transclude;

在transclude后续中,主要就是transcludeTemplate要领,其主要就是经由历程此函数,依据option.template将自定义标签转化为实在内容的元素节点;如<hello></hello>这个自定义标签,会依据此标签内里实在元素而转化为实在的dom构造;

// app.vue
<hello></hello>

// template: 
<div class="hello" _v-0480c730="">
  <h1 _v-0480c730="">hello {{ msg }} welcome here</h1>
  <h3 v-if="show" _v-0480c730="">this is v-if</h3>
</div>

函数起首会经由历程上述parseTemplate要领将模版数据转化为一个暂时的DocumentFragment,然后依据是不是将根元素举行替代,即option.replace是不是为true举行对应处置惩罚,而假如须要替代,主要举即将替代元素上的属性值和模版根元素属性值举行兼并,也就是将替代元素上面的属性兼并并增加到根节点上面,假如两个上面都有此属性,则举行兼并后的作为终究此属性值,假如模板根元素上没有此属性而自定义元素上有,则将其设置到根元素上,即:

options._replacerAttrs = extractAttrs(replacer)
        mergeAttrs(el, replacer)

所以,综上,在compile中,el = transclude(el, options)重如果对元素举行处置惩罚,将一个简朴的自定义标签依据它对应的template模板数据和option的一些设置,举行整合处置惩罚,末了返回整顿后的元素数据;

_compile函数之compileRoot 与 compile

前面,说了下vue在_compile函数中,起首对el元素举行了处置惩罚,重如果处置惩罚了自定义标签元素;将自定义标签转化为实在html元素,并对元素属性和实在html根节点属性举行兼并;

在这,主要说下对元素根节点的的编译历程,即var rootLinker = compileRoot(el, options, contextOptions),compileRoot会天生一个终究的linker函数;而末了经由历程实行天生的linker函数,完成统统编译历程;

而在源码,能够看到另有compile这个要领,也是对元素举行编译,并天生一个终究的linker函数,那这两个有什么区别呢?为何要离开处置惩罚呢?

依据我的明白,compileRoot主要对根节点举行编译,在这儿的根节点不仅包含模板中的根节点,也包含自定义的标签;以下组件<hello></hello>:

// hello.vue

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

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

经由历程compileRoot主要处置惩罚<hello>节点和<div class=”hello”></div>节点;而compile主要处置惩罚全部元素及元素下面的子节点;也包含已经由历程compileRoot处置惩罚过的节点,只是根节点假如已处置惩罚,在compile中就不会再举行处置惩罚;

那为何会离开举行处置惩罚呢,由于我们在前面说过,关于根节点,它也包含了自定义的标签节点,即上面的<hello></hello>,统统就离开举行了处置惩罚;

而在详细申明compileRoot怎样处置惩罚之前,我们先要晓得一点,在vue中,基本上统统的dom操纵都是经由历程指令(directive)的体式格局处置惩罚的;如dom属性的操纵(修正class、style)、事宜的增加、数据的增加、节点的天生等;而基本大部份的指令都是经由历程写在元素属性上面(如v-bind、v-if、v-show、v-for)等;所以在编译历程当中,重如果对元素的属性举行提取、依据差别的属性然后天生对应的Derective的实例;而在实行终究编译天生的linker函数时,也就是对统统天生的指令实例实行bind;并对其增加相应式处置惩罚,也就是watcher;

下面,我们主要说下详细compileRoot内里的代码剖析:

//  el(假造元素,如<hello></hello>)元素上的统统attributes
//  <hello @click.stop="hello" style="color: red" class="hello" :class="{'selected': true}"></hello>
//  ['@click.stop', 'style', 'class', ':class']
var containerAttrs = options._containerAttrs 

// 假造元素对应实在html根节点统统attributes
// <div class="hello"> ... </div>
// ['class', '_v-b9ed5d18']
var replacerAttrs = options._replacerAttrs 
 

这两个主要保留着根元素的属性列表;包含自定义元素和其对应的模板根元素的属性;而它们在哪儿去提取的呢?就是我们前面说的transclude要领内里,假如忘记了能够回到对应函数内里去检察;

// 2. container attributes
if (containerAttrs && contextOptions) {
    contextLinkFn = compileDirectives(containerAttrs, contextOptions)
}
// 3. replacer attributes
if (replacerAttrs) {
    replacerLinkFn = compileDirectives(replacerAttrs, options)
}

compileDirectives主要对传入的attrs和options,经由历程正则,对一些属性指令初始化基本信息,并天生对应的处置惩罚函数并返回到表面,而终究处置惩罚的是

this._directives.push(
    new Directive(descriptor, this, node, host, scope, frag)
)

也就是上面说的天生对应的指令实例化对象,并保留在this._directives中;

详细compileDirectives内里的细致代码,就不细说,这里掏出一部份举行说下:

// event handlers
// onRE: /^v-on:|^@/ 是不是为事宜相干属性,如“v-on:click”、"@click"
if (onRE.test(name)) {
    arg = name.replace(onRE, '')
    pushDir('on', publicDirectives.on)
} 

这个是主要婚配属性名是不是是v-on:范例的,也就是事宜相干的,假如是,则掏出对应的事宜名,然后将其举行指令参数初始化,天生一个指令形貌对象:

/**
    指令形貌对象,以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"  //未处置惩罚前的原始属性值
      }

    **/
    dirs.push({
      name: dirName,
      attr: rawName,
      raw: rawValue,
      def: def,
      arg: arg,
      modifiers: modifiers,
      // conversion from interpolation strings with one-time token
      // to expression is differed until directive bind time so that we
      // have access to the actual vm context for one-time bindings.
      expression: parsed && parsed.expression,
      filters: parsed && parsed.filters,
      interp: interpTokens,
      hasOneTime: hasOneTimeToken
    })

天生形貌对象数组以后,经由历程下面函数去初始化指令实例化对象:

function makeNodeLinkFn (directives) {
  return function nodeLinkFn (vm, el, host, scope, frag) {
    // reverse apply because it's sorted low to high
    var i = directives.length
    while (i--) {
      vm._bindDir(directives[i], el, host, scope, frag)
    }
  }
}

Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {

    this._directives.push(
      new Directive(descriptor, this, node, host, scope, frag)
    )
    // console.log(new Directive(descriptor, this, node, host, scope, frag))
  }

那末,在天生指令数组以后,在哪举行指令的绑定呢?就是下面这儿,在compileRoot返回的终究函数中:

 export function compileRoot (el, options, contextOptions) {
 
    // 指令的天生历程
    ......
 
    return function rootLinkFn (vm, el, scope) {
        // link context scope dirs
        var context = vm._context
        var contextDirs
        if (context && contextLinkFn) {
          contextDirs = linkAndCapture(function () {
            contextLinkFn(context, el, null, scope)
          }, context)
        }
    
        // link self
        var selfDirs = linkAndCapture(function () {
          if (replacerLinkFn) replacerLinkFn(vm, el)
        }, vm)
    
    
        // return the unlink function that tearsdown context
        // container directives.
        return makeUnlinkFn(vm, selfDirs, context, contextDirs)
      }
}


// link函数的实行历程会天生新的Directive实例,push到_directives数组中
// 而这些_directives并没有竖立对应的watcher,watcher也没有网络依靠,
// 统统都还处于初始阶段,因而capture阶段须要找到这些新增加的directive,
// 顺次实行_bind,在_bind里会举行watcher天生,实行指令的bind和update,完成相应式构建
 function linkAndCapture (linker, vm) {
  /* istanbul ignore if */
  if (process.env.NODE_ENV === 'production') {
    // reset directives before every capture in production
    // mode, so that when unlinking we don't need to splice
    // them out (which turns out to be a perf hit).
    // they are kept in development mode because they are
    // useful for Vue's own tests.
    vm._directives = []
  }
  // 先记录下数组里本来有若干元素,他们都是已实行过_bind的,我们只_bind新增加的directive
  var originalDirCount = vm._directives.length
  // 在天生的linker中,会对元素的属性举行指令化处置惩罚,并保留到_directives中
  linker()
  // slice出新增加的指令们
  var dirs = vm._directives.slice(originalDirCount)
  // 依据 priority 举行排序
  // 对指令举行优先级排序,使得背面指令的bind历程是按优先级从高到低举行的
  sortDirectives(dirs)
  for (var i = 0, l = dirs.length; i < l; i++) {
    dirs[i]._bind()
  }
  return dirs
}

也就是经由历程这儿dirs[i]._bind()举行绑定;也就是终究compileRoot天生的终究函数中,当实行此函数,起首会实行linkAndCapture, 而这儿会先去实行传入的函数,也就是contextLinkFn和replacerLinkFn,经由历程上面两个要领,天生指令数组后,再实行轮回,并举行_bind()处置惩罚;

而关于_bind()详细干了什么,会在背面细致举行申明;实在主要经由历程指令对元素举行初始化处置惩罚和对须要双向绑定的举行绑定处置惩罚;

指令-directive

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

从而到达隐蔽或许展现元素的结果;

未完待续,后续会延续完美……

    原文作者:彭锋
    原文地址: https://segmentfault.com/a/1190000013569879
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞