vue的模板编译,静态节点原理

1,270 阅读5分钟

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

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

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

分三个阶段:

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

与web 组件API的HTML内容模板(<template>)元素无关,后者是浏览器提供的模版元素。提前在页面中做标记,让浏览器自动将其解析为DOM子树,但先跳过渲染,后面在运行时使用JavaScript实例化

模板解析

  • 在template模板内,除了有常规的HTML标签外,还有一些文本信息以及在文本信息中包含过滤器。
  • 不同的内容需要不同的解析规则,对应不同解析器。parseHTML,parseText、过滤器解析器parseFilters(解析文本中包含过滤器时)。
  • HTML解析器是主线,先用HTML解析器进行解析整个模板,在解析过程中如果碰到文本内容,那就调用文本解析器来解析文本,如果碰到文本中包含过滤器那就调用过滤器解析器来解析。
  • 解析器内维护了一个栈,用来保证构建的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函数时为其传入的两个参数分别是:

  1. template:待转换的模板字符串;
  2. options:转换时所需的选项,提供了一些解析HTML模板的参数,4个不同的内容分别调用的钩子函数

例如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字符串是以'<'开头,则有以下几种可能
               * 开始标签:<div>
               * 结束标签:</div>
               * 注释:<!-- 我是注释 -->
               * 条件注释:<!-- [if !IE] --> <!-- [endif] -->
               * DOCTYPE:<!DOCTYPE html>
               * 需要一一去匹配尝试
               */
            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

parseText函数接收两个参数

  1. 待解析的文本内容text
  2. 包裹变量的符号delimiters
let text = "我叫{{name}},我今年{{age}}岁了"
let res = parseText(text)
res = {
    expression:"我叫"+_s(name)+",我今年"+_s(age)+"岁了",
    tokens:[
        "我叫",
        {'@binding': name },
        ",我今年"
        {'@binding': age },
    	"岁了"
    ]
}
  1. 纯文本截取出来,存入rawTokens中,同时再调用JSON.stringify给这段文本包裹上双引号,存入tokens中
  2. 用_s()包裹变量存入tokens中,同时再把变量名构造成{'@binding': exp}存入rawTokens

如何保证AST节点层级关系

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

优化

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

在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:跳过这个元素及其子元素的编译过程。可以用来显示原始标签。会加快编译
  • 如果没有使用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之一;

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

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的来历如图 在这里插入图片描述