Vue2.0源码学习2:模板编译和DOM渲染

开始

上一节总结了Vue的响应式数据原理,下面总结一下Vue中模板编译。模板编译情景众多,复杂多变,现在只学习了普通标签的解析,编译,未能对组件,指令,事件等多种情况进行深入学习总结。

模板编译

基本流程

  • 解析模板代码生成AST语法树,主要依赖正则。

  • 将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 函数

    (functionanonymous( ) {

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 = /([sS]*?)s+(?:in|of)s+([sS]*)/;

const stripParensRE = /^(|)$/g;

const ELEMENT_NDOE='1';

const TEXT_NODE='3'

exportfunction 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}]}

*/

functionparseStartTag() {

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)})`

}

}

}

//三部分 标签,属性,子

exportfunction 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){ rnreturn ${astStr} rn}`;

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'

exportfunction 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

*/

exportfunction createElement(tag,data={},...children){

return vNode(tag,data,data.key,children,undefined);

}

/**

* 文本节点

* @param {*} text

*/

exportfunction 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]);

    }

    }

    }

以上是 Vue2.0源码学习2:模板编译和DOM渲染 的全部内容, 来源链接: utcz.com/a/29740.html

回到顶部