vue官方推荐的是单文件 .vue 编写组件,简单易学但没有jsx灵活,很多知名的UI库最终选择了使用jsx,例如:ant-desing-vue, vant。但是在 vue 中使用 jsx 也存在一些问题。比如:大部分 vue 指令无法使用或者很难使用。在 vue3 中专门开了个 issue 讨论解决方案,至今没有优雅的解决方案。 jsv 而不是 jsx 可能是更好的解决方案。
一、先看看 .vue 与 .jsx 的比较
1. 变量作用域比较
- 1.1 .vue 中无法使用当前作用域变量,必须return后才能使用
// Scope.vue
<template>
<div>{{ state.count }}</div>
<button @click="handleClick">点击加1</button>
</template>
<script>
import { reactive } from "vue";
export default {
setup() {
let state = reactive({ count: 0 });
function handleClick() {
state.count++;
}
// 在模板中无法直接使用setup中的变量,必须return
return { state, handleClick };
},
};
</script>
- 1.2 .jsx 中可以直接使用当前作用域的变量
// Scope.jsx
import { reactive } from "vue";
export default {
setup() {
let state = reactive({
count: 0
});
function handleClick() {
state.count++;
}
// 可以在渲染函数中直接使用当前作用域的变量
return ()=>(
<div>
<div >{ state.count }</div>
<button onClick={handleClick}>点击加1</button>
</div>
)
},
};
2. 灵活性比较
- 2.1 .vue 中一个文件只能写一个组件
// NoMulti.vue
<template>
<Title :title="state.title" />
</template>
<script>
import { reactive } from "vue";
// 不能在一个.vue中写多个组件,必须将 Title 写在另外一个文件中
import Title from "./Title.vue";
export default {
components: { Title },
setup() {
let state = reactive({ title: "jsv-compiler" });
return { state };
},
};
</script>
// Title.vue
<template>
<h1>hello {{title}}</h1>
</template>
<script>
export default {
props: {
title: String
}
}
</script>
- 2.2. .jsx 中可以写多个组件
// Multi.jsx
import { reactive } from "vue";
export default {
setup() {
let state = reactive({
title: 'jsv-compiler'
});
// 一个文件中可以写多个组件
let Title = ()=><h1>hello {state.title}</h1>
return ()=>(
<div>
<Title />
</div>
)
},
};
3. 指令比较
- 3.1. .vue 原生支持优雅的指令写法
<A v-model:argument.modifer="val" />
- 3.2. .jsx 本身不支持指令 社区有多种指令的书写方式,各种千奇百怪,为此还开了issue讨论,至今仍然没有友好的解决方式。因此可以暂时认为 .jsx 不支持指令或支持的不好. github.com/vuejs/jsx/i…
<A v-model={[val, 'argument', ['modifier']]} />
4. 运行时性能比较
- 4.1. .vue 支持hoist,block,patchProps等运行时性能提升,至少比 .jsx 性能快了3倍 vue-next-template-explorer.netlify.app
// 编译前
<div>
<div>静态节点</div>
<div >{{state.count }}</div>
</div>
// 编译后
const { createVNode: _createVNode, toDisplayString: _toDisplayString, openBlock: _openBlock, createBlock: _createBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "静态节点", -1 /* HOISTED */)
// render 函数 上面的代码只会执行一次
// 每次重新渲染都再次执行 render 函数
// 1. 关于 _hoisted_1 静态节点变量提升,作用是再次执行render函数时,不用重新创建节点,直接从内存中读取;
// 2. 关于 _openBlock, _createBlock,作用是在 dom-diff 时,不比较静态节点,只比较可变节点;
// 3. 关于 patchProps,作用是在 dom-diff 时,只比较 text 的变化,不比较其他任何属性的变化;
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1, // 静态节点变量提升
_createVNode("div", null, _toDisplayString(_ctx.state.count), 1 /* TEXT */) // 此处的 1 用于 patchProps 的标识
]))
}
// Check the console for the AST
- 4.2. .jsx 没有运行时优化 www.babeljs.cn/repl
// 编译前
<div>
<div >static node</div>
<div >{ state.count }</div>
</div>
// 编译后 (使用 vite 工具编译)
jsx(
"div",
null /* @__PURE__ */,
jsx("div", null, "static node") /* @__PURE__ */,
jsx("div", null, state.count)
)
5. 生态比较
- 5.1. 当前知名的UI库比如 ant-desing-vue,vant 内部采用了 jsx
- 5.2. 但对外提供的组件仍然是 vue的模板语法。对于使用者如果要用 jsx,还得自己把 vue 改成 jsx,代价高昂呀!
二、这才是正文:是 jsv 不是 jsx !
经过比较我们发现 .jsx 灵活但对指令支持不友好也没有运行时性能优化。 .vue很好的支持指令和运行时性能优化,但不灵活。
那有没有一种解决方案,既有 jsx 的灵活性,又有原生支持vue指令和运行时高性能,在 js代码 中直接使用 vue 模板语法?就像这样: 文件后缀为 .jsv
// 文件命名为 App.jsv
import { reactive } from "vue";
export default {
name: "App",
setup() {
let state = reactive({
title: 'jsv-compiler',
count: 0
});
function handleClick() {
state.count++;
}
// 一个文件中写多个组件。此处的 Title 是 组件。
let Title = <template><h1>hello {{state.title}}</h1></template>
// 直接使用当前作用域的变量,比如 state.count
return (
<template>
<Title />
<div>{{ state.count }}</div>
<button @click="handleClick">点击加1</button>
</template>
)
},
};
目标有了那就开始魔改吧。我写了个编译器集成了vite插件实现:jsv-compiler
1. 创建vue3项目
npx create-vite-app <your projectname>
2. 安装依赖包
npm i -D jsv-compiler
3. 配置插件
文件: vite.config.js
import {jsvPlugin} from 'jsv-compiler'
export default {
configureServer: [jsvPlugin]
}
4. 使用示例
新建文件后缀为 .jsv
// 文件命名为 **App.jsv**
import { reactive } from "vue";
export default {
name: "App",
setup() {
let state = reactive({
title: 'jsv-compiler',
count: 0
});
function handleClick() {
state.count++;
}
// 一个文件中写多个组件。此处的 Title 是 组件。
let Title = <template><h1>hello {{state.title}}</h1></template>
// 直接使用当前作用域的变量,比如 state.count
return (
<template>
<Title />
<div>{{ state.count }}</div>
<button @click="handleClick">点击加1</button>
</template>
)
},
};
启动项目 npm run dev
,很神奇的魔改成功了:
还是有点小激动的!
5. 语法高亮
- 5.1 在vscode的插件市场中下载插件: jsv
- 5.2 因为是第一个版本,在 App.jsv 中暂时无法语法高亮,需将文件名改为 App.js ,并在
<template></template>
旁边加上反引号 才能在vscode中实现语法高亮,实际在编译层面是不需要加反引号的。
6. 复盘下魔改的主要过程
主要看编译结果,还是看这个简单的例子
// 编译前
<div>
<div>静态节点</div>
<div >{{state.count }}</div>
</div>
- 6.1. 我是如何做到在一个文件中写多个template ?
在原来的 .vue 中, template 编译后会被写到单独的文件中,并return 或者 export 出去。
为了达到在一个文件中写多个组件,我将其编译成一个自执行函数并返回render函数。同时利用闭包特性,仍然能使用静态节点提升后的变量 _hoisted_1
// jsv的编译结果
(() => {
const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, "静态节点", -1 /* HOISTED */)
return function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_createVNode("div", null, _toDisplayString(
(() => {
try {
return state
} catch {
return _ctx.state
}
})().count), 1 /* TEXT */)
]))
}
})()
- 6.2. 如何做到使用当前作用域的变量 state?
我将_ctx.state编译成自执行函数:
(() => {
try {
return state
} catch {
return _ctx.state
}
})()
- 6.3. 如何使用当前作用域的组件?
我们知道在 .vue 中必须要将组件注册后才能在 template 中使用,比如
// NoMulti.vue
<template>
<Title :title="state.title" />
</template>
<script>
import { reactive } from "vue";
import Title from "./Title.vue";
export default {
components: { Title }, // 必须要在此处注册组件
setup() {
let state = reactive({ title: "jsv-compiler" });
return { state };
},
};
</script>
为了实现不注册也能直接使用,我将其编译成这样:
const _component_Title = (()=>{
try {
return Title;
} catch (error) {
return _resolveComponent("Title");
}
}) ()
也是一个自执行函数,哈哈。如果当前作用域有 Title,就使用当前作用域的 Title。如果当前作用域没有 Title, 就使用注册的组件_resolveComponent("Title")。
还有很多魔性修改来不及写了。。。大家发现了吧,大量用到了自执行函数。。。
三、总结
在 .js 中使用 vue 模板语法 (简称 jsv)
vue3 模板 | jsv | jsx | |
---|---|---|---|
灵活性 | 不灵活 | 灵活 | 灵活 |
文件中组件数量 | 1个文件1个组件 | 1个文件多个组件 | 1个文件多个组件 |
能否使用当前作用域变量 | 不能,需在setup()方法中return后才能使用 | 可以使用 | 可以使用 |
是否很好支持指令 | 原生支持 | 原生支持 | 不支持或支持不友好 |
运行时性能 | 支持hoist,createBlock,patchProps | 支持hoist,createBlock,patchProps | 不支持 |
自动热更新 | 支持 | 不支持 | 不支持 |
值得一提的是 jsv 保持了原本 vue 的高性能,支持hoist,createBlock,patchProps等在 jsx 中难以实现的特性,比 jsx快了近3倍。 可能有人会说为什么要折腾这些?我想说的是 人类对效率的追求总是无止境的,正是像这样的折腾铸就了人类宏伟的科技蓝图。也希望我的一点贡献能抛砖引玉激发起大家的兴趣尝试一下
备注: jsv-compiler 在线编译器: ruige24601.github.io/jsv-compile…