(文章中包含源码和原理分析, 须要有必定的基础, 若是看不懂能够直接翻到最底部, 有现成的库能够解决问题)javascript
2019年05月30日, Vue 的建立者尤雨溪发布了一个请求意见稿(RFC), 内容是在即将发布的 Vue 3.0 中使用函数式风格来编写 Vue 组件.html
接着 Vue 开发团队放出了能够在 Vue 2.0 中使用这个特性的插件 vue-function-plugin.vue
这一次的变化引发了不少质疑, 与之相比当 Facebook 发布 React hooks 的时候获得了很大的好评. 那么 vue-function-api 到底好很差, 相似的改变在 vue 和 react 上为了获得了不一样的反馈 ? 我也是抱着这个好奇心来亲自尝试一下.java
通过短暂的尝试, 简单总结了 vue-function-api 和 react hooks 的一些区别, 由于接触时间还短, 可能会有遗漏或不许确的地方, 还请指正.react
先直观看一下区别:git
React 写法github
import React, { useState, useEffect } from 'react'
export function Demo () {
const [count, setCount] = useState(0)
const [time, setTime] = useState(new Date())
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date())
}, 1000)
return () => {
clearInterval(timer)
}
})
return (
<span> <span>{count}</span> <button onClick={() => setCount(count + 1)}>+1</button> <span>{time.toString()}</span> </span>
)
}
复制代码
Vue 写法vue-router
<template>
<span>
<span>{{ count }}</span>
<button @click="addCount">+1</button>
<span>{{ time.toString() }}</span>
</span>
</template>
<script> import { value, onCreated, onBeforeDestroy } from 'vue-function-api' export default { name: 'Demo', components: {}, props: {}, setup(props, context) { const count = value(0) const addCount = () => { count.value++ } const time = value(new Date()) let timer = 0 onCreated(() => { timer = setInterval(() => { time.value = new Date() }) }) onBeforeDestroy(() => { clearInterval(timer) }) return { addCount, timer, } }, } </script>
<style scoped> </style>
复制代码
React 的代码更加纯粹, 整个组件变成了一个函数, state 和 set 方法直接被用于渲染, 整个代码表现很是的一致.vuex
Vue 大致依然保留 template, script, style 基本三段的写法, 固然这也是 vue 的一大优点. Vue 把原来的 data, computed, lifecycle, watch 等融合在一个 setup 函数中完成. 整体上是模板, 对象, 函数式融合的风格.typescript
React 将原来大的 state 拆分红一个一个小的 state, 每一个 state 是一个包含 value 和 set 方法的组合.
Vue 将整个 data 拆分红一个一个的 value, value 的返回时是一个包装对象, 经过读取和修改对象的 value 属性进行状态的操做, 这种作法的缘由大概是 Vue 自己就是是基于对象的 setter 和 getter 特性而构建的.
React 提供 useEffect 方法, 用于组件初始化, 更新以及销毁时作一些带有反作用的方法, 这个方法简化了本来须要三个生命周期函数才能完成的事情. 固然对原有的改动也比较大.
Vue 基本是将原来的 lifecycle 方法原封不动移植, 每个 lifecycle 都有对应的方法进行包装.
看到这有同窗就要问了: 说了这么一大堆, 怎么还没进入正题 ?
emmmmmm, 写跑题了, 进入正题吧.
事情是这样, 因为业务规划, 原有的一个大系统中的一部分须要拆分出来独立成一个新系统. 这个老系统整个的结构仍是基于好久以前的脚手架作的, 而新的脚手架已经有了翻天覆地的变化. 此次迁移须要创建在新脚手架之上进行开发.
既然是新脚手架, 新的环境, 新的代码, 那咱们为何不进行新的尝试呢. 因而乎, 打算在项目的一个小角落里使用 vue-function-api, 和其余组件共存.
当时这个页面大概是这样 (列出了核心部分):
const menuMaxHeight = () => {
const userInfoHeight = this.$refs['sidebar-userInfo'] && this.$refs['sidebar-userInfo'].$el.clientHeight
const bannerHeight = this.$refs['sidebar-banner'] && this.$refs['sidebar-banner'].$el.clientHeight
this.menuMaxHeight = window.innerHeight - userInfoHeight - bannerHeight
}
export default {
// ...
data() {
return {
menuMaxHeight: 400,
}
},
computed: {
...mapGetters(['menu']),
userInfo() {
const info = this.$store.getters.userInfo
const env = window.ENVIRONMENT === 'preview'
? 'preview'
: process.env.NODE_ENV === 'development'
? 'local'
: process.env.NODE_ENV === 'test'
? 'test'
: 'online'
return {
userName: `${info.name || ''} (${env})`,
}
},
},
mounted() {
window.addEventListener('resize', menuMaxHeight)
menuMaxHeight()
},
beforeDestroyed(){
window.removeEventListener('resize', menuMaxHeight)
}
// ...
}
复制代码
首先修改的是 menuMaxHeight
, 这是一个动态获取元素高度的而且实时同步到模板中的一个功能, 用到了 mounted
, beforeDestroyed
, 对 window
注册和解绑 resize
事件.
const useMenuHeigth = (initValue, context) => {
const menuMaxHeight = value(400)
const calcHeight = () => {
const userInfoHeight = context.refs['sidebar-userInfo'] && context.refs['sidebar-userInfo'].$el.clientHeight
const bannerHeight = context.refs['sidebar-banner'] && context.refs['sidebar-banner'].$el.clientHeight
menuMaxHeight.value = window.innerHeight - userInfoHeight - bannerHeight
}
onMounted(() => {
window.addEventListener('resize', calcHeight)
})
onBeforeDestroy(() => {
window.removeEventListener('resize', calcHeight)
})
}
export default {
// ...
setup(props, context) {
const menuMaxHeight = useMenuHeigth(400, context)
return {
menuMaxHeight
}
}
computed: {
...mapGetters(['menu']),
userInfo() {
const info = this.$store.getters.userInfo
const env = window.ENVIRONMENT === 'preview'
? 'preview'
: process.env.NODE_ENV === 'development'
? 'local'
: process.env.NODE_ENV === 'test'
? 'test'
: 'online'
return {
userName: `${info.name || ''} (${env})`,
}
},
},
// ...
}
复制代码
修改以后, 很惊喜的发现代码清晰了不少, 原来分散到各处的代码合并到了一个方法中, 一目了然.
接下来处理 userinfo, 代码中用到了 vuex 中保存的 userInfo, 并对数据作一些转换.
机智的我想起了, mapGetters 是须要绑定到 computed 的上, 既然 computed 写法变了, 因此我也修改一下个人写法, 因而代码是这样的:
import { mapGetters } from 'vuex'
const useGetters = (getters) => {
const computedObject = mapGetters(getters)
Object.keys(computedObject).forEach((key) => {
computedObject[key] = computed(computedObject[key])
})
return computedObject
}
// ...js
setup(props, context) {
const menuMaxHeight = useMenuHeigth(400, context)
const { menu, userInfo: vUserInfo } = useGetters(['menu', 'userInfo'])
const userInfo = computed(() => {
const info = vUserInfo
function getUsername(info) {
const env = window.ENVIRONMENT === 'preview'
? 'preview'
: process.env.NODE_ENV === 'development'
? 'local'
: process.env.NODE_ENV === 'test'
? 'test'
: 'online'
return `${info.name || ''} (${env})`
}
return {
userName: getUsername(info),
}
})
return {
menuMaxHeight,
menu,
userInfo,
}
}
// ...
复制代码
嗯, 看起来很合理
...
...
...
对方不想和你说话并抛出了一个异常
问题出在哪呢 ?
咱们知道 mapGetters 实际上是一个快捷方法, 那咱们不用快捷方法, 直接使用 this.$store 来获取, 看看问题究竟出在哪.
const useGetters = (getters) => {
const computedObject = mapGetters(getters)
getters.forEach((key) => {
computedObject[key] = computed(function getter() {
return this.$store.getters[key]
})
})
return computedObject
}
复制代码
$store 丢了 ( router 也丢了 ) , 难怪不推荐使用 this, 既然不推荐 this, 又给咱们提供了 context, 或许在 context 里吧, 不过仍是异想天开了, context 里面也没有.
为何呢 ?
只有源码才知道
看了一下源码, 从初始化阶段找到了 mixin 部分:
首先能够看到 在 beforeCreate 阶段, 判断有没有 setup 方法, 若是有, 则修改 data 属性, 在读取执行 data 的时候执行 initSetup
方法, 并传递了 vm, 这是 vm 中是存在 $store 的
继续找:
setup 是直接调用的, 因此 this 确定不是 vm, ctx 是由 createSetupContext
建立
死心吧
全部属性都是固定的, 没有其余拓展的方法.
再看 在 computed 执行的时候 this 里为何没有 $store
在 initSetup
中找到 bingding 最后调用的 setVmProperty
方法进行设置.
咱们来看一下 computed 是如何建立的
咱们调用 computed(function getter() { return this.$store.getters[key] })
的时候, getter 方法就会传递到 computed 这个方法中, 接下来经过 createComponentInstance
建立了一个 vue 实例, 并增长一个 $$state 的 computed 属性.
接下来在 read 方法, 咱们猜想取 value 的时候就是调用的这个方法, 这个方法调用了 computedHost 这个对象的 $$state 属性, 也就是说当咱们执行 getter 时, this 指向的是 computedHost 这个 vm.
因此关键就在 createComponentInstance
$state 什么都没有 !!!!
撞墙了
眼看着 vue-function-api 的代码实现把路都封死了. 咱们还能怎么办呢.
灵光一闪, 既然 vue-function-api 能写一个 mixin 篡改 data 方法, 我也能够用 mixin 去篡改 setup 方法, 并把丢掉的 vm 找回来, 在执行 setup 的时候 vm 仍是完整的.
因而写了一个 plugin
export const plugin: PluginObject<PluginOptions> = {
install(Vue, options = {}) {
if (curVue) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn('Vue function api helper init duplicated !')
}
}
function wrapperSetup(this: Vue) {
let vm = this
let $options = vm.$options
let setup = $options.setup
if (!setup) {
return
}
if (typeof setup !== 'function') {
// eslint-disable-next-line no-console
console.warn('The "setup" option should be a function that returns a object in component definitions.', vm)
return
}
// wapper the setup option, so that we can use prototype properties and mixin properties in context
$options.setup = function wrappedSetup(props, ctx) {
// to extend context
}
}
Vue.mixin({
beforeCreate: wrapperSetup,
})
},
}
复制代码
这部分是否是和 vue-function-api 很像 ?
咱们要作的核心就在 wrappedSetup
这个方法里, 在最开始咱们就经过 this 拿到了当前的 vm 对象, 因此在 wrappedSetup
咱们就能随心所欲的使用 vm 中的属性了.
$options.setup = function wrappedSetup(props, ctx) {
// to extend context
ctx.store = vm.$store
return setup(props, ctx)
}
复制代码
store 找回来了, 填坑成功!!!
既然咱们能够从 vm 中拿到全部丢掉的属性, 那咱们是否是能够作一个通用的方法, 将全部丢掉的属性都追加到 context 中呢. 这样既符合 vue-function-api 中 context 的使用预期, 又能够追加以前插件丢失掉的属性, 何乐而不为呢.
大概想到了几个对 vm 拓展的场景,
作法也很简单, 在注册时先遍历 vm 和 Vue.prototype, 获取到全部以 $ 开头的属性, 保存起来. 而后在 wrappedSetup 中, 对比当前 Vue.prototype 和 vm 多出来的属性, 追加到 context 中.
export const plugin: PluginObject<PluginOptions> = {
install(Vue, options = {}) {
if (curVue) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn('Vue function api helper init duplicated !')
}
}
const pureVueProtoKeys = Object.keys(Vue.prototype)
const pureVm = Object.keys(new Vue())
const extraKeys = (options.extraKeys || []).concat(DEFAULT_EXTRA_KEYS)
function wrapperSetup(this: Vue) {
let vm = this
let $options = vm.$options
let setup = $options.setup
if (!setup) {
return
}
if (typeof setup !== 'function') {
// eslint-disable-next-line no-console
console.warn('The "setup" option should be a function that returns a object in component definitions.', vm)
return
}
// wapper the setup option, so that we can use prototype properties and mixin properties in context
$options.setup = function wrappedSetup(props, ctx) {
// to extend context
Object.keys(vm)
.filter(x => /^\$/.test(x) && pureVm.indexOf(x) === -1)
.forEach((x) => {
// @ts-ignore
ctx[x.replace(/^\$/, '')] = vm[x]
})
Object.keys(vm.$root.constructor.prototype)
.filter(x => /^\$/.test(x) && pureVueProtoKeys.indexOf(x) === -1)
.forEach((x) => {
// @ts-ignore
ctx[x.replace(/^\$/, '')] = vm[x]
})
// to extend context with router properties
extraKeys.forEach((key) => {
// @ts-ignore
let value = vm['$' + key]
if (value) {
ctx[key] = value
}
})
// @ts-ignore
return setup(props, ctx)
}
}
Vue.mixin({
beforeCreate: wrapperSetup,
})
},
}
复制代码
中间遇到一个问题, $router 和 $route 是不可遍历的, 会被漏掉, 因此提供 extraKeys 属性, 默认为['router', 'route'], 判断 extraKeys 中全部 vm 中存在的属性, 追加到 ctx 中.
plugin 写好以后安装, 接下来就能够从 context 中取咱们想要的属性了. 不过当咱们使用 vuex 的 getter 时很麻烦, 由于 mapGetters 仍是用不了.
因而针对于 vuex 的场景封装了 useGetters 的方法.
export function useGetters(context: SetupContext, getters: string[]) {
const computedObject: AnyObject = {}
getters.forEach((key) => {
computedObject[key] = computed(() => context.store.getters[key])
})
return computedObject
}
复制代码
接下来经过 useGetters(context, []) 就能够愉快的使用 getter 了.
最后通过一系列的改造后, 在实际代码中是这个样子的:
const useMenuHeigth = (initValue, context) => {
const menuMaxHeight = value(400)
const calcHeight = () => {
const userInfoHeight = context.refs['sidebar-userInfo'] && context.refs['sidebar-userInfo'].$el.clientHeight
const bannerHeight = context.refs['sidebar-banner'] && context.refs['sidebar-banner'].$el.clientHeight
menuMaxHeight.value = window.innerHeight - userInfoHeight - bannerHeight
}
onMounted(() => {
window.addEventListener('resize', calcHeight)
})
onBeforeDestroy(() => {
window.removeEventListener('resize', calcHeight)
})
}
export default {
name: 'app',
components: {
SkeMenu,
SkeSideBar,
SkeUserInfo,
SkeSideBanner,
breadcrumb,
},
setup(props, context) {
const menuMaxHeight = useMenuHeigth(400, context)
const { menu, userInfo: vUserInfo } = useGetters(context, ['menu', 'userInfo'])
const userInfo = computed(() => {
const info = vUserInfo.value
function getUsername(info) {
const env = window.ENVIRONMENT === 'preview'
? 'preview'
: process.env.NODE_ENV === 'development'
? 'local'
: process.env.NODE_ENV === 'test'
? 'test'
: 'online'
return `${info.name || ''} (${env})`
}
return {
userName: getUsername(info),
}
})
return {
menuMaxHeight,
menu,
userInfo,
}
},
}
复制代码
大功告成 !!!
先别急着走, 既然已经作了这么多, 固然要封装一个库出来. 顺便推广一下本身, 哈哈
公布一下, vue-function-api-extra 如今已经发布, 而且开源. 能够经过 npm 或 yarn 进行安装.
Github 地址: github.com/chrisbing/v… npm 地址: www.npmjs.com/package/vue…
欢迎下载和 star
很简单, 在入口的最前面, 注意必定要在其余插件的前面安装, 安装 plugin, 就能够从 context 得到全部拓展的属性. 包括 store, router, 经过安装组件库得到的如 $confirm $message 等快捷方法, 本身经过 Vue.prototype 追加的变量, 均可以获取到.
import Vue from 'vue'
import { plugin } from 'vue-function-api-extra'
Vue.use(plugin)
复制代码
export default {
setup(props, context){
// use route
const route = context.route
// use store
const store = context.store
// use properties
// if you run "Vue.prototype.$isAndroid = true" before
const isAndroid = context.isAndroid
return {
}
}
}
复制代码
注意全部追加的属性都必须以 "$" 开头, 到 context 访问的时候要去掉 $, 这一点和 vue-function-api 内置的 slots, refs 的规则保持一致
若是想要使用 vuex 中的 getters 方法, 则能够引用 useGetters, 固然 plugin 是必定要安装的.
import { useGetters } from 'vue-function-api-extra'
export default {
setup(props, context){
const getters = useGetters(context, ['userInfo', 'otherGetter'])
return {
...getters
}
}
}
复制代码
后续会增长更多的 helper, 让你们更愉快的使用 vue-function-api 的新特性.