JSX简介
JSX是一种Javascript的语法扩展,JSX
= Javascript
+ XML
,即在Javascript
里面写XML
,因为JSX
的这个特性,所以他即具备了Javascript
的灵活性,同时又兼具html
的语义化和直观性。
应用场景
为了让大家更方便的去理解JSX
的作用及用法,小编先为大家罗列了几个可能会用到JSX
的应用场景。
在消息框内添加html
在开发过程中,经常会用到消息框,使用消息框可能的一种写法是这样的。
Message.alert({ messge: '确定要删除?', type: 'warning' })
但是有时候产品或UI希望message
可以自定义一些样式,这时候你可能就需要让Message.alert
支持JSX
了(当然也可以使用插槽/html
等方式解决)
Message.alert({ // 此处使用了JSX messge: <div>确定要删除<span style="color:red">学习子君Vue系列文章</span>的笔记?</div>, type: 'warning' })
函数式组件
在小编前面的文章实战技巧,Vue原来还可以这样写中介绍了为什么要使用函数式组件,及函数式组件与普通组件的区别。
虽然在Vue.2.5
之后,函数式组件也可以使用模板语法,但使用JSX
可能会更方便一些(个人理解)
export default { // 通过配置functional属性指定组件为函数式组件 functional: true, /** * 渲染函数 * @param {*} h * @param {*} context 函数式组件没有this, props, slots等都在context上面挂着 */ render(h, context) { const { props } = context if (props.avatar) { return <img src={props.avatar}></img> } return <img src="default-avatar.png"></img> } }
一个表单的需求
为了方便快速开发管理系统,小编对所使用的UI库中的表单进行了二次封装,封装之后的效果如下(仅供参考):
<template> <custom-form v-model="formData" :fields="fields" /> </template> <script> export default { data() { return { formData: {}, fields: Object.freeze([ { label: '字段1', props: 'field1', type: 'input' }, { label: '字段2', props: 'field2', type: 'number' } ]) } } } </script>
这样封装之后,定义表单时,只需要定义简单的JSON
即可快速完成表单开发。
但有时候会有一些特殊的需求,比如希望可以给输入框后面加一个按钮或者图标之类的,这时候就需要考虑使用JSX
去处理了
{ label: '字段2', props: 'field2', type: 'number', // 会渲染到表单元素后面 renderSuffix() { return <button onClick={this.$_handleSelect}>选择</button> } }
其他一些场景
比如我们一条数据需要根据状态不同,定义不同的展现方式,这时候你可能会想到用策略模式,这时候如果将每一个策略都写成一个JSX
,那么就不需要针对每一个策略定义一个单文件组件了。
当然如果你说,我就喜欢用JSX
,那么所有的场景你都可以用。
学习JSX
,先了解一下createElement
你是否看过写的Vue
代码经过编译之后的样子,比如下面这段代码
<template> <div>我是子君,我的公众号是<span class="emphasize">前端有的玩</span></div> </template>
小编对这段代码进行编译之后,得到下面这段代码
function () { var e = this, // e._self._c 对应源码里面的createElement t = e._self._c; // 返回了一个 createElement('div',[]) return t("div", [ // e._v 对应源码里面的createTextVNode e._v("我是子君,我的公众号是"), t("span", { staticClass: "emphasize" }, [e._v("前端有的玩")]), ]); }
通过对上面的代码进行分析,不难发现,Vue
模板中的每一个元素编译之后都会对应一个createElement
,那么这个createElement
到底是什么,嗯,这个你面试的时候也许已经提到过了。
那么什么是createElement
无论是Vue
还是React
,都存在createElement
,而且作用基本一致。
可能你对createElement
不是很了解,函数名翻译过来就是增加一个元素,但他的返回值你一定知道。
createElement
函数返回的值称之为虚拟节点,即VNode
,而由VNode
扎堆组成的树便是大名鼎鼎,面试必问的虚拟DOM
。
createElement
函数的参数,在这里小编偷个懒抄一下Vue
官方文档
// @returns {VNode} createElement( // {String | Object | Function} // 一个 HTML 标签名、组件选项对象,或者 // resolve 了上述任何一种的一个 async 函数。必填项。 'div', // {Object} // 一个与模板中 attribute 对应的数据对象。可选。 { // (详情见下一节) }, // {String | Array} // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成, // 也可以使用字符串来生成“文本虚拟节点”。可选。 [ '先写一些文字', createElement('h1', '一则头条'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] )
从上面可以看出createElement
一共有三个参数,三个参数分别是
-
第一个参数是需要渲染的组件,可以是组件的标签,比如
div
;或者是一个组件对象,也就是你天天写的export default {}
;亦或者可以是一个异步函数。 -
第二个参数是这个组件的属性,是一个对象,如果组件没有参数,可以传null(关于组件的属性,下文将依次介绍)
-
第三个参数是这个组件的子组件,可以是一个字符串(textContent)或者一个由VNodes组成的数组
用createElement
写一个组件吧
表单示例
假设我们需要开发一个下面这样的表格(element-ui的)
用模板代码去开发
如果我们用模板代码去开发这个表单,那么代码大概就长这样
<el-form :inline="true" :model="formInline" class="demo-form-inline"> <el-form-item label="审批人"> <el-input v-model="formInline.user" placeholder="审批人"></el-input> </el-form-item> <el-form-item label="活动区域"> <el-select v-model="formInline.region" placeholder="活动区域"> <el-option label="区域一" value="shanghai"></el-option> <el-option label="区域二" value="beijing"></el-option> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="onSubmit">查询</el-button> </el-form-item> </el-form>
用createElement
去实现
如果我们直接将上面的代码转换为用createElement
去实现,那么代码将会是这样的
export default { methods: { $_handleChangeUser(value) { this.formInline.user = value } }, render(createElement) { return createElement( 'ElForm', { props: { inline: true, model: this.formInline }, staticClass: 'demo-form-inline' }, [ createElement( 'ElFormItem', { props: { label: '审批人' } }, [ createElement('ElInput', { props: { value: this.formInline.user }, attrs: { placeholder: '审批人' }, on: { input: this.$_handleChangeUser } }) ] ), createElement( 'ElFormItem', { props: { label: '活动区域' } }, [ createElement( 'ElSelect', { props: { value: this.formInline.region, placeholder: '活动区域' } }, [ createElement('ElOption', { props: { label: '区域一', value: 'shanghai' } }), createElement('ElOption', { props: { label: '区域二', value: 'beijing' } }) ] ) ] ), createElement('ElFormItem', null, [ createElement( 'ElButton', { props: { type: 'primary' }, on: { click: this.$_handleSubmit } }, '查询' ) ]) ] ) } }
看到上面的代码,你可能会惊呼,代码好多啊,好痛苦,想当年发明JSX
的人刚开始天天也是写createElement
,写的直掉头发,太痛苦了。
然后就使劲挠头,当额头锃光发亮的时候,终于想到了一种新的语法,就是JSX。从此之后,头发呼呼的又长回来了(本段纯属虚构)。
看到上面代码,你会发现有一个render
函数,这个函数叫做渲染函数,相当于通过createElement
或JSX
去实现功能的主入口方法。
而且你熟悉的v-model
也没见了,而是用value
+ input
代替了。
是时候使用JSX
代替createElement
了
看到上面用createElement
去实现组件,太麻烦了,别说工作效率提高了,就是那些嵌套可以嵌套正确就很赞了,所以我们需要用JSX
去简化整个逻辑。
methods: { $_handleInputUser(value) { this.formInline.user = value }, $_handleChangeRegion(value) { this.formInline.region = value }, $_handleSubmit() {} }, /** *将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 *版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 *const h = this.$createElement,这样你就可以去掉 (h) 参数了。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。 */ render(h) { return ( <ElForm inline model={this.formInline} class="demo-form-inline"> <ElFormItem label="审批人"> <ElInput value={this.formInline.user} onInput={this.$_handleInputUser} placeholder="审批人" ></ElInput> </ElFormItem> <ElFormItem label="活动区域"> <ElSelect value={this.formInline.region} onChange={this.$_handleChangeRegion} placeholder="活动区域" > <ElOption label="区域一" value="shanghai"></ElOption> <ElOption label="区域二" value="beijing"></ElOption> </ElSelect> </ElFormItem> <ElFormItem> <ElButton type="primarty" onClick={this.$_handleSubmit}> 查询 </ElButton> </ElFormItem> </ElForm> ) }
看了上面的代码,大家其实会发现用JSX
与template
的语法都属于xml
的写法,而且也比较像,但实质上还是有许多区别的,下面小编将为大家一一分析
没有v-model
怎么办,还有其他指令可以用吗?
当你选择使用JSX
的时候,你就要做好和指令说拜拜的时候了,在JSX
中, 你唯一可以使用的指令是v-show
,除此之外,其他指令都是不可以使用的,有没有感到很慌,这就对了。不过呢,换一个角度思考,指令只是Vue
在模板代码里面提供的语法糖,现在你已经可以写Js
了,那些语法糖用Js
都可以代替了。
v-model
v-model
是Vue
提供的一个语法糖,它本质上是由 value
属性(默认) + input
事件(默认)组成的,如果对自定义v-model
不了解的同学建议阅读小编的文章进行了解 绝对干货~!学会这些Vue小技巧,可以早点下班和女神约会了。 所以,在JSX
中,我们便可以回归本质,通过传递value
属性并监听input
事件来实现数据的双向绑定
export default { data() { return { name: '' } }, methods: { // 监听 onInput 事件进行赋值操作 $_handleInput(e) { this.name = e.target.value } }, render() { // 传递 value 属性 并监听 onInput事件 return <input value={this.name} onInput={this.$_handleInput}></input> } }
经小编测试,在新版脚手架vue-cli4中,已经默认集成了对v-model的支持,大家可以直接使用<input v-model={this.value}>,
如果你的项目比较老,也可以安装插件babel-plugin-jsx-v-model来进行支持
同样的,在JSX
中,对于.sync
也需要用属性+事件来实现,如下代码所示:
export default { methods: { $_handleChangeVisible(value) { this.visible = value } }, render() { return ( <ElDialog title="测试.sync" visible={this.visible} on={{ 'update:visible': this.$_handleChangeVisible }} ></ElDialog> ) } }
v-if 与 v-for
在模板代码里面我们通过v-for
去遍历元素,通过v-if
去判断是否渲染元素,在jsx
中,对于v-for
,你可以使用for
循环,array.map
来代替,
对于v-if
,可以使用if
语句,三元表达式
等来代替循环遍历列表
const list = ['java', 'c++', 'javascript', 'c#', 'php'] return ( <ul> {list.map(item => { return <li>{item}</li> })} </ul> )
使用条件判断
const isGirl = false return isGirl ? <span>小妹,哥哥教你写Vue</span> : <span>鸟你干啥</span>
v-bind
在模板代码中,我们一般通过 v-bind:prop="value"
或:prop="value"
来给组件绑定属性,在JSX
里面写法也类似
render() { return <input value={this.name}></input> }
v-html 与 v-text
在说v-html
与v-text
之前,我们需要先了解一下Vue
中的属性,Vue
中的属性一共分为三种,第一种是大家写bug时候最常用的props
,即组件自定义的属性;第二种是attrs
,是指在父作用域里面传入的,但并未在子组件内定义的属性。第三种比较特殊,是domProps
,经小编不完全测试,在Vue
中,domProps
主要包含三个,分别是innerHTML
,textContent/innerText
和value
。
-
v-html
: 在模板代码中,我们用v-html
指令来更新元素的innerHTML
内容,而在JSX
里面,如果要操纵组件的innerHTML
,就需要用到domProps
export default { data() { return { content: '<div>这是子君写的一篇新的文章</div>' } }, render() { // v-html 指令在JSX的写法是 domPropsInnerHTML return <div domPropsInnerHTML={this.content}></div> } }
v-text
: 看了上面的v-html
,你是不是立即就想到了v-text
在JSX
的写法domPropsInnerText
,是的,你没有想错
export default { data() { return { content: '这是子君写的一篇新的文章的内容' } }, render() { return <div domPropsInnerText={this.content}></div> } }
但实际上我们不需要使用domPropsInnerText
,而是将文本作为元素的子节点去使用即可
<div>{this.content}</div>
实际上,对于domProps
,只有innerHTML
才需要使用domPropsInnerHTML
的写法,其他使用正常写法即可
我还要监听事件呢
监听事件与原生事件
当我们开发一个组件之后,一般会通过this.$emit('change')
的方式对外暴露事件,然后通过v-on:change
的方式去监听事件,
很遗憾,在JSX
中你无法使用v-on
指令,但你将解锁一个新的姿势
render() { return <CustomSelect onChange={this.$_handleChange}></CustomSelect> }
JSX
中,通过on
+ 事件名称的大驼峰写法来监听,比如事件icon-click
,在JSX
中写为onIconClick
有时候我们希望可以监听一个组件根元素上面的原生事件,这时候会用到.native
修饰符,有点绝望啊,修饰符也是不能用了,但好在也有替代方案,如下代码
render() { // 监听下拉框根元素的click事件 return <CustomSelect nativeOnClick={this.$_handleClick}></CustomSelect> }
监听原生事件的规则与普通事件是一样的,只需要将前面的on
替换为nativeOn
除了上面的监听事件的方式之外,我们还可以使用对象的方式去监听事件
render() { return ( <ElInput value={this.content} on={{ focus: this.$_handleFocus, input: this.$_handleInput }} nativeOn={{ click: this.$_handleClick }} ></ElInput> ) }
事件修饰符
和指令一样,除了个别的之外,大部分的事件修饰符都无法在JSX
中使用,这时候你肯定已经习惯了,肯定有替代方案的。
-
.stop
: 阻止事件冒泡,在JSX
中使用event.stopPropagation()
来代替 -
.prevent
:阻止默认行为,在JSX
中使用event.preventDefault()
来代替 -
.self
:只当事件是从侦听器绑定的元素本身触发时才触发回调,使用下面的条件判断进行代替
if (event.target !== event.currentTarget){ return }
.enter
与keyCode
: 在特定键触发时才触发回调
if(event.keyCode === 13) { // 执行逻辑 }
除了上面这些修饰符之外,尤大大为了照顾我们这群CV仔,还是做了一点优化的,对于.once
,.capture
,.passive
,.capture.once
,尤大大提供了前缀语法帮助我们简化代码
render() { return ( <div on={{ // 相当于 :click.capture '!click': this.$_handleClick, // 相当于 :input.once '~input': this.$_handleInput, // 相当于 :mousedown.passive '&mousedown': this.$_handleMouseDown, // 相当于 :mouseup.capture.once '~!mouseup': this.$_handleMouseUp }} ></div> ) }
对了,还有插槽
插槽就是子组件中提供给父组件使用的一个占位符,插槽分为默认插槽,具名插槽和作用域插槽,下面小编依次为你带来每种在JSX
中的用法与如何去定义插槽。
默认插槽
- 使用默认插槽
使用element-ui
的Dialog
时,弹框内容就使用了默认插槽,在JSX
中使用默认插槽的用法与普通插槽的用法基本是一致的,如下代码所示:
render() { return ( <ElDialog title="弹框标题" visible={this.visible}> {/*这里就是默认插槽*/} <div>这里是弹框内容</div> </ElDialog> ) }
- 自定义默认插槽
在Vue
的实例this
上面有一个属性$slots
,这个上面就挂载了一个这个组件内部的所有插槽,使用this.$slots.default
就可以将默认插槽加入到组件内部
export default { props: { visible: { type: Boolean, default: false } }, render() { return ( <div class="custom-dialog" vShow={this.visible}> {/**通过this.$slots.default定义默认插槽*/} {this.$slots.default} </div> ) } }
具名插槽
-
使用具名插槽
有时候我们一个组件需要多个插槽,这时候就需要为每一个插槽起一个名字,比如
element-ui
的弹框可以定义底部按钮区的内容,就是用了名字为footer
的插槽
render() { return ( <ElDialog title="弹框标题" visible={this.visible}> <div>这里是弹框内容</div> {/** 具名插槽 */} <template slot="footer"> <ElButton>确定</ElButton> <ElButton>取消</ElButton> </template> </ElDialog> ) }
- 自定义具名插槽
在上节自定义默认插槽时提到了$slots
,对于默认插槽使用this.$slots.default
,而对于具名插槽,可以使用this.$slots.footer
进行自定义
render() { return ( <div class="custom-dialog" vShow={this.visible}> {this.$slots.default} {/**自定义具名插槽*/} <div class="custom-dialog__foolter">{this.$slots.footer}</div> </div> ) }
作用域插槽
-
使用作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的,这时候就需要用到作用域插槽,在
JSX
中,因为没有v-slot
指令,所以作用域插槽的使用方式就与模板代码里面的方式有所不同了。比如在element-ui
中,我们使用el-table
的时候可以自定义表格单元格的内容,这时候就需要用到作用域插槽
data() { return { data: [ { name: '子君' } ] } }, render() { return ( {/**scopedSlots即作用域插槽,default为默认插槽,如果是具名插槽,将default该为对应插槽名称即可*/} <ElTable data={this.data}> <ElTableColumn label="姓名" scopedSlots={{ default: ({ row }) => { return <div style="color:red;">{row.name}</div> } }} ></ElTableColumn> </ElTable> ) }
自定义作用域插槽
使用作用域插槽不同,定义作用域插槽也与模板代码里面有所不同。加入我们自定义了一个列表项组件,用户希望可以自定义列表项标题,这时候就需要将列表的数据通过作用域插槽传出来。
render() { const { data } = this // 获取标题作用域插槽 const titleSlot = this.$scopedSlots.title return ( <div class="item"> {/** 如果有标题插槽,则使用标题插槽,否则使用默认标题 */} {titleSlot ? titleSlot(data) : <span>{data.title}</span>} </div> ) }
只能在render
函数里面使用JSX
吗
当然不是,你可以定义method
,然后在method
里面返回JSX
,然后在render
函数里面调用这个方法,不仅如此,JSX
还可以直接赋值给变量,比如下面这段代码
methods: { $_renderFooter() { return ( <div> <ElButton>确定</ElButton> <ElButton>取消</ElButton> </div> ) } }, render() { const buttons = this.$_renderFooter() return ( <ElDialog visible={this.visible}> <div>这里是一大坨内容</div> <template slot="footer">{buttons}</template> </ElDialog> ) }
指令
基础用法
虽然大部分内置的指令无法直接在JSX
里面使用,但是自定义的指令可以在JSX
里面使用,就拿element-ui
的v-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> ) }