整体构建流程
不同构建版本的解释
其实我一开始存在一个误区,觉得vue就是需要编译的。其实不是,真正需要编译的是不存在render的Vue实例
// 需要编译器
new Vue({
template: '<div>{{ hi }}</div>'
})
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})
渲染目标的区别
el
类型:string | Element
提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器,也可以是一个 HTMLElement 实例。 特性:
- 存在el时,实例自动编译,无需手动调用$mount编译
- 如果不存在template和render,挂载元素的html会被提取出来当作模版
- 所有的挂载元素会被 Vue 生成的 DOM 替换。因此不推荐挂载 root 实例到 或者 上
- 作为挂载元素,实例挂载后可以用vm.$el访问到挂载目标
import Vue from 'vue/dist/vue.common.js'
new Vue({
el: '#app',
// 这里为什么可以用对象而不是函数呢?
data: {
name: '坚果'
},
mounted(){
console.log(this.$el)
}
})
<html>
<body>
<div id="app">
我叫{{name}}
</div>
</body>
</html>
template
类型:string
一个字符串模板 特性:
- 模版将替换挂载元素
import Vue from 'vue/dist/vue.common.js'
new Vue({
el: '#app',
template: '<div class="a">vue {{name}}</div>',
data: {
name: '坚果'
},
})
- 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板(只取匹配到的id内的元素替换整个模版)
import Vue from 'vue/dist/vue.common.js'
new Vue({
el: '#app',
template: '#app',
data: {
name: '坚果'
},
})
<html lang="">
<body>
<div id="app">
// 注意这里
<div class="new-app">html {{name}}</div>
</div>
</body>
</html>
render(无需经历编译)
字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode
- 设置了render就不会再取template选择或者从el中提取模版渲染了
import Vue from 'vue/dist/vue.common.js'
new Vue({
el: '#app',
template: '<div class="a">vue {{name}}</div>',
data: {
name: '坚果'
},
render: function(createElement) {
return createElement('div',
{
class: 'name',
style: 'color: red'
},
[
createElement('div', this.name),
createElement('div', { class: 'age'}, '11111')
])
}
})
寻找模版
首先想编译,就需要找到模版。之前介绍api的时候已经提过了,就是el和template,所以我们先来看看是怎么获取到模版的,并且如何实现之前总结的一些特性。 总体来说分个步骤
- 根据传入的el获取挂载元素
- 没有传入render的情况下获取template
- 将template编译为render
$mount
上文提到的el特性1,存在el时,实例自动调用mount编译,不存在则需要手动调用mount编译
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// a uid
vm._uid = uid++;
// 先暂时把$option假定为new Vue的参数
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
// ....删掉了一大堆代码(initState,生命周期hook等),直接看我们这一期的核心代码
// 存在el自动调用$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
获取el
寻找模版这一节后续所有代码都包含在$mount内 首先el可能传入Element也可能传入字符串,所以需要用query抹平差异,最后都返回Element
// 首先在Vue构造函数的原型链上添加$mount,所以_init内能用this.$mount调用
Vue.prototype.$mount = function (
el,
hydrating
) {
// el存在就抹平string和DOM的差异,最终得到DOM
el = el && query(el);
}
function query (el) {
// string(#id),获取Element
if (typeof el === 'string') {
var selected = document.querySelector(el);
if (!selected) {
warn(
'Cannot find element: ' + el
);
// 没有匹配到Element,直接创建一个空div兜底
return document.createElement('div')
}
return selected
} else {
// 返回Element
return el
}
}
所有的挂载元素会被 Vue 生成的 DOM 替换。因此不推荐挂载 root 实例到 或者 上
// 不允许挂载到body或者html上,因为会被Vue生成的dom替换(符合el特性4)
if (el === document.body || el === document.documentElement) {
warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
获取template
- 判断是否存在render,存在直接忽略el和template选项,因为编译el和template目的就是为了获取render函数
- 是否存在template,存在需要区分2种情况,模版字符串和id
- id:通过idToTemplate函数获取template
- string模版字符串,直接去编译
var options = this.$options;
// 解析template/el并转换为render(存在render直接跳过)
if (!options.render) {
var template = options.template;
// 最终拿去编译的为template
// 存在template模版将替换挂载的元素(符合template特性1)
if (template) {
// 存在template,template接收字符串模版或者#选择符
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 找到id对应的innerHtml(符合template特性2)
template = idToTemplate(template);
if (!template) {
warn(
("Template element not found or is empty: " + (options.template)),
this
);
}
}
// 是Element类型,获取innerHTML
} else if (template.nodeType) {
template = template.innerHTML;
} else {
{
warn('invalid template option:' + template, this);
}
return this
}
}
}
// 寻找template中id对于的Element,返回其内容(存在缓存)
var idToTemplate = cached(function (id) {
var el = query(id);
return el && el.innerHTML
});
// 缓存函数
function cached (fn) {
// 存储已经通过id寻找的template
var cache = Object.create(null);
return (function cachedFn (str) {
// 存在直接返回
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}
- 不存在template就通过el获取outerHTML(包含标签本身),然后赋值给template
if (template) {
// ...忽略
} else {
// 不存在template和render,挂载元素的html会被提取出来当作模版(符合el特性3)
template = getOuterHTML(el);
}
function getOuterHTML (el) {
// outerHTML也innerHTML的区别就是包含标签本身
if (el.outerHTML) {
return el.outerHTML
} else {
// 个人理解至少在$mount这个流程中不会走到这,因为要么不存在el,所以getOuterHTML不执行,要么el存在经过query处理后至少会返回一个<div></div>
var container = document.createElement('div');
container.appendChild(el.cloneNode(true));
return container.innerHTML
}
}
编译模版
获取到template后将其编译成render函数
if (template) {
// 开启编译
var ref = compileToFunctions(template, {
outputSourceRange: true, // 输出范围,编译中每个节点的范围
delimiters: options.delimiters, // 改变纯文本插入分隔符 ['{{', '}}']
comments: options.comments // 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
// 保存render,用于生成vNode给diff使用
options.render = render;
options.staticRenderFns = staticRenderFns;
}
return mount.call(this, el, hydrating)
核心编译的代码,即将来袭,第一次看比较绕,我会拆出几个部分来表达
源码代码串梳理
接着用代码梳理一下执行顺序
首先执行createCompilerCreator函数
var createCompiler = createCompilerCreator(function baseCompile(){...});
function createCompilerCreator (baseCompile) {
// 执行这部分
return function createCompiler (baseOptions) {
function compile (
template,
options
) {
// ...暂时忽略一些代码
}
return {
compile: compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
此时得到createCompiler函数然后执行
const { compile, compileToFunctions } = createCompiler(baseOptions);
createCompiler函数内部执行createCompileToFunctionFn
function createCompileToFunctionFn (compile) {
var cache = Object.create(null);
// 返回这个函数
return function compileToFunctions (
template,
options,
vm
) {
// ... 暂时忽略一些代码
}
}
最后执行得到的compileToFunctions,
if (template) {
// 开启编译
var ref = compileToFunctions(template, {
outputSourceRange: true, // 输出范围,编译中每个节点的范围
delimiters: options.delimiters, // 改变纯文本插入分隔符 ['{{', '}}']
comments: options.comments // 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
// 保存render,用于生成vNode给diff使用
options.render = render;
options.staticRenderFns = staticRenderFns;
}
这一节是教大家怎么梳理函数执行顺序,但是因为存在太多的闭包调用和函数传递,所以接下来这一节就抛弃这些闭包按真正的执行顺序慢慢梳理
compileToFunctions
这个函数的核心功能就两个
- 缓存,缓存编译过的template,避免重复的编译工作量
- 调用编译函数compile
- 通过new Function将compile得到的字符串生成render函数
function createCompileToFunctionFn (compile) {
// 创建存储缓存的对象{key: val}
var cache = Object.create(null);
return function compileToFunctions (
template, // 模版
options, // 参数
vm // Vue实例
) {
options = extend({}, options);
// 检查缓存是否存在
// 默认'{{,}}'+template,避免修改了delimiters
var key = options.delimiters
? String(options.delimiters) + template
: template;
// 存在缓存直接抛出编译结果
if (cache[key]) {
return cache[key]
}
// 编译,compile来自createCompiler内
var compiled = compile(template, options);
var res = {};
var fnGenErrors = [];
// 这里非常重要,非常重要,非常重要,先暂时放一下,后续再聊
// 这里的render也就是compileToFunctions执行后得到的render,用于生成vNode
res.render = new Function(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return new Function(code, fnGenErrors)
});
// 缓存,return
return (cache[key] = res)
}
}
createCompiler
compile来自createCompiler函数,因为省掉了一些代码,所以目前核心目前就1点
- 调用createCompileToFunctionFn传递compile函数
export function createCompilerCreator (baseCompile) {
return function createCompiler (baseOptions) {
function compile (
template,
options
) {
// ... 省去错误收集的源码
// 真正的编译函数
const compiled = baseCompile(template.trim())
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
compile核心就是调用baseCompile函数(本文重点,本文重点,本文重点)
baseCompile函数核心功能如下
- 解析template
- 静态标记
- 生成render字符串
const createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
// 解析template为AST
const ast = parse(template.trim(), options)
// 静态标记
if (options.optimize !== false) {
optimize(ast, options)
}
// 生成render字符串
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
parse
解析template为AST,核心代码就是parseHTML 想将一个字符串解析为AST,我们就需要识别以下这些东西
- 开始标签
- 结束标签
- 文本标签
- 注释
parseHTML结构也如上述枚举,区分这四种情况处理
// 将HTML模板字符串转化为AST
export function parse(template, options) {
// ...
parseHTML(template, {
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
shouldKeepComment: options.comments,
// 当解析到开始标签时,调用该函数
start (tag, attrs, unary) {
},
// 当解析到结束标签时,调用该函数
end () {
},
// 当解析到文本时,调用该函数
chars (text) {
},
// 当解析到注释时,调用该函数
comment (text) {
}
})
return root
}
parseHTML就是利用循环不断的截取html,匹配不同标签,直到html为空为止
function parseHTML(html, options) {
// 节点栈
const stack = [];
// 索引
let index = 0;
// 当前循环的html,为了匹配文本而创建的变量
let last;
// 栈顶的节点名
let lastTag;
// 循环直到html为空(每次匹配都会裁剪html)
while (html) {
last = html;
if (!lastTag) {
// 注释,开始标签,结束标签以这个判断,不是<开头为文本标签
let textEnd = html.indexOf('<');
if (textEnd === 0) {
// ...解析代码都在这个位置
}
}
}
}
解析注释
HTML的注释比较简单,用正则匹配开头(),如果保留注释就添加到栈里,不保留注释就直接抛弃注释内容,截取html
var comment = /^<!\--/;
// 存在注释
if (comment.test(html)) {
// 获取注释结尾的位置
var commentEnd = html.indexOf('-->');
if (commentEnd >= 0) {
// 保留注释
if (options.shouldKeepComment) {
// 调用注释函数
options.comment(
html.substring(4, commentEnd), // 传入<!--xx-->的xx内容(这里的4是因为<!--长度固定为4)
index, // 起始位置
index + commentEnd + 3 // 结束位置(3是因为-->长度固定为3)
);
}
// 移动索引和截取html字符串
advance(commentEnd + 3);
continue
}
}
advance函数作用如下
- 移动索引,用于记录匹配节点的起始和结束位置(或者下一个节点的起始位置)
- 截取html
function advance (n) {
index += n;
html = html.substring(n);
}
比如如下demo
<!--注释-->
<div>坚果</div>
// 解析后html会变成,起始位置为0,结束位置9
<div>坚果</div>
options.comment函数就是之前说的parseHTML.comment函数,主要是为了维护AST栈
export function parse(template, options) {
// ...
parseHTML(template, {
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
comment: function comment (text, start, end) {
var child = {
type: 3, // 文本类型
text: text, // 注释内容
isComment: true // 标记为注释
};
// 开启了统计节点范围就记录开始和结束位置
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
// 给当前父标签添加子标签,比如{type: 1, tag: 'div', children: []}
currentParent.children.push(child);
}
});
}
解析开始标签
正则匹配标签,正则匹配标签,正则匹配标签,重要的事情说三遍。
解析开始标签最重要是获取以下几个信息
- 标签名
- 属性名和属性值
既然最重要是正则表达式,那么就先分析一下正则表达式,暂时忽略unicode码
分析正则
匹配起始和标签名
匹配普通属性和值
匹配动态属性和值
开始标签的结束点
匹配起始标签
const 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/;
const ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
const qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
const startTagOpen = new RegExp(("^<" + qnameCapture));
const startTagClose = /^\s*(\/?)>/;
// 判断是否匹配到开始标签
var startTagMatch = parseStartTag();
/*
demo
<div class="jg" style="color:red">{{name}}</div>
*/
function parseStartTag () {
// start = ['<div', 'div', index: 0, input: '<div class="jg" style="color:red">{{name}}</div>', groups: undefined]
var start = html.match(startTagOpen);
if (start) {
// 存储开始标签的信息
var match = {
tagName: start[1], // 标签名
attrs: [], // 属性
start: index // 开始位置
};
/*
移动index和截取html,html删掉<div
html = (空格)class="jg" style="color:red">{{name}}</div>
*/
advance(start[0].length);
var end, attr;
/*
!(end = html.match(startTagClose)) 没有匹配>或者/>时去获取属性
attr = html.match(dynamicArgAttribute) || html.match(attribute)获取动态和普通属性
*/
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
/*
第一次循环attr = [' class="jg"', 'class', '=', 'jg']
第二次循环attr = [' style="color:red"', 'style', '=', 'color:red']
*/
attr.start = index; // 记录属性起始位置
advance(attr[0].length); // 截取字符串
attr.end = index; // 记录属性结束位置
match.attrs.push(attr); // 存储属性
}
// html = >{{name}}</div>
// 匹配到>或者/>,
if (end) {
// 是否自闭合,/>
match.unarySlash = end[1]; // (end[1] === '/')
/*
截取字符串和移动index
html = {{name}}</div>
*/
advance(end[0].length);
// 记录开始标签结束位置
match.end = index;
return match
}
}
}
获取到的开始标签信息,需要整理一下startTagMatch.attrs的内容,将其整理成{class: 'jg', start, end},并且维护标签栈信息,最后触发parseHTML.start
/*
startTagMatch = {
attrs: [
[' class="jg"', 'class', '=', 'jg', start: 5, end: 15],
[' style="color:red"', 'style', '=', 'color:red',start: 16, end: 33]
],
end: 34
start: 0
tagName: "div"
unarySlash: ""
}
*/
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
function handleStartTag(match) {
const tagName = match.tagName;
const unarySlash = match.unarySlash;
// 是否要求为正确的html
if (expectHTML) {
// 栈顶为p标签(当前匹配标签的父标签),内部不允许包含div等等元素,直接闭合
// <p><div>1</div></p> -> <p></p>
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag(lastTag);
}
// <td>1<td>2</td></td> -> <td>1</td>
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag(tagName);
}
}
// 是否不需要闭合的标签(link,meta等)或者已经闭合的标签,
const unary = isUnaryTag(tagName) || !!unarySlash;
const l = match.attrs.length;
const attrs = new Array(l);
// 修改match.attrs属性
for (let i = 0; i < l; i++) {
const args = match.attrs[i];
/*
args[3]:双引号内的内容
args[4]:单引号内的内容
args[5]:没符号包裹的值
*/
const value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
value: value
};
if (options.outputSourceRange) {
attrs[i].start = args.start + args[0].match(/^\s*/).length;
attrs[i].end = args.end;
}
}
// 没闭合就添加到标签栈顶
if (!unary) {
stack.push({
tag: tagName,
lowerCasedTag: tagName.toLowerCase(),
attrs: attrs,
start: match.start,
end: match.end
});
// 修改栈顶标签
lastTag = tagName;
}
// 调用parseHTML.start函数
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
}
接着就是调用parseHTML.start函数
生成AST
function parse(template, options) {
// 父标签
let currentParent
// 是否拥有v-pre或者在v-pre的标签下
let inVPre = false
parseHTML(template, {
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
/*
tag = div
attrs = [{"name":"class","value":"jg","start":5,"end":15}, {"name":"style","value":"color:red","start":16,"end":33}]
unary = false
start = 0
end = 34
*/
start(tag, attrs, unary, start, end) {
/*
创建AST节点
element = {
"type":1,
"tag":"div",
"attrsList":[{"name":"class","value":"jg","start":5,"end":15},{"name":"style","value":"color:red","start":16,"end":33}],
"attrsMap":{"class":"jg","style":"color:red"},
"rawAttrsMap":{},
"children":[]
}
*/
let element = createASTElement(tag, attrs, currentParent);
{
// 记录范围
if (options.outputSourceRange) {
element.start = start;
element.end = end;
element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
cumulated[attr.name] = attr;
return cumulated;
}, {});
}
}
// ... 后续代码衔接在这
}
})
}
// 创建AST节点
function createASTElement(tag, attrs, parent) {
return {
type: 1, // 标签节点为1
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
rawAttrsMap: {},
parent, // 存储父标签
children: [] // 存储子标签
};
}
处理 v-pre
function parse(template, options) {
// 父标签
let currentParent
// 是否拥有v-pre或者在v-pre的标签下
let inVPre = false
parseHTML(template, {
// ...省略上节代码
start(tag, attrs, unary, start, end) {
// ...省略上节代码
if (!inVPre) {
// 处理是否存在v-pre属性
processPre(element);
if (element.pre) {
inVPre = true;
}
}
// 存在v-pre属性
if (inVPre) {
processRawAttrs(element);
}
// 下一部分代码
}
})
}
// 存在将element添加pre属性
function processPre(el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true;
}
}
// 判断是否存在某个属性,并且删除attrsList内属性
function getAndRemoveAttr(el, name, removeFromMap) {
let val;
if ((val = el.attrsMap[name]) != null) {
const list = el.attrsList;
for (let i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
// 删除v-pre属性
list.splice(i, 1);
break;
}
}
}
if (removeFromMap) {
delete el.attrsMap[name];
}
return val;
}
// 处理属性,因为v-pre为直接跳过自身和子标签的编译,所以直接将属性值转为字符串即可
function processRawAttrs(el) {
const list = el.attrsList;
const len = list.length;
if (len) {
const attrs = (el.attrs = new Array(len));
for (let i = 0; i < len; i++) {
attrs[i] = {
name: list[i].name,
value: JSON.stringify(list[i].value)
};
if (list[i].start != null) {
attrs[i].start = list[i].start;
attrs[i].end = list[i].end;
}
}
}
else if (!el.pre) {
// 没有属性的非根节点
el.plain = true;
}
}
处理v-for
v-if,v-once等等都差不多,都是给element添加属性
if (inVPre) {
processRawAttrs(element);
} else {
processFor(element);
processIf(element);
processOnce(element);
}
// ...代码衔接出
function processFor(el) {
let exp;
// 存在v-for属性
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
// 处理v-for
const res = parseFor(exp);
// 揉合element和res数据
if (res) {
extend(el, res);
}
else {
warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for']);
}
}
}
// 匹配v-for="(item, index) in list"中的(item, index) in list
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
// 匹配逗号后的字符 ,index)
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/;
// 去除()
const stripParensRE = /^\(|\)$/g;
function parseFor(exp) {
// inMatch = ['(item, index) in list', '(item, index)', 'list', index: 0, input: '(item, index) in list', groups: undefined]
const inMatch = exp.match(forAliasRE);
if (!inMatch)
return;
const res = {};
// 获取数据源,res.for = list
res.for = inMatch[2].trim();
// 删除(),alias = item, index
const alias = inMatch[1].trim().replace(stripParensRE, '');
// iteratorMatch = [', index', ' index', undefined, index: 4, input: 'item, index', groups: undefined]
const iteratorMatch = alias.match(forIteratorRE);
if (iteratorMatch) {
// 把,后的删除,res.alias = item
res.alias = alias.replace(forIteratorRE, '').trim();
// 存储index
res.iterator1 = iteratorMatch[1].trim();
// 可能循环对象(key, value, index),所以iteratorMatch[2]存在
if (iteratorMatch[2]) {
res.iterator2 = iteratorMatch[2].trim();
}
}
else {
// 不存在,后的内容
res.alias = alias;
}
// res = {alias: "item", for: "list", iterator1: "index"}
return res;
}
处理其它
// 是否存在根element,不存在赋值为当前element
if (!root) {
root = element;
}
// 是否为自闭合标签
if (!unary) {
// 修改栈顶标签为当前element
currentParent = element;
// 维护AST栈
stack.push(element);
} else {
// 自闭合标签
closeElement(element);
}
function closeElement(element) {
// 处理key,v-bind,v-on,ref等等
if (!inVPre && !element.processed) {
element = processElement(element, options);
}
// 是v-pre标签,修改inVPre值
if (element.pre) {
inVPre = false;
}
}
解析结束标签
和匹配开始的标签差不多,只不过变成了</开头,>结尾
/*
还是之前的demo
<div class="jg" style="color:red">{{name}}</div>
html = </div>
*/
const endTag = /^<\/((?:[a-zA-Z_][\-\.0-9_a-zA-Z]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Z]*)[^>]*>/
// endTagMatch = ['</div>', 'div', index: 0, input: '</div>', groups: undefined]
const endTagMatch = html.match(endTag);
if (endTagMatch) {
// 记录起始索引
const curIndex = index;
// 截取html,移动索引
advance(endTagMatch[0].length);
// 传入标签名,起始位置,结束位置
parseEndTag(endTagMatch[1], curIndex, index);
continue;
}
function parseEndTag(tagName, start, end) {
let pos, lowerCasedTagName;
if (start == null)
start = index;
if (end == null)
end = index;
/*
找到对应的标签栈的位置,此刻pos=0,可能存在<div><span></div>,所以需要找到对应的标签名
stack = [{"tag":"div","lowerCasedTag":"div","attrs":[{"name":"class","value":"jg","start":5,"end":15},{"name":"style","value":"color:red","start":16,"end":33}],"start":0,"end":34}]
*/
if (tagName) {
lowerCasedTagName = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break;
}
}
}
else {
// If no tag name is provided, clean shop
pos = 0;
}
// 匹配到对应的栈
if (pos >= 0) {
/*
闭合该标签下所有该标签
1. <div><span>xx</div> -> <div><span>xx</span></div>
2. <div>xx<span></div> -> <div>xx<span></span></div>
*/
for (let i = stack.length - 1; i >= pos; i--) {
if ((i > pos || !tagName) && options.warn) {
options.warn(`tag <${stack[i].tag}> has no matching end tag.`, {
start: stack[i].start,
end: stack[i].end
});
}
// parseHTML.end,闭合标签
if (options.end) {
// 传入正确的栈内起始标签信息
options.end(stack[i].tag, start, end);
}
}
// 出栈
stack.length = pos;
// 最后标签为出栈后栈顶的内容
lastTag = pos && stack[pos - 1].tag;
}
// 特殊处理</br>,当作自闭合标签处理
else if (lowerCasedTagName === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end);
}
}
// 特殊处理p, 1111</p> ->渲染成1111<p></p>
else if (lowerCasedTagName === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
parseHTML.end就很简单了,维护AST栈,修改当前标签即可
function parse(template, options) {
let currentParent
parseHTML(template, {
end(tag, start, end) {
const element = stack[stack.length - 1];
// 出栈
stack.length -= 1;
currentParent = stack[stack.length - 1];
if (options.outputSourceRange) {
element.end = end;
}
closeElement(element);
}
}
}
解析文本
/*
demo: <div class="jg" style="color:red">{{name}}文本<文本{{age}}</div>
经过开始标签的解析,html = {{name}}文本<文本{{age}}</div>
*/
function parseHTML(html, options) {
// 最后一个节点类型
let lastTag;
while (html) {
last = html;
if (!lastTag) {
// 注释,开始标签,结束标签以这个判断,不是<开头为文本标签
let textEnd = html.indexOf('<');
if (textEnd === 0) {
// ...省略其它解析
} else {
// 文本解析
let text, rest, next;
// {{name}}文本<文本{{age}}</div>时
// textEnd = 10
if (textEnd >= 0) {
// rest = <文本{{age}}</div>
rest = html.slice(textEnd);
// 在文本内允许存在<字符,所以需要不端循环,直到匹配到结束标签,开始标签,注释
while (!endTag.test(rest) &&
!startTagOpen.test(rest) &&
!comment.test(rest) &&
!conditionalComment.test(rest)) {
// 跳过第一个,next = 10
next = rest.indexOf('<', 1);
if (next < 0)
break;
textEnd += next;
rest = html.slice(textEnd);
}
// text = {{name}}文本<文本{{age}}
text = html.substring(0, textEnd);
}
// 没有<,全部为文本
if (textEnd < 0) {
text = html;
}
// 截取html,移动index
if (text) {
advance(text.length);
}
// 触发parseHTML.chars
if (options.chars && text) {
options.chars(text, index - text.length, index);
}
}
}
}
function parse(template, options) {
let currentParent
parseHTML(template, {
// ...
chars(text, start, end) {
// 必须用标签包裹,template = '111'会报错
if (!currentParent) {
{
if (text === template) {
warnOnce('Component template requires a root element, rather than just text.', { start });
}
else if ((text = text.trim())) {
warnOnce(`text "${text}" outside root element will be ignored.`, {
start
});
}
}
return;
}
// 文本必然为栈顶标签的子节点
const children = currentParent.children;
if (text) {
let res;
let child;
// 不在v-pre标签内,是否是动态文本,也就是存在{{}}
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
};
}
// 普通文本
else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
};
}
// 添加到父标签的children里,维护AST栈
if (child) {
if (options.outputSourceRange) {
child.start = start;
child.end = end;
}
children.push(child);
}
}
}
})
}
// 提取文本
function parseText(text, delimiters) {
// const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const tagRE = defaultTagRE;
// 没有匹配到{{...}}
if (!tagRE.test(text)) {
return;
}
const tokens = [];
const rawTokens = [];
// tagRE.lastIndex:每次正则匹配的起始位置,1234,第一次匹配1,lastIndex = 1,第二次匹配就从2开始
let lastIndex = (tagRE.lastIndex = 0);
let match, index, tokenValue;
// 开始匹配{{}}
while ((match = tagRE.exec(text))) {
/*
第一次匹配match = ['{{name}}', 'name', index: 0, input: '{{name}}文本<文本{{age}}', groups: undefined]
第二次匹配match = ['{{age}}', 'age', index: 13, input: '{{name}}文本<文本{{age}}', groups: undefined]
*/
index = match.index;
/*
匹配普通文本
第一次匹配不进入这里,因为index=0,lastIndex=0
第二次匹配index=13,lastIndex=8
*/
if (index > lastIndex) {
rawTokens.push((tokenValue = text.slice(lastIndex, index)));
tokens.push(JSON.stringify(tokenValue));
}
const exp = match[1].trim();
// _s()后续会转为支持的函数,目前以字符串拼接
tokens.push(`_s(${exp})`);
rawTokens.push({ '@binding': exp });
lastIndex = index + match[0].length;
}
// {{age}}文本,这种情况会把"文本"加入rawTokens,tokens
if (lastIndex < text.length) {
rawTokens.push((tokenValue = text.slice(lastIndex)));
tokens.push(JSON.stringify(tokenValue));
}
/*
expression: "_s(name)+\"文本<文本\"+_s(age)",
tokens: [@binding: "name", "文本<文本", {@binding: 'age'}]
*/
return {
expression: tokens.join('+'),
tokens: rawTokens
};
}
到这一步我们得到的AST结构为
{
"type": 1,
"tag": "div",
"attrsList": [],
"attrsMap": {
"class": "jg",
"style": "color:red"
},
"rawAttrsMap": {
"class": {
"name": "class",
"value": "jg",
"start": 5,
"end": 15
},
"style": {
"name": "style",
"value": "color:red",
"start": 16,
"end": 33
}
},
"children": [
{
"type": 2,
"expression": "_s(name)+\"文本<文本\"+_s(age)",
"tokens": [
{
"@binding": "name"
},
"文本<文本",
{
"@binding": "age"
}
],
"text": "{{name}}文本<文本{{age}}",
"start": 34,
"end": 54
}
],
"start": 0,
"end": 60,
"plain": false,
"staticClass": "\"jg\"",
"staticStyle": "{\"color\":\"red\"}"
}
generate
由于篇幅问题这里不再介绍如何生成render
function generate(ast, options) {
const state = new CodegenState(options);
const code = ast
? ast.tag === 'script'
? 'null'
: genElement(ast, state)
: '_c("div")';
return {
// 利用with绑定this,最后调用的this指向vue实例,所以可以访问到methods或者data等
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
};
}
<div class="jg" :class="[other]" style="color:red" @click="handleClick" v-if="show">
{{name}}文本<文本{{age}}
<div v-for="i in list" :key="i"></div>
</div>
// 输出render-------
`with(this){
return (c) ?
_c('div',{
staticClass:"jg",
class:[other],
staticStyle:{"color":"red"},
on:{"click":handleClick}},
[
_v(_s(name)+"文本<文本"+_s(age)),
_l((list), function(i){return _c('div',{key:i})})
],2)
: _e()
}`
再次回到compileToFunctions
function createCompileToFunctionFn (compile) {
// 创建存储缓存的对象{key: val}
var cache = Object.create(null);
return function compileToFunctions (
template, // 模版
options, // 参数
vm // Vue实例
) {
// 。。。
var res = {};
// 将刚刚返回的render字符串,利用new Function生成一个函数
res.render = new Function(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return new Function(code, fnGenErrors)
});
// 缓存,return
return (cache[key] = res)
}
}
render是在渲染Watcher中执行如下函数
updateComponent = () => {
// _render执向vm,所以with内的代码的this全部指向vm
// 又因为渲染前已经做好了事件,data,methods等等代理,所以都可以访问到
vm._update(vm._render(), hydrating);
};
也就是会遇到_c(),_l(),_s(),这些又是什么呢? 只有一个目的,根据各种情况生成vNode!!!
function installRenderHelpers(target) {
// 其实没有这里没有_c,为了方便看直接加一起了
target._c = (a, b, c, d) => createElement$1(vm, a, b, c, d, false);
target._o = markOnce;
target._n = toNumber;
target._s = toString;
target._l = renderList;
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;
}
// Vue引入的时候执行
function renderMixin(Vue) {
// 注册辅助函数
installRenderHelpers(Vue.prototype);
}
比如createElement$1
function createElement$1(context, tag, data, children, normalizationType, alwaysNormalize) {
return new VNode(tag, data, children, undefined, undefined, context);
}
class VNode {
constructor(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
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;
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child() {
return this.componentInstance;
}
}
经过这一串的处理,最终将vNode传递给patch,最后渲染页面。