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

2020-07-11  本文已影响0人  泰然自若_750f

开始

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

模板编译

基本流程

   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("静态节点"))) 
   }
    (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

主要依赖正则解析(我正则很渣,看懂都很难,以后再深入学习吧,直接照搬珠峰架构姜文老师)

实现步骤

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 渲染

基本流程

生成虚拟DOM

// 代码位置 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

      }
}

掘金地址:https://juejin.im/user/5efd45a1f265da22f511c7f3/posts

上一篇下一篇

猜你喜欢

热点阅读