模板编译

模板:写在标签中的类似于原生HTML的内容

模板编译是用模板生成一个render函数

render函数就可以生成与模板对应的VNode,之后再进行patch算法,最后完成视图渲染。

分三个阶段:

功能 源码路径
模板解析 将一堆模板字符串用正则等方式解析成抽象语法树AST 解析器 src/compiler/parser/index.js
优化 遍历AST,找出其中的静态节点,并打上标记 优化器 src/compiler/optimizer.js
代码生成 将AST转换成渲染函数 代码生成器 src/compiler/codegen/index.js

模板解析

在模板内,除了有常规的HTML标签外,还有一些文本信息以及在文本信息中包含过滤器。
不同的内容需要不同的解析规则,对应不同解析器。常规HTML的parseHTML,文本parseText、过滤器解析器parseFilters(解析文本中如果包含过滤器)。

HTML解析器是主线,先用HTML解析器进行解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。

总之:一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点,最终完成将整个模板字符串转化成AST。解析器内维护了一个栈,用来保证构建的AST节点层级与真正DOM层级一致。

parseHTML

// 代码位置:/src/complier/parser/index.js

/**
 * Convert HTML string to AST.
 * 将HTML模板字符串转化为AST
 */
export function parse(template, options) {
     
   // ...
  parseHTML(template, {
     
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    // 当解析到开始标签时,调用该函数
    start (tag, attrs, unary) {
     
     //生成元素类型的AST节点
    },
    // 当解析到结束标签时,调用该函数
    end () {
     

    },
    // 当解析到文本时,调用该函数
    chars (text) {
     
	//生成文本类型的AST节点
    },
    // 当解析到注释时,调用该函数
    comment (text) {
     
	//生成评论类型的AST节点
    }
  })
  return root
}

调用parseHTML函数时为其传入的两个参数分别是:
template:待转换的模板字符串;
options:转换时所需的选项,提供了一些解析HTML模板时的一些参数,同时还把这4个钩子函数作为参数传给解析器parseHTML,当解析器解析出不同的内容时调用不同的钩子函数从而生成不同的AST。

例如start

// 当解析到标签的开始位置时,触发start
start (tag, attrs, unary) {
     
	let element = createASTElement(tag, attrs, currentParent)
}

export function createASTElement (tag,attrs,parent) {
     
  return {
     
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent,
    children: []
  }
}

完整的

function parseHTML(html, options) {
     
	const stack = []       // 维护AST节点层级的栈
	const expectHTML = options.expectHTML
	const isUnaryTag = options.isUnaryTag || no
	const canBeLeftOpenTag = options.canBeLeftOpenTag || no   //用来检测一个标签是否是可以省略闭合标签的非自闭合标签
	let index = 0   //解析游标,标识当前从何处开始解析模板字符串
	let last,   // 存储剩余还未解析的模板字符串
	    lastTag  // 存储着位于 stack 栈顶的元素

	// 开启一个 while 循环,循环结束的条件是 html 为空,即 html 被 parse 完毕
	while (html) {
     
		last = html;
		// 确保即将 parse 的内容不是在纯文本标签里 (script,style,textarea)
		if (!lastTag || !isPlainTextElement(lastTag)) {
     
		   let textEnd = html.indexOf('<')
              /**
               * 如果html字符串是以'<'开头,则有以下几种可能
               * 开始标签:
* 结束标签:
* 注释: * 条件注释: * DOCTYPE: * 需要一一去匹配尝试 */
if (textEnd === 0) { // 解析是否是注释 if (comment.test(html)) { } // 解析是否是条件注释 if (conditionalComment.test(html)) { } // 解析是否是DOCTYPE const doctypeMatch = html.match(doctype) if (doctypeMatch) { } // 解析是否是结束标签 const endTagMatch = html.match(endTag) if (endTagMatch) { } // 匹配是否是开始标签 const startTagMatch = parseStartTag() if (startTagMatch) { } } // 如果html字符串不是以'<'开头,则解析文本类型 let text, rest, next if (textEnd >= 0) { } // 如果在html字符串中没有找到'<',表示这一段html字符串都是纯文本 if (textEnd < 0) { text = html html = '' } // 把截取出来的text转化成textAST if (options.chars && text) { options.chars(text) } } else { // 父元素为script、style、textarea时,其内部的内容全部当做纯文本处理 } //将整个字符串作为文本对待 if (html === last) { options.chars && options.chars(html); if (!stack.length && options.warn) { options.warn(("Mal-formatted tag at end of template: \"" + html + "\"")); } break } } // Clean up any remaining tags parseEndTag(); //parse 开始标签 function parseStartTag() { } //处理 parseStartTag 的结果 function handleStartTag(match) { } //parse 结束标签 function parseEndTag(tagName, start, end) { } }

textParser

判断传入的文本是否包含变量
构造expression
构造tokens

let text = "我叫{
     {name}},我今年{
     {age}}岁了"
let res = parseText(text)
res = {
     
    expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
    tokens:[
        "我叫",
        {
     '@binding': name },
        ",我今年"
        {
     '@binding': age },
    	"岁了"
    ]
}

parseText函数接收两个参数,一个是传入的待解析的文本内容text,一个包裹变量的符号delimiters。

纯文本截取出来,存入rawTokens中,同时再调用JSON.stringify给这段文本包裹上双引号,存入tokens中
用_s()包裹变量存入tokens中,同时再把变量名构造成{’@binding’: exp}存入rawTokens

如何保证AST节点层级关系

Vue在HTML解析器的开头定义了一个栈stack。HTML解析器在从前向后解析模板字符串时,每当遇到开始标签时就会调用start钩子函数,在start钩子函数内部将解析得到的开始标签推入栈中,而每当遇到结束标签时就会调用end钩子函数,在end钩子函数内部将解析得到的结束标签所对应的开始标签从栈中弹出。
标签没有被正确闭合,此时控制台就会抛出警告:‘tag has no matching end tag.'这就是栈的第二个用途: 检测模板字符串中是否有未正确闭合的标签。

优化

静态节点一旦首次渲染上了之后不管状态再怎么变化它都不会变了

在AST中找出所有静态节点并打上标记;
在AST中找出所有静态根节点并打上标记;

function isStatic (node: ASTNode): boolean {
     
  if (node.type === 2) {
      // 包含变量的动态文本节点
    return false
  }
  if (node.type === 3) {
      // 不包含变量的纯文本节点
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

静态节点的情况:

  • 节点使用了v-pre指令
  • 如果没有使用v-pre指令,那它要成为静态节点必须满足:
  1. 不能使用动态绑定语法,即标签上不能有v-、@、:开头的属性
  2. 不能使用v-if、v-else、v-for指令;
  3. 不能是内置组件,即标签名不能是slot和component;
  4. 标签名必须是平台保留标签,即不能是组件;
  5. 当前节点的父节点不能是带有 v-for 的 template 标签;
  6. 节点的所有属性的 key 都必须是静态节点才有的 key,注:静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一;

标记完当前节点是否为静态节点之后,如果该节点是元素节点,那么还要继续去递归判断它的子节点.如果当前节点的子节点有一个不是静态节点,那就把当前节点也标记为非静态节点.


静态节点的情况:

  1. 节点本身必须是静态节点;
  2. 必须拥有子节点 children;
  3. 子节点不能只是只有一个文本节点;

如果当前节点不是静态根节点,那就继续递归遍历它的子节点node.children和node.ifConditions。

render代码生成

Vue只要调用了render函数,就可以把模板转换成对应的虚拟DOM。

当用户手写了render函数时,那么Vue在挂载该组件的时候就会调用用户手写的这个render函数。
如果没有写,Vue就要自己根据模板内容生成一个render函数供组件挂载的时候调用。

Vue.prototype.$mount = function (el){
     
  const options = this.$options
  // 如果用户没有手写render函数
  if (!options.render) {
     
    // 获取模板,先尝试获取内部模板,如果获取不到则获取外部模板
    let template = options.template
    if (template) {
     

    } else {
     
      template = getOuterHTML(el)
    }
    const {
      render, staticRenderFns } = compileToFunctions(template, {
     
      shouldDecodeNewlines,
      shouldDecodeNewlinesForHref,
      delimiters: options.delimiters,
      comments: options.comments
    }, this)
    options.render = render
    options.staticRenderFns = staticRenderFns
  }
}

compileToFunctions的来历如图
模板编译_第1张图片

你可能感兴趣的:(VUE,前端系统学习,vue)