Vue3 ref方法原理初探

89 阅读2分钟

前言

记得最开始学习vue3的时候,讲到refreactive的区别,说是ref是用defineProperty实现的,reactive是用Proxy实现的。

最近刚好看到了一篇文章,里面的ref是通过reactive封装对象实现的。

# 狂肝半个月!1.3万字深度剖析vue3响应式(附脑图)

当然这只是一个例子,但是不由产生了一个疑问,ref到底是怎么实现的呢?

RefImpl

带着上述疑问我去翻了vue3的源码

// packages\reactivity\src\ref.ts
// line:80
export function ref(value?: unknown) {
  return createRef(value, false)
}
// line:97
function createRef(rawValue: unknown, shallow: boolean) {
  // ...
  return new RefImpl(rawValue, shallow)
}
// line:104
class RefImpl<T> {
  private _value: T
  // ...
  constructor(value: T, public readonly __v_isShallow: boolean) {
    // ...
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    //...
    this._value = useDirectValue ? newVal : toReactive(newVal)
    triggerRefValue(this, newVal)
    //...
  }
}

这下就很清楚了,重点在于RefImpl,所以ref是通过es6类中的gettersetter实现的,并不是defineProperty

RefImpl中,通过将基本数据类型的值赋值给私有属性_value,然后使用get value()set value(),对数据访问和变化进行监听。同时,在getter中使用trackRefValue收集依赖,在setter中使用triggerRefValue触发收集的响应函数,从而实现响应式。

区别

那么defineProperty和es6类中的gettersetter到底有什么区别呢?

区别一:enumerable

首先,两者创建的都是一个伪属性,或者说虚拟属性。也就是说,这个属性默认情况下无法被枚举。为什么是默认情况呢?因为defineProperty可以显示指定enumerable: true将属性变为可枚举。

参考以下代码:

let obj1 = {};
var name = "zhangsan";
Object.defineProperty(obj1, "name", {
    get() {
      console.log("get name");
      return name;
    },
    set(val) {
      console.log("set name");
      name = val;
    }
});

class Person {
    _value = "";
    constructor(name) {
      this._value = name;
    }
    get name() {
      console.log("get name");
      return this._value;
    }
    set name(val) {
      console.log("set name");
      this._value = val;
    }
}

let obj2 = new Person("zhangsan");
for (k in obj1) console.log(k); // 没有输出
for (k in obj2) console.log(k); // _value

defineProperty增加enumerable: true:

Object.defineProperty(obj1, "name", {
    get() {
      console.log("get name");
      return name;
    },
    set(val) {
      console.log("set name");
      name = val;
    },
    // enumerable: true, // 将name属性变为可枚举的,默认为false        
});
for (k in obj1) console.log(k); // name

区别二:configurable

defineProperty创建的属性,默认无法被删除

delete obj1.name; // false

但是,当指定configurable: true时,可以通过delete删除属性。

将上述代码改为:

Object.defineProperty(obj1, "name", {
    get() {
      console.log("get name");
      return name;
    },
    set(val) {
      console.log("set name");
      name = val;
    },
    configurable: true 
});

delete obj1.name;
console.log(obj1.name); // undefined

可以看到name属性被删除了

而使用getter创建的属性,无法被删除

delete obj2.name; // true
// 虽然返回的是true,但是值仍然存在
console.log(obj2.name); // zhangsan

区别三:定义的时机

defineProperty可以在现有对象上定义,而settergetter只能在类或者对象初始化的时候定义。

区别四:定义的位置

当定义在类上的时候,属性在实例的原型上,这也是无法通过delete删除属性的原因。 而defineProperty将属性定义在实例自身上。

console.log(Object.getOwnPropertyDescriptor(obj1, "name")); // {enumerable: false, configurable: true, get: ƒ, set: ƒ}
console.log(Object.getOwnPropertyDescriptor(obj2, "name")); // undefined

……欢迎补充

参考资料

# vuejs/core

# 类

# getter

# defineProperty