从Vue编译后的代码看createElement
你是否看过写的Vue代码经过编译之后的样子,比如下面这段代码
代码语言:javascript复制<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 函数。 - 必填项。
- 一个 HTML 标签名、组件选项对象(比如
- Object
- 一个与模板中
attribute
对应的数据对象。 - 可选。
- 一个与模板中
- String | Array
- 子级虚拟节点 (VNodes),由
createElement()
构建而成,也可以使用字符串来生成“文本虚拟节点” - 可选。
- 子级虚拟节点 (VNodes),由
所以本质上面来说,在
Vue
里面,你也可以像写React
一样,通过Render
来使用JSX
在Vue中使用 Render
和 JSX
在Vue中,通常大家习惯了使用template
的语法。
尽管template
和 JSX
都属于xml的写法,而且他们也比较像,但是本质还是有许多不一样的地方:
老规矩,上传送门
v-model
当你选择使用JSX
的时候,你就要做好和指令说拜拜的时候了。
在JSX中, 你唯一可以使用的指令是v-show
,除此之外,其他指令都是不可以使用的,有没有感到很慌,这就对了。不过呢,换一个角度思考,指令只是Vue在模板代码里面提供的语法糖,现在你已经可以写Js了,那些语法糖用Js都可以代替了。
代码语言:javascript复制在新版脚手架
vue-cli4
中,已经默认集成了对v-model
的支持,大家可以直接使用,如果你的项目比较老,也可以安装插件babel-plugin-jsx-v-model来进行支持
export default {
name:'vInput',
props: {
value:[String,Number]
},
data() {
return {
name: ''
}
},
methods: {
// 监听 onInput 事件进行赋值操作
handleInput(e) {
this.name = e.target.value
// 这里对组件实现了v-model语法糖
this.$emit('input', e.target.value);
}
},
render() {
// 传递 value 属性 并监听 onInput事件
return <input value={this.name} onInput={this.handleInput}></input>
// 如果安装了插件或者使用vue-cli4 ,可以和template一样舒服的使用v-model
return <input v-model={this.name} />
}
}
// JSX : <Vinput v-model={this.value} />
// or template : <v-input v-model="value" />
注意上面的代码最后注释的代码,因为在JSX
中,我们已经通过babel
可以得到v-model
语法糖的支持,那么我们在使用JSX
写自己的组件的时候,一定要注意实现组件的v-model
语法糖,去支持该特性。
什么? 你还不懂什么是v-model
?
快去学习!!!
自定义model
代码语言:javascript复制export default {
name:'vInput',
props: {
defaultCode:[String,Number]
},
data() {
return {
name: ''
}
},
model: {
prop: 'defaultCode',
event: 'update'
},
methods: {
// 监听 onInput 事件进行赋值操作
handleInput(e) {
this.name = e.target.value
// 这里对组件实现了v-model语法糖
this.$emit('update', e.target.value);
}
},
render() {
// 传递 value 属性 并监听 onInput事件
return <input value={this.name} onInput={this.handleInput}></input>
}
}
.sync
和v-model
一样,.sync
也需要用属性 事件的方式来实现
.sync
目前在JSX
中没有任何babel
支持:(
export default {
data(){
return {
defaultCode:'',
},
},
methods: {
handleChangeDefaultCode(value) {
this.visible = value
}
},
render() {
return (
<v-input
defaultCode={this.defaultCode}
on={{ 'update:defaultCode': this.handleChangeDefaultCode }}
></v-input>
)
}
}
v-bind
在template中,我们一般通过v-bind:prop="value"
或:prop="value"
来给组件绑定属性,在JSX里面写法也类似:
render() {
return <v-input defaultCode={this.defaultCode}></v-input>
}
v-if 与 v-for
不要着急,这些指令只是黑魔法,用js很容易实现。
- v-if
render(){
const arg1 = 1;
const arg2 = 4;
return (
<div>
{this.show? arg1 : arg2}
</div>
)
}
写三元表达式只能写简单的,那么复杂的还得用if/else
代码语言:javascript复制 render(){
const { show } = this;
let ifText;
if(show){
ifText = (<p>1</p>)
}else{
ifText = (<p>4</p>);
}
// let ifText = show ? (<p>1</p>) : (<p>4</p>);
const showButton = true;
return (
<div>
{ifText}
{
showButton && <button/>
}
</div>
)
}
复制代码
- v-for
render(){
const t = 'hello world';
const arg1 = 1;
const arg2 = 2;
const hasButton = true;
const list = [1,2,3,4,5,6,7,8,9];
let jsx = (
<div>
<h1>
{
t === 'hello world' ? arg1 : arg2
}
</h1>
{
//如果hasButton为true,则渲染button组件
hasButton && <button/>
}
<ul>
{
// 替代 v-for
list.map((item) => <li>{item}</li>)
}
</ul>
</div>
)
return jsx;
}
自定义组件
很简单,只需要导入进来,不用再在components属性声明了,直接写在jsx中比如
代码语言:javascript复制<script>
import Vinput from './vInput'
export default {
name: "item",
render(){
return (
<Vinput/>
)
}
}
</script>
v-html 与 v-text
在说v-html
与v-text
之前,我们需要先了解一下Vue中的属性,Vue中的属性一共分为三种:
- props,即组件自定义的属性;
- attrs,是指在父作用域里面传入的,但并未在子组件内定义的属性。
- domProps,主要包含三个,分别是
innerHTML
,textContent/innerText
和value
。
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,而是将文本作为元素的子节点去使用即可
代码语言:javascript复制renderContent(h,{ node, data, store }){
……
return (
<span class="custom-tree-node">
……
<span class="node-label" domPropsInnerText={label}/>
// 但实际上我们不需要使用domPropsInnerText,而是将文本作为元素的子节点去使用即可
<span class="node-label">
{ label }
</span>
……
</span>);
},
事件 v-on
当我们开发一个组件之后,一般会通过this.$emit('change')
的方式对外暴露事件,然后通过v-on:change
的方式去监听事件,很遗憾,在JSX中你无法使用v-on
指令,但你将解锁一个新的姿势
return (
<wg-el-select
{...{ props }}
{...{ on }}
v-loading={loading}
value={this.value}
onChange={this.mychange}
>
{dataSourceM.map((item) => {
return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;
})}
</wg-el-select>
)
JSX中,通过on 事件名称的大驼峰
写法来监听,比如事件change,在JSX中写为onChange
事件监听 .native
监听原生事件的规则与普通事件是一样的,只需要将前面的on
替换为nativeOn
return (
<wg-el-select
{...{ props }}
{...{ on }}
v-loading={loading}
value={this.value}
nativeOnChange={this.mychange}
>
{dataSourceM.map((item) => {
return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;
})}
</wg-el-select>
)
除了上面的监听事件的方式之外,我们还可以使用对象的方式去监听事件
代码语言:javascript复制注意是双花括号,第一个花括号
{}
表示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>
)
事件修饰符
这里是一个使用所有修饰符的例子:
代码语言:javascript复制on: {
keyup: function (event) {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果按下去的不是 enter 键或者
// 没有同时按下 shift 键
// 则返回
if (!event.shiftKey || event.keyCode !== 13) return
// 阻止 事件冒泡
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
}
- .stop : 阻止事件冒泡,在JSX中使用event.stopPropagation()来代替
// 阻止 事件冒泡
event.stopPropagation()
}
- .prevent:阻止默认行为,在JSX中使用event.preventDefault() 来代替
// 阻止该元素默认的 keyup 事件
event.preventDefault()
}
- .self:只当事件是从侦听器绑定的元素本身触发时才触发回调,使用下面的条件判断进行代替
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
}
- .enter与keyCode: 在特定键触发时才触发回调
// 如果按下去的不是 enter 键或者
// 没有同时按下 shift 键
// 则返回
if (!event.shiftKey || event.keyCode !== 13) return
除此之外,官方还对此做了一定的优化,提供了前缀语法来帮助我们简化代码:
代码语言:javascript复制 render() {
return (
<div
on={{
// 相当于 :click.capture
'!click': this.click,
// 相当于 :input.once
'~input': this.input,
// 相当于 :mousedown.passive
'&mousedown': this.mousedown,
// 相当于 :mouseup.capture.once
'~!mouseup': this.mouseup
}}
></div>
)
}
插槽 slots
插槽就是子组件中提供给父组件使用的一个占位符,插槽分为默认插槽
,具名插槽
和作用域插槽
默认插槽
- 使用默认插槽
在JSX
中使用默认插槽的用法与普通插槽的用法基本是一致的,如下代码所示:
return (
<wg-el-select
on={{
click:this.myclick
}}
nativeOn={{
change:this.mychange,
}}
>
// 这里就是默认插槽
{dataSourceM.map((item) => {
return <wg-el-option key={item.itemValue} label={item.itemName} value={item.itemValue} />;
})}
</wg-el-select>
)
- 自定义默认插槽 你可以通过
this.$slots
访问静态插槽的内容,
这个上面就挂载了一个这个组件内部的所有插槽
this.$slots.default
即代表默认插槽
render() {
return (
<div>
{
// 通过this.$slots.default定义默认插槽
this.$slots.default
}
</div>
)
}
具名插槽
- 使用具名插槽
有时候我们一个组件需要多个插槽,这时候就需要为每一个插槽起一个名字,比如
element-ui
的弹框可以定义底部按钮区的内容,就是用了名字为footer
的插槽:
<template>
<el-dialog
:title="title"
:destroy-on-close="destroy"
:visible.sync="dialogVisible"
:width="width"
:before-close="handleClose"
:append-to-body="true"
>
<components :is="model" :_dataForm="formData" :formOtp="formOtp" ref="model">
</components>
<!-- 具名插槽 -->
<span slot="footer" class="dialog-footer">
<el-button type="primary" v-if="hasContinue" @click="handleContinue">保存并继续</el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</span>
</el-dialog>
</template>
修改为JSX:
代码语言:javascript复制 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}/>
},
- 自定义作用域插槽
假如我们自定义了一个列表项组件,用户希望可以自定义列表项标题,这时候就需要将列表的数据通过作用域插槽传出来。
代码语言:javascript复制```
render() {
const { data } = this
// 获取标题作用域插槽
const titleSlot = this.$scopedSlots.title
return (
<div class="item">
{/** 如果有标题插槽,则使用标题插槽,否则使用默认标题 */}
{titleSlot ? titleSlot(data) : <span>{data.title}</span>}
</div>
)
}
```
JSX除了在render中可以被使用外,还可以在method
定义的任何一个方法中使用
代码语言:javascript复制 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>
)
}
JSX中的函数式组件
Vue 官方传送门
函数式组件意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)。
因为函数式组件只是一个函数,所以渲染开销也低很多。然而,对持久化实例的缺乏也意味着函数式组件不会出现在 Vue devtools 的组件树里。
因为函数式组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。
实际上,它只是一个接受一些 prop 的函数。
所以在少了很多响应式处理和操作的基础上,函数式组件可以会提高速度和减少内存占用。
又因为只是函数,所以渲染开销也低很多
在template中,函数式组件可以这样(注意是Vue 2.5.0 及以上版本
):
<template functional>
</template
而在JSX中,
我们只需增加配置functional: true
就可以了
export default {
functional:true,
render(h, context){
return (
<div />
)
}
}
函数式组件render
相比普通组件render
的变化:
- 对于函数式组件 vue 增加了context对象,需要作为
render(h,context)
第二个参数传入 this.$slots.default
更新为context.children
props
原本是直接挂在this上的,现在变为context.props
挂在了context.props上。this.data
变为了context.data
需要注意的是对于函数式组件,没有被定义为prop的特性不会
自动添加到组件的根元素上,意思就是需要我们手动添加到组件根元素了,看个例子吧
//父组件
...
render(){
return (
<Item data={this.data} class="large"/>
)
}
//Item.vue组件
export default {
functional:true,
name: "item",
render(h,context){
return (
<div class="red" >
{context.props.data}
</div>
)
}
}
上面代码期待的是.large类名传入到了Item的根元素上,但是其实没有。我们需要增加点东西
代码语言:javascript复制// Item.vue
export 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
,可能并不是多么一件美好的事情,正如官方文档告诉我们的,“这就是深入底层的代价
”。