街角小林 发表于 2021-8-17 21:29:55

Vue0.11版本源码阅读系列三:指令编译

因为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)      : nullfunction 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    // 是自定义组件则给元素设置一个属性标志    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 指令有三种:repeat、if、'component:
var terminalDirectives = [&#39;repeat&#39;,&#39;if&#39;,&#39;component&#39;]function skip () {}skip.terminal = truefunction checkTerminalDirectives (el, options) {// v-pre指令是用来告诉vue跳过编译该元素及其所有子元素if (_.attr(el, &#39;pre&#39;) !== null) {    return skip}var value, dirNamefor (var i = 0; i < 3; i++) {    dirName = terminalDirectives    if (value = _.attr(el, dirName)) {      return makeTerminalNodeLinkFn(el, dirName, value, options)    }}}顺便一提的是attr方法,这个方法其实是专门用来获取vue的自定义属性的,也就是v-开头的属性,为什么我们在模板里写的带v-前缀的属性在最终渲染的元素上没有呢,就是因为在这个方法里把它给移除了:
exports.attr = function (node, attr) {attr = config.prefix + attrvar val = node.getAttribute(attr)// 如果该自定义指令存在,则把它从元素上删除if (val !== null) {    node.removeAttribute(attr)}return val}makeTerminalNodeLinkFn方法:
function makeTerminalNodeLinkFn (el, dirName, value, options) {// 解析指令值var descriptor = dirParser.parse(value)// 获取该指令的指令方法,vue内置了很多指令处理方法,都在/src/directives/文件夹下var def = options.directivesvar fn = function terminalNodeLinkFn (vm, el, host) {    // 创建并把指令绑定到元素    vm._bindDir(dirName, el, descriptor, def, host)}fn.terminal = truereturn fn}parse方法用来解析指令的值,请移步文章:vue0.11版本源码阅读系列四:详解指令值解析函数,比如指令值为click: a = a + 1 | uppercase,处理完最后会返回如许的信息:
{    arg: &#39;click&#39;,    expression: &#39;a = a + 1&#39;,    filters: [      { name: &#39;uppercase&#39;, 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,// 该指令对应的处理方法    transcluded: transcluded}非vue指令的属性如果存在动态绑定,也会进行处理,在该版本vue里的动态绑定是使用双大括号插值的,和2.x的使用v-bind不一样。
如:
,所以会通过正则来匹配判断是否存在动态绑定,最终返回下列格式的数据:
{    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 + &#39;:&#39; + value)      vm._bindDir(&#39;attr&#39;, 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      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, dir.def, host)      }      }    }}}总结一下compileNode的作用就是遍历元素上的属性,分别给其创建一个指令绑定函数,这个指令函数后续调用时会创建一个Directive实例,这个类后续再看。
如果该元素存在子元素的话会调用compileNodeList方法,子元素又有子元素的话又会继承调用,其实就是递归所有子元素调用compileNode方法。
compile方法最后返回了compositeLinkFn方法,这个方法被立刻执行了,这个方法里调用了刚才天生的nodeLinkFn和childLinkFn方法,执行结果就是会把所有的元素及子元素的指令进行绑定,也就是给元素上的某个属性大概说指令都创建了一个Directive实例。
Directive

指令这个类主要做的事是把DOM和数据绑定起来,实例化的时间会调用指令的bind方法,同时会实例化一个Watcher实例,后续数据更新的时间会调用指令的update方法。
function Directive (name, el, vm, descriptor, def, host) {this.name = namethis.el = elthis.vm = vmthis.raw = descriptor.rawthis.expression = descriptor.expressionthis.arg = descriptor.argthis.filters = _.resolveFilters(vm, descriptor.filters)this._host = hostthis._locked = falsethis._bound = falsethis._bind(def)}构造函数定义一些属性以及调用了_bind方法,resolveFilters方法会把过滤器以getter和setter分别收集到一个数组里,便于后续循环调用:
exports.resolveFilters = function (vm, filters, target) {var res = target || {}filters.forEach(function (f) {    var def = vm.$options.filters    if (!def) return    var args = f.args    var reader, writer    if (typeof def === &#39;function&#39;) {      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, .concat(args))          : reader.call(vm, value)      })    }    if (writer) {      if (!res.write) res.write = []      res.write.push(function (value, oldVal) {      return args          ? writer.apply(vm, .concat(args))          : writer.call(vm, value, oldVal)      })    }})return res}_bind方法:
p._bind = function (def) {if (typeof def === &#39;function&#39;) {    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    if (!watcher) {      // 该表达式未创建过watcher,则实例化一个      watcher = this.vm._watchers = 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 = vmthis.expression = expressionthis.cbs = this.id = ++uidthis.active = trueoptions = options || {}this.deep = !!options.deepthis.user = !!options.userthis.deps = Object.create(null)if (options.filters) {    this.readFilters = options.filters.read    this.writeFilters = options.filters.write}// 将表达式解析为getter/settervar res = expParser.parse(expression, options.twoWay)this.getter = res.getthis.setter = res.setthis.value = this.get()}构造函数的逻辑很简单,声明一些变量、将表达式解析为getter和setter的类型,比如: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.vmvar 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 = thisthis.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.idif (!this.newDeps) {    this.newDeps = dep    if (!this.deps) {      this.deps = dep      // 收集该watcher实例到该属性的依赖对象里      dep.addSub(this)    }}}afterGet用来做一些复位和清理工作:
p.afterGet = function () {Observer.target = nullfor (var id in this.deps) {    if (!this.newDeps) {// 删除本次依赖收集时已经不依赖的属性      this.deps.removeSub(this)    }}this.deps = this.newDeps}traverse方法用来深度遍历所有嵌套属性,如许已转换的所有嵌套属性都会作为依赖项进行收集,也就是该表达式的watcher会被该属性及其所有后代属性的dep对象收集,如许某个后代属性的值变了也会触发更新:
function traverse (obj) {var key, val, ifor (key in obj) {    val = obj// 就是这里,获取一下该属性即可触发getter,此时Observer.target属性还是该watcher    if (_.isArray(val)) {      i = val.length      while (i--) traverse(val)    } 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(value, oldValue)      // 某个回调删除了其他的回调的情况,现在属实不了解      var removed = l - cbs.length      if (removed) {          i -= removed          l -= removed      }      }    }}}逻辑很简单,遍历调用该watcher实例所有指令的update方法,指令会完成页面的更新工作。
批量更新请移步文章vue0.11版本源码阅读系列五:批量更新是怎么做的。
到这里模板编译的过程就结束了,接下来以一个指令的视角来看一下具体过程。
以if指令来看一下全过程

模板如下:
    我出来了

JavaScript代码如下:
window.vm = new Vue({    el: &#39;#app&#39;,    data: {      show: false    }})在控制台输入window.vm.show = true这个div就会显示出来。
根据上面的分析,我们知道对于v-if这个指令最终肯定调用了_bindDir方法:
https://p3.toutiaoimg.com/large/pgc-image/c5e8dbf209dd4b328545d88fcd65f1c3
进入Directive后在_bind里调用了if指令的bind方法,该方法简化后如下:
{    bind: function () {      var el = this.el      if (!el.__vue__) {            // 创建了两个注释元素把我们要显示隐藏的div给替换了,效果见下图            this.start = document.createComment(&#39;v-if-start&#39;)            this.end = document.createComment(&#39;v-if-end&#39;)            _.replace(el, this.end)            _.before(this.start, this.end)      }    }}https://p26.toutiaoimg.com/large/pgc-image/e1384d9badb34138bcafb06c75b083c1
可以看到bind方法做的事变是用两个注释元素把这个元素从页面上给替换了。 bind方法之后就是给这个指令创建watcher:
https://p9.toutiaoimg.com/large/pgc-image/af54b60d5f6f4be5b47016d0926c4cd3
接下来在watcher里给Observer.target赋值及进行取值操纵,触发了show属性的getter:
https://p3.toutiaoimg.com/large/pgc-image/bef851e8a9704a5f8cb8195bfd939cb5
依赖收集完后会调用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,这会触发show的setter:
https://p9.toutiaoimg.com/large/pgc-image/470c8d8cde6842009fad8155bdf037bd
然后会调用show属性的dep 的notify方法,dep的订阅者里现在就只有if指令的watcher,所以会调用watcher的update方法,最终调用到if指令的update方法,此时的值为true,所以会走到if分支里,unlink也没有值,所以会调用compile方法:
https://p26.toutiaoimg.com/large/pgc-image/aa5d221c7e8347fbada286aa66b8ef87
{    compile: function (frag) {      var vm = this.vm      transition.blockAppend(frag, this.end, vm)    }}忽略了部分编译过程,可以看到使用看transition类来显示元素。这个过渡类我们将在vue0.11版本源码阅读系列六:过渡原理里详细了解。
总结

可以发现在这个早期版本里没有所谓的虚拟DOM,没有diff算法,模板编译就是遍历元素及元素上的属性,给每个属性创建一个指令实例,对同样的指令表达式创建一个watcher实例,指令实例提供update方法给watcher,watcher会触发表达式里所有被观察属性的getter,然后watcher就会被这些属性的依赖收集实例dep收集起来,当属性值变化时会触发setter,在setter里会遍历dep里所有的watcher,调用更新方法,也就是指令实例提供的update方法,也就是最终指令对象的update方法完成页面更新。
固然,这部分的代码还是比较复杂的,远没有本文所说的这么简单,各种递归调用,各种函数重载,反复调用,让人看的云里雾里,有兴趣的还请自行阅读。
页: [1]
查看完整版本: Vue0.11版本源码阅读系列三:指令编译