模板编译
vue初次渲染=>先初始化数据=>将模版进行编译=>变成render()=>生成虚拟节点=>变成真实的dom=>放到页面 vue模版编译
- template
- render
- el 注意el(必须有)
优先级
js
import { initState } from "./initState";
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
let vm = this;
vm.$options = options;
// 初始化状态
initState(vm);
//渲染模版
//1.如果有el走且有template 则用template
//2.需要将模板编译成render函数
//3.如果有el但没有template 则用outerHTML
//4.render函数
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
// $mount 挂载方法
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el); //获取元素
vm.$el = el;
let options = vm.$options;
if (!options.render) {
// 没有render 需要模板编译
let template = options.template;
//3----------
if (!template && el) {
//获取html
el = el.outerHTML;
} else {
}
}
};
}创建ast语法树
js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //标签名称
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //<span:xx>
const startTagOpen = new RegExp(`^<${qnameCapture}`); //标签开头的正则,捕获的内容上标签名
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); //匹配标签结尾的</div>
const attribute =
/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; //匹配属性的
const startTagClose = /^\s*(\/?)>/; //匹配标签结尾的>
const defaultTagRE = /\{\{((?:.|\r|\n)+?)\}\}/g; //匹配{{}}
//el=<div id="app"><div>{{msg}}</div></div>
export function compileToFunction(el) {
// 1. 处理模板
parseHTML(el);
}
//遍历
//创建一个ast对象
function createASTElement(tag, attrs) {
return {
tag, //元素
attrs,
type: 1, //元素类型
children: [],
parent: null,
};
}
let root; //根元素
let currentParent; //当前父元素
let stack = []; //数据结构 栈
function start(tag, attrs) {
let element = createASTElement(tag, attrs);
if (!root) {
root = element;
}
currentParent = element;
stack.push(element);//-----入栈
}
//获取文本
function charts(text) {
text=text.replace(/\s/g,'')
if(text){
currentParent.children.push({
type:3,
text
})
}
}
//结束标签
function end(tag) {
let element = stack.pop();//出栈
currentParent = stack[stack.length - 1];
if(currentParent){//元素的闭合
element.parent = currentParent;
currentParent.children.push(element);
}
}
//解析html
function parseHTML(html) {
//开始标签
//文本
//结束标签
while (html) {
//判断标签
let textEnd = html.indexOf("<");
if (textEnd === 0) {
//开始标签
const startTagMatch = parseStartTag();
if (startTagMatch) {
start(startTagMatch.tagName, startTagMatch.attrs);
continue;
}
//结束标签
let 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);
charts(text);
}
}
function parseStartTag() {
const start = html.match(startTagOpen);
if (start) {
const match = {
tagName: start[1],
attrs: [],
};
//匹配到开始标签,截取掉
advance(start[0].length);
let attr, end;
//属性
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;
}
}
function advance(n) {
html = html.substring(n);
}
return root
}
//ast语法树
/**
* {
* tag:'div',
* attrs:[{id:'app'}],
* children:[{tag:null,text:'hello'},{tag:null,text:'world'}]
* }
*/![[Pasted image 20240630000138.png]]
将ast语法树变成render函数字符串
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/compile/index.js
import { parseHTML } from "./parseAst.js";
import { generate } from "./generate.js";
export function compileToFunction(el) {
// 1.把模板变成ast语法树
let ast = parseHTML(el);
console.log(ast, 2222);
//2. ast语法树生成render函数
//2.1 ast语法树变成字符串
//2.2 字符串变成函数
let code = generate(ast);
//3. render函数变成虚拟dom
console.log(code, 3333);
}js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/compile/generate.js
export function generate(el) {
/**
* _c:解析标签
* _v:解析文本
* _s:解析变量
*/
/**
* 1.先检查元素标签,
* 2.先判断是否需要解析属性,然后解析属性
*/
let children = genChildren(el);
let code = `_c('${el.tag}',${el.attrs.length ? `${genProps(el.attrs)}` : "undefined"}${children ? `,${children}` : ""})`;
console.log(code);
return code;
}
/**
* {type: 3, text: 'hello{{msg}}'}
* {tag: 'h1', attrs:[], type: 1, children: [{type: 3, text: '1111'}], parent: {},tag:'h1',type:1}
*/
//处理子集1
function genChildren(el) {
let children = el.children;
if (children) {
return children.map((child) => gen(child)).join(",");
}
}
/**
* 没解析的时候是
* [
* {name: 'id', value: 'app'},
* {name: 'class', value: 'title name value'},
* {name: 'style', value: 'color: red; font-size: 20px'}
* ]
*/
//处理属性,只解析样式然后其他的都是拼接的
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, val] = item.split(":");
obj[key] = val;
});
attr.value = obj;
}
//拼接
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0, -1)}}`; //移除最后一个逗号
}
const defaultTagRE = /\{\{((?:.|\r|\n)+?)\}\}/g; //匹配{{}}
//处理子集2
function gen(node) {
/**
* 1是元素
* 3是文本
*/
if (node.type === 1) {
debugger;
return generate(node);
} else {
let text = node.text;
if (!defaultTagRE.test(text)) {
//没有差值表达式
return `_v(${JSON.stringify(text)})`;
}
let tokens = [];
//TODO:正则匹配每次匹配需要重置为0
/**
* 什么情况需要把正则的lastIndex归0
* 1. 重新使用正则表达式进行新的匹配:当你想要重新开始匹配时,如果之前的匹配操作没有完成,lastIndex可能不会自动归零。这种情况下,手动将lastIndex归零是必要的。
* 2。 在不同字符串之间切换匹配:如果你用同一个正则表达式对象去匹配不同的字符串,为了避免lastIndex干扰新的字符串匹配,需要手动重置lastIndex。
* 3. 使用String.prototype.replace或String.prototype.split等方法后:虽然这些方法内部会处理lastIndex,但如果在这些方法之间有手动操作正则表达式,最好确保lastIndex是重置的。
* 4. 避免意外匹配结果:当多次执行exec方法或在循环中使用正则表达式进行匹配时,如果不重置lastIndex,可能会得到意外的匹配结果。
*/
let lastIndex = (defaultTagRE.lastIndex = 0); //重置正则
let match, index;
while ((match = defaultTagRE.exec(text))) {
index = match.index;
//为什么需要判断index>lastIndex
//因为正则匹配是贪婪匹配,会尽可能多的匹配
if (index > lastIndex) {
//如果index>lastIndex说明没有匹配到,需要把中间的文本push到tokens中
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("+")})`;
}
}
}
/**
* render(){
* return _c('div',{id:app},_v('hello'+_s(msg)),_c)
* }
*/[!question] 正则匹配每次匹配需要重置为0
- 重新使用正则表达式进行新的匹配:当你想要重新开始匹配时,如果之前的匹配操作没有完成,lastIndex可能不会自动归零。这种情况下,手动将lastIndex归零是必要的。
- 在不同字符串之间切换匹配:如果你用同一个正则表达式对象去匹配不同的字符串,为了避免lastIndex干扰新的字符串匹配,需要手动重置lastIndex。
- 使用String.prototype.replace或String.prototype.split等方法后:虽然这些方法内部会处理lastIndex,但如果在这些方法之间有手动操作正则表达式,最好确保lastIndex是重置的。
- 避免意外匹配结果:当多次执行exec方法或在循环中使用正则表达式进行匹配时,如果不重置lastIndex,可能会得到意外的匹配结果。
将render字符串变为函数
js
import { parseHTML } from "./parseAst.js";
import { generate } from "./generate.js";
export function compileToFunction(el) {
// 1.把模板变成ast语法树
let ast = parseHTML(el);
console.log(ast, 2222);
//2. ast语法树生成render函数
//2.1 ast语法树变成字符串
let code = generate(ast);
//2.2 字符串变成函数
//
let render = new Function(`with(this){return ${code}}`)
console.log(render, 1111);
//3. render函数变成虚拟dom
}[!question] with的用法
将render函数变为虚拟DOM
[!cite]
- render函数变为vnode
- vnode变为真实dom
- 生命周期
- 将render函数return出去
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/compile/index.js
import { parseHTML } from "./parseAst.js";
import { generate } from "./generate.js";
export function compileToFunction(el) {
// 1.把模板变成ast语法树
let ast = parseHTML(el);
console.log(ast, 2222);
//2. ast语法树生成render函数
//2.1 ast语法树变成字符串
let code = generate(ast);
//2.2 字符串变成函数
//
let render = new Function(`with(this){return ${code}}`)
return render
//3. render函数变成虚拟dom
}在挂载的方法接收render方法,执行挂载组件的方法mounetComponent
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/init.js
import { initState } from "./initState";
import {compileToFunction} from './compile/index'
import { mounetComponent } from "./lifecycle"
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
let vm = this;
vm.$options = options;
// 初始化状态
initState(vm);
//渲染模版
//1.如果有el走且有template 则用template
//2.需要将模板编译成render函数
//3.如果有el但没有template 则用outerHTML
//4.render函数
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
// $mount 挂载方法
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el); //获取元素
vm.$el = el;
let options = vm.$options;
if (!options.render) {
// 没有render 需要模板编译
let template = options.template;
//3----------
if (!template && el) {
//获取html
el = el.outerHTML;
//变成ast语法树
let render = compileToFunction(el);
//将render函数编程vnode
//vonde变为真实DOM放到页面上去
options.render = render;
} else {
}
}
mounetComponent(vm, el);
};
}执行mounetComponent方法,其中需要注册render方法,在初始化js中注册render方法
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/lifecycle.js
//生命周期文件
export function mounetComponent(vm,el){
//vm._render将render函数变为vnode
//vm._update将vnode变为真实dom
//render函数=》vnode=》真实dom
console.log(vm)
vm._update(vm._render())
}
//初始化生命周期
export function lifecycleMixin(Vue){
Vue.prototype._update = function(vnode){
}
}注册render方法
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/index.js
import {initMixin} from './init'
import {lifecycleMixin} from './lifecycle'
import {renderMixin} from './vnode/index'
function Vue(options){
this._init(options)
}
initMixin(Vue)
lifecycleMixin(Vue)//添加生命周期
renderMixin(Vue)//添加render
export default Vue挂载render函数,转成vnode(虚拟dom)
js
///Users/weizhifeng/Desktop/源码/utils/utils/vue-demo/src/vnode/index.js
export function renderMixin(Vue) {
Vue.prototype._c = function () {
//标签
//创建标签
return createElement(...arguments);
};
Vue.prototype._v = function (text) {
//文本
return createText(text);
};
Vue.prototype._s = function (val) {
//变量
return val == null
? ""
: typeof val === "object"
? JSON.stringify(val)
: val;
};
Vue.prototype._render = function () {
//render函数变为vnode
let vm = this;
let render = vm.$options.render;
let vnode = render.call(vm);
return vnode;
};
}
//创建元素
function createElement(tag, data = {}, ...children) {
return vnode(tag, data, data.key, children);
}
function vnode(tag, data, key, children, text) {
return {
tag,
data,
key,
children,
text,
};
}
//创建文本
function createText(text) {
return vnode(undefined, undefined, undefined,undefined, text);
}