Skip to content

Latest commit

 

History

History
364 lines (301 loc) · 13.1 KB

compile——生成render字符串.md

File metadata and controls

364 lines (301 loc) · 13.1 KB

本文是将html字符串编译为render字符串的最后一步,对应的源码中的文件是src/compiler/codegen/index.js,打开这个文件,内心是有点儿小崩溃的,不过仔细满满读,就会发现,这个文件所要实现的功能还是比较简单的,只不过因为需要处理的不同情况太多,所以代码会很长。

本篇文章和之前类似,主要目的是带着大家过一下模板编译的流程,具体各种指令等的处理,之后我会分别单独讲解。

前置内容

由于生成的render内容与虚拟dom关系和密切,这里我先简单介绍一下我们生成的字符串中,几个方法的含义是什么。

_c 该方法对应的是createElement方法,顾名思义,它的含义是创建一个元素,它的第一个参数是要定义的元素标签名、第二个参数是元素上添加的属性,第三个参数是子元素数组,第四个参数是子元素数组进行归一化处理的级别。

_v 该方法是创建一个文本结点。

_s 是把一个值转换为字符串。

_m 是渲染静态内容,它接收的第一个参数是一个索引值,指向最终生成的staticRenderFns数组中对应的内容,第二个参数是标识元素是否包裹在for循环内。

这四个函数是我们这篇文章中生成的字符串中包含的函数,接下来我们就看看如何把ast转换为render字符串。

生成render字符串

同样我们还是通过一个例子来学习。

<div id="app">
	<a :href="url">{{message}}</a>
	<p>静态根节点<span>静态内容</span></p>
</div>
<script type="text/javascript">
	var vm = new Vue({
		el: '#app',
		data: {
			message: '博客地址',
			url: 'https://www.imliutao.com'
		}
	})
</script>

经过模板解析和静态内容标记后,最终的ast如下:

element1 = {
  type: 1,
  tag: "div",
  attrsList: [{name: "id", value: "app"}],
  attrsMap: {id: "app"},
  parent: undefined,
  children: [
    {
      type: 1,
      tag: 'a',
      attrsList: [{name: ":href", value: "url"}],
      attrs: [{name: "href", value: "url"}],
      attrsMap: {':href': url},
      parent: ,
      children: [{
        type: 2,
        expression: '_s(message)',
        text: '{{message}}',
        static: false
      }],
      plain: false,
      static: false,
      staticRoot: false,
      hasBindings: true
    },
    {
      text: " ",
      type: 3,
      static: true
    },
    {
      type: 1,
      tag: 'p',
      attrsList: [],
      attrsMap: {},
      children: [{
			text: "静态根节点",
			type: 3,
			static: true
	      },
	      {
			attrsList: [],
			attrsMap: {}
			children: [{
				text: "静态内容",
				type: 3,
				static: true
			}],
			plain: true,
			tag: "span",
			type: 1,
			static: true
		  }
      ],
      plain: true,
      static: true,
      staticInFor: false,
      staticRoot: true
    }
  ],
  plain: false,
  attrs: [{name: "id", value: "'app'"}],
  static: false,
  staticRoot: false
}

拿到ast结构,我们接着看看generate函数的实现。

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
) {
  // save previous staticRenderFns so generate calls can be nested
  const prevStaticRenderFns: Array<string> = staticRenderFns
  const currentStaticRenderFns: Array<string> = staticRenderFns = []
  const prevOnceCount = onceCount
  onceCount = 0
  currentOptions = options
  warn = options.warn || baseWarn
  transforms = pluckModuleFunction(options.modules, 'transformCode')
  dataGenFns = pluckModuleFunction(options.modules, 'genData')
  platformDirectives = options.directives || {}
  isPlatformReservedTag = options.isReservedTag || no
  const code = ast ? genElement(ast) : '_c("div")'
  staticRenderFns = prevStaticRenderFns
  onceCount = prevOnceCount
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: currentStaticRenderFns
  }
}

函数一进来,我们通过prevStaticRenderFnsprevOnceCount分别保存了之前的staticRenderFnsonceCount,这是因为我们可能有内部模板inline-template,会导致嵌套调用该方法。

之后我们重置了currentStaticRenderFnsonceCount

transformsdataGenFns类似于我们之前讲parse函数时,里面会有preTransforms等钩子,之前是操作对生成ast进行特殊处理,这里是对生成render字符串进行特殊处理。其实我们之前也说过,options.modules包含klassstyle两个模块,pluckModuleFunction是取出模块中对应的方法,组成一个数组。在这里,transforms是一个空数组,vue源码中,还没有内置需要在生成render时特殊处理的属性等。dataGenFns包含两个元素,分别是处理ast中有classstyle相关属性时,对生成render字符串进行操作,我们这里的例子没有添加样式属性,就不细说了。

platformDirectiveshtmlmodeltext三个指令的特殊操作,这里也不赘述。

isPlatformReservedTag我们在多处使用过,这里也不多说了。

接着就是我们的重头戏了,如果ast为空,code = '_c("div")',即创建一个空的div,否则执行code = genElement(ast)

code生成之后,会使staticRenderFnsonceCount重新等于之前存储的值。

最终函数返回renderstaticRenderFns组成的对象。

genElement(ast)

该函数就是把ast转换成code的地方,我们看看它是如何操作的。

function genElement (el: ASTElement): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el)
  } else if (el.once && !el.onceProcessed) {
  	...
  } else {
    // component or element
    let code
    if (el.component) {
      ...
    } else {
      const data = el.plain ? undefined : genData(el)

      const children = el.inlineTemplate ? null : genChildren(el, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < transforms.length; i++) {
      code = transforms[i](el, code)
    }
    return code
  }
}

在这里就根据不同的指令,调用了许多不同的生成方法,我们的例子中,只可能走到genStatic和最后的else中,所以这里我们只讲这两个部分内容。其它的我们还是放在之后讲解指令时分别介绍。

先来看看genStatic:

function genStatic (el: ASTElement): string {
  el.staticProcessed = true
  staticRenderFns.push(`with(this){return ${genElement(el)}}`)
  return `_m(${staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
}

这个函数很简单,给el添加了一个staticProcessed = true,然后给staticRenderFns数组中添加了一个字符串,我们发现这个字符串和generate函数返回的render字符串一毛一样,这里是对静态根节点及其子内容单独分离出来处理。

最后会返回一个包裹_m函数的字符串,函数的第一个参数就是staticRenderFns新添加内容的索引,第二个参数是标识是否在for循环中,我们的例子里是空。

genStatic中还是会调用genElement方法来递归生成render字符串。

我们再来看else块的内容,主要是这一段:

const data = el.plain ? undefined : genData(el)

const children = el.inlineTemplate ? null : genChildren(el, true)
code = `_c('${el.tag}'${
	data ? `,${data}` : '' // data
	}${
	children ? `,${children}` : '' // children
})`

如果el.plaintrue,说明该结点没有属性,所以_c第二个参数是空,否则调用genData,很明显这个函数是生成_c第二个参数给元素添加属性的地方。

function genData (el: ASTElement): string {
  let data = '{'

  ...
  if (el.attrs) {
    data += `attrs:{${genProps(el.attrs)}},`
  }
  ...
  data = data.replace(/,$/, '') + '}'
  ...
  return data
}

因为属性包含了各种指令、事件等,这里我们只保留上面的例子中我们有的attrs

首先定义了一个字符串data,以{开始,然后加上attrs:{${genProps(el.attrs)}},

genProps用于把属性链接为字符串。

function genProps (props: Array<{ name: string, value: string }>): string {
  let res = ''
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    res += `"${prop.name}":${transformSpecialNewlines(prop.value)},`
  }
  return res.slice(0, -1)
}

// #3895, #4268
function transformSpecialNewlines (text: string): string {
  return text
    .replace(/\u2028/g, '\\u2028')
    .replace(/\u2029/g, '\\u2029')
}

代码其实很简单,就是把属性的键值用:对应,多个键值对用,隔开,最终去掉最后一个无用的,transformSpecialNewlines是对几个特殊字符的处理。

其实我们最终生成的data也是大括号包裹的对象转换成的字符串。我们这里只有一个attrs属性。

转换完属性,是对children的操作,如果不是内部模板,我们就执行genChildren(el, true)

function genChildren (el: ASTElement, checkSkip?: boolean): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
        el.for &&
        el.tag !== 'template' &&
        el.tag !== 'slot') {
      return genElement(el)
    }
    const normalizationType = checkSkip ? getNormalizationType(children) : 0
    return `[${children.map(genNode).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

如果el是一个循环,且只有一个子元素时,这里直接返回genElement(el)。细心的人可能会发现genChildren就是在genElementel.for返回false,走到else才执行的,这样岂不是死循环了?这里我们只讲了符合我们上面例子的部分,源码中else内还有如下判断:

if (el.component) {
  code = genComponent(el.component, el)
}

这里简单说一下,el.component保存的是<component :is="xxx">标签上is指向的模板。本身el上可能没有相关其它指令,但el.component上可能有。genComponent也会调用genChildren

接着往下,我们这里checkSkip传入的是truegetNormalizationType(children)返回的是子元素数组需要哪种级别的“normalization”处理。

// 0: 不需要归一化的处理
// 1: 简单归一化处理
// 2: 深度归一化处理
function getNormalizationType (children: Array<ASTNode>): number {
  let res = 0
  for (let i = 0; i < children.length; i++) {
    const el: ASTNode = children[i]
    if (el.type !== 1) {
      continue
    }
    // el上有`v-for`或标签名是`template`或`slot`,或者el是if块,但块内元素有内容符合上述三个条件的
    if (needsNormalization(el) ||
        (el.ifConditions && el.ifConditions.some(c => needsNormalization(c.block)))) {
      res = 2
      break
    }
    // el是自定义组件或el是if块,但块内元素有自定义组件的
    if (maybeComponent(el) ||
        (el.ifConditions && el.ifConditions.some(c => maybeComponent(c.block)))) {
      res = 1
    }
  }
  return res
}

function needsNormalization (el: ASTElement): boolean {
  return el.for !== undefined || el.tag === 'template' || el.tag === 'slot'
}

function maybeComponent (el: ASTElement): boolean {
  return !isPlatformReservedTag(el.tag)
}

从上面的代码中,可以看出如果有元素符合res = 2的条件,则直接跳出循环,且res = 1会被res = 2覆盖。这里的“归一化”其实就是把多维的children数组转换成一维,至于1和2的区别,是两种不同的方式来进行归一化,为了使归一化消耗最少,所以不同情况使用不同的方式进行归一化,感兴趣的可以翻开源码src/core/vdom/helpers/normalize-children.js,这里有详细的注释。

最终genChildren返回的字符串中会对children依次执行getNode,并通过,相连。

function genNode (node: ASTNode): string {
  if (node.type === 1) {
    return genElement(node)
  } else {
    return genText(node)
  }
}

function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

从上面代码可以看出,如果node.type === 1,则递归调用genElement,否则调用genTextgenText中会返回包含_v函数的字符串,传入的内容是表达式或纯文本字符串。

至此,整体上把例子生成的ast转换为render字符的代码基本讲解完毕。最终返回的内容为:

code = {
	render: "with(this){return _c('div',{attrs:{"id":"app"}},[_c('a',{attrs:{"href":url}},[_v(_s(message))]),_v(" "),_m(0)])}",
	staticRenderFns: [with(this){return _c('p',[_v("静态根节点"),_c('span',[_v("静态内容")])])}]
}

如果看到生成的内容,大体上我们知道每一部分都经历了什么,说明这篇文章就没有白讲。