Почему рефери Vue3 разбивает сердца многим крупным парням?

Vue.js

Недавнее предложение Vue3 для ref-sugar вызвало много дискуссий:Наггетс.Талант/пост/689417…

<script setup>
import Foo from './Foo.vue'

// declaring a variable that compiles to a ref
ref: count = 1
const inc = () => {
  count++
}
// access the raw ref object by prefixing with $
console.log($count.value)
</script>

<template>
  <Foo :count="count" @click="inc" />
</template>

Заинтересованные студенты могут прочитать обсуждение выше, это снова не будет обсуждаться. Предложение направлено на дальнейшее упрощение ref.Value формулировки, но поскольку поправка JS семантика самого языка, вызвала много споров.

В этой статье мы надеемся предоставить другое решение с возможно меньшей умственной нагрузкой наиболее консервативным способом.

1. Просмотрите vue-composition-api-rfc

Чтение и запись ссылки более избыточны, чем обычное значение, поскольку требуется доступ к .value. Youda очень осторожно относится к использованию синтаксического сахара во время компиляции для решения этой проблемы и ясно дал понять, что такая поддержка не предоставляется по умолчанию.v UE-состав-API-RFC.net show.app/this/#%E5%BC%…

Я полностью согласен с этим.

Однако .value должен быть добавлен в js, а .value не требуется в шаблоне, что, несомненно, вызывает определенную степень путаницы и фрагментации.

2. Решение: превратить в объекты новую строку ('foo'), а затем перехватить

Почему это происходит? По сути, мы не можем перехватывать значения базовых типов данных. Хорошо, тогда мы конвертируем базовый тип данных в соответствующий экземпляр класса-оболочки, а затем перехватываем его. Например:let str = 'foo'переписать какlet strObj = new String('foo'),В настоящее времяstrObjэто объект, следующий мы пытаемся перехватить, я написал библиотекуre-primitive, пример использования следующий:

const { rePrimitive, watchEffect } = require('../dist/re-primitive.cjs')

// 用 rePrimitive 代替 ref; 并传入String的包装对象  new String('foo')
let proxy = rePrimitive(new String('foo'))
// let proxy = rePrimitive('foo')  // 内部做了装箱,可简写去掉 new String()
// rePrimitive 的作用是将对象设置成响应式,并增加 setValue() 修改数据的方法

watchEffect(() => {
  console.log('输出 proxy instanceof String:', proxy instanceof String) // true , 可以看出 proxy是String的实例,可以使用所有的String的原型方法
  console.log('输出 proxy.valueOf():' + proxy.valueOf())
  console.log('输出:proxy.substring(1): ' + proxy.substring(1))
})
console.log('==========')
proxy.setValue('bar') // 响应式修改数据,重新执行 effect 函数

// 输出结果

// 输出 proxy instanceof String: true
// 输出 proxy.valueOf():foo
// 输出:proxy.substring(1): oo
// ==========
// 输出 proxy instanceof String: true
// 输出 proxy.valueOf():bar
// 输出:proxy.substring(1): ar
  • rePrimitive: Эквивалент реактивного, исх. Установите базовый тип данных как реактивный, если передается new String('foo'), то proxy по-прежнему является экземпляром String; Поддержка String | Number | Boolean трех типов, можно сократить, чтобы удалить активный бокс.
let proxyStr = rePrimitive('foo') // 等价于 let proxyStr = rePrimitive(new String('foo'))
let proxyNum = rePrimitive(123) // 等价于 let proxyNum = rePrimitive(new Number(123))
let proxyBool = rePrimitive(false) // 等价于 let proxyBool = rePrimitive(new Boolean(false))
  • setValue: оперативно изменяет данные и повторно выполняет функцию эффекта.

3. Процесс осуществления восстановления

import { reactive, trigger, track, effect, TrackOpTypes, TriggerOpTypes } from '@vue/reactivity'
// ts 的类型定义可先忽略
interface ReString extends String {
  setValue: (value: String) => void
}
interface ReNumber extends Number {
  setValue: (value: Number) => void
}
interface ReBoolean extends Boolean {
  setValue: (value: Boolean) => void
}

export const watchEffect = effect

export function rePrimitive(prim: String): ReString
export function rePrimitive(prim: Number): ReNumber
export function rePrimitive(prim: Boolean): ReBoolean
export function rePrimitive(prim: String | Number | Boolean) {
  const obj = {}
  setValue(prim)
  function setValue(value) {
    // 判断如果是基本数据类型则转成对应的包装对象
    if (typeof value === 'string') {
      prim = new String(value)
    } else if (typeof value === 'number') {
      prim = new Number(value)
    } else if (typeof value === 'boolean') {
      prim = new Boolean(value)
    } else {
      // 如果是包装对象则不作处理,例如 传的 new String('foo')
      prim = value
    }
    // 这里比较巧妙,将prim设置为obj的原型对象,
    // 这样 obj 就是 String 的实例对象, 可以使用 String的所有原型方法,包括 .valueOf()
    Object.setPrototypeOf(obj, prim)
    // 触发响应式更新
    trigger(obj, TriggerOpTypes.SET, '__value__')
  }

  return new Proxy(obj, {
    get(target, key, receiver) {
      if (key === 'setValue') {
        // 如果 key 是 'setValue', 则返回 setValue 函数,提供响应式更新数据的能力
        return setValue
      }
      track(target, TrackOpTypes.GET, '__value__')
      // track(target, 'get', '__value__')
      let result = Reflect.get(target, key, receiver)
      if (typeof result === 'function') {
        // 这里判断如果是函数,则必须处理this指向
        return result.bind(prim)
      }
      return result
    },
  }) as ReString | ReNumber | ReBoolean
}

В целом, это более изобретательно, потому чтоnew String('foo')Он неизменяем. Чтобы иметь возможность перехватывать и изменять данные через прокси, используйтеObject.setPrototypeOf(obj, prim)модифицироватьobjобъект-прототип.

4. Резюме

  • Используйте .valueOf(), чтобы получить значение. Разработчики должны всегда обращать внимание на то, что прокси — это объект String, Number, Boolean, и не забывать вызывать метод .valueOf() для получения фактического значения. Работает с плагинами eslint, чтобы помочь проверить отсутствующие методы .valueOf().
  • Изменено с помощью .setValue(). Это решение полностью сохраняет возможности языка js, но добавляет только метод .setValue(), который может быть чуть менее обременительным для разработчика, чем ref
  • Эта статья является просто предложением предоставить другое решение, и дружеские обсуждения приветствуются. Реализуйте репозиторий исходного кода:GitHub.com/Rigo24601/…