精华内容
下载资源
问答
  • vue模板渲染

    2020-06-27 12:28:45
    把template中的模板使用vue-template-complier进行编译,模板通过编译生成AST,再由AST生成Vue的渲染函数,render函数中使用到了js中的with方法,其中返回一个createElement函数(h函数)。...模板渲染的三个.

    在这里插入图片描述
    把template中的模板使用vue-template-complier进行编译,模板通过编译生成AST,再由AST生成Vue的渲染函数,render函数中使用到了js中的with方法,其中返回一个createElement函数(h函数)。createElement中接收三个参数,标签,属性(class,id,style),子元素
    例如:

    render(){
    	with(this){
    		return  createElement(param1,param2,param2);
    		}
    }
    

    模板渲染的三个步骤
    在这里插入图片描述
    此过程可以分成两个步骤:先将模板解析成AST(abstract syntax tree,抽象语法树),然后使用AST生成渲染函数。
    由于静态节点不需要总是重新渲染,所以生成AST之后,生成渲染函数之前这个阶段,需要做一个优化操作:遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现这个节点有这个标记,就不会重新渲染它。
    所以,在大体逻辑上,模板编译分三部分内容:
    1、将模板中的指令进行正则匹配,解析成AST(parse解析)
    2、遍历AST标记静态节点 (optimeize优化)
    3、使用AST生成渲染函数 (generate生成)
    这三部分内容在模板编译中分别抽象出三个模块实现各自的功能:解析器、优化器和代码生成器。

    静态节点:就是在一个标签中不包含任何的变量,没有改变的可能性

    除了静态节点,还有文本节点和元素节点

    经过渲染之后会生成render函数,返回的是一个createELement方法,这个方法是创建虚拟Dom,之后使用会使用diff算法中的patch(方法),把在缓存中的上一个编译出来的虚拟dom和新生成虚拟dom进行比较,之后在进行一个真实的渲染。

    展开全文
  • 一、vue模板渲染原理

    2020-09-30 09:52:19
    vue基础Dom 最后页面在浏览器里显示的是 张三 这个过程使用的模板渲染 <body> <div id="root"> <p>{{name}}</p> </div> <...下面根据vue模板渲染的原理 用js

    vue基础Dom 最后页面在浏览器里显示的是 张三

    这个过程使用的模板渲染

    <body>
    	<div id="root">
    		<p>{{name}}</p>
    	</div>
    	<script>
    		var app = new Vue({
    			el:'#root',
    			data:{
    				name:'张三'
    			}
    		})
    	</script>
    </body>
    

    下面根据vue模板渲染的原理 用js简单实现

    vue模板渲染时的执行流程

    • 获得模板 此时模板带有坑>指上文的{{name}}
    • 利用vue构造函数里的数据来填坑 > 指app里提供的data数据 最后生成可以在页面显示的‘标签’
    • 将最后生成的标签替换到原来 带有坑的标签
    <body>
    	<div id="root">
    		<p>{{name}}</p>
    		<p>{{age}}</p>
    	</div>
    	<script>
    		// 1.获得模板
    		var tmpNode = document.getElementById('root');
    		// 2.提供数据
    		let data = {
    			name:'李四',
    			age:22
    		}
    		// 3.将数据放进模板中 方式:递归 算法
    		function compiler(template,data){
    			let childNodes = template.childNodes; //获取子元素
    			for(let i = 0;i<childNodes.length;i++){
    				let type = childNodes[i].nodeType; // 1 代表元素 3 文本节点
    				if(type == 3){
    					// 3文本节点 判断里面是否有{{}}插值
    					let tex = childNodes[i].nodeValue; // 该属性只有文本节点才有意义
    					//判断有没有 {{}}
    					tex = tex.replace(brackets,function(_,g){
    						let key = g.trim(); //写在花括号里的内容
    						let value = data[key];
    						//将花括号里的值 用这个值替换
    						return value
    					})
    					childNodes[i].nodeValue = tex
    				}else if(type == 1){
    					//1.代表元素,进行递归
    					compiler(childNodes[i],data)
    				}
    			}
    		}
    		//括号的正则表达式
    		 let brackets = /\{\{(.+?)\}\}/g;
    		 // 利用模板生成一个需要被渲染的html标签 .cloneNode(true)深拷贝
    		 let generateNode = tmpNode.cloneNode(true) 
    		 compiler(generateNode,data)
    		 //4.将渲染好的html放到页面当中 模板被保留
    		 root.parentNode.replaceChild(generateNode,root)
    	</script>
    </body>
    

    上边就简单的实现了 vue 中的模板渲染。不断学习中。

    展开全文
  • vue 文件的构成熟悉 vue 的同学应该都知道,vue 单文件模板中一般含有三个部分,template,script,style。但是在编译后的 js 文件中,我们却没法在代码中直接找...


    vue 文件的构成

    熟悉 vue 的同学应该都知道,vue 单文件模板中一般含有三个部分,template,script,style。

    但是在编译后的 js 文件中,我们却没法在代码中直接找到这三部分,如果我们想从编译后的 js 中获取原始模板,应该怎么做?

    vue 并非直接使用 template 进行渲染,而是需要把 template 编译成渲染函数,才能渲染。

    new Vue({
        render:function () {},
        staticRenderFns:[]
    })
    

    并且当一个 vue 单文件同时存在 template 标签和 render 函数时,render 函数优先生效。

    事实上编译工具也确实会把 vue 单文件模板编译成这种形式,style 会单独提取出来,绑定作用域作为标识,而 script 部分除了加入了 render 和 staticRenderFns 以外,基本不变。

    /* 作用域标识为 data-v-3fd7f12e */
    .effect-mask[data-v-3fd7f12e] {
        opacity: 0;
    }
    
    // js 中肯定能找到对应的作用域标识,关联某个组件,上面的 css 就是这个组件的 style
    j = i("X/U8")(F.a, W, !1, Y, "data-v-3fd7f12e", null).exports,
    

    因此,我们如果想把一个编译后的单文件模板还原,主要的工作,就是把 render 和 staticRenderFns 中的模板从渲染函数还原成 template 模板。之后再把 script 引入的模块还原,根据作用域标识找回样式并格式化即可。

    本文主要说明如何把 js 代码构成的渲染函数,还原成 template 模板。

    处理 staticRenderFns

    staticRenderFns 是 template 中的静态模板片段,片段是纯 html,不含变量和表达式。

    对于这种静态模板,我们通过构造上下文对渲染函数求值,就可以获取到想要的结果。

    staticRenderFns 格式如下:

    staticRenderFns: [function () {
        var t = this.$createElement,
            e = this._self._c || t;
        return e("div", {
            staticClass: "btn on"
        }, [e("i", {
            staticClass: "icon iconfont"
        }), e("span", [this._v("下载")])])
    }]
    

    我们可以构造一个类 StaticRender,实现 $createElement、_v、_self ,然后把 staticRenderFns 中的渲染函数挂载到 StaticRender 的实例上,这样渲染函数就可以正常执行。

    $createElement 的函数签名如下:

    // vue/types/vue.d.ts
    export interface CreateElement {
      (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), children?: VNodeChildren): VNode;
      (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode;
    }
    

    在 staticRenderFns 渲染函数中,我们可以认为 $createElement 第一个参数是节点标签名,第二个参数是节点属性对象,第三个参数是子节点数组,第二、三个参数可选,返回值是一个元素节点。

    _v 只有一个参数,返回一个文本节点。

    我们只要构造好这两个方法,就可以轻松获得节点树,然后把节点转换成 html。

    // 定义节点类型
    interface TextNode {
        type:'text'
        text:string
    }
    
    interface Element {
        type:'element'
        tag:string
        attrMap?:{[key:string]:any}
        children:Node[]
    }
    
    type Node = Element | TextNode 
    
    // 定义 StaticRender 类
    
    export class StaticRender {
        _self = {}
        renderFunc:()=>Node  // 挂载的渲染函数
        constructor (renderFunc:()=>Node) {
            this._self = {}
            this.renderFunc = renderFunc
        }
        render () {  // 执行渲染函数,输出html
            var root = this.renderFunc ()
            var _html = this.toHtml(root)
            return _html
        }
        toHtml (root:Node):string {
            // 生成 html 
        }
        attrToString  (obj:{[key:string]:any}) {
            // 格式化属性到字符串
        }
    
        _v  (str:string) {
            return {
                text:str,
                type:'text'
            }
        }
        $createElement (tag:string,attrMap:{[key:string]:any},children:Node[]) {
            var _tag, _attrMap, _children;
            _tag = tag;
            if(Array.isArray(attrMap)){
                _children = attrMap
            }else{
                _attrMap = attrMap
            }
            if(Array.isArray(children)){
                _children = children
            }
            var ret = {
                tag:_tag,
                type:'element',
                attrMap:_attrMap || {},
                children:_children || []
            }
            return ret;
        }
    }
    

    执行求值,结果如下:

    <div class="btn on">
        <i class="icon iconfont"> </i>
        <span> 下载 </span> 
    </div>
    

    staticRenderFns 生成的 html 片段我们之后还会用到。

    处理 render

    render 渲染函数包含大量的变量、表达式,例如 v-if、v-for 的内容。我们很难通过构造简单的上下文求值得到模板。

    整体流程

    编译和还原本质上都是把代码解析成语法树然后进行变换,再生成新的代码。

    vue 模板在编译时基本没有丢掉原始信息,因为我们可以做到比较精准的还原。

    并且由于 vue 模板涉及的语法特性较少,主体是声明式的 xml,只涉及少量的 js 表达式,并且只用到了部分 js 语言特性,还原起来相对比较容易。

    因此,对于 render,我们使用变换语法树的方法获得模板。

    流程图

    从流程来看,我们需要解析器,变换器,生成器三个部分。

    解析器将渲染函数转换为 js 语法树。

    变换器将 js 语法树转换成 vue 模板语法树。

    生成器将 vue 模板语法树转换成 vue 模板字符串。

    解析器

    其中解析器属于比较大众化的需求,eslint、压缩/优化、代码高亮、类型检查等等都需要用到解析器,自然可以找到可用的轮子。

    把 js 代码转换成语法树我们可以使用 @typescript-eslint/typescript-estree。

    项目 estree[1] 则提供了各个版本 js 所定义的节点类型标准。

    一个 estree 节点的基本类型定义如下,包含类型、位置、长度等信息:

    interface BaseNode {
        type:string
        loc:{
            end:{
                line:number
                start:number
            },
            start:{
                line:number
                start:number
            }
        },
        range:[number,number]
    }
    

    不同的节点类型会增加各自特有的属性,例如函数调用表达式的类型定义如下:

    interface CallExpressionBase extends BaseNode {
        callee: LeftHandSideExpression;
        arguments: Expression[];
        typeParameters?: TSTypeParameterInstantiation;
        optional: boolean;
    }
    
    interface CallExpression extends CallExpressionBase {
        type: AST_NODE_TYPES.CallExpression;
        optional: false;
    }
    

    函数有调用者 callee 和参数 arguments 两个特有属性。

    完整的 js 语法树节点类型定义可以在 ts-estree.ts[2] 查阅。

    简单的 api 调用就可以获取到渲染函数的语法树。

    import { parse, TSESTreeOptions,AST } from "@typescript-eslint/typescript-estree"
    
    class Render {
        options:TSESTreeOptions = {
            errorOnUnknownASTType:true,
            loc:true,
            range:true,
        }
        ast:AST<TSESTreeOptions>
        constructor (code:string,staticTpls:string[]) {
            this.ast = parse(code, this.options);  // 获取语法树
        }
    }
    

    变换器

    有了 js 语法树节点类型定义,我们还需要 vue 模板的语法树节点类型定义,才能正确地完成转换。

    一个 vue 模板语法树节点类型定义如下:

    删减了非必要属性,完整版本可以查看 index.d.ts[3]

    type ASTNode = ASTElement | ASTText | ASTExpression;
    
    interface ASTElement {
      type: 1;
      tag: string;
      attrsList: { name: string; value: any }[];
      attrsMap: Record<string, any>;
      parent: ASTElement | undefined;
      children: ASTNode[];
    }
    
    interface ASTText {
      type: 3;
      text: string;
    }
    
    interface ASTExpression {
      type: 2;
      expression: string;
      text: string;
      tokens: (string | Record<string, any>)[];
    }
    

    render 用到的特性

    编写转换逻辑前,我们先来看看 render 渲染函数的基本形式,以及它用到了哪些 js 特性、我们需要处理哪些东西。

    此渲染函数包含了动态/静态属性,指令,v-for 列表,事件绑定等特性。

    function() {
        var t = this
            , e = t.$createElement
            , i = t._self._c || e;
        return i("transition", {
            attrs: {  
                name: "el-zoom"   // 属性
            },
            on: {   // 事件绑定
                click: function(e) {
                    t.onClick();
                }
            }
        }, [i("div", {   // 指令
            directives: [{
                name: "show",
                rawName: "v-show",
                value: t.visible,
                expression: "visible"
            }],
            staticClass: "el-time-panel",
            class: t.popperClass
        },t._l(t.list, function(e, s) {  // v-for 列表
        return i("ListPreview", {
          key: s + "_" + e.id,   // 动态属性
          attrs: {
            spriteData: e,
            playFlag: t.playing  // 动态属性
          }
        });
      }))])
    }
    

    render 渲染函数和 staticRenderFns 函数的格式一样,都是定义一个局部变量赋值为 $createElement 方法,定义一个局部变量赋值为 this。

    但是变量名并不是固定的,所以我们首先要分析出代表 $createElement 和 this 的变量。

    staticRenderFns 渲染函数中,this下只用到了 _v 方法,render 渲染函数中,this 下挂载了更多的内置方法,它们都以 _ 开头,我们主要需要处理的有:

    • _l:生成 v-for 结构

    • _e:生成空节点

    • _s:生成插值字符串

    • _m:生成静态 html 片段(staticRenderFns 中的 html 片段)

    • _v:生成文本节点

    其他不常见的内置函数可以遇到后再完善,例如 _u、_p 等。

    完整的内置方法列表可以查阅 vue/render-helpers[4],其生成逻辑在 vue/codegen[5]

    vue/codegen[6] 可以认为是 vue 模板的生成规范。

    除此之外,this 下面还挂载了 vue 实例的 data 和 methods,这些都可以在模板中使用,也是我们要处理的对象。

    v-if 以三元表达式的方式呈现。

    转换的基本思路

    1. 从 js 语法树根节点开始遍历,先获取到 this 和 $createElement 对应的标识符

    render 渲染函数内部一般不直接使用 this 和 $createElement,而是赋值给两个局部变量。这两个局部变量在渲染函数内会被大量使用,但是变量名并不是固定的,因此我们先要获取到变量名,在上面的渲染函数示例中,变量名分别为 t 和 i。

    在后面的遍历中,如果 t 作为参数出现在表达式中,我们要判断它是否是 this。如果 i 作为函数调用者出现,我们要判断它是否是 $createElement。

    然后,我们遍历到 return 语句处,它的节点类型是 ReturnStatement, ReturnStatement 的 argument 属性就是 return 后面跟着的表达式。

    这个表达式就是我们获取 vue 模板语法树的起点。

    interface ReturnStatement extends BaseNode {
        type: AST_NODE_TYPES.ReturnStatement;
        argument: Expression | null;
    }
    
    1. 转换主体

    入口表达式通常就是一个 $createElement 的函数调用表达式,但是也有可能是一个三元表达式。这是因为 v-if 可以出现在模板根节点。

    $createElement 的函数签名和 staticRenderFns 中的一样。

    // vue/types/vue.d.ts
    export interface CreateElement {
      (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), children?: VNodeChildren): VNode;
      (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode;
    }
    
    

    我们应把 $createElement 的函数调用表达式解析成一个 vue 语法树节点,tag 参数作为标签名,从 data 参数中获得属性对象,然后对其 children 参数递归解析,作为子节点。

    如果入口是一个三元表达式,三元表达式有如下定义:

    interface ConditionalExpression extends BaseNode {
        type: AST_NODE_TYPES.ConditionalExpression;
        test: Expression;
        consequent: Expression;
        alternate: Expression;
    }
    

    test 解析为 v-if 的判断条件,consequent 解析为 v-if 内的节点,alternate 解析为 v-else 内的节点。

    我们一般最终会转换成

    <template v-if = "testExp"></template>
    <template v-else></template>
    

    这是两个节点,为了保持解析方法的一致性和简单性,统一只返回一个节点。因此创建一个 wrap 节点,将这两个节点作为它的 children。

    // e1 为 v-if 解析后的节点,e2 为 v-else 解析后的节点
    function conditionElement(_e1:ASTNode,_e2:ASTNode){
        var element:ASTElement  = {
            tag:'$$condition_wrap',
            type:1,
            attrsList:[],
            attrsMap:{},
            children:[_e1,_e2],
            parent:undefined
        }
        return element
    }
    
    

    因为 wrap 节点造成不必要的过多嵌套,我们会在后续的优化环节把节点合并。

    1. 处理表达式

    render 渲染函数中存在大量的表达式,例如指令属性中、绑定属性中、插值字符串。表达式种类繁多,处理表达式是转换的重要一环。

    处理表达式的整体思路就是把它转换成一个字符串返回,例如二元表达式的处理:

    function expToString ( _exp:TSESTree.Expression):string {
        switch (_exp.type) {
            case AST_NODE_TYPES.BinaryExpression:  // 例如  a === b 
                if(_exp.operator == '==' || _exp.operator == '!=' || _exp.operator == '!==' || _exp.operator == '==='){ // == 就把左右互换
                    var ret = `${this.expToString(_exp.right)} ${_exp.operator} ${this.expToString(_exp.left)}`
                    return ret;
                }else{
                    var ret = `${this.expToString(_exp.left)} ${_exp.operator} ${this.expToString(_exp.right)}`
                    return ret;
                }
            // ...
        }
    }
    

    把标识符和操作符正确地拼接在一起即可。

    至少有十几种表达式会出现在 render 渲染函数中,我们都需要处理。

    除此之外,部分表达式还需要一些额外处理,我们看如下渲染函数片段:

    i("transition", {
        on: {   // 事件绑定
            click: function(e) {
                t.onClick();
            }
        }
    })
    

    它的 vue 模板应该是这样的:

    <transition @click="onClick()"> </transition>
    

    模板中用的属性和方法都挂载在 this,也就是这里的 t 下。渲染函数需要用 t. 来调用,但是模板中不需要,所以我们需要把它去掉。

    但是我们碰到 t. 就去掉也不行,例如下面的情况:

    i("transition", {
        on: {   // 事件绑定
            click: function(t) {
                t.onClick();
            }
        }
    })
    

    参数里有 t,函数里的 t 显然不再是 this,它已经被参数中的 t 覆盖了,这时我们就需要保留 t。

    除此之外,我们还会遇到这种情况:

    i("transition", {
        on: {   // 事件绑定
            mousedown: function(i) {
                i.stopPropagation(),
                t.globalMouseDown(
                    arguments[0],
                    "r",
                    e
                );
            }
        }
    })
    

    它的 vue 模板应该是这样的:

    <transition @mousedown="$event.stopPropagation(),globalMouseDown($event,'r',e)"> </transition>
    

    或者

    <transition @mousedown.stop="globalMouseDown($event,'r',e)"> </transition>
    

    $event 是 vue 模板的特有参数,事件函数的第一个参数都可以写作 $event,我们同样需要在处理表达式时处理此种情况。

    我们需要根据函数参数处理函数内部的表达式,但是显然这跨越了几个节点层次,我们需要知道前几层节点的情况,我们可以引入上下文解决此问题。

    1. 上下文

    函数有调用栈,我们同样用栈式结构生成上下文,为了保证不同节点间的上下文不会因为赋值互相干扰,我们引入 immutable, 使用不可变对象生成上下文。

    类型定义如下:

    import { List } from "immutable"
    type Context = {
        [key:string]:string
    }
    type ContextStack = List<Context>
    

    处理 $event 示例

    expToString ( _exp:TSESTree.Expression,_ctx:ContextStack):string {
        switch (_exp.type) {
            case AST_NODE_TYPES.FunctionExpression: // 节点类型为函数表达式节点
                var params = _exp.params.map(node=>{return this.parameterToString(node,_ctx)}) // 获取所有参数
                if(params.length > 0){
                    var eventId = params[0];
                    var nextCtx1 = _ctx.push({type:'eventId',value:eventId}) // 生成新的上下文
                    var bodyStr = this.statementToString(_exp.body,nextCtx1);
                    return bodyStr
                }
        }
    }
    
    1. 处理内置函数

    前面我们列出了一系列 _ 开头的内置函数,它们会影响节点的生成,我们都需要处理。

    • _l:生成 v-for 结构

    一个 t._l 调用的基本形式如下:

    t._l(t.list, function(e, s) {
        return i("Item", {
          key: s + "_" + e.id,
          attrs: {
            data: e,
            flag: t.playing
          }
        });
    })
    

    转换后应为

    <Item v-for="(e,s) in list" :key="s + '_' + e.id" :data="e" :flag="playing"></Item>
    

    我们需要从 _l 函数调用表达式的第一参数中获取到循环用的列表标识符,从第二个参数的函数表达式中获取到参数列表,从 return 语句中获取到循环用的元素节点。

    • _e:生成空节点

    空节点都是可以去掉的,为了保持解析方法的一致性,返回一个标识为 $$null 的节点。

    function nonNode () {
        var element:ASTElement  = {
            tag:'$$null',
            type:1,
            attrsList:[],
            attrsMap:{},
            children:[],
            parent:undefined
        }
        return element
    }
    
    • _s:生成插值字符串 & _v:生成文本节点

    _s 可能出现在 _v 内部,因此一起处理。

    // t._v(t._s(t.title.length) + "/15") => _s(t.title.length) + "/15"  =>  {{title.length + "/15"}}
    // t._v("保存") => "保存" => 保存
    function textNode (text:string) {
        var re = /_s\((.*?)\)/g;  // 匹配 _s() 将 _s() 去掉,整体用 {{}} 包裹
        if(re.test(text)){  // 处理 _s ,_s只会在 _v内部
            text =`{{${text.replace(re,(_a:string,b:any)=>{
                return b
            })}}}` 
        }else{ // 去掉静态文本两侧的双引号
            if(text.startsWith('"') && text.endsWith('"')){
                text = text.slice(1,-1)
            }
        }
        var element:ASTElement  = {  // 简化类型,用 $$text 标识文本节点
            tag:'$$text',
            type:1,
            attrsList:[],
            attrsMap:{text:text},
            children:[],
            parent:undefined
        }
        return element
    }
    
    • _m:生成静态 html 片段(staticRenderFns 中的 html 片段)

    m 一般以类似 t._m(0) 的形式出现,只有一个参数,参数为索引。我们之前解析的 staticRenderFns 数组中的索引,最终替换成之前生成好的 html片段即可。因此返回一个标识为 $$static_ 加索引的节点。

    function staticNode (_exp:TSESTree.Expression) {
        if(_exp.type == AST_NODE_TYPES.Literal){
            var index = _exp.raw;
            var tag = `$$static__${index}`
            var element:ASTElement  = {
                tag:tag,
                type:1,
                attrsList:[],
                attrsMap:{},
                children:[],
                parent:undefined
            }
            return element
        }else{
            throw new Error("解析 static node 错误")
        }
    }
    
    1. 处理属性对象

    属性都是键值对的形式,值主要就是表达式,我们之前已经处理过了。键的处理主要如下:

    键为 on 时,按绑定事件的格式处理。 键为 model 时,按 v-model 处理。 键为 directives 时,按指令格式处理。 键为 attrs 时,值是静态属性集合,需要拆开。 键为 staticClass、staticStyle 时,是静态类名和样式。 除此之外,如果值是个双引号包裹的字符串,则是静态属性,否则为绑定属性,属性名前加冒号。

    部分不常用的属性对象未列出,可以查阅 vue/codegen[7]

    1. 优化

    经过以上处理,我们已经得到了 vue 模板语法树,但是它还有冗余。有 _e 生成的空节点,还可能有 wrap 节点多层嵌套。

    生成出来的模板可能是这样的,因为 wrap 节点都会使用 template 标签:

    <template>
        <template>
            <template>
    
            </template>
        </template>
    </template>
    

    我们可以遍历 vue 模板语法树,删掉空节点,把多层 template 节点合并。

    每种类型的优化可以单独写成一个方法,例如:

    // 删除空节点
    function optimizeNode1(_root: ASTElement): ASTElement {
        _root.children = _root.children.filter(child=>{
            if(child.type == 1 && child.tag == '$$null' && !child.attrsMap['v-if']){
                    return false
            }else{
                return true;
            }
        }).map(child=>{
            if(child.type == 1){
                optimizeNode1(child)
            }
            return child;
        })
        return _root;
    }
    

    然后各个优化方法依次调用即可。

    每个优化环节都重新遍历一遍节点并非一种高效的做法,如果优化方法能够支持流式处理,流水线模式能够大幅提高效率。

    生成器

    将 vue 模板语法树转换成字符串的过程并不复杂,需要注意点有:

    • 将 $$static__ 节点替换成 staticRenderFns 中的 html 片段

    • 区分自闭合标签

    • v-else 属性不需要值

    最后可以用 js-beautify 库进行格式化。

    实例

    本文的完整代码在这里[8]

    并且支持在线转换[9]

    可以从含有 vue 模板的编译后代码中,例如,element-ui 官网下的 js[10] 中,用 $createElement 搜索渲染函数,然后按照以下格式输入到输入框,执行在线转换。

    {
        render:function () {
            var t = this.$createElement
            //....
        },
        staticRenderFns: [function () {
            var t = this.$createElement
            //....
        }]
    }
    

    例如:

    {
        render:function() {
            var t = this
            , e = t.$createElement
            , i = t._self._c || e;
            return i("transition", {
                attrs: {
                    name: "el-zoom-in-top"
                },
                on: {
                    "after-leave": function(e) {
                        t.$emit("dodestroy")
                    }
                }
            }, [i("div", {
                directives: [{
                    name: "show",
                    rawName: "v-show",
                    value: t.visible,
                    expression: "visible"
                }],
                staticClass: "el-time-panel el-popper",
                class: t.popperClass
            }, [i("div", {
                staticClass: "el-time-panel__content",
                class: {
                    "has-seconds": t.showSeconds
                }
            }, [i("time-spinner", {
                ref: "spinner",
                attrs: {
                    "arrow-control": t.useArrow,
                    "show-seconds": t.showSeconds,
                    "am-pm-mode": t.amPmMode,
                    date: t.date
                },
                on: {
                    change: t.handleChange,
                    "select-range": t.setSelectionRange
                }
            })], 1), i("div", {
                staticClass: "el-time-panel__footer"
            }, [i("button", {
                staticClass: "el-time-panel__btn cancel",
                attrs: {
                    type: "button"
                },
                on: {
                    click: t.handleCancel
                }
            }, [t._v(t._s(t.t("el.datepicker.cancel")))]), i("button", {
                staticClass: "el-time-panel__btn",
                class: {
                    confirm: !t.disabled
                },
                attrs: {
                    type: "button"
                },
                on: {
                    click: function(e) {
                        t.handleConfirm()
                    }
                }
            }, [t._v(t._s(t.t("el.datepicker.confirm")))])])])])
        }
    }
    

    点击转换,输出:

    
    <template>
        <transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
            <div v-show="visible" class="el-time-panel el-popper" :class="popperClass">
                <div class="el-time-panel__content" :class="{ 'has-seconds':showSeconds}">
                    <time-spinner ref="spinner" :arrow-control="useArrow" :show-seconds="showSeconds" :am-pm-mode="amPmMode" :date="date" @change="handleChange" @select-range="setSelectionRange">
                    </time-spinner>
                </div>
                <div class="el-time-panel__footer">
                    <button class="el-time-panel__btn cancel" type="button" @click="handleCancel">
                        {{t("el.datepicker.cancel")}}
                    </button>
                    <button class="el-time-panel__btn" :class="{ confirm:!disabled}" type="button" @click="handleConfirm()">
                        {{t("el.datepicker.confirm")}}
                    </button>
                </div>
            </div>
        </transition>
    </template>
    

    和 element-ui 源码对比,逻辑完全一致。

    参考资料

    [1]

    estree: https://github.com/estree/estree

    [2]

    ts-estree.ts: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/types/src/ts-estree.ts

    [3]

    index.d.ts: https://github.com/vuejs/vue/blob/dev/packages/vue-template-compiler/types/index.d.ts

    [4]

    vue/render-helpers: https://github.com/vuejs/vue/tree/dev/src/core/instance/render-helpers/index.js

    [5]

    vue/codegen: https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js

    [6]

    vue/codegen: https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js

    [7]

    vue/codegen: https://github.com/vuejs/vue/tree/dev/src/compiler/codegen/index.js

    [8]

    这里: https://github.com/mk33mk333/vue-template-transform

    [9]

    在线转换: https://mk33mk333.github.io/vue-template-transform/index.html

    [10]

    js: https://element.eleme.io/element-ui.0216a22.js


    如果你觉得这篇内容对你有价值,请点赞,并关注我们的官网 (https://wecteam.io/) 和我们的微信公众号 (WecTeam) ,每周都有优质文章推送~


    最后

    • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...

    • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

    点个在看支持我吧

    展开全文
  • Vue.js 是在前端(即浏览器内)进行的模板 渲染。 1.前后端渲染对比 前端渲染 的优势:是在浏览器里利用 JS 把数据和 HTML 模板进行组合,业务分离,后端只需要提供数据接口。 2.条件渲染 (提供指令来说明模板和...

    当获取到后端数据后,我们会把它按照一定的规则加载到写好的模板中,输出成在浏览器中显示的 HTML,这个过程就称之为渲染。而 Vue.js 是在前端(即浏览器内)进行的模板 渲染。
    1.前后端渲染对比
    前端渲染 的优势:是在浏览器里利用 JS 把数据和 HTML 模板进行组合,业务分离,后端只需要提供数据接口。
    2.条件渲染
    (提供指令来说明模板和数据间的逻辑关系)
    v-if/v-else 根据数据值来判断是否输出该DOM元素,以及包含的子元素

    <div v-if="yes">yes</div>
    <div v-else>no</div>       #v-else 必须紧跟 v-if
    

    当前 vm 实例中包含 data.yes = true,则模板引擎将会编译这个 DOM 节点,输出yes,否则输出no
    v-show 根据条件展示元素的一种指令

    <div v-show="show">show</div>
    <div v-else>hidden</div>
    

    也可以搭配 v-else 使用,用法和 v-if 一致。

    <div v-if="show">if</div>
    <div v-show="show">show</div>
    

    v-if vs v-show
    v-if 引起了 dom 操作级别的变化,而 v-show 仅发生了样式的变化,从切换的角度考虑,v-show 消耗的 性能要比 v-if 小。
    3.列表渲染
    v-for根据接收到数组重复渲染 v-for 绑定到的 DOM 元素及 内部的子元素,并且可以通过设置别名的方式,获取数组内数据渲染到节点中。

    <ul>
     <li v-for="item in items">
      <h3>{{item.title}}</h3>
      <p>{{item.description}}</p>
      </li>
      </ul>
      var vm = new Vue({ 
          el : '#app', 
          data: {  
            items : [   
              { title : 'title-1', description : 'description-1'},  
               { title : 'title-2', description : 'description-2'},   
               { title : 'title-3', description : 'description-3'},  
               { title : 'title-4', description : 'description-4'}  
                 ] 
                 } 
                 });
    

    输出结果:
    在这里插入图片描述

    <li v-for="(key, value) in objectDemo">
     {{key}} - {{$key}} : {{value}}
     </li>
     var vm = new Vue({ 
         el : '#app', 
         data: {  
           objectDemo : {   
             a : 'a-value',  
              b : 'b-value',   
             c : 'c-value',  
               } 
               } 
               });
    

    输出结果:
    在这里插入图片描述

    <li v-for="n in 5">
    {{ n }}
    </li>
    

    输出结果:
    在这里插入图片描述

    4.template标签的用法

    <template v-if="yes">
    <p>There is first dom</p>
    <p>There is second dom</p>
    <p>There is third dom</p>
    </template>
    

    结果:
    在这里插入图片描述

    <template v-for="item in items">
    <p>{{item.name}}</p>
    <p>{{item.desc}}</p>
    </template>
    
    展开全文
  • template渲染到页面流程图: *** template–编译–>render(){}–调用–>h()–生成–>虚拟DOM(JS对象)–映射–>真实DOM–渲染–>浏览器展示 *** 5-5 Vue插件的开发和使用 插件通常用来为 Vue 添加全局功能; 也是Vue-...
  • 目录模板模板的作用是什么模板书写...为了提高效率,vue会把模板编译成dom树(VNode),然后生成真实的Dom 模板书写位置 在挂载的元素内部直接书写 <div id="app">{{title}}</div> 在template 配置中书写
  • vue 宣传页预渲染成PHP template问题用vue做的宣传单页,build 后的页面只有一个div 标签,内容都在js 文件用,最关键的是一些页面需要php变量 输出一些数据,这些数据是无状态的,异步请求浪费一次请求,需求希望在...
  • 本文章主要记录在vue业务场景中,使用h5标签和element组件标签模板字符串渲染的代码解析

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,152
精华内容 1,660
关键字:

vue模板渲染

vue 订阅