乐趣区

关于javascript:Vue011版本源码阅读系列三指令编译

因为 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 = true
function 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 办法实现页面更新。

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

退出移动版