你是否看过写的 Vue 代码经过编译之后的样子,比如下面这段代码
<template> <div>Hellow world<span class="flex">Hellow world</span></div></template>
复制代码
vue2.xx 版本在线编译:传送门
vue3.xx 版本在线编译:传送门
通过对上面的代码进行分析,不难发现,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 中,通常大家习惯了使用template的语法。
尽管template 和 JSX 都属于 xml 的写法,而且他们也比较像,但是本质还是有许多不一样的地方:
老规矩,上传送门
当你选择使用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?
快去学习!!!
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> }}
复制代码
和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> ) }}
复制代码
在 template 中,我们一般通过v-bind:prop="value"或:prop="value"来给组件绑定属性,在 JSX 里面写法也类似:
render() { return <v-input defaultCode={this.defaultCode}></v-input>}
复制代码
不要着急,这些指令只是黑魔法,用 js 很容易实现。
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> ) }
复制代码
复制代码
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之前,我们需要先了解一下 Vue 中的属性,Vue 中的属性一共分为三种:
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>); },
复制代码
当我们开发一个组件之后,一般会通过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
监听原生事件的规则与普通事件是一样的,只需要将前面的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() // ... }}
复制代码
// 阻止 事件冒泡 event.stopPropagation()}
复制代码
// 阻止该元素默认的 keyup 事件 event.preventDefault()}
复制代码
// 如果触发事件的元素不是事件绑定的元素 // 则返回 if (event.target !== event.currentTarget) return}
复制代码
// 如果按下去的不是 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> ) }
复制代码
插槽就是子组件中提供给父组件使用的一个占位符,插槽分为默认插槽,具名插槽和作用域插槽
在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.default 即代表默认插槽
render() { return ( <div> { // 通过this.$slots.default定义默认插槽 this.$slots.default } </div> ) }
复制代码
<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> ) }```
复制代码
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-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> ) }
复制代码
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 一样使用Render和JSX,可能并不是多么一件美好的事情,正如官方文档告诉我们的,“这就是深入底层的代价”。
评论