因为vue指令很多,性能也很多,所以会有很多针对一些状况的非凡解决,这些逻辑如果不是对vue很相熟的话一时间是看不懂的,所以咱们只看一些根本逻辑。

compile

创立vue实例时当传递了参数el或者手动调用$mount办法即可启动模板编译过程,$mount办法里调用了_compile办法,简化之后其实调用的是compile(el, options)(this, el)compile也简化后代码如下:

function compile (el, options, partial, transcluded) {  var nodeLinkFn = compileNode(el, options)  var childLinkFn = el.hasChildNodes()      ? compileNodeList(el.childNodes, options)      : null    function compositeLinkFn (vm, el)     var childNodes = _.toArray(el.childNodes)    if (nodeLinkFn) nodeLinkFn(vm.$parent, el)    if (childLinkFn) childLinkFn(vm.$parent, childNodes)  }  return compositeLinkFn}

该办法会依据实例的一些状态来判断解决某个局部应用哪个办法,因为代码极大的简化了所以不是很显著。

先来看compileNode办法,这个办法会会对一般节点和文本节点调用不同的办法,只看一般节点:

function compileElement (el, options) {  var linkFn, tag, component  // 查看是否是自定义元素,也就是子组件  if (!el.__vue__) {    tag = el.tagName.toLowerCase()    component =      tag.indexOf('-') > 0 &&      options.components[tag]    // 是自定义组件则给元素设置一个属性标记    if (component) {      el.setAttribute(config.prefix + 'component', tag)    }  }   // 如果是自定义组件或者元素有属性的话  if (component || el.hasAttributes()) {    // 查看 terminal 指令    linkFn = checkTerminalDirectives(el, options)    // 如果不是terminal,建设失常的链接性能    if (!linkFn) {      var dirs = collectDirectives(el, options)      linkFn = dirs.length        ? makeNodeLinkFn(dirs)        : null    }  }  return linkFn}

terminal 指令有三种:repeatif'component

var terminalDirectives = [  'repeat',  'if',  'component']function skip () {}skip.terminal = truefunction checkTerminalDirectives (el, options) {  // v-pre指令是用来通知vue跳过编译该元素及其所有子元素  if (_.attr(el, 'pre') !== null) {    return skip  }  var value, dirName  for (var i = 0; i < 3; i++) {    dirName = terminalDirectives[i]    if (value = _.attr(el, dirName)) {      return makeTerminalNodeLinkFn(el, dirName, value, options)    }  }}

顺便一提的是attr办法,这个办法其实是专门用来获取vue的自定义属性的,也就是v-结尾的属性,为什么咱们在模板里写的带v-前缀的属性在最终渲染的元素上没有呢,就是因为在这个办法里把它给移除了:

exports.attr = function (node, attr) {  attr = config.prefix + attr  var val = node.getAttribute(attr)  // 如果该自定义指令存在,则把它从元素上删除  if (val !== null) {    node.removeAttribute(attr)  }  return val}

makeTerminalNodeLinkFn办法:

function makeTerminalNodeLinkFn (el, dirName, value, options) {  // 解析指令值  var descriptor = dirParser.parse(value)[0]  // 获取该指令的指令办法,vue内置了很多指令解决办法,都在/src/directives/文件夹下  var def = options.directives[dirName]  var fn = function terminalNodeLinkFn (vm, el, host) {    // 创立并把指令绑定到元素    vm._bindDir(dirName, el, descriptor, def, host)  }  fn.terminal = true  return fn}

parse办法用来解析指令的值,请移步文章:vue0.11版本源码浏览系列四:详解指令值解析函数,比方指令值为click: a = a + 1 | uppercase,解决完最初会返回这样的信息:

{    arg: 'click',    expression: 'a = a + 1',    filters: [        { name: 'uppercase', args: null }    ]}

_bindDir办法会创立一个指令实例:

exports._bindDir = function (name, node, desc, def, host) {  this._directives.push(    new Directive(name, node, this, desc, def, host)  )}

所以linkFn以及nodeLinkFn就是这个_bindDir的包装函数。

对于非terminal指令,调用的是collectDirectives办法,这个办法会遍历元素的所有属性attributes,如果是v-前缀的vue指令会被定义为下列格局的对象:

{    name: dirName,// 去除了v-前缀的指令名    descriptors: dirParser.parse(attr.value),// 指令值解析后的数据    def: options.directives[dirName],// 该指令对应的解决办法    transcluded: transcluded}

vue指令的属性如果存在动静绑定,也会进行解决,在该版本vue里的动静绑定是应用双大括号插值的,和2.x的应用v-bind不一样。

如:<div class="{{error}}"></div>,所以会通过正则来匹配判断是否存在动静绑定,最终返回下列格局的数据:

{    def: options.directives.attr,    _link: allOneTime// 是否所有属性都是一次性差值    ? function (vm, el) {// 一次性的话后续不须要更新        el.setAttribute(name, vm.$interpolate(value))    }    : function (vm, el) {// 非一次性的话如果依赖的响应数据变动了也须要扭转        var value = textParser.tokensToExp(tokens, vm)        var desc = dirParser.parse(name + ':' + value)[0]        vm._bindDir('attr', el, desc, def)    }}

collectDirectives办法最终会返回一个下面对象组成的数组,而后调用makeNodeLinkFn为每个指令创立一个绑定函数:

function makeNodeLinkFn (directives) {  return function nodeLinkFn (vm, el, host) {    var i = directives.length    var dir, j, k, target    while (i--) {      dir = directives[i]      if (dir._link) {        dir._link(vm, el)      } else {// v-前缀的指令        k = dir.descriptors.length        for (j = 0; j < k; j++) {          vm._bindDir(dir.name, el,            dir.descriptors[j], dir.def, host)        }      }    }  }}

总结一下compileNode的作用就是遍历元素上的属性,别离给其创立一个指令绑定函数,这个指令函数后续调用时会创立一个Directive实例,这个类后续再看。

如果该元素存在子元素的话会调用compileNodeList办法,子元素又有子元素的话又会持续调用,其实就是递归所有子元素调用compileNode办法。

compile办法最初返回了compositeLinkFn办法,这个办法被立刻执行了,这个办法里调用了方才生成的nodeLinkFnchildLinkFn办法,执行后果就是会把所有的元素及子元素的指令进行绑定,也就是给元素上的某个属性或者说指令都创立了一个Directive实例。

Directive

指令这个类次要做的事是把DOM和数据绑定起来,实例化的时候会调用指令的bind办法,同时会实例化一个Watcher实例,后续数据更新的时候会调用指令的update办法。

function Directive (name, el, vm, descriptor, def, host) {  this.name = name  this.el = el  this.vm = vm  this.raw = descriptor.raw  this.expression = descriptor.expression  this.arg = descriptor.arg  this.filters = _.resolveFilters(vm, descriptor.filters)  this._host = host  this._locked = false  this._bound = false  this._bind(def)}

构造函数定义一些属性以及调用了_bind办法,resolveFilters办法会把过滤器以gettersetter别离收集到一个数组里,便于后续循环调用:

exports.resolveFilters = function (vm, filters, target) {  var res = target || {}  filters.forEach(function (f) {    var def = vm.$options.filters[f.name]    if (!def) return    var args = f.args    var reader, writer    if (typeof def === 'function') {      reader = def    } else {      reader = def.read      writer = def.write    }    if (reader) {      if (!res.read) res.read = []      res.read.push(function (value) {        return args          ? reader.apply(vm, [value].concat(args))          : reader.call(vm, value)      })    }    if (writer) {      if (!res.write) res.write = []      res.write.push(function (value, oldVal) {        return args          ? writer.apply(vm, [value, oldVal].concat(args))          : writer.call(vm, value, oldVal)      })    }  })  return res}

_bind办法:

p._bind = function (def) {  if (typeof def === 'function') {    this.update = def  } else {// 这个版本的vue指令有这几个钩子办法:bind、update、unbind    _.extend(this, def)  }  this._watcherExp = this.expression  // 如果该指令存在bind办法,此时进行调用  if (this.bind) {    this.bind()  }  if (this._watcherExp && this.update){    var dir = this    var update = this._update = function (val, oldVal) {        dir.update(val, oldVal)    }    // 应用原始表达式作为标识符,因为过滤器会让同一个arg变成不同的观察者    var watcher = this.vm._watchers[this.raw]    if (!watcher) {      // 该表达式未创立过watcher,则实例化一个      watcher = this.vm._watchers[this.raw] = new Watcher(        this.vm,        this._watcherExp,        update,        {          filters: this.filters        }      )    } else {// 存在则把更新函数增加进入      watcher.addCb(update)    }    this._watcher = watcher    if (this._initValue != null) {// 带初始值的状况,见于v-model的状况      watcher.set(this._initValue)    } else if (this.update) {// 其余的会调用update办法,所以bind办法调用后紧接着会调用update办法      this.update(watcher.value)    }  }  this._bound = true}

到这里能够晓得实例化Directive的时候会调用指令的bind钩子函数,个别是做一些初始化工作,而后会对该指令初始化一个Watcher实例,这个实例会用来做依赖收集,最初非v-model的状况会立刻调用指令的update办法,watcher实例化的时候会计算表达式的值,所以此时失去的value就是最新的。

Watcher

Watcher实例用来解析表达式和收集依赖项,并在表达式的值变动时触发回调更新。第一篇里提到的$watch办法也是应用该类实现的。

function Watcher (vm, expression, cb, options) {  this.vm = vm  this.expression = expression  this.cbs = [cb]  this.id = ++uid  this.active = true  options = options || {}  this.deep = !!options.deep  this.user = !!options.user  this.deps = Object.create(null)  if (options.filters) {    this.readFilters = options.filters.read    this.writeFilters = options.filters.write  }  // 将表达式解析为getter/setter  var res = expParser.parse(expression, options.twoWay)  this.getter = res.get  this.setter = res.set  this.value = this.get()}

构造函数的逻辑很简略,申明一些变量、将表达式解析为gettersetter的类型,比方:a.b解析后的get为:

function anonymous(o){    return o.a.b}

set为:

function set(obj, val){    Path.se(obj, path, val)}

简略的说就是生成两个函数,一个用来给实例this设置值,一个用来获取实例this上的值,具体的解析逻辑比较复杂,有机会再详细分析或者可自行浏览源码:/src/parsers/path.js

最初调用了get办法:

p.get = function () {  this.beforeGet()  var vm = this.vm  var value  // 调用取值办法  value = this.getter.call(vm, vm)  // “触摸”每个属性,以便它们都作为依赖项进行跟踪,以便进行深刻察看  if (this.deep) {    traverse(value)  }  // 利用过滤器函数  value = _.applyFilters(value, this.readFilters, vm)  this.afterGet()  return value}

在调用取值函数前调用了beforeGet办法:

p.beforeGet = function () {  Observer.target = this  this.newDeps = {}}

到这里咱们晓得了第二篇vue0.11版本源码浏览系列二:数据察看里提到的Observer.target是什么了,逻辑也能够串起来,vue在数据察看时对每个属性进行了拦挡,在getter里会判断Observer.target是否存在,存在的话会把Observer.target对应的watcher实例收集到该属性的依赖对象实例dep里:

if (Observer.target) {    Observer.target.addDep(dep)}

beforeGet后紧接着就调用了该表达式的取值函数,会触发对应属性的getter

addDep办法:

p.addDep = function (dep) {  var id = dep.id  if (!this.newDeps[id]) {    this.newDeps[id] = dep    if (!this.deps[id]) {      this.deps[id] = dep      // 收集该watcher实例到该属性的依赖对象里      dep.addSub(this)    }  }}

afterGet用来做一些复位和清理工作:

p.afterGet = function () {  Observer.target = null  for (var id in this.deps) {    if (!this.newDeps[id]) {// 删除本次依赖收集时曾经不依赖的属性      this.deps[id].removeSub(this)    }  }  this.deps = this.newDeps}

traverse办法用来深度遍历所有嵌套属性,这样已转换的所有嵌套属性都会作为依赖项进行收集,也就是该表达式的watcher会被该属性及其所有后辈属性的dep对象收集,这样某个后辈属性的值变了也会触发更新:

function traverse (obj) {  var key, val, i  for (key in obj) {    val = obj[key]// 就是这里,获取一下该属性即可触发getter,此时Observer.target属性还是该watcher    if (_.isArray(val)) {      i = val.length      while (i--) traverse(val[i])    } else if (_.isObject(val)) {      traverse(val)    }  }}

如果某个属性的值后续发生变化依据第一篇咱们晓得在属性setter函数里会调用订阅者的update办法,这个订阅者就是Watcher实例,看一下这个办法:

p.update = function () {  if (!config.async || config.debug) {    this.run()  } else {    batcher.push(this)  }}

失常状况下是走else分支的,batcher会以异步和批量的形式来更新,然而最初也调用了run办法,所以先来看一下这个办法:

p.run = function () {  if (this.active) {    // 获取表达式的最新值    var value = this.get()    if (      value !== this.value ||      Array.isArray(value) ||      this.deep    ) {      var oldValue = this.value      this.value = value      var cbs = this.cbs      for (var i = 0, l = cbs.length; i < l; i++) {        cbs[i](value, oldValue)        // 某个回调删除了其余的回调的状况,目前属实不理解        var removed = l - cbs.length        if (removed) {          i -= removed          l -= removed        }      }    }  }}

逻辑很简略,遍历调用该watcher实例所有指令的update办法,指令会实现页面的更新工作。

批量更新请移步文章vue0.11版本源码浏览系列五:批量更新是怎么做的。

到这里模板编译的过程就完结了,接下来以一个指令的视角来看一下具体过程。

以if指令来看一下全过程

模板如下:

<div id="app">    <div v-if="show">我进去了</div></div>

JavaScript代码如下:

window.vm = new Vue({    el: '#app',    data: {        show: false    }})

在控制台输出window.vm.show = true这个div就会显示进去。

依据下面的剖析,咱们晓得对于v-if这个指令最终必定调用了_bindDir办法:

进入Directive后在_bind里调用了if指令的bind办法,该办法简化后如下:

{    bind: function () {        var el = this.el        if (!el.__vue__) {            // 创立了两个正文元素把咱们要显示暗藏的div给替换了,成果见下图            this.start = document.createComment('v-if-start')            this.end = document.createComment('v-if-end')            _.replace(el, this.end)            _.before(this.start, this.end)        }    }}

能够看到bind办法做的事件是用两个正文元素把这个元素从页面上给替换了。 bind办法之后就是给这个指令创立watcher

接下来在watcher里给Observer.target赋值及进行取值操作,触发了show属性的getter

依赖收集完后会调用if指令的update办法,看一下这个办法:

{    update: function (value) {        if (value) {            if (!this.unlink) {                var frag = templateParser.clone(this.template)                this.compile(frag)            }        } else {            this.teardown()        }    }}

因为咱们的初始值为false,所以走else分支调用了teardown办法:

{    teardown: function () {        if (!this.unlink) return        transition.blockRemove(this.start, this.end, this.vm)        this.unlink()        this.unlink = null    }}

本次unlink其实并没有值,所以就间接返回了,然而如果有值的话,teardown办法首先应用会应用transition类来移除元素,而后解除该指令的绑定。

当初让咱们在控制台输出window.vm.show = true,这会触发showsetter

而后会调用show属性的depnotify办法,dep的订阅者里目前就只有if指令的watcher,所以会调用watcherupdate办法,最终调用到if指令的update办法,此时的值为true,所以会走到if分支里,unlink也没有值,所以会调用compile办法:

{    compile: function (frag) {        var vm = this.vm        transition.blockAppend(frag, this.end, vm)    }}

疏忽了局部编译过程,能够看到应用看transition类来显示元素。这个过渡类咱们将在vue0.11版本源码浏览系列六:过渡原理里具体理解。

总结

能够发现在这个晚期版本里没有所谓的虚构DOM,没有diff算法,模板编译就是遍历元素及元素上的属性,给每个属性创立一个指令实例,对同样的指令表达式创立一个watcher实例,指令实例提供update办法给watcherwatcher会触发表达式里所有被察看属性的getter,而后watcher就会被这些属性的依赖收集实例dep收集起来,当属性值变动时会触发setter,在setter里会遍历dep里所有的watcher,调用更新办法,也就是指令实例提供的update办法,也就是最终指令对象的update办法实现页面更新。

当然,这部分的代码还是比较复杂的,远没有本文所说的这么简略,各种递归调用,各种函数重载,重复调用,让人看的云里雾里,有趣味的还请自行浏览。