Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
pf12345 opened this issue Feb 8, 2018 · 0 comments
Open

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

pf12345 opened this issue Feb 8, 2018 · 0 comments

Comments

@pf12345
Copy link
Owner

pf12345 commented Feb 8, 2018

上面说了下,在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的一些配置,进行整合处理,最后返回整理后的元素数据;

@pf12345 pf12345 changed the title Vue源码解析(二)--_compile函数之transclude Vue源码解析(二):_compile函数之transclude Feb 12, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant