写在前面
本篇文章发布于2020.01.14,目前Vue3.0
正处于Alpha测试阶段,如果后续更新/发布的话,会考虑更新这篇文章的内容。
阅读依赖
本篇文章默认读者已经了解以下知识,没有掌握的请去补课~
- Vue2.x的用法
- npm的安装与构建
- git相关操作
-
react hooks
相关知识(了解最好,非必须)
Vue3.0介绍
快速开始
首先把代码拉到本地(默认使用master分支即可),在根目录下执行npm install && npm run build
, 就能在/packages/vue/dist
下得到打包后的文件:
vue.cjs.js
vue.esm-bundler.j s
vue.esm.prod.js
vue.global.prod.js
vue.cjs.prod.js
vue.esm.js
vue.global.js
vue.runtime.esm-bundler.js
浏览器引入使用
如果在纯浏览器环境下,我们选用上面的vue.global.js
作为依赖,因为它包含了开发提示以及template编译器。直接来一段小demo:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue3.0 sample</title>
<!-- 在浏览器下template可以这么写了 -->
<script type="text/x-template" id="greeting">
<h3>{{ message }}</h3>
</script>
<script src="vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
const { createApp } = Vue;
const myApp = {
template: '#greeting',
data() {
return {
message: 'Hello Vue3.0'
}
}
}
createApp().mount(myApp, '#app')
</script>
</body>
</html>
浏览器中打开,你将看到如下文字:
Hello Vue3.0
使用webpack构建3.0的sfc
<template>
<img src="./logo.png">
<h1>Hello Vue 3!</h1>
<button @click="inc">Clicked {{ count }} times.</button>
</template>
<script type="text/javascript">
import { ref } from 'vue'
export default {
setup() {
const count = ref(0); // 响应式数据
// 事件回调不需要任何处理,直接作为对象方法返回即可;
const inc = () => {
count.value++
}
return {
count,
inc
}
}
}
</script>
与2.0不同了,这个setup
方法就是3.0的新API,定义了一个响应式属性count
和方法inc
,将他们作为对象返回,就能在template
中使用。其中,ref
是对一个原始值
添加响应式(装箱),通过.value
获取和修改(拆箱),对应2.0中的data
,如果需要对一个对象/数组添加响应式,则需要调用reactive()
方法
import { reactive } from 'vue'
export default {
setup() {
return {
foo: reactive({ a: 1, b: 2 })
}
}
}
拓展实践
为了更进一步理解setup,我们改造一下点击计数器——键盘计数器。
要实现的目标和思路为:
- 将计数器变成一个组件,由外部控制开启/关闭:
component
、ref
和v-if
的使用 - 计数器监听某个键盘按键,按键名称由父组件作为props传入(如Enter,Space等):
setup(props)
获取 - 组件渲染(
onMounted
)后开始监听,组件拆卸(onUnmounted
)后取消监听:生命周期钩子
在3.0中的用法 - 添加
is-odd
文本,表示按键次数是否为奇数:computed
vue3.0中的用法 - 按键次数为5的倍数(0除外)时,弹出alert窗口:
watch
在vue3.0中的用法
Talk is cheap, show me the code
首先是改造App.vue
父组件导入key-press-enter
子组件,注意看template
有何变化
<template>
<!-- 设置checkbox控制组件开关 -->
<input id="key-counter" type="checkbox" v-model="enableKeyPressCounter">
<label for="key-counter">check to enable keypress counter</label>
<key-press-counter v-if="enableKeyPressCounter" key-name="Enter" />
</template>
<script type="text/javascript">
import { ref } from 'vue'
import KeyPressCounter from './KeyPressCounter.vue';
export default {
components: {
KeyPressCounter // 组件用法和原来一致
},
setup() {
return {
enableKeyPressCounter: ref(false), // 是否开启组件
}
}
}
</script>
可以发现:template
现在可以像jsx一样作为碎片引入,不需要添加根元素了(当然#app根容器还是需要的)
接着是子组件KeyPressCounter.vue
:
<script>
<template>
<h3>Listening keypress: {{ keyName }}</h3>
<p>Press {{ pressCount }} times!</p>
<p>Is odd times: {{ isOdd }}</p>
</template>
<script type="text/javascript">
import { onMounted, onUnmounted, ref, effect, computed } from 'vue';
/**
* 创建一个键盘按键监听钩子
* @param keyName 按键名称
* @param callback 监听回调
*/
const useKeyboardListener = (keyName, callback) => {
const listener = (e) =>{
console.log(`你按下了${e.key}键`) // 用来验证监听时间是否开启/关闭
if (e.key !== keyName) {
return;
}
callback()
}
// 当组件渲染时监听
onMounted(()=>{
document.addEventListener('keydown', listener)
})
// 当组件拆解时监听
onUnmounted(()=>{
document.removeEventListener('keydown', listener)
})
}
export default {
name: "EnterCounter",
/**
* @param props 父组件传入的props
* @return { Object } 返回的对象可以在template中引用
*/
setup(props) {
const { keyName } = props
const pressCount = ref(0)
// hooks调用
useKeyboardListener(keyName, ()=>{
pressCount.value += 1;
})
// watch的用法,可以看到,现在无需声明字段或者source,vue自动追踪依赖
effect(()=>{
if (pressCount.value && pressCount.value % 5 === 0) {
alert(`you have press ${pressCount.value} times!`)
}
})
// computed的用法,基本是原来的配方
const isOdd = computed(()=> pressCount.value % 2 === 1)
return {
keyName,
pressCount,
isOdd
}
}
}
</script>
以后编写组件就是setup一把梭了!是不是越来越像react hooks
了?
对比一下传统写法:
<template>
<div>
<h3>Listening keypress: {{ keyName }}</h3>
<p>Press {{ pressCount }} times!</p>
<p>Is odd times: {{ isOdd }}</p>
</div>
</template>
<script type="text/javascript">
let listener
export default {
name: "EnterCounter",
props: {
keyName: String
},
computed: {
isOdd() {
return this.pressCount % 2 === 1;
}
},
data() {
return {
pressCount: 0
}
},
mounted() {
listener = (e) =>{
if (e.key !== this.keyName) {
return;
}
this.callback()
}
document.addEventListener('keydown', listener)
},
beforeUnmount() {
document.removeEventListener('keydown', listener)
},
watch: {
pressCount(newVal) {
if (newVal && newVal % 5 === 0) {
alert(`you have press ${newVal} times!`)
}
}
},
methods: {
callback() {
this.pressCount += 1;
}
}
}
</script>
当然,声明式vs函数式,不能说哪个一定比另外一个好。尤大依然为我们保持了传统api,这也意味着从2.0迁移到3.0,付出的成本是非常平滑的。
总结
- template支持碎片,即无需传入
- 传统的组件option api,现在可以用setup来实现,不仅比以前变得更加灵活,在类型分析上(typescript)将会支持得更好
- 大部分api如ref/reactive/onMounted等方法,现在支持按需导入,对于tree-shaking优化有利
- setup使开发者不必再关心令人头疼的
this
问题 - setup是一把双刃剑,如果你的思路足够清晰,那么它将会是你抽象逻辑的利器。反之使用不当同样也会让你的代码变成意大利面条🍝