Vue源码详解之compile函数的整体介绍与transclude分析

# Vue源码详解之compile函数的整体介绍与transclude分析

# 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内容里的,最终会转换成一个v-text的指令和一个textNode,而一个子组件<component><component>也会生成指令,还有slot,或者是你自己在元素上写的attribute比如hello=也会被编译为一个v-bind指令。我们看到,基本只要是涉及dom的(不是响应式的也包含在内,只要是vue提供的功能),不管是dom标签,还是dom属性、内容,都会被处理为指令。所以不要有指令就是attribute的惯性思维。

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')
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# transclude函数

/**
 * 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 = '<slot></slot>'
    }
    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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * 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 ' +
          '<body>. This will replace <body> 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
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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+?;/和/<!--/
  var frag = document.createDocumentFragment()
  var tagMatch = templateString.match(tagRE)
  var entityMatch = entityRE.test(templateString)
  var commentMatch = commentRE.test(templateString)
	
  if (!tagMatch && !entityMatch && !commentMatch) {
	// 如果没有tag 或者没有html字符实体(如&nbsp;) 或者 没有注释
    // text only, return a single text node.
    frag.appendChild(
      document.createTextNode(templateString)
    )
  } else {
    // 这里如前面的函数签名所说,使用了jQuery 和 component/domify中所使用的生成元素的策略
    // 我们要将模板变成实际的dom元素,一个简单的方法的是创建一个div document.createElement('div')
    // 然后再设置这个div的innerHtml为我们的模板,
    // (不直接创建一个模板的根元素是因为模板可能是片段实例,也就会生成多个dom元素)
    // (而设置这个div的outerHtml也不行哈,不能设置没有父元素的outerHtml)
    // 但是许多特殊元素只能再固定的父元素下存在,不能直接存在于div下,比如tbody,tr,th,td,legend等等等等
    // 那么怎么办? 所以就有了下面这个先获取第一个标签,然后按照map的里预先设置的内容,给模板设置设置好父元素,
    // 把模板嵌入到合适的父元素下,然后再层层进入父元素获取真正的模板元素.
    var tag = tagMatch && tagMatch[1]
    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
    // 这里是不断深入,进入正确的dom,
    // 比如你标签是tr,那么我会为包上table和tbody元素
    // 那么我拿到你的时候应该剥开外层的两个元素,让node指到tr
    while (depth--) {
      node = node.lastChild
    }

    var child
    /* eslint-disable no-cond-assign */
    // 用while循环把所有的子节点都提取了,因为可能是片段实例
    while (child = node.firstChild) {
    /* eslint-enable no-cond-assign */
      frag.appendChild(child)
    }
  }
  if (!raw) {
    trimNode(frag)
  }
  templateCache.put(cacheKey, frag)
  return frag
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
上次更新: 2022/7/6 上午11:51:19