Skip to content

Vue源码解析(二):_compile函数之transclude #2

Open
@pf12345

Description

@pf12345

上面说了下,在Vue.prototype.$mount完成了大部分工作,而在$mount方法里面,最主要的工作量由this._compile(el)承担;其主要包括transclude(嵌入)、compileRoot(根节点编译)、compile(页面其他的编译);而在这儿主要说明transclude方法;

通过对transclude进行网络翻译结果是"嵌入";其主要目的是将页面中自定义的节点转化为真实的html节点;如一个组件<hello></hello>其实际dom为<div><h1>hello {{message}}</h1></div>源码; 当我们使用时<div><hello></hello></div>; 会通过transclude将其转化为<div><div><h1>hello {{message}}</h1></div></div>,见源码注释;

那transclude具体干了什么呢,我们先看它的源码:

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) {
    // 把el(虚拟节点,如<hello></hello>)元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上
    // 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组
    options._containerAttrs = extractAttrs(el)
  }
  // for template tags, what we want is its content as
  // a documentFragment (for fragment instances)
  // 判断是否为 template 标签
  if (isTemplate(el)) {
    // 得到一段存放在documentFragment里的真实dom
    el = parseTemplate(el)
  }
  if (options) {
    if (options._asComponent && !options.template) {
      options.template = '<slot></slot>'
    }
    if (options.template) {
      // 将el的内容(子元素和文本节点)抽取出来
      options._content = extractContent(el)
      // 使用options.template 将虚拟节点转化为真实html, <hello></hello> => <div><h1>hello {{ msg }}</h1></div>
      // 但不包括未绑定数据, 则上面转化为 => <div><h1>hello</h1></div>
      el = transcludeTemplate(el, options)
    }
  }
  // isFragment: node is a DocumentFragment
  // 使用nodeType 为 11 进行判断是非为文档片段
  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
}

首先先看如下代码:

if (options) {
    // 把el(虚拟节点,如<hello></hello>)元素上的所有attributes抽取出来存放在了选项对象的_containerAttrs属性上
    // 使用el.attributes 方法获取el上面,并使用toArray方法,将类数组转换为真实数组
    options._containerAttrs = extractAttrs(el)
  }

而extractAttrs方法如下,其主要根据元素nodeType去判断是否为元素节点,如果为元素节点,且元素有相关属性,则将属性值取出之后,再转为属性数组;最后将属性数组放到options._containerAttrs中,为什么要这么做呢?因为现在的el可能不是真实的元素,而是诸如<hello class="test"></hello>,在后面编译过程,需要将其替换为真实的html节点,所以,它上面的属性值都会先取出来预存起来,后面合并到真实html根节点的属性上面;

function extractAttrs (el) {
  // 只查找元素节点及有属性
  if (el.nodeType === 1 && el.hasAttributes()) {
    // attributes 属性返回指定节点的属性集合,即 NamedNodeMap, 类数组
    return toArray(el.attributes)
  }
}

下一句,根据元素nodeName是否为“template”去判断是否为<template></template>元素;如果是,则走parseTemplate(el)方法,并覆盖当前el对象

if (isTemplate(el)) {
    // 得到一段存放在documentFragment里的真实dom
    el = parseTemplate(el)
  }

function isTemplate (el) {
  return el.tagName &&
    el.tagName.toLowerCase() === 'template'
}

parseTemplate则主要是将传入内容生成一段存放在documentFragment里的真实dom;进入函数,首先判断传入是否已经是一个文档片段,如果已经是,则直接返回;否则,判断传入是否为字符串,如果为字符串, 先判断是否是"#test"这种选择器类型,如果是,通过document.getElementById方法取出元素,如果文档中有此元素,将通过nodeToFragment方式,将其放入一个新的节点片段中并赋给frag,最后返回到外面;如果不是选择器类型字符串,则使用stringToFragment将其生成一个新的节点片段,并返回;如果传入非字符串而是节点(不管是什么节点,可以是元素节点、文本节点、甚至Comment节点等);则直接通过nodeToFragment生成节点片段并返回;

export function parseTemplate (template, shouldClone, raw) {
  var node, frag

  // if the template is already a document fragment,
  // do nothing
  // 是否为文档片段, nodetype是否为11
  // https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment
 // 判断传入是否已经是一个文档片段,如果已经是,则直接返回
  if (isFragment(template)) {
    trimNode(template)
    return shouldClone
      ? cloneNode(template)
      : template
  }
  // 判断传入是否为字符串
  if (typeof template === 'string') {
    // id selector
    if (!raw && template.charAt(0) === '#') {
      // id selector can be cached too
      frag = idSelectorCache.get(template)
      if (!frag) {
        node = document.getElementById(template.slice(1))
        if (node) {
          frag = nodeToFragment(node)
          // save selector to cache
          idSelectorCache.put(template, frag)
        }
      }
    } else {
      // normal string template
      frag = stringToFragment(template, raw)
    }
  } else if (template.nodeType) {
    // a direct node
    frag = nodeToFragment(template)
  }

  return frag && shouldClone
    ? cloneNode(frag)
    : frag
}

从上面可见,在parseTemplate里面最重要的是nodeToFragmentstringToFragment;那么,它们又是如何将传入内容转化为新的文档片段呢?首先看nodeToFragment

function nodeToFragment (node) {
  // if its a template tag and the browser supports it,
  // its content is already a document fragment. However, iOS Safari has
  // bug when using directly cloned template content with touch
  // events and can cause crashes when the nodes are removed from DOM, so we
  // have to treat template elements as string templates. (#2805)
  /* istanbul ignore if */
  // 是template元素或者documentFragment,使用stringToFragment转化并保存节点内容
  if (isRealTemplate(node)) {
    return stringToFragment(node.innerHTML)
  }
  // script template
  if (node.tagName === 'SCRIPT') {
    return stringToFragment(node.textContent)
  }
  // normal node, clone it to avoid mutating the original
  var clonedNode = cloneNode(node)
  var frag = document.createDocumentFragment()
  var child
  /* eslint-disable no-cond-assign */
  while (child = clonedNode.firstChild) {
  /* eslint-enable no-cond-assign */
    frag.appendChild(child)
  }
  trimNode(frag)
  return frag
}

其实看源码,很容易理解,首先判断传入内容是否为template元素或者documentFragment或者script标签,如果是,都直接走stringToFragment;后面就是先使用document.createDocumentFragment创建一个文档片段,然后将节点进行循环appendChild到创建的文档片段中,并返回新的片段;
那么,stringToFragment呢?这个就相对复杂一点了,如下:

function stringToFragment (templateString, raw) {
  // try a cache hit first
  var cacheKey = raw
    ? templateString
    : templateString.trim() //trim() 方法会从一个字符串的两端删除空白字符
  var hit = templateCache.get(cacheKey)
  if (hit) {
    return hit
  }
  // 创建一个文档片段
  var frag = document.createDocumentFragment()
  // tagRE: /<([\w:-]+)/
  // 匹配标签
  // '<test v-if="ok"></test>'.match(/<([\w:-]+)/) => ["<test", "test", index: 0, input: "<test v-if="ok"></test>"]
  var tagMatch = templateString.match(tagRE)
  // entityRE: /&#?\w+?;/
  var entityMatch = entityRE.test(templateString)
  // commentRE: /<!--/ 
  // 匹配注释
  var commentMatch = commentRE.test(templateString) 

  if (!tagMatch && !entityMatch && !commentMatch) {
    // text only, return a single text node.
    // 如果都没匹配到,创建一个文本节点添加到文档片段
    frag.appendChild(
      document.createTextNode(templateString)
    )
  } else {
    var tag = tagMatch && tagMatch[1]
    // map, 对标签进行修正;如是td标签,则返回"<table><tbody><tr>" + templateString +  "</tr></tbody></table>";
    // map['td'] = [3, "<table><tbody><tr>", "</tr></tbody></table>"]
    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

    while (depth--) {
      node = node.lastChild
    }

    var child
    document.body.appendChild(node);
    /* eslint-disable no-cond-assign */
    while (child = node.firstChild) {
    /* eslint-enable no-cond-assign */
      frag.appendChild(child)
    }
  }
  if (!raw) {
    // 移除文档中空文本节点及注释节点
    trimNode(frag)
  }
  templateCache.put(cacheKey, frag)
  return frag
}

首先去缓存查看是否已经有,如果有,则直接取缓存数据,减少程序运行;而后,通过正则判断是否为元素文本,如果不是,则说明为正常的文字文本,直接创建文本节点,并放入新建的DocumentFragment中再放入缓存中,并返回最终生成的DocumentFragment;如果是节点文本,则首先对文本进行修正;比如如果传入的是<td></td>则需要在其外层添加tr、tbody、table后才能直接使用appendChild将节点添加到文档碎片中,而无法直接添加td元素到div元素中;在最后返回一个DocumentFragment;

以上就是parseTemplate及其里面nodeToFragment、stringToFragment的具体实现;然后我们继续回到transclude;

在transclude后续中,重要就是transcludeTemplate方法,其主要就是通过此函数,根据option.template将自定义标签转化为真实内容的元素节点;如<hello></hello>这个自定义标签,会根据此标签里面真实元素而转化为真实的dom结构;

// app.vue
<hello></hello>

// template: 
<div class="hello" _v-0480c730="">
  <h1 _v-0480c730="">hello {{ msg }} welcome here</h1>
  <h3 v-if="show" _v-0480c730="">this is v-if</h3>
</div>

函数首先会通过上述parseTemplate方法将模版数据转化为一个临时的DocumentFragment,然后根据是否将根元素进行替换,即option.replace是否为true进行对应处理,而如果需要替换,主要进行将替换元素上的属性值和模版根元素属性值进行合并,也就是将替换元素上面的属性合并并添加到根节点上面,如果两个上面都有此属性,则进行合并后的作为最终此属性值,如果模板根元素上没有此属性而自定义元素上有,则将其设置到根元素上,即:

options._replacerAttrs = extractAttrs(replacer)
        mergeAttrs(el, replacer)

所以,综上,在compile中,el = transclude(el, options)主要是对元素进行处理,将一个简单的自定义标签根据它对应的template模板数据和option的一些配置,进行整合处理,最后返回整理后的元素数据;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions