Skip to main content

Vue的模板编译

· 9 min read
LIU

1.元素挂载顺序

vue 元素挂载的顺序之分

  1. 如果存在 render 函数,就用 render 函数渲染组件;
  2. 如果没有 render 函数,但是存在 template,就将 template 中的内容编译成 render 函数,最后做渲染;
  3. 如果既没有 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 函数主要分为三个部分:

  1. 将 html 代码转换成 ast 语法树
  2. 优化静态节点
  3. 通过 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 模板编译这块的整体逻辑主要分为三步:

  1. 解析器 将模版字符串转换成 ast 语法树

  2. 优化器 是对 AST 进行静态节点标记,主要用来做虚拟 DOM 的渲染优化

  3. 代码生成器 是使用 AST 生成 render 函数代码

  4. html 语法解析

  5. 生成 ast 语法树

  6. 生成代码

  7. 生成 render 函数

  8. 生成虚拟 dom

  9. 生成真实 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;
}

编译过程:

image-20220619162950806

image-20220515204753673