Vue2.0源码学习2:模板编译和虚拟DOM的生成

1,284 阅读4分钟

开始

上一节总结了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 函数
    (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]);
            }
    
        }
        
    
    }
    

结束

至此我们实现了Vue中的模板编译,虚拟dom和真实dom的生成,而没有实现DOM挂载因为涉及生命周期和依赖的收集,我们下一节继续总结。