My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

vue 源码学习二【编译器】

发表于 2020-02-05

Vue声明周期
分析官网的流程图可知
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
35
Vue.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
7
Vue.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
51
Vue.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
39
function 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var modules$1 = [
klass$1,
style$1,
model$1
];

var klass$1 = {
staticKeys: ['staticClass'],
transformNode: transformNode,
genData: genData
};

var style$1 = {
staticKeys: ['staticStyle'],
transformNode: transformNode$1,
genData: genData$1
};

var model$1 = {
preTransformNode: preTransformNode
};

baseOptions

baseOptions包含编译器在运作的时候所需的基本配置选项。

1
2
3
4
5
6
7
8
9
10
11
12
var 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
7
var ref = compileToFunctions(template, {
outputSourceRange: "development" !== 'production',
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,//更改纯文本插入符
comments: options.comments//设置为true时,保留且渲染模板HTML注释,false时舍弃注释
}, this);

参数说明
在我们innerHTML获取内容时,换行符和制表符分别被转换成了&#10和&#9。
在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会生成compiler

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
function 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)
}
}
}

传入实际编译器生成createCompiler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var 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 字符串。

编译器的基本知识

编译器的技术分为词法分析、语法分析和语义分析三个部分
通常编译器的第一项工作叫做词法分析
就像阅读文章一样,文章是由一个个的中文单词组成的
程序处理也一样,只不过这里不叫单词,而是叫做“词法记号”,英文叫 Token

1
<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
23
warn$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
9
parseHTML(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
4
start: 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
30
var 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函数中,一开始为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (!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
6
if (!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
2
3
4
5
6
7
8
9
  //这里的stack是parse函数中定义的stack
if (!unary) {//非一元标签
currentParent = element;//确定当前节点即接下来标签的父节点
stack.push(element);//存入栈中,到时供结束标签判断,前一个标签是一元标签还是最近的开始标签
} else {
//如果是一元的话,解析结束,调用结束标签函数,该函数在parseHTML函数调用前,parse函数中定义
closeElement(element);//会建立父子结点关系
}
},

小结

在解析完开始标签后,回调函数会做以下几件事
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
13
end: 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
72
chars: 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
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//解析HTML
function parseHTML (html, options) {
var stack = [];//在 while 循环中处理 html 字符流的时候每当遇到一个非单标签,都会将该开始标签 push 到该数组。它的作用模板中 DOM 结构规范性的检测
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no; //用来检测一个标签是否是一元标签
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;//用来检测一个标签是否是可以省略闭合标签的非一元标签
var index = 0; //标识着当前字符流的读入位置
//last 存储剩余还未编译的 html 字符串
//lastTag 始终存储着位于 stack 栈顶的元素
var last, lastTag;
while (html) {
last = html; //HTML为待解析的HTML片段,last 保留最原始的片段,advance会在解析过程中切割html
// 确保没有在script/style纯文本标签里面
//var isPlainTextElement = makeMap('script,style,textarea', true);
if (!lastTag || !isPlainTextElement(lastTag)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) { //如果以<开头
// comment = /^<!\--/;如果是以注释开头的
if (comment.test(html)) {
var commentEnd = html.indexOf('-->'); //拿到注释结尾的位置
if (commentEnd >= 0) { //如果有完整注释
//根据new vue时传递进来的comments选项值判断是否保留模板中注释
if (options.shouldKeepComment) {
//options.comment在parse函数调用parseHTML时传递进来,即回调
//该函数会将html.substring拿到注释的内容包装成 type:3的节点,
//然后push进其父节点的children属性
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
}
advance(commentEnd + 3);//裁剪html模板,位移index的值
continue
}
}

// 不处理条件注释,直接略过
if (conditionalComment.test(html)) {
var conditionalEnd = html.indexOf(']>');

if (conditionalEnd >= 0) {
advance(conditionalEnd + 2);
continue
}
}

// var doctype = /^<!DOCTYPE [^>]+>/i;
//不处理<!DOCTYPE>标签
var doctypeMatch = html.match(doctype);
if (doctypeMatch) {
advance(doctypeMatch[0].length);
continue
}

//var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
//qnameCapture详见parseStartTag函数
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[1], curIndex, index);//处理stack中开标签/一元标签
continue
}

//如果是正常编写的标签(由<>包裹)返回一个描述开始标签的对象
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1);
}
continue
}
}


//处理html非尖括号开头情况,即解析到文本内容的时候
var text = (void 0), rest = (void 0), next = (void 0);
if (textEnd >= 0) { //此时html非尖括号开头,但剩下的里面还含有尖括号
rest = html.slice(textEnd);//截断尖括号以前的内容,拿到剩下的内容
while ( 第一个尖括号不是开始标签也不是结束或者评论标签
!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)
) {
// 处理文本里的尖括号,直到找到是标签的尖括号的位置,即文本结束的位置
next = rest.indexOf('<', 1);//从第二个字符开始找剩下html里面的第一个尖括号
if (next < 0) { break } //如果没有,直接结束
textEnd += next;//如果有的话,重制text结束位置为该尖括号位置
rest = html.slice(textEnd);//截取html,在判断尖括号是否是标签或者注释
}//while循环的作用是找出非标签内容
text = html.substring(0, textEnd);//最终得到非标签内容
}

if (textEnd < 0) { //html里面没尖括号了
text = html;
}

if (text) {
advance(text.length);
}

if (options.chars && text) {
options.chars(text, index - text.length, index);//调用回调,处理文本结点
}
} else {
//处理textarea内容和结束标签
var endTagLength = 0;
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
//直接通过end标签拿到标签之间的文本内容,从避免分析中间的尖括号内容,直接保留
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');
}
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 (!stack.length && options.warn) {
options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length });
}
break
}
}

// Clean up any remaining tags
parseEndTag();

function advance (n) {
index += n;
html = html.substring(n);
}

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
32
function 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
5
function 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
15
function 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
2
3
4
5
6
function closeElement(element) {
trimEndingWhitespace(element);
if (!inVPre && !element.processed) {
//processElement会继续处理element上的属性key,is,inline-template,ref,v-bind,v-on等等,会挂载plain属性供generate创建render函数时使用
element = processElement(element, options);
}

使用 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
2
3
4
5
6
7
8
9
function isForbiddenTag(el) {
return (
el.tag === 'style' ||
(el.tag === 'script' && (
!el.attrsMap.type ||
el.attrsMap.type === 'text/javascript'
))
)
}

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
86
function 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
2
3
4
5
6
7
8
9
10
11
function generate (
ast,
options
) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}

CodegenState

1
2
3
4
5
6
7
8
9
10
11
12
var CodegenState = function CodegenState (options) {
this.options = options;
this.warn = options.warn || baseWarn;
this.transforms = pluckModuleFunction(options.modules, 'transformCode');
this.dataGenFns = pluckModuleFunction(options.modules, 'genData');
this.directives = extend(extend({}, baseDirectives), options.directives);
var isReservedTag = options.isReservedTag || no;
this.maybeComponent = function (el) { return !!el.component || !isReservedTag(el.tag); };
this.onceId = 0;
this.staticRenderFns = [];
this.pre = false;
};

genElement

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
function genElement (el, state) {
if (el.parent) {
el.pre = el.pre || el.parent.pre;
}

if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
var code;
if (el.component) {
code = genComponent(elp.comonent, el, state);
} else {//普通标签情况
var data;
//在processElement函数中
// el.plain = !el.key &&!el.scopedSlots && !el.attrsList.length
// v-pre 可以跳过编译
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);//拼接el标签上配置的内容
}

var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
}

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
83
function 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function genDirectives (el, state) {
var dirs = el.directives;
if (!dirs) { return }
var res = 'directives:[';
var hasRuntime = false;
var i, l, dir, needRuntime;
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i];
needRuntime = true;
var gen = state.directives[dir.name];
if (gen) {
// 处理ast里面的编译时指令
// 如果还需要运行时副本,则返回true
needRuntime = !!gen(el, dir, state.warn);
}
if (needRuntime) {
hasRuntime = true;
res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
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
42
function 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
22
var 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
28
function 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
37
function 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
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
function genChildren (
el,
state,
checkSkip,
altGenElement,
altGenNode
) {
var children = el.children;
if (children.length) {
var el$1 = children[0];
// optimize single v-for
if (children.length === 1 &&
el$1.for &&
el$1.tag !== 'template' &&
el$1.tag !== 'slot'
) {
var normalizationType = checkSkip
? state.maybeComponent(el$1) ? ",1" : ",0"
: "";
return ("" + ((altGenElement || genElement)(el$1, state)) + normalizationType)
}
var normalizationType$1 = checkSkip
? getNormalizationType(children, state.maybeComponent)
: 0;
var gen = altGenNode || genNode;
return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
}
}
function genNode (node, state) {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}

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
8
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err: err, code: code });
return noop
}
}

渲染函数字符串中的函数缩写

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
function installRenderHelpers (target) {
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;//v-for
target._t = renderSlot;
target._q = looseEqual;
target._i = looseIndexOf;
target._m = renderStatic;
target._f = resolveFilter;
target._k = checkKeyCodes;
target._b = bindObjectProps;
target._v = createTextVNode;
target._e = createEmptyVNode;
target._u = resolveScopedSlots;
target._g = bindObjectListeners;
target._d = bindDynamicKeys;
target._p = prependModifier;
}
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
});
installRenderHelpers(FunctionalRenderContext.prototype);
--->
render:("with(this){return " + code + "}")

_c

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
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
function createElement (context,tag,data,children,normalizationType,alwaysNormalize) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE;
}
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context,tag,data,children,normalizationType) {
//这里省略一系列边界判断
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) { //标签名是一般的html标签
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { //标签名是组件标签名
vnode = createComponent(Ctor, data, context, children, tag);
} else {
//未知或未列出的命名空间元素在运行时检查,
//因为当其父项标准化子元素时可能会为其分配一个命名空间
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) { applyNS(vnode, ns); }
if (isDef(data)) { registerDeepBindings(data); }
return vnode
} else {
return createEmptyVNode()
}
}
var VNode = function VNode (
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
this.tag = tag;
this.data = data;
this.children = children;//children本身是vnode对象集合,所以不需递归处理父子关系
this.text = text;
this.elm = elm;//挂载真正的dom对象
this.ns = undefined;
this.context = context;
this.fnContext = undefined;
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.raw = false;
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false;
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
};

注意createElement函数只是将一个结点转成vnode对象
上面generate得到的render字符串的逻辑是生成一个根结点,
里面传递的子节点数组也是调用createElement函数生成的一个个vnode
所以这里调用render生成vnode函数时,没有递归逻辑,
而是每个结点自己去调createElement生成自己的vnode

_s

1
2
3
4
5
6
7
function toString (val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}

_v

1
2
3
function createTextVNode (val) {
return new VNode(undefined, undefined, undefined, String(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
39
function 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
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
function mountComponent (vm,el,hydrating) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
{...}
callHook(vm, 'beforeMount');

var updateComponent;
if (config.performance && mark) {...
} else { //定义updateComponent
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true);
hydrating = false;
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}

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
32
var 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
3
updateComponent = 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
25
function 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
82
Vue.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
12
function 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
40
function 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)
)
)
)
}

流程图

My Little World

关于css的一些事

发表于 2020-01-27

1.CSS 选择器的优先级
!important
内联样式(1000)
id选择器(100)
类选择器(10)
元素选择器(1)

2.为兼容浏览器,对css基本样式做统一处理的三种方法
【reset】
消除浏览器默认样式,针对具体元素标签重写样式
【normalize】
样式规范确定的情况下,直接重写全部样式
【neat】
前两者结合,针对确定的样式直接重写,不确定的进行清除

3.css预处理语法工具需要具备的特性
可以变量声明和计算
支持语法表达式
可以定义函数进行处理
属性可以继承
可以自行实现autoprefix类似的兼容性补全功能

4.实现动画的6种方式
纯JS(利用setInternal,16ms)
svg(利用标签配置)
canvas(利用jsAPI配置)
css3-transition(过渡动画,不能独立实现动画)
css3-animation(纯css实现动画)
requestAnimationFrame(类似setInterval,但性能消耗低)
前两种兼容性良好,后四种适用于移动端

5.屏幕适配布局方案
想办法让比例不变
A.在html/body标签上加上zoom属性,缺点是不灵活
B.使用rem,相对于文档根元素font-size,
利用mediaQuery对常见几种屏幕宽度设置html的font-size
以font-size大小为1rem

6.响应式设计的两种方案
前端或者后端判断useragent来跳转不同站点得到不同页面
使用mediaQuery媒体查询让页面根据不同设备自动改变页面布局和显示

7.根据浏览器屏幕宽度和分辨率加载不同大小的图片
使用mediaQuery判断加载不同的背景图
使用H5的picture标签
模板进行判断然后渲染不同图片
请求图片时带上相关参数,让图片服务器吐出不同大小图片

table-layout: fixed; 妙用

具有滚动条的table表头和tbody分别用两个table实现时,在每列设置宽度的情况下,有时可能出现表头和tbody对不齐的情况,

这是因为table默认情况下,会按照表格内容进行列宽绘制,tbody和表头整个宽度是一样宽的,而tbody有滚动条宽度,表头没有滚动条,就会导致列对不上了
解决办法:

  1. 给table设置 table-layout: fixed;让table按照设置的列宽进行绘制
    .xxx-table {
    table-layout: fixed;
    }
  2. 不让所有的列都有具体的宽度,让某一列不设置宽度
My Little World

Shadow DOM

发表于 2020-01-27

Shadow DOM是HTML的一个规范,它允许浏览器开发者封装自己的HTML标签,css样式和特定js代码
同时也可以让开发者常见类似video这样的自定义一级标签,
创建的这些新标签和相关的api被称为 WEB Component

Shadow host 是shadow dom 的容器元素,就是最终使用的标签元素
Shadow root 是shadow dom 具体内容的根节点,可以使用document.createShadowRoot()创建
shadow tree 是shadow dom 包含的子节点数

在shadow root 上可以任意通过DOM操作添加任意shadow tree,同时指定样式和处理逻辑
并将自己的API暴露出来
完成创建后需要通过document.registerElement()在文档中注册元素
shadow dom 创建就完成了

My Little World

vuerouter源码初步学习

发表于 2020-01-27

工作流程:
URL改变->出发监听事件->改变vue-router里的current变量->
触发current变量的监听者->获取新组件->render新组件

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
class HistoryRoute{
constructor(){
this.current=null
}
}
class vueRouter{
constructor(options){
this.history= new HistoryRoute;
this.mode = options.mode || 'hash'
this.routes = options.routes || []
this.routesMap = this.createMap(this.routes)
this.init()
}
Init(){
if(this.mode=='hash'){
//如果没有#号,自动加上#
location.hash?"":location.hash='/'
//监听hash改变
window.addEventListener('load',()=>{
//slice去掉#号
this.history.current=location.hash.slice(1)
})
window.addEventListener('hashchange',()=>{
this.history.current=location.hash.slice(1)
})
}else{
location.pathname?"":location.pathname='/'
window.addEventListener('load',()=>{
this.history.current=location.pathname
})
window.addEventListener('hashchange',()=>{
this.history.current=location.pathname
})
}
}
//将路由对象[{path:'xxx',component:a}]转成:{xxx:a}
createMap(routes){
return routes.reduce((memo,current)=>{
memo[current.path] = current.components
return memo
})
}
}
//定义属性标签 get set ===>供VUE.use使用
//VUE.use(xxxx) xxx是一个函数时直接运行xxx函数,xxx是一个对象或者类时,运行其上面的install方法
vueRouter.install = function(Vue){
Vue.mixin({
beforeCreate(){
//this指向当前组件实例,this.$options指向创建VUE实例时传入的配置项
if(this.$options&&this.$option.router){
//把当前实例挂在_root上
this._root = this
this._router = this.$options.router
Vue.util.defineReactive(this,'current',this._router.history) //双向绑定,对路由变化进行监听,
}
Object.defineProperty(this,"$router",{ //能够获取$router但不能修改的原理。防止用户修改_router
get(){
return this._root._router;
}
})
}
})
Vue.components('router-view',{
render(r){
//拿到当前路径
//this指向proxy,当前组件代理对象;this._self当前组件实例化对象;this._self._root当前组件对象
let current = this._self._root._router.history.current
let routeMap = this._self._root._router.routeMap
return r(routeMap[current])
}
})
}
module.exports = vueRouter

My Little World

关于模块化

发表于 2020-01-27

什么是模块化

模块化就是把系统分离成独立功能的方法,从而实现需要什么功能,就加载什么功能
当一个项目开发得越来越复杂时,会出现问题:命名冲突;文件依赖
使用模块化开发,可以避免以上问题,并且提高开发效率:可维护性;可复用性

模块化开发演变
1.使用全局函数 ===>早期开发就是将重复得代码封装到函数中,再将一系列的函数放到一个文件中
存在的问题就是:污染全局变量;看不出相互的直接关系
2.使用对象命名空间 ===>通过对象命名空间的形式,从某种程度上解决了变量命名冲突的问题,但是并不能从根本上解决命名冲突
存在的问题是:内部状态可被外部改写;命名空间越来越来长
3.私有共有成员分离 ===>利用此种方式将函数包装成一个独立的作用域,私有空间的变量和函数不会影响全局作用域
存在问题:解决了变量命名冲突问题,但是没有解决降低开发复杂度的问题

Commonjs

commomJS规范加载模块是【同步】的,也就是说,加载完成才执行后面的操作
Node.js主要用于服务器编程,模块都是存在本地硬盘中加载比较快,所以Node.js采用CommonJS
三个部分:
module变量在每个模块内部,就代表当前模块
exports属性是对外的接口,用于导出当前模块的方法或变量
require()用来加载外部模块,读取并执行js文件,返回该模块的exports对象

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
加载时执行:
a.js
exports.done = ture
var b = require('b.js')
console.log('a.js-done')

b.js
exports.done = false
var a = require('a.js')
console.log(a.done)
console.log('b.js-done')

App.js 执行App.js
var a = require('a.js') // 因为引用b.js,执行b.js代码故打印出true b.js-done,引用完毕后接着执行打印出 a.js-done
var b = require('b.js')// 已经被a引用过,不再执行 直接在缓存中找出 exports {done:false} 返回
console.log(a.done)
console.log(b.done)

如果a.js改成如下
var b = require('b.js')
exports.done = ture
console.log('a.js-done')
执行App.js
打印结果为undefined b.js-done a.js-done true false
因为a依赖b时,执行b,b再依赖a时,exports.done = ture语句还未执行,故a 的 exports 为{} exports.done自然为undefined

只有commonjs因为同步加载,加载时执行可实现循环依赖,其他模式均不可以处理循环依赖的用法

AMD(requirejs)

AMD也就是【异步】模块定义,它采用异步方式加载模块,通过define方法去定义模块,require方法去加载模块
定义:
define([tools],function(){return {}}) //第一个参数为数组时,说明该模块还依赖其他模块,提前声明依赖,没有依赖时,只传入function即可
加载:
require([‘module’],callback) //第一个参数数组内成员即要加载的模块,callback是前面所有某块都加载成功之后的回调函数

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
/** a.js */

define(['b'], function(b) {
console.log(b)
var hello = function(){
console.log('hello work')
}
return {
Hello:Hello
}
});

/** b.js */

define(function(){
var name = 'max'
return{
name:name
}
})

/** 使用a.js,b.js */

/*
<script type='text/javascript' src='require.js'></script>
<script type='text/javascript'>
require.config({
paths:{
'a':'./a',
'b':'./b'
}
})
require(['a','b'],function(a,b){ //启用模块加载器
console.log(a)
console.log(b)
})
</script>
*/

CMD(seajs)

与AMD一样,只是定义和模块加载方式上不同
一个模块一个文件
define(function(require,exports,module){
//require 是可以把其他模块导入进来的一个参数
//exports可以把模块内的一些属性和方法导出
//module是一个对象,上面存储了与当前模块相关联的一些属性和方法
})
CMD推崇依赖就近,延迟执行,文件是提前加载好的,只有在require的时候才去执行文件
define(function(require,exports,module){
var math = require(‘./math’)
math.add()
})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** a.js */

define(function(require,exports,module) {
var b = require('b')
console.log(b)
exports.hello = function(){
console.log('hello work')
}
});

<script type='text/javascript' src='sea.js'></script>
<script type='text/javascript'>
seajs.config({
alias:{
'a':'./a',
'b':'./b'
}
})
seajs.use(['a','b'],function(a,b){ //启用模块加载器
console.log(a)
console.log(b)
})
</script>

ES6模块化

汲取CommonJS和AMD优点,支持异步加载,未来可以成为浏览器和服务器通用的模块化解决方案
export:用于把模块里的内容暴露出来, export default 为模块指定默认输出,一个模块只能有一个默认输出,所以export default 只能使用一次
import: 用于引入模块提供的功能===> import {x1,x2} form ‘./lib’; x1()

ES6模块运行机制:
ES6模块是动态引用,如果使用import从一个模块加载变量,变量不会被缓存,
而是成为一个指向被加载模块的引用。等脚本执行时,根据只读引用到被加载的那个模块中去取值

ES6与CommonJS模块差异
CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值得引用
CommonJS模块运行时加载,ES6模块编译时输出接口

加载器结构

模块部分

每个模块创建都先初始化数据,存储在缓存对象中
module1

数据初始化

module2
加载器中设计了一个名为Module的构造函数,【每个模块都是此构造函数的实例对象】
构造函数中给实例对象扩展了’未来’所需要用到的属性(uri:当前模块绝对路径地址,deps:模块的依赖列表….)及方法(load,resolve:获取当前模块绝对路径…)

模块存储

加载器中设计了一个名为cache的缓存对象,每个文件(模块)都会存储在cache对象中
具体存储方式:{‘当前模块绝对路径’: new Module()} 注意:当前模块的绝对路径是通过资源部分,资源定位方法实现的

资源部分

资源定位和依赖管理是加载器涉及的两大核心

资源定位

module3
加载器中设计了一个resolve()方法把模块名解析成绝对路径格式
检测当前模块名称是否有路径短名称配置,是否有模块短名称配置,是否有后缀
在加载器启动方法中会去调用传入数组列表中的模块,获取模块名称
然后根据当前项目绝对路径和模块名称进行拼接

动态加载script文件

动态创建script标签: document.create(‘script’); src指向当前模块绝对路径地址
加载文件同时,模块加载器解析当前模块所依赖的模块,以数组形式存储,更新到deps属性中

依赖管理

已知当前模块在cache中的形态,{‘当前模块绝对路径’: new Module()}
换算成:{‘当前模块绝对路径’: {uri:’当前模块绝对路径’,deps:[]}}
module4
deps存储当前模块的依赖列表,依赖列表通过动态加载script文件正则解析获取
重点:解析依赖—>获取依赖模块绝对路径地址—->动态加载—->提取依赖—->解析依赖
递归方式加载所有模块,直至模块全部加载完毕(模块的deps属性集合中没有依赖的绝对路径,即长度为0)

如何实现一个文件一个作用域?

保证模块拥有独立作用域,采用对象命名空间的思想,即每个模块返回一个接口对象,这个独立作用域就是一个对象

如何拿到依赖模块的接口对象?

参数即define传入的函数,执行函数,返回函数返回的结果即对象

如何让寻找依赖?

拼接地址,缓存,然后递归
图片:依赖加载策略 模块数据初始化 资源定位-动态加载 依赖管理解决方案

实现

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
(function(global) {
var startUp = global.startUp = {
version:'1.0.1'
}


var data = {} //配置信息
var cache = {}
var anonymousMeta = {}
//模块生命周期

var status = {
FETCHED: 1,
SAVED:2,
LOADING:3,
LOADED:4,
EXECTING:5,
EXECTED:6
}
var isArray = function (obj) {
return toString.call(obj) === "[object Array]"
}
var isFunction = function (obj) {
return toString.call(obj) === "[object Function]"
}
var isString = function (obj) {
return toString.call(obj) === "[object String]"
}
function scripts(){
return document.getElementsByTagName('script')
}
function getInteractiveScript(){ //支持data-main属性添加默认路径: <script data-main = 'common/js' src='module.js'></script>
var arrS = scripts()
var dataMain,src
var exp = /^.*\.js$/
arrS = [].slice.call(arrS)
arrS.forEach(function(script){
dataMain = script.getAttribute('data-main')
if(dataMain && !data.baseUrl && !(exp.test(dataMain))){
if(dataMain.substring(dataMain.length-1) !== '/'){
dataMain = (dataMain+'/')
}
data.baseUrl = dataMain
}
})
}
getInteractiveScript()
//是否使用了别名
function parseAlias(id){
var alias = data.alias //是否配置别名
return alias && isString(alias[id]) ? alias[id] :id //没有配置别名,返回原来值,有配置别名,返回别名对应值
}
//不能以'/'':'开头,必须是一个'/'后面跟随任意字符至少一个
var PATH_RE = /^([^\/:]+)(\/.+)$/ //([^\/:]+) 路径的短名称配置

//检测是否 书写路径短名称
function parsePaths(id) {
var paths = data.paths; //是否配置短路径
if(paths && (m=id.match(PATH_RE)) && isString(paths[m[1]])){
id = paths[m[1]]+m[2]
}
return id
}

//检测是否添加后缀
function normalize(path){
var last = path.length-1
var lastC = path.charAt(last)
return (lastC === '/' || path.substring(last-2) === '.js') ? path :path+'.js'
}
//添加根目录
function addBase(id,uri){
// var result;
// //相对路径
// if(id.charAt(0) === '.'){
// result = realpath( (uri ? uri.match(/[^?]*\//)[0] : data.cwd ) + id)
// }else{
// result = data.cwd+id
// }
var result = data.baseUrl ?data.cwd + data.baseUrl +id : data.cwd + id //支持data-main属性添加默认路径
return result
}

var DOT_RE = /\/.\//g // /a/b/./c/./d ==>/a/b/c/d /./ ==>/
var DOUBLE_DOT_RE = /\/[^/]+\/\.\.\//; // a/b/c/../../d ===>a/b/../d ==> a/d /xxxxx/../==> /
//规范路径
function realpath(path){
path = path.replace(DOT_RE,'/')
while(path.match(DOUBLE_DOT_RE)){
path= path.replace(DOUBLE_DOT_RE,'/')
}
return path
}
//生成绝对路径 资源路径解析
startUp.resolve = function (child,parent) {
if(!child) return ''
child = parseAlias(child) //检测是否有别名
child = parsePaths(child) //检测是否有路径别名,依赖模块中引包的模块路径地址 require('app/c)
child = normalize(child) //检测是否添加后缀
return addBase(child,parent) //添加根目录
}

startUp.request = function(url,callback){
var node = document.createElement('script')
node.src = url
document.body.appendChild(node)
node.onload = function(){
node.onload = null
document.body.removeChild(node) //加载依赖结束后移除加载时的script标签
callback()
}
}
//模块加载器启用
startUp.use = function(list,callback){
//检阅有没有预先加载的模块
Module.preload(function(){
Module.use(list,callback,data.cwd+"_use_"+cid()) //虚拟的根目录
})
}
//模块加载器配置
/**
*
* startUp.config({
* alias:{
* a:'common/js/a'
* },
* preload:[c.js]
* })
*/
startUp.config = function (options) {
var key,curr
for(key in options){
curr = Option[key]
data[key] = curr
}
}

//构造函数 模块初始化数据
function Module(uri,deps){
this.uri = uri;
this.deps = deps || [] //依赖项
this.exports = null
this.status = 0
this._waitings = {}
this._remain = 0
}

//分析主干(左子树 | 右子树)上的依赖项
Module.prototype.load = function(){
var m = this
m.status = status.LOADING//LOADING == 3 正在加载模块依赖项
var uris = m.resolve()//获取主干上的依赖项
var len = m.remain = uris.length

//加载主干上的依赖项(模块)
var seed
for(var i=0;i<len;i++){
seed = Module.get(uris[i]) //创建缓存信息
if(seed.status <status.LOADED){ //LOADED == 4 准备加载执行当前模块
seed._waitings[m.uri] = seed._waitings[m.uri] || 1 //多少模块依赖于我,_waitings是依赖我的模块的路径集合
}else{
seed._remain--
}
}
//如果依赖列表模块全部加载完毕
if(m._remain == 0){ //递归过程到此结束
//获取模块的接口对象
m.onload()
}

//准备执行根目录下的依赖列表中的模块
var requestCache = {}
for(var i=0;i<len;i++){
seed = Module.get(uris[i])
if(seed.status < status.FETCHED){
seed.fetch(requestCache)
}
}
for(uri in requestCache){
requestCache[uri]()
}
}

Module.prototype.fetch = function(requestCache){
var m =this
m.status = status.FETCHED
var uri = m.uri
requestCache[uri] = sendRequest; //Document.createElement('script)

function sendRequest(){
startUp.request(uri,onRequest) //动态加载script
}

function onRequest(){ //事件函数 script标签加载模块结束后,onload 函数中会被调用
if(anonymousMeta){ //模块的数据更新
m.save(uri,anonymousMeta)
}
m.load() //递归 模块加载策略
}
}

Module.prototype.onload = function(){
var mod = this
mod.status = LOADED //4
if(mod.callback){//获取模块接口对象
mod.callback()
}
var waitings = mod._waitings //依赖加载完,递归回调 被依赖模块(父)的callback
var key,m;
for(key in waitings){
var m = cache[key]
m._remain -= waitings[key]
if(m._remain == 0){ //判断父模块依赖是否全部加载完
m.onload()
}
}
}

//资源定位 解析依赖项生成绝对路径
Module.prototype.resolve = function(){
var mod = this
var ids = mod.deps
var uris = []
for(var i =0;i<ids.length;i++){
uris[i] = startUp.resolve(ids[i],mod.uri) //依赖项(主干 | 子树)
}
return uris;
}

//更改初始化数据
Module.prototype.save = function(uri,meta){
var mod = Module.get(uri)
mod.uri = uri
mod.deps = meta.deps || []
mod.factory = meta.factory
mod.status = status.SAVED
}

//获取接口对象
Module.prototype.exec = function(){
var module = this
//防止重复执行
if(module.status >= status.EXECTING){
return module.exports
}
module.status = status.EXECTING;
var uri = module.uri
function require(id){ //作为参数传递到define参数的函数中
return Module.get(require.resolve(id)).exec() //获取接口对象
}

require.resolve = function(id){
return startUp.resolve(id,uri)
}

var factory = module.factory //define传入的函数
var exports = isFunction(factory) ? factory(require,module.exports= {},module) :factory;
if(exports === undefined) {
exports = module.exports
}
module.exports = exports
module.status = status.EXECTED //6
return exports
}

//定义一个模块
Module.define = function(factory){
var deps
if(isFunction(factory)){
//正则解析依赖项
deps = parseDependencies(factory.toString())
}
//存储当前模块信息
var meta = {
id:'',
uri:'',
deps:deps,
factory:factory
}
anonymousMeta = meta
}

//检测缓存对象上是否有当前模块信息
Module.get = function(uri,deps){
//cache['xxxxx.js'] = {uri:'xxxxx.js',deps:[]} //module 实例对象
return cache[uri] || (cache[uri] = new Module(uri,deps))
}

Module.use = function(deps,callback,uri){
var m = Module.get(uri,isArray(deps)?dep:[deps])
console.log(module)
//所有模块都加载完毕
m.callback= function(){
var exports = [] //所有依赖项模块的接口对象
var uris = m.resolve()
for(var i = 0;i<uris.length;i++){
exports[i] = cache[uris[i]].exec() //获取模块对外定义的接口对象
}
if(callback){
callback.apply(global,exports)
}
}
m.load()
}

var _cid = 0

function cid(){
return _cid++
}
// data.preload = []
//取消当前项目文档的URL
data.cwd = document.URL.match(/[^?]*\//)[0]
Module.preload = function(callback){
var preload = data.preload ||[]
var length = data.preload.length
if(length){ //length !== 0 先加载预先设定模块
Module.use(preload,function () {
preload.splice(0,length)
callback()
},data.cwd+'_use_'+cid())
} else{
callback()
}

}

var REQUIRE_RE = /\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
function parseDependencies(code){
var ret = []
code.replace(REQUIRE_RE,function(m,m1,m2){
if(m2) ret.push(m2)
})
return ret
}
global.define = module.define
})(this)
My Little World

vue 源码学习一【new vue】

发表于 2020-01-27

new vue时发生了什么

平常练习时使用vue顺序是,将vue.js引入html,
new vue就可以开始使用vue框架写东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id = 'app'>
<p>{{a}}</p>
</div>
<script type="text/javascript" src="vue.js"></script>
<script>
let app = new Vue({
el:'#app',
data:{
a:'hello world'
},
method:{},
})
</script>
</body>

代码运行顺序是,当我们在script标签引入文件时,
引入的vuejs文件会先执行一遍,
执行的结果就是帮助我们生成一个挂载在全局的,vue对象的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.Vue = factory());//将vue挂到全局上下文中
}(this, function () { 'use strict';
function Vue (options) {
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
initGlobalAPI(Vue);
Vue.compile = compileToFunctions;

return Vue;

}));

当代码运行到new vue时,代码就会调用function Vue
function Vue 中会【调用】自己的_init()方法,
对即将生成的vue对象做初始化工作,即开始生成vue对象

_init方法定义在initMixin函数中
在调用initMixin生成构造函数流程中被挂到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
Vue.prototype._init = function (options) {
var vm = this;
vm._uid = uid$3++;

var startTag, endTag;
vm._isVue = true;// a flag to avoid this being observed
// 合并选项,将配置项以内置选项进行合并
if (options && options._isComponent) {
//优化内部组件实例化过程
// 由于动态选项合并非常慢,并且内部组件选项均不需要特殊处理。
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');//运行订阅了beforecreate钩子的相关方法
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
if (vm.$options.el) {//如果配置了el,就进行挂载处理挂载
vm.$mount(vm.$options.el);//会给给option挂上render,staticRenderFns属性
}
};

My Little World

vue 源码学习三

发表于 2020-01-27

钩子函数的执行顺序

不使用keep-alive beforeRouteEnter –> created –> mounted –> destroyed

使用keep-alive beforeRouteEnter –> created –> mounted –> activated –> deactivated 再次进入缓存的页面,只会触发beforeRouteEnter –>activated –> deactivated 。 created和mounted不会再执行。

我们可以利用不同的钩子函数,做不同的事。

vue.component

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
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
function _createElement (context,tag,data,children,normalizationType) {
//这里省略一系列边界判断
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);
if (config.isReservedTag(tag)) { //标签名是一般的html标签
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { //标签名是组件标签名
vnode = createComponent(Ctor, data, context, children, tag);
} else {
//未知或未列出的命名空间元素在运行时检查,
//因为当其父项标准化子元素时可能会为其分配一个命名空间
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) { applyNS(vnode, ns); }
if (isDef(data)) { registerDeepBindings(data); }
return vnode
} else {
return createEmptyVNode()
}
}
function createFunctionalComponent (
Ctor,
propsData,
data,
contextVm,
children
) {
var options = Ctor.options;
var props = {};
var propOptions = options.props;
if (isDef(propOptions)) {
for (var key in propOptions) {
props[key] = validateProp(key, propOptions, propsData || emptyObject);
}
} else {
if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
if (isDef(data.props)) { mergeProps(props, data.props); }
}

var renderContext = new FunctionalRenderContext(
data,
props,
children,
contextVm,
Ctor
);

var vnode = options.render.call(null, renderContext._c, renderContext);

if (vnode instanceof VNode) {
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
} else if (Array.isArray(vnode)) {
var vnodes = normalizeChildren(vnode) || [];
var res = new Array(vnodes.length);
for (var i = 0; i < vnodes.length; i++) {
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);
}
return res
}
}

生命周期合并策略

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

var strats = config.optionMergeStrategies;
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
});
function mergeHook (
parentVal,
childVal
) {
var res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal;
return res
? dedupeHooks(res)
: res
}

function mergeOptions (
parent,
child,
vm
) {
{
checkComponents(child);
}

if (typeof child === 'function') {
child = child.options;
}

normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirectives(child);

// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm);
}
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm);
}
}
}

var options = {};
var key;
for (key in parent) {
mergeField(key);
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
function initMixin$1 (Vue) {
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
return this
};
}
My Little World

一些关于vue的零散笔记

发表于 2020-01-27

释放内部对象且能防止外部修改的方式

通过创建中间对象挂载到实例上,通过中间对象的set方法进行拦截

1
2
3
4
5
6
7
8
9
10
11
12
function initGlobalAPI (Vue) {
var configDef = {};
configDef.get = function () { return config; }; //config对象是vue对象构建过程的内部对象,存储全局配置项
{
configDef.set = function () {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
);
};
}
Object.defineProperty(Vue, 'config', configDef);
}

Vue.config.optionMergeStrategies 应用中
Vue.config可以get,得到的即内部的config,但如果进行Vue.config = xxxx set操作就会报错
通过Vue.config可以获得内部config,从而可以修改内部config对象上的属性,从而修改到全局配置

关于3.0的思考

如何更快
Object.defineProperty–>Proxy
Virtual Dom重构
更多编译优化:
Slot默认编译为函数
重构vnode factory,统一输入参数,进一步优化编译处理函数(Monomorphic vnode factory)
编译时增加vnode/children 类型信息标识,从而做进一步优化(Compiler-generated flags for vnode/children types)

Function based API 对比Class API 好处
更好的TS类型推导支持
更灵活的逻辑复用能力
Tree-shaking 友好
代码更容易压缩

现有逻辑复用模式
Mixins
高阶组件
基于scoped slots/作用域插槽封装逻辑的组件
存在问题
数据来源不清晰 (Function based API 数据来源清晰)
命名冲突(Function based API 没有命名冲突)
无谓的额外组件的性能消耗 (Function based API 没有额外组件性能消耗)

对比React Hooks
同样的逻辑组合,复用能力更强
只调用一次
符合JS直觉
没有闭包变量问题
没有内存/GC压力
不存在内联回调导致子组件永远更新问题

2.0
mergeOption 选项策略处理
插件开发:以默认配置优先;以用户配置为覆盖
策略:听过JS 增强配置选项的表达力
{}==>function(){} el===根实例

数据劫持vue3.0改用proxy原因
defineProperty只能监听某个属性,不能对全对象监听,使用proxy可以省去for in提升效率
可以监听数组,不用再去单独的对数组做特异性操作

vue2.0 vdom性能瓶颈
虽然能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个Vdom树
vdom的性能跟模板大小正相关,跟动态节点的数量无关。
在一些组件整个模板内只有少量动态节点的情况下,这些遍历都是性能的浪费

编译3步骤

parse:AST
optimize:标记静态节点
generate 生成目标平台所需的代码 将AST转化成render function字符串

修饰符

v-model.lazy 从input事件中转变为在change事件中同步数据
v-model.number 可以将输入转换为Number类型
v-model.trim 可以自动过滤输入的首尾空格

v-for 循环时,key尽量赋值为不变的值,即静态值,如item.id ,否则列表增删时,会更新每一项

当子组件需要向父组件传递数据时,就要用到自定义事件
子组件用$emit来触发事件,父组件用$on()来监听子组件的事件
自定义组件的v-model :一个组件上的v-model 默认会利用名为value的prop和名为input的事件
将原生事件绑定到组件: 使用v-on的.native修饰符监听原生事件
.sync修饰符:父组件监听自定义事件按需更新数据

组件

动态组件:VUE 提供了一个特殊的元素<component>用来动态的挂载不同的组件,使用is特性来选择要挂载的组件, 还可以使用<keep-alive>标签使组件进行缓存

异步组件:VUE允许将组件定义为一个工厂函数,动态地解析组件。VUE只在组件需要渲染时触发工厂函数,并且把结果缓存起来用于后面地再次渲染

组件创建方式

1.调用Vue.extend(),创建名为xx的组件,template定义模板的标签,模板的内容需要写在该标签下

1
2
3
var xx = Vue.extend({
template:'<div>this is a component</div>'
})

2.使用<template>标签创建,需要加上id属性

1
2
3
<template id = 'mycom'>
<div>this is a component</div>
</template>

3.使用<script>标签创建,需要加id属性,同时还得加type=’text/x-template’ 不执行编译里面的代码

1
2
3
<script type='text/x-template' id = 'mycom'>
<div>this is a component</div>
</script>

组件全局注册方式

1.调用Vue.extend(),创建名为myCom的组件全局注册

1
Vue.component('my-com',myCom)

2.template及script标签构建的组件全局注册

1
2
3
Vue.component('my-comp',{
template:'#mycom'
})

组件局部注册

1.调用Vue.extend(),创建名为myCom的组件局部注册(只能在注册该组件的实例中使用,一处注册,一处使用)

1
2
3
4
5
6
var app = new Vue({
el:'#app',
components:{
'my-com':myCom
}
})

2.template及script构建的组件局部注册

1
2
3
4
5
6
7
8
var app = new Vue({
el:'#app'
componnets:{
'my-com':{
template:'#myCom'
}
}
})

组件化处理边界情况

1.访问根实例 this.$root.xxx
2.访问父组件实例:this.$parent.xxx
3.访问子组件实例或子元素:<child ref=’xxx’></child > =>this.$refs.xxx
4.依赖注入:

1
2
3
4
5
6
provide:function(){ //父组件
return{
getMap:this.getMap
}
}
inject:[getMap]//子组件

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了

5.统一处理事件侦听器时,在组件卸载前,统一清除监听器
6.组件循环引用,Vue.component注册的组件允许,但是模块化导入的不允许
7.模板定义的替代品:
A:内联模板:当inline-template这个特殊的特性出现在一个子组件上时,这个组件将会使用其里面的内容作为模板
而不是将其作为被分发的内容

1
<myCom inline-template><div><p>these are compiled as the component's own template</p></div></myCom>

B:x-template
在script标签里使用text/x-template,并且指定id,将这个id赋值给template

1
2
3
4
5
6
<script type='text/x-template' id = 'mycom'>
<div>this is a component</div>
</script>
Vue.component('my-comp',{
template:'#mycom'
})

8.强制更新
A:使用this.$forceUpdate
B:使用v-once创建低开销静态组件,组件包含大量静态内容可以在根元素上添加v-once特性以确保这些内容只计算一次然后缓存起来

组件通信类型

父子组件通信
1.使用props和$emit父子组件相互通信
2.父组件用$children或者利用ref操作子组件
3.子组件$parent访问父组件

非父子组件通信
1.使用中央事件总线(eventbus来处理非父子组件间的通信)
2.祖先元素通过provide提供数据,后代通过inject获取该数据
3.使用$attrs和$listeners实现祖孙组件通信
4.$root直接访问根组件

Vue.use

Vue.use(myPlugin)本质上是调用myPlugin.install(VUE)
使用插件必须在new vue()启动应用之前完成,实例化之前就要配置好
使用Vue.use多次注册相同插件,那只会注册成功一次
一个测试案例

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import {expect} from 'chai'
import Counter from '@/counter.vue'

describe('测试Counter.vue',()=>{
const Constructor = Vue.extend(Counter)
const vm = new Constructor().$mount()
const button = vm.$el.querySelector('button')
const clickE = new window.Event('click')
button.dispatchEvent(clickE)
vm._watcher.run()
expect(Number(vm.$el.querySelector('span').textContent)).to.equal(1)
})

vuex

默认的五种基本对象
state:存储状态(对象)
getters:对数据获取之前的再次编译,可以理解为state的计算属性,对state的数据进行筛选,过滤
mutations:同步修改状态,并且是同步的。在组件中使用$store.commit(‘’,params)
actions:异步修改状态,在组件中使用是$store.dispatch(‘’)
modules:store的子模块,为了开发大型项目,方便状态管理而使用的

My Little World

一些关于H5的知识

发表于 2020-01-27

h5 新增语义化布局标签:header,nav,section,aside,article,footer,均表现为块级
canvas,video,audio,sessionStorage,localStorage,
拖放API(源对象:ondragstart,ondrag,ondragend;源对象标签属性要设置为draggable=true
目标对象:ondragenter,ondragover,ondragleave,ondrop;
ondragover事件默认会屏蔽ondrop事件,
如果要触发ondrop事件,需要在ondragover事件函数中阻止默认屏蔽行为
event.preventDefault()
)
//谷歌浏览器v56 之后,window,document,body的touchstart,touchmove事件会被默认为pssive:true
//解决方法一:window.addEventListener(‘touchstart’,func,{passive:false})
//解决方法二,添加样式:*{touch-action:none} 取消所有元素默认事件

1
2
3
4
5
6
7
8
9
10
//实现把A标签拖到B标签
1.A标签开始拖动时
ondragstart(event){
event.dataTransfer.setData('source',event.target.id)
}
2.B标签监听拖拽结束后
ondrop(event){
var source = document.getElementById(event.dataTransfer.getData('source'))
event.target.appendChild(source)
}

History常用场景
单页应用中:实现网页无刷新更新数据的同时,解决浏览器无法前进/后退的问题
pushState:每执行一次都会增加一条历史记录,浏览器在返回时,就不会返回前一个页面,并且不会刷新浏览器
replaceState:用来修改当前历史记录,而不是创建一个新的历史记录,点击返回按钮照样会返回上一个页面
onpopstate:点击后退,前进或者调用history.back(),history.forward(),history.go()方法

跨文档通讯

定义:跨文档消息传送,来自不同域的页面间传递消息
使用场景:内嵌框架和其父文档相互进行数据传输或者通讯

postMessage

window.postMessage(message,origin,transfer)
message:发送到子文档的信息,一般转成字符串,否则可能出现浏览器不兼容
origin:域信息,告诉子文档,消息来源,用于判断来源是否匹配,然后才进行相关操作
transfer:转移消息对象,可选参数,是一串和message同时传递的Transferable对象,这些对象的所有权将被转移给消息的接收方,而发送方将不再保有所有权

onmessage

onmessage事件回调函数参数event事件对象
event.data:postMessage传输过来的第一个参数,通常是字符串类型,(也可以是其他类型,但可能出现浏览器不兼容情况,所以一般转成字符串传递)
event.origin:postMessage传输过来的第二个参数,是用来匹配来源方的域
event.source:来源方目标文档的window引用,通常用作单次握手回应数据(event.source.postMessage())
event.ports:MessageEvent接口特有属性,用于获取消息端口集合对象

实现

父文档A通过iframe内嵌框架加载B子文档
通过iframe内嵌框架的onload事件回调父文档的sendMsg函数
再sendMsg函数中通过postMessage()函数向子文档B发送消息

子文档B监听onmessage事件,当文档收到消息后会执行该事件回调函数
回调函数内通过event对象判断域是否安全,然后处理推送过来的消息,
可以再通过调用event.source.postMessage向父文档发送消息

地理位置

navigator.geolocation.getCurrentPosition(success,error,option)
success:成功得到位置时的回调函数,使用Position对象作为唯一的参数
error:失败时回调,使用PositionError对象作为唯一参数
options:可选参数,对请求做一些配置

离线存储优势

离线浏览:当用户网络断开时,可以继续访问页面
访问速度快:将资源缓存到本地,已缓存资源加载更快
减少服务器负载:浏览器将只会从服务器下载更新过或者更改过的资源

视频播放兼容

Flv.js

H5 Flash(FLV)播放器,纯原生js开发,使H5能够支持FLV格式的视频
原理:将FLV文件流转码复用为ISO BMFF(MP4碎片)片段,然后通过Media Source Extension将MP4片段汇进浏览器
使用es6编写,通过Babel Compiler编译成es5能够支持FLV格式的视频

Video.js

几乎兼容所有浏览器,且优先使用H5,在不支持的浏览器中自动使用Flash播放
构建视频播放页面耗时短,界面可定制,开源,纯JS和css实现,文档详细

#Canvas绘制动画步骤
1.清空canvas
除非接下来要画的内容会完全充满canvas(例如背景图),否则需要清空所有,最简单
的做法就是clearRect方法
2.保存canvas状态
如果需要改变一些会改变canvas状态的设置(样式,变形之类的),又要在每画一帧之时,都是原始状态的话,需要先保存一下
3.绘制动画图形
重绘动画帧
4.恢复canvas状态
如果已经保存了canvas的状态,可以先恢复它,然后重绘下一帧

transform

2D功能函数组合使用时,先写的后执行
perspective:100px 可以设置3d旋转时的景深
transform-origin:可以改变中心点

响应式布局

可以通过link标签配置media去使用不同比例的CSS样式

1
2
<link rel='stylesheet' media='screen and (max-device-width:480px)' href='./css1.css'>
<link rel='stylesheet' media='screen and (min-device-width:480px)' href='./css2.css'>

一些触摸事件的实现

My Little World

一些vueRouter官方文档看到的

发表于 2019-11-28

复用组件

当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。
因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。

复用组件时,想对路由参数的变化作出响应的话,可以简单地 watch (监测变化) $route 对象

参数或查询的改变并不会触发进入/离开的导航守卫。
可以通过观察 $route 对象来应对这些变化,或使用 beforeRouteUpdate 的组件内守卫。

参数组合

如果提供了 path,params 会被忽略,query 并不会被忽略。你需要提供路由的 name 或手写完整的带有参数的 path
path—–query name—–params

beforeRouteUpdate

如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2),你需要使用 beforeRouteUpdate 来响应这个变化 (比如抓取用户信息)

router-view标签

没有设置名字,那么默认为 default;如果设置了name,那么该视图显示路由中components中配置的name值对应的组件

重定向

重定向三种形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.
const router = new VueRouter({
routes: [
{ path: '/a', redirect: '/b' }
]
})

2.命名的路由
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
3.方法,动态返回重定向目标:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
}}
]
})

导航守卫并没有应用在跳转路由上,而仅仅应用在其目标上,为 /a 路由添加一个 beforeEach 或 beforeLeave 守卫并不会有任何效果

“重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,
/a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。

1
2
3
4
5
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})

props

通过配置props属性实现多路由复用组件

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
1.如果 props 被设置为 true,route.params 将会被设置为组件属性。
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },

// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})

2.如果 props 是一个对象,它会被按原样设置为组件属性。当 props 是静态的时候有用
const router = new VueRouter({
routes: [
{ path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
]
})

3.创建一个函数返回 props
const router = new VueRouter({
routes: [
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
]
})
URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件

请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应

路由生命周期

全局配置

router.beforeEach:当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

1
2
3
4
5
6
7
8
三个参数
to: 即将要进入的目标 路由对象
from: 当前导航正要离开的路由的路由对象
next:一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。
next(error): 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调

router.beforeResolve:在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用

router.afterEach:不会接受 next 函数也不会改变导航本身

路由配置

1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})

组件内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
//可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
next(vm => { //是支持给 next 传递回调的唯一钩子,beforeRouteUpdate,beforeRouteLeave都不支持,因为可以访问的到
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

数据获取

导航完成后获取数据

1
2
3
4
5
6
7
8
9
created () {
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
this.fetchData()
},
watch: {
// 如果路由有变化,会再次执行该方法
'$route': 'fetchData'
},

在导航完成前获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
beforeRouteEnter (to, from, next) {
getPost(to.params.id, (err, post) => {
next(vm => vm.setData(err, post))
})
},
// 路由改变前,组件就已经渲染完了
// 逻辑稍稍不同
beforeRouteUpdate (to, from, next) {
this.post = null
getPost(to.params.id, (err, post) => {
this.setData(err, post)
next()
})
},
methods: {
setData (err, post) {
if (err) {
this.error = err.toString()
} else {
this.post = post
}
}
}

滚动行为

scrollBehavior 这个功能只在支持 history.pushState 的浏览器中可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {

// return 期望滚动到哪个的位置
if (to.hash) {
return {
selector: to.hash //模拟“滚动到锚点”的行为
}
}
if (savedPosition) {
return savedPosition //返回 savedPosition,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样
} else {
return { x: 0, y: 0 } //让页面滚动到顶部
}
return new Promise((resolve, reject) => { //返回一个 Promise 来得出预期的位置描述
setTimeout(() => {
resolve({ x: 0, y: 0 })
}, 500)
})
}
})

1…8910…27
YooHannah

YooHannah

262 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces