初始化init时候挂载
Vue.prototype._init = function(options) {
// el,data
---------
if(vm.$options.el){
// 将数据挂载到这个模板上
vm.$mount(vm.$options.el);
}
}
原型上的挂载方法
- compileToFunction方法将模板转换为render函数
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options
el = document.querySelector(el);
vm.$el = el;
// 把模板转化成 对应的渲染函数 =》 虚拟dom概念 vnode =》 diff算法 更新虚拟dom =》 产生真实节点,更新
if(!options.render){ // 没有render用template,目前没render
let template = options.template;
if(!template && el){ // 用户也没有传递template 就取el的内容作为模板
template = el.outerHTML;
let render = compileToFunction(template);
options.render = render;
}
}
// options.render 就是渲染函数
// 调用render方法 渲染成真实dom 替换掉页面的内容
mountComponent(vm,el); // 组件的挂载流程
}
1.compileToFunction方法实现编译流程
let root = parserHTML(template)
// 生成代码
let code = generate(root)
//生成render函数
let render = new Function(`with(this){return ${code}}`); // code 中会用到数据 数据在vm上
问题核心:如何将template转换成render函数 ?
- 1.将template模板转换成 ast 语法树 - parserHTML
- 2.对静态语法做静态标记 - markUp
- 3.重新生成代码 - codeGen
2.parserHTML,将template模块转换成ast树
- 将html传入,查看要解析的内容是否存在,如果存在就不停的解析
- 将我们的html =》 词法解析(开始标签,结束标签,属性,文本)
- ast语法树 用来描述html语法的 使用栈来表示stack = []
1.先定义好匹配标签和属性等正则规则
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}[^>]*>`); // 匹配闭合标签的
// aa = " xxx " | ' xxxx ' | xxx
const attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // a=b a="b" a='b' //用来匹配属性的
const startTagClose = /^\s*(/?)>/; // /> <div/>
const defaultTagRE = /{{((?:.|\r?\n)+?)}}/g; // {{aaaaa}}
// 将解析后的结果 组装成一个树结构,这是ast树的结构
function createAstElement(tagName, attrs) {
return {
tag: tagName,
type: 1,
children: [],
parent: null,
attrs
}
}
2.查看解析内容,不停的解析
//初始化进入 textEnd == 0
// <div id="app" a=1 style="color:red;background:blue">
// hello {{arr}} world
// </div>
//开始标签解析完成后,这时候先解析文本 textEnd != 0
//"
// hello {{arr}} world
// </div>"
//当解析到最后的标签时
//"</div>" textEnd == 0
//最后 html 为 "" 返回对应的ast树
function parserHTML(html){
while (html) { // 看要解析的内容是否存在,如果存在就不停的解析
let textEnd = html.indexOf('<'); // 当前解析的开头
if (textEnd == 0) {
const startTagMatch = parseStartTag(html); // 解析开始标签
//开头匹配到的match
//startTagMatch = [tagName:'div',attrs:[{name:'id',value:'app',name:'a',value,'1'}]]
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;
if (textEnd > 0) {
text = html.substring(0, textEnd) // hello {{arr}} world
}
if (text) {
chars(text); //解析文本节点,将文本节点,推入
advance(text.length); //在删除html中的文本节点
}
}
}
3.解析开始标签
//解析开头
function parseStartTag() { //解析开头
const start = html.match(startTagOpen); //判断是否是开头标签
if (start) { //如果是开始标签
const match = {
tagName: start[1], //div
attrs: []
}
//start[0] <div
advance(start[0].length);
let end;
// 如果没有遇到标签结尾就不停的解析
let attr;
//判断遇到结束标签没有,如果没有并且有属性 例如 id = app a=1 等属性,则把属性也删除掉
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; // 不是开始标签
}
4.将开始解析的标签推入到stack中
//tagName :div attrs:[{name:'id',value:'app',name:'a',value,'1'}]
function start(tagName, attributes) {
let parent = stack[stack.length - 1];
let element = createAstElement(tagName, attributes); //创建到ast树结构中
if (!root) {
root = element;
}
if(parent){ //判断有没有父亲
element.parent = parent;// 当放入栈中时 继续父亲是谁
parent.children.push(element)
}
stack.push(element);
}
5.解析文本的方法
function chars(text) {
text = text.replace(/\s/g, ""); //去掉全局的空格
let parent = stack[stack.length - 1]; //找到第一个推进栈中的元素作为父亲
if (text) {
//将文本推入到子节点,并且定义文本的type 3
parent.children.push({
type: 3,
text
})
}
}
6.解析最后的结束标签
function end(tagName) {
let last = stack.pop(); //将栈中内容推出
if (last.tag !== tagName) { //判断开始标签是否和结束标签一致
throw new Error('标签有误');
}
}
总结:就是将传入的html要素,一个一个匹配解析,最后生成一棵ast语法树,vue中采用的是正则匹配的方式,来进行匹配的
{
"tag": "div",
"type": 1,
"children": [
{
"type": 3,
"text": "hello{{arr}}world"
}
],
"parent": null,
"attrs": [
{
"name": "id",
"value": "app"
},
{
"name": "a",
"value": "1"
},
{
"name": "style",
"value": "color:red;background:blue"
}
]
}
3.generate生成代码
- 就是将ast树的代码转换为 _c _v _s所包裹的字符串 //code generate代码生成
- 最终转换结果:_c('div',{id:"app",a:"1",style:{"color":"red","background":"blue"}},_v("hello"+_s(arr)+"world"))
1.调用generate函数,来生成对应的代码
export function generate(el) { // _c('div',{id:'app',a:1},_c('span',{},'world'),_v())
// 遍历树 将树拼接成字符串
let children = genChildren(el); // _v("hello"+_s(arr)+"world")
let code = `_c('${el.tag}',${
el.attrs.length? genProps(el.attrs): 'undefined'
}${
children? `,${children}`:''
})`;
//最后生成code _c('div',{id:"app",a:"1",style:{"color":"red","background":"blue"}},_v("hello"+_s(arr)+"world"))
return code;
}
2.genChildren先遍历ast树中children的部分,主要是针对于文本部分解析
function genChildren(el) {
let children = el.children; // 获取儿子 [{"type": 3,"text": "hello{{arr}}world" }]
if (children) { //在遍历孩子
return children.map(c => gen(c)).join(',') // c {text:"hello{{arr}}world",type: 3}
}
return false;
}
//生成代码
function gen(el) {
if (el.type == 1) { // element = 1 text = 3
return generate(el); //如果判断是要素,则重新调用genetate函数
} else {
//如果是文本
let text = el.text;
if (!defaultTagRE.test(text)) { //判断如果没有 {{ }} 大括号,则直接_v展示文本
return `_v('${text}')`;
} else {
// 'hello' + arr + 'world' hello {{arr}} {{aa}} world
let tokens = [];
let match;
let lastIndex = defaultTagRE.lastIndex = 0; // 默认初始化的时候为0 CSS-LOADER 原理一样
while (match = defaultTagRE.exec(text)) { // 看有没有匹配到
//match: [0:"{{arr}}":1:"arr",index:5 //表示从5位开始匹配到 ] 匹配到对应的数据
let index = match.index; // 开始索引
if (index > lastIndex) { //判断找到的{{}}大于最后一个
tokens.push(JSON.stringify(text.slice(lastIndex, index))) //找到前半部分
}
// tokens [hello]
tokens.push(`_s(${match[1].trim()})`); // JSON.stringify()
// tokens [""hello"", "_s(arr)"] //给{{}}的部分加上_s
lastIndex = index + match[0].length; //加到后半部分的数字端
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex))) //切割后半部分的东西到数组里面
}
// tokens [""hello"","_s(arr)",""world""]
return `_v(${tokens.join('+')})` //最后拼接上_v
}
}
}
3.解析attribute属性的拼接
//[{"name": "id","value": "app"},{"name": "a","value": "1"},{"name": "style","value": "color:red;background:blue"}]
function genProps(attrs) { // [{name:'xxx',value:'xxx'},{name:'xxx',value:'xxx'}]
let str = '';
for (let i = 0; i < attrs.length; i++) {
let attr = attrs[i];
if (attr.name === 'style') { // color:red;background:blue
let styleObj = {};
attr.value.replace(/([^;:]+):([^;:]+)/g, function() {
styleObj[arguments[1]] = arguments[2]
})
attr.value = styleObj
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
// id:"app",a:"1",style:{"color":"red","background":"blue"},
return `{${str.slice(0,-1)}}`
}
4.通过with语法生成函数
with语句
作用域名–一个可以按序检索的对象列表,通过它可以进行变量名的解析。with语句用于临时拓展作用域链,语法如下:
//调用对象属性和方法的简写with:
let obj = {
name: '使用with读取对象属性'
}
with(obj) { //严格模式下将禁用with关键字
console.log(name)
}
最后通过编辑等到的函数
{
with(this){
return _c('div',
{
id:"app",a:"1",style:{"color":"red","background":"blue"}
},
_v("hello"+_s(arr)+"world")
)}
}