1.元素挂载顺序
vue 元素挂载的顺序之分
- 如果存在 render 函数,就用 render 函数渲染组件;
- 如果没有 render 函数,但是存在 template,就将 template 中的内容编译成 render 函数,最后做渲染;
- 如果既没有 render 函数也没有 template 函数,就获取 el 里的内容作为 template,同样编译成 render 函数。
Vue.prototype.$mount = function (el) {
// 挂载
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
vm.$el = el;
if (!options.render) {
// 没有render方法
let template = options.template;
if (!template && el) {
// 没有template 但是有el 获取el中的内容
template = el.outerHTML;
}
// 将模板编译成render函数
const render = compileToFunctions(template);
options.render = render;
}
// 渲染时用的都是render函数
// 挂载组件
mountComponent(vm, el);
};
2.模板编译函数
从模板到 render 函数主要分为三个部分:
- 将 html 代码转换成 ast 语法树
- 优化静态节点
- 通过 ast 树 重新生成代码
export function compileToFunctions(template) {
// html模板 => render函数
// 将html代码转换成ast语法树
let ast = parseHTML(template);
console.log(ast);
// 2.优化静态节点
// 3.通过这棵树 重新生成代码
let code = generate(ast);
console.log(code);
// 4.将字符串变成函数
// 通过with来进行取值
// 稍后调用render函数的时候改变this
let render = new Function(`with(this){return ${code}}`);
console.log(render);
return render;
}
一个简单的例子:
<div id="app" style="color:red">
<div>{{name}} hello<span>world</span></div>
</div>
code 代码:
_c('div',{id:"app",style{"color":"red"}},
_c('div',undefined,_v(_s(name)+"hello"),_c('span',undefined,_v("world"))))
render 函数:
ƒ anonymous() {
with(this){
return _c('div',{id:"app",style:{"color":"red"}},
_c('div',undefined,_v(_s(name)+"hello"),_c('span',undefined,_v("world"))))
}
}
3.将模板代码转换成 ast 语法树
通过正则循环解析模板生成语法树
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`);
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`);
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
const startTagClose = /^\s*(\/?)>/;
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
export function parseHTML(html) {
function createASTElement(tagName, attrs) {
return {
tag: tagName, // 标签名
type: 1, // 元素类型
children: [], // 子元素列表
attrs, // 属性集合
parent: null, // 父元素
};
}
let root;
let currentParent;
let stack = []; // 栈结构
// 标签是否符号预期 <div><span></span></div>
function start(tagName, attrs) {
let element = createASTElement(tagName, attrs);
if (!root) {
root = element;
}
currentParent = element; // 当前解析的标签
stack.push(element);
}
function end(tagName) {
// 在结尾标签处创建父子关系
let element = stack.pop(); // 取最后一个
currentParent = stack[stack.length - 1];
if (currentParent) {
// 在闭合时可以知道父亲是谁
element.parent = currentParent;
currentParent.children.push(element);
}
}
function chars(text) {
text = text.replace(/\s/g, "");
if (text) {
currentParent.children.push({
type: 3,
text,
});
}
}
while (html) {
// 一直解析 直到字符串为空
let textEnd = html.indexOf("<");
if (textEnd == 0) {
const startTagMatch = parseStartTag(); // 开始标签匹配的结果
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
const endTagMatch = html.match(endTag);
if (endTagMatch) {
// 处理结束标签
advance(endTagMatch[0].length);
end(endTagMatch[1]); // 传入结束标签
continue;
}
}
let text;
if (textEnd > 0) {
// 是文本
text = html.substring(0, textEnd);
}
if (text) {
// 处理文本
advance(text.length);
chars(text);
}
}
function advance(n) {
// 将字符串进行截取操作 更新html内容
html = html.substring(n);
}
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1],
attrs: [],
};
advance(start[0].length); //删除开始标签
let end;
let attr;
// 不是结尾标签 并且可以匹配到属性
while (
!(end = html.match(startTagClose)) &&
(attr = html.match(attribute))
) {
match.attrs.push({
name: attr[1],
value: attr[3] || attr[4] || attr[5],
});
advance(attr[0].length);
}
if (end) {
advance(end[0].length);
return match;
}
}
}
return root;
}
4.通过 ast 语法树重新生成代码
把 ast 语法树转化成
_c
(创建节点)
_v
(创建文本)
_s
(获取对象文本)
的代码字符串
// 编写: <div id="app" style="color:red">hello {{name}}<span>hello</span></div>
// 结果: render() {
// return _c('div',{id:'app',style:{color:'red'}},_v('hello'+_s(name)),_c('span',null,_v('hello')))
// }
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
function genProps(attrs) {
let str = "";
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === "style") {
let obj = {};
attr.value.split(";").forEach((item) => {
let [key, value] = item.split(":");
obj[key] = value;
});
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`;
}
function gen(node) {
if (node.type == 1) {
return generate(node);
} else {
let text = node.text;
if (!defaultTagRE.test(text)) {
// 如果是普通文本
return `_v(${JSON.stringify(text)})`;
}
// _v('hello {{name}}') =》 _v('hello'+_s(name))
let tokens = []; // 存放每一段的代码 最后join
let lastIndex = (defaultTagRE.lastIndex = 0); // 正则是全局模式,每次使用前都置为0
let match, index; // 每次匹配结果
while ((match = defaultTagRE.exec(text))) {
index = match.index; // 保存匹配到的索引
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
tokens.push(`_s(${match[1].trim()})`);
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
return `_v(${tokens.join("+")})`;
}
}
function genChildren(el) {
const children = el.children;
if (children) {
// 将多个转化后的儿子用逗号拼接起来
return children.map((child) => gen(child)).join(",");
}
}
export function generate(el) {
let children = genChildren(el);
let code = `_c('${el.tag}',${
el.attrs.length ? `${genProps(el.attrs)}` : "undefined"
}${children ? `,${children}` : ""})`;
return code;
}
5.将字符串变成函数
// 通过with来进行取值(锁定变量作用域) 稍后调用render函数的时候改变this
let render = new Function(`with(this){return ${code}}`);
6.通过 render 函数产生虚拟节点
export function renderMixin(Vue) {
Vue.prototype._c = function () {
// 创建虚拟dom元素
return createElement(...arguments);
};
Vue.prototype._s = function (val) {
// stringify
return val == null
? ""
: typeof val == "object"
? JSON.stringify(val)
: val;
};
Vue.prototype._v = function (text) {
// 创建虚拟文本元素
return createTextVnode(text);
};
Vue.prototype._render = function () {
const vm = this;
const render = vm.$options.render;
let vnode = render.call(vm);
console.log(vnode);
return vnode;
};
}
function createElement(tag, data = {}, ...children) {
return vnode(tag, data, data.key, children);
}
function createTextVnode(text) {
return vnode(undefined, undefined, undefined, undefined, text);
}
// 用来产生虚拟dom
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text,
};
}
7.将虚拟节点转化成真实节点
export function patch(oldVnode, vnode) {
// 将虚拟节点转化成真实节点
let el = createElm(vnode); // 产生真实的dom
let parentElm = oldVnode.parentNode; // 获取老的app的父亲-》 body
parentElm.insertBefore(el, oldVnode.nextSibling); // 当前真实元素的后面
parentElm.removeChild(oldVnode); // 删除老的节点
}
function createElm(vnode) {
let { tag, children, key, data, text } = vnode;
if (typeof tag == "string") {
// 创建元素 放到vnode.el上
vnode.el = document.createElement(tag);
children.forEach((child) => {
// 遍历儿子 将子节点渲染后的结果添加到父节点中
vnode.el.appendChild(createElm(child));
});
} else {
// 创建文件,放到vnode.el上
vnode.el = document.createTextNode(text);
}
return vnode.el;
}
8.总结
Vue 模板编译这块的整体逻辑主要分为三步:
解析器 将模版字符串转换成 ast 语法树
优化器 是对 AST 进行静态节点标记,主要用来做虚拟 DOM 的渲染优化
代码生成器 是使用 AST 生成 render 函数代码
html 语法解析
生成 ast 语法树
生成代码
生成 render 函数
生成虚拟 dom
生成真实 dom,并替换老 dom
// vue 源码中写函数的含义
function installRenderHelpers(target) {
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;
}
编译过程: