1.元素挂载顺序
vue的元素挂载有顺序之分,如果存在render函数,就用render函数渲染组件;如果没有render函数,但是存在template,就将template中的内容编译成render函数,最后做渲染;如果既没有render函数也没有template函数,就获取el里的内容作为template,同样编译成render函数。因此从模板编译成render函数就是vue的关键点。
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.模板编译函数compileToFunctions
从模板到render函数主要分为三个部分:
- 1.将html代码转换成ast语法树
- 2.优化静态节点
- 3.通过ast树 重新生成代码
export function compileToFunctions(template) {
// html模板 => render函数
// 1.需要将html代码转换成ast语法树
// 虚拟DOM 是用来描述节点的 结构
// 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>
AST树:
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"))))
}
}
虚拟DOM:
3.将模板代码转换成ast语法树---parseHTML
通过正则循环解析模板生成语法树
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语法树重新生成代码---generate
把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.初始化数据
- 2.将模板进行编译
- 3.生成render函数
- 4.生成虚拟节点
- 5.生成真实的dom
- 6.挂载到页面上