Skip to content
On this page

composition-api 实践

@vue/composition-api 实践

原文地址:composition-api 实践

根据提案衍生的体验包 @vue/composition-api 我们可以对 v3.0 的思想加以实践。

实践内容:实现一个 todo list,且列表可拖拽。demo:示例详情

前期准备

1.定义列表结构

ts
type TodoItem = {
  id: number
  content: string
  status: 0 | 1
}

type TodoList = Array<TodoItem>

2.定义布局

文件目录

.
├── App.vue
├── assets
│   ├── drag.ts
├── components
│   ├── item-input.vue
│   └── item-list.vue
├── main.ts
└── views
    └── home.vue

我们将输入框和列表分为2个组件去处理,同时写了一个 useDrag() 方法来处理拖拽。

3.源码带来的一个问题和思考

js
// home.vue
setup (props, context) {
  // 我们选择将 state 初始化时用一个大对象包裹起来
  // 而不是零散的使用 ref、reactive
  const data = reactive({
    selected: null,
    list: [
      {
        id: 1,
        content: '计划内容、干什么事情',
        status: 1
      }
    ]
  })
  const delItem = (item: TodoItem) => {}
  const changeIndex = (nI: number, oI: number) => {}
  const addItem = (item: TodoItem) => {}

  watch(() => data.list, () => {
    console.log('====变化了')
  }, { lazy: true })

  return {
    // 选择使用大对象包裹,在解构之后是会丢失响应式的,可以使用 toRefs 将大对象里的属性添加引用包裹
    ...toRefs(data),
    delItem,
    changeIndex,
    addItem
  }
}

其实,这里也是我在实践时发现的一个问题,即:数组不能被 reactive ?

对于 data 的包裹完全是没有必要的,因为可以:

js
const selected = ref(null)
const list = reactive([
  // { ... }
])

但在实际的操作中,为 list push 一条记录是不会触发 watch 的

js
setup () {
  const arr = reactive([1, 2])
  watch(() => arr, () => console.log('arr change'), { lazy: true })
  setTimeout(() => { arr.push(3) }, 1000)
}

另外,在 setup 中即使你没有为对象或属性添加响应式,将其 return 后,响应式也会被自动添加。

例如

html
<template>
  <p @click="a = { b: 2 }">{{ a.b }}</p>
</template>

<script>  
export default {
  setup () {
    const a = {
      b: 1
    }
    return {
      a
    }
  }
}
</script>

这个例子中,对象 a 就被自动添加了响应式,模版也会被更新。vue 源码如下:

js
var binding = setup(props, ctx);

if (isPlainObject(binding)) {
    var bindingObj_1 = binding;
    vmStateManager.set(vm, 'rawBindings', binding);
    // 遍历返回值
    Object.keys(binding).forEach(function (name) {
      var bindingValue = bindingObj_1[name];
      // only make primitive value reactive
      if (!isRef(bindingValue)) {
          if (isReactive(bindingValue)) {
            bindingValue = ref(bindingValue);
          }
          else {
            // a non-reactive should not don't get reactivity
            bindingValue = ref(nonReactive(bindingValue));
          }
      }
      asVmProperty(vm, name, bindingValue);
    });
    return;
}

所以在 todo list 这个 demo 中,即使你的 list 不是 reactive 的,在点击 addItem push 后,template依旧会更新,但不理解的是这样做 watch 无效。因为我发现,使用 2.0 的 api watch 这个 list 是没问题的。

这不得不让我去思考 reactive 和 ref 的本质。

reactive 和 ref

在大概使用这2个 api 后不难发现,对于简单的数据类型我们要使用 ref,而复杂数据类型要使用 reactive。

ref 的作用是为简单数据类型包裹一层对象,这样就可以为对象设置 proxy,达到值的变化监听。

js
function ref(raw) {
    var _a;
    // 包裹的空对象
    var value = reactive((_a = {}, _a[RefKey] = raw, _a));
    return createRef({
        get: function () { return value[RefKey]; },
        set: function (v) { return (value[RefKey] = v); },
    });
}

reactive 的本质就是调用 2.0 api 里的 observe()

js
function reactive(obj) {
  // ...
  var observed = Vue.observable(obj);
  return observed
}

这样就会得到一个被监听对象的引用。上面我们说过,即使这个对象没有被 reactive,setup return 之后都会自动添加,那这样做的意义?

很明显,得到这个引用,我们可以单独为这个引用做处理,例如添加 watch,或者 computed,从而实现业务逻辑上的抽象与解耦。

⚠️但是,还是没有解决数组被 reactive 后,watch 不到的问题。。。

解耦业务代码

我们思考一下 3.0 的目的,我觉得一方面是为了迎合当下流行的思想,提升知名度与活跃度;另一方面,主要还是为了能更好的抽象与组织代码,充分利用 tree shaking 和 支持 ts。

利用函数的组合,移除了 mixins 中变量冲突与未知来源的问题,也拿掉了面条式的还要记属性顺序的固定的选项,当然,我觉得对于更初级的开发者会更习惯于 2.0 的 api 中明确的界限,例如 state 就放 data 里,method 就放 methods 里等。对于组合 api 式的都在 setup 中写更考验开发者的逻辑组合能力。

例如 todo list 中的拖拽 useDrag(),我们看看 /components/item-list.vue 文件中的 setup

js
// 接受了 props 列表数据
setup ({ list }: { [k: string]: unknown }, context: SetupContext) {
  const handleSelect = (item: TodoItem) => {
    context.emit('input', item)
  }
  const handleComplete = (item: TodoItem) => {
    // 我发现这里可以直接修改? props,而没有任何报错或警告
    item.status = item.status === 1 ? 0 : 1
  }
  const handleDelete = (item: TodoItem) => {
    context.emit('del-item', item)
  }
  const handleChange = (newIndex: number, oldIndex: number) => {
    context.emit('change-index', newIndex, oldIndex)
  }
  // 通过 useDrag 我们可以得到 2 个有用的信息
  // handleDown 是监听 mousedown 事件的回调
  // finalPosition 是在 mousemove 的过程中,当前被拖拽对象的位置
  // handleChange 是 mouseup 后,如果位置有变化就触发的回调,其实也可以返回而不用传参进去
  const { handleDown, targetIndex, finalPosition } = useDrag(handleChange)

  return {
    handleSelect,
    handleComplete,
    handleDelete,
    handleDown,
    targetIndex,
    finalPosition
  }
}

所以在 template 中,我们只需要简单的这样做就可以,是不是更清晰一些呢?关于 drag 的细节,可查看drag.ts

html
<template>
  <div
    class="todo-list__item"
    :style="finalPosition"
    @mousedown="handleDown"
  >
   ...
  </div>
<template>

于是,我们似乎发现,函数式组合的 api 可以提供我们一种能力,在任何地方的任何一个函数里,都可以写出与 vue 生命周期、钩子实例等息息相关的代码,就好像我们写在 2.0 时代的选项 componentOptions 中一样。

总结

有人说,这越来越像 react 了,确实,对于开创业界潮流 react 一直都是领先者,但你觉得真的是一样吗?由于机制本质的不一样,在 vue 中不需要那么多的原生 hooks 来帮助你实现或优化某些特性,在 vue 中,被称为 hooks 我觉得是不合适的,因为它并不是像钩子、或者守卫一样等待着被调用,它只会在 setup 被中调用一次,时机在 beforeCreate 和 created 之间。另外 mutable 的数据可以很优雅的处理很多 react 中不必要的麻烦,但也会带来一些问题,例如上面提到的“可以直接修改 props 的属性”。