分析官网的流程图可知
new Vue时,进行一系列initxxx初始化工作后会根据选项el和template配置情况去做编译
即
情况1:挂载了el和template情况下直接去将template编译成渲染函数
情况2:挂载了el而没有挂载template情况下,将以el外部的html为template进行编译
情况3:没有挂载el的情况下,需要手动调用$mount函数挂载,这时再去判断有没有挂载template,在进行依据template有无继续接下来的编译工作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35Vue.prototype._init = function (options) {
var vm = this;
vm._uid = uid$3++;
var startTag, endTag;
vm._isVue = true; // 防止被监听的标识
//合并内置选项与传进来的选项
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
//进行一系列初始化
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm);
initState(vm);//对数据进行劫持
initProvide(vm);
callHook(vm, 'created');
if (vm.$options.el) { //判断有没有配置el,有则调用vm.$mount进入情况1和2,没有则进入情况3,等待手动调用vm.$mount
vm.$mount(vm.$options.el);//生成渲染函数后,给option挂上render,staticRenderFns属性
}
};
}
$mount挂载函数的巧妙设计
一开始定义的$mount仅用于运行时,功能是将编译好的渲染函数运行实现挂载组件1
2
3
4
5
6
7Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
之后会将运行时定义存放到另一个变量
方便定义之后包含编译步骤的挂载函数,
同时编译运行方案还会调用这个函数
这样在打包时,就可以方便的去掉进行编译过程的代码1
var mount = Vue.prototype.$mount;
在定义含有编译过程的挂载函数,将template编译成渲染函数
再调用运行时$mount(这时依托在mount变量上),进行挂载组件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && query(el);//query检查挂载点dom是否存在
//不要挂载到<body>元素或者<html>元素,
//因为挂载点的本意是组件挂载的占位,它将会被组件自身的模板替换掉,而<body>元素和<html>元素是不能被替换掉的。
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
var options = this.$options;
// 解析 template/el 转换成render函数
if (!options.render) {
var template = options.template;
if (template) { //配置了template
if (typeof template === 'string') {
//如果第一个字符是 #,那么会把该字符串作为 css 选择符
//去选中对应的元素,并把该元素的 innerHTML 作为模板
if (template.charAt(0) === '#') {
template = idToTemplate(template);
if (!template) {
warn(("Template element not found or is empty: " + (options.template)),this);
}
}
} else if (template.nodeType) {
//如果第一个字符不是 #,且template的类型是元素节点,
//那就用 template 自身的字符串值作为模板
template = template.innerHTML;
} else {{ warn('invalid template option:' + template, this);}
//否则报错不存在配置的template
return this
}
} else if (el) { //如果template选项不存在,那么使用el元素的outerHTML作为模板内容
template = getOuterHTML(el);
}
if (template) {
//编译生成静态结点渲染函数和动态结点渲染函数
var ref = compileToFunctions(template, {...参数下文有说明}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
//生成渲染函数后,给option挂上render,staticRenderFns渲染函数属性
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mount.call(this, el, hydrating) //调用运行时挂载函数
};
compileToFunctions
compileToFunctions 作用就两个
1.调用compile编译生成渲染函数字符串
2.将渲染函数字符串生成渲染函数返回1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39function createCompileToFunctionFn (compile) {
var cache = Object.create(null);
return function compileToFunctions (template, options,vm) {
options = extend({}, options);
var warn$$1 = options.warn || warn;
delete options.warn;
{ //检测 new Function() 是否可用,
//在某些极端情况下(如CSP限制)给一个有用的提示
try {new Function('return 1');} catch (e) {
if (e.toString().match(/unsafe-eval|CSP/)) {
warn$$1(...警告内容);
}
}
}
// 有缓存直接返回缓存,不用再进行编译一次,
// cache在闭包父域定义,相当于全局,
// 所以每次编译的结果都可以缓存起来
//options.delimiters这个选项是配置纯文本插入分隔符
//这里用来拼接做缓存key值
var key = options.delimiters
? String(options.delimiters) + template
: template;
if (cache[key]) {
return cache[key]
}
//调用 baseCompile编译模板并返回其生成的值
var compiled = compile(template, options);
//将字符串代码转换成函数
var res = {};
var fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
return (cache[key] = res)//缓存字符串模板的编译结果,防止重复编译,提升性能
}
}
try-catch语句如果有错误发生且错误的内容中
包含如 ‘unsafe-eval’ 或者 ‘CSP’ 这些字样的信息时就会给出一个警告
CSP全称Content Security Policy ,可以直接翻译为内容安全策略
就是为了页面内容安全而制定的一系列防护策略
通过CSP所约束的的规责指定可信的内容来源
(这里的内容可以指脚本、图片、iframe、fton、style等等可能的远程的资源)。
通过CSP协定,让WEB处于一个安全的运行环境中
如果你的策略比较严格,那么 new Function() 将会受到影响,从而不能够使用
但是将模板字符串编译成渲染函数又依赖 new Function(),所以解决方案有两个:
1、放宽你的CSP策略
2、预编译
一些变量参数说明
因为之后分析会用到,事先了解一下
变量之间关系
1 | var modules$1 = [ |
baseOptions
baseOptions包含编译器在运作的时候所需的基本配置选项。1
2
3
4
5
6
7
8
9
10
11
12var baseOptions = {
expectHTML: true,
modules: modules$1,
directives: directives$1,
isPreTag: isPreTag,
isUnaryTag: isUnaryTag,
mustUseProp: mustUseProp,
canBeLeftOpenTag: canBeLeftOpenTag,
isReservedTag: isReservedTag,
getTagNamespace: getTagNamespace,
staticKeys: genStaticKeys(modules$1)
};
第三个属性:directives 值是三个属性 (model、text、html) 的对象,且属性的值都是函数。
第四个属性:isPreTag 它是一个函数,其作用是通过给定的标签名字检查标签是否是 ‘pre’ 标签。
第五个属性:isUnaryTag 是一个通过makeMap生成的函数,该函数的作用是检测给定的标签是否是一元标签。
第六个属性:mustUseProp 它是一个函数,其作用是用来检测一个属性在标签中是否要使用props进行绑定。
第七个属性:canBeLeftOpenTag 一个使用makeMap生成的函数,它的作用是检测非一元标签,但却可以自己补全并闭合的标签。比如 div 标签是一个双标签,你需要这样使用<div> text </div>,但是你依然可以省略闭合标签,直接这样写:<div> text ,且浏览器会自动补全。但是有些标签你不可以这样用,它们是严格的双标签。
第八个属性:isReservedTag 它是一个函数,其作用是检查给定的标签是否是保留的标签。
第九个属性:getTagNamespace 它也是一个函数,其作用是获取元素(标签)的命名空间。
第十个属性:staticKeys 它的值是通过以 modules 为参数调用 genStaticKeys 函数的返回值得到的。 其作用是根据编译器选项的 modules 选项生成一个静态键字符串。
compileToFunctions传入的参数
实际调用时1
2
3
4
5
6
7var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,//更改纯文本插入符
comments: options.comments//设置为true时,保留且渲染模板HTML注释,false时舍弃注释
}, this);
参数说明
在我们innerHTML获取内容时,换行符和制表符分别被转换成了
和	。
在IE中,不仅仅是 a 标签的 href 属性值,任何属性值都存在这个问题
这就会影响Vue的编译器在对模板进行编译后的结果,为了避免这些问题Vue需要知道什么时候要做兼容工作,
如果 shouldDecodeNewlines 为 true,意味着 Vue 在编译模板的时候,要对属性值中的换行符或制表符做兼容处理。
而shouldDecodeNewlinesForHref为true 意味着Vue在编译模板的时候,要对a标签的 href 属性值中的换行符或制表符做兼容处理。
delimiters和comments都是 Vue 提供的选项,Vue实例的$options属性
delimiters代表纯文本插入分隔符
comments代表编译时是否保留注释,会在parse阶段进行判断
compiler编译器生成
compiler的爷爷createCompilerCreator,
会生成compiler的爸爸createCompiler
createCompiler会生成compiler1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54function createCompilerCreator (baseCompile) {
return function createCompiler (baseOptions) {
function compile (
template,
options
) {
//所有的配置选项最终都会挂载在这个finalOptions 对象上
var finalOptions = Object.create(baseOptions);
var errors = [];
var tips = [];
var warn = function (msg, range, tip) { (tip ? tips : errors).push(msg) };
//options为调用 compileToFunctions 函数时传递的选项参数。
//baseOptions理解为编译器的默认选项或者基本选项,options 是用来提供定制能力的扩展选项
if (options) {
// merge custom modules
//如果 options.modules 存在,就在 finalOptions 对象上添加 modules 属性,
//其值为 baseOptions.modules 和 options.modules 这两个数组合并后的新数组
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules);
}
// merge custom directives
//对于directives 采用原型链的原理实现扩展属性对基本属性的覆盖
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
);
}
// copy other options
for (var key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key];
}
}
}
finalOptions.warn = warn;
var compiled = baseCompile(template.trim(), finalOptions);//调用 baseCompile并返回其生成的值
{
detectErrors(compiled.ast, warn);
}
compiled.errors = errors;
compiled.tips = tips;
return compiled
}
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
传入实际编译器生成createCompiler1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
var ref$1 = createCompiler(baseOptions);
var compile = ref$1.compile;
var compileToFunctions = ref$1.compileToFunctions;
可见compileToFunctions也由createCompiler函数生成
createCompiler函数 由 createCompilerCreator函数在传入函数 baseCompile 条件下生成
在createCompiler函数中会声明compile函数,在compile中会调用 baseCompile并返回其生成的值
然后对 createCompileToFunctionFn 函数传入compile函数,返回一个函数即compileToFunctions
在compileToFunctions 函数中会调用compile函数并处理其返回值
最终生成render和staticRenderFns属性
所以可见在compileToFunctions还是在调用baseCompile来生成render和staticRenderFns属性,
现在摘出来主要逻辑1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29//base函数
function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
// compileToFunctions调用compile
// 实际调用 baseCompile得到其生成的值
var compiled = compile(template, options);
// 调用后处理
//将字符串转成函数
var res = {};
var fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
baseCompile 的主要核心代码。1
var ast =parse(template.trim(), options);
parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。1
optimize(ast, options);
optimize 的主要作用是标记 static 静态节点,
这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时
会有一个 patch 的过程, diff 算法会直接跳过静态节点
从而减少了比较的过程,优化了 patch 的性能1
var code =generate(ast, options);
生成目标平台所需的代码,将 AST 转化成 render function 字符串的过程
得到结果是 render 的字符串以及 staticRenderFns 字符串。
编译器的基本知识
编译器的技术分为词法分析、语法分析和语义分析三个部分
通常编译器的第一项工作叫做词法分析
就像阅读文章一样,文章是由一个个的中文单词组成的
程序处理也一样,只不过这里不叫单词,而是叫做“词法记号”,英文叫 Token1
<div id="app" v-if="ret">{{ message }}</div>
其实,我们可以手写程序制定一些规则来区分每个不同的 Token,
这些规则用“正则文法”表达,符合正则文法的表达式称为“正则表达式”
通过他们来完成具体的词法分析工作
编译器下一个阶段的工作是语法分析
词法分析是识别一个个的单词,而语法分析就是在词法分析的基础上识别出程序的语法结构
这个结构是一个树状结构,是计算机容易理解和执行的
程序也要定义良好的语法结构,它的语法分析过程,就是构造这么一棵树
一个程序就是一棵树,这棵树叫做抽象语法树(Abstract Syntax Tree,AST)
树的每个节点(子树)是一个语法单元,这个单元的构成规则就叫“语法”。
而我们这里要讲的 parser 就是在编译器对源代码处理的第一步
parser 把某种特定格式的文本(字符串)转换成某种数据结构的程序(对象)
并且这个数据结构是编译器能够理解的
因为编译器的后续步骤
比如上面提到的 句法分析,类型检查/推导,代码优化,代码生成 等等都依赖于该数据结构
注:parse & parser 这两个单词,不要混淆,parse 是动词,代表“解析”的过程,parser 是名词,代表“解析器”。
Vue 的编译器也不例外, 在词法分析阶段 Vue 会把字符串模板解析成一个个的令牌(token)
该令牌将用于句法分析阶段,在句法分析阶段会根据令牌生成一棵 AST
最后再根据该 AST生成最终的渲染函数,这样就完成了代码的生成
parse
进入baseCompile第一步就是调用parse,将HTML转成AST
标签配对原理
在parse函数和parseHTML函数中都会声明一个stack数组用来存放开始标签
parse函数中的stack在传给parseHTML函数中的回调函数中被调用
parseHTML函数中的stack在解析开始结束标签时被调用
其实两个stack装的结点相同
只是parse函数中stack存放的虚拟节点对象是对parseHTML函数中存放的对象做了进一步处理的结果
配对原理就是
每遇到一个开始标签就push进stack中
每遇到一个结尾标签就在stack中找到与该结尾标签tag相同的最近一次push进去的开始标签的位置
如果该位置不是在stack最后一位,说明之前解析到的开始标签中存在未闭合的标签,
则先将与结束标签相同的开始标签之后的包括开始标签调用parseHTML传入的回调进行闭合,
然后通过更改stack长度,直接将开始标签及以后的标签全部pop出stack
parseHTML传入的回调会处理parse函数中声明的stack数组
变量声明
parse函数中会声明一系列变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23warn$2 = options.warn || baseWarn; //打印警告信息
//一个编译器选项,其作用是通过给定的标签名字判断该标签是否是 pre 标签
platformIsPreTag = options.isPreTag || no;
//一个编译器选项,其作用是用来检测一个属性在标签中是否要使用元素对象原生的 prop 进行绑定
platformMustUseProp = options.mustUseProp || no;
//一个编译器选项,其作用是用来获取元素(标签)的命名空间
platformGetTagNamespace = options.getTagNamespace || no;
transforms = pluckModuleFunction(options.modules, 'transformNode');
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
delimiters = options.delimiters;//在创建 Vue 实例对象时所传递的 delimiters 选项
/**存储开始标签,与解析到的闭合标签对比,进行一元标签处理和双标签判断处理**/
var stack = [];
//options.preserveWhitespace 选项用来告诉编译器在编译 html 字符串时是否放弃标签之间的空格
//如果为 true 则代表放弃。
var preserveWhitespace = options.preserveWhitespace !== false;
var whitespaceOption = options.whitespace;
var currentParent;/**维护元素描述对象之间的父子关系,当前节点父元素**/
var inVPre = false;//标识当前解析的标签是否在拥有 v-pre 的标签之内
var inPre = false;//标识当前正在解析的标签是否在 <pre></pre> 标签之内
var root;/**存储最终生成的AST**/
parseHTML
parse 函数中会执行parseHTML函数,然后返回root,即AST
parseHTML 函数的作用就是用来做词法分析的
parse函数的作用则是在词法分析的基础上传入回调,做句法分析从而生成一棵AST
被调用时传入参数如下:1
2
3
4
5
6
7
8
9parseHTML(template, {
warn: warn$2,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,//对应配置项comments,true为保留模板中注释语句
outputSourceRange: options.outputSourceRange,
start
解析完一段模板的开始标签后的回调
例如:<div id=’jk’ class=’menu’ v-if=’isShow’><span>123</span></div>
解析完<div id=’jk’ class=’menu’ v-if=’isShow’>的回调函数1
2
3
4start: function start (tag, attrs, unary, start$1, end) {
// check namespace.
// inherit parent ns if there is one
var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);
ns 变量,代表标签的命名空间
platformGetTagNamespace 函数只会获取 svg 和 math 这两个标签的命名空间
但这两个标签的所有子标签都会继承它们两个的命名空间1
2
3
4
5// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs);
}
这里通过isIE来判断宿主环境是不是IE浏览器,并且前元素的命名空间为svg
如果是通过guardIESVGBug处理当前元素的属性数组attrs,并使用处理后的结果重新赋值给attrs变量
该问题是svg标签中渲染多余的属性,如下svg标签:
<svg xmlns:feature=”http://www.openplans.org/topp"></svg>
被渲染为:
<svg xmlns:NS1=”” NS1:xmlns:feature=”http://www.openplans.org/topp""></svg>
标签中多了 ‘xmlns:NS1=”” NS1:’ 这段字符串,解决办法也很简单,将整个多余的字符串去掉即可
而 guardIESVGBug 函数就是用来修改NS1:xmlns:feature属性并移除xmlns:NS1=”” 属性的
接着start函数会创建虚拟结点,返回一个描述该标签的对象结构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30var element = createASTElement(tag, attrs, currentParent); //下文有代码
if (ns) {
element.ns = ns;
}
{
if (options.outputSourceRange) { //处理虚拟节点占用原模板源码范围
element.start = start$1;
element.end = end;
element.rawAttrsMap = element.attrsList.reduce(function (cumulated, attr) {
cumulated[attr.name] = attr;
return cumulated
}, {});
}
//检查每个属性名是否符合规则
attrs.forEach(function (attr) {
if (invalidAttributeRE.test(attr.name)) {
warn$2('属性名不能包含空格引号<, >, / 或者 =',
{start: attr.start + attr.name.indexOf("["),end: attr.start + attr.name.length});
}
});
}
//是否是不允许的style或者script标签,而且不是在服务端渲染的情况下 isForbiddenTag下文有
if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
warn$2(...警告内容,{ start: element.start });
}
for (var i = 0; i < preTransforms.length; i++) {
element = preTransforms[i](element, options) || element;
}
preTransforms 是通过pluckModuleFunction 函数从options.modules 选项中筛选出名字为preTransformNode 函数所组成的数组
实际上 preTransforms 数组中只有一个 preTransformNode 函数该函数只用来处理 input 标签
inVpre 定义在parse函数中,一开始为false1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23if (!inVPre) {
//解析是否有配置v-pre指令,有的话删除
//v-pre指令可以跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。
//跳过大量没有指令的节点会加快编译。
processPre(element);
if (element.pre) {
inVPre = true;
}
}
if (platformIsPreTag(element.tag)) {
inPre = true;
}
if (inVPre) {
processRawAttrs(element);
} else if (!element.processed) { // 处理结构指令v-for,v-if,v-once
//processFor会将解析的结果挂到element上
//{for:in后面的数组表达式,alias:in前的表达式}
//解析结果只是对表达式进行解析,in/of前后配置的啥,解析出来
processFor(element);
processIf(element);//处理v-if,v-else,v-else-if的表达式
processOnce(element);//处理v-once指令
}
所有process 系列函数的作用都会先拿到解析的配置的值,
然后将v-属性从attrsList列表里面移除,只剩下纯html支持的属性
将针对v-*属性的解析结果挂到虚拟节点对象上
使这个对象能更加详细地描述一个元素1
2
3
4
5
6if (!root) {
root = element;//root节点没有挂载东西时,挂上,即找到了根节点
{
checkRootConstraints(root);
}
}
在编写 Vue 模板的时候会受到两种约束
首先模板必须有且仅有一个被渲染的根元素
第二不能使用 slot 标签和 template 标签作为模板的根元素
checkRootConstraints 函数内部首先通过判断el.tag === ‘slot’ || el.tag === ‘template’
来判断根元素是否是slot 标签或 template 标签
如果是则打印警告信息
接着会判断当前元素是否使用了 v-for 指令
因为v-for 指令会渲染多个节点所以根元素是不允许使用 v-for 指令的
1 | //这里的stack是parse函数中定义的stack |
小结
在解析完开始标签后,回调函数会做以下几件事
1.针对ie浏览器对svg和math标签属性做一个兼容性处理
2.生成虚拟dom对象
3.标识开始标签在原始代码所在的位置
4.检查属性命名是否规则
5.检查是不是不允许的script和style标签
6.处理配置的v-for,v-if,v-once属性
7.如果根结点还没有被挂载,则挂载到根结点root上,然后检查是否允许挂到根结点(template,slot,v-for)
8.如果是一元标签,解析结束;如果不是,更新当前父元素结点为该虚拟dom,并存入stack,用于配对结束标签
end
解析完一个结束标签时调用1
2
3
4
5
6
7
8
9
10
11
12
13end: function end (tag, start, end$1) {
var element = stack[stack.length - 1];
//每当遇到一个非一元标签的结束标签时,
//都会回退 currentParent 变量的值为之前的值
//这样我们就修正了当前正在解析的元素的父级元素
//在解析兄弟节点时是必须要这样做的
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end$1;
}
closeElement(element);
},
chars
解析到一段非标签内容后调用
比如1
<div></div>
拿到’hello‘这一段后调用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72chars: function chars (text, start, end) {
if (!currentParent) { //一些边界情况处理
{//没有根元素,只有文本。
if (text === template) {
warnOnce('Component template requires a root element, rather than just text.',{ start: start }
);
} else if ((text = text.trim())) { //文本在根元素之外
warnOnce(("text \"" + text + "\" outside root element will be ignored."),{ start: start }
);
}
}
return
}
//来解决 IE 浏览器中渲染 <textarea> 标签的 placeholder 属性时存在的 bug 的
if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text
) {return}
var children = currentParent.children;
//对空格空行进行删除,编译器只会保留那些 不存在于开始标签之后的空格。
if (inPre || text.trim()) {
//isTextTag判断是不是style或者script标签
//decodeHTMLCached 函数对文本进行解码。
text = isTextTag(currentParent) ? text : decodeHTMLCached(text);
} else if (!children.length) {
text = '';
} else if (whitespaceOption) {
if (whitespaceOption === 'condense') {
//代码压缩模式下,如果包含换行符,删除这样的空白结点,否则压缩成空格
text = lineBreakRE.test(text) ? '' : ' ';
} else {
text = ' ';
}
} else {
//preserveWhitespace 是一个布尔值代表着是否保留空格,只有它为真的情况下才会保留空格。
//但即使preserveWhitespace 常量的值为真,如果当前节点的父节点没有子元素则也不会保留空格,
text = preserveWhitespace ? ' ' : '';
}
if (text) {
if (!inPre && whitespaceOption === 'condense') {
//将连续的空格压缩为单个空格
text = text.replace(whitespaceRE$1, ' ');
}
var res;
var child;
//模板中存在的文本节点包含了 Vue 语法中的字面量表达式,
//而 parseText 函数的作用就是用来解析这段包含了字面量表达式的文本的
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
//含有表达式或者变量的情况
//res返回动态变量的描述
//例{{a}},返回:{expression: "_s(a)",tokens: [{@binding: "a"}]
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text: text
};
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
//纯常量字符串的情况
child = {
type: 3,
text: text
};
}
if (child) {
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
children.push(child);
}
}
},
小结
主要作用就是处理换行符,空格符,
然后按照常量字符串和动态字符串分别创建虚拟dom,
因为动态字符串需要进行一下解析
对虚拟dom挂载在源码中起止位置信息
将虚拟dom推入父节点children属性
comments
针对解析到的注释进行处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 comment: function comment (text, start, end) {
//只会处理被标签包裹的注释,因为根节点不允许有兄弟结点
if (currentParent) {
//普通文本节点与注释节点的元素描述对象的类型是一样的都是 3 ,
//不同的是注释节点的元素描述对象拥有 isComment 属性,并且该属性的值为 true,
//目的就是用来与普通文本节点作区分的。
var child = {
type: 3,
text: text,
isComment: true
};
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
currentParent.children.push(child);
}
}
});
真正的parseHTML
1 | //解析HTML |
parseStartTag
解析开始标签
使用unicode为基础的正则startTagOpen匹配html tag
unicode会用于比较标签名,组件名以及属性路径
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
var ncname = “[a-zA-Z_][\-\.0-9_a-zA-Z” + (unicodeRegExp.source) + “]*”;
var qnameCapture = “((?:” + ncname + “\:)?” + ncname + “)”;
var startTagOpen = new RegExp((“^<” + qnameCapture)); 标签开头正则
【正则表达式的source属性是其表达式的字符串形式】1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32function parseStartTag () {
var start = html.match(startTagOpen);
if (start) {
var match = {
tagName: start[1],
attrs: [],//用来存储将来被匹配到的属性
start: index //初始值为 index,是当前字符流读入位置在整个 html 字符串中的相对位置
};
advance(start[0].length);
var end, attr;
//startTagClose = /^\s*(\/?)>/;标签结束正则,只可以匹配>或者/>,或者之前有n个空格
//attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
//dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
//dynamicArgAttribute捕获标签上配置的属性push到节点的attr属性里面
//得到的结果是['key=val','key','=','val',]
//while条件是没有匹配到开始标签的结束部分,并且匹配到了开始标签中的属性,这个时候循环体将被执行,直到遇到开始标签的结束部分为止
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
attr.start = index;
advance(attr[0].length);
attr.end = index;
match.attrs.push(attr);
}
<!-- 即使匹配到了开始标签的开始部分以及属性部分但是却没有匹配到开始标签的结束部分,这说明这根本就不是一个开始标签。所以只有当变量end存在,即匹配到了开始标签的结束部分时,才能说明这是一个完整的开始标签 -->
if (end) {
match.unarySlash = end[1];//是否是单标签,双标签该值为空字符串,单标签该值为'/'
advance(end[0].length);//位移一个标签结束符'>'或者'/>'加前面n个空格的距离
match.end = index;
return match//一个html双标签的开头标签或者单标签解析完毕
}
<!-- 只有当变量end存在时,即能够确定确实解析到了一个开始标签的时候parseStartTag函数才会有返回值,并且返回值是match对象,其他情况下parseStartTag全部返回undefined -->
}
}
主要作用是
1.正则拿到开始标签名
2.while循环拿到属性的key-value对象集合,对象中包含该属性在源码中所占的起止位置
3.标识是否为一元标签,返回解析结果
handleStartTag
处理开始标签的解析结果,对上一步结果做进一步处理1
2
3
4
5function handleStartTag (match) {
var tagName = match.tagName;
var unarySlash = match.unarySlash;
//baseOptions中默认设置为true
if (expectHTML) {
如果最近一次遇到的开始标签是 p 标签
并且当前正在解析的开始标签必须不能是段落式内容(Phrasing content)模型
每一个 html 元素都拥有一个或多个内容模型(content model)
其中p 标签本身的内容模型是流式内容(Flow content)
并且 p 标签的特性是只允许包含段落式内容(Phrasing content)
所以条件成立的情况如下:<p><h1></h1></p>
在解析上面这段 html 字符串的时候,首先遇到p标签的开始标签,此时lastTag被设置为 p
紧接着会遇到 h1 标签的开始标签,由于 h2 标签的内容模型属于非段落式内容(Phrasing content)模型
所以会立即调用 parseEndTag(lastTag) 函数闭合 p 标签,此时由于强行插入了</p> 标签
所以解析后的字符串将变为如下内容:<p></p><h2></h2></p>
继续解析该字符串,会遇到 <h2></h2>标签并正常解析之
最后解析器会遇到一个单独的p 标签的结束标签,即:</p>
这个时候就回到了我们前面讲过的,当解析器遇到 p 标签或者 br 标签的结束标签时会补全他们
最终<p><h2></h2></p>
这段 html 字符串将被解析为:<p></p><h2></h2><p></p>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag);//处理结束标签
}
//当前正在解析的标签是一个可以省略结束标签的标签,并且与上一次解析到的开始标签相同
//例<p>max<p>kaixin
//当解析到一个p标签的开始标签并且下一次遇到的标签也是p标签的开始标签时,会立即关闭第二个p标签。
//即调用:parseEndTag(tagName) 函数,然后由于第一个p标签缺少闭合标签所以会Vue会给你一个警告。
if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
parseEndTag(tagName);
}
}
var unary = isUnaryTag$$1(tagName) || !!unarySlash;//是否是一元标签
var l = match.attrs.length;
var attrs = new Array(l);//整理属性
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
var value = args[3] || args[4] || args[5] || '';
//对于a标签是否插入换行编码
var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
? options.shouldDecodeNewlinesForHref
: options.shouldDecodeNewlines;
attrs[i] = {
name: args[1],
//decodeAttr 用是对属性值中所包含的 html 实体进行解码,将其转换为实体对应的字符。
value: decodeAttr(value, shouldDecodeNewlines)
};
if (options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length;
attrs[i].end = args.end;
}
}
//如果开始标签是非一元标签,则将该开始标签的信息入栈,
//即push到stack数组中,并将lastTag的值设置为该标签名。
if (!unary) {//这里的stack是parseHTML中定义的stack
stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end });
lastTag = tagName;//lastTag 所存储的标签名字始终保存着 stack 栈顶的元素
}
if (options.start) { 调用配置的开头标签处理函数,是parse函数中传输parseHTML函数的参数
options.start(tagName, attrs, unary, match.start, match.end);
}
}
作用如下:
1.处理p标签闭合情况,以及a标签属性值是否插入换行的处理
2.进一步处理属性值结合,将正则得到的数组转成key-value对象
3.将节点push到stack中,进行闭合标签配对,设置lastTag为最近一次的开始标签
4.调用开始标签回调start
createASTElement
创建虚拟节点1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function createASTElement (
tag,
attrs,
parent
) {
return {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent: parent,
children: []
}
}
parseEndTag
处理结束标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 function parseEndTag (tagName, start, end) {
var pos, lowerCasedTagName;
//当 start 和 end 不存在时,将这两个变量的值设置为当前字符流的读入位置,即index
//在handleStartTag 函数中调用时会被这样这样调用
if (start == null) { start = index; }
if (end == null) { end = index; }
//找到最近的相同类型的开始标签的位置
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {//调用 parseEndTag() 函数时不传递任何参数,就处理stack栈中剩余未处理的标签,
pos = 0;
}
//删除最近开始标签 以后的开始标签(可能是一元标签)
if (pos >= 0) {//检测是否缺少闭合标签
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (i > pos || !tagName && options.warn) {
//如果发现 stack 数组中存在索引大于 pos 的元素,那么一定有元素是缺少闭合标签的
options.warn(("tag <" + (stack[i].tag) + "> has no matching end tag."),{ start: stack[i].start, end: stack[i].end });
}
if (options.end) {
options.end(stack[i].tag, start, end);//回调parse函数调用时传递进来的回调函数
}
}
//从stack中删除开标签
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;//将lastTag重制为stack栈顶元素
} else if (lowerCasedTagName === 'br') {
//为了与浏览器的行为相同,专门处理br与p的结束标签,即:</br> 和</p>。
//</br> 标签被正常解析为 <br> 标签,而</p>标签被正常解析为 <p></p>
if (options.start) {
options.start(tagName, [], true, start, end);
}
} else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
}
主要作用是
1.利用stack进行配对,处理stack和lastTag
2.特殊处理br和p标签
3.调用end回调函数处理父子关系
closeElement
1 | function closeElement(element) { |
使用 v-if 或 v-else-if 或 v-else 创建多个根结点时
v-else-if 或 v-else创建的虚拟节点放到v-if节点的ifconditions属性下面【待验证】1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 if (!stack.length && element !== root) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
{
checkRootConstraints(element);
}
//给root.ifconditions属性push if,elseif,else等条件模板下的虚拟dom对象
addIfCondition(root, {
exp: element.elseif,
block: element
});
} else {
warnOnce(
"Component template should contain exactly one root element. " +
"If you are using v-if on multiple elements, " +
"use v-else-if to chain them instead.",
{ start: element.start }
);
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else {
if (element.slotScope) {
var name = element.slotTarget || '"default"'
;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
}
//这样就建立了元素描述对象间的父子级关系。
currentParent.children.push(element);
element.parent = currentParent;
}
}
element.children = element.children.filter(function (c) { return !(c).slotScope; });
trimEndingWhitespace(element);
if (element.pre) {
inVPre = false;
}
if (platformIsPreTag(element.tag)) {
inPre = false;
}
for (var i = 0; i < postTransforms.length; i++) {
postTransforms[i](element, options);
}
}
主要作用是
1.处理条件渲染节点关系
2.建立父子结点关系
isForbiddenTag
1 | function isForbiddenTag(el) { |
optimize
通过两个递归函数给结点挂载static和staticRoot两个属性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86function optimize (root, options) {
if (!root) { return }
isStaticKey = genStaticKeysCached(options.staticKeys || '');
isPlatformReservedTag = options.isReservedTag || no;
// first pass: mark all non-static nodes.
markStatic$1(root);
// second pass: mark static roots.
markStaticRoots(root, false);
}
//判断结点是否为静态结点
function isStatic (node) {
if (node.type === 2) { // expression
return false
}
if (node.type === 3) { // text
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)
))
}
function markStatic$1 (node) {
node.static = isStatic(node);
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
if (
!isPlatformReservedTag(node.tag) &&
node.tag !== 'slot' &&
node.attrsMap['inline-template'] == null
) {
return
}
for (var i = 0, l = node.children.length; i < l; i++) {
var child = node.children[i];
markStatic$1(child);
if (!child.static) {//子节点一但为动态结点,则父节点为动态结点
node.static = false;
}
}
if (node.ifConditions) { //处理条件渲染节点是否为静态结点
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
var block = node.ifConditions[i$1].block;
markStatic$1(block);
if (!block.static) {
node.static = false;
}
}
}
}
}
function markStaticRoots (node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
//对只含有一个纯文本结点的node添加staticRoot = false
//否则staticRoot = true
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true;
return
} else {
node.staticRoot = false;
}
if (node.children) {
for (var i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
markStaticRoots(node.ifConditions[i$1].block, isInFor);
}
}
}
}
主要处理思想就是
子节点一旦为动态结点,则父节点为动态结点, static = false
只含有一个纯文本结点的node,没有其他子节点,staticRoot = false
generate
1 | function generate ( |
CodegenState
1 | var CodegenState = function CodegenState (options) { |
genElement
1 | function genElement (el, state) { |
genData$2
genData$2 生成el标签上配置的内容1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83function genData$2 (el, state) {
var data = '{';
//优先处理指令,因为指令可能会影响其他属性
//很重要这个步骤很重要,会给input等存在交互的标签因为v-model绑定响应事件
//genDirectives下文有详解
var dirs = genDirectives(el, state);
if (dirs) { data += dirs + ','; }
// key
if (el.key) {
data += "key:" + (el.key) + ",";
}
// ref
if (el.ref) {
data += "ref:" + (el.ref) + ",";
}
if (el.refInFor) {
data += "refInFor:true,";
}
// pre
if (el.pre) {
data += "pre:true,";
}
// record original tag name for components using "is" attribute
if (el.component) {
data += "tag:\"" + (el.tag) + "\",";
}
// module data generation functions
for (var i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el);
}
// attributes
if (el.attrs) {
data += "attrs:" + (genProps(el.attrs)) + ",";
}
// DOM props
if (el.props) {
data += "domProps:" + (genProps(el.props)) + ",";
}
// event handlers
if (el.events) {
data += (genHandlers(el.events, false)) + ",";
}
if (el.nativeEvents) {
data += (genHandlers(el.nativeEvents, true)) + ",";
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += "slot:" + (el.slotTarget) + ",";
}
// scoped slots
if (el.scopedSlots) {
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
}
// component v-model
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
// inline-template
if (el.inlineTemplate) {
var inlineTemplate = genInlineTemplate(el, state);
if (inlineTemplate) {
data += inlineTemplate + ",";
}
}
data = data.replace(/,$/, '') + '}';
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")";
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data);
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data);
}
return data
}
genDirectives
1 | function genDirectives (el, state) { |
el.directives
el.directives属性在closeElement—>processElement—>processAttrs
函数调用流程中被挂载1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42function processAttrs (el) {
var list = el.attrsList;
var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name;
value = list[i].value;
if (dirRE.test(name)) { // var dirRE = /^v-|^@|^:|^#/;
el.hasBindings = true;
if (bindRE.test(name)) { ...// v-bind
} else if (onRE.test(name)) {... // v-on
} else { // normal directives
name = name.replace(dirRE, '');
var argMatch = name.match(argRE);
var arg = argMatch && argMatch[1];
isDynamic = false;
if (arg) {
name = name.slice(0, -(arg.length + 1));
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1);
isDynamic = true;
}
}
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]);
if (name === 'model') {
checkForAliasModel(el, value);
}
}
} else {....}
}
}
function addDirective (el,name,rawName,value,arg,isDynamicArg,modifiers,range) {
(el.directives || (el.directives = [])).push(rangeSetItem({
name: name,
rawName: rawName,
value: value,
arg: arg,
isDynamicArg: isDynamicArg,
modifiers: modifiers
}, range));
el.plain = false;
}
v-model
假如现在标签为<input v-model=’a’ >
那么现在的el.directives=[{name: “model”,rawName: “v-model”,value: “a”,isDynamicArg: false,start: 106,end: 117,modifiers:undefined}]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22var gen = state.directives[dir.name];
--->
state = new CodegenState()
--->
var CodegenState = function CodegenState (options) {
this.directives = extend(extend({}, baseDirectives), options.directives);
}
--->
options (finalOptions = Object.create(baseOptions);--->baseCompile()--->generate())
--->
var baseOptions = {
directives: directives$1,
}
--->
directives$1 = {
model: model,
text: text,
html: html
};
--->>>>>>>>>>
state.directives = directives$1
state.directives[el.directives[0].name] ===>model
针对不同标签处理双向绑定,添加相应事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28function model (el,dir,_warn) {
warn$1 = _warn;
var value = dir.value;
var modifiers = dir.modifiers;
var tag = el.tag;
var type = el.attrsMap.type;
{if (tag === 'input' && type === 'file') {warn$1(....);}}
if (el.component) {
genComponentModel(el, value, modifiers);
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers);
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers);
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else {
warn$1(...);
}
return true
}
针对input 的v-model绑定input事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37function genDefaultModel (
el,
value,
modifiers
) {
var type = el.attrsMap.type;
var ref = modifiers || {};
var lazy = ref.lazy;
var number = ref.number;
var trim = ref.trim;
var needCompositionGuard = !lazy && type !== 'range';
var event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'; //默认input事件触发双向绑定
var valueExpression = '$event.target.value';
if (trim) {
valueExpression = "$event.target.value.trim()";
}
if (number) {
valueExpression = "_n(" + valueExpression + ")";
}
//input事件响应函数字符串形式
var code = genAssignmentCode(value, valueExpression);
if (needCompositionGuard) {
code = "if($event.target.composing)return;" + code;
}
addProp(el, 'value', ("(" + value + ")"));
addHandler(el, event, code, null, true); //addHandler函数给el绑定events属性
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()');
}
}
$event.target.composing是vue自己添加的属性,用于控制在输入中日韩这类语言时
确定一个字或词输入完成后再更新绑定值,而不是每次输入一个字母就更新一次绑定值
genChildren
1 | var children = el.inlineTemplate ? null : genChildren(el, state, true); |
genElement函数中组建完本身标签内容后,会继续组建子标签
1 | function genChildren ( |
genChildren函数会循环父标签的的children属性生成每个子标签的渲染函数字符串,
以数组字符串形式返回给父标签的_c函数,用作参数
小结
假如html内容为:1
2
3
4<div id = 'app' class='hello'>
<p >{{a+'...'+arr[0]}}</p>
<input class="kk" key='kkk' ref='hellodiv' v-model='a' @input='search'/>
</div>
那么generate函数返回的render值如下1
"with(this){return _c('div',{staticClass:"hello",attrs:{"id":"app"}},[_c('p',[_v(_s(a+'...'+arr[0]))]),_v(" "),_c('input',{directives:[{name:"model",rawName:"v-model",value:(a),expression:"a"}],key:"kkk",ref:"hellodiv",staticClass:"kk",domProps:{"value":(a)},on:{"input":[function($event){if($event.target.composing)return;a=$event.target.value},search]}})])}"
可见,generate函数只是将parse和optimize处理得到的标签描述对象ast整理成函数参数
交由不同类型的可以生成vnode对象的处理函数,整个函数,由字符串拼接形成
整个函数字符串会在compileToFunctions函数中调用createFunction转化为真正的函数对象1
2
3
4
5
6
7
8function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err: err, code: code });
return noop
}
}
渲染函数字符串中的函数缩写
1 | function installRenderHelpers (target) { |
_c
1 | vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); }; |
注意createElement函数只是将一个结点转成vnode对象
上面generate得到的render字符串的逻辑是生成一个根结点,
里面传递的子节点数组也是调用createElement函数生成的一个个vnode
所以这里调用render生成vnode函数时,没有递归逻辑,
而是每个结点自己去调createElement生成自己的vnode
_s
1 | function toString (val) { |
_v
1 | function createTextVNode (val) { |
_l
循环组建结点的创建函数字符串1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39function renderList (
val,
render
) {
var ret, i, l, keys, key;
if (Array.isArray(val) || typeof val === 'string') {
ret = new Array(val.length);
for (i = 0, l = val.length; i < l; i++) {
ret[i] = render(val[i], i);
}
} else if (typeof val === 'number') {
ret = new Array(val);
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i);
}
} else if (isObject(val)) {
if (hasSymbol && val[Symbol.iterator]) {
ret = [];
var iterator = val[Symbol.iterator]();//获取对象生成器
var result = iterator.next();
while (!result.done) {
ret.push(render(result.value, ret.length));
result = iterator.next();
}
} else {
keys = Object.keys(val);
ret = new Array(keys.length);
for (i = 0, l = keys.length; i < l; i++) {
key = keys[i];
ret[i] = render(val[key], key, i);
}
}
}
if (!isDef(ret)) {
ret = [];
}
(ret)._isVList = true;
return ret
}
mountComponent
1 | function mountComponent (vm,el,hydrating) { |
watch
watcher 构造函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
//...省略不重要代码
this.expression = expOrFn.toString(); //updateComponent字符串化
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn; //指向updateComponent
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(....);
}
}
this.value = this.lazy
? undefined
: this.get();//里面会调用this.getter
};
Watcher.prototype.get = function get () {
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);//就是在调用updateComponent
} catch (e) {...
} finally {...
}
return value //最终返回value
};
可知在new Watcher过程中构造函数会将updateComponent挂到getter属性上,
在通过get属性进行调用
再来看一下updateComponent怎么定义1
2
3updateComponent = function () {
vm._update(vm._render(), hydrating);
};
updateComponent要调用vue的_update函数
注意这里调用了vm._render(),生成了vnode,当做参数传递了进去
vue的_update函数是在构建vue构造函数时
调用lifecycleMixin函数被挂载1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25function lifecycleMixin (Vue) {
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;//vm.$el初次渲染时为挂载点所在html,即最原始的,parse处理的html模板
var prevVnode = vm._vnode;//这里初次渲染时为null
var restoreActiveInstance = setActiveInstance(vm);//给全局变量activeInstance赋值为vm
vm._vnode = vnode;//给vue挂上得到的vnode
if (!prevVnode) {//初次渲染走这里
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {//更新时走这里
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();//处理完后更新activeInstance为null
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null;
}
if (vm.$el) {
vm.$el.__vue__ = vm;
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el;
}
};
patch
patch函数直接在生成vue构造函数时被挂载1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82Vue.prototype.__patch__ = inBrowser ? patch : noop;
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
function createPatchFunction (backend) {
var i, j;
var cbs = {};
var modules = backend.modules;
var nodeOps = backend.nodeOps;
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
}
//省略若干函数声明......
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); }
return
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {//更新时,详见更新节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {//初次渲染时oldVnode为原始html结点
if (isRealElement) {
//是真实的DOM而且是服务端渲染的结果
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode
} else {
warn(....);
}
}
//不是服务端渲染或者合成失败,就创建空的vnode代替老结点
oldVnode = emptyNodeAt(oldVnode);
//oldVnode:{elm:指向原始未编译前的真实html结点}
}
// 替换正存在的结点
var oldElm = oldVnode.elm;//原来结点
var parentElm = nodeOps.parentNode(oldElm);//原节点的父节点
// 创建dom结点
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
//创建dom结束后,body里面会有两个挂载点,一个编译前html,一个编译后html
//所以新老结点更新的思路是,在父元素在新增新结点子元素,然后将老结点删除
// 递归更新父占位符节点元素
if (isDef(vnode.parent)) {....}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);//从父元素中删除老结点
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm //返回真正的dom结点
}
}
createElm
生成真正的dom结点挂载在各自的vnode的elm属性上,并在生成的过程中建立父子关系1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {//预渲染时克隆源节点,不再重新生成
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
if (data && data.pre) {creatingElmInVPre++;}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) { warn(...);}
}
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);//没有命名空间直接生成真正html标签挂到elm属性上
setScope(vnode);
{
createChildren(vnode, children, insertedVnodeQueue); //构建子节点
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);//处理标签上属性(id,class....)
}
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {//处理注释结点
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {//处理纯静态文本结点
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
----->nodeOps.createElement----->
function createElement$1 (tagName, vnode) {
var elm = document.createElement(tagName);
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple');
}
return elm
}
---->nodeOps.createTextNode---->
function createTextNode (text) {
return document.createTextNode(text)
}
---->nodeOps.createComment---->
function createComment (text) {
return document.createComment(text)
}
createChildren
构建子节点1
2
3
4
5
6
7
8
9
10
11
12function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
{
checkDuplicateKeys(children);//检查子节点是否有重复key值
}
for (var i = 0; i < children.length; ++i) { //递归调用createElm创建子节点
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
insert
构建父子dom结点关系1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 function insert (parent, elm, ref$$1) {
if (isDef(parent)) {
if (isDef(ref$$1)) {
if (nodeOps.parentNode(ref$$1) === parent) {
nodeOps.insertBefore(parent, elm, ref$$1);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
---->nodeOps.insertBefore----->纯dom操作
function insertBefore (parentNode, newNode, referenceNode) {
parentNode.insertBefore(newNode, referenceNode);
}
----->nodeOps.appendChild---->纯dom操作
function appendChild (node, child) {
node.appendChild(child);
}
----->nodeOps.removeChild---->纯dom操作
function removeChild (node, child) {
node.removeChild(child);
}
小结
mountComponent主要做这样几件事
1.执行beforeMount钩子函数
2.定义updateComponent,并在new watcher的时候调用
3.调用updateComponent函数时会调用vm_render函数生成vnode对象,这个过程ast中变量会拿到实际的值
4.updateComponent函数会将vnode传递给vm_update,vm_update在生成vue构造函数时被挂载
5.vm_update会调用vm__patch__,vm__patch__会调用patch函数
6.patch函数会根据传入参数特征处理第一次挂载还是更新操作
7.patch函数会调用createElement递归将vnode对象里面的结点转成真正的dom结点挂载在对应的elm属性上,同时会处理好父子关系以及标签上class,id之类的属性
8.因为处理最外层结点时,即挂载点结点时,获取的父节点与未编译的挂载点是同一个,所以要把未编译的挂载点调用removeVnodes移除,
更新节点
patchVnode
负责具体的结点更新工作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly
) {
if (oldVnode === vnode) {
return
}
//...省略若干边界/特殊条件处理
var i;
var data = vnode.data;
//组件才会有hook属性
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode);
}
var oldCh = oldVnode.children;
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {//处理标签上属性更新
for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
}
if (isUndef(vnode.text)) {//如果新结点不是纯文本结点
if (isDef(oldCh) && isDef(ch)) {//如果新老结点都有子节点
if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) { //如果新结点有子节点,老结点没有,直接新增
{
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) { //如果老结点有子节点,新结点没有,直接删除
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {//如果是文本结点,清空
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) { //如果新结点也是纯文本结点,则更新内容
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
}
}
updateChildren
负责判断子节点是否需要更新,递归调用patchVnode进行更新操作
主要思路是以新结点为标尺,对比老结点,在老结点树上面移动或者新增删除节点1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91 function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh);
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
//因为老node的开始节点竟然与新node的结束节点一致,说明老node的开始节点需要移动到当前的老node结束节点的后一位
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
//老node的结束节点移动到老node的开始节点之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
//判断老结点数组中是否有当前这个新结点
if (isUndef(idxInOld)) { // 如果没有在当前老结点前面新增这个新结点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
//如果有且key相同,对比之后,移动到当前老结点前面,保持新老结点位置相同
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
如果新老结点不是同一个key,则新增新结点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
//此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把他们全部插进before的后边,before很多时候是为null的
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
//newStartIdx > newEndIdx,可以认为newCh先遍历完。
//此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}