当前篇:vue源码分析【4】-parse函数
以下代码和分析过程需要结合vue.js源码查看,通过打断点逐一比对。
模板代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./../../oldVue.js"></script>
</head>
<body>
<div id="app">
<h2>开始存钱</h2>
<div>每月存 :¥{{ money }}</div>
<div>存:{{ num }}个月</div>
<div>总共存款: ¥{{ total }}</div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
<msg-tip :msginfo='msgText' :totalnum='total'></msg-tip>
</div>
<script>
debugger;
// 定义一个新组件
var a = {
props:['msginfo', 'totalnum'],
data: function () {
return {
count: 0
}
},
template: '<div>{{ msginfo }}存了¥{{ totalnum }}</div>'
}
var app = new Vue({
el: '#app',
components: { msgTip: a},
beforeCreate() { },
created() { },
beforeMount() { },
mounted: () => { },
beforeUpdate() { },
updated() { },
beforeDestroy() { },
destroyed() { },
data: function () {
return {
money: 100,
num: 12,
arryList: [{name:'子树'}],
msgText: "优秀的乃古:"
}
},
computed: {
total() {
return this.money * this.num;
}
},
watch:{
money:{
handler(newVal, oldVal){
this.msgText = newVal+this.msgText
},
deep:true,
immediate:true
}
},
methods: {
getMoreMoney() {
this.money = this.money * 2
this.arryList.unshift({name: '大树'})
}
}
})
</script>
</body>
</html>
1. 前言
本文的结构依据点,线,面来展开。
- 点即函数的作用
- 线即函数的执行流程
- 面即源码的详细解读
十分不建议直接看源码,很多函数非常长,并且链路很长,在没有对函数有大概的了解情况,大概率下,你读了一遍源码后会发现,wc 我刚看了源码了吗?可是咋记不清它们做了啥操作。因此,先看作用,再看流程,再展开看源码。
1. 执行流程
parse函数的入口有点绕,我们先梳理下执行流程:
// 执行1 (末尾的mount)
Vue.prototype.$mount = function (el,hydrating) { //执行的位置
var ref = compileToFunctions(
template,
{
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
}
// 执行2
var compileToFunctions = ref$1.compileToFunctions;
// 执行3
var ref$1 = createCompiler(baseOptions);
// 执行4
var createCompiler = createCompilerCreator(
function baseCompile(template,options) {
//最终走到这里,返回ast树
var ast = parse(template.trim(), options);
}
);
最后走到目标函数,parse。
2. ast
2-1. 基本信息
什么是ast:
它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
作用:
主要有以下应用场景:
- 编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
elint、pretiier对代码错误或风格的检查;webpack通过babel转译javascript语法;
2-2.生成过程
js 执行的第一步是读取 js 文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析( Parser )生成 AST,最后生成机器码执行。
整个解析过程主要分为以下两个步骤:
- 分词:将整个代码字符串分割成最小语法单元数组
- 语法分析:在分词基础上建立分析语法单元之间的关系
JS Parser 是 js 语法解析器,它可以将 js 源码转成 AST,常见的 Parser 有 esprima、traceur、acorn、shift 等。
词法分析:
词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元:
例如 var 这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。
词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。
最终,整个代码将被分割进一个tokens列表(或者说一维数组)。
语法分析
语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
2-3. ast结构
为了直接的了解到parse做了什么,我们先看一下它返回ast的结构,以下是一个input标签的ast树:
AST的每层的element,包含自身节点的信息(tag,attr等),同时parent,children分别指向其父element和子element,层层嵌套,形成一棵树。
{
attrs: [{name: "id", value: "\"app\""}],
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
children: [ // 子节点(包括注释节点和空节点)
{
type: 1, tag: "h2", attrsList: Array(0), attrsMap: {…}, parent: {…},
children:[
{type: 3, text: "开始存钱", static: true}
]
},
{type: 3, text: " ", static: true}, // 空节点
{
type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, parent: {…},
children:[
{
type: 2,
expression: "\"每月存 :¥\"+_s(money)", // 渲染的表达式
tokens: ["每月存 :¥", {@binding: "money"}], // 将静态文本中的变量字段进行拆分
text: "每月存 :¥{{ money }}", // 静态文本
static: false //非静态
}
]
},
{type: 3, text: " ", static: true},
{type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
{type: 3, text: " ", static: true},
{type: 1, tag: "div", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
{type: 3, text: " ", static: true},
{
type: 1,
tag: "button",
attrsList: Array(1),
attrsMap: {@click: "getMoreMoney"},
parent: {…},
events: {click: {value: "getMoreMoney"}} //事件列表
},
{type: 3, text: " ", static: true},
{
plain: false,
type: 1,
tag: ""msg-tip"",
static: false,
staticRoot: false
attrs: [{name: "msginfo", value: "msgText"}, {name: "totalnum", value: "total"}],
attrsList: [{name: ":msginfo", value: "msgText"},{name: ":totalnum", value: "total"}],
attrsMap: {:msginfo: "msgText", :totalnum: "total"},
parent: { //父节点
attrs: [{…}],
attrsList: [{…}],
attrsMap: {id: "app"},
children: (11) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}],
parent: undefined,
plain: false,
static: false,
staticRoot: false,
tag: "div",
type: 1
},
children:[],
hasBindings: true
},
],
parent: undefined,
plain: false,
static: false,
staticRoot: false,
tag: "div",
type: 1,
__proto__: Object
}
3. parse基本信息
什么是parse
先看下官方解释:
语法分析器(parser)通常是作为编译器或解释器的组件出现的,它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树、抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。
parse函数非常的长,我们先把主要代码列一下,看下它的结构和逻辑
它主要是把template编译成Ast树。
我们先缩减代码:
function parse(
template,
options
) {
// 标签相关的判断
platformIsPreTag = options.isPreTag || no;
...
// 定义AST模型对象
var stack = [];
...
function closeElement(element) {...} //克隆节点
// 主要的解析方法
parseHTML(
template,
{
//工具函数
warn: warn$2,
...
start: function start(
tag,
attrs,
unary
) { ... },
end: function end() { ... },
chars: function chars(text) {...},//把text添加到属性节点,ast模板数据
comment: function comment(text) {...}//把text添加到注释节点,ast模板数据
}
);
return root
}
4. 详解parse
4-1. 基本信息
执行流程:
在parse函数中,先是定义一些变量,如何调用parseHTML函数对模板进行解析。 调用parse HTML函数时传递的options参数中的start函数是构建一个元素类型的AST节点并将它压入栈中,chars函数是构建一个文本类型的AST节点,comment函数是构建一个注释类型的AST节点,end是从栈中取出一个节点以便完成AST树状结构的构建。
- 定义以下几个变量:
- platformIsPreTag(函数):判断标签是否是pre 如果是则返回真
- platformMustUseProp(函数):检验标签和属性是否对应,判断这个属性是不是要放在 el.props 中
- transforms:遍历options.modules每一项,取key为'transformNode'项的value组成一个数组,不存在返回空数组, 例:transforms = [ƒ transformNode(), ƒ transformNode$1()]
- delimiters:将vue双括号的格式变成我们在template中定义的delimiters格式
- stack:标签堆栈
- preserveWhitespace:属于模板编译器的选项:让模板的元素和元素之间没有空格。
入参:
parse 函数的入参,就是template模板和配置项,例如:
template:
"<div id="app">
<h2>开始存钱</h2>
<div>每月存 :¥{{ money }}</div>
<div>存:{{ num }}个月</div>
<div>总共存款: ¥{{ total }}</div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
<msg-tip :msginfo="msgText" :totalnum="total"></msg-tip>
</div>"
options:// 第一节,执行流程分析中的入参,告诉程序如何解析ast
{
//当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。
comments: undefined,
// 改变纯文本插入分隔符。修改指令的书写风格,
// 比如默认是{{mgs}} delimiters: ['${', '}']之后变成这样 ${mgs}
delimiters: undefined,
shouldDecodeNewlines: false, // IE在属性值中编码换行,而其他浏览器则不会
shouldDecodeNewlinesForHref: false, // chrome在a[href]中编码内容
warn: ƒ (msg, tip)
}
源码:
function parse(
template, //html 模板 例:"<div id=\"app\">\n <!--this is comment--> {{ message }}\n </div>"
options //例: {shouldDecodeNewlines: false, shouldDecodeNewlinesForHref: false, delimiters: '', comments: '', warn: ƒ}
) {
warn$2 = options.warn || baseWarn; //警告日志函数,通过finalOptions.warn挂载的
//【说明1 options.isPreTag】
platformIsPreTag = options.isPreTag || no; // tag === 'pre';
platformMustUseProp = options.mustUseProp || no; // 检验标签和属性是否对应
platformGetTagNamespace = options.getTagNamespace || no; //判断 tag 是否是svg或者math 标签
debugger
// 遍历options.modules每一项,取key为'transformNode'项的value组成一个数组,不存在返回空数组
transforms = pluckModuleFunction(options.modules, 'transformNode');// modules对应的是modules$1
//定义在model$2中,model$2又挂载在modules$1,modules$1又挂载在modules
// 例:[ƒ preTransformNode()]
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
// 这个没搞清干嘛用的
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
//【说明2 delimiters】
delimiters = options.delimiters;
var stack = []; // 标签堆栈
//【说明3 preserveWhitespace】
var preserveWhitespace = options.preserveWhitespace !== false;
var root;
var currentParent; //当前父节点
var inVPre = false; //标记 标签是否还有 v-pre 指令
var inPre = false; // 判断标签是否是pre
var warned = false;
// 可忽略-----
function warnOnce(msg) {
if (!warned) {
warned = true;
warn$2(msg); //警告日志函数
}
}
//克隆节点
function closeElement(element) {
if (element.pre) {
inVPre = false;
}
if (platformIsPreTag(element.tag)) { //element.tag === 'pre';
inPre = false;
}
// postTransforms数组为空所以不执行这里
for (var i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options);
}
}
// 后一节单独解析
// 这里是执行parseHTML,传入2个参数
parseHTML(
template,
...
)
return root
}
4-2. pluckModuleFunction
作用:
遍历options.modules每一项,取key为'xxx'(例如:preTransformNode)项的value组成一个数组(例如:[preTransformNode()]),不存在返回空数组
源码:
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
/**入参例子。
modules入参:[
{staticKeys: Array(1), transformNode: ƒ, genData: ƒ},
{staticKeys: Array(1), transformNode: ƒ, genData: ƒ},
{preTransformNode: ƒ}
]
key入参:'preTransformNode',
结果:返回了包含函数的数组[preTransformNode()]
*/
function pluckModuleFunction(
modules, //数组或者对象
key //key
) {
debugger
return modules ?
modules.map(function (m) {
return m[key]; // 获取modules[key] 值
}).filter(function (_) {
return _; //过滤modules[key] 值,如果不存在则丢弃
}) : []
}
4-3. delimiters
<body>
<div id="app">
<!-- 插值符改为了es6插值形式了 -->
${num}
<p><button @click="add">ADD</button></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
let app = new Vue({
el: '#app',
data:{
num:1
},
methods:{
add(){
this.num++;
}
},
delimiters:['${','}'] //将常用{{}}格式变成${}的格式
})
</script>
</body>
4-3. preserveWhitespace
preserveWhitespace属于模板编译器的选项:让模板的元素和元素之间没有空格。
可以在vue-loader中设置:
{
vue: {
reserveWhitespace: false
}
}
5.parseHTML入参
parseHTML定义和调用的代码都非常的长,所以我们分开讲解,此篇讲解parseHTML在parse函数中调用时的2个入参。
5-1. 基本信息
parseHTML是parse里面的核心方法,我们也先看下结构,这里执行parseHTML,传入2个参数,一个是template,一个是对象,对象里面是一系列的配置和函数。
parseHTML(
template, //字符串模板 参考4-1的入参
{
warn: warn$2, // 警告函数
expectHTML: options.expectHTML, // 预期是html的标志
isUnaryTag: options.isUnaryTag, // 匹配标签是否是 'area,base,br,col,embed...'
canBeLeftOpenTag: options.canBeLeftOpenTag, //判断标签是否是 'colgroup,dd,dt,li,options,p,td...'
shouldDecodeNewlines: options.shouldDecodeNewlines, //IE在属性值中编码换行,而其他浏览器则不会
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, //chrome在a[href]中编码内容
shouldKeepComment: options.comments, //是否保留且渲染模板中的 HTML 注释
/**
这个和end相对应,主要处理开始标签和标签的属性(内置和普通属性),
等到更新后的currentParent和element。
由于开始标签里面有属性和指令,此过程也会对指令和属性进行逐步解析,
最终生成完整的elementcurrentParent。
*/
start: function start( tag, attrs, unary ) {},
//闭合元素,更新stack和currentParent
end: function end() {},
//处理文本和{{}}
chars: function chars(text) {},
//把text添加到注释节点,ast模板数据
comment: function comment(text) {
currentParent.children.push({
type: 3, //注释节点
text: text,
isComment: true
});
}
}
接下来依次对start,end,chars,comment进项详细解析
5-2. start
5-2-0. 基本信息
作用:
这个和end相对应,主要处理开始标签和标签的属性(内置和普通属性),等到更新后的currentParent和element。 由于开始标签里面有属性和指令,此过程也会对指令和属性进行逐步解析,最终生成完整的elementcurrentParent。
执行流程:
- 定义命名空间ns
- 如果当前有父节点, 如果有父节点,且有命名空间,则返回命名空间
- 否则,tag 是'svg'或者'math' 标签,则返回true,否则返回undefined
- 通过
标签,属性和父节点来创建一个符合 AST格式的对象element - 如果命名空间ns存在,则将它挂载到element上的ns属性上
- 如果element的tag是style标签 或者 是script 标签(且script 标签type属性不存在 或者script 标签的type是javascript 属性) 的时候,会触发警告:禁用带有副作用的标记
- 遍历preTransforms(一个数组,里面是一个返回包含各种input type类型的ast对象的函数),执行里面的函数preTransformNode,生成我们的ast对象(element)
- 检查节点是否有v-pre指令,并打上标记
- 如果含有 v-pre 指令,我们就把当前节点当作了静态节点,不需要再对里面的其它指令进行处理。只是简单的把虚拟dom的attrsList拷贝到attrs中。
- 前面执行preTransformNode的时候,如果有type属性会设置processed属性为true,表示经过ast转换处理
- 如果此时processed为false,表示没有type属性,这时对if,for,once等指令进行处理。
- 为什么要这么做呢?因为processed属性为true,就说明了preTransformNode函数有type属性,才会在preTransformNode直接对if,for,once等指令进行处理。所以,processed为false就表示还没有处理过这些指令,就并且在当前函数进行处理。
- 判断根节点是否存在
- 存在,直接把我们创建的ast作为根节点。
- 不存在,且标签堆栈还没有数据
- 如果根元素带有v-if,并且element有v-else-if或v-else, 检查element根约束 根节点不能是slot或者template标签,并且不能含有v-for 属性。然后将element和elseif挂载到root上。
- 否则,警告:组件模板应该只包含一个根元素。 如果在多个元素上使用v-If, 使用v-else-if来链接它们
- 如果currentParent父节点存在。并且标签不是script或者style。
- 如果element有elseif或者else属性,找到上一个兄弟节点,如果上一个兄弟节点是if,则下一个兄弟节点则是elseif
- 否则,element有slotScope(如果存在slotscope属性,即是作用域插槽),作用域插槽将挂在父节点的scopedslots属性上----作用域插槽和普通节点最大的不同点是它不会将当前结点挂在ast对象树上,而是挂在了父节点的scopedslots属性上。
- 否则,以上2种情况都不满足的话,把当前元素添加到父元素的children数组中,并且设置当前元素的父元素
- 如果当前标签不是单标签,也不是闭合标签,就标志当前currentParent 是当前标签。
- 非单元素,更新父级和保存该元素
源码:
start: function start(
tag, //标签名称 例:'button'
attrs, //标签属性 例:[{name: "@click", value: "getMoreMoney"}]
unary // 是否是单标签,是为true
) {
/**
currentParent : 父节点。这里为节点'div'。
如果有父节点,且有命名空间,则返回命名空间;
否则,tag 是svg或者math 标签,则返回true,否则返回undefined
*/
var ns = (currentParent && currentParent.ns) ||
platformGetTagNamespace(tag);
// 可以忽略
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs);
}
// 通过`标签,属性和父节点`来创建一个符合 AST格式的对象
var element = createASTElement(tag, attrs, currentParent);
if (ns) { //判断 tag 是否是svg或者math 标签
element.ns = ns;
}
// 可忽略--------
// 是否是style||script标签
// 是否在服务器node环境下
if (
isForbiddenTag(element) &&
!isServerRendering()
) {
element.forbidden = true;
// 禁止在模板中放置带有副作用的标记
...
}
// preTransforms为[preTransformNode()]
for (var i = 0; i < preTransforms.length; i++) {
/**
对于input节点的type进行处理后的ast。
element:
例:{
attrs: [{name: "type", value: "\"checkbox\""}],
attrsList: [{name: "v-model", value: "num"},{name: "type", value: "radio"}],
attrsMap: {type: "checkbox", v-model: "num"},
children: [],
directives:[{name: "model", rawName: "v-
model", value: "num", arg: null, modifiers: undefined}],
hasBindings: true,
if: "(isNumber)==='checkbox'&&(num>1)", //本次添加
// 本次添加, block为branch0本身
ifConditions: [
// 这个对象的block为branch0本身
{exp: "(isNumber)==='checkbox'&&(num>1)", block: {…}},
// 这个对象的block为branch1本身
{exp: "(isNumber)==='radio'&&(num>1)", block: {…}},
// 这个对象的block为branch2本身
{exp: "num>1", block: {…}}
],
parent: {type: 1, tag: "div", attrsList: Array(1), …},
processed: true,
plain: false,
tag: "input",
type: 1,
}
*/
element = preTransforms[i](element, options) || element;
}
/**
v-pre指令的作用:跳过这个元素和它的子元素的编译过程,
一些静态的内容不需要编辑加这个指令可以加快编辑。
所以,如果有v-pre了,我们就把当前节点当作了静态节点,
不需要再对里面的其它指令进行处理。
*/
// 这里的作用是检查节点是否有v-pre指令,并打上标记
if (!inVPre) { //如果 标签 没有 v-pre 指令
/**
通过getAndRemoveAttr函数检查标签是否有v-pre 指令
含有 v-pre 指令的标签里面的指令则不会被编译
*/
processPre(element);
if (element.pre) { //标记 标签是否还有 v-pre 指令
inVPre = true; //如果标签有v-pre 指令 则标记为true
}
}
if (platformIsPreTag(element.tag)) { // 判断标签是否是pre 如果是则返回真
inPre = true;
}
if (inVPre) { //如果含有 v-pre 指令
//浅拷贝属性 把虚拟dom的attrsList拷贝到attrs中,如果没有pre块,标记plain为true
processRawAttrs(element);
} else if (!element.processed) {
// structural directives 指令
//判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中
processFor(element);
//获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性
processIf(element);
//获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁
processOnce(element);
// element-scope stuff
//校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性
processElement(element, options);
}
// 检查根约束 根节点不能是slot或者template标签,并且不能含有v-for 属性
function checkRootConstraints(el) {
{
if (el.tag === 'slot' || el.tag === 'template') {
...
}
if (el.attrsMap.hasOwnProperty('v-for')) {
...
}
}
}
// 如果根节点不存在,直接把我们创建的ast作为根节点
if (!root) {
root = element;
// 检查根约束 根节点不能是slot或者template标签,并且不能含有v-for 属性
checkRootConstraints(root);
} else if (!stack.length) { // 如果标签堆栈还没有数据
//根元素带有v-if,且element有v-else-if或v-else
if (root.if && (element.elseif || element.else)) {
//检查根约束 根节点不能是slot或者template标签,并且不能含有v-for 属性
checkRootConstraints(element);
//为if指令添加标记
addIfCondition(
root, //根节点
{
exp: element.elseif, //view 视图中的elseif 属性
block: element //当前的虚拟dom
}
);
} else {
/**
组件模板应该只包含一个根元素。如果在多个元素上使用v-If,使用v-else-if来链接它们
*/
warnOnce(
...
);
}
}
//如果currentParent父节点存在。并且标签是script或者style,给出警告,标签不会解析
if (
currentParent &&
!element.forbidden //如果是style或者是是script 标签并且type属性不存在 或者存在并且是javascript 属性 的时候返回真
) {
if (element.elseif || element.else) { //如果有elseif或者else属性的时候
//找到上一个兄弟节点,如果上一个兄弟节点是if,则下一个兄弟节点则是elseif
processIfConditions(element, currentParent);
} else if (element.slotScope) { // scoped slot 作用域的槽
//作用域插槽和普通节点最大的不同点是它不会将当前结点挂在ast对象树上,
//而是挂在了父节点的scopedslots属性上。
currentParent.plain = false;
//获取slotTarget作用域标签,如果获取不到则定义为default
var name = element.slotTarget || '"default"';
(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
} else {
//如果父节点存在currentParent则在父节点添加一个子节点,并且
currentParent.children.push(element);
//当前节点上添加parent属性
element.parent = currentParent;
}
}
// var unary = isUnaryTag$$1(tagName) || //函数匹配标签是否是 'hr,img,input..'
// !!unarySlash; //如果是/> 则为真
//如果当前标签不是单标签,也不是闭合标签,就标志当前currentParent 是当前标签
if (!unary) {
currentParent = element;
//为parse函数 stack标签堆栈 添加一个标签
stack.push(element);
} else {
closeElement(element);
}
},
5-2-1 createASTElement
作用:
创建ast对象
将start函数传进来的参数转换,通过标签,属性和父节点来创建一个符合 AST格式的对象。
function createASTElement(
tag, //标签名称 例:'button'
attrs, //标签属性 例:[{name: "@click", value: "getMoreMoney"}]
parent // 当前父节点,例:{type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, …}
) {
return {
type: 1, //dom 类型
tag: tag,
attrsList: attrs,
/**
* 对象属性 把数组对象转换成 对象 例:
* attrsMap: {id: "app"}
*/
//【说明2 makeAttrsMap】
attrsMap: makeAttrsMap(attrs),
parent: parent, // 当前父节点
children: []
}
}
最终返回的对象格式如下。可以看到格式和我们之前说到的ast是一致的了
{
attrsList: [{name: "@click", value: "getMoreMoney"}]
attrsMap: {@click: "getMoreMoney"}
children: []
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …}
tag: "button"
type: 1
}
5-2-2. makeAttrsMap
作用:
把数组对象转换成 对象
例如,将:
[{name: "id", value: "app"}]
转换成
{ id: app }
源码:
function makeAttrsMap(attrs) { //标签属性 例:[{name: "id", value: "app"}]
debugger
var map = {};
for (var i = 0, l = attrs.length; i < l; i++) {
// 可以忽略------
if (
"development" !== 'production' &&
map[attrs[i].name] && !isIE && !isEdge
) {
warn$2('duplicate attribute: ' + attrs[i].name);
}
//-----------
map[attrs[i].name] = attrs[i].value;
}
return map
}
5-2-3. preTransforms
这里的preTransforms其实就是下一节的函数preTransformNode。只不过用数组再外面包装了一下。
//定义在model$2中,model$2又挂载在modules$1,modules$1又挂载在modules
// [preTransformNode()]
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
var model$2 = {
preTransformNode: preTransformNode
}
5-2-4. preTransformNode
作用:
可以看到,函数一开始就判断了标签是否是input,所以这个函数是处理input节点的。更具体点的话,它是处理input的type类型的,它返回一个包含各种input type类型的ast对象。
执行流程:
- 如果标签是input,且有type属性,则进入我们的主流程
- 经过一系列处理,拿到type的变量(注意:不是值)
- 分别拿到v-if,v-else, v-else-if 的表达式
- 分别创建3个ast:
- branch0:type为checkbox的ast,并对这个ast中的v-for进行转义,添加转义后的4个属性到本身
- branch1:type为radio的ast,并对这个ast中的v-for进行转义,添加转义后的4个属性到本身
- branch2:type为变量的ast,并对这个ast中的v-for进行转义,添加转义后的4个属性到本身
- 最后将branch0,branch1,branch2分别挂载到branch0的ifConditions属性数组中,然后返回最终的branch0
源码:
/**
* Expand input[v-model] with dyanmic type bindings into v-if-else chains
* 使用dyanmic类型绑定将输入[v-model]展开到v-if-else链中
* Turn this:
* 把这个
* <input v-model="data[type]" :type="type">
* into this: 到这个
* <input v-if="type === 'checkbox'" type="checkbox" v-model="data[type]">
* <input v-else-if="type === 'radio'" type="radio" v-model="data[type]">
* <input v-else :type="type" v-model="data[type]">
*
*/
/**入参说明
el=》虚拟dom
例:{
attrsList: [{name: ":type", value: ""isNumber""},{name: "v-model", value: "num"}],
attrsMap: {:type: "isNumber", v-model: "num"}
children: [],
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …}
tag: "input",
type: 1
},
options=》基础配置
例:{
shouldDecodeNewlines: false,
shouldDecodeNewlinesForHref: false,
delimiters: undefined, comments: undefined, warn: ƒ
}
*
*/
function preTransformNode(
el,
options
) {
if (el.tag === 'input') {
// map,例:{:type: "isNumber", v-model: "num"}
var map = el.attrsMap;
// 如果属性中没有v-model 则退出,说明这整个函数都了为了input标签的v-model指令服务的
if (!map['v-model']) {
return
}
var typeBinding; //类型
// 如果有动态属性type
if (map[':type'] || map['v-bind:type']) {
/**
获取 :属性 或者v-bind:属性, 并在el.attrsList删除它,如果属性有使用
过滤器,那么要去过滤后的最终的属性变量名,
例:'isNumber'
*/
typeBinding = getBindingAttr(el, 'type');
}
/**
如果获取不到type属性也获取不到v-bind:type属性,但可以获取到v-bind属性,
typeBinding进行转换为:v-bind:abc,变成 (abc).type
*/
if (!map.type && !typeBinding && map['v-bind']) {
typeBinding = "(" + (map['v-bind']) + ").type";
}
if (typeBinding) {
/**
例:<input v-if='num>1' :type="isNumber" v-model='num'/>,则
ifCondition为:"num>1";
ifConditionExtra 为:"&&(num>1)"
*/
// 拿到判断条件
// 在attrsMap获取v-if值,并且删除v-if项
var ifCondition = getAndRemoveAttr(el, 'v-if', true);
// 对判断条件进行扩展
var ifConditionExtra = ifCondition ? ("&&(" + ifCondition + ")") : "";
// 获取 v-else 属性值并且判断它是否存在,例:false
var hasElse = getAndRemoveAttr(el, 'v-else', true) != null;
// 获取 v-else-if 属性值,例:undefined
var elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true);
// 以下处理input type为checkbox的情况------------------------------
/**克隆 创建 ast 元素,这里是el经过上面的处理,已经删除了一些属性,
可以看到attrsList和attrsMap中已经删除了if以及type相关的属性
例:{
attrsList: [{name: "v-model", value: "num"}],
attrsMap: {v-model: "num"},
children: [],
parent: {type: 1, tag: "div", attrsList: Array(1), …}
tag: "input",
type: 1
}
*/
var branch0 = cloneASTElement(el);
// 判断获取v-for属性是否存在,如果有则转义 v-for指令, 给虚拟dom添加4个for相关的属性
processFor(branch0);
/**添加type 属性 值为checkbox
branch0,例:{
attrsList: [{name: "v-model", value: "num"},{name: "type", value: "checkbox"}],
attrsMap: {type: "checkbox", v-model: "num"},
children: [],
parent: {type: 1, tag: "div", attrsList: Array(1), …}
tag: "input",
type: 1
}
*/
addRawAttr(branch0, 'type', 'checkbox');
/**
校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,
slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性。
branch0值:
例:{
attrs: [{name: "type", value: "\"checkbox\""}], //本次添加
attrsList: [{name: "v-model", value: "num"},{name: "type", value: "checkbox"}],
attrsMap: {type: "checkbox", v-model: "num"},
children: [],
directives:[{name: "model", rawName: "v-
model", value: "num", arg: null, modifiers: undefined}],//本次添加
hasBindings: true, //本次添加
parent: {type: 1, tag: "div", attrsList: Array(1), …},
plain: false, //本次添加
tag: "input",
type: 1
}
*/
processElement(branch0, options);
branch0.processed = true; // 表示当前创建的元素已经加工处理了,防止它被重复处理
/**
branch0.if,例:"(isNumber)==='checkbox'&&(num>1)",
其目的是为了区别我们input的类型是什么,因为input可以是select,radio,checkbox等
*/
branch0.if = "(" + typeBinding + ")==='checkbox'" + ifConditionExtra;
/**
为if指令添加标记if和ifConditions属性。
branch0值:
例:{
attrs: [{name: "type", value: "\"checkbox\""}],
attrsList: [{name: "v-model", value: "num"},{name: "type", value: "checkbox"}],
attrsMap: {type: "checkbox", v-model: "num"},
children: [],
directives:[{name: "model", rawName: "v-
model", value: "num", arg: null, modifiers: undefined}],
hasBindings: true,
if: "(isNumber)==='checkbox'&&(num>1)", //本次添加
// 本次添加, block为branch0本身
ifConditions: [{exp: "(isNumber)==='checkbox'&&(num>1)", block: {…}}],
parent: {type: 1, tag: "div", attrsList: Array(1), …},
processed: true,
plain: false,
tag: "input",
type: 1,
}
*/
addIfCondition(
branch0, //虚拟dom
{
exp: branch0.if,
block: branch0 // 这里是添加本身的虚拟dom
}
);
// 以下:处理type为 radio元素---------------------------------
/**
branch1,例:
{
attrsList: [{name: "v-model", value: "num"}],
attrsMap: {v-model: "num"},
children: [],
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…} …},
tag: "input",
type: 1
}
*/
var branch1 = cloneASTElement(el);
//删除v-for 属性
getAndRemoveAttr(branch1, 'v-for', true);
/**
添加type 属性
branch1,例:
{
attrsList: [{name: "v-model", value: "num"}, {name: "type", value: "radio"}],
attrsMap: {type: "radio", v-model: "num"},
children: [],
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…} …},
tag: "input",
type: 1
}
*/
addRawAttr(branch1, 'type', 'radio');
/**
branch1值:
例:{
attrs: [{name: "type", value: "\"radio\""}], //本次添加
attrsList: [{name: "v-model", value: "num"},{name: "type", value: "radio"}],
attrsMap: {type: "radio", v-model: "num"},
children: [],
directives:[{name: "model", rawName: "v-
model", value: "num", arg: null, modifiers: undefined}],//本次添加
hasBindings: true, //本次添加
parent: {type: 1, tag: "div", attrsList: Array(1), …},
plain: false, //本次添加
tag: "input",
type: 1
}
*/
processElement(branch1, options);
/**
为if指令添加标记if和ifConditions属性,本次改动的主要是block。
branch0值:
例:{
attrs: [{name: "type", value: "\"checkbox\""}],
attrsList: [{name: "v-model", value: "num"},{name: "type", value: "radio"}],
attrsMap: {type: "checkbox", v-model: "num"},
children: [],
directives:[{name: "model", rawName: "v-
model", value: "num", arg: null, modifiers: undefined}],
hasBindings: true,
if: "(isNumber)==='checkbox'&&(num>1)", //本次添加
// 本次添加, block为branch0本身
ifConditions: [
// 这个对象的block为branch0本身
{exp: "(isNumber)==='checkbox'&&(num>1)", block: {…}},
// 这个对象的block为branch1本身
{exp: "(isNumber)==='radio'&&(num>1)", block: {…}},
],
parent: {type: 1, tag: "div", attrsList: Array(1), …},
processed: true,
plain: false,
tag: "input",
type: 1,
}
*/
addIfCondition(branch0, {
exp: "(" + typeBinding + ")==='radio'" + ifConditionExtra,
block: branch1
});
// 3. 处理type为 其它类型元素,类型为变量------------------------
var branch2 = cloneASTElement(el);
//删除v-for属性
getAndRemoveAttr(branch2, 'v-for', true);
//添加:type 属性
addRawAttr(branch2, ':type', typeBinding);
/**
branch2值:
例:{
attrs: [{name: "type", value: "isNumber"}], //本次添加
attrsList: [{name: "v-model", value: "num"},{name: ":type", value: "isNumber"}],
attrsMap: { :type: "isNumber", v-model: "num"},
children: [],
directives:[{name: "model", rawName: "v-
model", value: "num", arg: null, modifiers: undefined}],//本次添加
hasBindings: true, //本次添加
parent: {type: 1, tag: "div", attrsList: Array(1), …},
plain: false, //本次添加
tag: "input",
type: 1
}
*/
processElement(branch2, options);
/**
为if指令添加标记if和ifConditions属性,本次改动的主要是block。
branch0值:
例:{
attrs: [{name: "type", value: "\"checkbox\""}],
attrsList: [{name: "v-model", value: "num"},{name: "type", value: "radio"}],
attrsMap: {type: "checkbox", v-model: "num"},
children: [],
directives:[{name: "model", rawName: "v-
model", value: "num", arg: null, modifiers: undefined}],
hasBindings: true,
if: "(isNumber)==='checkbox'&&(num>1)", //本次添加
// 本次添加, block为branch0本身
ifConditions: [
// 这个对象的block为branch0本身
{exp: "(isNumber)==='checkbox'&&(num>1)", block: {…}},
// 这个对象的block为branch1本身
{exp: "(isNumber)==='radio'&&(num>1)", block: {…}},
// 这个对象的block为branch2本身
{exp: "num>1", block: {…}}
],
parent: {type: 1, tag: "div", attrsList: Array(1), …},
processed: true,
plain: false,
tag: "input",
type: 1,
}
*/
addIfCondition(
branch0,
{
exp: ifCondition, //v-if 属性值
block: branch2 //ast元素,这里是添加本身的虚拟dom
}
);
debugger
//判断是else还是elseif
if (hasElse) {
branch0.else = true;
} else if (elseIfCondition) {
branch0.elseif = elseIfCondition;
}
//返回转换过虚拟dom的对象值
return branch0
}
}
}
5-2-5. processFor
作用:
判断获取v-for属性是否存在,如果有则转义 v-for指令, 给虚拟dom添加以下4个属性:
- alias: "item", // value字符串
- for: "num", // data字符串
- iterator1: "index" //key字符串
- iterator2: "index" //key字符串 这个属性不一定会添加
源码:
/**
* 入参el,例:
{
attrsList: [{name: ":key", value: "index"}],
attrsMap: {v-for: "(item, index) in num", :key: "index"},
children: [],
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …},
tag: "div",
type: 1,
__proto__: Object
}
*
*/
function processFor(
el
) {
var exp;
// 获取attrsMap中v-for指令 属性 例:"(item, index) in num"
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
// 转换 for指令 语句成为一个对象
/**
* res,例:
{
alias: "item", // value字符串
for: "num", // data字符串
iterator1: "index" //key字符串
}
*/
var res = parseFor(exp);
// 如果能转换的话,直接转换的对象合并到虚拟dom上去
if (res) {
extend(el, res);
} else {
warn$2(
("Invalid v-for expression: " + exp)
);
}
}
}
5-2-6. parseFor
作用:
转换 for指令 语句成为一个对象,
例:
v-for = "(item, index) in num" :key='index'
转换成:
{
alias: "item", // value字符串
for: "num", // data字符串
iterator1: "index" //key字符串
}
源码:
function parseFor(
exp // for表达式,例:"(item, index) in num"
) {
debugger
/**
* inMatch,获取匹配字符串。
* 例:["(item, index) in num", "(item, index)", "num"]
*/
var inMatch = exp.match(forAliasRE);
if (!inMatch) { //如果匹配不上则返回出去
return
}
var res = {};
/**
* res,获取到数据 data 字符串,然后挂载到for上,例:{ for: "num" } "item, index"
*/
res.for = inMatch[2].trim();
/**
* alias,去除括号,例:"item, index"
*/
var alias = inMatch[1].trim().replace(stripParensRE, '');
/**
* iteratorMatch,获取分隔符
* 例:[ ", index", " index", undefined]
*/
var iteratorMatch = alias.match(forIteratorRE);
// 如果存在分隔符index等,就需要给res挂载3个参数
// 如果不存在分隔符,例:v-for = "item in num",则只需要挂载alias
if (iteratorMatch) {
/**
* 这里操作为:从"item, index"去掉 key , index ,+字符串, 获得value 字符串
* res,获取到数据 data 字符串,然后挂载到alias上,
* 例:{ alias: "item", for: "num" }
*/
res.alias = alias.replace(forIteratorRE, '');
/**
* 获取到"item, index"中除了item剩余的字符串,再去掉空格,然后挂载到iterator1 上,
* res, 例:{ alias: "item", for: "num", iterator1: "index" }
*/
res.iterator1 = iteratorMatch[1].trim(); //获取第二个字符串 key
if (iteratorMatch[2]) { //一般没有这个
// 获取第三个字符串 index
// iterator2:index字符串
res.iterator2 = iteratorMatch[2].trim();
}
} else {
res.alias = alias; //单个字符串的时候 value in data
}
return res
}
5-2-7. getBindingAttr
获取 :属性 或者v-bind:属性,或者获取属性 移除传进来的属性name,并且返回获取到 属性的值
/**入参说明
el=》虚拟dom
例:{
attrsList: [{name: ":type", value: ""isNumber""},{name: "v-model", value: "num"}],
attrsMap: {:type: "isNumber", v-model: "num"}
children: [],
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …}
tag: "input",
type: 1
},
name=》属性名
例:'type',"class",'slot',"key","ref" 等
*
*/
function getBindingAttr(
el,
name,
getStatic // false
) {
/**
* 获取 :属性 或者v-bind:属性,并在el.attrsList删除它,
* 它获取的是一个我们绑定变量名(注意,这里获取的是变量名,不是值)
* 例:'isNumber'
*/
var dynamicValue = getAndRemoveAttr(el, ':' + name) ||
getAndRemoveAttr(el, 'v-bind:' + name);
if (dynamicValue != null) {
/*
*处理value 解析成正确的value,如果有过滤器,就获取过滤后最终的变量名,
例:'isNumber'
*/
let parseFiltersValue = parseFilters(dynamicValue);
return parseFiltersValue
} else if (getStatic !== false) {
//移除传进来的属性name,并且返回获取到 属性的值
var staticValue = getAndRemoveAttr(el, name);
if (staticValue != null) {
//转换成字符串
return JSON.stringify(staticValue)
}
}
}
5-2-8. getAndRemoveAttr
作用:
从el.attrsList移除传进来的属性name,并且返回获取到 属性的值
源码:
function getAndRemoveAttr(
el, //el 虚拟dom
name,//属性名称 需要删除的属性 name,获取值的name属性
removeFromMap //是否要删除属性的标志 undefined
) {
var val;
// 如果属性map中的对应属性有值
if ((val = el.attrsMap[name]) != null) {
/**
* 例:list:
* [{name: "class", value: "full-input"},
* {name: ":type", value: "texts"}]
*/
var list = el.attrsList;
// 从el.attrsList删除传入的属性项
for (var i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1);
break
}
}
}
if (removeFromMap) { //是否要删除属性的标志
delete el.attrsMap[name];
}
return val
}
5-2-9. parseFilters
处理value 解析成正确的value,把过滤器 转换成vue 虚拟dom的解析方法函数 , 比如把过滤器 ' ab | c | d' 转换成 _f("d")(_f("c")(ab))
//匹配 ) 或 . 或 + 或 - 或 _ 或 $ 或 ]
var validDivisionCharRE = /[\w).+\-_$\]]/;
function parseFilters(
/**
* 例如我们的html中:
* <input :type="texts | recordType" v-model='message'/>
* 这里是exp就是:"texts | recordType"
*/
exp
) {
// 是否在 ''中
var inSingle = false;
// 是否在 "" 中
var inDouble = false;
// 是否在 ``
var inTemplateString = false;
// 是否在 正则 \\ 中
var inRegex = false;
// 是否在 {{ 中发现一个 culy加1 然后发现一个 } culy减1 直到culy为0 说明 { .. }闭合
var curly = 0;
// 跟{{ 一样 有一个 [ 加1 有一个 ] 减1
var square = 0;
// 跟{{ 一样 有一个 ( 加1 有一个 ) 减1
var paren = 0;
var lastFilterIndex = 0;
var c, prev, i, expression, filters;
// for循环传入的value字符串
// 0x27 == ' ; 0x5C == \ ; 0x22 == " ; 0x60 == ` ; 0x2f == / ; 0x7c == |
for (i = 0; i < exp.length; i++) {
prev = c;
// 遍历拿到每个过滤字符串的code码
c = exp.charCodeAt(i);
if (inSingle) {
if (c === 0x27 && prev !== 0x5C) {// ' \
inSingle = false;
}
} else if (inDouble) {
if (c === 0x22 && prev !== 0x5C) {// " \
inDouble = false;
}
} else if (inTemplateString) {
if (c === 0x60 && prev !== 0x5C) {// ` \
inTemplateString = false;
}
} else if (inRegex) {
if (c === 0x2f && prev !== 0x5C) { // / \
inRegex = false;
}
}
else if (
// 如果在 之前不在 ' " ` / 即字符串 或者正则中
// 那么就判断 当前字符是否是 |
// 如果当前 字符为 |
// 且 不在 { } 对象中
// 且 不在 [] 数组中
// 且不在 () 中
// 那么说明此时是过滤器的一个 分界点
// 当前字符是 | 并且左右两侧不是 |
c === 0x7C &&
exp.charCodeAt(i + 1) !== 0x7C &&
exp.charCodeAt(i - 1) !== 0x7C && !curly && !square && !paren
) {
/*
初始化时,expression为空,说明这是第一个 管道符号 "|",
再次遇到 | 因为前面 expression = 'texts '
执行 pushFilter()
*/
if (expression === undefined) {
// first filter, end of expression
// 过滤器表达式 就是管道符号之后开始
// 此例值为:lastFilterIndex = 7, i = 6
lastFilterIndex = i + 1;
// 存储过滤器的 表达式,如:"texts"
// 这里匹配如果字符串是 'ab | c' 则把ab匹配出来
expression = exp.slice(0, i).trim();
} else {
pushFilter();
}
}
else {
// 是否匹配特殊字符
switch (c) {
// 解析为 “ 时,标记为双引号
case 0x22: inDouble = true; break // "
// 解析为 ’ 时,标记为单引号
case 0x27: inSingle = true; break // '
// 解析为 ` 时,标记为模板字符串
case 0x60: inTemplateString = true; break // `
// 解析为( 时,paren 计数加一, 通过 paren 是否为0判断 () 是否闭合
case 0x28: paren++; break // (
// 解析为 )时,paren 计数减一
case 0x29: paren--; break // )
// 解析为 [ 时, square 计数加一 ,通过 square 是否为0判断 [] 是否闭合
case 0x5B: square++; break // [
// 解析为 ] 时, square 计数减一
case 0x5D: square--; break // ]
// 解析为 { 时, curly 计数加一, 通过 curly 是否为0判断 {} 是否闭合
case 0x7B: curly++; break // {
// 解析为 } 时, curly 计数减一,
case 0x7D: curly--; break // }
}
// 如果是 / ,向前找第一个非空格的字符
if (c === 0x2f) { // /
let j = i - 1
let p
// find first non-whitespace prev char
for (; j >= 0; j--) {
p = exp.charAt(j)
if (p !== ' ') break
}
// 如果 p 不存在或者正则校验不通过,则说明是正则
if (!p || !validDivisionCharRE.test(p)) {
inRegex = true
}
}
}
// 如果最终没有过滤表达式,说明不符合过滤条件,
// 直接将传入的字符串当作输出的返回值
if (expression === undefined) { // exp = "texts | recordType"
expression = exp.slice(0, i).trim();
} else if (lastFilterIndex !== 0) {
pushFilter();
}
// 获取当前过滤器的 并将其存储在filters 数组中,拿到的是后面的过滤函数组成的数组
// 取 "texts | recordType | recordType1" 中的过滤函数组成:
// filters = [ 'recordType' , 'recordType1']
function pushFilter() {
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());
lastFilterIndex = i + 1;
}
debugger
// "recordType"
if (filters) {
for (i = 0; i < filters.length; i++) {
//把过滤器封装成函数 虚拟dom需要渲染的函数
expression = wrapFilter(
expression, // "texts"
filters[i] //"recordType"
);
// 返回,例:"_f("recordType")(texts)
}
}
//返回值
return expression
}
5-2-10. processElement
作用:
校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性
源码:
function processElement(element, options) {
//获取属性key值,校验key 是否放在template 标签上面 为el 虚拟dom添加 key属性
processKey(element);
// determine whether this is a plain element after
// removing structural attributes
//确定这是否是一个普通元素后
//删除结构属性
element.plain = !element.key && !element.attrsList.length; //如果没有key 也没有属性
//获取ref 属性,并且判断ref 是否含有v-for指令 为el虚拟dom 添加 ref 属性
processRef(element);
//检查插槽作用域 为el虚拟dom添加 slotName或者slotScope或者slot
processSlot(element);
// 判断虚拟dom 是否有 :is属性,是否有inline-template 内联模板属性 如果有则标记下 为el 虚拟dom 添加component属性或者inlineTemplate 标志
processComponent(element);
//转换数据
for (var i = 0; i < transforms.length; i++) {
element = transforms[i](element, options) || element;
}
//检查属性,为虚拟dom属性转换成对应需要的虚拟dom vonde数据 为el虚拟dom 添加muted, events,nativeEvents,directives
processAttrs(element);
}
5-3. end
作用:
闭合元素,更新stack和currentParent
当一个标签解析完成后,会检测标签存储的栈,如果栈顶中的标签与当前标签不同,则会忽略不匹配的标签(修改栈顶的位置)并提示,直到找到匹配的标签,然后执行 外部传入的end
源码:
end: function end() {
debugger
// 取出stack中最后一个元素,其实这也是需要闭合元素的开始标签,如</div> 的开始标签就是<div>
// 此时取出的element包含该元素的所有信息,包括他的子元素信息
var element = stack[stack.length - 1];
// 取出当前元素的最后一个子节点
var lastNode = element.children[element.children.length - 1];
// 如果最后一个子节点是空文本节点,清除当前子节点, 为什么这么做呢?
// 因为我们在写HTML时,标签之间都有间距,有时候就需要这个间距才能达到我们想要的效果,
// 比如:<div> <span>111</span> <span>222</span> </div>
// 此时111与222之间就有一格的间距,在ast模板解析时,这个不能忽略,
// 此时的div的子节点会解析成三个数组, 中间的就是一个文本,只是这个文本是个空格,
// 而222的span标签后面的空格我们是不需要的,因为如果我们写了,div的兄弟节点之间会有一个空格的。
// 所以我们需要清除children数组中没有用的项
if (
lastNode && //判断子节点最后一个节点
lastNode.type === 3 //文本节点 Text 空格等
&& lastNode.text === ' ' //空格
&& !inPre //标志需要编译的状态
) {
element.children.pop(); //删除空格文本节点
}
// 下面才是最重要的,也是end方法真正要做的,
// 就是找到了闭合标签,就把保存的开始标签的信息清除,并更新currentParent
/**
* stack,例:
* [ {type: 1, tag: "div"...}, {type: 1, tag: "button"...}]
* 此时出栈后为:
* [ {type: 1, tag: "div"...}]
*/
stack.length -= 1; // 出栈一个当前标签
// 更新currentParent为出栈后的stack的最后一个标签
currentParent = stack[stack.length - 1];
closeElement(element);
},
5-4. chars
作用:
把text添加到属性节点或者添加到注释节点,ast模板数据
源码:
chars: function chars(
text // 例:{{item}}
) {
debugger
//如果是文本,没有父节点,返回并警告:
// 组件模板需要根元素,而不仅仅是文本,将忽略根元素外的text
if (!currentParent) { //警告日志
{
if (text === template) {
warnOnce(
//组件模板需要根元素,而不仅仅是文本
...
);
} else if ((text = text.trim())) {
warnOnce(
// 将忽略根元素外的text \“”+text+“\”。)
...
);
}
}
return
}
// 不用管这里
if (
isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text
) {
return
}
var children = currentParent.children; // 获取到同级的兄弟节点
/**
* inPre,判断标签是否是pre 如果是则返回真,则不需要去空格
* (isTextTag(currentParent),判断标签是否是script或者是style
* decodeHTMLCached(text),获取 真实dom的textContent文本
* preserveWhitespace,只有在开始标记之后没有空格时才保留空格,
* 如果是children.length存在并且preserveWhitespace为真则保留空格
*/
text = inPre ||
text.trim() ?
( isTextTag(currentParent) ? text : decodeHTMLCached(text) ) :
(preserveWhitespace && children.length) ? ' ' : '';
// 解析文本,处理{{}} 这种形式的文本
if (text) {
var res;
/**
* text, {{item}}
* res,例:
{
expression: "_s(item)",
tokens: [{@binding: "item"}]
}
*/
if (
!inVPre && //标签是否还有 v-pre 指令
text !== ' ' && //空节点
(res = parseText(text, delimiters)) //匹配view 指令,并且把他转换成 虚拟dom vonde 需要渲染的函数
) {
/**
* [{
type: 2,
expression: "_s(item)",
tokens: [{@binding: "item"}],
text: "{{item}}"
}]
*/
children.push({ //添加为属性节点
type: 2, // Attr 代表属性
expression: res.expression,
tokens: res.tokens,
text: text
});
} else if (
text !== ' ' ||
!children.length ||
children[children.length - 1].text !== ' '
) {
children.push({
type: 3,
text: text
});
}
}
}
6.parseHTML
6-1. 基本信息
作用:
parseHTML(html, options)接受两个参数——模板和选项,通过循环截取解析,根据解析到不同的内容,调用选项中不同的钩子函数构建AST节点。
HTML解析,其接受template以及一些配置参数其中最主要的就是 start 和 end ,在parse HTML 内部对template 进行了标签拆分,拆分的时候又会根据不同类型的标签进行处理, 其主要分为几个阶段:
- 起始标签开合
- 起始标签闭合
- 结束标签闭合
- 文本解析
在parseHTML函数中,会维护一个栈,这个栈用来记录DOM的层级关系。解析HTML模板时是一个循环的过程,模板解析时又分为纯文本内容元素与非纯文本内容元素来进行处理。
参数说明:
以下,对parseHTML的入参进行说明。
本模板我们是有一个父组件,和一个子组件msg-tip,因为parseHTML是解析组件用的,所以这里会解析2次。
第1次解析:html为整个父组件的字符串模板,例:
// html
"<div id="app">
<input v-if="num>1" :type="isNumber" v-model="num">
<!-- 我是注释 -->
<div v-for="(item, index) in num" :key="index">{{item}}</div>
<h2>开始存钱</h2>
<div>每月存 :¥{{ money }}</div>
<div>存:{{ num }}个月</div>
<div>总共存款: ¥{{ total }}</div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
<msg-tip :msginfo="msgText" :totalnum="total"></msg-tip>
</div>
"
// options
{
canBeLeftOpenTag: ƒ (val),
chars: ƒ chars( text // 例:{{item}} ),
comment: ƒ comment(text),
end: ƒ end(),
expectHTML: true,
isUnaryTag: ƒ (val),
shouldDecodeNewlines: false,
shouldDecodeNewlinesForHref: false,
shouldKeepComment: undefined,
start: ƒ start( tag, attrs, unary // 如果不是单标签则为真 ),
warn: ƒ (msg, tip)
}
第2次解析:html为子组件msg-tip的字符串模板,例:
// html
"<div>{{ msginfo }}存了¥{{ totalnum }}</div>
"
// options
{
...同上
}
源码: 源码非常长,我们看下基本结构
function parse(template,options) {
parseHTML(
template,
...
)
}
function parseHTML(
html, //字符串模板
options //参数
) {
debugger
var stack = []; // parseHTML 节点标签堆栈
var expectHTML = options.expectHTML; //标志是html,true
// 函数,匹配标签是否是单标签,如'frame,hr,img,input..'
var isUnaryTag$$1 = options.isUnaryTag || no;
//判断标签是否是双标签,如: 'colgroup,dd,dt,li,tr..'
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;
var index = 0; //记录的是html当前找到那个索引
var last, //用来比对,当这些条件都走完后,如果last==html 说明匹配不到啦,结束while循环
lastTag;
while (html) { ... }
parseEndTag();
// while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串
// 直到最后一个标签被截取完没有匹配到则跳出循环
/**
*
var str="Hello"
var a = str.substring(3)
console.log(a) // 'lo'
console.log(str) // 'Hello'
*/
function advance(n) {
index += n; //让索引叠加
html = html.substring(n); //截取当前索引 和 后面的字符串。
}
function parseStartTag(){...}
function handleStartTag(match) {...}
function parseEndTag(...)
}
6-2. while (html)
作用:
执行流程:
逻辑1:如果不存在上一个标签,或者标签不是script,style,textarea
-
声明textEnd:
'<'符号出现的位置。 -
如果textEnd==0:第一个字符是<,那么就只有两种情况,1开始标签 2结束标签- 然后分别处理html是注释,Doctype,End tag,Start tag的情况,匹配完要删除匹配到的,并且更新index,给下一轮匹配做工作
- 匹配完要删除匹配到的,并且更新index,给下一轮匹配做工作
-
如果textEnd>=0:<前面还有文本- 设置rest:删除html前面文本后的内容
- 如果剩余部分的 HTML(rest) 不符合标签的格式那肯定就是文本,并且还是以 < 开头的文本,这个时候只需要循环把 textEnd 累加,直到剩余的 模板字符串 符合标签的规则之后,更新rest和textEnd
- 最后,一次性把 text 从 模板字符串 中截取出来就好了。
- 设置rest:删除html前面文本后的内容
-
如果textEnd<0:都没有匹配到 < 符号 则表示纯文本,把html置空 跳出 while循环
逻辑2:如果存在上一个标签,且标签是script,style,textarea
- 父元素为纯文本内容(script/style)的处理逻辑,会被当做文本来处理,只需要将文本截取出来并触发钩子函数charts()即可
源码:
while (html) { //循环html
last = html;
if (
!lastTag ||
!isPlainTextElement(lastTag) // 如果标签不是script,style,textarea
) {
var textEnd = html.indexOf('<'); //匹配开始标签或者结束标签的位置
if (textEnd === 0) { //表示是开始或者结束标签
// 注释--------------------------------------------------
// 匹配 开始字符串为<!--任何字符串,注释标签 如果匹配上
if (comment.test(html)) {
var commentEnd = html.indexOf('-->'); //获取注释标签的结束位置
if (commentEnd >= 0) { //如果注释标签结束标签位置大于0,则有注释内容
if (options.shouldKeepComment) { //shouldKeepComment为真时候。获取注释标签内容
// 截取注释标签的内容
options.comment(html.substring(4, commentEnd));
}
//截取字符串重新循环 while 跳出循环就是靠该函数,
//每次匹配到之后就截取掉字符串,直到最后一个标签被截取完没有匹配到则跳出循环
advance(commentEnd + 3);
continue
}
}
// 这里不用关注--------------------
//这里思路是先匹配到注释节点,在匹配到这里的ie浏览器加载样式节点
if (conditionalComment.test(html)) {
//匹配ie浏览器动态加样式结束符号
var conditionalEnd = html.indexOf(']>');
if (conditionalEnd >= 0) {
advance(conditionalEnd + 2);
continue
}
}
// Doctype:--------------------------------------------------
//匹配html的头文件 <!DOCTYPE html>
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue
}
// End tag:--------------------------------------------------
//匹配开头必需是</ ,后面可以忽略是任何字符串
// ^<\\/((?:[a-zA-Z_][\\w\\-\\.]*\\:)?[a-zA-Z_][\\w\\-\\.]*)[^>]*>
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
// 匹配完要删除匹配到的,并且更新index,给下一轮匹配做工作
advance(endTagMatch[0].length);
//查找parseHTML的stack栈中与当前tagName标签名称相等的标签,
//调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除,
//为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点
parseEndTag(
endTagMatch[1],
curIndex,
index
);
continue
}
// Start tag:--------------------------------------------------
// 解析开始标记 标记开始标签
// 获取开始标签的名称,属性集合,开始位置和结束位置,并且返回该对象
/**
* 例:
{
attrs: [" id=\"app\"", "id", "=", "app", undefined, undefined]
end: 14
start: 0
tagName: "div"
unarySlash: ""
}
*/
var startTagMatch = parseStartTag();
if (startTagMatch) {
//把数组对象属性值循环变成对象,这样可以过滤相同的属性
//为parseHTML 节点标签堆栈 插入一个桟数据
//调用options.start 为parse函数 stack标签堆栈 添加一个标签
handleStartTag(startTagMatch);
//匹配tag标签是pre,textarea,并且第二个参数的第一个字符是回车键
if (shouldIgnoreFirstNewline(lastTag, html)) {
//第一个标签,例:<div id="app"> ,然年将剩余的html部分继续执行while循环,
// 可以发现,这时候的input标签上方是有一行空格的,那个空格就是删除了的第一个<div>
// 所以这个textEnd就会大于0 了。
/**
* 此时的html:
* "
<input v-if="num>1" :type="isNumber" v-model="num">
<!-- 我是注释 -->
<div v-for="(item, index) in num" :key="index">{{item}}</div>
<h2>开始存钱</h2>
<div>每月存 :¥{{ money }}</div>
<div>存:{{ num }}个月</div>
<div>总共存款: ¥{{ total }}</div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
<msg-tip :msginfo="msgText" :totalnum="total"></msg-tip>
</div>"
*/
advance(1);
}
continue
}
}
var text = (void 0),
rest = (void 0),
next = (void 0);
if (textEnd >= 0) {
/**
* 删除了第一个<符号前的空格。
* 此时的rest:
"<input v-if="num>1" :type="isNumber" v-model="num">
<!-- 我是注释 -->
<div v-for="(item, index) in num" :key="index">{{item}}</div>
<h2>开始存钱</h2>
<div>每月存 :¥{{ money }}</div>
<div>存:{{ num }}个月</div>
<div>总共存款: ¥{{ total }}</div>
<button @click="getMoreMoney">{{arryList[0].name}}多存一点</button>
<msg-tip :msginfo="msgText" :totalnum="total"></msg-tip>
</div>"
*/
rest = html.slice(textEnd);
// 如果rest的开头不是任何开始标签
while (
!endTag.test(rest) && //匹配开头必需是</ 后面可以忽略是任何字符串
!startTagOpen.test(rest) && // 匹配开头必需是< 后面可以忽略是任何字符串
!comment.test(rest) && // 匹配 开始字符串为<!--任何字符串
!conditionalComment.test(rest) //匹配开始为 <![ 字符串
) {
// <在纯文本中,要宽容,把它当作文本来对待
next = rest.indexOf('<', 1); //匹配是否有多个<
if (next < 0) {
break
}
textEnd += next; //截取 索引位置
rest = html.slice(textEnd); //获取 < 字符串 < 获取他们两符号< 之间的字符串
}
/**
* text截取<前面的内容,如这里是空格:
"
"
*/
text = html.substring(0, textEnd);
advance(textEnd);
}
if (textEnd < 0) { //都没有匹配到 < 符号 则表示纯文本
text = html; //出来text
html = ''; //把html至空 跳槽 while循环
}
if (options.chars && text) {
// 例如,空字符串变成了 "\n "
options.chars(text);
}
} else {
// 处理是script,style,textarea
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
text = text
.replace(/<!\--([\s\S]*?)-->/g, '$1') // #7298
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
}
//匹配tag标签是pre,textarea,并且第二个参数的第一个字符是回车键
if (shouldIgnoreFirstNewline(stackedTag, text)) {
text = text.slice(1);
}
if (options.chars) {
options.chars(text);
}
return ''
});
index += html.length - rest$1.length;
html = rest$1;
parseEndTag(stackedTag, index - endTagLength, index);
}
if (html === last) {
options.chars && options.chars(html);
if ("development" !== 'production' && !stack.length && options.warn) {
options.warn(("Mal-formatted tag at end of template: \"" + html + "\""));
}
break
}
}
6-3. parseStartTag
作用:
执行流程:
源码:
while (html) {
last = html;
if (
!lastTag ||
!isPlainTextElement(lastTag)
) {
var textEnd = html.indexOf('<');
if (textEnd === 0) {
}
}
}
function parseStartTag() {
//匹配开始标签 匹配开头必需是< 后面可以忽略是任何字符串
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1], //标签名称
attrs: [], //标签属性集合
start: index //标签的开始索引
};
//标记开始标签的位置,截取了开始标签
advance(start[0].length);
var end, attr;
while (
!(end = html.match(startTagClose)) //没有到 关闭标签 > 标签
&& (attr = html.match(attribute)) //收集属性
) {
//截取属性标签
advance(attr[0].length);
match.attrs.push(attr); //把属性收集到一个集合
}
if (end) {
match.unarySlash = end[1]; //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空
//截取掉开始标签,并且更新索引
advance(end[0].length);
match.end = index; //开始标签的结束位置
return match
}
}
}
6-4. parseEndTag
作用:
解析关闭标签, 查找我们之前保存到stack栈中的元素, 如果找到了,也就代表这个标签的开始和结束都已经找到了, 此时stack中保存的也就需要删除(pop)了, 并且缓存最近的标签lastTag
执行流程:
源码:
function parseEndTag(
tagName, //标签名称
start, //结束标签开始位置
end //结束标签结束位置
) {
var pos,
lowerCasedTagName;
if (start == null) { //如果没有传开始位置
start = index; //就那当前索引
}
if (end == null) { //如果没有传结束位置
end = index; //就那当前索引
}
if (tagName) { //结束标签名称
lowerCasedTagName = tagName.toLowerCase(); //将字符串转化成小写
}
// Find the closest opened tag of the same type 查找最近打开的相同类型的标记
if (tagName) {
// 获取stack堆栈最近的匹配标签
for (pos = stack.length - 1; pos >= 0; pos--) {
//找到最近的标签相等
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
// If no tag name is provided, clean shop
//如果没有提供标签名称,请清理商店
pos = 0;
}
if (pos >= 0) { //这里就获取到了stack堆栈的pos索引
// Close all the open elements, up the stack 关闭所有打开的元素,向上堆栈
for (var i = stack.length - 1; i >= pos; i--) {
if ("development" !== 'production' && //如果stack中找不到tagName 标签的时候就输出警告日志,找不到标签
(i > pos || !tagName) &&
options.warn
) {
options.warn(
("tag <" + (stack[i].tag) + "> has no matching end tag.")
);
}
if (options.end) {
//调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除,
//为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点
options.end(
stack[i].tag,//结束标签名称
start, //结束标签开始位置
end //结束标签结束位置
);
}
}
// Remove the open elements from the stack
//从堆栈中删除打开的元素
// 为parseHTML 节点标签堆栈 出桟当前匹配到的标签
stack.length = pos;
//获取到上一个标签,就是当前节点的父节点
lastTag = pos && stack[pos - 1].tag;
} else if (lowerCasedTagName === 'br') {
if (options.start) {
//标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中
//获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性
//获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁
//校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性
// 标志当前的currentParent当前的 element
//为parse函数 stack标签堆栈 添加一个标签
options.start(
tagName,
[], true,
start,
end
);
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
//标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中
//获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性
//获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁
//校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性
// 标志当前的currentParent当前的 element
//为parse函数 stack标签堆栈 添加一个标签
options.start(
tagName,
[], false,
start,
end);
}
if (options.end) {
//删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除,
//为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点
options.end(
tagName,
start,
end
);
}
}
}
}
6-5. handleStartTag
作用:
把数组对象属性值循环变成对象,这样可以过滤相同的属性
为parseHTML 节点标签堆栈 插入一个桟数据
用options.start 为parse函数 stack标签堆栈 添加一个标签
源码:
function handleStartTag(match) {
/*
* match = {
tagName: start[1], //标签名称
attrs: [], //标签属性集合
start: index, //开始标签的开始索引
match:index , //开始标签的 结束位置
unarySlash:'' //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空
};
* */
var tagName = match.tagName; //开始标签名称
var unarySlash = match.unarySlash; //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空
if (expectHTML) { //true
if (
lastTag === 'p' //上一个标签是p
/*
判断标签是否是
'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +
'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +
'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +
'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +
'title,tr,track'
*/
&& isNonPhrasingTag(tagName)
) {
//查找parseHTML的stack栈中与当前tagName标签名称相等的标签,
//调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除,
//为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点
parseEndTag(lastTag);
}
if (
canBeLeftOpenTag$$1(tagName) && //判断标签是否是 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'
lastTag === tagName //上一个标签和现在标签相同 <li><li> 编译成 <li></li> 但是这种情况是不会出现的 因为浏览器解析的时候会自动补全如果是<li>我是li标签<li> 浏览器自动解析成 <li>我是li标签</li><li> </li>
) {
//查找parseHTML的stack栈中与当前tagName标签名称相等的标签,
//调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除,
//为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点
parseEndTag(tagName);
}
}
var unary = isUnaryTag$$1(tagName) || //函数匹配标签是否是 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen, link,meta,param,source,track,wbr'
!!unarySlash; //如果是/> 则为真
var l = match.attrs.length;
var attrs = new Array(l); //数组属性对象转换正真正的数组对象
for (var i = 0; i < l; i++) {
var args = match.attrs[i]; //获取属性对象
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
//对FF bug进行黑客攻击:https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (
IS_REGEX_CAPTURING_BROKEN && //这个应该是 火狐浏览器私有 标志
args[0].indexOf('""') === -1
) {
if (args[3] === '') {
delete args[3];
}
if (args[4] === '') {
delete args[4];
}
if (args[5] === '') {
delete args[5];
}
}
var value = args[3] || args[4] || args[5] || '';
var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref // true chrome在a[href]中编码内容
: options.shouldDecodeNewlines; //flase //IE在属性值中编码换行,而其他浏览器则不会
attrs[i] = { //把数组对象属性值循环变成对象,这样可以过滤相同的属性
name: args[1], //属性名称
//属性值
value: decodeAttr(value, shouldDecodeNewlines) //替换html 中的特殊符号,转义成js解析的字符串,替换 把 <替换 < , > 替换 > , "替换 ", &替换 & , 替换\n ,	替换\t
};
}
if (!unary) { //如果不是单标签
// 为parseHTML 节点标签堆栈 插入一个桟数据
stack.push({ //标签堆栈
tag: tagName, //开始标签名称
lowerCasedTag: tagName.toLowerCase(), //变成小写记录标签
attrs: attrs //获取属性
});
//设置结束标签
lastTag = tagName;
}
//
if (options.start) {
//标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中
//获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性
//获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁
//校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性
// 标志当前的currentParent当前的 element
//为parse函数 stack标签堆栈 添加一个标签
options.start(
tagName, //标签名称
attrs, //标签属性
unary, // 如果不是单标签则为真
match.start, //开始标签的开始位置
match.end //开始标签的结束的位置
);
}
}
源码提问
vue中模板编译原理
模板(template)》 ast语法树(抽象语法树)》 codegen方法 ==》render函数 ==》createElement方法 ==》 Virtual Dom(虚拟dom)模板转语法树
模板结合数据,生成抽象语法树,描述html、js语法
语法树生成render函数
render函数
生成Virtual Dom(虚拟dom),描述真实的dom节点
渲染成真实dom