资讯专栏INFORMATION COLUMN

Vue源码详细解析:transclude,compile,link,依赖,批处理...一网打尽,全解

yy736044583 / 684人阅读

摘要:先说遍历,很简单,如下行左右代码就足够遍历一个对象了遇到普通数据属性,直接处理,遇到对象,遍历属性之后递归进去处理属性,遇到数组,递归进去处理数组元素。这样改进之后我就不需要对数组元素进行响应式处理,只是遇到数组的时候把数组的方法变异即可。

用了Vue很久了,最近决定系统性的看看Vue的源码,相信看源码的同学不在少数,但是看的时候却发现挺有难度,Vue虽然足够精简,但是怎么说现在也有10k行的代码量了,深入进去逐行查看的时候感觉内容庞杂并且搞不懂代码的目的,同时网上的深入去仔细阐述Vue的compile/link/ expression parse/依赖订阅和收集/batcher的文章却不多,我自己读源码时,深感在这些环节可供参考的资料稀缺。网上较多的文章都在讲getter/setter、Mutation Observer和LRU缓存。所以我趁着寒假详细的阅读了Vue构建整个响应式过程的代码,基本包括数据observe到模板解析、transclude、compile、link、指令的bind、update、dom批处理更新、数组diff等等环节,并用这篇文章详细的介绍出来,希望能帮到想学习Vue源码或者想参与Vue维护、提交pr的同学。

Vue源码详解系列文章和配套的我整理的Vue源码注释版已经在git上开项:Vue源码注释版及详解,欢迎大家在git上查看,并配合注释版源码使用。订阅文章更新请watch。
注释版源码主要注释了本文中涉及的部分,依然有很多没有涉及,我个人精力有限,欢迎大家提pr,如果您喜欢,多谢您的star~

本文介绍的源码版本是当前(17年2月23日)1.x版本的最新版v1.0.26,2.x版本的源码我先学学虚拟dom之后再进行。

源码整体概览

Vue源码构造实例的过程就一行this._init(options),用你的参数对象去执行init初始化函数。init函数中先进行了大量的参数初始化操作this.xxx = blabla,然后剩下这么几行代码(后文所有的英文注释是尤雨溪所写,中文是我添加的,英文注释极其精确、简洁,请勿忽略

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)
}

基本就是触发init钩子,初始化一些状态,初始化event,然后触发created钩子,最后挂载到具体的元素上面去。_initState()方法中包含了数据的初始化操作,也就是让数据变成响应式的,让Vue能够监听到数据的变动。而this.$mount()方法则承载了绝大部分的代码量,负责模板的嵌入、编译、link、指令和watcher的生成、批处理的执行等等。

从数据的响应化说起

嗯,是的,虽然这个observe数据的部分已经被很多文章说烂了,但是我并不只是讲getter/setter,这里应该会有你没看过的部分,比如Vue是如何解决"getter/setter无法监听属性的添加和删除"的。

熟悉Vue的同学都了解Vue的响应式特性,对于data对象的几乎任何更改我们都能够监听到。这是MVVM的基础,基本思路就是遍历每一个属性,然后使用Object.defineProperty将这个属性设置为响应式的(即我能监听到他的改动)。

先说遍历,很简单,如下10行左右代码就足够遍历一个对象了:

function touch (obj) {
    if (typeof obj === "object")
      if (Array.isArray(obj)) {
        for (let i = 0,l = obj.length; i < l; i++) {
          touch(obj[i])
        }
      } else {
        let keys = Object.keys(obj)
        for (let key of keys) touch(obj[key])
      }
    console.log(obj)
  }

遇到普通数据属性,直接处理,遇到对象,遍历属性之后递归进去处理属性,遇到数组,递归进去处理数组元素(console.log)。

遍历完就到处理了,也就是Object.defineProperty部分了,对于一个对象,我们可以用这个来改写它属性的getter/setter,这样,当你改属性的值我就有办法监听到。但是对于数组就有问题了。

你也许想到可以遍历当前存在的下标,然后执行Object.defineProperty。这种处理方法先不说性能问题,很多时候我们操作数组是采用push、pop、splice、unshift等方法来操作的,光是push你就没办法监听,更不要说pop后你设置的getter/setter就直接没了。

所以,Vue的方法是,改写数组的push、pop等8个方法,让他们在执行之后通知我数组更新了(这种方法带来的后果就是你不能直接修改数组的长度或者通过下标去修改数组。参见官网)。这样改进之后我就不需要对数组元素进行响应式处理,只是遇到数组的时候把数组的方法变异即可。于是在用户使用数组的push、pop等方法会改变数组本身的方法时,可以监听到数组变动。

此外,当数组内部元素是对象时,设置getter/setter是可以监听对象的,所以对于数组元素还是要遍历一下的。如果不是对象,比如a[0]是字符串、数字?那就没办法了,但是vue为数组提供了$set和$remove,方便我们可以通过下标去响应式的改动数组元素,这里后文再说。

我们先说说怎么“变异”数组的push等方法,并且找出数组元素中的对象,让对象响应式。我们结合我的注释版源码来看一下。

Vue.prototype._initData = function () {
    // 初始化数据,其实一方面把data的内容代理到vm实例上,
    // 另一方面改造data,变成reactive的
    // 即get时触发依赖收集(将订阅者加入Dep实例的subs数组中),set时notify订阅者
    var dataFn = this.$options.data
    var data = this._data = dataFn ? dataFn() : {}
   
    var props = this._props
    // proxy data on instance
    var keys = Object.keys(data)
    var i, key
    i = keys.length
    while (i--) {
        key = keys[i]
        // 将data属性的内容代理到vm上面去,使得vm访问指定属性即可拿到_data内的同名属性
        // 实现vm.prop === vm._data.prop,
        // 这样当前vm的后代实例就能直接通过原型链查找到父代的属性
        // 比如v-for指令会为数组的每一个元素创建一个scope,这个scope就继承自vm或上级数组元素的scope,
        // 这样就可以在v-for的作用域中访问父级的数据
        this._proxy(key)
    }
    // observe data
    //重点来了
    observe(data, this)
  }

(注释里的依赖收集、Dep什么的大家看不懂没关系,请跳过,后面会细说)

代码中间做了_proxy操作,注释里我已经写明原因。_proxy操作也很简单想了解的话大家自己查看源码即可。

代理完了之后就开始observe这个data:

export function observe (value, vm) {
  if (!value || typeof value !== "object") {
    // 保证只有对象会进入到这个函数
    return
  }
  var ob
  if (
    //如果这个数据身上已经有ob实例了,那observe过了,就直接返回那个ob实例
    hasOwn(value, "__ob__") &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 是对象(包括数组)的话就深入进去遍历属性,observe每个属性
    ob = new Observer(value)
  }
  if (ob && vm) {
    // 把vm加入到ob的vms数组当中,因为有的时候我们会对数据手动执行$set/$delete操作,
    // 那么就要提示vm实例这个行为的发生(让vm代理这个新$set的数据,和更新界面)
    ob.addVm(vm)
  }
  return ob
}

代码的执行过程一般都是进入到那个else if里,执行new Observer(value),至于shouldConvert和后续的几个判断则是为了防止value不是单纯的对象而是Regexp或者函数之类的,或者是vm实例再或者是不可扩展的,shouldConvert则是某些特殊情况下为false,它的解释参见源码里尤雨溪的注释。

那好,现在就进入到拿当前的data对象去new Observer(value),现在你可能会疑惑,递归遍历的过程不是应该是纯命令式的、面向过程的吗?怎么代码跑着跑着跑出来一句new一个对象了,嗯先不用管,我们先理清代码执行过程,先带着这个疑问。同时,我们注意到代码最后return了ob,结合代码,我们可以理解为如果return的是undifned,那么说明传进来的value不是对象,反之return除了一个ob,则说明这个value是对象或数组,他可以添加或删除属性,这一点我们先记着,这个东西后面有用。

我们先看看Observer构造函数:

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object"s property keys into getter/setters that
 * collect dependencies and dispatches updates.
 *
 * @param {Array|Object} value
 * @constructor
 */

function Observer (value) {
  this.value = value
  this.dep = new Dep()
  def(value, "__ob__", this) //value的__ob__属性指向这个Ob实例
  if (isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    // 如果是对象则使用walk遍历每个属性
    this.walk(value)
  }
}
observe一个数组

上述代码中,如果遇到数组data中的数组实例增加了一些“变异”的push、pop等方法,这些方法会在数组原本的push、pop方法执行后发出消息,表明发生了改动。听起来这好像可以用继承的方式实现: 继承数组然后在这个子类的原型上附加上变异的方法。

但是你需要知道的是在es5及更低版本的js里,无法完美继承数组,主要原因是Array.call(this)时,Array根本不是像一般的构造函数那样对你传进去this进行改造,而是直接返回一个新的数组。所以一般的继承方式就没法实现了。参见这篇文章,所以出现了新建一个iframe,然后直接拿那个iframe里的数组的原型进行修改,添加自定义方法,诸如此类的hack方法,在此按下不表。

但是如果当前浏览器里存在__proto__这个非标准属性的话(大部分都有),那又可以有方法继承,就是创建一个继承自Array.prototype的Object: Object.create(Array.prototype),在这个继承了数组原生方法的对象上添加方法或者覆盖原有方法,然后创建一个数组,把这个数组的__proto__指向这个对象,这样这个数组的响应式的length属性又得以保留,又获得了新的方法,而且无侵入,不会改变本来的数组原型。

Vue就是基于这个思想,先判断__proto__能不能用(hasProto),如果能用,则把那个一个继承自Array.prototype的并且添加了变异方法的Object (arrayMethods),设置为当前数组的__proto__,完成改造,如果__proto__不能用,那么就只能遍历arrayMethods就一个个的把变异方法def到数组实例上面去,这种方法效率不高,所以优先使用改造__proto__的那个方法。

源码里后面那句this.observeArray非常简单,for遍历传进去的value,然后对每个元素执行observe,处理之前说的数组的元素为对象或者数组的情况。好了,对于数组的讨论先打住,至于数组的变异方法怎么通知我他进行了更改之类的我们不说了,我们先说清楚对象的情况,对象说清楚了,再去看源码就一目了然了。

observe 对象

对于对象,上面的代码执行this.walk(value),他“游走”对象的每个属性,对属性和属性值执行defineReactive函数。

function Dep () {
  this.id = uid++
  this.subs = []
}

Dep.prototype.depend = function () {
  Dep.target.addDep(this)
}

Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = toArray(this.subs)
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

function defineReactive (obj, key, val) {
  // 生成一个新的Dep实例,这个实例会被闭包到getter和setter中
  var dep = new Dep()

  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set
  // 对属性的值继续执行observe,如果属性的值是一个对象,那么则又递归进去对他的属性执行defineReactive
  // 保证遍历到所有层次的属性
  var childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      // 只有在有Dep.target时才说明是Vue内部依赖收集过程触发的getter
      // 那么这个时候就需要执行dep.depend(),将watcher(Dep.target的实际值)添加到dep的subs数组中
      // 对于其他时候,比如dom事件回调函数中访问这个变量导致触发的getter并不需要执行依赖收集,直接返回value即可
      if (Dep.target) {
        dep.depend()
        if (childOb) {
         //如果value是对象,那就让生成的Observer实例当中的dep也收集依赖
          childOb.dep.depend()
        }
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            //如果数组元素也是对象,那么他们observe过程也生成了ob实例,那么就让ob的dep也收集依赖
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // observe这个新set的值
      childOb = observe(newVal)
      // 通知订阅我这个dep的watcher们:我更新了
      dep.notify()
    }
  })
}

我们来说说这个Dep,Dep类的定义极其简单,一个id,一个数组,他就是一个很基本的发布者-观察者模式的实现,作为一个发布者,他的subs属性用来存放了订阅他的观察者,也就是后面我们会说到的watcher。

defineProperty是用来将对象的属性转化为响应式的getter/setter的,defineProperty函数执行过程中新建了一个Dep,闭包在了属性的getter和setter中,因此每个属性都有一个唯一的Dep与其对应,我们暂且可以把属性和他对应的Dep理解为一体的。

Dep其实是dependence依赖的缩写,我之前一直没能理解依赖、依赖收集是什么,其实对于我们的一个模板{{a+b}},我们会说他的依赖有a和b,其实就是依赖了data的a和b属性,更精确的说是依赖了a属性中闭包的dep实例和b属性中闭包的那个dep实例。

详细来说:我们的这个{{a+b}}在dom里最终会被"a+b"表达式的真实值所取代,所以存在一个求出这个“a+b”的表达式的过程,求值的过程就会自然的分别触发a和b的getter,而在getter中,我们看到执行了dep.depend(),这个函数实际上回做dep.addSub(Dep.target),即在dep的订阅者数组中存放了Dep.target,让Dep.target订阅dep

那Dep.target是什么?他就是我们后面介绍的Watcher实例,为什么要放在Dep.target里呢?是因为getter函数并不能传参,dep可以通过闭包的形式放进去,那watcher可就不行了,watcher内部存放了a+b这个表达式,也是由watcher计算a+b的值,在计算前他会把自己放在一个公开的地方(Dep.target),然后计算a+b,从而触发表达式中所有遇到的依赖的getter,这些getter执行过程中会把Dep.target加到自己的订阅列表中。等整个表达式计算成功,Dep.target又恢复为null.这样就成功的让watcher分发到了对应的依赖的订阅者列表中,订阅到了自己的所有依赖。

我们可以看到这是极其精妙的一笔!在一个表达式的求值过程中隐式的完成依赖订阅。

上面完成的是订阅的过程,而上面setter代码里的dep.notify就负责完成数据变动时通知订阅者的功能。而且数据变化时,后文会说明只有依赖他的那些dom会精确更新,不会出现一些介绍mvvm的文章里虽然实现了订阅更新但是重新计算整个视图的情况。

于是一整个对象订阅、notify的过程就结束了。

Observer类?

现在我们明白了Dep的作用和收集订阅依赖的过程,但是对于watcher是什么肯定还是云里雾里的,先别急。我们先解决之前的疑问:为什么命令式的监听过程中出现了个new Observer()?而且构造函数第一行就创建了一个dep(这个dep不是defineReactive里的那个闭包dep,注意区分),在defineReactive函数的getter中还执行了childOb.dep.depend(),去完成了这个dep的watcher添加?

我们考虑一下这样的情况,比如我的data:{a:{b:true}},这个时候,如果页面有dom上有个指令:class="a",而我想响应式的删除data.a的b属性,此时我就没有办法了,因为defineReactive中的getter/setter都不会执行(他们甚至还会在delete a.b时被清空),闭包里的那个dep就无法通知对应的watcher。

这就是getter和setter存在的缺陷:只能监听到属性的更改,不能监听到属性的删除与添加。

Vue的解决办法是提供了响应式的api: vm.$set/vm.$delete/ Vue.set/ Vue.delete /数组的$set/数组的$remove。

具体方法是为所有的对象和数组(只有这俩哥们才可能delete和新建属性),也创建一个dep,也完成收集依赖的过程。我们回到源码defineReactive再看一遍,在执行defineReactive(data,"a",{b:true})时,他首先创造了那个闭包在getter/setter中的dep,然后var childOb = observe(val),val是{b:true},那就会为这个对象new Observer(val),并放在val.__ob__上,而这个ob实例上存放了一个Dep实例。现在我们看到,有两个Dep实例,一个是闭包里的dep,一个是为{b:true}创建的ob上的这个dep。而:class="a"生成的watcher的求值过程中会触发到a的getter,那就会执行:

dep.depend()
if (childOb) {
    //如果value是对象,那就让生成的Observer实例当中的dep也收集依赖
    childOb.dep.depend()
}

这一步,:class="a"的watcher既会订阅闭包dep,也会订阅ob的dep。

当我们执行Vue.delete(this.a,"b"),内部会执行del函数,他会找到要删除属性的那个对象,也是{b:true},它的__ob__属性存放了ob,现在先删除属性,然后执行ob.dep.notify,通知所有依赖这个对象的watcher重新计算,这个时候属性已经删除了,重新计算的值(为空)就会刷新到页面上,完成dom响应式更新。参见此处源码。

不仅对于属性的删除这样,属性的的添加也是类似的,都是为了弥补getter和setter存在的缺陷,都会找到这个dep执行notify。不过data的顶级属性略有不同,涉及到digest,此处不表。

同时我们再回到之前遍历数组的代码,我们数组的响应化代码甚至都里没有getter/setter,他连那个闭包的dep都没有,代码只是变异了一下push/pop方法。他有的只是那个childOb上的dep,所以数组的响应式过程都是notify的这个dep,不管是数组的变异方法),还是数组的$set/$remove里我们都会看到是在这个dep上触发notify,通知订阅了整个数组的watcher进行更新。所以你知道这个dep的重要性了把。当然这也就有问题了,我一个watcher订阅整个数组,当数组的元素有改动我就会收到消息,但我不知道变动的是哪个,难道我要用整个数组重新构造一下dom?所以这就是数组diff算法的使用场景了。

至于Observer,这个额外的实例上存放了一个dep,这个dep配合Observer的addVm、removeVm、vms等属性来一起搞定data的顶级属性的新增或者删除,至于为什么不直接在数据上存放dep,而是搞个Observer,并把dep定义在上面,我觉得是Observer的那些方法和vms等属性,并不是所有的dep都应该具有的,作为dep的实例属性是不应该的,所以就抽象了个Observer这么个东东吧,顺便把walk、convert之类的函数变成方法挂在Observer上了,抽象出个专门用来observe的类而已,这部分纯属个人臆测。

_compile

介绍完响应式的部分,算是开了个头了,后面的内容很多,但是层层递进,最终完成响应式精确订阅和批处理更新的整个过程,过程比较流程,内容耦合度也高,所以我们先来给后文的概览,介绍一下大体过程。

我们最开始的代码里提到了Vue处理完数据和event之后就到了$mount,而$mount就是在this._compile后触发编译完成的钩子而已,所以核心就是Vue.prototype._compile。

_compile包含了Vue构建的三个阶段,transclude,compile,link。而link阶段其实是放在linkAndCapture里执行的,这里又包含了watcher的生成,指令的bind、update等操作。

我先简单讲讲什么是指令,虽然Vue文档里说的指令是v-if,v-for等这种HTML的attribute,其实在Vue内部,只要是被Vue处理的dom上的东西都是指令,比如dom内容里的{{a}},最终会转换成一个v-text的指令和一个textNode,而一个子组件也会生成指令,还有slot,或者是你自己在元素上写的attribute比如hello={{you}}也会被编译为一个v-bind指令。我们看到,基本只要是涉及dom的(不是响应式的也包含在内,只要是vue提供的功能),不管是dom标签,还是dom属性、内容,都会被处理为指令。所以不要有指令就是attribute的惯性思维。

回过头来,_compile部分大致分为如下几个部分

transclude

transclude的意思是内嵌,这个步骤会把你template里给出的模板转换成一段dom,然后抽取出你el选项指定的dom里的内容(即子元素,因为模板里可能有slot),把这段模板dom嵌入到el里面去,当然,如果replace为true,那他就是直接替换el,而不是内嵌。我们大概明白transclude这个名字的意义了,但其实更关键的是把template转换为dom的过程(如`

{{a}}

`字符串转为真正的段落元素),这里为后面的编译准备好了dom。

compile

compile的的过程具体就是**遍历模板解析出模板里的指令**。更精确的说是解析后生成了指令描述对象。
同时,compile函数是一个高阶函数,他执行完成之后的返回值是另一个函数:link,所以compile函数的第一个阶段是编译,返回出去的这个函数完成另一个阶段:link。

link

compile阶段将指令解析成为指令描述对象(descriptor),闭包在了link函数里,link函数会把descriptor传入Directive构造函数,创建出真正的指令实例。此外link函数是作为参数传入linkAndCaptrue中的,后者负责执行link,同时取出这些新生成的指令,先按照指令的预置的优先级从高到低排好顺序,然后遍历指令执行指令的_bind方法,这个方法会为指令创建watcher,并计算表达式的值,完成前面描述的依赖收集。并最后执行对应指令的bind和update方法,使指令生效、界面更新。

此外link函数最终的返回值是unlink函数,负责在vm卸载时取消对应的dom到数据的绑定。

是时候回过头来看看Vue官网这张经典的图了,以前我刚学Vue时也是对于Watcher,Directive之类的概念云里雾里。但是现在大家看这图是不是很清晰明了?

模板中每个指令/数据绑定都有一个对应的 watcher 对象,在计算过程中它把属性记录为依赖。之后当依赖的 setter 被调用时,会触发 watcher 重新计算 ,也就会导致它的关联指令更新 DOM。 --Vue官网

上代码:

Vue.prototype._compile = function (el) {
    var options = this.$options

    // transclude and init element
    // transclude can potentially replace original
    // so we need to keep reference; this step also injects
    // the template and caches the original attributes
    // on the container node and replacer node.
    var original = el
    el = transclude(el, options)
    // 在el这个dom上挂一些参数,并触发"beforeCompile"钩子,为compile做准备
    this._initElement(el)

    // handle v-pre on root node (#2026)
    // v-pre指令的话就什么都不用做了。
    if (el.nodeType === 1 && getAttr(el, "v-pre") !== null) {
      return
    }

    // root is always compiled per-instance, because
    // container attrs and props can be different every time.
    var contextOptions = this._context && this._context.$options
    var rootLinker = compileRoot(el, options, contextOptions)

    // resolve slot distribution
    // 具体是将各个slot存储到vm._slotContents的对应属性里面去,
    // 然后后面的compile阶段会把slot解析为指令然后进行处理
    resolveSlots(this, options._content)

    // compile and link the rest
    var contentLinkFn
    var ctor = this.constructor
    // component compilation can be cached
    // as long as it"s not using inline-template
    // 这里是组件的情况才进入的,大家先忽略此段代码
    if (options._linkerCachable) {
      contentLinkFn = ctor.linker
      if (!contentLinkFn) {
        contentLinkFn = ctor.linker = compile(el, options)
      }
    }

    // link phase
    // make sure to link root with prop scope!
    var rootUnlinkFn = rootLinker(this, el, this._scope)
    // compile和link一并做了
    var contentUnlinkFn = contentLinkFn
      ? contentLinkFn(this, el)
      : compile(el, options)(this, el)

    // register composite unlink function
    // to be called during instance destruction
    this._unlinkFn = function () {
      rootUnlinkFn()
      // passing destroying: true to avoid searching and
      // splicing the directives
      contentUnlinkFn(true)
    }

    // finally replace original
    if (options.replace) {
      replace(original, el)
    }

    this._isCompiled = true
    this._callHook("compiled")
  }

尤雨溪的注释已经极尽详细,上面的代码很清晰(如果你用过angular,那你会感觉很熟悉,angular里也是有transclude,compile和link的,虽然实际差别很大)。我们在具体进入各部分代码前先说说为什么dom的编译要分成compile和link两个phase。

在组件的多个实例、v-for数组等场合,我们会出现同一个段模板要绑定不同的数据然后分发到dom里面去的需求。这也是mvvm性能考量的主要场景:大数据量的重复渲染生成。而重复渲染的模板是一致的,不一致的是他们需要绑定的数据,因此compile阶段找出指令的过程是不用重复计算的,只需要link函数(和里面闭包的指令),而模板生成的dom使用原生的cloneNode方法即可复制出一份新的dom。现在,复制出的新dom+ link+具体的数据即可完成渲染,所以分离compile、并缓存link使得Vue在渲染时避免大量重复的性能消耗。

transclude函数

这里大家可以考虑一下,我给你一个空的documentFragment和一段html字符串,让你把html生成dom放进fragment里,你应该怎么做?innerHTML?documentFragment可是没有innerHtml的哦。那先建个div再innerHTML?那万一我的html字符串的是tr元素呢?tr并不能直接放进div里哦,那直接用outerHTML?没有parent Node的元素是不能设置outerHTML的哈(parent是fragment也不行),那我先用正则提取第一个标签,先createElement这个标签然后在写他的innerHTML总可以了吧?并不行,我没告诉你我给你的这段HTML最外层就一个元素啊,万一是个片段实例呢(也就是包含多个顶级元素,如

1

2

),所以我才说给你一个fragment当容器,让你把dom装进去。

上面这个例子说明了实际转换dom过程中,可能遇到的一个小坑,只是想说明字符串转dom并不是看起来那么一行innerHTML的事。

/**
 * Process an element or a DocumentFragment based on a
 * instance option object. This allows us to transclude
 * a template node/fragment before the instance is created,
 * so the processed fragment can then be cloned and reused
 * in v-for.
 *
 * @param {Element} el
 * @param {Object} options
 * @return {Element|DocumentFragment}
 */

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) {
    options._containerAttrs = extractAttrs(el)
  }
  // for template tags, what we want is its content as
  // a documentFragment (for fragment instances)
  if (isTemplate(el)) {
    el = parseTemplate(el)
  }
  if (options) {
    // 如果当前是component,并且没有模板,只有一个壳
    // 那么只需要处理内容的嵌入
    if (options._asComponent && !options.template) {
      options.template = ""
    }
    if (options.template) {
    //基本都会进入到这里
      options._content = extractContent(el)
      el = transcludeTemplate(el, options)
    }
  }
  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
}

我们看上面的代码,先options._containerAttrs = extractAttrs(el),这样就把el元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上。因为我们前面说过,这些属性是vm实际挂载的根元素上的,如果vm是一个组件之类的,那么他们应该是在父组件的作用于编译/link的,所以需要预先提取出来,因为如果replace为true,el元素会被模板元素替换,但是他上面的属性是会编译link后merge到模板元素上面去。
然后进入到那个两层的if里, extractContent(el),将el的内容(子元素和文本节点)抽取出来,因为如果模板里有slot,那么他们要分发到对应的slot里。
然后就到el = transcludeTemplate(el, options)

/**
 * Process the template option.
 * If the replace option is true this will swap the $el.
 *
 * @param {Element} el
 * @param {Object} options
 * @return {Element|DocumentFragment}
 */

function transcludeTemplate (el, options) {
  var template = options.template
  var frag = parseTemplate(template, true)
  if (frag) {
    // 对于非片段实例情况且replace为true的情况下,frag的第一个子节点就是最终el元素的替代者
    var replacer = frag.firstChild
    var tag = replacer.tagName && replacer.tagName.toLowerCase()
    if (options.replace) {
      /* istanbul ignore if */
      if (el === document.body) {
        process.env.NODE_ENV !== "production" && warn(
          "You are mounting an instance with a template to " +
          ". This will replace  entirely. You " +
          "should probably use `replace: false` here."
        )
      }
      // there are many cases where the instance must
      // become a fragment instance: basically anything that
      // can create more than 1 root nodes.
      if (
        // multi-children template
        frag.childNodes.length > 1 ||
        // non-element template
        replacer.nodeType !== 1 ||
        // single nested component
        tag === "component" ||
        resolveAsset(options, "components", tag) ||
        hasBindAttr(replacer, "is") ||
        // element directive
        resolveAsset(options, "elementDirectives", tag) ||
        // for block
        replacer.hasAttribute("v-for") ||
        // if block
        replacer.hasAttribute("v-if")
      ) {
        return frag
      } else {
        // 抽取replacer自带的属性,他们将在自身作用域下编译
        options._replacerAttrs = extractAttrs(replacer)
        // 把el的所有属性都转移到replace上面去,因为我们后面将不会再处理el直至他最后被replacer替换
        mergeAttrs(el, replacer)
        return replacer
      }
    } else {
      el.appendChild(frag)
      return el
    }
  } else {
    process.env.NODE_ENV !== "production" && warn(
      "Invalid template option: " + template
    )
  }
}

首先执行解析parseTemplate(template, true),得到一段存放在documentFragment里的真实dom,然后就判断是否需要replace。(若replace为false)之后判断是否是片段实例,官网已经讲述哪几种情况对应片段实例,而代码里那几个判断就是对应的处理。若不是,那就进入后续的情况,我已经注释代码作用,就不再赘述。我们来说说parseTemplate,因为vue支持template选项写#app这样的HTML选择符,也支持直接存放模板字符串、document fragment、dom元素等等,所以针对各种情况作了区分,如果是一个已经好的dom那几乎不用处理,否则大部分情况下都是执行stringToFragment:

function stringToFragment (templateString, raw) {
  // 缓存机制
  // try a cache hit first
  var cacheKey = raw
    ? templateString
    : templateString.trim()
  var hit = templateCache.get(cacheKey)
  if (hit) {
    return hit
  }
    //这三个正则分别是/<([w:-]+)/ 和/&#?w+?;/和/
              
  
最新活动
阅读需要支付1元查看
<