Vue2 + JSX使用总结

什么是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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容