Heim > Web-Frontend > js-Tutorial > Hauptteil

So lösen Sie das Problem der Vorlagenkompilierung in Vue.js

小云云
Freigeben: 2018-01-26 11:57:31
Original
2032 Leute haben es durchsucht

In diesem Artikel wird hauptsächlich das Problem der Vorlagenkompilierung in Vue.js vorgestellt. Als Referenz für alle hoffe ich, dass Sie nach dem Lesen dieses Artikels eine klare Lösung für das Problem der Vorlagenkompilierung in Vue.js haben.

Vorab geschrieben

Da ich mich sehr für Vue.js interessiere und der Technologie-Stack, an dem ich normalerweise arbeite, auch Vue.js ist, habe ich einige Zeit damit verbracht, Vue.js zu studieren und zu lernen in den letzten Monaten erstellt, zusammengefasst und ausgegeben.

Ursprüngliche Adresse des Artikels: https://github.com/answershuto/learnVue.

Während des Lernprozesses wurden chinesische Kommentare zu Vue https://github.com/answershuto/learnVue/tree/master/vue-src hinzugefügt. Ich hoffe, dass es anderen Freunden helfen kann, die Vue-Quelle lernen möchten Code hilft.

Es kann zu Abweichungen im Verständnis kommen. Sie können gerne Probleme ansprechen und darauf hinweisen, um gemeinsam zu lernen und Fortschritte zu erzielen.

$mount

Schauen Sie sich zuerst den Mount-Code an

/*把原本不带编译的$mount方法保存下来,在最后会调用。*/
const mount = Vue.prototype.$mount
/*挂载组件,带模板编译*/
Vue.prototype.$mount = function (
 el?: string | Element,
 hydrating?: boolean
): Component {
 el = el && query(el)

 /* istanbul ignore if */
 if (el === document.body || el === document.documentElement) {
  process.env.NODE_ENV !== 'production' && warn(
   `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
  )
  return this
 }

 const options = this.$options
 // resolve template/el and convert to render function
 /*处理模板templete,编译成render函数,render不存在的时候才会编译template,否则优先使用render*/
 if (!options.render) {
  let template = options.template
  /*template存在的时候取template,不存在的时候取el的outerHTML*/
  if (template) {
   /*当template是字符串的时候*/
   if (typeof template === 'string') {
    if (template.charAt(0) === '#') {
     template = idToTemplate(template)
     /* istanbul ignore if */
     if (process.env.NODE_ENV !== 'production' && !template) {
      warn(
       `Template element not found or is empty: ${options.template}`,
       this
      )
     }
    }
   } else if (template.nodeType) {
    /*当template为DOM节点的时候*/
    template = template.innerHTML
   } else {
    /*报错*/
    if (process.env.NODE_ENV !== 'production') {
     warn('invalid template option:' + template, this)
    }
    return this
   }
  } else if (el) {
   /*获取element的outerHTML*/
   template = getOuterHTML(el)
  }
  if (template) {
   /* istanbul ignore if */
   if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    mark('compile')
   }

   /*将template编译成render函数,这里会有render以及staticRenderFns两个返回,这是vue的编译时优化,static静态不需要在VNode更新时进行patch,优化性能*/
   const { render, staticRenderFns } = compileToFunctions(template, {
    shouldDecodeNewlines,
    delimiters: options.delimiters
   }, this)
   options.render = render
   options.staticRenderFns = staticRenderFns

   /* istanbul ignore if */
   if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    mark('compile end')
    measure(`${this._name} compile`, 'compile', 'compile end')
   }
  }
 }
 /*Github:https://github.com/answershuto*/
 /*调用const mount = Vue.prototype.$mount保存下来的不带编译的mount*/
 return mount.call(this, el, hydrating)
}
Nach dem Login kopieren

Durch den Mount-Code können wir während des Mount-Vorgangs erkennen, ob die Renderfunktion nicht vorhanden ist ( render (Wenn die Funktion vorhanden ist, wird render zuerst verwendet.) Die Vorlage wird kompiliertToFunctions, um render und staticRenderFns abzurufen. Wenn beispielsweise einer handgeschriebenen Komponente eine Vorlage hinzugefügt wird, wird diese zur Laufzeit kompiliert. Die Renderfunktion gibt den VNode-Knoten zurück, nachdem sie zum Seitenrendering und Patchen während der Aktualisierung ausgeführt wurde. Schauen wir uns als Nächstes an, wie die Vorlage kompiliert wird.

Einige Grundlagen

Zuerst wird die Vorlage in einen AST-Syntaxbaum kompiliert. Was ist also AST?

In der Informatik ist ein abstrakter Syntaxbaum (oder abgekürzt als AST) oder Syntaxbaum (Syntaxbaum) eine baumartige Darstellung der abstrakten Syntaxstruktur von Quellcode, insbesondere einer Programmiersprache Code.

AST erhält die Renderfunktion durch Generieren. VNode ist der virtuelle DOM-Knoten von Vue. Die spezifische Definition lautet wie folgt:

export default class VNode {
 tag: string | void;
 data: VNodeData | void;
 children: ?Array<VNode>;
 text: string | void;
 elm: Node | void;
 ns: string | void;
 context: Component | void; // rendered in this component's scope
 functionalContext: Component | void; // only for functional component root nodes
 key: string | number | void;
 componentOptions: VNodeComponentOptions | void;
 componentInstance: Component | void; // component instance
 parent: VNode | void; // component placeholder node
 raw: boolean; // contains raw HTML? (server only)
 isStatic: boolean; // hoisted static node
 isRootInsert: boolean; // necessary for enter transition check
 isComment: boolean; // empty comment placeholder?
 isCloned: boolean; // is a cloned node?
 isOnce: boolean; // is a v-once node?
 /*Github:https://github.com/answershuto*/
 
 constructor (
  tag?: string,
  data?: VNodeData,
  children?: ?Array<VNode>,
  text?: string,
  elm?: Node,
  context?: Component,
  componentOptions?: VNodeComponentOptions
 ) {
  /*当前节点的标签名*/
  this.tag = tag
  /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
  this.data = data
  /*当前节点的子节点,是一个数组*/
  this.children = children
  /*当前节点的文本*/
  this.text = text
  /*当前虚拟节点对应的真实dom节点*/
  this.elm = elm
  /*当前节点的名字空间*/
  this.ns = undefined
  /*编译作用域*/
  this.context = context
  /*函数化组件作用域*/
  this.functionalContext = undefined
  /*节点的key属性,被当作节点的标志,用以优化*/
  this.key = data && data.key
  /*组件的option选项*/
  this.componentOptions = componentOptions
  /*当前节点对应的组件的实例*/
  this.componentInstance = undefined
  /*当前节点的父节点*/
  this.parent = undefined
  /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
  this.raw = false
  /*静态节点标志*/
  this.isStatic = false
  /*是否作为跟节点插入*/
  this.isRootInsert = true
  /*是否为注释节点*/
  this.isComment = false
  /*是否为克隆节点*/
  this.isCloned = false
  /*是否有v-once指令*/
  this.isOnce = false
 }

 // DEPRECATED: alias for componentInstance for backwards compat.
 /* istanbul ignore next */
 get child (): Component | void {
  return this.componentInstance
 }
}
Nach dem Login kopieren

Für einige Einzelheiten zu VNode finden Sie unter VNode-Knoten.

createCompiler

createCompiler wird zum Erstellen eines Compilers verwendet, und der Rückgabewert ist „compile“ und „compileToFunctions“. Compile ist ein Compiler, der die eingehende Vorlage in den entsprechenden AST-Baum, die Renderfunktion und die staticRenderFns-Funktion konvertiert. compileToFunctions ist ein zwischengespeicherter Compiler und staticRenderFns und Renderfunktionen werden in Funktionsobjekte konvertiert.

Da verschiedene Plattformen unterschiedliche Optionen haben, übergibt createCompiler je nach Plattform eine Basisoption und wird mit den von der Kompilierung selbst übergebenen Optionen zusammengeführt, um die endgültigen endgültigen Optionen zu erhalten.

compileToFunctions

Lassen Sie uns zunächst den Code von compileToFunctions veröffentlichen.

 /*带缓存的编译器,同时staticRenderFns以及render函数会被转换成Funtion对象*/
 function compileToFunctions (
  template: string,
  options?: CompilerOptions,
  vm?: Component
 ): CompiledFunctionResult {
  options = options || {}

  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production') {
   // detect possible CSP restriction
   try {
    new Function('return 1')
   } catch (e) {
    if (e.toString().match(/unsafe-eval|CSP/)) {
     warn(
      'It seems you are using the standalone build of Vue.js in an ' +
      'environment with Content Security Policy that prohibits unsafe-eval. ' +
      'The template compiler cannot work in this environment. Consider ' +
      'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
      'templates into render functions.'
     )
    }
   }
  }
  /*Github:https://github.com/answershuto*/
  // check cache
  /*有缓存的时候直接取出缓存中的结果即可*/
  const key = options.delimiters
   ? String(options.delimiters) + template
   : template
  if (functionCompileCache[key]) {
   return functionCompileCache[key]
  }

  // compile
  /*编译*/
  const compiled = compile(template, options)

  // check compilation errors/tips
  if (process.env.NODE_ENV !== 'production') {
   if (compiled.errors && compiled.errors.length) {
    warn(
     `Error compiling template:\n\n${template}\n\n` +
     compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
     vm
    )
   }
   if (compiled.tips && compiled.tips.length) {
    compiled.tips.forEach(msg => tip(msg, vm))
   }
  }

  // turn code into functions
  const res = {}
  const fnGenErrors = []
  /*将render转换成Funtion对象*/
  res.render = makeFunction(compiled.render, fnGenErrors)
  /*将staticRenderFns全部转化成Funtion对象 */
  const l = compiled.staticRenderFns.length
  res.staticRenderFns = new Array(l)
  for (let i = 0; i < l; i++) {
   res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors)
  }

  // check function generation errors.
  // this should only happen if there is a bug in the compiler itself.
  // mostly for codegen development use
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== &#39;production&#39;) {
   if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
    warn(
     `Failed to generate render function:\n\n` +
     fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n$[code]\n`).join('\n'),
     vm
    )
   }
  }

  /*存放在缓存中,以免每次都重新编译*/
  return (functionCompileCache[key] = res) 
 }
Nach dem Login kopieren

Wir können feststellen, dass es im Abschluss ein functionCompileCache-Objekt als Cache gibt.

 /*作为缓存,防止每次都重新编译*/
 const functionCompileCache: {
  [key: string]: CompiledFunctionResult;
 } = Object.create(null)
Nach dem Login kopieren

Nach der Eingabe von „compileToFunctions“ wird zunächst geprüft, ob kompilierte Ergebnisse im Cache vorhanden sind. Wenn Ergebnisse vorhanden sind, werden diese direkt aus dem Cache gelesen. Dies verhindert, dass jedes Mal dieselbe Vorlage wiederholt kompiliert werden muss.

  // check cache
  /*有缓存的时候直接取出缓存中的结果即可*/
  const key = options.delimiters
   ? String(options.delimiters) + template
   : template
  if (functionCompileCache[key]) {
   return functionCompileCache[key]
  }
Nach dem Login kopieren

Die Kompilierungsergebnisse werden am Ende von „compileToFunctions“ zwischengespeichert

 /*存放在缓存中,以免每次都重新编译*/
 return (functionCompileCache[key] = res)
Nach dem Login kopieren

Kompilieren

 /*编译,将模板template编译成AST树、render函数以及staticRenderFns函数*/
 function compile (
  template: string,
  options?: CompilerOptions
 ): CompiledResult {
  const finalOptions = Object.create(baseOptions)
  const errors = []
  const tips = []
  finalOptions.warn = (msg, tip) => {
   (tip ? tips : errors).push(msg)
  }

  /*做下面这些merge的目的因为不同平台可以提供自己本身平台的一个baseOptions,内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层compiler中,所以在这里需要merge一下*/
  if (options) {
   // merge custom modules
   /*合并modules*/
   if (options.modules) {
    finalOptions.modules = (baseOptions.modules || []).concat(options.modules)
   }
   // merge custom directives
   if (options.directives) {
    /*合并directives*/
    finalOptions.directives = extend(
     Object.create(baseOptions.directives),
     options.directives
    )
   }
   // copy other options
   for (const key in options) {
    /*合并其余的options,modules与directives已经在上面做了特殊处理了*/
    if (key !== 'modules' && key !== 'directives') {
     finalOptions[key] = options[key]
    }
   }
  }

  /*基础模板编译,得到编译结果*/
  const compiled = baseCompile(template, finalOptions)
  if (process.env.NODE_ENV !== 'production') {
   errors.push.apply(errors, detectErrors(compiled.ast))
  }
  compiled.errors = errors
  compiled.tips = tips
  return compiled
 }
Nach dem Login kopieren

Kompilieren macht hauptsächlich zwei Dinge, eines ist die Zusammenführungsoption ( Wie bereits erwähnt, wird die eigene Option der Plattform mit der eingehenden Option zusammengeführt, und die andere ist baseCompile, die die Vorlage kompiliert.

Werfen wir einen Blick auf baseCompile

baseCompile

function baseCompile (
 template: string,
 options: CompilerOptions
): CompiledResult {
 /*parse解析得到ast树*/
 const ast = parse(template.trim(), options)
 /*
  将AST树进行优化
  优化的目标:生成模板AST树,检测不需要进行DOM改变的静态子树。
  一旦检测到这些静态树,我们就能做以下这些事情:
  1.把它们变成常数,这样我们就再也不需要每次重新渲染时创建新的节点了。
  2.在patch的过程中直接跳过。
 */
 optimize(ast, options)
 /*根据ast树生成所需的code(内部包含render与staticRenderFns)*/
 const code = generate(ast, options)
 return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
 }
}
Nach dem Login kopieren

baseCompile analysiert zuerst die Vorlage, um einen AST-Syntaxbaum zu erhalten, führt dann einige Optimierungen durch Optimieren durch und schließlich generieren Render und staticRenderFns abrufen.

parse

Der Quellcode von parse finden Sie unter https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js# L53.

parse verwendet reguläre Methoden, um die Anweisungen, Klassen, Stile und andere Daten in der Vorlage zu analysieren, um einen AST-Syntaxbaum zu bilden.

Optimieren

Die Hauptfunktion von Optimieren besteht darin, statische Knoten zu markieren. Dies ist eine Optimierung von Vue während des Kompilierungsprozesses. Später wird ein Patch-Prozess durchgeführt , diff Der Algorithmus überspringt statische Knoten direkt, wodurch der Vergleichsprozess reduziert und die Patch-Leistung optimiert wird.

Generieren

Generieren ist der Prozess der Konvertierung des AST-Syntaxbaums in einen Render-Funktions-String. Das Ergebnis ist ein Render-String und ein staticRenderFns-String.

Zu diesem Zeitpunkt wurde unsere Vorlage in den AST-Syntaxbaum, die Render-Funktionszeichenfolge und die staticRenderFns-Zeichenfolge konvertiert, die wir benötigen.

Zum Beispiel

Sehen Sie sich das Kompilierungsergebnis dieses Codes an

<p class="main" :class="bindClass">
  <p>{{text}}</p>
  <p>hello world</p>
  <p v-for="(item, index) in arr">
    <p>{{item.name}}</p>
    <p>{{item.value}}</p>
    <p>{{index}}</p>
    <p>---</p>
  </p>
  <p v-if="text">
    {{text}}
  </p>
  <p v-else></p>
</p>
Nach dem Login kopieren

Nach der Konvertierung wird der AST-Baum erhalten, wie unten gezeigt:


Wir können sehen, dass der äußerste p der Wurzelknoten dieses AST-Baums ist. Es gibt viele Daten auf dem Knoten, die die Form des Knotens darstellen ob es sich um einen statischen Knoten handelt, und staticClass gibt ein statisches Klassenattribut an (nicht bind:class). kinder stellt die untergeordneten Knoten des Knotens dar. Sie können sehen, dass „kinder“ ein Array mit einer Länge von 4 ist, das die vier p untergeordneten Knoten unter dem Knoten enthält. Die Knoten in untergeordneten Knoten haben eine ähnliche Struktur wie der übergeordnete Knoten und bilden Schicht für Schicht einen AST-Syntaxbaum.

Sehen wir uns die von AST erhaltene Renderfunktion an

with(this){
  return _c( 'p',
        {
          /*static class*/
          staticClass:"main",
          /*bind class*/
          class:bindClass
        },
        [
          _c( 'p', [_v(_s(text))]),
          _c('p',[_v("hello world")]),
          /*这是一个v-for循环*/
          _l(
            (arr),
            function(item,index){
              return _c( 'p',
                    [_c('p',[_v(_s(item.name))]),
                    _c('p',[_v(_s(item.value))]),
                    _c('p',[_v(_s(index))]),
                    _c('p',[_v("---")])]
                  )
            }
          ),
          /*这是v-if*/
          (text)?_c('p',[_v(_s(text))]):_c('p',[_v("no text")])],
          2
      )
}
Nach dem Login kopieren

_c, _v, _s, _q

Beim Betrachten der Renderfunktionszeichenfolge haben wir festgestellt, dass es eine gibt viele _c , _v, _s, _q, was genau sind diese Funktionen?

带着问题,我们来看一下core/instance/render。

/*处理v-once的渲染函数*/
 Vue.prototype._o = markOnce
 /*将字符串转化为数字,如果转换失败会返回原字符串*/
 Vue.prototype._n = toNumber
 /*将val转化成字符串*/
 Vue.prototype._s = toString
 /*处理v-for列表渲染*/
 Vue.prototype._l = renderList
 /*处理slot的渲染*/
 Vue.prototype._t = renderSlot
 /*检测两个变量是否相等*/
 Vue.prototype._q = looseEqual
 /*检测arr数组中是否包含与val变量相等的项*/
 Vue.prototype._i = looseIndexOf
 /*处理static树的渲染*/
 Vue.prototype._m = renderStatic
 /*处理filters*/
 Vue.prototype._f = resolveFilter
 /*从config配置中检查eventKeyCode是否存在*/
 Vue.prototype._k = checkKeyCodes
 /*合并v-bind指令到VNode中*/
 Vue.prototype._b = bindObjectProps
 /*创建一个文本节点*/
 Vue.prototype._v = createTextVNode
 /*创建一个空VNode节点*/
 Vue.prototype._e = createEmptyVNode
 /*处理ScopedSlots*/
 Vue.prototype._u = resolveScopedSlots

 /*创建VNode节点*/
 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
Nach dem Login kopieren

通过这些函数,render函数最后会返回一个VNode节点,在_update的时候,经过patch与之前的VNode节点进行比较,得出差异后将这些差异渲染到真实的DOM上。

相关推荐:

微信小程序template模板详解

微信小程序的template模板如何使用

HTML5中