Vue2.0源碼學習2:模板編譯和DOM渲染

開始

上一節總結了Vue的響應式數據原理,下面總結一下Vue中模板編譯。模板編譯情景衆多,複雜多變,如今只學習了普通標籤的解析,編譯,未能對組件,指令,事件等多種狀況進行深刻學習總結。html

模板編譯

基本流程

  • 解析模板代碼生成AST語法樹,主要依賴正則。node

  • 將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
複製代碼

主要依賴正則解析(我正則很渣,看懂都很難,之後再深刻學習吧,直接照搬珠峯架構姜文老師)數組

實現步驟bash

  • 先解析開始標籤 如<div id='app'> ={ tagName:'div',attrs:[{id:app}]}架構

    方法:parseStartTag [1:< 2:div 3:id='app' 4:>] 四個部分 獲得 tag,attr 而後進入 start 方法,建立ast節點。app

  • 解析子節點標籤(遞歸)dom

  • 解析到結束標籤 注意:解析玩開始節點後將節點入棧,解析到結束節點後而後將開始節點出棧,此時棧的最後一點就是當前節點的父節點。函數

    例如: [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]);
            }
    
        }
        
    
    }
    複製代碼
相關文章
相關標籤/搜索