文章内容主要分两块。
第一块是了解jsx运行环境,因为jsx只是语法糖,最终都是需要babel来转译语法,所以需要配置相关babel插件。vue-cli3脚手架工具生成的应用工程默认支持jsx/tsx,省去了自己配置的繁琐,但了解相关babel插件对理解和书写jsx非常有帮助。
第二块是实践jsx在vue中的语法以及相关案例。了解jsx是如何生成最终的VNode。tsx应用Demo代码放在github vue-tsx-demo (opens new window)
vue-cli3自动生成的app项目中,babel.config.js预设了 presets: ["@vue/app"],该插件为babel-preset-app (opens new window)
里面包含插件,主要是babel解析,以支持许多扩展语法。比如jsx、es6语法
等:
"@babel/core": "^7.9.0",
"@babel/helper-compilation-targets": "^7.8.7",
"@babel/helper-module-imports": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@vue/babel-preset-jsx": "^1.1.2",
"babel-plugin-dynamic-import-node": "^2.3.0",
里面重点插件有:
直接支持ES6模块语法,浏览器能自己解析import语法
,可显著减少包体积)。注意:指定esmodules目标时,浏览器目标将被忽略启用将ES6模块语法转换为其他模块类型的功能
。"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为"auto"。设置为false不会转换模块
react和vue底层vnode diff对比不是使用相同的数据结构,所以导致两者jsx书写方式有些许不同。目前两者大部分jsx语法一致,是因为有各种babel插件辅助做了这部分事。但对于动态属性这样自由化较高的地方,需要我们知道两者本质区别(即不同的VNode数据结构)。
vue template模板本质上最终生成render函数,而render函数本质上是生成VNode,所以有必要了解这个VNode数据结构。Vue2.x VNode diff核心算法借鉴的是snabbdom (opens new window)库,所以数据结构也有snabbdom数据结构的影子,比如事件需要放置在on属性上,方便最终patch时挂载到真实dom元素上(react则自定义模拟事件系统,所有事件都冒泡到顶层document处理)。更多VNode信息可查看Vue官方文档 - 深入数据对象 (opens new window)。
以下查看jsx转译为h函数:
render (h) {
return (
<div
// Component props
propsMsg="hi"
// Normal attributes or component props.
id="foo"
// DOM properties are prefixed with `domProps`
domPropsInnerHTML="bar"
// event listeners are prefixed with `on` or `nativeOn`
onClick={this.clickHandler}
nativeOnClick={this.nativeClickHandler}
// other special top-level properties
class={{ foo: true, bar: false }}
style={{ color: 'red', fontSize: '14px' }}
key="key"
ref="ref"
// assign the `ref` is used on elements/components with v-for
refInFor
slot="slot">
</div>
)
}
以上jsx语法糖等同于如何h函数生成VNode:
render (h) {
return h('div', {
// Component props
props: {
msg: 'hi'
},
// Normal HTML attributes
attrs: {
id: 'foo'
},
// DOM props
domProps: {
innerHTML: 'bar'
},
// Event handlers are nested under "on", though
// modifiers such as in v-on:keyup.enter are not
// supported. You'll have to manually check the
// keyCode in the handler instead.
on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using vm.$emit.
nativeOn: {
click: this.nativeClickHandler
},
// Class is a special module, same API as `v-bind:class`
class: {
foo: true,
bar: false
},
// Style is also same as `v-bind:style`
style: {
color: 'red',
fontSize: '14px'
},
// Other special top-level properties
key: 'key',
ref: 'ref',
// Assign the `ref` is used on elements/components with v-for
refInFor: true,
slot: 'slot'
})
}
使用jsx代替vue需要解决的系列问题:
bable jsx插件会通过正则匹配的方式在编译阶段将书写在组件上属性进行“分类”。 onXXX的均被认为是事件,nativeOnXXX是原生事件,domPropsXXX是Dom属性。
class,staticClass,style,key,ref,refInFor,slot,scopedSlots这些被认为是顶级属性,至于我们属性声明的props,以及html属性attrs,不需要加前缀,插件会将其统一分类到attrs属性下,然后在运行阶段根据是否在props声明来决定属性归属(即属于props还是attrs)。
export default {
name: "button-counter",
props: ["count"],
methods: {
onClick() {
this.$emit("change", this.count + 1);
}
},
render() {
return (
<button onClick={this.onClick}>You clicked me {this.count} times.</button>
);
}
};
在React中所有属性都是顶级属性,直接使用{...props}就可以了,但是在Vue中,你需要明确该属性所属的分类,如一个动态属性value和事件change,你可以使用如下方式(延展属性)传递:
const dynamicProps = {
props: {},
on: {},
}
if(haValue) dynamicProps.props.value = value
if(hasChange) dynamicProps.on.change = onChange
<Dynamic {...dynamicProps} />
尽量使用明确分类的方式传递属性,而不是要babel插件帮你分类及合并属性。
常用的v-if和v-for,使用js语法中的if/for语句就能实现了。v-model属于prop + input事件语法糖,也可以使用babel插件自动实现。
// v-show,同理v-if
render(){
return (
<div>
{this.show?'你帅':'你丑'}
</div>
)
}
// v-for
render(){
return (
<div>
{this.list.map((v)=>{
return <p>{v}</p>
})}
</div>
)
}
// v-model: 传值和监听事件改变值(babel插件已支持)
data(){
return{
text:'',
}
},
methods:{
input(e){
this.text=e.target.value
}
},
render(){
return (
<div>
<input type="text" value={this.text} onInput={this.input}/>
<p>{this.text}</p>
</div>
)
}
slot直接通过this.$slots
对象拿到,scopedSlot通过this.$scopedSlots
对象拿到($scopedSlots每项是待调用函数)。
export default {
render(h) {
return <div>
{
(this.title || this.$slots.header) && (
<div class="header">
<span class="title">{this.title}</span>
{this.$slots.header}
</div>
)
}
{this.$slots.default}
</div>
},
}
不需要注册,直接使用
import Todo from './Todo.jsx'
export default {
render(h) {
return <Todo /> // no need to register Todo via components option
},
}
export default {
functional:true,
render(h,context){
return (
<div class="red" {...context.data}>
{context.props.data}
</div>
)
}
}
安装@vue/babel-sugar-v-model
babel插件后即可自动解析v-model,官方更推荐使用vModel
或者value + onInput事件
。
<el-input vModel_trim={inputValue}/>
// 或者使用
<el-input
value={this.inputValue}
onInput={val => this.inputValue = val.trim()}/>