Vue2核心原理(简易版)-模版编译
输入了什么
<div id="app" a=1 style="color:red;background:blue">
hello {{arr}} world
</div>
let vm = new Vue({
data() {
//this = vm
return {
arr: { name: 'zf' }
}
}
});
vm.$mount('#app')
我们的预期
- 提取出
id="app"这个元素的HTML模板编译成渲染函数
正题,是怎么实现的?
先来看下我画的一张思维导图,大概看一眼
核心策略就是首先用正则对html模版进行词法分析,解析出不同的token,在解析这些token的时候可以得到对应的标签开始、结束以及里面的文本,利用一个栈结构不断的入栈、出栈生成AST树,最后将AST树转化成代码,再构建虚拟dom,进而生成真实dom,patch到页面上去。
- 用正则分解出不同的token(tokenizing)。大致有以下的几种token:
a. 开始标签开始
b. 标签名
c. 属性
d. 开始标签闭合
e. 文本
f. 闭合标签
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; // 用来获取的标签名的 match后的索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配开始标签的
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配闭合标签的
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // a=b a="b" a='b'
const startTagClose = /^\s*(\/?)>/; // /> <div/>
- 解析后的token用栈结构生成AST树(parse)。根据分解时的不同词法token,我们可以得到对应的如下流程:
对应的,我们根据分解时获取到的不同状态,可以得到如下的生成过程:export function parserHTML(html) { function advance(len) { html = html.substring(len); } 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 false; // 不是开始标签 } while (html) { // 看要解析的内容是否存在,如果存在就不停的解析 let textEnd = html.indexOf('<'); // 当前解析的开头 if (textEnd == 0) { const startTagMatch = parseStartTag(html); // 解析开始标签 if (startTagMatch) { start(startTagMatch.tagName, startTagMatch.attrs) continue; } const endTagMatch = html.match(endTag); if (endTagMatch) { end(endTagMatch[1]); advance(endTagMatch[0].length); continue; } } let text; // // </div> if (textEnd > 0) { text = html.substring(0, textEnd) } if (text) { chars(text); advance(text.length); } } return root; }let root = null; let stack = []; function start(tagName, attributes) { let parent = stack[stack.length - 1]; let element = createAstElement(tagName, attributes); if (!root) { root = element; } if(parent){ element.parent = parent;// 当放入栈中时 继续父亲是谁 parent.children.push(element) } stack.push(element); } function end(tagName) { let last = stack.pop(); if (last.tag !== tagName) { throw new Error('标签有误'); } } function chars(text) { text = text.replace(/\s/g, ""); let parent = stack[stack.length - 1]; if (text) { parent.children.push({ type: 3, text }) } } - 生成代码(codegen)。我们最后生成的代码是一种给render函数运行的字符串形式
_c('div',{id:'app',a:1},_c('span',{},'world'),_v(text))。其中_c方法的三个参数分别代表了标签名、属性键值对的集合、子元素集合。具体过程如下图:
_c和_v方法就是分别生成元素节点和文本节点vdom的两个方法,我们将这两个方法和我们生成的render函数挂载到Vue原型,这样我们就可以随处使用啦!
function createElement(vm, tag, data = {}, ...children) { return vnode(vm, tag, data, data.key, children, undefined); } function createTextElement(vm, text) { return vnode(vm, undefined, undefined, undefined, undefined, text); } function vnode(vm, tag, data, key, children, text) { return { vm, tag, data, key, children, text, // ..... } } Vue.prototype._c = function(){ // createElement return createElement(this,...arguments) } Vue.prototype._v = function (text) { // createTextElement return createTextElement(this,text) } Vue.prototype._s = function(val){ // stringify if(typeof val == 'object') return JSON.stringify(val) return val; } Vue.prototype._render = function(){ const vm = this; let render = vm.$options.render; // 就是我们解析出来的render方法,同时也有可能是用户写的 let vnode = render.call(vm); return vnode; } - 挂载组件($mount)。最后,当组件更新或者是$mount的时候,我们就可以调用
vm._render()获取到我们的vdom,再进行diff等操作,生成真实dom渲染到浏览器上了!export function mountComponent(vm, el) { // 更新函数 数据变化后 会再次调用此函数 let updateComponent = () => { // 调用render函数,生成虚拟dom vm._update(vm._render()); // 后续更新可以调用updateComponent方法 // 用虚拟dom 生成真实dom } updateComponent(); }