vue模版编译流程

189 阅读8分钟

什么是模版编译

平常开发中写在template标签中类似原声html的内容称之为模版,Vue模版编译会把原生元素以及非原生元素内容进行编译,经过一系列的逻辑处理生成渲染函数,也就是render函数,模版转换成render函数的过程称之为模版编译过程。

编译流程

image.png

模版编译内部过程

首先补充个概念AST抽象语法树 都知道我们在template标签中写的内容对于Vue来讲其实就是字符串,那么如何解析字符串并且从中提取标签名、自定义属性、变量插值呢?就是借助一个叫AST语法树的东西。(如下图)

image.png 从上图能看出,一个简单html转换成了一个js对象,而这个对象一些属性就对应着标签中的有效信息。

具体编译流程

将模版字符串转换成AST后,我们就可以对其进行各种操作处理了,处理完后用处理后的AST来生成render函数。其具体流程可大致分为三个阶段:

1.模版解析阶段:将模版字符串解析成AST

2.优化阶段:将AST便利,找到静态节点,打上静态标记

3.渲染函数生成阶段:将AST转换成渲染函数

源码入口如下:

image.png

可以看到核心代码就是三个阶段

  • parse解析模版字符串获取AST
  • optimize主要就是标记静态节点,优化节点diff过程之间跳过静态节点,从而减少比较过程,优化patch的性能
  • generate是将AST转换成render函数字符串的过程

image.png

模版解析阶段

parse主要就是解析html,源码中就是通过parseHTML函数解析html

image.png parseHTML传入了两个参数 参数template:其实就是模版字符串 参数options:就是转换模版时所需要用到的选项,其中最重要的就是start,end,chars,comment这四个钩子函数,就是遇到模版字符串中解析到不同内容时调用不同的钩子函数来生成AST。

源码中首先解析的是注释内容就会调用comment函数生成对应的注释类型的AST

image.png

解析到结束标签就会调用end函数 解析到开始标签就会调用start函数生成元素类型的AST节点

image.png 解析到文本的时候调用chars函数生成文本的AST节点

image.png

解析HTML注释节点

HTML的注释是以<!--开头,以-->结尾,两个标签之间的内容就是注释文本内容,解析其实就是用正则匹配这两个标签,匹配条件后获取数据,注释就被解析出来了。

image.png

其中advance函数就是用来移动解析位置的,解析完一段后游标往后移动,确保不会重复解析

image.png

解析条件注释

条件注释举个例子,例如<!-- [if !IE]> -->我是注释<!--< ![endif] -->,也和注释节点类似,匹配到节点后直接移动解析游标,还有解析DOCTYPE也和解析条件注释一致 image.png

解析开始标签

源码中就是调用parseStartTag函数,例如遇到<div就会进行正则匹配,会获取到当前开始标签的标签名,也就是match中的tagName

image.png 其中的while循环就是用正则匹配开始标签中例如<div :msg='message' 的自定义属性 image.png

至此开始标签的解析基本完毕,但是源码中并没有直接去调用start函数去创建AST,而是通过handleStartTag函数去处理parseStartTag获取到的参数,其中会去判断当前的开始标签是否是自闭合标签,如果是自闭合的标签就直接调用start函数生成AST,反之就存入stack栈中

image.png

解析结束标签

匹配到例如</div>的节点就会将标签名提取出来,游标往后移动,然后执行parseEndTag函数 image.png 匹配到结束标签,parseEndTag函数内部会从stack栈中找最近相同的一个标签节点,也就是匹配两个tagName是否一致,一致就执行end钩子函数,反之就抛出错误没有闭合标签,将该匹配到的元素弹出stack栈,至此一遍循环走完,模版字符串长度不为0继续往下执行。

举个例子: 比如模版字符串为<div><span></span></div>,当解析到div是会将<div>入栈,解析道<span>时入栈, 解析到</span>时,会将栈顶元素也就是<span>拿出来比对tagName,一致就会创建AST节点数据,并且把该标签弹出栈,依此类推完成闭环 image.png

解析文本内容

其实就是去匹配是不是属于上面几种正则的情况,如果没有一个匹配上的就认为是文本内容就去调用chars函数, 在解析模板字符串之前,我们先查找一下第一个<出现在什么位置,如果第一个<在第一个位置,那么说明模板字符串是以其它类型开始的;如果第一个<不在第一个位置而在模板字符串中间某个位置,那么说明模板字符串是以文本开头的,那么从开头到第一个<出现的位置就都是文本内容了;如果在整个模板字符串里没有找到<,那说明整个模板字符串都是文本

解析完毕

返回的AST对象如下

image.png

AST优化阶段

优化阶段其实就是做了两件事,1是标记静态节点,2是标记静态根节点,那么为什么要标记静态节点呢? 其实模版编译做的就是生成render函数,而render函数就可以生成对应的vNode,之后就是patch节点,最后就是渲染视图,patch节点会去对比新旧节点之间的差异,之所以标记静态节点就是在patch比对的过程中不用去对比这些节点从而提高patch的性能,也就是这部分优化的意义。源码如下

image.png

标记静态节点

首先来分析静态节点标记,从源码中能看出isStatic就是来标记静态节点,内部判断主要就是判断元素节点类型是不是文本类型也就是type是不是为3,是就标记static为true,如果节点类型是元素节点也就是type为1就递归执行markStatic,直到标记完所有节点。

image.png

标记静态根节点

标记静态根节点也与标记静态节点类似,都是递归寻找匹配的节点,可以看出满足静态根节点就要满足3个条件,满足条件就标记为静态根节点。

  • 本身是静态节点、
  • 必须拥有子节点 children
  • 子节点不能只是只有一个文本节点;

image.png

render函数生成阶段

render函数可以自己写,也可以根据模版生成render函数字符串

如何根据AST生成render函数字符串

根据上面转换得到的AST数据转换成render函数字符串,生成render函数其实就是一个递归的过程,依次找到不同的节点,根据不同的节点转换成对应的vnode类型。 元素类型节点会按照这个规则生成函数字符串_c(tagName,data,children)

// 元素类型节点
_c('div',{attrs:{"msg":message}},[/*子元素节点*/])
// 文本类型节点
_v('123')
// 注释节点
_e()

一层一层便利下来生成对应的数据,最终得到转换的数据为

"_c('myButoom',{attrs:{"msg":message}},[(true)?_c('div',[_v("123")]):_e()])"

// 生成的render函数字符串
"with(this){return _c('myButoom',{attrs:{\"msg\":message}},[(true)?_c('div',[_v(\"123\")]):_e()])}"

以上就是根据一个简单的节点生成的render函数字符串的一个简单的过程。

源码中细节其实就是调用genElement创建vnode,为空就创建一个空的元素型divVNode。然后将得到的结果用with(this){return ${code}}包裹返回,内部做了各种不同属性类型的处理,

image.png 此次就根据上面的3个节点类型进行一下分析,分别是元素节点、文本节点、注释节点

元素节点

其实就是按照_c(tagName,data,[children])这个规则套入对应的数据,tagName就是标签名,data就是标签属性,children代表子元素。

image.png data内部会根据各种类型数据生成对应数据类型(此处只分析attrs属性),其逻辑非常简单,就是在拼接字符串,最终data生成{attrs:{"msg":message}}数据。 image.png 最后就是children列表的生成,获取子节点列表children其实就是遍历ASTchildren属性中的元素,然后根据元素属性的不同生成不同的vnode创建函数调用字符串。

image.png genNode又会根据节点类型去分别处理,最终返回render函数字符串。 image.png

文本节点

文本节点就是调用_v('文本')函数来创建,所以生成文本节点的render函数就是生成一个_v(text)函数调用的字符串。 image.png

注释节点

注释节点就是调用_e('注释')函数来创建的,所以生成注释节点的render函数就是生成一个_e(text)函数调用的字符串。 image.png

结束

最后,通过分析源码了解了生成render函数的基本实现过程,另外附上个人梳理的思维导图,还望大佬们多多指教。感恩。。。

template模版编译.png