Skip to content

Commit

Permalink
Workletizable Context Objects (#6284)
Browse files Browse the repository at this point in the history
This pull request introduces the concept of _Context Objects_ on the
Worklet Runtime.

## What?

Context Object is a shareable object that doesn't lose its `this`
binding when it goes to the Worklet Runtime. To define an object as a
Context Object, the user has to define property `__workletContextObject`
on it. The value of this property doesn't matter.

```ts
// Explicit Context Object
const obj = {
  foo() { return 1; },
  bar() { return this.foo(); },
  __workletContextObject: true,
}
```

Context Objects are also auto-detected when Babel processes files marked
with a worklet directive. If a top level object has a method that
references `this` inside we treat is as a Context Object.

```ts
'worklet';
// Implicit Context Object
const obj = {
  foo() { return 1; },
  bar() { return this.foo(); },
};
```

## Why?

Currently it's not possible to refer to an object's methods from its
other methods.

```ts
const obj = {
  foo() {
    console.log("foo");
  }
  bar() {
    this.foo();
  }
}

runOnUI(() => {
  obj.bar(); // Crash, the binding is lost on serialization.
})();
```

## How?

Context Objects are handled as another type of shareable entity on the
React Runtime.
1. The plugin adds a special property `__workletObjectFactory` to
Context Objects. It's a function that creates an identical object.
2. We serialize the factory instead of the object itself.
3. We recreate the object on the Worklet Runtime via ShareableHandle
mechanism.

## Test plan

- [x] Add unit tests
- [x] Add a runtime test suite
  • Loading branch information
tjzel authored Jul 17, 2024
1 parent 054ee4e commit 922fd12
Show file tree
Hide file tree
Showing 12 changed files with 884 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,10 @@ export default function RuntimeTestsExample() {
},
},
{
testSuiteName: 'babelPlugin',
testSuiteName: 'babel plugin',
importTest: () => {
require('./tests/plugin/fileWorkletization.test');
require('./tests/plugin/contextObjects.test');
},
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import React, { useEffect } from 'react';
import { View } from 'react-native';
import { useSharedValue, runOnUI } from 'react-native-reanimated';
import {
render,
wait,
describe,
getRegisteredValue,
registerValue,
test,
expect,
} from '../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';

const SHARED_VALUE_REF = 'SHARED_VALUE_REF';

describe('Test context objects', () => {
test('non-context methods are workletized', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo() {
return 1;
},
__workletContextObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.foo();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(1);
});

test('non-context properties are workletized', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo: () => 1,
__workletContextObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.foo();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(1);
});

test('methods preserve implicit context', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo() {
return 1;
},
bar() {
return this.foo() + 1;
},
__workletContextObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.bar();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
});

test('methods preserve explicit context', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo() {
return 1;
},
bar() {
return this.foo.call(contextObject) + 1;
},
__workletContextObject: true,
};

useEffect(() => {
runOnUI(() => {
output.value = contextObject.bar();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
});

test('methods change the state of the object', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo: 1,
bar() {
this.foo += 1;
},
__workletContextObject: true,
};

useEffect(() => {
runOnUI(() => {
contextObject.bar();
output.value = contextObject.foo;
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
});

test("the object doesn't persist in memory", async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
const contextObject = {
foo: 1,
bar() {
this.foo += 1;
},
__workletContextObject: true,
};

useEffect(() => {
runOnUI(() => {
contextObject.bar();
output.value = contextObject.foo;
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(2);
await render(<ExampleComponent />);
await wait(100);
const sharedValue2 = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue2.onUI).toBe(2);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,46 @@ import {
test,
expect,
} from '../../ReanimatedRuntimeTestsRunner/RuntimeTestsApi';
import { getThree } from './fileWorkletization';
import { getThree, implicitContextObject } from './fileWorkletization';

const SHARED_VALUE_REF = 'SHARED_VALUE_REF';

describe('Test workletization', () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);
describe('Test file workletization', () => {
test('Functions and methods are workletized', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);

useEffect(() => {
runOnUI(() => {
output.value = getThree();
})();
});
useEffect(() => {
runOnUI(() => {
output.value = getThree();
})();
});

return <View />;
};

test('Test file workletization', async () => {
return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(3);
});

test('WorkletContextObjects are workletized', async () => {
const ExampleComponent = () => {
const output = useSharedValue<number | null>(null);
registerValue(SHARED_VALUE_REF, output);

useEffect(() => {
runOnUI(() => {
output.value = implicitContextObject.getFive();
})();
});

return <View />;
};
await render(<ExampleComponent />);
await wait(100);
const sharedValue = await getRegisteredValue(SHARED_VALUE_REF);
expect(sharedValue.onUI).toBe(5);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ const getterContainer = {
export const getThree = () => {
return getOne() + getterContainer.getTwo();
};

export const implicitContextObject = {
getFour() {
return 4;
},
getFive() {
return this.getFour() + 1;
},
};
Loading

0 comments on commit 922fd12

Please sign in to comment.