美文网首页
Vue2.0源码学习2:模板编译和DOM渲染

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

作者: 泰然自若_750f | 来源:发表于2020-07-11 07:54 被阅读0次

    开始

    上一节总结了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
    

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

    实现步骤

    • 先解析开始标签 如<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]);
              }
      
          }
          
      
      }
      

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/vtebcktx.html