美文网首页
Vue2 + JSX使用总结

Vue2 + JSX使用总结

作者: 东扯葫芦西扯瓜 | 来源:发表于2022-05-29 17:10 被阅读0次

    什么是JSX

    摘自 React 官方:
    它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模板语言,但它具有 JavaScript 的全部功能。

    Vue 什么时候应当使用JSX

    这里说的是应当,而不是必须。因为在绝大多数情况下,模板语法都能胜任,只不过写起来看着不太好看而已。或者使用模板语法,那写起来恐怕不是一般的长,而且阅读会费劲很多。

    下面我们来看下应当使用JSX而不是模板语法的情况:

    假设现在有如下需求:
    封装一个机遇ant-design-vue的输入框组件,组件要求有如下功能
    1.传入form属性,布尔值,组件自动套上a-form-model-item标签,并接收相应的属性
    2.placeholderTip 属性,布尔值,组件自动挡套上a-tooltip,显示值为placeholder
    3.传入span,数字,并且大于0小于24,自动套上a-col标签
    4.如果都没传,那就只渲染a-input标签
    5.如果同时传1,2,3中两个以上的属性,那么包裹顺序为,从外到里依次是a-col,a-form-model-item,a-tooltip

    让我们先用模板语法实现下这个组件 input.vue

    <!--input 输入框-->
    <template>
      <div>
    <!--    先判断是否有span-->
        <a-col v-if="span > 0 && span<24" :span="span">
          <!--    先判断是否有form-->
          <a-form-model-item  v-if="form" :label="label" :prop="prop">
            <!--    先判断是否有placeholderTip-->
            <a-tooltip v-if="placeholderTip" placement="topLeft" :title="placeholder">
              <a-input v-bind="$attrs"
                       :value="value"
                       @change="inputChange"
              />
            </a-tooltip>
            <!--    都没有,只渲染a-input额-->
            <a-input v-else
                     v-bind="$attrs"
                     :value="value"
                     @change="inputChange"
            />
          </a-form-model-item>
          <!--    如果没有form,判断是否有placeholderTip-->
          <a-tooltip v-else-if="placeholderTip" placement="topLeft" :title="placeholder">
            <a-input v-bind="$attrs"
                     :value="value"
                     @change="inputChange"
            />
          </a-tooltip>
          <!--    都没有,只渲染a-input额-->
          <a-input v-else
                   v-bind="$attrs"
                   :value="value"
                   @change="inputChange"
          />
        </a-col>
        <!--    先判断是否有form-->
        <a-form-model-item  v-else-if="form" :label="label" :prop="prop">
          <!--    先判断是否有placeholderTip-->
          <a-tooltip v-if="placeholderTip" placement="topLeft" :title="placeholder">
            <a-input v-bind="$attrs"
                     :value="value"
                     @change="inputChange"
            />
          </a-tooltip>
          <!--    都没有,只渲染a-input额-->
          <a-input v-else
                   v-bind="$attrs"
                   :value="value"
                   @change="inputChange"
          />
        </a-form-model-item>
        <!--    如果没有form,判断是否有placeholderTip-->
        <a-tooltip v-else-if="placeholderTip" placement="topLeft" :title="placeholder">
          <a-input v-bind="$attrs"
                   :value="value"
                   @change="inputChange"
          />
        </a-tooltip>
    <!--    都没有,只渲染a-input额-->
        <a-input v-else
                  v-bind="$attrs"
                 :value="value"
                 @change="inputChange"
        />
      </div>
    </template>
    <script>
    export default {
      name: "Input",
      props: {
        value: [String, Number], // 值
        placeholderTip: Boolean, // 输入框 placeholder提示
        form: Boolean, // 是否使用form-item标签包裹
        label: String, // 标签
        prop: String, // 校验的prop
        placeholder:String,//提示
        span:Number,//span
      },
      methods: {
        /**
         * 输入框改变
         * @param e
         */
        inputChange(e) {
          let v = e.target.value
          this.$emit("input", v)
          this.$emit("change", v)
        },
      },
    }
    </script>
    
    

    从上面代码我们可以看出,有好几段看起来一样的代码,但是我们却不好抽离出来。或者说并不能完全剥离。
    比如这段

     <a-tooltip v-else-if="placeholderTip" placement="topLeft" :title="placeholder">
            <a-input v-bind="$attrs"
                     :value="value"
                     @change="inputChange"
            />
          </a-tooltip>
    

    在代码中被用了4次,看着是可以剥离出去的,抽成一个新的子组件,然而接着你发现,他里面的

      <a-input v-bind="$attrs"
                     :value="value"
                     @change="inputChange"
            />
    

    这个却是在父组件有单独使用。而且就算剥离出去,模板里面的多个相同的判断,v-else-if="placeholderTip" 和 v-if="placeholderTip"也无法减少。

    下面我们用jsx来实现一下

    <script>
    export default {
      name: "Input",
      props: {
        value: [String, Number], // 值
        placeholderTip: Boolean, // 输入框 placeholder提示
        form: Boolean, // 是否使用form-item标签包裹
        label: String, // 标签
        prop: String, // 校验的prop
        placeholder:String,//提示
        span:Number,//span
      },
      methods: {
        /**
         * 输入框改变
         * @param e
         */
        inputChange(e) {
          let v = e.target.value
          this.$emit("input", v)
          this.$emit("change", v)
        },
        /**
         * 渲染form-item节点
         * @param child
         * @returns {JSX.Element}
         */
        renderFormItem (child) {
          const { label, prop} = this
          return <a-form-model-item label={label} prop={prop}>
            {child}
          </a-form-model-item>
        },
        /**
         * 渲染输入框组件
         * @returns {JSX.Element}
         */
        renderInputDom(){
          return <a-input attrs={this.$attrs}
                         value={this.value}
                         onChange={this.inputChange}
          />
        },
      },
      render (createElement, context) {
        const { placeholderTip, form,span } = this
        const hasSpan = typeof span === "number" && span>0 && span<24
        const inputChild = placeholderTip ? <a-tooltip placement="topLeft" title={this.placeholder}>
          { this.renderInputDom()}
        </a-tooltip> : this.renderInputDom()
        if(form){
          return hasSpan ? <a-col span={span}>{this.renderFormItem(inputChild)}</a-col> : this.renderFormItem(inputChild)
        }
        return hasSpan ? <a-col span={span}>{inputChild}</a-col> : inputChild
      }
    }
    </script>
    
    

    看下两者的不同:
    首先就从代码行数来说,用模板91行,去掉模板里面的注释,那也还有80行,而用jsx,不到60行
    其次,使用jsx,我们将渲染a-input和a-form-model-item抽离成渲染函数,是否有a-tooltip 和 a-col则使用三元运算符配合。在需要的地方调用相应的渲染函数,相比模板语法的直接复制标签,jsx维护性更好。

    上面这个例子也许还不能看出jsx的重要性。下面说个复杂点的需求。
    1.一个表单页面,表单项是动态的
    2.页面渲染哪个表单组件(输入框还是下拉框,或者单选,复选框等),是根据服务器返回的数据指明的值渲染的
    3.页面每行排几个表单元素,是动态的,根据服务器返回的值和表单元素本身一些特性来决定(比如多文本输入框,富文本,直接要求占整行)
    4.表单元素排列的顺序先后,由数据数组的下标决定

    这个需求,例子就不写了。如果用模板语法,那会更糟糕。

    那我们什么时候应当使用jsx,而不是模板?
    1.页面的渲染有比较多的条件,而且这些条件又不在同一层,有交叉,嵌套等情况
    2.页面元素是动态的,元素间排列组合是动态的

    当然,对于vue的开发者来说,一般的业务开发,还是模板为主,用起来更简单。至于jsx,除了上面说的动态表单外,组件封装可能会用的相对较多。

    jsx用法总结

    对于使用vue-cli创建的项目,jsx是自带的,我们不需要安装啥东西。如果么有使用vue-cli创建项目,这里假设已经安装了@vue/babel-preset-jsx插件

    • 1 如何使用jsx替代template标签渲染dom?
      使用模板
    <template>
        <div>
            <div>测试</div>
        </div>
    </template>
    

    使用jsx。render函数用于渲染html,在methods的方法里面,也可以直接return html标签。在.vue文件中,需要写在script标签里面,在js文件或者jsx文件中,则不用script标签

    jsx返回标签,可以简单理解为拼串,在大括号{}里面可以写js代码

    <script>
    export default {
        methods:{
        /**
        渲染子元素
        **/
            renderChild(){
                return <div>测试</div>
            }
        },
        render(){
            return <div>{ this.renderChild() }</div>
        }
    }
    </script>
    

    2 如何书写属性,特别是值是动态变化的属性
    使用模板

    <template>
        <div>
            <div :title="$route.name">测试</div>
        </div>
    </template>
    

    jsx。有变化的地方就是用大括号{},至于大括号里面,那就是js代码。所以下面的示例title={this.$route.name},也可写成 title={this.getName()},类似这样,在getName函数里面return 出值就好。

    <script>
    export default {
        /**
        渲染name
        **/
            getName(){
                return this.$route.name
                //假设组件最终需要的是组件或者标签,这里还可以类似这样
                //return <div>{this.$route.name}</div>
            }
        render(){
            return <div title={this.$route.name}>测试</div>
            //return <div title={this.getName()}>测试</div>
        }
    }
    </script>
    

    这里重点说下class和style。由于class和style写法相对比较多,同样的jsx也可以有多种形式
    测试样式效果图


    image.png

    样式

    .default999{
      color:#999999;
      background: blueviolet;
    }
    .border-red{
      border:solid 1px red;
      margin-bottom: 20px;
    }
    .yellow-bk{
      background: yellow;
    }
    .red{
      color:red;
    }
    .green{
      color:green;
    }
    .edit{
      box-shadow: 1px 1px 1px #2b96ff;
    }
    .view{
      box-shadow: 2px 2px 1px #8cc5ff;
    }
    .bold-font{
      font-weight: bold;
    }
    .line-through{
      text-decoration: line-through;
    }
    

    使用模板

    <template>
      <div>
        <div :class="{'yellow-bk':$route.query.isEdit === 'true'}">yellow-bk</div>
        <div class="default999" :class="{'yellow-bk':$route.query.isEdit === 'true'}">yellow-bk default999</div>
        <div class="border-red" :class="$route.query.isEdit === 'true' ? 'yellow-bk' : 'default999'">yellow-bk | default / border-red</div>
        <div class="border-red" :class="[$route.query.id === 'xxx' ? 'red' : 'green', editStyle]">red | green / bold-font | line-through /border-red</div>
        <div class="border-red" :style="{
            'margin-bottom':$route.query.isEdit === 'true' ? '8px' : '10px',
            'display':$route.query.isEdit === 'true' ? 'block' : 'flex'
          }">style 1 /border-red</div>
        <div class="border-red" :style="errStyle">style 2 /border-red / errStyle</div>
      </div>
    </template>
    <script>
    export default {
      data (){
        return {
          errStyle:{
            color:"red",
            background:"#85ce61"
          }
        }
      },
      computed:{
        editStyle(){
          return this.$route.query.isEdit === "false" ? "bold-font" : "line-through"
        }
      },
      methods: {
        /**
         * 输入框改变
         * @param e
         */
        inputChange(e) {
          let v = e.target.value
          this.$emit("input", v)
          this.$emit("change", v)
        },
      },
    }
    </script>
    
    

    style其实就是一个对象,所以不管怎么变,只要得到一个对象或者返回一个对象即可
    class花样要多点,模板有对象形式,三元运算符形式,数组形式,默认类名。当然也可以用函数返回

    jsx

    <script>
    export default {
      data (){
        return {
          errStyle:{
            color:"red",
            background:"#85ce61"
          }
        }
      },
      computed:{
        editStyle(){
          return this.$route.query.isEdit === "false" ? "bold-font" : "line-through"
        }
      },
      methods: {
        /**
         * 输入框改变
         * @param e
         */
        inputChange(e) {
          let v = e.target.value
          this.$emit("input", v)
          this.$emit("change", v)
        },
      },
      render(){
        return   <div>
          <div class={{'yellow-bk':this.$route.query.isEdit === 'true'}}>yellow-bk</div>
          <div class={{ 'yellow-bk': this.$route.query.isEdit === 'true',default999:true }}>yellow-bk/default999</div>
          <div class={this.$route.query.isEdit === 'true' ? 'yellow-bk border-red' : 'default999 border-red'}>yellow-bk  /border-red</div>
          <div class={[this.$route.query.id === 'xxx' ? 'red' : 'green', this.editStyle,'border-red']}>red | green / bold-font | line-through /border-red </div>
          <div class="border-red" style={{
            'margin-bottom':this.$route.query.isEdit === 'true' ? '8px' : '10px',
            'display':this.$route.query.isEdit === 'true' ? 'block' : 'flex'
          }}>style 1 /border-red</div>
          <div class="border-red" style={this.errStyle}>style 2 /border-red / errStyle</div>
        </div>
      }
    }
    </script>
    

    模板语法可以使用两个class,一个正常使用,一个变量形式 ,如

    <span class="red" :class={blue:isEdit}></span>
    

    jsx只能写一个class,上面需要写成

    <span :class={blue:isEdit,red:true}></span>
    

    3 指令
    在jsx里面,指令变成小驼峰,例如v-model可变为vModel

    3.1 自定义指令
    模板语法

    <template>
      <div>
        <div v-default></div>
        <div v-default:指令参数默认值></div>
        <div v-default="{name:'动态数据默认值'}"></div>
        <div v-default:指令参数默认值="{name:'动态数据默认值'}"></div>
      </div>
    
    </template>
    <script>
    export default {
      directives:{
        default:{
          // 当被绑定的元素插入到 DOM 中时……
          bind: function (el, binding, vnode) {
            const actionName = binding.arg
            const value = binding.value
            console.log("actionName",actionName)
            console.log("value",value)
            let innerHtml = (actionName || "") + (value ? ((actionName ? "+" : "") + JSON.stringify(value)):  "")
            el.innerHTML=innerHtml || "自定义指令"
          }
        }
      }
    }
    </script>
    

    代码运行后结果如图:


    image.png

    jsx

    <script>
    export default {
      directives:{
        default:{
          // 当被绑定的元素插入到 DOM 中时……
          bind: function (el, binding, vnode) {
            const actionName = binding.arg
            const value = binding.value
            console.log("actionName",actionName)
            console.log("value",value)
            let innerHtml = (actionName || "") + (value ? ((actionName ? "+" : "") + JSON.stringify(value)):  "")
            el.innerHTML=innerHtml || "自定义指令"
          }
        }
      },
      render(createElement, context) {
        return   <div>
          <div vDefault></div>
          <div vDefault:指令参数默认值></div>
          <div vDefault={{name:'动态数据默认值'}}></div>
          <div vDefault:指令参数默认值={{name:'动态数据默认值'}}></div>
        </div>
      }
    }
    </script>
    

    上面这段代码看似没问题,但是运行后,我们发现结果如下:


    image.png

    发现多了个true,也就是指令没有传值的时候,默认为true,想要实现模板的效果,还需更改下,vDefault={undefined},下面是更改后的代码

    <script>
    export default {
      name:"Input",
      directives:{
        default:{
          // 当被绑定的元素插入到 DOM 中时……
          bind: function (el, binding, vnode) {
            const actionName = binding.arg
            const value = binding.value
            console.log("actionName",actionName)
            console.log("value",value)
            let innerHtml = (actionName || "") + (value ? ((actionName ? "+" : "") + JSON.stringify(value)):  "")
            el.innerHTML=innerHtml || "自定义指令"
          }
        }
      },
      render(createElement, context) {
        return   <div>
          <div vDefault={undefined}></div>
          <div vDefault:指令参数默认值={undefined}></div>
          <div vDefault={{name:'动态数据默认值'}}></div>
          <div vDefault:指令参数默认值={{name:'动态数据默认值'}}></div>
        </div>
      }
    }
    </script>
    

    3.2 内置指令
    这些指令有 v-html,v-if,v-for,v-text,v-show,v-model,v-bind,v-on,v-slot等。其中只有少部分适用于驼峰形式

    3.2.1 适用于驼峰形式的指令:v-show,v-model,v-on(在事件绑定处单独说明)
    以表单双向数据绑定的v-model举例
    模板语法

    <template>
    <a-input v-model="value">
    </template>
    <script>
        export default{
            data(){
                return {
                    value:"",//值
                    }
            }
        }
    </script>
    

    使用jsx

    <script>
        export default{
            data(){
                return {
                    value:"",//值
                    }
            },
            render(){
                return <a-input vModel={this.value}/>
                }
        }
    </script>
    

    修饰符
    模板语法

    <input v-model.trim="value"/>
    

    jsx,使用_分隔修饰符

    <input vModel_trim={this.value} />
    

    3.2.2 不适用与驼峰形式的指令。内置指令大部分都不适用于驼峰形式,除v-slot放插槽处单独说明外,下面一一列举。

    3.2.2.1 v-html
    我们先用v-html来试下使用驼峰形式的例子
    模板语法

    <template>
      <div v-html="'自定义html'"></div>
    </template>
    

    按照上面的写法使用jsx

    <script>
    export default {
      render(createElement, context) {
        return   <div>
          <div vHtml={"自定义html"}></div>
        </div>
      }
    }
    </script>
    

    写好后,我们运行,发现报错
    vue.runtime.esm.js?c320:619 [Vue warn]: Failed to resolve directive: html
    (found in)


    image.png

    就是说html不是一个指令。@vue/babel-preset-jsx给出的标准写法是使用domPropsInnerHTML

    <script>
    export default {
      name:"Input",
      render(createElement, context) {
        return   <div>
          <div domPropsInnerHTML={"自定义html"}></div>
        </div>
      }
    }
    </script>
    

    3.2.2.2 v-text

    模板语法

    <template>
      <div>
        <div v-text="text"></div>
      </div>
    </template>
    
    <script>
    export default {
      data(){
        return{
          text:"vText文字"
        }
      }
    }
    </script>
    
    

    jsx语法,使用domPropsInnerText

    <script>
    export default {
      name: 'JsxExample',
      data(){
        return{
          text:"vText文字"
        }
      },
      render() {
        return   <div>
          <div domPropsInnerText={this.text}></div>
        </div>
      }
    }
    </script>
    
    

    3.2.2.3 v-if
    这恐怕是最简单的了,v-if就是if else 语法
    使用模板

    <template>
        <div>
            <div v-if="$route.query.id === 'xxx'">测试</div>
            <div v-else>else渲染</div>
        </div>
    </template>
    

    jsx
    使用 domPropsInnerText

    <script>
    export default {
        render(){
            if(this.$route.query.id === 'xxx'){
                return <div>测试</div>
            }
            return <div>else渲染</div>
        }
    }
    </script>
    
    或者
    
    <script>
    export default {
      render(){
        return <div>{this.$route.query.id === 'xxx' ? "测试" : "else渲染"}</div>
      }
    }
    </script>
    

    3.2.2.4 v-for
    使用模板

    <template>
      <div>
        <div v-for="(item,index) in list" :key="index">
           <span>{{item + index}}</span>
         </div>
      </div>
    </template>
    <script>
    export default {
      data (){
        return {
          list:["测试","测试","测试","测试","测试","测试","测试"]
        }
      },
    }
    </script>
    

    使用jsx

    v-for 的jsx习惯使用map方法和reduce方法。最终的结果就是得到一个由dom节点组成的数组。所以除了习惯性的map和reduce方法以外,理论上可以遍历的方法都可以使用。下面分别使用map,reduce和for 循环来实现。

    <script>
    export default {
      data (){
        return {
          list:["测试","测试","测试","测试","测试","测试","测试"],//数据列表
        }
      },
      methods:{
        /**
         * 使用map
         * @returns {JSX.Element}
         */
        renderDomUseMap(){
          return <div>
            {
              this.list.map((item,index)=><div><span>{item + index} use map</span></div>)
            }
          </div>
        },
        /**
         * 使用reduce
         * @returns {JSX.Element}
         */
        renderDomUseReduce(){
          return <div>
            {
              this.list.reduce((result,current,index)=>{
                result.push(<div><span>{current + index} use reduce</span></div>)
                return result
              },[])
            }
          </div>
        },
            /**
         * 使用for循环
         * @returns {[]}
         */
        renderDomUseFor(){
          let listDom = []
          for(let i=0;i<this.list.length;i++){
            listDom.push(<p>{this.list[i] + i} use for</p>)
          }
          return <div>
            {listDom}
          </div>
        }
      },
      render(createElement, context) {
        // return this.renderDomUseMap()
         // return this.renderDomUseFor()
        return this.renderDomUseReduce()
      }
    }
    </script>
    
    

    3.2.2.5 v-bind="$attrs"
    封装组件的时候,为了能全部集成我们组件内依赖的某个组件的属性,比如我们封装一个自定义功能的输入框,希望能全部基础a-input的属性,又不想去全部吧属性定义一遍。这时候会用到v-bind="$attrs"
    我们先用模板语法定义一个输入框组件,组件名字my-input.vue。这里的输入框基于ant-design-vue 的a-input组件

    <template>
      <div>
        <a-input :value="value" v-bind="$attrs" @change="inputChange"/>
      </div>
    </template>
    
    <script>
    export default {
    name:"MyInput",
      props:{
        value:String,//值
      },
      methods:{
        /**
         * 点击
         */
        inputChange(e){
          console.log(e)
          this.$emit("input",e.target.value)
          this.$emit("change",e.target.value)
        }
      }
    }
    </script>
    
    

    然后我们在父级页面引用,这里父级页面为home-view.vue

    <template>
      <div class="home">
        <my-input v-model="inputValue" style="width: 300px;margin:0 auto;"/>
      </div>
    </template>
    
    <script>
    import MyInput from '@/components/my-input.vue'
    export default {
      name: 'HomeView',
      components: {
        MyInput
      },
      data(){
        return{
          inputValue:"",//值
        }
      }
    }
    </script>
    
    

    代码运行结果界面


    image.png

    现在我们在父级组件引用标签处加上我们组件内并没有定义的属性,addon-before,虽然我们没有定义,但是a-inpu携带该属性,且my-input组件使用了v-bind="$attrs"

     <my-input v-model="inputValue" addon-before="Http://" style="width: 300px;margin:0 auto;"/>
    

    加上后运行效果如下


    image.png

    jsx语法
    下面用jsx实现v-bind="$attrs"

    <script>
    export default {
      name: 'MyInput',
      props:{
        value:String,//值
      },
      methods:{
        /**
         * 点击
         */
        inputChange(e){
          console.log(e)
          this.$emit("input",e.target.value)
          this.$emit("change",e.target.value)
        }
      },
      render() {
        return   <div>
          <a-input value={this.value} vOn:change={this.inputChange}  attrs={this.$attrs} />
        </div>
      }
    }
    </script>
    

    只需在a-input标签上加上 attrs={this.$attrs} 即可

    4 如何绑定事件
    4.1 普通事件绑定
    模板语法

    <template>
      <div>
        <div @click="bindEvent">绑定事件</div>
        <a-input @change="inputChange" v-model="value"></a-input>
      </div>
    </template>
    <script>
    export default {
      name:"Input",
      data(){
        return{
          value:"",//值
        }
      },
      methods:{
        /**
         * 绑定事件
         */
        bindEvent(e){
          console.log(e)
        },
        /**
         * 输入事件
         * @param e
         */
        inputChange(e){
          console.log(e)
        }
    
      }
    }
    </script>
    
    
    

    jsx

    <script>
    export default {
      name:"Input",
      data(){
        return{
          value:"",//值
        }
      },
      methods:{
        /**
         * 绑定事件
         */
        bindEvent(e){
          console.log(e)
        },
            /**
         * 绑定事件
         */
        bindEventByVon(e){
          console.log(e)
        },
        /**
         * 输入事件
         * @param e
         */
        inputChange(e){
          console.log(e)
        },
        input(e){
          this.value = e.target.value
        }
    
      },
      render(){
        return   <div>
          <div onClick={this.bindEvent}>绑定事件</div>
           <div vOn:click={this.bindEventByVon}>v-on指令形式绑定事件</div>
        <a-input onChange={this.inputChange} vModel={this.value} />
      </div>
      }
    }
    </script>
    

    结合事件说下v-model。由于v-model是由属性 value和事件input组成,因此 v-model除了如上述示例使用vModel以外,还可以分开写,如下

     <a-input onChange={this.inputChange} value={this.value} onInput={e=>this.value=e.target.value}/>
    

    4.2 绑定事件时传递参数
    在模板语法中,我们可以随意如下书写

    <div>
        <a-input @input="input()"/>
        <a-button @click="submit('form')">按钮</a-button>
    </div>
    

    使用jsx时,按照模板语法的思路和习惯,我们可能会如下书写

    <div>
        <a-input onInput={this.input()}/>
        <a-button onClick={this.submit('form')}>按钮</a-button>
    </div>
    

    这时候会发现,页面刚加载事件就被调用了。如果把模板语法看成是在页面写html的话,写jsx就是通过javascript创建页面元素,所以this.input()就是直接调用了该函数,所以不能写括号,需要写出this.input,也就是不需要调用,因为事件需要某些因素条件才能出发。那同理,我们也不能写成this.submit('form'),这样函数就会直接被调用了。但是事件确实需要传参的话,就需要套在一个匿名函数里面调用,如下

    <div>
        <a-input onInput={()=>this.input()}/>
        <a-button onClick={()=>this.submit('form')}>按钮</a-button>
    </div>
    

    4.3 事件修饰符
    在vue里面,有些很好用得事件修饰符,比如@click.stop @click.13等。jsx里面修饰符用_连接
    模板语法

    <template>
      <div>
        <input @click.stop.prevent="click" />
      </div>
    </template>
    
    <script>
    export default {
      methods:{
        /**
         * 点击
         */
        click(){
          console.log("点击")
        }
      }
    }
    </script>
    
    

    jsx

    <script>
    export default {
      name: 'JsxExample',
      methods:{
        click(){
          console.log("click")
        }
      },
      render() {
        return   <div>
          <input vOn:click_stop_prevent={this.click} />
        </div>
      }
    }
    </script>
    

    4.4 v-on="$listeners"
    和v-bind="$attrs"类似功能,v-on="$listeners"可以让子组件继承所有我们依赖的组件的事件

    模板语法

    <template>
      <div>
        <a-input :value="realValue" v-bind="$attrs" v-on="$listeners" @input="inputEvent" />
      </div>
    </template>
    
    <script>
    export default {
      props:{
        value:[InputEvent,String],//值
      },
      data(){
        return{
          realValue:"",//真实的值
        }
      },
      watch:{
        value:function (e){
          this.realValue = (typeof e === "string" || !e) ? e : e.target.value
        }
      },
      methods:{
        inputEvent(e){
          console.log(e)
          this.$emit("input",e)
        }
      }
    }
    </script>
    

    这里顺便讲下基于ant-design-vue和基于element-ui的输入框使用v-on="$listeners"时的一些小区别。
    ant-design-vue 的 a-input 的 input事件反出的是event事件,但是value属性接收的是字符串或数字。因此不能直接将prop的value赋值给 a-input,需要单独做处理后,见上面代码的 watch监听。使用v-on="$listeners"的情况下,直接将prop的value赋值给 a-input,会重新触发$listeners里面的input或者change事件,造成接收值不准确,报错。

    使用element-ui就不存在这个问题,因为element-ui的input事件直接返回value值,而不是event事件。使用element-ui可以如下:

    <template>
      <div>
        <el-input :value="value" v-bind="$attrs" v-on="$listeners" @input="inputEvent" />
      </div>
    </template>
    
    <script>
    export default {
      props:{
        value:[String],//值
      },
      methods:{
        inputEvent(value){
          console.log(value)
          this.$emit("input",value)
        }
      }
    }
    </script>
    
    

    那如何验证v-on="$listeners"生效呢?我们在父级组件绑定一个没有直接声明的事件即可。这里以ant-design-vue 的 a-input举例。ant-design-vue的a-input组件有个回车事件pressEnter。

    父级组件HomeView.vue代码

    <template>
      <div class="home">
        <my-input v-model="inputValue" addon-before="Http://" style="width: 300px;margin:0 auto;" @pressEnter="pressEnter"/>
      </div>
    </template>
    
    <script>
    import MyInput from '@/components/jsx-example.vue'
    
    export default {
      name: 'HomeView',
      components: {
        MyInput
      },
      data(){
        return{
          inputValue:"",//值
        }
      },
      methods:{
        /**
         * 按下回车键
         */
        pressEnter(e){
          console.log(e)
        },
      }
    }
    </script>
    
    

    运行后在输入框按回车健,我们可以看到pressEnter事件成功打印了值

    v-on="$listeners"的jsx语法。使用on监听

    <script>
    export default {
      name: 'MyInput',
      props:{
        value:[String,InputEvent],//值
      },
      data(){
        return{
          realValue:"",//真实的值
        }
      },
      watch:{
        value:function (e){
          this.realValue = (typeof e === "string" || !e) ? e : e.target.value
        }
      },
      methods:{
        /**
         * 点击
         */
        inputChange(e){
          console.log(e)
          this.$emit("input",e)
        }
      },
      render() {
        return   <div>
          <a-input value={this.realValue}
                   attrs={this.$attrs}
                   vOn:change={this.inputChange}
                   on={this.$listeners}
          />
        </div>
      }
    }
    </script>
    
    

    既然可以用on属性,那我们在jsx监听事件时,也可以直接在on里面书写。如下

      render() {
        return   <div>
          <a-input value={this.realValue}
                   attrs={this.$attrs}
                   on={{
                     change:this.inputChange,
                     ...this.$listeners
                   }}
          />
        </div>
      }
    

    5 插槽
    插槽包括父组件使用jsx和子组件使用jsx,默认插槽,具名插槽以及作用域插槽。

    5.1 默认插槽与具名插槽
    我们先从简单的例子开始,创建一个my-slot组件,使用模板语法,组件里面包括默认插槽和具名插槽
    my-slot.vue

    <template>
      <div>
        <div>
          <slot name="top"></slot>
        </div>
        <slot></slot>
        <div>
          <slot name="bottom"></slot>
        </div>
      </div>
    </template>
    

    然后我们在父级组件,HomeView.vue同样使用模板语法使用插槽,代码如下

    <template>
      <div class="home">
        <my-slot>
          默认插槽
          <template #top>
            顶部插槽内容
          </template>
          <template #bottom>
            底部部插槽内容
          </template>
        </my-slot>
      </div>
    </template>
    
    <script>
    import MySlot from '@/components/my-slot.vue'
    export default {
      name: 'HomeView',
      components: {
        MySlot
      },
    }
    </script>
    
    

    或者使用vue比较老的插槽使用语法slot属性,该属性在vue 2.6.0版本后被废弃

    <template>
      <div class="home">
        <my-slot>
          默认插槽
          <div slot="top">
            顶部插槽内容
          </div>
          <div slot="bottom">
            底部部插槽内容
          </div>
        </my-slot>
      </div>
    </template>
    
    <script>
    import MySlot from '@/components/my-slot.vue'
    export default {
      name: 'HomeView',
      components: {
        MySlot
      },
    }
    </script>
    

    新建一个AboutView.vue,作为新的父级组件,使用jsx语法

    按jsx-vue2示例的写法

    <script>
    import MySlot from '@/components/my-slot.vue'
    
    export default {
      name: 'AboutView',
      components: {
        MySlot
      },
      render() {
        return   <div class="home">
          <my-slot>
            <div slot="top">
              顶部插槽内容
            </div>
            默认插槽
            <div slot="bottom">
              底部部插槽内容
            </div>
          </my-slot>
      </div>
      }
    }
    </script>
    
    

    父级使用jsx语法使用插槽还是比较简单的,和模板语法没啥区别,甚至和模板语法被废弃的slot属性完全一样。

    接下来我们对my-slot.vue进行jsx改造。jsx里面,子组件使用this.$slots接收插槽,默认插槽的名字是default。代码如下

    <script>
    export default {
      name: 'MySlot',
      render() {
        const slots = this.$slots
        console.log(slots)
        return    <div>
          <div>
            {slots.top}
          </div>
          {slots.default}
          <div>
            {slots.bottom}
          </div>
        </div>
      }
    }
    </script>
    
    

    5.2 作用域插槽
    作用域插槽,就是父级组件可以使用子组件通过prop传递过来的变量的插槽。我们先将模板语法的my-slot定义的插槽改造成作用域插槽

    <template>
      <div>
        子组件原本内容
        <div>
          <slot name="testScopeSlot" :user="user"></slot>
        </div>
      </div>
    </template>
    <script>
    export default {
      data(){
        return{
          user:{
            name:"张三"
          },//用户信息
        }
      }
    }
    </script>
    

    相应的,对HomeView.vue做相应的改造,以便能够接收使用user

    <template>
      <div class="home">
        <my-slot>
          <template #testScopeSlot="{user}">
            作用域插槽内容:{{user.name}}
          </template>
        </my-slot>
      </div>
    </template>
    
    <script>
    import MySlot from '@/components/my-slot.vue'
    
    export default {
      name: 'HomeView',
      components: {
        MySlot
      },
    }
    </script>
    
    

    若父级组件使用vue 2.6.0后废弃的语法,如下

    <template>
      <div class="home">
        <my-slot>
          <div slot="testScopeSlot" slot-scope="{user}">
            作用域插槽内容:{{user.name}}
          </div>
        </my-slot>
      </div>
    </template>
    
    <script>
    import MySlot from '@/components/my-slot.vue'
    
    export default {
      name: 'HomeView',
      components: {
        MySlot
      },
    }
    </script>
    
    

    对AboutView.vue进行改造,以便能使用jsx语法接收和使用my-slot的user变量

    <script>
    import MySlot from '@/components/my-slot.vue'
    
    export default {
      name: 'AboutView',
      components: {
        MySlot
      },
      render() {
        return   <div class="home">
          <my-slot
              scopedSlots={{
                testScopeSlot: ({user}) => {
                  return `作用域插槽内容:${user.name}`
                }
              }}
          >
          </my-slot>
      </div>
      }
    }
    </script>
    
    

    这里相对之前的都比较难于理解,用slot slot-scope已经不管用了。父组件想要读到子组件通过插槽返出的变量,需要在子组件标签上挂载scopedSlots属性。scopedSlots是一个对象,里面包含了子组件定义的各个插槽,以名字为键名,键值是一个函数。默认插槽名字仍然是default。本示例定义的插槽名字是testScopeSlot,testScopeSlot的值是函数,函数的参数是对象,对象里包含user,即子组件返出的变量名。

    下面我们使用jsx改造my-slot的作用域插槽

    <script>
    export default {
      name: 'MySlot',
      data(){
        return{
          user:{
            name:"张三"
          },//用户信息
        }
      },
      render() {
        const scopedSlots = this.$scopedSlots
        console.log(scopedSlots)
        // const testScopeSlotDom = scopedSlots.testScopeSlot({user:this.user})
        // console.log(testScopeSlotDom)
        return  <div>
          子组件原本内容
          <div>
            {scopedSlots.testScopeSlot({user:this.user})}
        </div>
      </div>
      }
    }
    </script>
    
    
    image.png

    由于testScopeSlot是一个函数,因此我们只需要执行testScopeSlot函数即可,然后将use作为函数的参数传递就行。这里有点绕,可以这样反过来理解,父级组件定义了一个函数,函数接收一个对象参数,对象中包含user属性,将这个函数传递到子组件,子组件执行这个函数,并将子组件变量作为参数传递给函数,子组件执行函数后,函数将相应的结果return出去,被父组件接收,然后父组件处理,用于显示。

    下面我们将最初定义的默认插槽和具名插槽都改成作用域插槽试试。更改后的my-slot

    <script>
    export default {
      name: 'MySlot',
      data(){
        return{
          topInfo:"我是顶部插槽数据",//顶部插槽
          defaultInfo:"我是默认插槽数据",//顶部插槽
          bottomInfo:"我是顶部插槽数据",//顶部插槽
        }
      },
      render() {
        const scopedSlots = this.$scopedSlots
        const {topInfo,defaultInfo,bottomInfo} = this
        return   <div>
          <div>
            {scopedSlots.top({topInfo})}
          </div>
          {scopedSlots.default({defaultInfo})}
          <div>
            {scopedSlots.bottom({bottomInfo})}
          </div>
        </div>
      }
    }
    </script>
    
    

    相应的,我们更改AboutView.vue文件

    <script>
    import MySlot from '@/components/my-slot.vue'
    
    export default {
      name: 'AboutView',
      components: {
        MySlot
      },
      render() {
        return   <div class="home">
          <my-slot
              scopedSlots={{
                top: ({topInfo}) => {
                  return `作用域插槽内容:${topInfo}`
                },
                default: ({defaultInfo}) => {
                  return `作用域插槽内容:${defaultInfo}`
                },
                bottom: ({bottomInfo}) => {
                  return `作用域插槽内容:${bottomInfo}`
                }
              }}
          >
          </my-slot>
      </div>
      }
    }
    </script>
    

    运行结果


    image.png

    按照vue默认定义的作用域插槽数据,参数是一个对象形式。因此我们在子组件执行函数时,需要按对象形式传递,如 { topInfo } 。既然是我们自己传递参数,那我们是不是可以更改下参数传递形式,如下 my-slot.vue

    <script>
    export default {
      name: 'MySlot',
      data(){
        return{
          topInfo:"我是顶部插槽数据",//顶部插槽
          defaultInfo:"我是默认插槽数据",//顶部插槽
          bottomInfo:"我是顶部插槽数据",//顶部插槽
        }
      },
      render() {
        const scopedSlots = this.$scopedSlots
        const {topInfo,defaultInfo,bottomInfo} = this
        return   <div>
          <div>
            {scopedSlots.top(topInfo)}
          </div>
          {scopedSlots.default(defaultInfo)}
          <div>
            {scopedSlots.bottom(bottomInfo)}
          </div>
        </div>
      }
    }
    </script>
    
    

    然后相应的 AboutView.vue做更改

    <script>
    import MySlot from '@/components/my-slot.vue'
    
    export default {
      name: 'AboutView',
      components: {
        MySlot
      },
      render() {
        return   <div class="home">
          <my-slot
              scopedSlots={{
                top: (topInfo) => {
                  return `作用域插槽内容:${topInfo}`
                },
                default: (defaultInfo) => {
                  return `作用域插槽内容:${defaultInfo}`
                },
                bottom: (bottomInfo) => {
                  return `作用域插槽内容:${bottomInfo}`
                }
              }}
          >
          </my-slot>
      </div>
      }
    }
    </script>
    
    

    这里把传递和接收参数都改成字符串,运行结果相同。这里也提现了jsx在某种情况下的优势,相比模板语法,jsx能更灵活的控制代码逻辑。

    参考:vue2-jsx: https://github.com/vuejs/jsx-vue2

    相关文章

      网友评论

          本文标题:Vue2 + JSX使用总结

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