Skip to content

Commit

Permalink
perf(reactivity): improve ref performance by using class-based implem…
Browse files Browse the repository at this point in the history
…entation (#1900)
  • Loading branch information
RobbinBaauw authored Aug 21, 2020
1 parent 0f8c991 commit 07919e0
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 74 deletions.
81 changes: 47 additions & 34 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { effect, ReactiveEffect, trigger, track } from './effect'
import { TriggerOpTypes, TrackOpTypes } from './operations'
import { Ref } from './ref'
import { isFunction, NOOP } from '@vue/shared'
import { ReactiveFlags } from './reactive'
import { ReactiveFlags, toRaw } from './reactive'

export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T
Expand All @@ -20,6 +20,47 @@ export interface WritableComputedOptions<T> {
set: ComputedSetter<T>
}

class ComputedRefImpl<T> {
private _value!: T
private _dirty = true

public readonly effect: ReactiveEffect<T>

public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean

constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})

this[ReactiveFlags.IS_READONLY] = isReadonly
}

get value() {
if (this._dirty) {
this._value = this.effect()
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}

set value(newValue: T) {
this._setter(newValue)
}
}

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>
Expand All @@ -42,37 +83,9 @@ export function computed<T>(
setter = getterOrOptions.set
}

let dirty = true
let value: T
let computed: ComputedRef<T>

const runner = effect(getter, {
lazy: true,
scheduler: () => {
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})
computed = {
__v_isRef: true,
[ReactiveFlags.IS_READONLY]:
isFunction(getterOrOptions) || !getterOrOptions.set,

// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
}
track(computed, TrackOpTypes.GET, 'value')
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any
return computed
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
}
105 changes: 65 additions & 40 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const convert = <T extends unknown>(val: T): T =>

export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return r ? r.__v_isRef === true : false
return Boolean(r && r.__v_isRef === true)
}

export function ref<T extends object>(
Expand All @@ -44,26 +44,34 @@ export function shallowRef(value?: unknown) {
return createRef(value, true)
}

class RefImpl<T> {
private _value: T

public readonly __v_isRef = true

constructor(private _rawValue: T, private readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}

get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}

set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}

function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
let value = shallow ? rawValue : convert(rawValue)
const r = {
__v_isRef: true,
get value() {
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
trigger(r, TriggerOpTypes.SET, 'value', newVal)
}
}
}
return r
return new RefImpl(rawValue, shallow)
}

export function triggerRef(ref: Ref) {
Expand Down Expand Up @@ -103,21 +111,32 @@ export type CustomRefFactory<T> = (
set: (value: T) => void
}

export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
const { get, set } = factory(
() => track(r, TrackOpTypes.GET, 'value'),
() => trigger(r, TriggerOpTypes.SET, 'value')
)
const r = {
__v_isRef: true,
get value() {
return get()
},
set value(v) {
set(v)
}
class CustomRefImpl<T> {
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']

public readonly __v_isRef = true

constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory(
() => track(this, TrackOpTypes.GET, 'value'),
() => trigger(this, TriggerOpTypes.SET, 'value')
)
this._get = get
this._set = set
}

get value() {
return this._get()
}

set value(newVal) {
this._set(newVal)
}
return r as any
}

export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
return new CustomRefImpl(factory) as any
}

export function toRefs<T extends object>(object: T): ToRefs<T> {
Expand All @@ -131,19 +150,25 @@ export function toRefs<T extends object>(object: T): ToRefs<T> {
return ret
}

class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true

constructor(private readonly _object: T, private readonly _key: K) {}

get value() {
return this._object[this._key]
}

set value(newVal) {
this._object[this._key] = newVal
}
}

export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {
return {
__v_isRef: true,
get value(): any {
return object[key]
},
set value(newVal) {
object[key] = newVal
}
} as any
return new ObjectRefImpl(object, key) as any
}

// corner case when use narrows type
Expand Down

0 comments on commit 07919e0

Please sign in to comment.