写点什么

Vue 2x 中使用 render 和 jsx 的最佳实践 (3)

作者:默默的成长
  • 2022-10-17
    山东
  • 本文字数:9544 字

    阅读完需:约 1 分钟

从Vue编译后的代码看createElement

你是否看过写的 Vue 代码经过编译之后的样子,比如下面这段代码


<template>    <div>Hellow world<span class="flex">Hellow world</span></div></template>
复制代码




通过对上面的代码进行分析,不难发现,Vue 模板中的每一个元素编译之后都会对应一个createElement


无论是 Vue 还是 React,都存在 createElement,而且作用基本一致。


createElement 函数返回的值称之为虚拟节点,即VNode,而由VNode扎堆组成的树便是大名鼎鼎的虚拟DOM


到这里,是不是逻辑和上面 React 提到的是一样的?


(o゜▽゜)o☆[BINGO!]


我们来看看 Vue 官方文档定义的createElement


// @returns {VNode}createElement(  // {String | Object | Function}  // 一个 HTML 标签名、组件选项对象,或者  // resolve 了上述任何一种的一个 async 函数。必填项。  'div',
// {Object} // 一个与模板中 attribute 对应的数据对象。可选。 { // (详情见下一节) },
// {String | Array} // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成, // 也可以使用字符串来生成“文本虚拟节点”。可选。 [ '先写一些文字', createElement('h1', '一则头条'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ])
复制代码


从上面可以看出 createElement 同样有三个参数,三个参数分别是:


  • String | Object | Function

  • 一个 HTML 标签名、组件选项对象(比如div),或者 resolve 了上述任何一种的一个 async 函数。

  • 必填项。

  • Object

  • 一个与模板中 attribute 对应的数据对象。

  • 可选。

  • String | Array

  • 子级虚拟节点 (VNodes),由 createElement() 构建而成,也可以使用字符串来生成“文本虚拟节点”

  • 可选。


所以本质上面来说,在Vue里面,你也可以像写React一样,通过Render来使用JSX

在Vue中使用 Render 和 JSX

在 Vue 中,通常大家习惯了使用template的语法。


尽管template 和 JSX 都属于 xml 的写法,而且他们也比较像,但是本质还是有许多不一样的地方:


老规矩,上传送门

v-model


当你选择使用JSX的时候,你就要做好和指令说拜拜的时候了。


在 JSX 中, 你唯一可以使用的指令是v-show,除此之外,其他指令都是不可以使用的,有没有感到很慌,这就对了。不过呢,换一个角度思考,指令只是 Vue 在模板代码里面提供的语法糖,现在你已经可以写 Js 了,那些语法糖用 Js 都可以代替了。


在新版脚手架vue-cli4中,已经默认集成了对v-model的支持,大家可以直接使用,如果你的项目比较老,也可以安装插件babel-plugin-jsx-v-model来进行支持


export default {  name:'vInput',  props: {      value:[String,Number]  },  data() {    return {      name: ''    }  },  methods: {    // 监听 onInput 事件进行赋值操作    handleInput(e) {      this.name = e.target.value            // 这里对组件实现了v-model语法糖      this.$emit('input', e.target.value);    }  },  render() {    // 传递 value 属性 并监听 onInput事件    return <input value={this.name} onInput={this.handleInput}></input>        // 如果安装了插件或者使用vue-cli4 ,可以和template一样舒服的使用v-model    return <input v-model={this.name} />  }}
// JSX : <Vinput v-model={this.value} />// or template : <v-input v-model="value" />
复制代码


注意上面的代码最后注释的代码,因为在JSX中,我们已经通过babel可以得到v-model语法糖的支持,那么我们在使用JSX写自己的组件的时候,一定要注意实现组件的v-model语法糖,去支持该特性。


什么? 你还不懂什么是v-model


快去学习!!!

自定义model

export default {  name:'vInput',  props: {      defaultCode:[String,Number]  },  data() {    return {      name: ''    }  },  model: {    prop: 'defaultCode',    event: 'update'  },  methods: {    // 监听 onInput 事件进行赋值操作    handleInput(e) {      this.name = e.target.value            // 这里对组件实现了v-model语法糖      this.$emit('update', e.target.value);    }  },  render() {    // 传递 value 属性 并监听 onInput事件    return <input value={this.name} onInput={this.handleInput}></input>  }}
复制代码

.sync

v-model一样,.sync也需要用属性+事件的方式来实现


.sync目前在JSX中没有任何babel支持:(


export default {  data(){      return {          defaultCode:'',      },  },  methods: {    handleChangeDefaultCode(value) {      this.visible = value    }  },  render() {    return (      <v-input        defaultCode={this.defaultCode}        on={{ 'update:defaultCode': this.handleChangeDefaultCode }}      ></v-input>    )  }}
复制代码

v-bind

在 template 中,我们一般通过v-bind:prop="value":prop="value"来给组件绑定属性,在 JSX 里面写法也类似:


render() {    return <v-input defaultCode={this.defaultCode}></v-input>}
复制代码

v-if 与 v-for


不要着急,这些指令只是黑魔法,用 js 很容易实现。


  • v-if


render(){    const arg1 = 1;    const arg2 = 4;    return (        <div>            {this.show? arg1 : arg2}        </div>    )}
复制代码


写三元表达式只能写简单的,那么复杂的还得用 if/else


  render(){    const { show } = this;    let ifText;    if(show){        ifText = (<p>1</p>)    }else{        ifText = (<p>4</p>);    }    // let ifText = show ? (<p>1</p>) : (<p>4</p>);        const showButton = true;    return (      <div>        {ifText}        {            showButton && <button/>        }      </div>    )  }
复制代码
复制代码


  • v-for


render(){    const t = 'hello world';    const arg1 = 1;    const arg2 = 2;    const hasButton = true;    const list = [1,2,3,4,5,6,7,8,9];    let jsx = (        <div>            <h1>            {                t === 'hello world' ?  arg1 : arg2            }            </h1>            {                //如果hasButton为true,则渲染button组件                hasButton && <button/>            }            <ul>            {                // 替代 v-for                list.map((item) => <li>{item}</li>)            }            </ul>        </div>    )    return jsx;}
复制代码

自定义组件

很简单,只需要导入进来,不用再在 components 属性声明了,直接写在 jsx 中比如


<script>  import Vinput from './vInput'    export default {      name: "item",      render(){        return (            <Vinput/>        )      }    }</script>
复制代码

v-html 与 v-text

在说v-htmlv-text之前,我们需要先了解一下 Vue 中的属性,Vue 中的属性一共分为三种:


  • props,即组件自定义的属性;

  • attrs,是指在父作用域里面传入的,但并未在子组件内定义的属性。

  • domProps,主要包含三个,分别是innerHTML,textContent/innerTextvalue


v-html


template 中,我们用v-html指令来更新元素的 innerHTML 内容,而在 JSX 里面,如果要操纵组件的 innerHTML,就需要用到domProps


// v-html 指令在JSX的写法是 domPropsInnerHTML
renderContent(h,{ node, data, store }){ const { dataModel , showIcon, icon, hasOptions} = this; const { title, valueFormat } = dataModel; const key = isEmpty(title) ? 'label' : title; const label = isEmpty(valueFormat) ? data[key] : valueFormat(data); if(icon) data.icon = icon;
const add = this.nodeOptionClick.bind(this,'add', node, data); const edit = this.nodeOptionClick.bind(this,'edit', node, data); const remove = this.nodeOptionClick.bind(this,'remove', node, data); //nativeOnClick={(e)=>{e.stopPropagation();}} return ( <span class="custom-tree-node"> <div class="left-all" title={data[key]}> { showIcon && <wg-icon name={data.icon}/> } // v-html 指令在JSX的写法是 domPropsInnerHTML <span class="node-label" domPropsInnerHTML={label}/> </div> { hasOptions && ( <span class="right-op"> <wg-icon class="op-button" onClick={add} name="icon-xinzeng1"> </wg-icon> <wg-icon class="op-button" onClick={edit} name="icon-jianyi"> </wg-icon> <wg-icon class="op-button-danger" onClick={remove} name="icon-shanchu"> </wg-icon> </span> ) } </span>); },
复制代码


v-text


举一反三,v-text 指令在 JSX 的写法是 domPropsInnerText


但实际上我们不需要使用 domPropsInnerText,而是将文本作为元素的子节点去使用即可


renderContent(h,{ node, data, store }){      ……      return (          <span class="custom-tree-node">            ……            <span class="node-label" domPropsInnerText={label}/>                        // 但实际上我们不需要使用domPropsInnerText,而是将文本作为元素的子节点去使用即可             <span class="node-label">            { label }            </span>            ……          </span>);    },
复制代码

事件 v-on

当我们开发一个组件之后,一般会通过this.$emit('change')的方式对外暴露事件,然后通过v-on:change的方式去监听事件,很遗憾,在 JSX 中你无法使用v-on指令,但你将解锁一个新的姿势


return (  <wg-el-select    {...{ props }}    {...{ on }}    v-loading={loading}    value={this.value}    onChange={this.mychange}  >    {dataSourceM.map((item) => {      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;    })}  </wg-el-select> )
复制代码


JSX 中,通过on + 事件名称的大驼峰写法来监听,比如事件 change,在 JSX 中写为 onChange

事件监听 .native

监听原生事件的规则与普通事件是一样的,只需要将前面的on替换为nativeOn


return (  <wg-el-select    {...{ props }}    {...{ on }}    v-loading={loading}    value={this.value}    nativeOnChange={this.mychange}  >    {dataSourceM.map((item) => {      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;    })}  </wg-el-select> )
复制代码


除了上面的监听事件的方式之外,我们还可以使用对象的方式去监听事件


注意是双花括号,第一个花括号{}表示 v-bind,第二个表示这是个对象json


return (  <wg-el-select    on={{        click:this.myclick    }}    nativeOn={{        change:this.mychange,    }}  >    {dataSourceM.map((item) => {      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;    })}  </wg-el-select> )
复制代码

事件修饰符

这里是一个使用所有修饰符的例子:



on: { keyup: function (event) { // 如果触发事件的元素不是事件绑定的元素 // 则返回 if (event.target !== event.currentTarget) return // 如果按下去的不是 enter 键或者 // 没有同时按下 shift 键 // 则返回 if (!event.shiftKey || event.keyCode !== 13) return // 阻止 事件冒泡 event.stopPropagation() // 阻止该元素默认的 keyup 事件 event.preventDefault() // ... }}
复制代码


  • .stop : 阻止事件冒泡,在 JSX 中使用 event.stopPropagation()来代替


    // 阻止 事件冒泡    event.stopPropagation()}
复制代码


  • .prevent:阻止默认行为,在 JSX 中使用 event.preventDefault() 来代替


    // 阻止该元素默认的 keyup 事件    event.preventDefault()}
复制代码


  • .self:只当事件是从侦听器绑定的元素本身触发时才触发回调,使用下面的条件判断进行代替


    // 如果触发事件的元素不是事件绑定的元素    // 则返回    if (event.target !== event.currentTarget) return}
复制代码


  • .enter 与 keyCode: 在特定键触发时才触发回调


    // 如果按下去的不是 enter 键或者    // 没有同时按下 shift 键    // 则返回    if (!event.shiftKey || event.keyCode !== 13) return
复制代码


除此之外,官方还对此做了一定的优化,提供了前缀语法来帮助我们简化代码:


 render() {    return (      <div        on={{          // 相当于 :click.capture          '!click': this.click,          // 相当于 :input.once          '~input': this.input,          // 相当于 :mousedown.passive          '&mousedown': this.mousedown,          // 相当于 :mouseup.capture.once          '~!mouseup': this.mouseup        }}      ></div>    )  }
复制代码


插槽 slots

插槽就是子组件中提供给父组件使用的一个占位符,插槽分为默认插槽具名插槽作用域插槽


默认插槽

  • 使用默认插槽


JSX中使用默认插槽的用法与普通插槽的用法基本是一致的,如下代码所示:


return (  <wg-el-select    on={{        click:this.myclick    }}    nativeOn={{        change:this.mychange,    }}  >     // 这里就是默认插槽    {dataSourceM.map((item) => {      return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;    })}  </wg-el-select> )
复制代码


  • 自定义默认插槽 你可以通过 this.$slots 访问静态插槽的内容,


这个上面就挂载了一个这个组件内部的所有插槽


this.$slots.default 即代表默认插槽


render() {    return (      <div>        {            // 通过this.$slots.default定义默认插槽            this.$slots.default        }      </div>    )  }
复制代码

具名插槽

  • 使用具名插槽

  • 有时候我们一个组件需要多个插槽,这时候就需要为每一个插槽起一个名字,比如element-ui的弹框可以定义底部按钮区的内容,就是用了名字为footer的插槽:


<template>  <el-dialog    :title="title"    :destroy-on-close="destroy"    :visible.sync="dialogVisible"    :width="width"    :before-close="handleClose"    :append-to-body="true"  >    <components :is="model" :_dataForm="formData" :formOtp="formOtp" ref="model">    </components>        <!-- 具名插槽 -->        <span slot="footer" class="dialog-footer">      <el-button type="primary" v-if="hasContinue" @click="handleContinue">保存并继续</el-button>      <el-button type="primary" @click="handleSubmit">保存</el-button>      <el-button @click="dialogVisible = false">取 消</el-button>    </span>  </el-dialog></template>
复制代码


修改为 JSX:


 render() {    return (      <el-dialog title={this.title} visible={this.visible}>        ……        {/** 具名插槽 */}        <template slot="footer">          <el-button>保存</el-button>          <el-button>取消</el-button>        </template>      </el-dialog>    )  }
复制代码


  • 自定义具名插槽


对于默认插槽使用this.$slots.default


而对于具名插槽,可以使用this.$slots.footer进行自定义


render() {    return (      <div>        {            // 通过this.$slots.footer            this.$slots.footer        }      </div>    )  }
复制代码

作用域插槽

  • 使用作用域插槽

  • 有时让插槽内容能够访问子组件中才有的数据是很有用的,这时候就需要用到作用域插槽,

  • JSX中,因为没有v-slot指令,所以作用域插槽的使用方式就与模板代码里面的方式有所不同了。

  • 比如在element-ui中,我们使用el-table的时候可以自定义表格单元格的内容,这时候就需要用到作用域插槽


// TODO: 创建操作表头    createColumnsOption(data){      const { switchChange, editClick, removeClick } =this;      if(data.switchKey) this.tableData = this.dataSource.map(c=>c[data.switchKey]  === 1);      const noRemove = data?.noRemove || false;      const noEdit = data?.noEdit || false;      const p = {        props:{          type:'operation',          label:'操作',          width: data.switchKey ? noRemove || noEdit ? '92' : '120' : noRemove || noEdit ? '55' : '80',          fixed:'right',          ...data,        },        scopedSlots:{default:(props)=>{        // scopedSlots即作用域插槽,default为默认插槽,如果是具名插槽,将default该为对应插槽名称即可          const { row, $index } = props;                    return (            <div class="flex ac tableOption">              {                noEdit === false && <wg-button onClick={editClick.bind(this,props)} icon="icon-bianji1" type="text"/>              }              {                noRemove === false && <wg-button onClick={removeClick.bind(this,props)} icon="icon-shanchu" type="text"/>              }              {                data.switchKey && (                  <el-switch v-model={this.tableData[$index]} onchange={(v)=>switchChange(v, props)}  class="ml12"/>                )              }            </div>          )        }}      };      return <wg-table-column {...p}/>    },
复制代码


  • 自定义作用域插槽


假如我们自定义了一个列表项组件,用户希望可以自定义列表项标题,这时候就需要将列表的数据通过作用域插槽传出来。


```render() {    const { data } = this    // 获取标题作用域插槽    const titleSlot = this.$scopedSlots.title    return (      <div class="item">        {/** 如果有标题插槽,则使用标题插槽,否则使用默认标题 */}        {titleSlot ? titleSlot(data) : <span>{data.title}</span>}      </div>    )  }```
复制代码

JSX除了在render中可以被使用外,还可以在method定义的任何一个方法中使用

 methods: {    renderFooter() {      return (        <div>          <el-button>保存</el-button>          <el-button>取消</el-button>        </div>      )    },  }, render() {    return (      <el-dialog title={this.title} visible={this.visible}>        ……        {/** 具名插槽 */}        <template slot="footer">          {              this.renderFooter()          }        </template>      </el-dialog>    )  }
复制代码

指令

虽然大部分内置的指令无法直接在JSX里面使用,但是自定义的指令可以在JSX里面使用,就拿element-uiv-loading指令来说,可以这样用


  render() {    /**     * 一个组件上面可以使用多个指令,所以是一个数组     * name 对应指令的名称, 需要去掉 v- 前缀     * value 对应 `v-loading="value"`中的value     */    const directives = [{ name: 'loading', value: this.loading }]    return (      <div        {...{          directives        }}      ></div>    )  }复制代码
复制代码

指令修饰符

有些指令还可以使用修饰符,比如上例中的v-loading,你可以通过修饰符指定是否全屏遮罩,是否锁定屏幕的滚动,这时候就需要这样写 v-loading.fullscreen.lock = "loading"


  render() {    /**     * modifiers指定修饰符,如果使用某一个修饰符,则指定这个修饰符的值为 true     * 不使用可以设置为false或者直接删掉     */    const directives = [      {        name: 'loading',        value: this.loading,        modifiers: { fullscreen: true, lock: false }      }    ]    return (      <div        {...{          directives        }}      ></div>    )  }
复制代码

JSX中的函数式组件

Vue 官方传送门


函数式组件意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。


因为函数式组件只是一个函数,所以渲染开销也低很多。然而,对持久化实例的缺乏也意味着函数式组件不会出现在 Vue devtools 的组件树里。


因为函数式组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。


实际上,它只是一个接受一些 prop 的函数。


所以在少了很多响应式处理和操作的基础上,函数式组件可以会提高速度和减少内存占用。


又因为只是函数,所以渲染开销也低很多


在 template 中,函数式组件可以这样(注意是Vue 2.5.0 及以上版本):


<template functional>
</template
复制代码


而在 JSX 中,


我们只需增加配置functional: true就可以了


export default {      functional:true,      render(h, context){        return (          <div />        )      }}
复制代码


函数式组件render相比普通组件render的变化:


  • 对于函数式组件 vue 增加了 context 对象,需要作为render(h,context) 第二个参数传入

  • this.$slots.default更新为context.children

  • props原本是直接挂在 this 上的,现在变为context.props挂在了 context.props 上。

  • this.data变为了context.data


需要注意的是对于函数式组件,没有被定义为 prop 的特性不会自动添加到组件的根元素上,意思就是需要我们手动添加到组件根元素了,看个例子吧


//父组件 ... render(){      return (        <Item data={this.data} class="large"/>      )    }//Item.vue组件export default {    functional:true,      name: "item",      render(h,context){        return (          <div class="red" >            {context.props.data}          </div>        )      }    }
复制代码


上面代码期待的是.large 类名传入到了 Item 的根元素上,但是其实没有。我们需要增加点东西


// Item.vueexport default {    functional:true,      name: "item",      render(h,context){        return (          <div class="red" {...context.data}>            {context.props.data}          </div>        )      }    }
复制代码


注意到,通过展开运算符把所有的属性添加到了根元素上,这个 context.data 就是你在父组件给子组件增加的属性,他会跟你在子元素根元素的属性智能合并,现在.large 类名就传进来了。这个很有用,当你在父组件给子组件绑定事件时就需要这个了。


向 createElement 通过传入 context.data 作为第二个参数,我们就把 my-functional-button 上面所有的特性和事件监听器都传递下去了。事实上这是非常透明的,那些事件甚至并不要求 .native 修饰符


上面是 vue 官网的一段话



在函数式组件中,不需要.native 修饰符,所以在函数式组件中,nativeOn 并不会生效

总结


在 Vue 中像写 React 一样使用RenderJSX,可能并不是多么一件美好的事情,正如官方文档告诉我们的,“这就是深入底层的代价”。

用户头像

还未添加个人签名 2022-10-11 加入

还未添加个人简介

评论

发布
暂无评论
Vue 2x 中使用 render 和 jsx 的最佳实践 (3)_前端_默默的成长_InfoQ写作社区