Vue2.0源码学习2:模板编译和DOM渲染
开始
上一节总结了Vue的响应式数据原理,下面总结一下Vue中模板编译。模板编译情景众多,复杂多变,现在只学习了普通标签的解析,编译,未能对组件,指令,事件等多种情况进行深入学习总结。
模板编译
基本流程
-
解析模板代码生成AST语法树,主要依赖正则。
image -
将ast 语法树生成代码。
with(this){
return _c("div",{id:"app"},_c("div",{class:"content"},_v("名称:"+_s(name)),_c("h5",undefined,_v("年龄:"+_s(age)))),_c("p",{style:{"color":"red"}},_v("静态节点")))
}
- 生成可执行的 render 函数
(function anonymous( ) {
with(this){
return _c("div",{id:"app"},_c("div",{class:"content"},_v("名称:"+_s(name)),_c("h5",undefined,_v("年龄:"+_s(age)))),_c("p",{style:{"color":"red"}},_v("静态节点")))
}
})
生成 AST 语法树
代码位置 complier 中的 parser.js
主要依赖正则解析(我正则很渣,看懂都很难,以后再深入学习吧,直接照搬珠峰架构姜文老师)
实现步骤
-
先解析开始标签 如
<div id='app'> ={ tagName:'div',attrs:[{id:app}]}
方法:parseStartTag [1:< 2:div 3:id='app' 4:>] 四个部分 得到 tag,attr 然后进入 start 方法,创建ast节点。
-
解析子节点标签(递归)
-
解析到结束标签
注意:解析玩开始节点后将节点入栈,解析到结束节点后然后将开始节点出栈,此时栈的最后一点就是当前节点的父节点。例如:
[div,p]
解析到</p>
此时出栈[div]
得到p
,取栈尾 将p
插入到div
的子节点。
import {extend} from '../util/index.js'
// 字母a-zA-Z_ - . 数组小写字母 大写字母
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名
// ?:匹配不捕获 <aaa:aaa>
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// startTagOpen 可以匹配到开始标签 正则捕获到的内容是 (标签名)
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
// 闭合标签 </xxxxxxx>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
// <div aa = "123" bb=123 cc='123'
// 捕获到的是 属性名 和 属性值 arguments[1] || arguments[2] || arguments[2]
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
// <div > <br/>
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
// 匹配动态变量的 +? 尽可能少匹配 {{}}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
const stripParensRE = /^\(|\)$/g;
const ELEMENT_NDOE='1';
const TEXT_NODE='3'
export function parseHTML(html) {
console.log(html)
// ast 树 表示html的语法
let root; // 树根
let currentParent;
let elementStack = []; //
/**
* ast 语法元素
* @param {*} tagName
* @param {*} attrs
*/
function createASTElement(tagName,attrs){
return {
tag:tagName, //标签
attrs, //属性
children:[], //子节点
attrsMap: makeAttrsMap(attrs),
parent:null, //父节点
type:ELEMENT_NDOE //节点类型
}
}
// console.log(html)
function start(tagName, attrs) {
//创建跟节点
let element=createASTElement(tagName,attrs);
if(!root)
{
root=element;
}
currentParent=element;//最新解析的元素
//processFor(element);
elementStack.push(element); //元素入栈 //可以保证 后一个是的parent 是他的前一个
}
function end(tagName) { // 结束标签
//最后一个元素出栈
let element=elementStack.pop();
let parent=elementStack[elementStack.length-1];
//节点前后不一致,抛出异常
if(element.tag!==tagName)
{
throw new TypeError(`html tag is error ${tagName}`);
}
if(parent)
{
//子元素的parent 指向
element.parent=parent;
//将子元素添进去
parent.children.push(element);
}
}
/**
* 解析到文本
* @param {*} text
*/
function chars(text) { // 文本
//解析到文本
text=text.replace(/\s/g,'');
//将文本加入到当前元素
currentParent.children.push({
type:TEXT_NODE,
text
})
}
// 根据 html 解析成树结构 </span></div>
while (html) {
//如果是html 标签
let textEnd = html.indexOf('<');
if (textEnd == 0) {
const startTageMatch = parseStartTag();
if (startTageMatch) {
// 开始标签
start(startTageMatch.tagName,startTageMatch.attrs)
}
const endTagMatch = html.match(endTag);
if (endTagMatch) {
advance(endTagMatch[0].length);
end(endTagMatch[1])
}
// 结束标签
}
// 如果不是0 说明是文本
let text;
if (textEnd > 0) {
text = html.substring(0, textEnd); // 是文本就把文本内容进行截取
chars(text);
}
if (text) {
advance(text.length); // 删除文本内容
}
}
function advance(n) {
html = html.substring(n);
}
/**
* 解析开始标签
* <div id='app'> ={ tagName:'div',attrs:[{id:app}]}
*/
function parseStartTag() {
const start = html.match(startTagOpen); // 匹配开始标签
if (start) {
const match = {
tagName: start[1], // 匹配到的标签名
attrs: []
}
advance(start[0].length);
let end, attr;
//开始匹配属性 如果没有匹配到标签的闭合 并且比配到标签的 属性
while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
advance(attr[0].length);
match.attrs.push({ name: attr[1], value: attr[3] || attr[4] || attr[5] })
};
//匹配到闭合标签
if (end) {
advance(end[0].length);
return match;
}
}
}
return root;
}
将AST 语法树转换为代码
如:return _c("div",{id:"app"},_c("div",{class:"content"},_v("名称:"+_s(name)),_c("h5",undefined,_v("年龄:"+_s(age)))),_c("p",{style:{"color":"red"}},_v("静态节点")
其中:_c 是创建普通节点,_v 是创建文本几点,_s 是待变从数据取值(处理模板中{{XXX}})
最后返回的是字符串代码。
每一个普通节点都会生成 _c('标签名',{属性},子(_v文本,_c(普通子节点)))
由于是树行结构,所以需要递归嵌套
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g //匹配 {{}}
/**
* 属性
* @param {*} attrs
*/
function genProps(attrs){
let str='';
for(let i=0;i<attrs.length;i++)
{
let attr=attrs[i];
//目前暂时处理 style 特殊情况 例如 @click v-model 都得特殊处理
// {
// name:'style',
// value:'color:red;border:1px'
// }
if(attr.name==='style')
{
let obj={};
attr.value.split(';').forEach(element => {
let [key='',value='']= element.split(':');
obj[key]=value;
});
attr.value=obj;
}
str+=`${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0,-1)}}`;
}
function gen(el){
//还是元素节点
if(el.type==='1')
{
return generate(el);
}
else{
let text=el.text;
if(!text) return;
//一次解析
if(defaultTagRE.test(el.text))
{
defaultTagRE.lastIndex=0
let lastIndex = 0, //上一次的匹配后的索引
index=0,
match=[],
result=[];
while(match=defaultTagRE.exec(text)){
index=match.index;
//先将 bb{{aa}} 中的 bb 添加
result.push(`${JSON.stringify(text.slice(lastIndex,index))}`);
//添加匹配的结果
result.push(`_s(${match[1].trim()})`);
lastIndex = index + match[0].length;
console.log(lastIndex);
}
//例如:11{{sd}}{{sds}}23 此时 23还未添加
if(lastIndex<text.length)
{
//result.push(`_v(${JSON.stringify(text.slice(lastIndex))})`);
result.push(JSON.stringify(text.slice(lastIndex)));
}
console.log(result);
//返回
return `_v(${result.join('+')})`
}
//没有变量
else{
return `_v(${JSON.stringify(text)})`
}
}
}
//三部分 标签,属性,子
export function generate(el){
let children = genChildren(el); // 生成孩子字符串
let result = `_c("${el.tag}",${
el.attrs.length? `${genProps(el.attrs)}` : undefined
}${
children? `,${children}` :undefined
})`;
return result;
}
生成render 函数
let astStr=generate(ast);
let renderFnStr = `with(this){ \r\nreturn ${astStr} \r\n}`;
let render=new Function(renderFnStr);
return render;
DOM 渲染
基本流程
- 调用render 函数生成虚拟dom
- 首次生成真实dom
- 更新dom,通过diff算法实现对dom的更新。(后面整理总结)
生成虚拟DOM
- 在生成render 函数中有_c(创建普通节点),_v(创建文本节点),_s(处理{{xxx}})等方法,这需要在render.js 实现。所有方法都挂载到Vue 的原型上。
// 代码位置 render.js
import {createElement,createNodeText} from './vdom/create-element.js'
export function renderMixin(Vue){
//创建节点
Vue.prototype._c=function(){
return createElement(...arguments);
}
//创建文本节点
Vue.prototype._v=function(text){
return createNodeText(text);
}
Vue.prototype._s=function(val){
return val===null?"":(typeof val==='object'?JSON.stringify(val):val);
}
// 生成虚拟节点的方法
Vue.prototype._render=function(){
const vm=this;
//这就是上一部分生成的 render 函数
const {render}=vm.$options;
//执行
let node=render.call(vm);
console.log(node);
return node;
}
}
// 代码位置 vom/create-element.js
/**
* 创建节点
* @param {*} param0
*/
export function createElement(tag,data={},...children){
return vNode(tag,data,data.key,children,undefined);
}
/**
* 文本节点
* @param {*} text
*/
export function createNodeText(text){
console.log(text);
return vNode(undefined,undefined,undefined,undefined,text)
}
/**
* 虚拟节点
*/
function vNode(tag,data,key,children,text){
return {
tag,
data,
key,
children,
text
}
}
-
数据代理
我们发现在 生成的render 函数中有
with(this){todo XXX}
with 语句的原本用意是为逐级的对象访问提供命名空间式的速写方式. 也就是在指定的代码区域, 直接通过节点名称调用对象。
在 with中的 this也就是 Vue的实例vm。但是上一节中我们得到的响应式数据都在vm._data 中,所以我们需要实现 vm.age可以取得 vm._data.age,所以需要代理。
实现代理有两种方案-
Object.defineProperty
(源码采用) __defineGetter__ 和 __defineSetter__
// state.js 中 function initData(vm){ const options=vm.$options; if(options.data) { // 如果 data 是函数得到函数执行的返回值 let data=typeof options.data==='function'?(options.data).call(vm):options.data; vm._data=data; for(let key in data) { proxy(vm,'_data',key) } observe(data) } } // 代理 function proxy(target,source,key){ Object.defineProperty(target,key,{ get(){ return target[source][key] }, set(newValue){ target[source][key]=newValue; } }) }
真实dom的生成
patch.js
/** * 創建元素 * @param {*} vnode */ function createElement(vnode){ let {tag,data,key,children,text}=vnode; if(typeof tag==='string') { vnode.el=document.createElement(tag); updateProps(vnode); children.forEach(child => { if(child instanceof Array) { child.forEach(item=>{ vnode.el.appendChild(createElement(item)); }) } else{ vnode.el.appendChild(createElement(child)); } }); } else{ vnode.el=document.createTextNode(text); } return vnode.el; } /** * jiu * @param {*} vnode * @param {*} oldNode */ function updateProps(vnode,oldProps={}){ let {el,data}=vnode; for(let key in oldProps) { //旧有新无 删除 if(!data[key]) { el.removeAttribute(key); } } el.style={}; for(let key in data) { if(key==='style') { for(let styleName in data[key]) { el.style[styleName]=data[key][styleName]; } } else{ el.setAttribute(key,data[key]); } } }
-