Vue.js의 템플릿 컴파일 문제를 해결하는 방법

小云云
풀어 주다: 2018-01-26 11:57:31
원래의
1981명이 탐색했습니다.

이 글은 모두가 참고할 수 있도록 주로 Vue.js의 템플릿 컴파일 문제를 소개합니다. 이 글을 읽고 나면 Vue.js의 템플릿 컴파일 문제에 대한 명확한 해결책을 얻을 수 있기를 바랍니다.

앞에 작성

저는 Vue.js에 관심이 많고, 제가 주로 작업하는 기술 스택도 Vue.js이기 때문에 지난 몇 달간 시간을 내어 Vue.js 소스 코드를 연구해서 만들었습니다. 요약 및 출력.

기사 원본 주소: https://github.com/answershuto/learnVue.

학습 과정에서 Vue https://github.com/answershuto/learnVue/tree/master/vue-src에 중국어 댓글이 추가되었습니다. Vue 소스 코드를 배우고 싶은 다른 친구들에게 도움이 되길 바랍니다.

이해에 차이가 있을 수 있습니다. 문제를 제기하고 지적하여 함께 배우고 발전하는 것을 환영합니다.

$mount

먼저 마운트 코드를 살펴보세요

/*把原本不带编译的$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)
}
로그인 후 복사

마운트 코드를 통해 마운트 과정에서 렌더 기능이 없으면(렌더 기능이 있으면 렌더가 먼저 사용됩니다), 템플릿은 렌더링 및 staticRenderFns를 가져오기 위해 ToFunctions로 컴파일됩니다. 예를 들어 템플릿이 손으로 작성한 구성 요소에 추가되면 런타임에 컴파일됩니다. 렌더링 함수는 업데이트 중 페이지 렌더링 및 패치를 위해 실행된 후 VNode 노드를 반환합니다. 다음으로 템플릿이 어떻게 컴파일되는지 살펴보겠습니다.

기본 사항

먼저 템플릿이 AST 구문 트리로 컴파일되는데 AST란 무엇일까요?

컴퓨터 과학에서 추상 구문 트리(AST로 약칭) 또는 구문 트리(구문 트리)는 소스 코드의 추상 구문 구조를 트리 모양으로 표현한 것입니다. 여기서는 특히 프로그래밍 언어의 소스 코드를 나타냅니다.

AST는 generate를 통해 렌더링 기능을 가져옵니다. VNode는 Vue의 가상 DOM 노드입니다.

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
 }
}
로그인 후 복사

VNode에 대한 자세한 내용은 VNode 노드를 참조하세요. .

createCompiler

createCompiler는 컴파일러를 생성하는 데 사용되며 반환 값은 compile 및 compileToFunctions입니다. compile은 수신 템플릿을 해당 AST 트리, 렌더링 함수 및 staticRenderFns 함수로 변환하는 컴파일러입니다. compileToFunctions는 캐시된 컴파일러이며 staticRenderFns 및 렌더링 함수는 Function 객체로 변환됩니다.

플랫폼마다 옵션이 다르기 때문에 createCompiler는 플랫폼에 따라 baseOptions를 전달하고 컴파일 자체에서 전달된 옵션과 병합되어 최종 finalOptions를 얻습니다.

compileToFunctions

먼저 compileToFunctions 코드를 올려보겠습니다.

 /*带缓存的编译器,同时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) 
 }
로그인 후 복사

클로저에는 캐시로 functionCompileCache 개체가 있다는 것을 알 수 있습니다.

 /*作为缓存,防止每次都重新编译*/
 const functionCompileCache: {
  [key: string]: CompiledFunctionResult;
 } = Object.create(null)
로그인 후 복사

compileToFunctions에 들어간 후 먼저 캐시에 컴파일된 결과가 있는지 확인합니다. 결과가 있으면 캐시에서 직접 읽습니다. 이렇게 하면 매번 동일한 템플릿을 반복적으로 컴파일할 필요가 없습니다.

  // check cache
  /*有缓存的时候直接取出缓存中的结果即可*/
  const key = options.delimiters
   ? String(options.delimiters) + template
   : template
  if (functionCompileCache[key]) {
   return functionCompileCache[key]
  }
로그인 후 복사

컴파일 결과는 compileToFunctions

 /*存放在缓存中,以免每次都重新编译*/
 return (functionCompileCache[key] = res)
로그인 후 복사

compile

 /*编译,将模板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
 }
로그인 후 복사

compile의 마지막에 캐시됩니다. compile

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
 }
}
로그인 후 복사

compile은 주로 두 가지 작업을 수행합니다. 하나는 옵션을 병합하는 것입니다(앞서 언급했듯이 플랫폼 자체 옵션을 들어오는 옵션과 병합). 다른 하나는 템플릿을 컴파일하는 baseCompile입니다.

baseCompile을 살펴보겠습니다

baseCompile

<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>
로그인 후 복사

baseCompile은 먼저 템플릿을 구문 분석하여 AST 구문 트리를 가져온 다음 최적화를 통해 일부 최적화를 수행하고 마지막으로 생성을 통해 렌더링 및 staticRenderFns를 가져옵니다.

parse

parse 소스 코드는 https://github.com/answershuto/learnVue/blob/master/vue-src/compiler/parser/index.js#L53에서 찾을 수 있습니다.

parse는 일반 방법을 사용하여 템플릿의 명령, 클래스, 스타일 및 기타 데이터를 구문 분석하여 AST 구문 트리를 형성합니다.

optimize

optimize의 주요 기능은 정적 노드를 표시하는 것입니다. 이는 나중에 인터페이스를 업데이트할 때 패치 프로세스가 있으며 diff 알고리즘은 정적 노드를 직접 건너뜁니다. 비교 프로세스를 줄이고 패치 성능을 최적화합니다.

generate

generate는 AST 구문 트리를 렌더링 기능 문자열로 변환하는 프로세스입니다. 결과는 렌더링 문자열과 staticRenderFns 문자열입니다.

이 시점에서 템플릿 템플릿은 필요한 AST 구문 트리, 렌더링 함수 문자열 및 staticRenderFns 문자열로 변환되었습니다.

예를 들어

이 코드의 컴파일 결과를 살펴보겠습니다

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
      )
}
로그인 후 복사

변환 후 아래와 같이 AST 트리를 얻습니다.

가장 바깥쪽 p가 루트임을 알 수 있습니다. 이 AST 트리 노드에는 노드의 모양을 나타내는 많은 데이터가 있습니다. 예를 들어 static은 정적 노드인지 여부를 나타내고 staticClass는 정적 클래스 속성(bind:class가 아님)을 나타냅니다. children은 노드의 하위 노드를 나타냅니다. children은 노드 아래에 4개의 p 하위 노드를 포함하는 길이가 4인 배열임을 알 수 있습니다. 하위 노드는 상위 노드와 유사한 구조를 가지며 계층별로 AST 구문 트리를 형성합니다.

AST에서 얻은 렌더 함수를 살펴보겠습니다

/*处理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)
로그인 후 복사
로그인 후 복사

_c, _v, _s, _q

🎜렌더 함수 문자열을 살펴본 후 _c, _v, _s, _q가 많이 있는 것을 발견했습니다. 이 기능은 무엇입니까? 🎜

带着问题,我们来看一下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)
로그인 후 복사
로그인 후 복사

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

相关推荐:

微信小程序template模板详解

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

HTML5中