likes
comments
collection
share

藏在 Vue2 中的 composition API

作者站长头像
站长
· 阅读数 36

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

写在前面

由于上半年发生一些事情导致这期间断更好久,年初的文章近期才整理发出来🤣🤣🤣。这期间官方发布了 Vue2.7,所以文中部分内容和发布时间可能有所出入,感谢评论区小伙伴指正🌟🌟🌟

背景

熟悉 Vue2 的小伙伴们都清楚,Vue2 推崇 options API,这对新人来说上手很简单,但也伴随而来了一些小问题,比如逻辑服用很困难this黑盒mixins命名冲突等等。后来 Vue3 通过 composition API 解决了这些痛点。

但也因为采用了过于前卫的 Proxy ,让一些必须考虑兼容性的应用无法立刻升级。所以历史问题依旧存在。

藏在 Vue2 中的 composition API

所幸,今年年初笔者也遇到了类似的苦恼,最终发现了属于 Vue2composition API

藏在 Vue2 中的 composition API

嚯~这不仔细看还真以为是 Vue3 哈,但我们可是额外保留了 Vue2 良好的 兼容性 哦~

藏在 Vue2 中的 composition API

至于是如何实现的,让我们一起来看一下吧~

Vue2的祖训

首先大家都清楚,options API 推荐我们通过 data 创建 响应式对象 ;在 methods 中声明方法;而且还有一套完整的 生命周期函数 可供使用。

除此之外,官网 还贴心地告诉大家在各个 生命周期函数 中应该做那些事情,这样一来新手很快就能上手写代码了。

藏在 Vue2 中的 composition API

其中关于 beforeCreate 是这样讲的,

在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用

也就是说在 beforeCreate 阶段没有完成 数据响应式 ,因此很多教程会告诉大家不要在这个阶段操作 this.data ,大部分行为最好在 mounted 阶段编写。

久而久之这便形成了一个公认的规则:尽量不要在 beforeCreate 中操作 this.data

在规则的边缘试探

Vue 有一定研究的同学可能尝试过在 实例 之外的地方,用 Vue.util.defineReactiveVue.observable 创建 响应式对象

像这样:

藏在 Vue2 中的 composition API

于是乎现我们发现,好像 定义响应式变量 也可以不定义在 data方法 也可以不放在 methods

但是绑定在 原型链 上有点不妥,而且变量的作用域也不合适,

所以我们尝试在 beforeCreate 阶段做这些事情,顺便绑定到 this

藏在 Vue2 中的 composition API

发现这种方式也不错,逻辑比较集中,不用像之前那样上下来回翻文件了。

于是我们做了一个更大胆的尝试~

藏在 Vue2 中的 composition API

我做了一个违背祖训的决定

很快我们发现这种写法像极了 Vue3composition API ,只是没有我们重写 响应式原理 ,一切改造都是基于 Vue2 现有的 API,所以也不存在兼容性问题。

既然如此,何不借助 Vue2 的现有逻辑创造一个新的 composition API 呢,既不用担心兼容性问题,将来迁移 Vue3 时还无需做过多改动。

藏在 Vue2 中的 composition API

我们以常用的几个API为例,用 Vue2 来实现一下

defineComponent

Vue3 中,defineComponent 仅仅是在定义 组件 时提供类型推导的辅助函数,并没有额外其他功能

export default defineComponent({
  name: 'App,
  setup (props) {
    // ...
  }
})

但我们可以借助这个方法实现一个很重要的事情:让 options 支持 setup

setup 的核心作用便是将 返回值 挂载到 this 上。

所以我们先将 setup 函数执行,再把返回值绑定至 this

export const defineComponent = function (options) {
  const { beforeCreate, setup, ...restOptions } = options

  return {
    beforeCreate: setup
      ? function () {
        beforeCreate && beforeCreate()

        const options = setup(this)

        // 代理到this
        proxyToThis.call(this, options)
      } : undefined,
    ...restOptions
  }
}

但问题在于如何绑定到 this 呢?

经过思考我们想到两个可行性方案:

  1. Object.defineProperty

    这方案老熟悉了哈~,在大部分场景下也确实好用,但 Object.defineProperty 不允许重复设置 key ,否则报错 Cannot redefine property: xxx

    为什么会出现重复设置呢?

    大部分 Vue2 项目在 逻辑复用 时避免不了 mixinsextends ,这种情况下允许覆盖相同属性,所以该方案难以兼容此场景。

藏在 Vue2 中的 composition API

  1. this.$options

    看过源码的小伙伴或许知道,Vue 在组件初始化阶段会将 组件配置 绑定到 this.$options 上。

    所以 【修改 this.$options === 修改组件配置】

    藏在 Vue2 中的 composition API

    那么问题又来了,我们怎么知道返回的内容是 data ,还是 methods ,还是 computed 呢?

    仅依靠 数据类型 是不严谨的,因为 computedmethods 都是函数,而 data 可以是任意值。

    所以我们需要对这几类数据打上特殊标识

    const isComputed = Symbol('isComputed')
    const isRef = Symbol('isRef')
    

    以帮助我们在将目标绑定至 this 时能放到正确位置,说白了就是做搬运🤣。

藏在 Vue2 中的 composition API

reactive、ref 等基本API

上文有介绍,可以用 Vue.util.defineReactiveVue.observable 创建响应式对象,

Vue.observable 方法的返回值即是响应式对象,而且可以一次设置多个 key-value,所以在这里最适合。

export const reactive = Vue.observable

ref 也类似,这里就不赘述啦~

藏在 Vue2 中的 composition API

别急别急,这只是最基础的API,我们无需重复造轮子,所以直接复用即可。

toRef、toRefs

这两个 API 一直备受争议,因为方法返回的结构会被包装在 value 中, 像这样{ value: xxx }, 但在 template 引用时却无需获取 value,直接引用即可。

其实在 Vue3源码 中,绑定至 this 时会经过特殊处理:将 this.[key] 转发给 this.[key].value

像这样:

proxy(target, key, {
  get: function getterHandler() {
    var value = getter ? getter.call(target) : val;
    // 如果是 ref ,则直接获取到 value
    if (isRef(value)) {
      return value.value;
    } else {
      return value;
    }
  },
});

所以通过 this 访问时不需要读取 value,而直接访问则需要。

藏在 Vue2 中的 composition API

所以呢,我们要先在对象身上打上 isRef 标识,以便将来代理到 this 时直接获取到他们的 value

+ const isRef = Symbol('isRef')

+ export const toRef = function (obj, key) {
+   const ObjectRefImpl = {
+     get value () {
+       return obj[key]
+     },
+     set value (val) {
+       obj[key] = val
+     }
+   }
+   // 1. 打上特殊标识
+   ObjectRefImpl[isRef] = true
+   return ObjectRefImpl
+ }

const proxyToThis = function (obj) {
  for (const key in obj) {
    if (key in this) {
      continue
    }

    const value = obj[key]
    if (typeof value === 'function' && value[isComputed]) {
      // ...
+   } else if (value[isRef]) {
+     // 2. 如果是 ref ,则直接获取到 value
+     Object.defineProperty(this, key, {
+       get () {
+         return value.value
+       },
+       set (val) {
+         value.value = val
+       }
+     })
+   } else {
      // ...
    }
  }
}

computed

关于 computed ,有一种方案是直接代理给 this,像这样:

const double = () => state.count * 2

Object.defineProperty(this, 'double', {
  get () {
    return double()
  }
})

computedVue原理 中可不仅如此,而是一种特殊的 观察者,内部包含了 脏检查 等优化处理。

ps:本文对于 观察者 暂不展开细说,感兴趣的小伙伴可以查看 这篇文章

所以我们干脆把 computed 放入 this.$options 交给 Vue 处理好了。

+ const isComputed = Symbol('isComputed')
+ export const computed = function (getter) {
+   getter[isComputed] = true
+   return getter
+ }

const proxyToThis = function (obj) {
  for (const key in obj) {
    if (key in this) {
      continue
    }

    const value = obj[key]
+   if (typeof value === 'function' && value[isComputed]) {
+     // 如果是getter,则放到 options 中交给 Vue 处理
+     this.$options.computed[key] = value
+   } else if (value[isRef]) {
      // ...
    } else {
      // ...
    }
  }
}

藏在 Vue2 中的 composition API

onMounted 等生命周期函数

这里和前面会有所区别,因为 生命周期函数 不需要 return ,所以现有方案无法和 this 绑定

藏在 Vue2 中的 composition API

不怕不怕,看看 源码 是如何实现的。

源码 的思路是专门获取到当前组件的 this ,然后把 回调函数 添加到 this 上,由于考虑的情况比较全面,所以代码比较复杂。

但我们不需要考虑那么多,只需要保证在 setup 中使用 composition API 正常即可,毕竟太灵活了也不好嘛,还是要做一点限制的。

所以我们可以在 const options = setup(this) 之前 保存 组件实例,在其之后 取消绑定

核心改动如下:

let currentInstance = null

export const defineComponent = function (options) {
  const { beforeCreate, setup, ...restOptions } = options

  return {
    beforeCreate: setup
      ? function () {
        beforeCreate && beforeCreate()

+       // 保存 this
+       currentInstance = this

+       // 使用 this
+       const options = setup(currentInstance)

        proxyToThis(options)

+       // 取消 this 的绑定
+       currentInstance = null
      } : undefined,
    ...restOptions
  }
}

然后在 生命周期函数 中使用即可:

export const onMounted = function (cb) {
  if (!currentInstance) throw new Error('onMounted只能在 setup 中使用哦')

  if (currentInstance.$options.mounted) {
    currentInstance.$options.mounted.push(cb)
  } else {
    currentInstance.$options.mounted = cb
  }
}

其他 生命周期函数 同理~

藏在 Vue2 中的 composition API

小试锋芒

现在可以放心地在 历史债项目 中使用 composition API 啦!

import { defineComponent, reactive, toRefs, onMounted } from 'vue2-composition-api'
export default defineComponent({
  name: 'App',
  setup (props) {
    const state = reactive({
      id: '1',
      count: 0,
    })

    const add = () => state.count++

    const double = computed(() => state.count * 2)
    
    onMounted(() => {
      console.log('onMounted', state.count);
    })

    return {
      double,
      add,
      ...toRefs(state)
    }
  }
})

可以看到体验和 Vue3 一模一样!

藏在 Vue2 中的 composition API

思考与总结

本篇文章的意义有两点

  1. 因兼容性而无法升级到Vue3的项目 提供良好的过渡方案,以便将来可以顺滑地过渡。毕竟历史的车轮不会因为浏览器的版本问题而停滞不前,相信有朝一日我们一定可以全面拥抱 Vue3

    但也仅局限于对 响应式API 的改造,诸如 setup script自定义渲染器 这一类依赖 Vue3 编译器 的特性依然只能望而却步。

  2. 打破限制,寻求更多的可能,与其抱怨现阶段的不足,不如尝试着改变现状!

代码地址如下,欢迎小伙伴们拍砖~

npm

感谢读到这里的小伙伴,希望这篇文章能够给你带来帮助,蟹蟹~

转载自:https://juejin.cn/post/7144651931187675149
评论
请登录