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

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

实现步骤

  • 先解析开始标签 如

    ={ tagName:'div',attrs:[{id:app}]}

    方法:parseStartTag [1:< 2:div 3:id='app' 4:>] 四个部分 得到 tag,attr 然后进入 start 方法,创建ast节点。

  • 解析子节点标签(递归)

  • 解析到结束标签
    注意:解析玩开始节点后将节点入栈,解析到结束节点后然后将开始节点出栈,此时栈的最后一点就是当前节点的父节点。

    例如: [div,p] 解析到

    此时出栈[div] 得到p,取栈尾 将p 插入到div的子节点。

import {extend} from '../util/index.js'
//              字母a-zA-Z_ - . 数组小写字母 大写字母  
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名
// ?:匹配不捕获   
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// startTagOpen 可以匹配到开始标签 正则捕获到的内容是 (标签名)
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
// 闭合标签   
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 
//
\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的 //

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 解析成树结构
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); } /** * 解析开始标签 *
={ 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 {
                 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

生成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]);
            }
    
        }
        
    
    }
    

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

你可能感兴趣的:(Vue2.0源码学习2:模板编译和DOM渲染)