├── src ├── apis │ ├── state.ts │ ├── computed.ts │ ├── lifecycle.ts │ ├── inject.ts │ └── watch.ts ├── reactivity │ ├── index.ts │ ├── set.ts │ ├── reactive.ts │ └── ref.ts ├── component │ ├── index.ts │ ├── componentProps.ts │ └── component.ts ├── types │ └── basic.ts ├── vmStateManager.ts ├── symbols.ts ├── env.d.ts ├── runtimeContext.ts ├── createElement.ts ├── index.ts ├── install.ts ├── helper.ts ├── utils.ts └── setup.ts ├── .gitignore ├── test ├── setupTest.js ├── helpers │ └── wait-for-update.js ├── templateRefs.spec.js ├── apis │ ├── inject.spec.js │ ├── computed.spec.js │ ├── state.spec.js │ ├── lifecycle.spec.js │ └── watch.spec.js ├── ssr │ └── serverPrefetch.spec.js ├── setupContext.spec.js ├── types │ └── defineComponent.spec.ts └── setup.spec.js ├── .github └── workflows │ └── nodejs.yml ├── tsconfig.json ├── scripts └── release.sh ├── rollup.config.js ├── package.json ├── CHANGELOG.md ├── README.zh-CN.md └── README.md /src/apis/state.ts: -------------------------------------------------------------------------------- 1 | export { reactive, ref, Ref, isRef, toRefs, set } from '../reactivity'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | *.log 5 | dist 6 | lib 7 | .vscode 8 | .idea 9 | .rpt2_cache 10 | TODO.md 11 | -------------------------------------------------------------------------------- /src/reactivity/index.ts: -------------------------------------------------------------------------------- 1 | export { reactive, isReactive, nonReactive } from './reactive'; 2 | export { ref, isRef, Ref, createRef, UnwrapRef, toRefs } from './ref'; 3 | export { set } from './set'; 4 | -------------------------------------------------------------------------------- /src/component/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | Data, 3 | createComponent, 4 | defineComponent, 5 | SetupFunction, 6 | SetupContext, 7 | ComponentInstance, 8 | ComponentRenderProxy, 9 | } from './component'; 10 | export { PropType, PropOptions } from './componentProps'; 11 | -------------------------------------------------------------------------------- /test/setupTest.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common'); 2 | const FullVue = require('vue/dist/vue.runtime.common'); 3 | const plugin = require('../src').default; 4 | 5 | FullVue.config.productionTip = false; 6 | FullVue.config.devtools = false; 7 | Vue.config.productionTip = false; 8 | Vue.config.devtools = false; 9 | Vue.use(plugin); 10 | -------------------------------------------------------------------------------- /src/types/basic.ts: -------------------------------------------------------------------------------- 1 | export type AnyObject = Record; 2 | 3 | // Conditional returns can enforce identical types. 4 | // See here: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650 5 | // prettier-ignore 6 | type Equal = 7 | (() => U extends Left ? 1 : 0) extends (() => U extends Right ? 1 : 0) ? true : false; 8 | 9 | export type HasDefined = Equal extends true ? false : true; 10 | -------------------------------------------------------------------------------- /src/vmStateManager.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInstance, Data } from './component'; 2 | 3 | export interface VfaState { 4 | refs?: string[]; 5 | rawBindings?: Data; 6 | slots?: string[]; 7 | } 8 | 9 | function set(vm: ComponentInstance, key: K, value: VfaState[K]): void { 10 | const state = (vm.__secret_vfa_state__ = vm.__secret_vfa_state__ || {}); 11 | state[key] = value; 12 | } 13 | 14 | function get(vm: ComponentInstance, key: K): VfaState[K] | undefined { 15 | return (vm.__secret_vfa_state__ || {})[key]; 16 | } 17 | 18 | export default { 19 | set, 20 | get, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [10.x, 12.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: npm install, build, and test 24 | run: | 25 | npm install 26 | npm run build --if-present 27 | npm test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "esnext"], 5 | "declaration": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "strictNullChecks": true, 12 | "strictFunctionTypes": true, 13 | "noImplicitAny": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "importHelpers": true, 18 | "stripInternal": true, 19 | "downlevelIteration": true, 20 | "noUnusedLocals": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /src/symbols.ts: -------------------------------------------------------------------------------- 1 | import { hasSymbol } from './utils'; 2 | 3 | function createSymbol(name: string): string { 4 | return hasSymbol ? (Symbol.for(name) as any) : name; 5 | } 6 | 7 | export const WatcherPreFlushQueueKey = createSymbol('vfa.key.preFlushQueue'); 8 | export const WatcherPostFlushQueueKey = createSymbol('vfa.key.postFlushQueue'); 9 | export const AccessControlIdentifierKey = createSymbol('vfa.key.accessControlIdentifier'); 10 | export const ReactiveIdentifierKey = createSymbol('vfa.key.reactiveIdentifier'); 11 | export const NonReactiveIdentifierKey = createSymbol('vfa.key.nonReactiveIdentifier'); 12 | 13 | // must be a string, symbol key is ignored in reactive 14 | export const RefKey = 'vfa.key.refKey'; 15 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import { VfaState } from './vmStateManager'; 3 | import { VueWatcher } from './apis/watch'; 4 | 5 | declare global { 6 | interface Window { 7 | Vue: VueConstructor; 8 | } 9 | } 10 | 11 | declare module 'vue/types/vue' { 12 | interface Vue { 13 | readonly _uid: number; 14 | readonly _data: Record; 15 | _watchers: VueWatcher[]; 16 | __secret_vfa_state__?: VfaState; 17 | } 18 | 19 | interface VueConstructor { 20 | observable(x: any): T; 21 | util: { 22 | warn(msg: string, vm?: Vue); 23 | defineReactive( 24 | obj: Object, 25 | key: string, 26 | val: any, 27 | customSetter?: Function, 28 | shallow?: boolean 29 | ); 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [[ -z $1 ]]; then 5 | echo "Enter new version: " 6 | read -r VERSION 7 | else 8 | VERSION=$1 9 | fi 10 | 11 | read -p "Releasing $VERSION - are you sure? (y/n) " -n 1 -r 12 | echo 13 | if [[ $REPLY =~ ^[Yy]$ ]]; then 14 | echo "Releasing $VERSION ..." 15 | 16 | if [[ -z $SKIP_TESTS ]]; then 17 | npm run test 18 | fi 19 | 20 | # build 21 | VERSION=$VERSION npm run build 22 | 23 | # commit 24 | # git add -A 25 | # git commit -m "build: build $VERSION" 26 | # tag version 27 | npm version "$VERSION" --message "build: release $VERSION" 28 | 29 | # publish 30 | git push origin refs/tags/v"$VERSION" 31 | git push 32 | if [[ -z $RELEASE_TAG ]]; then 33 | npm publish 34 | else 35 | npm publish --tag "$RELEASE_TAG" 36 | fi 37 | fi 38 | -------------------------------------------------------------------------------- /src/runtimeContext.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import { ComponentInstance } from './component'; 3 | import { assert } from './utils'; 4 | 5 | let currentVue: VueConstructor | null = null; 6 | let currentVM: ComponentInstance | null = null; 7 | 8 | export function getCurrentVue(): VueConstructor { 9 | if (process.env.NODE_ENV !== 'production') { 10 | assert(currentVue, `must call Vue.use(plugin) before using any function.`); 11 | } 12 | 13 | return currentVue!; 14 | } 15 | 16 | export function setCurrentVue(vue: VueConstructor) { 17 | currentVue = vue; 18 | } 19 | 20 | export function getCurrentVM(): ComponentInstance | null { 21 | return currentVM; 22 | } 23 | 24 | export function setCurrentVM(vm: ComponentInstance | null) { 25 | currentVM = vm; 26 | } 27 | 28 | export { currentVue, currentVM }; 29 | -------------------------------------------------------------------------------- /src/createElement.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { currentVM, getCurrentVue } from './runtimeContext'; 3 | import { defineComponentInstance } from './helper'; 4 | import { warn } from './utils'; 5 | 6 | type CreateElement = Vue['$createElement']; 7 | 8 | let fallbackCreateElement: CreateElement; 9 | 10 | const createElement: CreateElement = function createElement(...args: any) { 11 | if (!currentVM) { 12 | warn('`createElement()` has been called outside of render function.'); 13 | if (!fallbackCreateElement) { 14 | fallbackCreateElement = defineComponentInstance(getCurrentVue()).$createElement; 15 | } 16 | 17 | return fallbackCreateElement.apply(fallbackCreateElement, args); 18 | } 19 | 20 | return currentVM.$createElement.apply(currentVM, args); 21 | } as any; 22 | 23 | export default createElement; 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VueConstructor } from 'vue'; 2 | import { Data, SetupFunction, SetupContext } from './component'; 3 | import { currentVue } from './runtimeContext'; 4 | import { install } from './install'; 5 | import { mixin } from './setup'; 6 | 7 | declare module 'vue/types/options' { 8 | interface ComponentOptions { 9 | setup?: SetupFunction; 10 | } 11 | } 12 | 13 | const _install = (Vue: VueConstructor) => install(Vue, mixin); 14 | const plugin = { 15 | install: _install, 16 | }; 17 | // Auto install if it is not done yet and `window` has `Vue`. 18 | // To allow users to avoid auto-installation in some cases, 19 | if (currentVue && typeof window !== 'undefined' && window.Vue) { 20 | _install(window.Vue); 21 | } 22 | 23 | export default plugin; 24 | export { default as createElement } from './createElement'; 25 | export { SetupContext }; 26 | export { 27 | createComponent, 28 | defineComponent, 29 | ComponentRenderProxy, 30 | PropType, 31 | PropOptions, 32 | } from './component'; 33 | // For getting a hold of the interal instance in setup() - useful for advanced 34 | // plugins 35 | export { getCurrentVM as getCurrentInstance } from './runtimeContext'; 36 | 37 | export * from './apis/state'; 38 | export * from './apis/lifecycle'; 39 | export * from './apis/watch'; 40 | export * from './apis/computed'; 41 | export * from './apis/inject'; 42 | -------------------------------------------------------------------------------- /src/apis/computed.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentVue, getCurrentVM } from '../runtimeContext'; 2 | import { createRef, Ref } from '../reactivity'; 3 | import { defineComponentInstance } from '../helper'; 4 | import { warn } from '../utils'; 5 | 6 | interface Option { 7 | get: () => T; 8 | set: (value: T) => void; 9 | } 10 | 11 | // read-only 12 | export function computed(getter: Option['get']): Readonly>>; 13 | // writable 14 | export function computed(options: Option): Ref>; 15 | // implement 16 | export function computed( 17 | options: Option['get'] | Option 18 | ): Readonly>> | Ref> { 19 | const vm = getCurrentVM(); 20 | let get: Option['get'], set: Option['set'] | undefined; 21 | if (typeof options === 'function') { 22 | get = options; 23 | } else { 24 | get = options.get; 25 | set = options.set; 26 | } 27 | 28 | const computedHost = defineComponentInstance(getCurrentVue(), { 29 | computed: { 30 | $$state: { 31 | get, 32 | set, 33 | }, 34 | }, 35 | }); 36 | 37 | return createRef({ 38 | get: () => (computedHost as any).$$state, 39 | set: (v: T) => { 40 | if (process.env.NODE_ENV !== 'production' && !set) { 41 | warn('Computed property was assigned to but it has no setter.', vm!); 42 | return; 43 | } 44 | 45 | (computedHost as any).$$state = v; 46 | }, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/apis/lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import { ComponentInstance } from '../component'; 3 | import { getCurrentVue } from '../runtimeContext'; 4 | import { ensureCurrentVMInFn } from '../helper'; 5 | 6 | const genName = (name: string) => `on${name[0].toUpperCase() + name.slice(1)}`; 7 | function createLifeCycle(lifeCyclehook: string) { 8 | return (callback: Function) => { 9 | const vm = ensureCurrentVMInFn(genName(lifeCyclehook)); 10 | injectHookOption(getCurrentVue(), vm, lifeCyclehook, callback); 11 | }; 12 | } 13 | 14 | function injectHookOption(Vue: VueConstructor, vm: ComponentInstance, hook: string, val: Function) { 15 | const options = vm.$options as any; 16 | const mergeFn = Vue.config.optionMergeStrategies[hook]; 17 | options[hook] = mergeFn(options[hook], val); 18 | } 19 | 20 | // export const onCreated = createLifeCycle('created'); 21 | export const onBeforeMount = createLifeCycle('beforeMount'); 22 | export const onMounted = createLifeCycle('mounted'); 23 | export const onBeforeUpdate = createLifeCycle('beforeUpdate'); 24 | export const onUpdated = createLifeCycle('updated'); 25 | export const onBeforeUnmount = createLifeCycle('beforeDestroy'); 26 | export const onUnmounted = createLifeCycle('destroyed'); 27 | export const onErrorCaptured = createLifeCycle('errorCaptured'); 28 | export const onActivated = createLifeCycle('activated'); 29 | export const onDeactivated = createLifeCycle('deactivated'); 30 | export const onServerPrefetch = createLifeCycle('serverPrefetch'); 31 | -------------------------------------------------------------------------------- /src/apis/inject.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInstance } from '../component'; 2 | import { ensureCurrentVMInFn } from '../helper'; 3 | import { hasOwn, warn } from '../utils'; 4 | 5 | const NOT_FOUND = {}; 6 | export interface InjectionKey extends Symbol {} 7 | 8 | function resolveInject(provideKey: InjectionKey, vm: ComponentInstance): any { 9 | let source = vm; 10 | while (source) { 11 | // @ts-ignore 12 | if (source._provided && hasOwn(source._provided, provideKey)) { 13 | //@ts-ignore 14 | return source._provided[provideKey]; 15 | } 16 | source = source.$parent; 17 | } 18 | 19 | return NOT_FOUND; 20 | } 21 | 22 | export function provide(key: InjectionKey | string, value: T): void { 23 | const vm: any = ensureCurrentVMInFn('provide'); 24 | if (!vm._provided) { 25 | const provideCache = {}; 26 | Object.defineProperty(vm, '_provided', { 27 | get: () => provideCache, 28 | set: v => Object.assign(provideCache, v), 29 | }); 30 | } 31 | 32 | vm._provided[key as string] = value; 33 | } 34 | 35 | export function inject(key: InjectionKey | string): T | undefined; 36 | export function inject(key: InjectionKey | string, defaultValue: T): T; 37 | export function inject(key: InjectionKey | string, defaultValue?: T): T | undefined { 38 | if (!key) { 39 | return defaultValue; 40 | } 41 | 42 | const vm = ensureCurrentVMInFn('inject'); 43 | const val = resolveInject(key as InjectionKey, vm); 44 | if (val !== NOT_FOUND) { 45 | return val; 46 | } else { 47 | if (defaultValue === undefined && process.env.NODE_ENV !== 'production') { 48 | warn(`Injection "${String(key)}" not found`, vm); 49 | } 50 | return defaultValue; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/component/componentProps.ts: -------------------------------------------------------------------------------- 1 | import { Data } from './component'; 2 | 3 | export type ComponentPropsOptions

= { 4 | [K in keyof P]: Prop | null; 5 | }; 6 | 7 | type Prop = PropOptions | PropType; 8 | 9 | export interface PropOptions { 10 | type?: PropType | null; 11 | required?: Required; 12 | default?: T | null | undefined | (() => T | null | undefined); 13 | validator?(value: any): boolean; 14 | } 15 | 16 | export type PropType = PropConstructor | PropConstructor[]; 17 | 18 | type PropConstructor = 19 | | { new (...args: any[]): T & object } 20 | | { (): T } 21 | | { new (...args: string[]): Function }; 22 | 23 | type RequiredKeys = { 24 | [K in keyof T]: T[K] extends 25 | | { required: true } 26 | | (MakeDefaultRequired extends true ? { default: any } : never) 27 | ? K 28 | : never; 29 | }[keyof T]; 30 | 31 | type OptionalKeys = Exclude>; 32 | 33 | // prettier-ignore 34 | type InferPropType = T extends null 35 | ? any // null & true would fail to infer 36 | : T extends { type: null } 37 | ? any // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` 38 | : T extends ObjectConstructor | { type: ObjectConstructor } 39 | ? { [key: string]: any } 40 | : T extends Prop 41 | ? V 42 | : T; 43 | 44 | // prettier-ignore 45 | export type ExtractPropTypes = { 46 | readonly [K in RequiredKeys]: InferPropType; 47 | } & { 48 | readonly [K in OptionalKeys]?: InferPropType; 49 | }; 50 | -------------------------------------------------------------------------------- /test/helpers/wait-for-update.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue'); 2 | 3 | // helper for async assertions. 4 | // Use like this: 5 | // 6 | // vm.a = 123 7 | // waitForUpdate(() => { 8 | // expect(vm.$el.textContent).toBe('123') 9 | // vm.a = 234 10 | // }) 11 | // .then(() => { 12 | // // more assertions... 13 | // }) 14 | // .then(done) 15 | window.waitForUpdate = initialCb => { 16 | let end; 17 | const queue = initialCb ? [initialCb] : []; 18 | 19 | function shift() { 20 | const job = queue.shift(); 21 | if (queue.length) { 22 | let hasError = false; 23 | try { 24 | job.wait ? job(shift) : job(); 25 | } catch (e) { 26 | hasError = true; 27 | const done = queue[queue.length - 1]; 28 | if (done && done.fail) { 29 | done.fail(e); 30 | } 31 | } 32 | if (!hasError && !job.wait) { 33 | if (queue.length) { 34 | Vue.nextTick(shift); 35 | } 36 | } 37 | } else if (job && (job.fail || job === end)) { 38 | job(); // done 39 | } 40 | } 41 | 42 | Vue.nextTick(() => { 43 | if (!queue.length || (!end && !queue[queue.length - 1].fail)) { 44 | throw new Error('waitForUpdate chain is missing .then(done)'); 45 | } 46 | shift(); 47 | }); 48 | 49 | const chainer = { 50 | then: nextCb => { 51 | queue.push(nextCb); 52 | return chainer; 53 | }, 54 | thenWaitFor: wait => { 55 | if (typeof wait === 'number') { 56 | wait = timeout(wait); 57 | } 58 | wait.wait = true; 59 | queue.push(wait); 60 | return chainer; 61 | }, 62 | end: endFn => { 63 | queue.push(endFn); 64 | end = endFn; 65 | }, 66 | }; 67 | 68 | return chainer; 69 | }; 70 | 71 | function timeout(n) { 72 | return next => setTimeout(next, n); 73 | } 74 | -------------------------------------------------------------------------------- /src/install.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject } from './types/basic'; 2 | import { hasSymbol, hasOwn, isPlainObject, assert } from './utils'; 3 | import { isRef } from './reactivity'; 4 | import { setCurrentVue, currentVue } from './runtimeContext'; 5 | import { VueConstructor } from 'vue'; 6 | 7 | /** 8 | * Helper that recursively merges two data objects together. 9 | */ 10 | function mergeData(from: AnyObject, to: AnyObject): Object { 11 | if (!from) return to; 12 | let key: any; 13 | let toVal: any; 14 | let fromVal: any; 15 | 16 | const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from); 17 | 18 | for (let i = 0; i < keys.length; i++) { 19 | key = keys[i]; 20 | // in case the object is already observed... 21 | if (key === '__ob__') continue; 22 | toVal = to[key]; 23 | fromVal = from[key]; 24 | if (!hasOwn(to, key)) { 25 | to[key] = fromVal; 26 | } else if ( 27 | toVal !== fromVal && 28 | (isPlainObject(toVal) && !isRef(toVal)) && 29 | (isPlainObject(fromVal) && !isRef(fromVal)) 30 | ) { 31 | mergeData(fromVal, toVal); 32 | } 33 | } 34 | return to; 35 | } 36 | 37 | export function install(Vue: VueConstructor, _install: (Vue: VueConstructor) => void) { 38 | if (currentVue && currentVue === Vue) { 39 | if (process.env.NODE_ENV !== 'production') { 40 | assert(false, 'already installed. Vue.use(plugin) should be called only once'); 41 | } 42 | return; 43 | } 44 | 45 | Vue.config.optionMergeStrategies.setup = function(parent: Function, child: Function) { 46 | return function mergedSetupFn(props: any, context: any) { 47 | return mergeData( 48 | typeof parent === 'function' ? parent(props, context) || {} : {}, 49 | typeof child === 'function' ? child(props, context) || {} : {} 50 | ); 51 | }; 52 | }; 53 | 54 | setCurrentVue(Vue); 55 | _install(Vue); 56 | } 57 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | // import filesize from 'rollup-plugin-filesize'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import resolve from 'rollup-plugin-node-resolve'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import replace from 'rollup-plugin-replace'; 7 | 8 | const builds = { 9 | 'cjs-dev': { 10 | outFile: 'vue-composition-api.js', 11 | format: 'cjs', 12 | mode: 'development', 13 | }, 14 | 'cjs-prod': { 15 | outFile: 'vue-composition-api.min.js', 16 | format: 'cjs', 17 | mode: 'production', 18 | }, 19 | 'umd-dev': { 20 | outFile: 'vue-composition-api.umd.js', 21 | format: 'umd', 22 | mode: 'development', 23 | }, 24 | 'umd-prod': { 25 | outFile: 'vue-composition-api.umd.min.js', 26 | format: 'umd', 27 | mode: 'production', 28 | }, 29 | es: { 30 | outFile: 'vue-composition-api.module.js', 31 | format: 'es', 32 | mode: 'development', 33 | }, 34 | }; 35 | 36 | function getAllBuilds() { 37 | return Object.keys(builds).map(key => genConfig(builds[key])); 38 | } 39 | 40 | function genConfig({ outFile, format, mode }) { 41 | const isProd = mode === 'production'; 42 | return { 43 | input: './src/index.ts', 44 | output: { 45 | file: path.join('./dist', outFile), 46 | format: format, 47 | globals: { 48 | vue: 'Vue', 49 | }, 50 | exports: 'named', 51 | name: format === 'umd' ? 'vueCompositionApi' : undefined, 52 | }, 53 | external: ['vue'], 54 | plugins: [ 55 | typescript({ 56 | typescript: require('typescript'), 57 | }), 58 | resolve(), 59 | replace({ 'process.env.NODE_ENV': JSON.stringify(isProd ? 'production' : 'development') }), 60 | isProd && terser(), 61 | ].filter(Boolean), 62 | }; 63 | } 64 | 65 | let buildConfig; 66 | 67 | if (process.env.TARGET) { 68 | buildConfig = genConfig(builds[process.env.TARGET]); 69 | } else { 70 | buildConfig = getAllBuilds(); 71 | } 72 | 73 | export default buildConfig; 74 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode, ComponentOptions, VueConstructor } from 'vue'; 2 | import { ComponentInstance } from './component'; 3 | import { currentVue, getCurrentVM } from './runtimeContext'; 4 | import { assert, warn } from './utils'; 5 | 6 | export function ensureCurrentVMInFn(hook: string): ComponentInstance { 7 | const vm = getCurrentVM(); 8 | if (process.env.NODE_ENV !== 'production') { 9 | assert(vm, `"${hook}" get called outside of "setup()"`); 10 | } 11 | return vm!; 12 | } 13 | 14 | export function defineComponentInstance( 15 | Ctor: VueConstructor, 16 | options: ComponentOptions = {} 17 | ) { 18 | const silent = Ctor.config.silent; 19 | Ctor.config.silent = true; 20 | const vm = new Ctor(options); 21 | Ctor.config.silent = silent; 22 | return vm; 23 | } 24 | 25 | export function isComponentInstance(obj: any) { 26 | return currentVue && obj instanceof currentVue; 27 | } 28 | 29 | export function createSlotProxy(vm: ComponentInstance, slotName: string) { 30 | return (...args: any) => { 31 | if (!vm.$scopedSlots[slotName]) { 32 | return warn(`slots.${slotName}() got called outside of the "render()" scope`, vm); 33 | } 34 | 35 | return vm.$scopedSlots[slotName]!.apply(vm, args); 36 | }; 37 | } 38 | 39 | export function resolveSlots( 40 | slots: { [key: string]: Function } | void, 41 | normalSlots: { [key: string]: VNode[] | undefined } 42 | ): { [key: string]: true } { 43 | let res: { [key: string]: true }; 44 | if (!slots) { 45 | res = {}; 46 | } else if (slots._normalized) { 47 | // fast path 1: child component re-render only, parent did not change 48 | return slots._normalized as any; 49 | } else { 50 | res = {}; 51 | for (const key in slots) { 52 | if (slots[key] && key[0] !== '$') { 53 | res[key] = true; 54 | } 55 | } 56 | } 57 | 58 | // expose normal slots on scopedSlots 59 | for (const key in normalSlots) { 60 | if (!(key in res)) { 61 | res[key] = true; 62 | } 63 | } 64 | 65 | return res; 66 | } 67 | -------------------------------------------------------------------------------- /src/reactivity/set.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentVue } from '../runtimeContext'; 2 | import { isArray } from '../utils'; 3 | import { defineAccessControl } from './reactive'; 4 | 5 | function isUndef(v: any): boolean { 6 | return v === undefined || v === null; 7 | } 8 | 9 | function isPrimitive(value: any): boolean { 10 | return ( 11 | typeof value === 'string' || 12 | typeof value === 'number' || 13 | // $flow-disable-line 14 | typeof value === 'symbol' || 15 | typeof value === 'boolean' 16 | ); 17 | } 18 | 19 | function isValidArrayIndex(val: any): boolean { 20 | const n = parseFloat(String(val)); 21 | return n >= 0 && Math.floor(n) === n && isFinite(val); 22 | } 23 | 24 | /** 25 | * Set a property on an object. Adds the new property, triggers change 26 | * notification and intercept it's subsequent access if the property doesn't 27 | * already exist. 28 | */ 29 | export function set(target: any, key: any, val: T): T { 30 | const Vue = getCurrentVue(); 31 | const { warn, defineReactive } = Vue.util; 32 | if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target))) { 33 | warn(`Cannot set reactive property on undefined, null, or primitive value: ${target}`); 34 | } 35 | if (isArray(target) && isValidArrayIndex(key)) { 36 | target.length = Math.max(target.length, key); 37 | target.splice(key, 1, val); 38 | return val; 39 | } 40 | if (key in target && !(key in Object.prototype)) { 41 | target[key] = val; 42 | return val; 43 | } 44 | const ob = target.__ob__; 45 | if (target._isVue || (ob && ob.vmCount)) { 46 | process.env.NODE_ENV !== 'production' && 47 | warn( 48 | 'Avoid adding reactive properties to a Vue instance or its root $data ' + 49 | 'at runtime - declare it upfront in the data option.' 50 | ); 51 | return val; 52 | } 53 | if (!ob) { 54 | target[key] = val; 55 | return val; 56 | } 57 | defineReactive(ob.value, key, val); 58 | // IMPORTANT: define access control before trigger watcher 59 | defineAccessControl(target, key, val); 60 | ob.dep.notify(); 61 | return val; 62 | } 63 | -------------------------------------------------------------------------------- /test/templateRefs.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { ref, watchEffect, createElement: h } = require('../src'); 3 | 4 | describe('ref', () => { 5 | it('should work', done => { 6 | let dummy; 7 | const vm = new Vue({ 8 | setup() { 9 | const ref1 = ref(null); 10 | watchEffect(() => { 11 | dummy = ref1.value; 12 | }); 13 | 14 | return { 15 | bar: ref1, 16 | }; 17 | }, 18 | template: `

19 | 20 |
`, 21 | components: { 22 | test: { 23 | id: 'test', 24 | template: '
test
', 25 | }, 26 | }, 27 | }).$mount(); 28 | waitForUpdate(() => { 29 | expect(dummy).toBe(vm.$refs.bar); 30 | }).then(done); 31 | }); 32 | 33 | it('should dynamically update refs', done => { 34 | const vm = new Vue({ 35 | setup() { 36 | const ref1 = ref(null); 37 | const ref2 = ref(null); 38 | watchEffect(() => { 39 | dummy1 = ref1.value; 40 | dummy2 = ref2.value; 41 | }); 42 | 43 | return { 44 | value: 'bar', 45 | bar: ref1, 46 | foo: ref2, 47 | }; 48 | }, 49 | template: '
', 50 | }).$mount(); 51 | waitForUpdate(() => { 52 | expect(dummy1).toBe(vm.$refs.bar); 53 | expect(dummy2).toBe(null); 54 | vm.value = 'foo'; 55 | }) 56 | .then(() => { 57 | // vm updated. ref update occures after updated; 58 | }) 59 | .then(() => { 60 | // no render cycle, empty tick 61 | }) 62 | .then(() => { 63 | expect(dummy1).toBe(null); 64 | expect(dummy2).toBe(vm.$refs.foo); 65 | }) 66 | .then(done); 67 | }); 68 | 69 | // TODO: how ? 70 | // it('work with createElement', () => { 71 | // let root; 72 | // const vm = new Vue({ 73 | // setup() { 74 | // root = ref(null); 75 | // return () => { 76 | // return h('div', { 77 | // ref: root, 78 | // }); 79 | // }; 80 | // }, 81 | // }).$mount(); 82 | // expect(root.value).toBe(vm.$el); 83 | // }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const toString = (x: any) => Object.prototype.toString.call(x); 4 | 5 | export function isNative (Ctor: any): boolean { 6 | return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) 7 | } 8 | 9 | export const hasSymbol = 10 | typeof Symbol !== 'undefined' && isNative(Symbol) && 11 | typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys) 12 | 13 | export const noopFn: any = (_: any) => _; 14 | 15 | const sharedPropertyDefinition = { 16 | enumerable: true, 17 | configurable: true, 18 | get: noopFn, 19 | set: noopFn, 20 | }; 21 | 22 | export function proxy(target: any, key: string, { get, set }: { get?: Function; set?: Function }) { 23 | sharedPropertyDefinition.get = get || noopFn; 24 | sharedPropertyDefinition.set = set || noopFn; 25 | Object.defineProperty(target, key, sharedPropertyDefinition); 26 | } 27 | 28 | export function def(obj: Object, key: string, val: any, enumerable?: boolean) { 29 | Object.defineProperty(obj, key, { 30 | value: val, 31 | enumerable: !!enumerable, 32 | writable: true, 33 | configurable: true, 34 | }); 35 | } 36 | 37 | const hasOwnProperty = Object.prototype.hasOwnProperty; 38 | export function hasOwn(obj: Object | any[], key: string): boolean { 39 | return hasOwnProperty.call(obj, key); 40 | } 41 | 42 | export function assert(condition: any, msg: string) { 43 | if (!condition) throw new Error(`[vue-composition-api] ${msg}`); 44 | } 45 | 46 | export function isArray(x: unknown): x is T[] { 47 | return Array.isArray(x); 48 | } 49 | 50 | export function isObject(val: unknown): val is Record { 51 | return val !== null && typeof val === 'object'; 52 | } 53 | 54 | export function isPlainObject(x: unknown): x is Record { 55 | return toString(x) === '[object Object]'; 56 | } 57 | 58 | export function isFunction(x: unknown): x is Function { 59 | return typeof x === 'function'; 60 | } 61 | 62 | export function warn(msg: string, vm?: Vue) { 63 | Vue.util.warn(msg, vm); 64 | } 65 | 66 | export function logError(err: Error, vm: Vue, info: string) { 67 | if (process.env.NODE_ENV !== 'production') { 68 | warn(`Error in ${info}: "${err.toString()}"`, vm); 69 | } 70 | if (typeof window !== 'undefined' && typeof console !== 'undefined') { 71 | console.error(err); 72 | } else { 73 | throw err; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue/composition-api", 3 | "version": "0.5.0", 4 | "description": "Provide logic composition capabilities for Vue.", 5 | "keywords": [ 6 | "vue", 7 | "composition-api", 8 | "function-api" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/vuejs/composition-api.git" 13 | }, 14 | "main": "dist/vue-composition-api.js", 15 | "umd:main": "dist/vue-composition-api.umd.js", 16 | "module": "dist/vue-composition-api.module.js", 17 | "typings": "dist/index.d.ts", 18 | "author": { 19 | "name": "liximomo", 20 | "email": "liximomo@gmail.com" 21 | }, 22 | "license": "MIT", 23 | "sideEffects": false, 24 | "files": [ 25 | "dist/" 26 | ], 27 | "scripts": { 28 | "start": "cross-env TARGET=es rollup -c -w", 29 | "build": "rollup -c", 30 | "test": "cross-env NODE_ENV=test jest", 31 | "release": "bash scripts/release.sh" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/vuejs/composition-api/issues" 35 | }, 36 | "homepage": "https://github.com/vuejs/composition-api#readme", 37 | "devDependencies": { 38 | "@types/jest": "^24.0.13", 39 | "@types/node": "^12.0.2", 40 | "cross-env": "^5.2.0", 41 | "husky": "^2.7.0", 42 | "jest": "^24.8.0", 43 | "lint-staged": "^8.2.1", 44 | "prettier": "^1.18.2", 45 | "rollup": "^1.12.0", 46 | "rollup-plugin-node-resolve": "^5.0.0", 47 | "rollup-plugin-replace": "^2.2.0", 48 | "rollup-plugin-terser": "^4.0.4", 49 | "rollup-plugin-typescript2": "^0.21.0", 50 | "ts-jest": "^24.0.2", 51 | "typescript": "^3.6.2", 52 | "vue": "^2.5.22", 53 | "vue-router": "^3.1.3", 54 | "vue-server-renderer": "^2.6.10" 55 | }, 56 | "peerDependencies": { 57 | "vue": "^2.5.22" 58 | }, 59 | "dependencies": { 60 | "tslib": "^1.9.3" 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "lint-staged" 65 | } 66 | }, 67 | "lint-staged": { 68 | "*.js": [ 69 | "prettier --write", 70 | "git add" 71 | ], 72 | "*.ts": [ 73 | "prettier --parser=typescript --write", 74 | "git add" 75 | ] 76 | }, 77 | "jest": { 78 | "verbose": true, 79 | "setupFiles": [ 80 | "/test/setupTest.js" 81 | ], 82 | "setupFilesAfterEnv": [ 83 | "/test/helpers/wait-for-update.js" 84 | ], 85 | "moduleFileExtensions": [ 86 | "ts", 87 | "js" 88 | ], 89 | "testMatch": [ 90 | "/test/**/*.spec.{js,ts}" 91 | ], 92 | "preset": "ts-jest" 93 | }, 94 | "prettier": { 95 | "printWidth": 100, 96 | "singleQuote": true, 97 | "trailingComma": "es5" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/apis/inject.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { inject, provide, ref, reactive } = require('../../src'); 3 | 4 | let injected; 5 | const injectedComp = { 6 | render() {}, 7 | setup() { 8 | return { 9 | foo: inject('foo'), 10 | bar: inject('bar'), 11 | }; 12 | }, 13 | created() { 14 | injected = [this.foo, this.bar]; 15 | }, 16 | }; 17 | 18 | beforeEach(() => { 19 | injected = null; 20 | }); 21 | 22 | describe('Hooks provide/inject', () => { 23 | beforeEach(() => { 24 | warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); 25 | }); 26 | afterEach(() => { 27 | warn.mockRestore(); 28 | }); 29 | 30 | it('should work', () => { 31 | new Vue({ 32 | template: ``, 33 | setup() { 34 | const count = ref(1); 35 | provide('foo', count); 36 | provide('bar', false); 37 | }, 38 | components: { 39 | child: { 40 | template: ``, 41 | components: { 42 | injectedComp, 43 | }, 44 | }, 45 | }, 46 | }).$mount(); 47 | 48 | expect(injected).toEqual([1, false]); 49 | }); 50 | 51 | it('should return a default value when inject not found', () => { 52 | let injected; 53 | new Vue({ 54 | template: ``, 55 | components: { 56 | child: { 57 | template: `
{{ msg }}
`, 58 | setup() { 59 | injected = inject('not-existed-inject-key', 'foo'); 60 | return { 61 | injected, 62 | }; 63 | }, 64 | }, 65 | }, 66 | }).$mount(); 67 | 68 | expect(injected).toBe('foo'); 69 | }); 70 | 71 | it('should work for ref value', done => { 72 | const Msg = Symbol(); 73 | const app = new Vue({ 74 | template: ``, 75 | setup() { 76 | provide(Msg, ref('hello')); 77 | }, 78 | components: { 79 | child: { 80 | template: `
{{ msg }}
`, 81 | setup() { 82 | return { 83 | msg: inject(Msg), 84 | }; 85 | }, 86 | }, 87 | }, 88 | }).$mount(); 89 | 90 | app.$children[0].msg = 'bar'; 91 | waitForUpdate(() => { 92 | expect(app.$el.textContent).toBe('bar'); 93 | }).then(done); 94 | }); 95 | 96 | it('should work for reactive value', done => { 97 | const State = Symbol(); 98 | let obj; 99 | const app = new Vue({ 100 | template: ``, 101 | setup() { 102 | provide(State, reactive({ msg: 'foo' })); 103 | }, 104 | components: { 105 | child: { 106 | template: `
{{ state.msg }}
`, 107 | setup() { 108 | obj = inject(State); 109 | return { 110 | state: obj, 111 | }; 112 | }, 113 | }, 114 | }, 115 | }).$mount(); 116 | expect(obj.msg).toBe('foo'); 117 | app.$children[0].state.msg = 'bar'; 118 | waitForUpdate(() => { 119 | expect(app.$el.textContent).toBe('bar'); 120 | }).then(done); 121 | }); 122 | 123 | it('should work when combined with 2.x provide option', () => { 124 | const State = Symbol(); 125 | let obj1; 126 | let obj2; 127 | new Vue({ 128 | template: ``, 129 | setup() { 130 | provide(State, { msg: 'foo' }); 131 | }, 132 | provide: { 133 | X: { msg: 'bar' }, 134 | }, 135 | components: { 136 | child: { 137 | setup() { 138 | obj1 = inject(State); 139 | obj2 = inject('X'); 140 | }, 141 | template: `
`, 142 | }, 143 | }, 144 | }).$mount(); 145 | expect(obj1.msg).toBe('foo'); 146 | expect(obj2.msg).toBe('bar'); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /test/ssr/serverPrefetch.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { createRenderer } = require('vue-server-renderer'); 3 | const { ref, onServerPrefetch, getCurrentInstance, provide, inject } = require('../../src'); 4 | 5 | function fetch(result) { 6 | return new Promise(resolve => { 7 | setTimeout(() => { 8 | resolve(result); 9 | }, 10); 10 | }); 11 | } 12 | 13 | describe('serverPrefetch', () => { 14 | it('should prefetch async operations before rendering', async () => { 15 | const app = new Vue({ 16 | setup() { 17 | const count = ref(0); 18 | 19 | onServerPrefetch(async () => { 20 | count.value = await fetch(42); 21 | }); 22 | 23 | return { 24 | count, 25 | }; 26 | }, 27 | render(h) { 28 | return h('div', this.count); 29 | }, 30 | }); 31 | 32 | const serverRenderer = createRenderer(); 33 | const html = await serverRenderer.renderToString(app); 34 | expect(html).toBe('
42
'); 35 | }); 36 | 37 | it('should prefetch many async operations before rendering', async () => { 38 | const app = new Vue({ 39 | setup() { 40 | const count = ref(0); 41 | const label = ref(''); 42 | 43 | onServerPrefetch(async () => { 44 | count.value = await fetch(42); 45 | }); 46 | 47 | onServerPrefetch(async () => { 48 | label.value = await fetch('meow'); 49 | }); 50 | 51 | return { 52 | count, 53 | label, 54 | }; 55 | }, 56 | render(h) { 57 | return h('div', [this.count, this.label]); 58 | }, 59 | }); 60 | 61 | const serverRenderer = createRenderer(); 62 | const html = await serverRenderer.renderToString(app); 63 | expect(html).toBe('
42meow
'); 64 | }); 65 | 66 | it('should pass ssrContext', async () => { 67 | const child = { 68 | setup(props, { ssrContext }) { 69 | const content = ref(); 70 | 71 | expect(ssrContext.foo).toBe('bar'); 72 | 73 | onServerPrefetch(async () => { 74 | content.value = await fetch(ssrContext.foo); 75 | }); 76 | 77 | return { 78 | content, 79 | }; 80 | }, 81 | render(h) { 82 | return h('div', this.content); 83 | }, 84 | }; 85 | 86 | const app = new Vue({ 87 | components: { 88 | child, 89 | }, 90 | render(h) { 91 | return h('child'); 92 | }, 93 | }); 94 | 95 | const serverRenderer = createRenderer(); 96 | const html = await serverRenderer.renderToString(app, { foo: 'bar' }); 97 | expect(html).toBe('
bar
'); 98 | }); 99 | 100 | it('should not share context', async () => { 101 | const instances = []; 102 | function createApp(context) { 103 | return new Vue({ 104 | setup() { 105 | const count = ref(0); 106 | 107 | onServerPrefetch(async () => { 108 | count.value = await fetch(context.result); 109 | }); 110 | 111 | instances.push(getCurrentInstance()); 112 | 113 | return { 114 | count, 115 | }; 116 | }, 117 | render(h) { 118 | return h('div', this.count); 119 | }, 120 | }); 121 | } 122 | 123 | const serverRenderer = createRenderer(); 124 | const promises = []; 125 | // Parallel requests 126 | for (let i = 1; i < 3; i++) { 127 | promises.push( 128 | new Promise(async resolve => { 129 | const app = createApp({ result: i }); 130 | const html = await serverRenderer.renderToString(app); 131 | expect(html).toBe(`
${i}
`); 132 | resolve(); 133 | }) 134 | ); 135 | } 136 | await Promise.all(promises); 137 | expect((instances[0] === instances[1]) === instances[2]).toBe(false); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/setupContext.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { ref, watch, createElement: h } = require('../src'); 3 | 4 | describe('setupContext', () => { 5 | it('should have proper properties', () => { 6 | let context; 7 | const vm = new Vue({ 8 | setup(_, ctx) { 9 | context = ctx; 10 | }, 11 | }); 12 | expect(context).toBeDefined(); 13 | expect('parent' in context).toBe(true); 14 | expect(context.root).toBe(vm.$root); 15 | expect(context.parent).toBe(vm.$parent); 16 | expect(context.slots).toBeDefined(); 17 | expect(context.attrs).toBe(vm.$attrs); 18 | expect(context.listeners).toBe(vm.$listeners); 19 | 20 | // CAUTION: this will be removed in 3.0 21 | expect(context.refs).toBe(vm.$refs); 22 | expect(typeof context.emit === 'function').toBe(true); 23 | }); 24 | 25 | it('slots should work in render function', () => { 26 | const vm = new Vue({ 27 | template: ` 28 | 29 | 32 | 35 | 36 | `, 37 | components: { 38 | test: { 39 | setup(_, { slots }) { 40 | return () => { 41 | return h('div', [slots.default(), slots.item()]); 42 | }; 43 | }, 44 | }, 45 | }, 46 | }).$mount(); 47 | expect(vm.$el.innerHTML).toBe('foomeh'); 48 | }); 49 | 50 | it('warn for slots calls outside of the render() function', () => { 51 | warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); 52 | 53 | new Vue({ 54 | template: ` 55 | 56 | 59 | 60 | `, 61 | components: { 62 | test: { 63 | setup(_, { slots }) { 64 | slots.default(); 65 | }, 66 | }, 67 | }, 68 | }).$mount(); 69 | expect(warn.mock.calls[0][0]).toMatch( 70 | 'slots.default() got called outside of the "render()" scope' 71 | ); 72 | warn.mockRestore(); 73 | }); 74 | 75 | it('staled slots should be removed', () => { 76 | const Child = { 77 | template: '
', 78 | }; 79 | const vm = new Vue({ 80 | components: { Child }, 81 | template: ` 82 | 83 | 86 | 87 | `, 88 | }).$mount(); 89 | expect(vm.$el.textContent).toMatch(`foo foo`); 90 | }); 91 | 92 | it('slots should be synchronized', done => { 93 | let slotKeys; 94 | const Foo = { 95 | setup(_, { slots }) { 96 | slotKeys = Object.keys(slots); 97 | return () => { 98 | slotKeys = Object.keys(slots); 99 | return h('div', [ 100 | slots.default && slots.default('from foo default'), 101 | slots.one && slots.one('from foo one'), 102 | slots.two && slots.two('from foo two'), 103 | slots.three && slots.three('from foo three'), 104 | ]); 105 | }; 106 | }, 107 | }; 108 | 109 | const vm = new Vue({ 110 | data: { 111 | a: 'one', 112 | b: 'two', 113 | }, 114 | template: ` 115 | 116 | 117 | 118 | 119 | `, 120 | components: { Foo }, 121 | }).$mount(); 122 | expect(slotKeys).toEqual(['one', 'two']); 123 | expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`a from foo one b from foo two`); 124 | vm.a = 'two'; 125 | vm.b = 'three'; 126 | waitForUpdate(() => { 127 | // expect(slotKeys).toEqual(['one', 'three']); 128 | expect(vm.$el.innerHTML.replace(/\s+/g, ' ')).toMatch(`a from foo two b from foo three `); 129 | }).then(done); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/component/component.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VueConstructor, VNode, ComponentOptions as Vue2ComponentOptions } from 'vue'; 2 | import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'; 3 | import { UnwrapRef } from '../reactivity'; 4 | import { HasDefined } from '../types/basic'; 5 | 6 | export type Data = { [key: string]: unknown }; 7 | 8 | export type ComponentInstance = InstanceType; 9 | 10 | // public properties exposed on the proxy, which is used as the render context 11 | // in templates (as `this` in the render option) 12 | export type ComponentRenderProxy

= { 13 | $data: S; 14 | $props: PublicProps; 15 | $attrs: Data; 16 | $refs: Data; 17 | $slots: Data; 18 | $root: ComponentInstance | null; 19 | $parent: ComponentInstance | null; 20 | $emit: (event: string, ...args: unknown[]) => void; 21 | } & P & 22 | S; 23 | 24 | // for Vetur and TSX support 25 | type VueConstructorProxy = VueConstructor & { 26 | new (...args: any[]): ComponentRenderProxy< 27 | ExtractPropTypes, 28 | UnwrapRef, 29 | ExtractPropTypes 30 | >; 31 | }; 32 | 33 | type VueProxy = Vue2ComponentOptions< 34 | Vue, 35 | UnwrapRef, 36 | never, 37 | never, 38 | PropsOptions, 39 | ExtractPropTypes 40 | > & 41 | VueConstructorProxy; 42 | 43 | export interface SetupContext { 44 | readonly attrs: Record; 45 | readonly slots: { [key: string]: (...args: any[]) => VNode[] }; 46 | readonly parent: ComponentInstance | null; 47 | readonly root: ComponentInstance; 48 | readonly listeners: { [key: string]: Function }; 49 | 50 | emit(event: string, ...args: any[]): void; 51 | } 52 | 53 | export type SetupFunction = ( 54 | this: void, 55 | props: Props, 56 | ctx: SetupContext 57 | ) => RawBindings | (() => VNode | null); 58 | 59 | interface ComponentOptionsWithProps< 60 | PropsOptions = ComponentPropsOptions, 61 | RawBindings = Data, 62 | Props = ExtractPropTypes 63 | > { 64 | props?: PropsOptions; 65 | setup?: SetupFunction; 66 | } 67 | 68 | interface ComponentOptionsWithoutProps { 69 | props?: undefined; 70 | setup?: SetupFunction; 71 | } 72 | 73 | // overload 1: object format with no props 74 | export function defineComponent( 75 | options: ComponentOptionsWithoutProps 76 | ): VueProxy; 77 | // overload 2: object format with object props declaration 78 | // see `ExtractPropTypes` in ./componentProps.ts 79 | export function defineComponent< 80 | Props, 81 | RawBindings = Data, 82 | PropsOptions extends ComponentPropsOptions = ComponentPropsOptions 83 | >( 84 | // prettier-ignore 85 | options: ( 86 | // prefer the provided Props, otherwise infer it from PropsOptions 87 | HasDefined extends true 88 | ? ComponentOptionsWithProps 89 | : ComponentOptionsWithProps) & 90 | Omit, keyof ComponentOptionsWithProps> 91 | ): VueProxy; 92 | // implementation, close to no-op 93 | export function defineComponent(options: any) { 94 | return options as any; 95 | } 96 | 97 | // createComponent is kept around for retro-compatibility 98 | // overload 1: object format with no props 99 | export function createComponent( 100 | options: ComponentOptionsWithoutProps 101 | ): VueProxy; 102 | // overload 2: object format with object props declaration 103 | // see `ExtractPropTypes` in ./componentProps.ts 104 | export function createComponent< 105 | Props, 106 | RawBindings = Data, 107 | PropsOptions extends ComponentPropsOptions = ComponentPropsOptions 108 | >( 109 | // prettier-ignore 110 | options: ( 111 | // prefer the provided Props, otherwise infer it from PropsOptions 112 | HasDefined extends true 113 | ? ComponentOptionsWithProps 114 | : ComponentOptionsWithProps) & 115 | Omit, keyof ComponentOptionsWithProps> 116 | ): VueProxy; 117 | // implementation, deferring to defineComponent, but logging a warning in dev mode 118 | export function createComponent(options: any) { 119 | if (process.env.NODE_ENV !== 'production') { 120 | Vue.util.warn('`createComponent` has been renamed to `defineComponent`.'); 121 | } 122 | return defineComponent(options); 123 | } 124 | -------------------------------------------------------------------------------- /src/reactivity/reactive.ts: -------------------------------------------------------------------------------- 1 | import { AnyObject } from '../types/basic'; 2 | import { getCurrentVue } from '../runtimeContext'; 3 | import { isPlainObject, def, hasOwn, warn } from '../utils'; 4 | import { isComponentInstance, defineComponentInstance } from '../helper'; 5 | import { 6 | AccessControlIdentifierKey, 7 | ReactiveIdentifierKey, 8 | NonReactiveIdentifierKey, 9 | RefKey, 10 | } from '../symbols'; 11 | import { isRef, UnwrapRef } from './ref'; 12 | 13 | const AccessControlIdentifier = {}; 14 | const ReactiveIdentifier = {}; 15 | const NonReactiveIdentifier = {}; 16 | 17 | function isNonReactive(obj: any): boolean { 18 | return ( 19 | hasOwn(obj, NonReactiveIdentifierKey) && obj[NonReactiveIdentifierKey] === NonReactiveIdentifier 20 | ); 21 | } 22 | 23 | export function isReactive(obj: any): boolean { 24 | return hasOwn(obj, ReactiveIdentifierKey) && obj[ReactiveIdentifierKey] === ReactiveIdentifier; 25 | } 26 | 27 | /** 28 | * Proxing property access of target. 29 | * We can do unwrapping and other things here. 30 | */ 31 | function setupAccessControl(target: AnyObject): void { 32 | if ( 33 | !isPlainObject(target) || 34 | isNonReactive(target) || 35 | Array.isArray(target) || 36 | isRef(target) || 37 | isComponentInstance(target) 38 | ) { 39 | return; 40 | } 41 | 42 | if ( 43 | hasOwn(target, AccessControlIdentifierKey) && 44 | target[AccessControlIdentifierKey] === AccessControlIdentifier 45 | ) { 46 | return; 47 | } 48 | 49 | if (Object.isExtensible(target)) { 50 | def(target, AccessControlIdentifierKey, AccessControlIdentifier); 51 | } 52 | const keys = Object.keys(target); 53 | for (let i = 0; i < keys.length; i++) { 54 | defineAccessControl(target, keys[i]); 55 | } 56 | } 57 | 58 | /** 59 | * Auto unwrapping when access property 60 | */ 61 | export function defineAccessControl(target: AnyObject, key: any, val?: any) { 62 | if (key === '__ob__') return; 63 | 64 | let getter: (() => any) | undefined; 65 | let setter: ((x: any) => void) | undefined; 66 | const property = Object.getOwnPropertyDescriptor(target, key); 67 | if (property) { 68 | if (property.configurable === false) { 69 | return; 70 | } 71 | getter = property.get; 72 | setter = property.set; 73 | if ((!getter || setter) /* not only have getter */ && arguments.length === 2) { 74 | val = target[key]; 75 | } 76 | } 77 | 78 | setupAccessControl(val); 79 | Object.defineProperty(target, key, { 80 | enumerable: true, 81 | configurable: true, 82 | get: function getterHandler() { 83 | const value = getter ? getter.call(target) : val; 84 | // if the key is equal to RefKey, skip the unwrap logic 85 | if (key !== RefKey && isRef(value)) { 86 | return value.value; 87 | } else { 88 | return value; 89 | } 90 | }, 91 | set: function setterHandler(newVal) { 92 | if (getter && !setter) return; 93 | 94 | const value = getter ? getter.call(target) : val; 95 | // If the key is equal to RefKey, skip the unwrap logic 96 | // If and only if "value" is ref and "newVal" is not a ref, 97 | // the assignment should be proxied to "value" ref. 98 | if (key !== RefKey && isRef(value) && !isRef(newVal)) { 99 | value.value = newVal; 100 | } else if (setter) { 101 | setter.call(target, newVal); 102 | } else { 103 | val = newVal; 104 | } 105 | setupAccessControl(newVal); 106 | }, 107 | }); 108 | } 109 | 110 | function observe(obj: T): T { 111 | const Vue = getCurrentVue(); 112 | let observed: T; 113 | if (Vue.observable) { 114 | observed = Vue.observable(obj); 115 | } else { 116 | const vm = defineComponentInstance(Vue, { 117 | data: { 118 | $$state: obj, 119 | }, 120 | }); 121 | observed = vm._data.$$state; 122 | } 123 | 124 | return observed; 125 | } 126 | /** 127 | * Make obj reactivity 128 | */ 129 | export function reactive(obj: T): UnwrapRef { 130 | if (process.env.NODE_ENV !== 'production' && !obj) { 131 | warn('"reactive()" is called without provide an "object".'); 132 | // @ts-ignore 133 | return; 134 | } 135 | 136 | if (!isPlainObject(obj) || isReactive(obj) || isNonReactive(obj) || !Object.isExtensible(obj)) { 137 | return obj as any; 138 | } 139 | 140 | const observed = observe(obj); 141 | def(observed, ReactiveIdentifierKey, ReactiveIdentifier); 142 | setupAccessControl(observed); 143 | return observed as UnwrapRef; 144 | } 145 | 146 | /** 147 | * Make sure obj can't be a reactive 148 | */ 149 | export function nonReactive(obj: T): T { 150 | if (!isPlainObject(obj)) { 151 | return obj; 152 | } 153 | 154 | // set the vue observable flag at obj 155 | def(obj, '__ob__', (observe({}) as any).__ob__); 156 | // mark as nonReactive 157 | def(obj, NonReactiveIdentifierKey, NonReactiveIdentifier); 158 | 159 | return obj; 160 | } 161 | -------------------------------------------------------------------------------- /test/apis/computed.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { ref, computed } = require('../../src'); 3 | 4 | describe('Hooks computed', () => { 5 | beforeEach(() => { 6 | warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); 7 | }); 8 | afterEach(() => { 9 | warn.mockRestore(); 10 | }); 11 | 12 | it('basic usage', done => { 13 | const vm = new Vue({ 14 | template: '

{{ b }}
', 15 | setup() { 16 | const a = ref(1); 17 | const b = computed(() => a.value + 1); 18 | return { 19 | a, 20 | b, 21 | }; 22 | }, 23 | }).$mount(); 24 | expect(vm.b).toBe(2); 25 | expect(vm.$el.textContent).toBe('2'); 26 | vm.a = 2; 27 | expect(vm.b).toBe(3); 28 | waitForUpdate(() => { 29 | expect(vm.$el.textContent).toBe('3'); 30 | }).then(done); 31 | }); 32 | 33 | it('with setter', done => { 34 | const vm = new Vue({ 35 | template: '
{{ b }}
', 36 | setup() { 37 | const a = ref(1); 38 | const b = computed({ 39 | get: () => a.value + 1, 40 | set: v => (a.value = v - 1), 41 | }); 42 | return { 43 | a, 44 | b, 45 | }; 46 | }, 47 | }).$mount(); 48 | expect(vm.b).toBe(2); 49 | expect(vm.$el.textContent).toBe('2'); 50 | vm.a = 2; 51 | expect(vm.b).toBe(3); 52 | waitForUpdate(() => { 53 | expect(vm.$el.textContent).toBe('3'); 54 | vm.b = 1; 55 | expect(vm.a).toBe(0); 56 | }) 57 | .then(() => { 58 | expect(vm.$el.textContent).toBe('1'); 59 | }) 60 | .then(done); 61 | }); 62 | 63 | it('warn assigning to computed with no setter', () => { 64 | const vm = new Vue({ 65 | setup() { 66 | const b = computed(() => 1); 67 | return { 68 | b, 69 | }; 70 | }, 71 | }); 72 | vm.b = 2; 73 | expect(warn.mock.calls[0][0]).toMatch( 74 | '[Vue warn]: Computed property was assigned to but it has no setter.' 75 | ); 76 | }); 77 | 78 | it('watching computed', done => { 79 | const spy = jest.fn(); 80 | const vm = new Vue({ 81 | setup() { 82 | const a = ref(1); 83 | const b = computed(() => a.value + 1); 84 | return { 85 | a, 86 | b, 87 | }; 88 | }, 89 | }); 90 | vm.$watch('b', spy); 91 | vm.a = 2; 92 | waitForUpdate(() => { 93 | expect(spy).toHaveBeenCalledWith(3, 2); 94 | }).then(done); 95 | }); 96 | 97 | it('caching', () => { 98 | const spy = jest.fn(); 99 | const vm = new Vue({ 100 | setup() { 101 | const a = ref(1); 102 | const b = computed(() => { 103 | spy(); 104 | return a.value + 1; 105 | }); 106 | return { 107 | a, 108 | b, 109 | }; 110 | }, 111 | }); 112 | expect(spy.mock.calls.length).toBe(0); 113 | vm.b; 114 | expect(spy.mock.calls.length).toBe(1); 115 | vm.b; 116 | expect(spy.mock.calls.length).toBe(1); 117 | }); 118 | 119 | it('as component', done => { 120 | const Comp = Vue.extend({ 121 | template: `
{{ b }} {{ c }}
`, 122 | setup() { 123 | const a = ref(1); 124 | const b = computed(() => { 125 | return a.value + 1; 126 | }); 127 | return { 128 | a, 129 | b, 130 | }; 131 | }, 132 | }); 133 | 134 | const vm = new Comp({ 135 | setup(_, { _vm }) { 136 | const c = computed(() => { 137 | return _vm.b + 1; 138 | }); 139 | 140 | return { 141 | c, 142 | }; 143 | }, 144 | }).$mount(); 145 | expect(vm.b).toBe(2); 146 | expect(vm.c).toBe(3); 147 | expect(vm.$el.textContent).toBe('2 3'); 148 | vm.a = 2; 149 | expect(vm.b).toBe(3); 150 | expect(vm.c).toBe(4); 151 | waitForUpdate(() => { 152 | expect(vm.$el.textContent).toBe('3 4'); 153 | }).then(done); 154 | }); 155 | 156 | it('rethrow computed error', () => { 157 | const vm = new Vue({ 158 | setup() { 159 | const a = computed(() => { 160 | throw new Error('rethrow'); 161 | }); 162 | 163 | return { 164 | a, 165 | }; 166 | }, 167 | }); 168 | expect(() => vm.a).toThrowError('rethrow'); 169 | }); 170 | 171 | it('Mixins should not break computed properties', () => { 172 | const ExampleComponent = Vue.extend({ 173 | props: ['test'], 174 | render: h => h('div'), 175 | setup: props => ({ example: computed(() => props.test) }), 176 | }); 177 | 178 | Vue.mixin({ 179 | computed: { 180 | foobar() { 181 | return 'test'; 182 | }, 183 | }, 184 | }); 185 | 186 | const app = new Vue({ 187 | render: h => 188 | h('div', [ 189 | h(ExampleComponent, { props: { test: 'A' } }), 190 | h(ExampleComponent, { props: { test: 'B' } }), 191 | ]), 192 | }).$mount(); 193 | 194 | expect(app.$children[0].example).toBe('A'); 195 | expect(app.$children[1].example).toBe('B'); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /src/reactivity/ref.ts: -------------------------------------------------------------------------------- 1 | import { Data } from '../component'; 2 | import { RefKey } from '../symbols'; 3 | import { proxy, isPlainObject } from '../utils'; 4 | import { HasDefined } from '../types/basic'; 5 | import { reactive } from './reactive'; 6 | 7 | type BailTypes = Function | Map | Set | WeakMap | WeakSet | Element; 8 | 9 | // corner case when use narrows type 10 | // Ex. type RelativePath = string & { __brand: unknown } 11 | // RelativePath extends object -> true 12 | type BaseTypes = string | number | boolean; 13 | 14 | declare const _refBrand: unique symbol; 15 | export interface Ref { 16 | readonly [_refBrand]: true; 17 | value: T; 18 | } 19 | 20 | // prettier-ignore 21 | // Recursively unwraps nested value bindings. 22 | // Unfortunately TS cannot do recursive types, but this should be enough for 23 | // practical use cases... 24 | export type UnwrapRef = T extends Ref 25 | ? UnwrapRef2 26 | : T extends BailTypes | BaseTypes 27 | ? T // bail out on types that shouldn't be unwrapped 28 | : T extends object ? { [K in keyof T]: UnwrapRef2 } : T 29 | 30 | // prettier-ignore 31 | type UnwrapRef2 = T extends Ref 32 | ? UnwrapRef3 33 | : T extends BailTypes | BaseTypes 34 | ? T 35 | : T extends object ? { [K in keyof T]: UnwrapRef3 } : T 36 | 37 | // prettier-ignore 38 | type UnwrapRef3 = T extends Ref 39 | ? UnwrapRef4 40 | : T extends BailTypes | BaseTypes 41 | ? T 42 | : T extends object ? { [K in keyof T]: UnwrapRef4 } : T 43 | 44 | // prettier-ignore 45 | type UnwrapRef4 = T extends Ref 46 | ? UnwrapRef5 47 | : T extends BailTypes | BaseTypes 48 | ? T 49 | : T extends object ? { [K in keyof T]: UnwrapRef5 } : T 50 | 51 | // prettier-ignore 52 | type UnwrapRef5 = T extends Ref 53 | ? UnwrapRef6 54 | : T extends BailTypes | BaseTypes 55 | ? T 56 | : T extends object ? { [K in keyof T]: UnwrapRef6 } : T 57 | 58 | // prettier-ignore 59 | type UnwrapRef6 = T extends Ref 60 | ? UnwrapRef7 61 | : T extends BailTypes | BaseTypes 62 | ? T 63 | : T extends object ? { [K in keyof T]: UnwrapRef7 } : T 64 | 65 | // prettier-ignore 66 | type UnwrapRef7 = T extends Ref 67 | ? UnwrapRef8 68 | : T extends BailTypes | BaseTypes 69 | ? T 70 | : T extends object ? { [K in keyof T]: UnwrapRef8 } : T 71 | 72 | // prettier-ignore 73 | type UnwrapRef8 = T extends Ref 74 | ? UnwrapRef9 75 | : T extends BailTypes | BaseTypes 76 | ? T 77 | : T extends object ? { [K in keyof T]: UnwrapRef9 } : T 78 | 79 | // prettier-ignore 80 | type UnwrapRef9 = T extends Ref 81 | ? UnwrapRef10 82 | : T extends BailTypes | BaseTypes 83 | ? T 84 | : T extends object ? { [K in keyof T]: UnwrapRef10 } : T 85 | 86 | // prettier-ignore 87 | type UnwrapRef10 = T extends Ref 88 | ? V // stop recursion 89 | : T 90 | 91 | interface RefOption { 92 | get(): T; 93 | set?(x: T): void; 94 | } 95 | class RefImpl implements Ref { 96 | readonly [_refBrand]!: true; 97 | public value!: T; 98 | constructor({ get, set }: RefOption) { 99 | proxy(this, 'value', { 100 | get, 101 | set, 102 | }); 103 | } 104 | } 105 | 106 | export function createRef(options: RefOption) { 107 | // seal the ref, this could prevent ref from being observed 108 | // It's safe to seal the ref, since we really shoulnd't extend it. 109 | // related issues: #79 110 | return Object.seal(new RefImpl(options)); 111 | } 112 | 113 | type RefValue = T extends Ref ? V : UnwrapRef; 114 | 115 | // without init value, explicit typed: a = ref<{ a: number }>() 116 | // typeof a will be Ref<{ a: number } | undefined> 117 | export function ref(): Ref; 118 | // with null as init value: a = ref<{ a: number }>(null); 119 | // typeof a will be Ref<{ a: number } | null> 120 | export function ref(raw: null): Ref; 121 | // with init value: a = ref({ a: ref(0) }) 122 | // typeof a will be Ref<{ a: number }> 123 | export function ref extends true ? S : RefValue>( 124 | raw: T 125 | ): Ref; 126 | // implementation 127 | export function ref(raw?: any): any { 128 | // if (isRef(raw)) { 129 | // return {} as any; 130 | // } 131 | 132 | const value = reactive({ [RefKey]: raw }); 133 | return createRef({ 134 | get: () => value[RefKey] as any, 135 | set: v => ((value[RefKey] as any) = v), 136 | }); 137 | } 138 | 139 | export function isRef(value: any): value is Ref { 140 | return value instanceof RefImpl; 141 | } 142 | 143 | // prettier-ignore 144 | type Refs = { 145 | [K in keyof Data]: Data[K] extends Ref 146 | ? Ref 147 | : Ref 148 | } 149 | 150 | export function toRefs(obj: T): Refs { 151 | if (!isPlainObject(obj)) return obj as any; 152 | 153 | const res: Refs = {} as any; 154 | Object.keys(obj).forEach(key => { 155 | let val: any = obj[key]; 156 | // use ref to proxy the property 157 | if (!isRef(val)) { 158 | val = createRef({ 159 | get: () => obj[key], 160 | set: v => (obj[key as keyof T] = v), 161 | }); 162 | } 163 | // todo 164 | res[key as keyof T] = val; 165 | }); 166 | 167 | return res; 168 | } 169 | -------------------------------------------------------------------------------- /test/types/defineComponent.spec.ts: -------------------------------------------------------------------------------- 1 | import { createComponent, defineComponent, createElement as h, ref, SetupContext, PropType } from '../../src'; 2 | import Router from 'vue-router'; 3 | 4 | const Vue = require('vue/dist/vue.common.js'); 5 | 6 | type Equal = (() => U extends Left ? 1 : 0) extends (() => U extends Right 7 | ? 1 8 | : 0) 9 | ? true 10 | : false; 11 | 12 | const isTypeEqual = (shouldBeEqual: Equal) => { 13 | void shouldBeEqual; 14 | expect(true).toBe(true); 15 | }; 16 | const isSubType = (shouldBeEqual: SubType extends SuperType ? true : false) => { 17 | void shouldBeEqual; 18 | expect(true).toBe(true); 19 | }; 20 | 21 | describe('defineComponent', () => { 22 | it('should work', () => { 23 | const Child = defineComponent({ 24 | props: { msg: String }, 25 | setup(props) { 26 | return () => h('span', props.msg); 27 | }, 28 | }); 29 | 30 | const App = defineComponent({ 31 | setup() { 32 | const msg = ref('hello'); 33 | return () => 34 | h('div', [ 35 | h(Child, { 36 | props: { 37 | msg: msg.value, 38 | }, 39 | }), 40 | ]); 41 | }, 42 | }); 43 | const vm = new Vue(App).$mount(); 44 | expect(vm.$el.querySelector('span').textContent).toBe('hello'); 45 | }); 46 | 47 | it('should infer props type', () => { 48 | const App = defineComponent({ 49 | props: { 50 | a: { 51 | type: Number, 52 | default: 0, 53 | }, 54 | b: String, 55 | }, 56 | setup(props, ctx) { 57 | type PropsType = typeof props; 58 | isTypeEqual(true); 59 | isSubType(true); 60 | isSubType<{ readonly b?: string; readonly a: number }, PropsType>(true); 61 | return () => null; 62 | }, 63 | }); 64 | new Vue(App); 65 | expect.assertions(3); 66 | }); 67 | 68 | it('custom props interface', () => { 69 | interface IPropsType { 70 | b: string; 71 | } 72 | const App = defineComponent({ 73 | props: { 74 | b: {}, 75 | }, 76 | setup(props, ctx) { 77 | type PropsType = typeof props; 78 | isTypeEqual(true); 79 | isSubType(true); 80 | isSubType<{ b: string }, PropsType>(true); 81 | return () => null; 82 | }, 83 | }); 84 | new Vue(App); 85 | expect.assertions(3); 86 | }); 87 | 88 | it('custom props type function', () => { 89 | interface IPropsTypeFunction { 90 | fn: (arg: boolean) => void; 91 | } 92 | const App = defineComponent({ 93 | props: { 94 | fn: Function as PropType<(arg: boolean) => void>, 95 | }, 96 | setup(props, ctx) { 97 | type PropsType = typeof props; 98 | isTypeEqual(true); 99 | isSubType void }>(true); 100 | isSubType<{ fn: (arg: boolean) => void }, PropsType>(true); 101 | return () => null; 102 | }, 103 | }); 104 | new Vue(App); 105 | expect.assertions(3); 106 | }); 107 | 108 | it('no props', () => { 109 | const App = defineComponent({ 110 | setup(props, ctx) { 111 | isTypeEqual(true); 112 | isTypeEqual(true); 113 | return () => null; 114 | }, 115 | }); 116 | new Vue(App); 117 | expect.assertions(2); 118 | }); 119 | 120 | it('infer the required prop', () => { 121 | const App = defineComponent({ 122 | props: { 123 | foo: { 124 | type: String, 125 | required: true, 126 | }, 127 | bar: { 128 | type: String, 129 | default: 'default', 130 | }, 131 | zoo: { 132 | type: String, 133 | required: false, 134 | }, 135 | }, 136 | propsData: { 137 | foo: 'foo', 138 | }, 139 | setup(props) { 140 | type PropsType = typeof props; 141 | isSubType<{ readonly foo: string; readonly bar: string; readonly zoo?: string }, PropsType>( 142 | true 143 | ); 144 | isSubType( 145 | true 146 | ); 147 | return () => null; 148 | }, 149 | }); 150 | new Vue(App); 151 | expect.assertions(2); 152 | }); 153 | 154 | describe('compatible with vue router', () => { 155 | it('RouteConfig.component', () => { 156 | new Router({ 157 | routes: [ 158 | { 159 | path: '/', 160 | name: 'root', 161 | component: defineComponent({}), 162 | }, 163 | ], 164 | }); 165 | }); 166 | }); 167 | 168 | describe('retro-compatible with createComponent', () => { 169 | it('should still work and warn', () => { 170 | const warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); 171 | const Child = createComponent({ 172 | props: { msg: String }, 173 | setup(props) { 174 | return () => h('span', props.msg); 175 | }, 176 | }); 177 | 178 | const App = createComponent({ 179 | setup() { 180 | const msg = ref('hello'); 181 | return () => 182 | h('div', [ 183 | h(Child, { 184 | props: { 185 | msg: msg.value, 186 | }, 187 | }), 188 | ]); 189 | }, 190 | }); 191 | const vm = new Vue(App).$mount(); 192 | expect(vm.$el.querySelector('span').textContent).toBe('hello'); 193 | expect(warn.mock.calls[0][0]).toMatch( 194 | '[Vue warn]: `createComponent` has been renamed to `defineComponent`.' 195 | ); 196 | warn.mockRestore(); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.5.0 2 | 3 | - New: `watchEffect` function, lingin up with the latest version of the RFC ([RFC docs](https://vue-composition-api-rfc.netlify.com/api.html#watcheffect)) (#275) 4 | - Fix: `setup` from a mixin should called before the component's own (#276) 5 | - Fix(types): Fix corner case in `UnWrapRef` internal type (#261) 6 | - types: Add `Element` to bailout types for unwrapping (#278) 7 | 8 | # 0.4.0 9 | 10 | - **Refactor: rename `createComponent` to `defineComponent`** (the `createComponent` function is still there but deprecated) [#230](https://github.com/vuejs/composition-api/issues/230) 11 | - Fix: correct the symbol check; fixes the compatibility issue in iOS 9 [#218](https://github.com/vuejs/composition-api/pull/218) 12 | - Fix: avoid accessing undeclared instance fields on type-level; fixes Vetur template type checking; fixes vue-router type compatibility [#189](https://github.com/vuejs/composition-api/pull/189) 13 | - Fix: `onUnmounted` should not be run on `deactivated` [#217](https://github.com/vuejs/composition-api/pull/217) 14 | 15 | # 0.3.4 16 | 17 | - Fixed `reactive` setter not working on the server. 18 | - New `isServer` setup context property. 19 | 20 | # 0.3.3 21 | 22 | - Fixed make `__ob__` unenumerable [#149](https://github.com/vuejs/composition-api/issues/149). 23 | - Fixed computed type 24 | - Expose `getCurrentInstance` for advanced usage in Vue plugins. 25 | - New `onServerPrefetch` lifecycle hook and new `ssrContext` setup context property [#198](https://github.com/vuejs/composition-api/issues/198). 26 | 27 | # 0.3.2 28 | 29 | - Improve TypeScript type infer for `props` option [#106](https://github.com/vuejs/composition-api/issues/106). 30 | - Fix return type of `createComponent` not being compatible with `vue-router` [#130](https://github.com/vuejs/composition-api/issues/130). 31 | - Expose `listeners` on `SetupContext` [#132](https://github.com/vuejs/composition-api/issues/132). 32 | 33 | # 0.3.1 34 | 35 | - Fix cleaup callback not running when watcher stops [#113](https://github.com/vuejs/composition-api/issues/113). 36 | - Fix watcher callback not flushing at right timing [#120](https://github.com/vuejs/composition-api/issues/120). 37 | 38 | # 0.3.0 39 | 40 | - Improve TypeScript type definitions. 41 | - Fix `context.slots` not being avaliable before render [#84](https://github.com/vuejs/composition-api/issues/84). 42 | 43 | ## Changed 44 | 45 | The `render` function returned from `setup` no longer receives any parameters. 46 | 47 | ### Previous 48 | 49 | ```js 50 | export default { 51 | setup() { 52 | return props => h('div', prop.msg); 53 | }, 54 | }; 55 | ``` 56 | 57 | ### Now 58 | 59 | ```js 60 | export default { 61 | setup(props) { 62 | return () => h('div', prop.msg); 63 | }, 64 | }; 65 | ``` 66 | 67 | # 0.2.1 68 | 69 | - Declare your expected prop types directly in TypeScript: 70 | 71 | ```js 72 | import { createComponent, createElement as h } from '@vue/composition-api'; 73 | 74 | interface Props { 75 | msg: string; 76 | } 77 | 78 | const MyComponent = 79 | createComponent < 80 | Props > 81 | { 82 | props: { 83 | msg: {}, // required by vue 2 runtime 84 | }, 85 | setup(props) { 86 | return () => h('div', props.msg); 87 | }, 88 | }; 89 | ``` 90 | 91 | - Declare ref type in TypeScript: 92 | ```js 93 | const dateRef = ref < Date > new Date(); 94 | ``` 95 | - Fix `createComponent` not working with `import()` [#81](https://github.com/vuejs/composition-api/issues/81). 96 | - Fix `inject` type declaration [#83](https://github.com/vuejs/composition-api/issues/83). 97 | 98 | # 0.2.0 99 | 100 | ## Fixed 101 | 102 | - `computed` property is called immediately in `reactive()` [#79](https://github.com/vuejs/composition-api/issues/79). 103 | 104 | ## Changed 105 | 106 | - rename `onBeforeDestroy()` to `onBeforeUnmount()` [lifecycle-hooks](https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks). 107 | - Remove `onCreated()` [lifecycle-hooks](https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks). 108 | - Remove `onDestroyed()` [lifecycle-hooks](https://vue-composition-api-rfc.netlify.com/api.html#lifecycle-hooks). 109 | 110 | # 0.1.0 111 | 112 | **The package has been renamed to `@vue/composition-api` to be consistent with RFC.** 113 | 114 | The `@vue/composition-api` reflects the [Composition API](https://vue-composition-api-rfc.netlify.com/) RFC. 115 | 116 | # 2.2.0 117 | 118 | - Improve typescript support. 119 | - Export `createElement`. 120 | - Export `SetupContext`. 121 | - Support returning a render function from `setup`. 122 | - Allow string keys in `provide`/`inject`. 123 | 124 | # 2.1.2 125 | 126 | - Remove auto-unwrapping for Array ([#53](https://github.com/vuejs/composition-api/issues/53)). 127 | 128 | # 2.1.1 129 | 130 | - Export `set()` function. Using exported `set` whenever you need to use [Vue.set](https://vuejs.org/v2/api/#Vue-set) or [vm.\$set](https://vuejs.org/v2/api/#vm-set). The custom `set` ensures that auto-unwrapping works for the new property. 131 | - Add a new signature of `provide`: `provide(key, value)`. 132 | - Fix multiple `provide` invoking per component. 133 | - Fix order of `setup` invoking. 134 | - `onErrorCaptured` not triggered ([#25](https://github.com/vuejs/composition-api/issues/25)). 135 | - Fix `this` losing in nested setup call ([#38](https://github.com/vuejs/composition-api/issues/38)). 136 | - Fix some edge cases of unwarpping. 137 | - Change `context.slots`'s value. It now proxies to `$scopeSlots` instead of `$slots`. 138 | 139 | # 2.0.6 140 | 141 | ## Fixed 142 | 143 | - watch callback is called repeatedly with multi-sources 144 | 145 | ## Improved 146 | 147 | - reduce `watch()` memory overhead 148 | 149 | # 2.0.0 150 | 151 | Implement the [newest version of RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md) 152 | 153 | ## Breaking Changes 154 | 155 | `this` is not available inside `setup()`. See [setup](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#the-setup-function) for details. 156 | 157 | ## Features 158 | 159 | Complex Prop Types: 160 | 161 | ```ts 162 | import { createComponent, PropType } from 'vue'; 163 | 164 | createComponent({ 165 | props: { 166 | options: (null as any) as PropType<{ msg: string }>, 167 | }, 168 | setup(props) { 169 | props.options; // { msg: string } | undefined 170 | }, 171 | }); 172 | ``` 173 | 174 | # 1.x 175 | 176 | Implement the [init version of RFC](https://github.com/vuejs/rfcs/blob/903f429696524d8f93b4976d5b09dfb3632e89ef/active-rfcs/0000-function-api.md) 177 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # Vue Composition API 2 | 3 | > [Vue Composition API](https://vue-composition-api-rfc.netlify.com/) 4 | 5 | `@vue/composition-api` 使开发者们可以在 `Vue 2.x` 中使用 `Vue 3.0` 引入的**基于函数**的**逻辑复用机制**。 6 | 7 | [**English Version**](./README.md) 8 | 9 | --- 10 | 11 | # 导航 12 | 13 | - [安装](#安装) 14 | - [使用](#使用) 15 | - [TypeScript](#TypeScript) 16 | - [TSX](#tsx) 17 | - [限制](#限制) 18 | - [API](https://vue-composition-api-rfc.netlify.com/api.html) 19 | - [更新日志](https://github.com/vuejs/composition-api/blob/master/CHANGELOG.md) 20 | 21 | # 安装 22 | 23 | **npm** 24 | 25 | ```bash 26 | npm install @vue/composition-api --save 27 | ``` 28 | 29 | **yarn** 30 | 31 | ```bash 32 | yarn add @vue/composition-api 33 | ``` 34 | 35 | **CDN** 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | 通过全局变量 `window.vueCompositionApi` 来使用。 42 | 43 | # 使用 44 | 45 | 在使用任何 `@vue/composition-api` 提供的能力前,必须先通过 `Vue.use()` 进行安装: 46 | 47 | ```js 48 | import Vue from 'vue'; 49 | import VueCompositionApi from '@vue/composition-api'; 50 | 51 | Vue.use(VueCompositionApi); 52 | ``` 53 | 54 | 安装插件后,您就可以使用新的 [Composition API](https://vue-composition-api-rfc.netlify.com/) 来开发组件了。 55 | 56 | # TypeScript 57 | 58 | **本插件要求使用 TypeScript 3.5.1 以上版本,如果你正在使用 `vetur`,请将 `vetur.useWorkspaceDependencies` 设为 `true`。** 59 | 60 | 为了让 TypeScript 在 Vue 组件选项中正确地推导类型,我们必须使用 `defineComponent` 来定义组件: 61 | 62 | ```ts 63 | import { defineComponent } from '@vue/composition-api'; 64 | 65 | const Component = defineComponent({ 66 | // 启用类型推断 67 | }); 68 | 69 | const Component = { 70 | // 无法进行选项的类型推断 71 | // TypeScript 无法知道这是一个 Vue 组件的选项对象 72 | }; 73 | ``` 74 | 75 | ## TSX 76 | 77 | :rocket: 这里有一个配置好 TS/TSX 支持的[示例仓库](https://github.com/liximomo/vue-composition-api-tsx-example)来帮助你快速开始. 78 | 79 | 要支持 TSX,请创建一个类型定义文件并提供正确的 JSX 定义。内容如下: 80 | 81 | ```ts 82 | // 文件: `shim-tsx.d.ts` 83 | import Vue, { VNode } from 'vue'; 84 | import { ComponentRenderProxy } from '@vue/composition-api'; 85 | 86 | declare global { 87 | namespace JSX { 88 | // tslint:disable no-empty-interface 89 | interface Element extends VNode {} 90 | // tslint:disable no-empty-interface 91 | interface ElementClass extends ComponentRenderProxy {} 92 | interface ElementAttributesProperty { 93 | $props: any; // 定义要使用的属性名称 94 | } 95 | interface IntrinsicElements { 96 | [elem: string]: any; 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | # 限制 103 | 104 | ## `Ref` 自动展开 (unwrap) 105 | 106 | 数组索引属性无法进行自动展开: 107 | 108 | ### **不要**使用 `Array` 直接存取 `ref` 对象: 109 | 110 | ```js 111 | const state = reactive({ 112 | list: [ref(0)], 113 | }); 114 | // 不会自动展开, 须使用 `.value` 115 | state.list[0].value === 0; // true 116 | 117 | state.list.push(ref(1)); 118 | // 不会自动展开, 须使用 `.value` 119 | state.list[1].value === 1; // true 120 | ``` 121 | 122 | ### **不要**在数组中使用含有 `ref` 的普通对象: 123 | 124 | ```js 125 | const a = { 126 | count: ref(0), 127 | }; 128 | const b = reactive({ 129 | list: [a], // `a.count` 不会自动展开!! 130 | }); 131 | 132 | // `count` 不会自动展开, 须使用 `.value` 133 | b.list[0].count.value === 0; // true 134 | ``` 135 | 136 | ```js 137 | const b = reactive({ 138 | list: [ 139 | { 140 | count: ref(0), // 不会自动展开!! 141 | }, 142 | ], 143 | }); 144 | 145 | // `count` 不会自动展开, 须使用 `.value` 146 | b.list[0].count.value === 0; // true 147 | ``` 148 | 149 | ### **应该**总是将 `ref` 存放到 `reactive` 对象中: 150 | 151 | ```js 152 | const a = reactive({ 153 | count: ref(0), 154 | }); 155 | const b = reactive({ 156 | list: [a], 157 | }); 158 | // 自动展开 159 | b.list[0].count === 0; // true 160 | 161 | b.list.push( 162 | reactive({ 163 | count: ref(1), 164 | }) 165 | ); 166 | // 自动展开 167 | b.list[1].count === 1; // true 168 | ``` 169 | 170 | ### `reactive` 会返回一个修改过的原始的对象 171 | 172 | 此行为与 Vue 2 中的 `Vue.observable` 一致 173 | > Vue 3 中会返回一个新的的代理对象. 174 | 175 | --- 176 | 177 | ## `watch()` API 178 | 179 | 不支持 `onTrack` 和 `onTrigger` 选项。 180 | 181 | --- 182 | 183 | ## 模板 Refs 184 | 185 | > :white_check_mark: 支持     :x: 不支持 186 | 187 | :white_check_mark: 字符串 ref && 从 `setup()` 返回 ref: 188 | 189 | ```html 190 | 193 | 194 | 210 | ``` 211 | 212 | :white_check_mark: 字符串 ref && 从 `setup()` 返回 ref && 渲染函数 / JSX: 213 | 214 | ```jsx 215 | export default { 216 | setup() { 217 | const root = ref(null); 218 | 219 | onMounted(() => { 220 | // 在初次渲染后 DOM 元素会被赋值给 ref 221 | console.log(root.value); //
222 | }); 223 | 224 | return { 225 | root, 226 | }; 227 | }, 228 | render() { 229 | // 使用 JSX 230 | return () =>
; 231 | }, 232 | }; 233 | ``` 234 | 235 | :x: 函数 ref: 236 | 237 | ```html 238 | 241 | 242 | 253 | ``` 254 | 255 | :x: 渲染函数 / JSX: 256 | 257 | ```jsx 258 | export default { 259 | setup() { 260 | const root = ref(null); 261 | 262 | return () => 263 | h('div', { 264 | ref: root, 265 | }); 266 | 267 | // 使用 JSX 268 | return () =>
; 269 | }, 270 | }; 271 | ``` 272 | 273 | 如果你依然选择在 `setup()` 中写 `render` 函数,那么你可以使用 `SetupContext.refs` 来访问模板引用,它等价于 Vue 2.x 中的 `this.$refs`: 274 | 275 | > :warning: **警告**: `SetupContext.refs` 并不属于 `Vue 3.0` 的一部分, `@vue/composition-api` 将其曝光在 `SetupContext` 中只是临时提供一种变通方案。 276 | 277 | ```js 278 | export default { 279 | setup(initProps, setupContext) { 280 | const refs = setupContext.refs; 281 | onMounted(() => { 282 | // 在初次渲染后 DOM 元素会被赋值给 ref 283 | console.log(refs.root); //
284 | }); 285 | 286 | return () => 287 | h('div', { 288 | ref: 'root', 289 | }); 290 | 291 | // 使用 JSX 292 | return () =>
; 293 | }, 294 | }; 295 | ``` 296 | 297 | 如果项目使用了 TypeScript,你还需要扩展 `SetupContext` 类型: 298 | 299 | ```ts 300 | import Vue from 'vue'; 301 | import VueCompositionApi from '@vue/composition-api'; 302 | 303 | Vue.use(VueCompositionApi); 304 | 305 | declare module '@vue/composition-api/dist/component/component' { 306 | interface SetupContext { 307 | readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }; 308 | } 309 | } 310 | ``` 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Composition API 2 | 3 | > [Vue Composition API](https://vue-composition-api-rfc.netlify.com/) 4 | 5 | `@vue/composition-api` provides a way to use `Vue 3.0`'s **Composition api** in `Vue 2.x`. 6 | 7 | **Note: the primary goal of this package is to allow the community to experiment with the API and provide feedback before it's finalized. The implementation may contain minor inconsistencies with the RFC as the latter gets updated. We do not recommend using this package for production yet at this stage.** 8 | 9 | [**中文文档**](./README.zh-CN.md) 10 | 11 | --- 12 | 13 | # Navigation 14 | 15 | - [Installation](#Installation) 16 | - [Usage](#Usage) 17 | - [TypeScript](#TypeScript) 18 | - [TSX](#tsx) 19 | - [Limitations](#Limitations) 20 | - [API](https://vue-composition-api-rfc.netlify.com/api.html) 21 | - [Changelog](https://github.com/vuejs/composition-api/blob/master/CHANGELOG.md) 22 | 23 | # Installation 24 | 25 | **npm** 26 | 27 | ```bash 28 | npm install @vue/composition-api 29 | ``` 30 | 31 | **yarn** 32 | 33 | ```bash 34 | yarn add @vue/composition-api 35 | ``` 36 | 37 | **CDN** 38 | 39 | ```html 40 | 41 | ``` 42 | 43 | By using the global variable `window.vueCompositionApi` 44 | 45 | # Usage 46 | 47 | You must install `@vue/composition-api` via `Vue.use()` before using other APIs: 48 | 49 | ```js 50 | import Vue from 'vue'; 51 | import VueCompositionApi from '@vue/composition-api'; 52 | 53 | Vue.use(VueCompositionApi); 54 | ``` 55 | 56 | After installing the plugin you can use the [Composition API](https://vue-composition-api-rfc.netlify.com/) to compose your component. 57 | 58 | # TypeScript 59 | 60 | **This plugin requires TypeScript version >3.5.1. If you are using vetur, make sure to set `vetur.useWorkspaceDependencies` to `true`.** 61 | 62 | To let TypeScript properly infer types inside Vue component options, you need to define components with `defineComponent`: 63 | 64 | ```ts 65 | import { defineComponent } from '@vue/composition-api'; 66 | 67 | const Component = defineComponent({ 68 | // type inference enabled 69 | }); 70 | 71 | const Component = { 72 | // this will NOT have type inference, 73 | // because TypeScript can't tell this is options for a Vue component. 74 | }; 75 | ``` 76 | 77 | ## TSX 78 | 79 | :rocket: An Example [Repository](https://github.com/liximomo/vue-composition-api-tsx-example) with TS and TSX support is provided to help you start. 80 | 81 | To support TSX, create a declaration file with following content in your project. 82 | 83 | ```ts 84 | // file: shim-tsx.d.ts 85 | import Vue, { VNode } from 'vue'; 86 | import { ComponentRenderProxy } from '@vue/composition-api'; 87 | 88 | declare global { 89 | namespace JSX { 90 | // tslint:disable no-empty-interface 91 | interface Element extends VNode {} 92 | // tslint:disable no-empty-interface 93 | interface ElementClass extends ComponentRenderProxy {} 94 | interface ElementAttributesProperty { 95 | $props: any; // specify the property name to use 96 | } 97 | interface IntrinsicElements { 98 | [elem: string]: any; 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | # Limitations 105 | 106 | ## `Ref` Unwrap 107 | 108 | `Unwrap` is not working with Array index. 109 | 110 | ### **Should not** store `ref` as a **direct** child of `Array`: 111 | 112 | ```js 113 | const state = reactive({ 114 | list: [ref(0)], 115 | }); 116 | // no unwrap, `.value` is required 117 | state.list[0].value === 0; // true 118 | 119 | state.list.push(ref(1)); 120 | // no unwrap, `.value` is required 121 | state.list[1].value === 1; // true 122 | ``` 123 | 124 | ### **Should not** use `ref` in a plain object when working with `Array`: 125 | 126 | ```js 127 | const a = { 128 | count: ref(0), 129 | }; 130 | const b = reactive({ 131 | list: [a], // `a.count` will not unwrap!! 132 | }); 133 | 134 | // no unwrap for `count`, `.value` is required 135 | b.list[0].count.value === 0; // true 136 | ``` 137 | 138 | ```js 139 | const b = reactive({ 140 | list: [ 141 | { 142 | count: ref(0), // no unwrap!! 143 | }, 144 | ], 145 | }); 146 | 147 | // no unwrap for `count`, `.value` is required 148 | b.list[0].count.value === 0; // true 149 | ``` 150 | 151 | ### **Should** always use `ref` in a `reactive` when working with `Array`: 152 | 153 | ```js 154 | const a = reactive({ 155 | count: ref(0), 156 | }); 157 | const b = reactive({ 158 | list: [a], 159 | }); 160 | // unwrapped 161 | b.list[0].count === 0; // true 162 | 163 | b.list.push( 164 | reactive({ 165 | count: ref(1), 166 | }) 167 | ); 168 | // unwrapped 169 | b.list[1].count === 1; // true 170 | ``` 171 | 172 | ### ***Using*** `reactive` will mutate the origin object 173 | 174 | This is an limitation of using `Vue.observable` in Vue 2. 175 | > Vue 3 will return an new proxy object. 176 | 177 | --- 178 | 179 | ## `watch()` API 180 | 181 | `onTrack` and `onTrigger` are not available in `WatchOptions`. 182 | 183 | --- 184 | 185 | ## Template Refs 186 | 187 | > :white_check_mark: 188 | > Support     :x: Not Supported 189 | 190 | :white_check_mark: 191 | String ref && return it from `setup()`: 192 | 193 | ```html 194 | 197 | 198 | 214 | ``` 215 | 216 | :white_check_mark: 217 | String ref && return it from `setup()` && Render Function / JSX: 218 | 219 | ```jsx 220 | export default { 221 | setup() { 222 | const root = ref(null); 223 | 224 | onMounted(() => { 225 | // the DOM element will be assigned to the ref after initial render 226 | console.log(root.value); //
227 | }); 228 | 229 | return { 230 | root, 231 | }; 232 | }, 233 | render() { 234 | // with JSX 235 | return () =>
; 236 | }, 237 | }; 238 | ``` 239 | 240 | :x: Function ref: 241 | 242 | ```html 243 | 246 | 247 | 258 | ``` 259 | 260 | :x: Render Function / JSX in `setup()`: 261 | 262 | ```jsx 263 | export default { 264 | setup() { 265 | const root = ref(null); 266 | 267 | return () => 268 | h('div', { 269 | ref: root, 270 | }); 271 | 272 | // with JSX 273 | return () =>
; 274 | }, 275 | }; 276 | ``` 277 | 278 | If you really want to use template refs in this case, you can access `vm.$refs` via `SetupContext.refs`. 279 | 280 | > :warning: **Warning**: The `SetupContext.refs` won't exist in `Vue 3.0`. `@vue/composition-api` provide it as a workaround here. 281 | 282 | ```js 283 | export default { 284 | setup(initProps, setupContext) { 285 | const refs = setupContext.refs; 286 | onMounted(() => { 287 | // the DOM element will be assigned to the ref after initial render 288 | console.log(refs.root); //
289 | }); 290 | 291 | return () => 292 | h('div', { 293 | ref: 'root', 294 | }); 295 | 296 | // with JSX 297 | return () =>
; 298 | }, 299 | }; 300 | ``` 301 | 302 | You may also need to augment the `SetupContext` when working with TypeScript: 303 | 304 | ```ts 305 | import Vue from 'vue'; 306 | import VueCompositionApi from '@vue/composition-api'; 307 | 308 | Vue.use(VueCompositionApi); 309 | 310 | declare module '@vue/composition-api/dist/component/component' { 311 | interface SetupContext { 312 | readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }; 313 | } 314 | } 315 | ``` 316 | 317 | ## SSR 318 | 319 | Even if there is no definitive Vue 3 API for SSR yet, this plugin implements the `onServerPrefetch` lifecycle hook that allows you to use the `serverPrefetch` hook found in the classic API. 320 | 321 | ```js 322 | import { onServerPrefetch } from '@vue/composition-api'; 323 | 324 | export default { 325 | setup (props, { ssrContext }) { 326 | const result = ref(); 327 | 328 | onServerPrefetch(async () => { 329 | result.value = await callApi(ssrContext.someId); 330 | }); 331 | 332 | return { 333 | result, 334 | }; 335 | }, 336 | }; 337 | ``` 338 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import { VueConstructor } from 'vue'; 2 | import { ComponentInstance, SetupContext, SetupFunction, Data } from './component'; 3 | import { Ref, isRef, isReactive, nonReactive } from './reactivity'; 4 | import { getCurrentVM, setCurrentVM } from './runtimeContext'; 5 | import { resolveSlots, createSlotProxy } from './helper'; 6 | import { hasOwn, isPlainObject, assert, proxy, warn, isFunction } from './utils'; 7 | import { ref } from './apis/state'; 8 | import vmStateManager from './vmStateManager'; 9 | 10 | function asVmProperty(vm: ComponentInstance, propName: string, propValue: Ref) { 11 | const props = vm.$options.props; 12 | if (!(propName in vm) && !(props && hasOwn(props, propName))) { 13 | proxy(vm, propName, { 14 | get: () => propValue.value, 15 | set: (val: unknown) => { 16 | propValue.value = val; 17 | }, 18 | }); 19 | 20 | if (process.env.NODE_ENV !== 'production') { 21 | // expose binding to Vue Devtool as a data property 22 | // delay this until state has been resolved to prevent repeated works 23 | vm.$nextTick(() => { 24 | proxy(vm._data, propName, { 25 | get: () => propValue.value, 26 | set: (val: unknown) => { 27 | propValue.value = val; 28 | }, 29 | }); 30 | }); 31 | } 32 | } else if (process.env.NODE_ENV !== 'production') { 33 | if (props && hasOwn(props, propName)) { 34 | warn(`The setup binding property "${propName}" is already declared as a prop.`, vm); 35 | } else { 36 | warn(`The setup binding property "${propName}" is already declared.`, vm); 37 | } 38 | } 39 | } 40 | 41 | function updateTemplateRef(vm: ComponentInstance) { 42 | const rawBindings = vmStateManager.get(vm, 'rawBindings') || {}; 43 | if (!rawBindings || !Object.keys(rawBindings).length) return; 44 | 45 | const refs = vm.$refs; 46 | const oldRefKeys = vmStateManager.get(vm, 'refs') || []; 47 | for (let index = 0; index < oldRefKeys.length; index++) { 48 | const key = oldRefKeys[index]; 49 | const setupValue = rawBindings[key]; 50 | if (!refs[key] && setupValue && isRef(setupValue)) { 51 | setupValue.value = null; 52 | } 53 | } 54 | 55 | const newKeys = Object.keys(refs); 56 | const validNewKeys = []; 57 | for (let index = 0; index < newKeys.length; index++) { 58 | const key = newKeys[index]; 59 | const setupValue = rawBindings[key]; 60 | if (refs[key] && setupValue && isRef(setupValue)) { 61 | setupValue.value = refs[key]; 62 | validNewKeys.push(key); 63 | } 64 | } 65 | vmStateManager.set(vm, 'refs', validNewKeys); 66 | } 67 | 68 | function resolveScopedSlots(vm: ComponentInstance, slotsProxy: { [x: string]: Function }): void { 69 | const parentVode = (vm.$options as any)._parentVnode; 70 | if (!parentVode) return; 71 | 72 | const prevSlots = vmStateManager.get(vm, 'slots') || []; 73 | const curSlots = resolveSlots(parentVode.data.scopedSlots, vm.$slots); 74 | // remove staled slots 75 | for (let index = 0; index < prevSlots.length; index++) { 76 | const key = prevSlots[index]; 77 | if (!curSlots[key]) { 78 | delete slotsProxy[key]; 79 | } 80 | } 81 | 82 | // proxy fresh slots 83 | const slotNames = Object.keys(curSlots); 84 | for (let index = 0; index < slotNames.length; index++) { 85 | const key = slotNames[index]; 86 | if (!slotsProxy[key]) { 87 | slotsProxy[key] = createSlotProxy(vm, key); 88 | } 89 | } 90 | vmStateManager.set(vm, 'slots', slotNames); 91 | } 92 | 93 | function activateCurrentInstance( 94 | vm: ComponentInstance, 95 | fn: (vm_: ComponentInstance) => any, 96 | onError?: (err: Error) => void 97 | ) { 98 | let preVm = getCurrentVM(); 99 | setCurrentVM(vm); 100 | try { 101 | return fn(vm); 102 | } catch (err) { 103 | if (onError) { 104 | onError(err); 105 | } else { 106 | throw err; 107 | } 108 | } finally { 109 | setCurrentVM(preVm); 110 | } 111 | } 112 | 113 | export function mixin(Vue: VueConstructor) { 114 | Vue.mixin({ 115 | beforeCreate: functionApiInit, 116 | mounted(this: ComponentInstance) { 117 | updateTemplateRef(this); 118 | }, 119 | updated(this: ComponentInstance) { 120 | updateTemplateRef(this); 121 | }, 122 | }); 123 | 124 | /** 125 | * Vuex init hook, injected into each instances init hooks list. 126 | */ 127 | 128 | function functionApiInit(this: ComponentInstance) { 129 | const vm = this; 130 | const $options = vm.$options; 131 | const { setup, render } = $options; 132 | 133 | if (render) { 134 | // keep currentInstance accessible for createElement 135 | $options.render = function(...args: any): any { 136 | return activateCurrentInstance(vm, () => render.apply(this, args)); 137 | }; 138 | } 139 | 140 | if (!setup) { 141 | return; 142 | } 143 | if (typeof setup !== 'function') { 144 | if (process.env.NODE_ENV !== 'production') { 145 | warn( 146 | 'The "setup" option should be a function that returns a object in component definitions.', 147 | vm 148 | ); 149 | } 150 | return; 151 | } 152 | 153 | const { data } = $options; 154 | // wrapper the data option, so we can invoke setup before data get resolved 155 | $options.data = function wrappedData() { 156 | initSetup(vm, vm.$props); 157 | return typeof data === 'function' 158 | ? (data as (this: ComponentInstance, x: ComponentInstance) => object).call(vm, vm) 159 | : data || {}; 160 | }; 161 | } 162 | 163 | function initSetup(vm: ComponentInstance, props: Record = {}) { 164 | const setup = vm.$options.setup!; 165 | const ctx = createSetupContext(vm); 166 | 167 | // resolve scopedSlots and slots to functions 168 | resolveScopedSlots(vm, ctx.slots); 169 | 170 | let binding: ReturnType> | undefined | null; 171 | activateCurrentInstance(vm, () => { 172 | binding = setup(props, ctx); 173 | }); 174 | 175 | if (!binding) return; 176 | if (isFunction(binding)) { 177 | // keep typescript happy with the binding type. 178 | const bindingFunc = binding; 179 | // keep currentInstance accessible for createElement 180 | vm.$options.render = () => { 181 | resolveScopedSlots(vm, ctx.slots); 182 | return activateCurrentInstance(vm, () => bindingFunc()); 183 | }; 184 | return; 185 | } 186 | if (isPlainObject(binding)) { 187 | const bindingObj = binding; 188 | vmStateManager.set(vm, 'rawBindings', binding); 189 | Object.keys(binding).forEach(name => { 190 | let bindingValue = bindingObj[name]; 191 | // only make primitive value reactive 192 | if (!isRef(bindingValue)) { 193 | if (isReactive(bindingValue)) { 194 | bindingValue = ref(bindingValue); 195 | } else { 196 | // a non-reactive should not don't get reactivity 197 | bindingValue = ref(nonReactive(bindingValue)); 198 | } 199 | } 200 | asVmProperty(vm, name, bindingValue); 201 | }); 202 | return; 203 | } 204 | 205 | if (process.env.NODE_ENV !== 'production') { 206 | assert( 207 | false, 208 | `"setup" must return a "Object" or a "Function", got "${Object.prototype.toString 209 | .call(binding) 210 | .slice(8, -1)}"` 211 | ); 212 | } 213 | } 214 | 215 | function createSetupContext(vm: ComponentInstance & { [x: string]: any }): SetupContext { 216 | const ctx = { 217 | slots: {}, 218 | } as SetupContext; 219 | const props: Array = [ 220 | 'root', 221 | 'parent', 222 | 'refs', 223 | 'attrs', 224 | 'listeners', 225 | 'isServer', 226 | 'ssrContext', 227 | ]; 228 | const methodReturnVoid = ['emit']; 229 | props.forEach(key => { 230 | let targetKey: string; 231 | let srcKey: string; 232 | if (Array.isArray(key)) { 233 | [targetKey, srcKey] = key; 234 | } else { 235 | targetKey = srcKey = key; 236 | } 237 | srcKey = `$${srcKey}`; 238 | proxy(ctx, targetKey, { 239 | get: () => vm[srcKey], 240 | set() { 241 | warn(`Cannot assign to '${targetKey}' because it is a read-only property`, vm); 242 | }, 243 | }); 244 | }); 245 | methodReturnVoid.forEach(key => { 246 | const srcKey = `$${key}`; 247 | proxy(ctx, key, { 248 | get() { 249 | return (...args: any[]) => { 250 | const fn: Function = vm[srcKey]; 251 | fn.apply(vm, args); 252 | }; 253 | }, 254 | }); 255 | }); 256 | if (process.env.NODE_ENV === 'test') { 257 | (ctx as any)._vm = vm; 258 | } 259 | return ctx; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/apis/watch.ts: -------------------------------------------------------------------------------- 1 | import { ComponentInstance } from '../component'; 2 | import { Ref, isRef } from '../reactivity'; 3 | import { assert, logError, noopFn, warn } from '../utils'; 4 | import { defineComponentInstance } from '../helper'; 5 | import { getCurrentVM, getCurrentVue } from '../runtimeContext'; 6 | import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols'; 7 | 8 | type CleanupRegistrator = (invalidate: () => void) => void; 9 | 10 | type SimpleEffect = (onCleanup: CleanupRegistrator) => void; 11 | 12 | type StopHandle = () => void; 13 | 14 | type WatcherCallBack = (newVal: T, oldVal: T, onCleanup: CleanupRegistrator) => void; 15 | 16 | type WatcherSource = Ref | (() => T); 17 | 18 | type MapSources = { 19 | [K in keyof T]: T[K] extends WatcherSource ? V : never; 20 | }; 21 | 22 | type FlushMode = 'pre' | 'post' | 'sync'; 23 | 24 | interface WatcherOption { 25 | lazy: boolean; // whether or not to delay callcack invoking 26 | deep: boolean; 27 | flush: FlushMode; 28 | } 29 | 30 | export interface VueWatcher { 31 | lazy: boolean; 32 | get(): any; 33 | teardown(): void; 34 | } 35 | 36 | let fallbackVM: ComponentInstance; 37 | 38 | function flushPreQueue(this: any) { 39 | flushQueue(this, WatcherPreFlushQueueKey); 40 | } 41 | 42 | function flushPostQueue(this: any) { 43 | flushQueue(this, WatcherPostFlushQueueKey); 44 | } 45 | 46 | function hasWatchEnv(vm: any) { 47 | return vm[WatcherPreFlushQueueKey] !== undefined; 48 | } 49 | 50 | function installWatchEnv(vm: any) { 51 | vm[WatcherPreFlushQueueKey] = []; 52 | vm[WatcherPostFlushQueueKey] = []; 53 | vm.$on('hook:beforeUpdate', flushPreQueue); 54 | vm.$on('hook:updated', flushPostQueue); 55 | } 56 | 57 | function getWatcherOption(options?: Partial): WatcherOption { 58 | return { 59 | ...{ 60 | lazy: false, 61 | deep: false, 62 | flush: 'post', 63 | }, 64 | ...options, 65 | }; 66 | } 67 | 68 | function getWatcherVM() { 69 | let vm = getCurrentVM(); 70 | if (!vm) { 71 | if (!fallbackVM) { 72 | fallbackVM = defineComponentInstance(getCurrentVue()); 73 | } 74 | vm = fallbackVM; 75 | } else if (!hasWatchEnv(vm)) { 76 | installWatchEnv(vm); 77 | } 78 | return vm; 79 | } 80 | 81 | function flushQueue(vm: any, key: any) { 82 | const queue = vm[key]; 83 | for (let index = 0; index < queue.length; index++) { 84 | queue[index](); 85 | } 86 | queue.length = 0; 87 | } 88 | 89 | function queueFlushJob(vm: any, fn: () => void, mode: Exclude) { 90 | // flush all when beforeUpdate and updated are not fired 91 | const fallbackFlush = () => { 92 | vm.$nextTick(() => { 93 | if (vm[WatcherPreFlushQueueKey].length) { 94 | flushQueue(vm, WatcherPreFlushQueueKey); 95 | } 96 | if (vm[WatcherPostFlushQueueKey].length) { 97 | flushQueue(vm, WatcherPostFlushQueueKey); 98 | } 99 | }); 100 | }; 101 | 102 | switch (mode) { 103 | case 'pre': 104 | fallbackFlush(); 105 | vm[WatcherPreFlushQueueKey].push(fn); 106 | break; 107 | case 'post': 108 | fallbackFlush(); 109 | vm[WatcherPostFlushQueueKey].push(fn); 110 | break; 111 | default: 112 | assert(false, `flush must be one of ["post", "pre", "sync"], but got ${mode}`); 113 | break; 114 | } 115 | } 116 | 117 | function createVueWatcher( 118 | vm: ComponentInstance, 119 | getter: () => any, 120 | callback: (n: any, o: any) => any, 121 | options: { 122 | deep: boolean; 123 | sync: boolean; 124 | immediateInvokeCallback?: boolean; 125 | noRun?: boolean; 126 | before?: () => void; 127 | } 128 | ): VueWatcher { 129 | const index = vm._watchers.length; 130 | // @ts-ignore: use undocumented options 131 | vm.$watch(getter, callback, { 132 | immediate: options.immediateInvokeCallback, 133 | deep: options.deep, 134 | lazy: options.noRun, 135 | sync: options.sync, 136 | before: options.before, 137 | }); 138 | 139 | return vm._watchers[index]; 140 | } 141 | 142 | // We have to monkeypatch the teardown function so Vue will run 143 | // runCleanup() when it tears down the watcher on unmmount. 144 | function patchWatcherTeardown(watcher: VueWatcher, runCleanup: () => void) { 145 | const _teardown = watcher.teardown; 146 | watcher.teardown = function(...args) { 147 | _teardown.apply(watcher, args); 148 | runCleanup(); 149 | }; 150 | } 151 | 152 | function createWatcher( 153 | vm: ComponentInstance, 154 | source: WatcherSource | WatcherSource[] | SimpleEffect, 155 | cb: WatcherCallBack | null, 156 | options: WatcherOption 157 | ): () => void { 158 | const flushMode = options.flush; 159 | const isSync = flushMode === 'sync'; 160 | let cleanup: (() => void) | null; 161 | const registerCleanup: CleanupRegistrator = (fn: () => void) => { 162 | cleanup = () => { 163 | try { 164 | fn(); 165 | } catch (error) { 166 | logError(error, vm, 'onCleanup()'); 167 | } 168 | }; 169 | }; 170 | // cleanup before running getter again 171 | const runCleanup = () => { 172 | if (cleanup) { 173 | cleanup(); 174 | cleanup = null; 175 | } 176 | }; 177 | const createScheduler = (fn: T): T => { 178 | if (isSync || /* without a current active instance, ignore pre|post mode */ vm === fallbackVM) { 179 | return fn; 180 | } 181 | return (((...args: any[]) => 182 | queueFlushJob( 183 | vm, 184 | () => { 185 | fn(...args); 186 | }, 187 | flushMode as 'pre' | 'post' 188 | )) as any) as T; 189 | }; 190 | 191 | // effect watch 192 | if (cb === null) { 193 | const getter = () => (source as SimpleEffect)(registerCleanup); 194 | const watcher = createVueWatcher(vm, getter, noopFn, { 195 | noRun: true, // take control the initial gettet invoking 196 | deep: options.deep, 197 | sync: isSync, 198 | before: runCleanup, 199 | }); 200 | 201 | patchWatcherTeardown(watcher, runCleanup); 202 | 203 | // enable the watcher update 204 | watcher.lazy = false; 205 | 206 | const originGet = watcher.get.bind(watcher); 207 | if (isSync) { 208 | watcher.get(); 209 | } else { 210 | vm.$nextTick(originGet); 211 | } 212 | watcher.get = createScheduler(originGet); 213 | 214 | return () => { 215 | watcher.teardown(); 216 | }; 217 | } 218 | 219 | let getter: () => any; 220 | if (Array.isArray(source)) { 221 | getter = () => source.map(s => (isRef(s) ? s.value : s())); 222 | } else if (isRef(source)) { 223 | getter = () => source.value; 224 | } else { 225 | getter = source as () => any; 226 | } 227 | 228 | const applyCb = (n: any, o: any) => { 229 | // cleanup before running cb again 230 | runCleanup(); 231 | cb(n, o, registerCleanup); 232 | }; 233 | let callback = createScheduler(applyCb); 234 | if (!options.lazy) { 235 | const originalCallbck = callback; 236 | // `shiftCallback` is used to handle the first sync effect run. 237 | // The subsequent callbacks will redirect to `callback`. 238 | let shiftCallback = (n: any, o: any) => { 239 | shiftCallback = originalCallbck; 240 | applyCb(n, o); 241 | }; 242 | callback = (n: any, o: any) => { 243 | shiftCallback(n, o); 244 | }; 245 | } 246 | 247 | // @ts-ignore: use undocumented option "sync" 248 | const stop = vm.$watch(getter, callback, { 249 | immediate: !options.lazy, 250 | deep: options.deep, 251 | sync: isSync, 252 | }); 253 | 254 | // Once again, we have to hack the watcher for proper teardown 255 | const watcher = vm._watchers[vm._watchers.length - 1]; 256 | patchWatcherTeardown(watcher, runCleanup); 257 | 258 | return () => { 259 | stop(); 260 | }; 261 | } 262 | 263 | export function watchEffect( 264 | effect: SimpleEffect, 265 | options?: Omit, 'lazy'> 266 | ): StopHandle { 267 | const opts = getWatcherOption(options); 268 | const vm = getWatcherVM(); 269 | return createWatcher(vm, effect, null, opts); 270 | } 271 | 272 | export function watch( 273 | source: SimpleEffect, 274 | options?: Omit, 'lazy'> 275 | ): StopHandle; 276 | export function watch( 277 | source: WatcherSource, 278 | cb: WatcherCallBack, 279 | options?: Partial 280 | ): StopHandle; 281 | export function watch[]>( 282 | sources: T, 283 | cb: (newValues: MapSources, oldValues: MapSources, onCleanup: CleanupRegistrator) => any, 284 | options?: Partial 285 | ): StopHandle; 286 | export function watch( 287 | source: WatcherSource | WatcherSource[] | SimpleEffect, 288 | cb?: Partial | WatcherCallBack, 289 | options?: Partial 290 | ): StopHandle { 291 | let callback: WatcherCallBack | null = null; 292 | if (typeof cb === 'function') { 293 | // source watch 294 | callback = cb as WatcherCallBack; 295 | } else { 296 | // effect watch 297 | if (process.env.NODE_ENV !== 'production') { 298 | warn( 299 | `\`watch(fn, options?)\` signature has been moved to a separate API. ` + 300 | `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + 301 | `supports \`watch(source, cb, options?) signature.` 302 | ); 303 | } 304 | options = cb as Partial; 305 | callback = null; 306 | } 307 | 308 | const opts = getWatcherOption(options); 309 | const vm = getWatcherVM(); 310 | 311 | return createWatcher(vm, source, callback, opts); 312 | } 313 | -------------------------------------------------------------------------------- /test/apis/state.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { reactive, ref, watch, set, toRefs, computed } = require('../../src'); 3 | 4 | describe('api/ref', () => { 5 | it('should work with array', () => { 6 | let arr; 7 | new Vue({ 8 | setup() { 9 | arr = ref([2]); 10 | arr.value.push(3); 11 | arr.value.unshift(1); 12 | }, 13 | }); 14 | expect(arr.value).toEqual([1, 2, 3]); 15 | }); 16 | 17 | it('should hold a value', () => { 18 | const a = ref(1); 19 | expect(a.value).toBe(1); 20 | a.value = 2; 21 | expect(a.value).toBe(2); 22 | }); 23 | 24 | it('should be reactive', done => { 25 | const a = ref(1); 26 | let dummy; 27 | watch(a, () => { 28 | dummy = a.value; 29 | }); 30 | expect(dummy).toBe(1); 31 | a.value = 2; 32 | waitForUpdate(() => { 33 | expect(dummy).toBe(2); 34 | }).then(done); 35 | }); 36 | 37 | it('should make nested properties reactive', done => { 38 | const a = ref({ 39 | count: 1, 40 | }); 41 | let dummy; 42 | watch( 43 | a, 44 | () => { 45 | dummy = a.value.count; 46 | }, 47 | { deep: true } 48 | ); 49 | expect(dummy).toBe(1); 50 | a.value.count = 2; 51 | waitForUpdate(() => { 52 | expect(dummy).toBe(2); 53 | }).then(done); 54 | }); 55 | }); 56 | 57 | describe('api/reactive', () => { 58 | it('should work', done => { 59 | const app = new Vue({ 60 | setup() { 61 | return { 62 | state: reactive({ 63 | count: 0, 64 | }), 65 | }; 66 | }, 67 | render(h) { 68 | return h('div', [h('span', this.state.count)]); 69 | }, 70 | }).$mount(); 71 | 72 | expect(app.$el.querySelector('span').textContent).toBe('0'); 73 | app.state.count++; 74 | waitForUpdate(() => { 75 | expect(app.$el.querySelector('span').textContent).toBe('1'); 76 | }).then(done); 77 | }); 78 | 79 | it('should warn for non-object params', () => { 80 | warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); 81 | reactive(); 82 | expect(warn.mock.calls[0][0]).toMatch( 83 | '[Vue warn]: "reactive()" is called without provide an "object".' 84 | ); 85 | reactive(false); 86 | expect(warn.mock.calls[1][0]).toMatch( 87 | '[Vue warn]: "reactive()" is called without provide an "object".' 88 | ); 89 | warn.mockRestore(); 90 | }); 91 | }); 92 | 93 | describe('api/toRefs', () => { 94 | it('should work', done => { 95 | const state = reactive({ 96 | foo: 1, 97 | bar: 2, 98 | }); 99 | 100 | let dummy; 101 | watch( 102 | () => state, 103 | () => { 104 | dummy = state.foo; 105 | } 106 | ); 107 | const stateAsRefs = toRefs(state); 108 | expect(dummy).toBe(1); 109 | expect(stateAsRefs.foo.value).toBe(1); 110 | expect(stateAsRefs.bar.value).toBe(2); 111 | state.foo++; 112 | waitForUpdate(() => { 113 | dummy = 2; 114 | expect(stateAsRefs.foo.value).toBe(2); 115 | stateAsRefs.foo.value++; 116 | }) 117 | .then(() => { 118 | dummy = 3; 119 | expect(state.foo).toBe(3); 120 | }) 121 | .then(done); 122 | }); 123 | 124 | it('should proxy plain object but not make it a reactive', () => { 125 | const spy = jest.fn(); 126 | const state = { 127 | foo: 1, 128 | bar: 2, 129 | }; 130 | 131 | watch(() => state, spy, { flush: 'sync', lazy: true }); 132 | const stateAsRefs = toRefs(state); 133 | 134 | expect(stateAsRefs.foo.value).toBe(1); 135 | expect(stateAsRefs.bar.value).toBe(2); 136 | state.foo++; 137 | expect(stateAsRefs.foo.value).toBe(2); 138 | 139 | stateAsRefs.foo.value++; 140 | expect(state.foo).toBe(3); 141 | 142 | expect(spy).not.toHaveBeenCalled(); 143 | }); 144 | }); 145 | 146 | describe('unwrapping', () => { 147 | it('should work', () => { 148 | const obj = reactive({ 149 | a: ref(0), 150 | }); 151 | const objWrapper = ref(obj); 152 | let dummy; 153 | watch( 154 | () => obj, 155 | () => { 156 | dummy = obj.a; 157 | }, 158 | { deep: true, flush: 'sync' } 159 | ); 160 | expect(dummy).toBe(0); 161 | expect(obj.a).toBe(0); 162 | expect(objWrapper.value.a).toBe(0); 163 | obj.a++; 164 | expect(dummy).toBe(1); 165 | objWrapper.value.a++; 166 | expect(dummy).toBe(2); 167 | }); 168 | 169 | it('should not unwrap a ref', () => { 170 | const a = ref(0); 171 | const b = ref(a); 172 | expect(a.value).toBe(0); 173 | expect(b.value).toBe(a); 174 | }); 175 | 176 | it('should not unwrap a ref when re-assign', () => { 177 | const a = ref('foo'); 178 | expect(a.value).toBe('foo'); 179 | const b = ref(); 180 | a.value = b; 181 | expect(a.value).toBe(b); 182 | }); 183 | 184 | it('should unwrap ref in a nested object', () => { 185 | const a = ref(0); 186 | const b = ref({ 187 | count: a, 188 | }); 189 | expect(b.value.count).toBe(0); 190 | a.value++; 191 | expect(b.value.count).toBe(1); 192 | }); 193 | 194 | it('should unwrap when re-assign', () => { 195 | const a = ref(); 196 | const b = ref(a); 197 | expect(b.value).toBe(a); 198 | const c = ref(0); 199 | b.value = { 200 | count: c, 201 | }; 202 | expect(b.value.count).toBe(0); 203 | c.value++; 204 | expect(b.value.count).toBe(1); 205 | }); 206 | 207 | it('should keep reactivity(same ref)', () => { 208 | const a = ref(1); 209 | const obj = reactive({ 210 | a, 211 | b: { 212 | c: a, 213 | }, 214 | }); 215 | let dummy1; 216 | let dummy2; 217 | watch( 218 | () => obj, 219 | () => { 220 | dummy1 = obj.a; 221 | dummy2 = obj.b.c; 222 | }, 223 | { deep: true, flush: 'sync' } 224 | ); 225 | expect(dummy1).toBe(1); 226 | expect(dummy2).toBe(1); 227 | a.value++; 228 | expect(dummy1).toBe(2); 229 | expect(dummy2).toBe(2); 230 | obj.a++; 231 | expect(dummy1).toBe(3); 232 | expect(dummy2).toBe(3); 233 | }); 234 | 235 | it('should keep reactivity(different ref)', () => { 236 | const count = ref(1); 237 | const count1 = ref(1); 238 | const obj = reactive({ 239 | a: count, 240 | b: { 241 | c: count1, 242 | }, 243 | }); 244 | 245 | let dummy1; 246 | let dummy2; 247 | watch( 248 | () => obj, 249 | () => { 250 | dummy1 = obj.a; 251 | dummy2 = obj.b.c; 252 | }, 253 | { deep: true, flush: 'sync' } 254 | ); 255 | expect(dummy1).toBe(1); 256 | expect(dummy2).toBe(1); 257 | expect(obj.a).toBe(1); 258 | expect(obj.b.c).toBe(1); 259 | obj.a++; 260 | expect(dummy1).toBe(2); 261 | expect(dummy2).toBe(1); 262 | expect(count.value).toBe(2); 263 | expect(count1.value).toBe(1); 264 | count.value++; 265 | expect(dummy1).toBe(3); 266 | expect(count.value).toBe(3); 267 | count1.value++; 268 | expect(dummy2).toBe(2); 269 | expect(count1.value).toBe(2); 270 | }); 271 | 272 | it('should keep reactivity(new property of object)', () => { 273 | const count = ref(1); 274 | const obj = reactive({ 275 | a: {}, 276 | b: [], 277 | }); 278 | let dummy; 279 | watch( 280 | () => obj, 281 | () => { 282 | dummy = obj.a.foo; 283 | }, 284 | { deep: true, flush: 'sync' } 285 | ); 286 | expect(dummy).toBe(undefined); 287 | set(obj.a, 'foo', count); 288 | expect(dummy).toBe(1); 289 | count.value++; 290 | expect(dummy).toBe(2); 291 | obj.a.foo++; 292 | expect(dummy).toBe(3); 293 | }); 294 | 295 | it('ref should be replaced)', () => { 296 | const bRef = ref(1); 297 | const obj = reactive({ 298 | a: { 299 | b: bRef, 300 | }, 301 | }); 302 | 303 | let dummy; 304 | watch( 305 | () => obj, 306 | () => { 307 | dummy = obj.a.b; 308 | }, 309 | { deep: true, lazy: true, flush: 'sync' } 310 | ); 311 | expect(dummy).toBeUndefined(); 312 | const replacedRef = ref(2); 313 | obj.a.b = replacedRef; 314 | expect(dummy).toBe(2); 315 | obj.a.b++; 316 | expect(replacedRef.value).toBe(3); 317 | expect(dummy).toBe(3); 318 | 319 | // bRef.value should not change 320 | expect(bRef.value).toBe(1); 321 | }); 322 | 323 | it('should not unwrap ref in Array index', () => { 324 | const a = ref(0); 325 | const state = reactive({ 326 | list: [a], 327 | }); 328 | 329 | expect(state.list[0]).toBe(a); 330 | expect(state.list[0].value).toBe(0); 331 | }); 332 | 333 | it('should now unwrap plain object when using set at Array', () => { 334 | const state = reactive({ 335 | list: [], 336 | }); 337 | 338 | let dummy; 339 | watch( 340 | () => state.list, 341 | () => { 342 | dummy = state.list[0].count; 343 | }, 344 | { lazy: true, flush: 'sync' } 345 | ); 346 | expect(dummy).toBeUndefined(); 347 | const a = ref(0); 348 | set(state.list, 0, { 349 | count: a, 350 | }); 351 | expect(dummy).toBe(a); 352 | }); 353 | 354 | it('should not call the computed property until accessing it', () => { 355 | const spy = jest.fn(); 356 | const state = reactive({ 357 | count: 1, 358 | double: computed(() => { 359 | spy(); 360 | return state.count * 2; 361 | }), 362 | }); 363 | 364 | expect(spy).not.toHaveBeenCalled(); 365 | expect(state.double).toBe(2); 366 | expect(spy).toHaveBeenCalled(); 367 | }); 368 | }); 369 | -------------------------------------------------------------------------------- /test/setup.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { ref, computed, createElement: h, provide, inject } = require('../src'); 3 | 4 | describe('setup', () => { 5 | beforeEach(() => { 6 | warn = jest.spyOn(global.console, 'error').mockImplementation(() => null); 7 | }); 8 | afterEach(() => { 9 | warn.mockRestore(); 10 | }); 11 | 12 | it('should works', () => { 13 | const vm = new Vue({ 14 | setup() { 15 | return { 16 | a: ref(1), 17 | }; 18 | }, 19 | }).$mount(); 20 | expect(vm.a).toBe(1); 21 | }); 22 | 23 | it('should be overrided by data option of plain object', () => { 24 | const vm = new Vue({ 25 | setup() { 26 | return { 27 | a: ref(1), 28 | }; 29 | }, 30 | data: { 31 | a: 2, 32 | }, 33 | }).$mount(); 34 | expect(vm.a).toBe(2); 35 | }); 36 | 37 | it("should access setup's value in data", () => { 38 | const vm = new Vue({ 39 | setup() { 40 | return { 41 | a: ref(1), 42 | }; 43 | }, 44 | data() { 45 | return { 46 | b: this.a, 47 | }; 48 | }, 49 | }).$mount(); 50 | expect(vm.a).toBe(1); 51 | expect(vm.b).toBe(1); 52 | }); 53 | 54 | it('should work with `methods` and `data` options', done => { 55 | let calls = 0; 56 | const vm = new Vue({ 57 | template: `
{{a}}{{b}}{{c}}
`, 58 | setup() { 59 | return { 60 | a: ref(1), 61 | }; 62 | }, 63 | beforeUpdate() { 64 | calls++; 65 | }, 66 | created() { 67 | this.m(); 68 | }, 69 | data() { 70 | return { 71 | b: this.a, 72 | c: 0, 73 | }; 74 | }, 75 | methods: { 76 | m() { 77 | this.c = this.a; 78 | }, 79 | }, 80 | }).$mount(); 81 | expect(vm.a).toBe(1); 82 | expect(vm.b).toBe(1); 83 | expect(vm.c).toBe(1); 84 | vm.a = 2; 85 | waitForUpdate(() => { 86 | expect(calls).toBe(1); 87 | expect(vm.a).toBe(2); 88 | expect(vm.b).toBe(1); 89 | expect(vm.c).toBe(1); 90 | vm.b = 2; 91 | }) 92 | .then(() => { 93 | expect(calls).toBe(2); 94 | expect(vm.a).toBe(2); 95 | expect(vm.b).toBe(2); 96 | expect(vm.c).toBe(1); 97 | }) 98 | .then(done); 99 | }); 100 | 101 | it('should receive props as first params', () => { 102 | let props; 103 | new Vue({ 104 | props: ['a'], 105 | setup(_props) { 106 | props = _props; 107 | }, 108 | propsData: { 109 | a: 1, 110 | }, 111 | }).$mount(); 112 | expect(props.a).toBe(1); 113 | }); 114 | 115 | it('warn for existing props', () => { 116 | new Vue({ 117 | props: { 118 | a: {}, 119 | }, 120 | setup() { 121 | const a = ref(); 122 | return { 123 | a, 124 | }; 125 | }, 126 | }); 127 | expect(warn.mock.calls[0][0]).toMatch( 128 | '[Vue warn]: The setup binding property "a" is already declared as a prop.' 129 | ); 130 | }); 131 | 132 | it('warn for existing instance properties', () => { 133 | new Vue({ 134 | setup(_, { _vm }) { 135 | _vm.a = 1; 136 | return { 137 | a: ref(), 138 | }; 139 | }, 140 | }); 141 | expect(warn.mock.calls[0][0]).toMatch( 142 | '[Vue warn]: The setup binding property "a" is already declared.' 143 | ); 144 | }); 145 | 146 | it('should merge result properly', () => { 147 | const injectKey = Symbol('foo'); 148 | const A = Vue.extend({ 149 | setup() { 150 | provide(injectKey, 'foo'); 151 | return { a: 1 }; 152 | }, 153 | }); 154 | const Test = Vue.extend({ 155 | extends: A, 156 | setup() { 157 | const injectVal = inject(injectKey); 158 | return { 159 | injectVal, 160 | }; 161 | }, 162 | }); 163 | let vm = new Test({ 164 | setup() { 165 | return { b: 2 }; 166 | }, 167 | }); 168 | expect(vm.a).toBe(1); 169 | expect(vm.b).toBe(2); 170 | expect(vm.injectVal).toBe('foo'); 171 | // no instance data 172 | vm = new Test(); 173 | expect(vm.a).toBe(1); 174 | // no child-val 175 | const Extended = Test.extend({}); 176 | vm = new Extended(); 177 | expect(vm.a).toBe(1); 178 | // recursively merge objects 179 | const WithObject = Vue.extend({ 180 | setup() { 181 | return { 182 | obj: { 183 | a: 1, 184 | }, 185 | }; 186 | }, 187 | }); 188 | vm = new WithObject({ 189 | setup() { 190 | return { 191 | obj: { 192 | b: 2, 193 | }, 194 | }; 195 | }, 196 | }); 197 | expect(vm.obj.a).toBe(1); 198 | expect(vm.obj.b).toBe(2); 199 | }); 200 | 201 | it('should have access to props', () => { 202 | const Test = { 203 | props: ['a'], 204 | render() {}, 205 | setup(props) { 206 | return { 207 | b: props.a, 208 | }; 209 | }, 210 | }; 211 | const vm = new Vue({ 212 | template: ``, 213 | components: { Test }, 214 | }).$mount(); 215 | expect(vm.$refs.test.b).toBe(1); 216 | }); 217 | 218 | it('props should not be reactive', done => { 219 | let calls = 0; 220 | const vm = new Vue({ 221 | template: ``, 222 | setup() { 223 | return { msg: ref('hello') }; 224 | }, 225 | beforeUpdate() { 226 | calls++; 227 | }, 228 | components: { 229 | child: { 230 | template: `{{ localMsg }}`, 231 | props: ['msg'], 232 | setup(props) { 233 | return { localMsg: props.msg, computedMsg: computed(() => props.msg + ' world') }; 234 | }, 235 | }, 236 | }, 237 | }).$mount(); 238 | const child = vm.$children[0]; 239 | expect(child.localMsg).toBe('hello'); 240 | expect(child.computedMsg).toBe('hello world'); 241 | expect(calls).toBe(0); 242 | vm.msg = 'hi'; 243 | waitForUpdate(() => { 244 | expect(child.localMsg).toBe('hello'); 245 | expect(child.computedMsg).toBe('hi world'); 246 | expect(calls).toBe(1); 247 | }).then(done); 248 | }); 249 | 250 | it('this should be undefined', () => { 251 | const vm = new Vue({ 252 | template: '
', 253 | setup() { 254 | expect(this).toBe(global); 255 | }, 256 | }).$mount(); 257 | }); 258 | 259 | it('should not make returned non-reactive object reactive', done => { 260 | const vm = new Vue({ 261 | setup() { 262 | return { 263 | form: { 264 | a: 1, 265 | b: 2, 266 | }, 267 | }; 268 | }, 269 | template: '
{{ form.a }}, {{ form.b }}
', 270 | }).$mount(); 271 | expect(vm.$el.textContent).toBe('1, 2'); 272 | 273 | // should not trigger a re-render 274 | vm.form.a = 2; 275 | waitForUpdate(() => { 276 | expect(vm.$el.textContent).toBe('1, 2'); 277 | 278 | // should trigger a re-render 279 | vm.form = { a: 2, b: 3 }; 280 | }) 281 | .then(() => { 282 | expect(vm.$el.textContent).toBe('2, 3'); 283 | }) 284 | .then(done); 285 | }); 286 | 287 | it("should put a unenumerable '__ob__' for non-reactive object", () => { 288 | const clone = obj => JSON.parse(JSON.stringify(obj)); 289 | const componentSetup = jest.fn(props => { 290 | const internalOptions = clone(props.options); 291 | return { internalOptions }; 292 | }); 293 | const ExternalComponent = { 294 | props: ['options'], 295 | setup: componentSetup, 296 | }; 297 | new Vue({ 298 | components: { ExternalComponent }, 299 | setup: () => ({ options: {} }), 300 | template: ``, 301 | }).$mount(); 302 | expect(componentSetup).toReturn(); 303 | }); 304 | 305 | it('current vue should exist in nested setup call', () => { 306 | const spy = jest.fn(); 307 | new Vue({ 308 | setup() { 309 | new Vue({ 310 | setup() { 311 | spy(1); 312 | }, 313 | }); 314 | 315 | spy(2); 316 | }, 317 | }); 318 | expect(spy.mock.calls.length).toBe(2); 319 | expect(spy).toHaveBeenNthCalledWith(1, 1); 320 | expect(spy).toHaveBeenNthCalledWith(2, 2); 321 | }); 322 | 323 | it('inline render function should receive proper params', () => { 324 | let p; 325 | const vm = new Vue({ 326 | template: ``, 327 | components: { 328 | child: { 329 | name: 'child', 330 | props: ['msg'], 331 | setup() { 332 | return props => { 333 | p = props; 334 | return null; 335 | }; 336 | }, 337 | }, 338 | }, 339 | }).$mount(); 340 | expect(p).toBe(undefined); 341 | }); 342 | 343 | it('inline render function should work', done => { 344 | // let createElement; 345 | const vm = new Vue({ 346 | props: ['msg'], 347 | template: '
1
', 348 | setup(props) { 349 | const count = ref(0); 350 | const increment = () => { 351 | count.value++; 352 | }; 353 | 354 | return () => 355 | h('div', [ 356 | h('span', props.msg), 357 | h( 358 | 'button', 359 | { 360 | on: { 361 | click: increment, 362 | }, 363 | }, 364 | count.value 365 | ), 366 | ]); 367 | }, 368 | propsData: { 369 | msg: 'foo', 370 | }, 371 | }).$mount(); 372 | expect(vm.$el.querySelector('span').textContent).toBe('foo'); 373 | expect(vm.$el.querySelector('button').textContent).toBe('0'); 374 | vm.$el.querySelector('button').click(); 375 | waitForUpdate(() => { 376 | expect(vm.$el.querySelector('button').textContent).toBe('1'); 377 | vm.msg = 'bar'; 378 | }) 379 | .then(() => { 380 | expect(vm.$el.querySelector('span').textContent).toBe('bar'); 381 | }) 382 | .then(done); 383 | }); 384 | }); 385 | -------------------------------------------------------------------------------- /test/apis/lifecycle.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { 3 | onBeforeMount, 4 | onMounted, 5 | onBeforeUpdate, 6 | onUpdated, 7 | onBeforeUnmount, 8 | onUnmounted, 9 | onErrorCaptured, 10 | } = require('../../src'); 11 | 12 | describe('Hooks lifecycle', () => { 13 | describe('beforeMount', () => { 14 | it('should not have mounted', () => { 15 | const spy = jest.fn(); 16 | const vm = new Vue({ 17 | render() {}, 18 | setup(_, { _vm }) { 19 | onBeforeMount(() => { 20 | expect(_vm._isMounted).toBe(false); 21 | expect(_vm.$el).toBeUndefined(); // due to empty mount 22 | expect(_vm._vnode).toBeNull(); 23 | expect(_vm._watcher).toBeNull(); 24 | spy(); 25 | }); 26 | }, 27 | }); 28 | expect(spy).not.toHaveBeenCalled(); 29 | vm.$mount(); 30 | expect(spy).toHaveBeenCalled(); 31 | }); 32 | }); 33 | 34 | describe('mounted', () => { 35 | it('should have mounted', () => { 36 | const spy = jest.fn(); 37 | const vm = new Vue({ 38 | template: '
', 39 | setup(_, { _vm }) { 40 | onMounted(() => { 41 | expect(_vm._isMounted).toBe(true); 42 | expect(_vm.$el.tagName).toBe('DIV'); 43 | expect(_vm._vnode.tag).toBe('div'); 44 | spy(); 45 | }); 46 | }, 47 | }); 48 | expect(spy).not.toHaveBeenCalled(); 49 | vm.$mount(); 50 | expect(spy).toHaveBeenCalled(); 51 | }); 52 | 53 | it('should call for manually mounted instance with parent', () => { 54 | const spy = jest.fn(); 55 | const parent = new Vue(); 56 | expect(spy).not.toHaveBeenCalled(); 57 | new Vue({ 58 | parent, 59 | template: '
', 60 | setup() { 61 | onMounted(() => { 62 | spy(); 63 | }); 64 | }, 65 | }).$mount(); 66 | expect(spy).toHaveBeenCalled(); 67 | }); 68 | 69 | it('should mount child parent in correct order', () => { 70 | const calls = []; 71 | new Vue({ 72 | template: '
', 73 | setup() { 74 | onMounted(() => { 75 | calls.push('parent'); 76 | }); 77 | }, 78 | components: { 79 | test: { 80 | template: '', 81 | setup(_, { _vm }) { 82 | onMounted(() => { 83 | expect(_vm.$el.parentNode).toBeTruthy(); 84 | calls.push('child'); 85 | }); 86 | }, 87 | components: { 88 | nested: { 89 | template: '
', 90 | setup(_, { _vm }) { 91 | onMounted(() => { 92 | expect(_vm.$el.parentNode).toBeTruthy(); 93 | calls.push('nested'); 94 | }); 95 | }, 96 | }, 97 | }, 98 | }, 99 | }, 100 | }).$mount(); 101 | expect(calls).toEqual(['nested', 'child', 'parent']); 102 | }); 103 | }); 104 | 105 | describe('beforeUpdate', () => { 106 | it('should be called before update', done => { 107 | const spy = jest.fn(); 108 | const vm = new Vue({ 109 | template: '
{{ msg }}
', 110 | data: { msg: 'foo' }, 111 | setup(_, { _vm }) { 112 | onBeforeUpdate(() => { 113 | expect(_vm.$el.textContent).toBe('foo'); 114 | spy(); 115 | }); 116 | }, 117 | }).$mount(); 118 | expect(spy).not.toHaveBeenCalled(); 119 | vm.msg = 'bar'; 120 | expect(spy).not.toHaveBeenCalled(); // should be async 121 | waitForUpdate(() => { 122 | expect(spy).toHaveBeenCalled(); 123 | }).then(done); 124 | }); 125 | 126 | it('should be called before render and allow mutating state', done => { 127 | const vm = new Vue({ 128 | template: '
{{ msg }}
', 129 | data: { msg: 'foo' }, 130 | setup(_, { _vm }) { 131 | onBeforeUpdate(() => { 132 | _vm.msg += '!'; 133 | }); 134 | }, 135 | }).$mount(); 136 | expect(vm.$el.textContent).toBe('foo'); 137 | vm.msg = 'bar'; 138 | waitForUpdate(() => { 139 | expect(vm.$el.textContent).toBe('bar!'); 140 | }).then(done); 141 | }); 142 | 143 | it('should not be called after destroy', done => { 144 | const beforeUpdate = jest.fn(); 145 | const destroyed = jest.fn(); 146 | 147 | Vue.component('todo', { 148 | template: '
{{todo.done}}
', 149 | props: ['todo'], 150 | setup() { 151 | onBeforeUpdate(beforeUpdate); 152 | onUnmounted(destroyed); 153 | }, 154 | }); 155 | 156 | const vm = new Vue({ 157 | template: ` 158 |
159 | 160 |
161 | `, 162 | data() { 163 | return { 164 | todos: [{ id: 1, done: false }], 165 | }; 166 | }, 167 | computed: { 168 | pendingTodos() { 169 | return this.todos.filter(t => !t.done); 170 | }, 171 | }, 172 | }).$mount(); 173 | 174 | vm.todos[0].done = true; 175 | waitForUpdate(() => { 176 | expect(destroyed).toHaveBeenCalled(); 177 | expect(beforeUpdate).not.toHaveBeenCalled(); 178 | }).then(done); 179 | }); 180 | }); 181 | 182 | describe('updated', () => { 183 | it('should be called after update', done => { 184 | const spy = jest.fn(); 185 | const vm = new Vue({ 186 | template: '
{{ msg }}
', 187 | data: { msg: 'foo' }, 188 | setup(_, { _vm }) { 189 | onUpdated(() => { 190 | expect(_vm.$el.textContent).toBe('bar'); 191 | spy(); 192 | }); 193 | }, 194 | }).$mount(); 195 | expect(spy).not.toHaveBeenCalled(); 196 | vm.msg = 'bar'; 197 | expect(spy).not.toHaveBeenCalled(); // should be async 198 | waitForUpdate(() => { 199 | expect(spy).toHaveBeenCalled(); 200 | }).then(done); 201 | }); 202 | 203 | it('should be called after children are updated', done => { 204 | const calls = []; 205 | const vm = new Vue({ 206 | template: '
{{ msg }}
', 207 | data: { msg: 'foo' }, 208 | components: { 209 | test: { 210 | template: `
`, 211 | setup(_, { _vm }) { 212 | onUpdated(() => { 213 | expect(_vm.$el.textContent).toBe('bar'); 214 | calls.push('child'); 215 | }); 216 | }, 217 | }, 218 | }, 219 | setup(_, { _vm }) { 220 | onUpdated(() => { 221 | expect(_vm.$el.textContent).toBe('bar'); 222 | calls.push('parent'); 223 | }); 224 | }, 225 | }).$mount(); 226 | 227 | expect(calls).toEqual([]); 228 | vm.msg = 'bar'; 229 | expect(calls).toEqual([]); 230 | waitForUpdate(() => { 231 | expect(calls).toEqual(['child', 'parent']); 232 | }).then(done); 233 | }); 234 | 235 | it('should not be called after destroy', done => { 236 | const updated = jest.fn(); 237 | const destroyed = jest.fn(); 238 | 239 | Vue.component('todo', { 240 | template: '
{{todo.done}}
', 241 | props: ['todo'], 242 | setup() { 243 | onUpdated(updated); 244 | onUnmounted(destroyed); 245 | }, 246 | }); 247 | 248 | const vm = new Vue({ 249 | template: ` 250 |
251 | 252 |
253 | `, 254 | data() { 255 | return { 256 | todos: [{ id: 1, done: false }], 257 | }; 258 | }, 259 | computed: { 260 | pendingTodos() { 261 | return this.todos.filter(t => !t.done); 262 | }, 263 | }, 264 | }).$mount(); 265 | 266 | vm.todos[0].done = true; 267 | waitForUpdate(() => { 268 | expect(destroyed).toHaveBeenCalled(); 269 | expect(updated).not.toHaveBeenCalled(); 270 | }).then(done); 271 | }); 272 | }); 273 | 274 | describe('beforeUnmount', () => { 275 | it('should be called before destroy', () => { 276 | const spy = jest.fn(); 277 | const vm = new Vue({ 278 | render() {}, 279 | setup(_, { _vm }) { 280 | onBeforeUnmount(() => { 281 | expect(_vm._isBeingDestroyed).toBe(false); 282 | expect(_vm._isDestroyed).toBe(false); 283 | spy(); 284 | }); 285 | }, 286 | }).$mount(); 287 | expect(spy).not.toHaveBeenCalled(); 288 | vm.$destroy(); 289 | vm.$destroy(); 290 | expect(spy).toHaveBeenCalled(); 291 | expect(spy.mock.calls.length).toBe(1); 292 | }); 293 | }); 294 | 295 | describe('unmounted', () => { 296 | it('should be called after destroy', () => { 297 | const spy = jest.fn(); 298 | const vm = new Vue({ 299 | render() {}, 300 | setup(_, { _vm }) { 301 | onUnmounted(() => { 302 | expect(_vm._isBeingDestroyed).toBe(true); 303 | expect(_vm._isDestroyed).toBe(true); 304 | spy(); 305 | }); 306 | }, 307 | }).$mount(); 308 | expect(spy).not.toHaveBeenCalled(); 309 | vm.$destroy(); 310 | vm.$destroy(); 311 | expect(spy).toHaveBeenCalled(); 312 | expect(spy.mock.calls.length).toBe(1); 313 | }); 314 | }); 315 | 316 | describe('errorCaptured', () => { 317 | let globalSpy; 318 | 319 | beforeEach(() => { 320 | globalSpy = Vue.config.errorHandler = jest.fn(); 321 | }); 322 | 323 | afterEach(() => { 324 | Vue.config.errorHandler = null; 325 | }); 326 | 327 | it('should capture error from child component', () => { 328 | const spy = jest.fn(); 329 | 330 | let child; 331 | let err; 332 | const Child = { 333 | setup(_, { _vm }) { 334 | child = _vm; 335 | err = new Error('child'); 336 | throw err; 337 | }, 338 | render() {}, 339 | }; 340 | 341 | new Vue({ 342 | setup() { 343 | onErrorCaptured(spy); 344 | }, 345 | render: h => h(Child), 346 | }).$mount(); 347 | 348 | expect(spy).toHaveBeenCalledWith(err, child, 'data()'); 349 | // should propagate by default 350 | expect(globalSpy).toHaveBeenCalledWith(err, child, 'data()'); 351 | }); 352 | }); 353 | }); 354 | -------------------------------------------------------------------------------- /test/apis/watch.spec.js: -------------------------------------------------------------------------------- 1 | const Vue = require('vue/dist/vue.common.js'); 2 | const { ref, reactive, watch, watchEffect } = require('../../src'); 3 | 4 | describe('api/watch', () => { 5 | const anyFn = expect.any(Function); 6 | let spy; 7 | beforeEach(() => { 8 | spy = jest.fn(); 9 | }); 10 | 11 | afterEach(() => { 12 | spy.mockReset(); 13 | }); 14 | 15 | it('should work', done => { 16 | const onCleanupSpy = jest.fn(); 17 | const vm = new Vue({ 18 | setup() { 19 | const a = ref(1); 20 | watch(a, (n, o, _onCleanup) => { 21 | spy(n, o, _onCleanup); 22 | _onCleanup(onCleanupSpy); 23 | }); 24 | return { 25 | a, 26 | }; 27 | }, 28 | template: `
{{a}}
`, 29 | }).$mount(); 30 | expect(spy).toBeCalledTimes(1); 31 | expect(spy).toHaveBeenLastCalledWith(1, undefined, anyFn); 32 | expect(onCleanupSpy).toHaveBeenCalledTimes(0); 33 | vm.a = 2; 34 | vm.a = 3; 35 | expect(spy).toBeCalledTimes(1); 36 | waitForUpdate(() => { 37 | expect(spy).toBeCalledTimes(2); 38 | expect(spy).toHaveBeenLastCalledWith(3, 1, anyFn); 39 | expect(onCleanupSpy).toHaveBeenCalledTimes(1); 40 | 41 | vm.$destroy(); 42 | }) 43 | .then(() => { 44 | expect(onCleanupSpy).toHaveBeenCalledTimes(2); 45 | }) 46 | .then(done); 47 | }); 48 | 49 | it('basic usage(value wrapper)', done => { 50 | const vm = new Vue({ 51 | setup() { 52 | const a = ref(1); 53 | watch(a, (n, o) => spy(n, o), { flush: 'pre' }); 54 | 55 | return { 56 | a, 57 | }; 58 | }, 59 | template: `
{{a}}
`, 60 | }).$mount(); 61 | expect(spy).toBeCalledTimes(1); 62 | expect(spy).toHaveBeenLastCalledWith(1, undefined); 63 | vm.a = 2; 64 | expect(spy).toBeCalledTimes(1); 65 | waitForUpdate(() => { 66 | expect(spy).toBeCalledTimes(2); 67 | expect(spy).toHaveBeenLastCalledWith(2, 1); 68 | }).then(done); 69 | }); 70 | 71 | it('basic usage(function)', done => { 72 | const vm = new Vue({ 73 | setup() { 74 | const a = ref(1); 75 | watch(() => a.value, (n, o) => spy(n, o)); 76 | 77 | return { 78 | a, 79 | }; 80 | }, 81 | template: `
{{a}}
`, 82 | }).$mount(); 83 | expect(spy).toBeCalledTimes(1); 84 | expect(spy).toHaveBeenLastCalledWith(1, undefined); 85 | vm.a = 2; 86 | expect(spy).toBeCalledTimes(1); 87 | waitForUpdate(() => { 88 | expect(spy).toBeCalledTimes(2); 89 | expect(spy).toHaveBeenLastCalledWith(2, 1); 90 | }).then(done); 91 | }); 92 | 93 | it('multiple cbs (after option merge)', done => { 94 | const spy1 = jest.fn(); 95 | const a = ref(1); 96 | const Test = Vue.extend({ 97 | setup() { 98 | watch(a, (n, o) => spy1(n, o)); 99 | }, 100 | }); 101 | new Test({ 102 | setup() { 103 | watch(a, (n, o) => spy(n, o)); 104 | return { 105 | a, 106 | }; 107 | }, 108 | template: `
{{a}}
`, 109 | }).$mount(); 110 | a.value = 2; 111 | waitForUpdate(() => { 112 | expect(spy1).toHaveBeenLastCalledWith(2, 1); 113 | expect(spy).toHaveBeenLastCalledWith(2, 1); 114 | }).then(done); 115 | }); 116 | 117 | it('with option: lazy', done => { 118 | const vm = new Vue({ 119 | setup() { 120 | const a = ref(1); 121 | watch(a, (n, o) => spy(n, o), { lazy: true }); 122 | 123 | return { 124 | a, 125 | }; 126 | }, 127 | template: `
{{a}}
`, 128 | }).$mount(); 129 | expect(spy).not.toHaveBeenCalled(); 130 | vm.a = 2; 131 | waitForUpdate(() => { 132 | expect(spy).toHaveBeenLastCalledWith(2, 1); 133 | }).then(done); 134 | }); 135 | 136 | it('with option: deep', done => { 137 | const vm = new Vue({ 138 | setup() { 139 | const a = ref({ b: 1 }); 140 | watch(a, (n, o) => spy(n, o), { lazy: true, deep: true }); 141 | 142 | return { 143 | a, 144 | }; 145 | }, 146 | template: `
{{a}}
`, 147 | }).$mount(); 148 | const oldA = vm.a; 149 | expect(spy).not.toHaveBeenCalled(); 150 | vm.a.b = 2; 151 | expect(spy).not.toHaveBeenCalled(); 152 | waitForUpdate(() => { 153 | expect(spy).toHaveBeenLastCalledWith(vm.a, vm.a); 154 | vm.a = { b: 3 }; 155 | }) 156 | .then(() => { 157 | expect(spy).toHaveBeenLastCalledWith(vm.a, oldA); 158 | }) 159 | .then(done); 160 | }); 161 | 162 | it('should flush after render (lazy=true)', done => { 163 | let rerenderedText; 164 | const vm = new Vue({ 165 | setup() { 166 | const a = ref(1); 167 | watch( 168 | a, 169 | (newVal, oldVal) => { 170 | spy(newVal, oldVal); 171 | rerenderedText = vm.$el.textContent; 172 | }, 173 | { lazy: true } 174 | ); 175 | return { 176 | a, 177 | }; 178 | }, 179 | render(h) { 180 | return h('div', this.a); 181 | }, 182 | }).$mount(); 183 | expect(spy).not.toHaveBeenCalled(); 184 | vm.a = 2; 185 | waitForUpdate(() => { 186 | expect(rerenderedText).toBe('2'); 187 | expect(spy).toBeCalledTimes(1); 188 | expect(spy).toHaveBeenLastCalledWith(2, 1); 189 | }).then(done); 190 | }); 191 | 192 | it('should flush after render (lazy=false)', done => { 193 | let rerenderedText; 194 | var vm = new Vue({ 195 | setup() { 196 | const a = ref(1); 197 | watch(a, (newVal, oldVal) => { 198 | spy(newVal, oldVal); 199 | if (vm) { 200 | rerenderedText = vm.$el.textContent; 201 | } 202 | }); 203 | return { 204 | a, 205 | }; 206 | }, 207 | render(h) { 208 | return h('div', this.a); 209 | }, 210 | }).$mount(); 211 | expect(spy).toBeCalledTimes(1); 212 | expect(spy).toHaveBeenLastCalledWith(1, undefined); 213 | vm.a = 2; 214 | waitForUpdate(() => { 215 | expect(rerenderedText).toBe('2'); 216 | expect(spy).toBeCalledTimes(2); 217 | expect(spy).toHaveBeenLastCalledWith(2, 1); 218 | }).then(done); 219 | }); 220 | 221 | it('should flush before render', done => { 222 | const vm = new Vue({ 223 | setup() { 224 | const a = ref(1); 225 | watch( 226 | a, 227 | (newVal, oldVal) => { 228 | spy(newVal, oldVal); 229 | expect(vm.$el.textContent).toBe('1'); 230 | }, 231 | { lazy: true, flush: 'pre' } 232 | ); 233 | return { 234 | a, 235 | }; 236 | }, 237 | render(h) { 238 | return h('div', this.a); 239 | }, 240 | }).$mount(); 241 | vm.a = 2; 242 | waitForUpdate(() => { 243 | expect(spy).toBeCalledTimes(1); 244 | expect(spy).toHaveBeenLastCalledWith(2, 1); 245 | }).then(done); 246 | }); 247 | 248 | it('should flush synchronously', done => { 249 | const vm = new Vue({ 250 | setup() { 251 | const a = ref(1); 252 | watch(a, (n, o) => spy(n, o), { lazy: true, flush: 'sync' }); 253 | return { 254 | a, 255 | }; 256 | }, 257 | render(h) { 258 | return h('div', this.a); 259 | }, 260 | }).$mount(); 261 | expect(spy).not.toHaveBeenCalled(); 262 | vm.a = 2; 263 | expect(spy).toHaveBeenLastCalledWith(2, 1); 264 | vm.a = 3; 265 | expect(spy).toHaveBeenLastCalledWith(3, 2); 266 | waitForUpdate(() => { 267 | expect(spy).toBeCalledTimes(2); 268 | }).then(done); 269 | }); 270 | 271 | it('should support watching unicode paths', done => { 272 | const vm = new Vue({ 273 | setup() { 274 | const a = ref(1); 275 | watch(a, (n, o) => spy(n, o), { lazy: true }); 276 | 277 | return { 278 | 数据: a, 279 | }; 280 | }, 281 | render(h) { 282 | return h('div', this['数据']); 283 | }, 284 | }).$mount(); 285 | expect(spy).not.toHaveBeenCalled(); 286 | vm['数据'] = 2; 287 | expect(spy).not.toHaveBeenCalled(); 288 | waitForUpdate(() => { 289 | expect(spy).toHaveBeenLastCalledWith(2, 1); 290 | }).then(done); 291 | }); 292 | 293 | it('should allow to be triggered in setup', () => { 294 | new Vue({ 295 | setup() { 296 | const count = ref(0); 297 | watch(count, (n, o) => spy(n, o), { flush: 'sync' }); 298 | count.value++; 299 | }, 300 | }); 301 | expect(spy).toBeCalledTimes(2); 302 | expect(spy).toHaveBeenNthCalledWith(1, 0, undefined); 303 | expect(spy).toHaveBeenNthCalledWith(2, 1, 0); 304 | }); 305 | 306 | it('should run in a expected order', done => { 307 | const result = []; 308 | var vm = new Vue({ 309 | setup() { 310 | const x = ref(0); 311 | 312 | // prettier-ignore 313 | watchEffect(() => { void x.value; result.push('sync effect'); }, { flush: 'sync' }); 314 | // prettier-ignore 315 | watchEffect(() => { void x.value; result.push('pre effect'); }, { flush: 'pre' }); 316 | // prettier-ignore 317 | watchEffect(() => { void x.value; result.push('post effect'); }, { flush: 'post' }); 318 | 319 | // prettier-ignore 320 | watch(x, () => { result.push('sync callback') }, { flush: 'sync' }) 321 | // prettier-ignore 322 | watch(x, () => { result.push('pre callback') }, { flush: 'pre' }) 323 | // prettier-ignore 324 | watch(x, () => { result.push('post callback') }, { flush: 'post' }) 325 | 326 | const inc = () => { 327 | result.push('before inc'); 328 | x.value++; 329 | result.push('after inc'); 330 | }; 331 | 332 | return { x, inc }; 333 | }, 334 | template: `
{{x}}
`, 335 | }).$mount(); 336 | expect(result).toEqual(['sync effect', 'sync callback', 'pre callback', 'post callback']); 337 | result.length = 0; 338 | 339 | waitForUpdate(() => { 340 | expect(result).toEqual(['pre effect', 'post effect']); 341 | result.length = 0; 342 | 343 | vm.inc(); 344 | }) 345 | .then(() => { 346 | expect(result).toEqual([ 347 | 'before inc', 348 | 'sync effect', 349 | 'sync callback', 350 | 'after inc', 351 | 'pre effect', 352 | 'pre callback', 353 | 'post effect', 354 | 'post callback', 355 | ]); 356 | }) 357 | .then(done); 358 | }); 359 | 360 | describe('simple effect', () => { 361 | let renderedText; 362 | it('should work', done => { 363 | let onCleanup; 364 | const onCleanupSpy = jest.fn(); 365 | const vm = new Vue({ 366 | setup() { 367 | const count = ref(0); 368 | watchEffect(_onCleanup => { 369 | onCleanup = _onCleanup; 370 | _onCleanup(onCleanupSpy); 371 | spy(count.value); 372 | renderedText = vm.$el.textContent; 373 | }); 374 | 375 | return { 376 | count, 377 | }; 378 | }, 379 | render(h) { 380 | return h('div', this.count); 381 | }, 382 | }).$mount(); 383 | expect(spy).not.toHaveBeenCalled(); 384 | waitForUpdate(() => { 385 | expect(onCleanup).toEqual(anyFn); 386 | expect(onCleanupSpy).toHaveBeenCalledTimes(0); 387 | expect(renderedText).toBe('0'); 388 | expect(spy).toHaveBeenLastCalledWith(0); 389 | vm.count++; 390 | }) 391 | .then(() => { 392 | expect(renderedText).toBe('1'); 393 | expect(spy).toHaveBeenLastCalledWith(1); 394 | expect(onCleanupSpy).toHaveBeenCalledTimes(1); 395 | vm.$destroy(); 396 | }) 397 | .then(() => { 398 | expect(onCleanupSpy).toHaveBeenCalledTimes(2); 399 | }) 400 | .then(done); 401 | }); 402 | 403 | it('sync=true', () => { 404 | const vm = new Vue({ 405 | setup() { 406 | const count = ref(0); 407 | watchEffect( 408 | () => { 409 | spy(count.value); 410 | }, 411 | { 412 | flush: 'sync', 413 | } 414 | ); 415 | 416 | return { 417 | count, 418 | }; 419 | }, 420 | }); 421 | expect(spy).toHaveBeenLastCalledWith(0); 422 | vm.count++; 423 | expect(spy).toHaveBeenLastCalledWith(1); 424 | }); 425 | }); 426 | 427 | describe('Multiple sources', () => { 428 | let obj1, obj2; 429 | it('do not store the intermediate state', done => { 430 | new Vue({ 431 | setup() { 432 | obj1 = reactive({ a: 1 }); 433 | obj2 = reactive({ a: 2 }); 434 | watch([() => obj1.a, () => obj2.a], (n, o) => spy(n, o)); 435 | return { 436 | obj1, 437 | obj2, 438 | }; 439 | }, 440 | template: `
{{obj1.a}} {{obj2.a}}
`, 441 | }).$mount(); 442 | expect(spy).toBeCalledTimes(1); 443 | expect(spy).toHaveBeenLastCalledWith([1, 2], undefined); 444 | obj1.a = 2; 445 | obj2.a = 3; 446 | 447 | obj1.a = 3; 448 | obj2.a = 4; 449 | waitForUpdate(() => { 450 | expect(spy).toBeCalledTimes(2); 451 | expect(spy).toHaveBeenLastCalledWith([3, 4], [1, 2]); 452 | obj2.a = 5; 453 | obj2.a = 6; 454 | }) 455 | .then(() => { 456 | expect(spy).toBeCalledTimes(3); 457 | expect(spy).toHaveBeenLastCalledWith([3, 6], [3, 4]); 458 | }) 459 | .then(done); 460 | }); 461 | 462 | it('basic usage(lazy=false, flush=none-sync)', done => { 463 | const vm = new Vue({ 464 | setup() { 465 | const a = ref(1); 466 | const b = ref(1); 467 | watch([a, b], (n, o) => spy(n, o), { lazy: false, flush: 'post' }); 468 | 469 | return { 470 | a, 471 | b, 472 | }; 473 | }, 474 | template: `
{{a}} {{b}}
`, 475 | }).$mount(); 476 | expect(spy).toBeCalledTimes(1); 477 | expect(spy).toHaveBeenLastCalledWith([1, 1], undefined); 478 | vm.a = 2; 479 | expect(spy).toBeCalledTimes(1); 480 | waitForUpdate(() => { 481 | expect(spy).toBeCalledTimes(2); 482 | expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); 483 | vm.a = 3; 484 | vm.b = 3; 485 | }) 486 | .then(() => { 487 | expect(spy).toBeCalledTimes(3); 488 | expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); 489 | }) 490 | .then(done); 491 | }); 492 | 493 | it('basic usage(lazy=true, flush=none-sync)', done => { 494 | const vm = new Vue({ 495 | setup() { 496 | const a = ref(1); 497 | const b = ref(1); 498 | watch([a, b], (n, o) => spy(n, o), { lazy: true, flush: 'post' }); 499 | 500 | return { 501 | a, 502 | b, 503 | }; 504 | }, 505 | template: `
{{a}} {{b}}
`, 506 | }).$mount(); 507 | vm.a = 2; 508 | expect(spy).not.toHaveBeenCalled(); 509 | waitForUpdate(() => { 510 | expect(spy).toBeCalledTimes(1); 511 | expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); 512 | vm.a = 3; 513 | vm.b = 3; 514 | }) 515 | .then(() => { 516 | expect(spy).toBeCalledTimes(2); 517 | expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); 518 | }) 519 | .then(done); 520 | }); 521 | 522 | it('basic usage(lazy=false, flush=sync)', () => { 523 | const vm = new Vue({ 524 | setup() { 525 | const a = ref(1); 526 | const b = ref(1); 527 | watch([a, b], (n, o) => spy(n, o), { lazy: false, flush: 'sync' }); 528 | 529 | return { 530 | a, 531 | b, 532 | }; 533 | }, 534 | }); 535 | expect(spy).toBeCalledTimes(1); 536 | expect(spy).toHaveBeenLastCalledWith([1, 1], undefined); 537 | vm.a = 2; 538 | expect(spy).toBeCalledTimes(2); 539 | expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); 540 | vm.a = 3; 541 | vm.b = 3; 542 | expect(spy.mock.calls.length).toBe(4); 543 | expect(spy).toHaveBeenNthCalledWith(3, [3, 1], [2, 1]); 544 | expect(spy).toHaveBeenNthCalledWith(4, [3, 3], [3, 1]); 545 | }); 546 | 547 | it('basic usage(lazy=true, flush=sync)', () => { 548 | const vm = new Vue({ 549 | setup() { 550 | const a = ref(1); 551 | const b = ref(1); 552 | watch([a, b], (n, o) => spy(n, o), { lazy: true, flush: 'sync' }); 553 | 554 | return { 555 | a, 556 | b, 557 | }; 558 | }, 559 | }); 560 | expect(spy).not.toHaveBeenCalled(); 561 | vm.a = 2; 562 | expect(spy).toBeCalledTimes(1); 563 | expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); 564 | vm.a = 3; 565 | vm.b = 3; 566 | expect(spy).toBeCalledTimes(3); 567 | expect(spy).toHaveBeenNthCalledWith(2, [3, 1], [2, 1]); 568 | expect(spy).toHaveBeenNthCalledWith(3, [3, 3], [3, 1]); 569 | }); 570 | }); 571 | 572 | describe('Out of setup', () => { 573 | it('should work', done => { 574 | const obj = reactive({ a: 1 }); 575 | watch(() => obj.a, (n, o) => spy(n, o)); 576 | expect(spy).toHaveBeenLastCalledWith(1, undefined); 577 | obj.a = 2; 578 | waitForUpdate(() => { 579 | expect(spy).toBeCalledTimes(2); 580 | expect(spy).toHaveBeenLastCalledWith(2, 1); 581 | }).then(done); 582 | }); 583 | 584 | it('simple effect', done => { 585 | const obj = reactive({ a: 1 }); 586 | watchEffect(() => spy(obj.a)); 587 | expect(spy).not.toHaveBeenCalled(); 588 | waitForUpdate(() => { 589 | expect(spy).toBeCalledTimes(1); 590 | expect(spy).toHaveBeenLastCalledWith(1); 591 | obj.a = 2; 592 | }) 593 | .then(() => { 594 | expect(spy).toBeCalledTimes(2); 595 | expect(spy).toHaveBeenLastCalledWith(2); 596 | }) 597 | .then(done); 598 | }); 599 | }); 600 | 601 | describe('cleanup', () => { 602 | function getAsyncValue(val) { 603 | let handle; 604 | let resolve; 605 | const p = new Promise(_resolve => { 606 | resolve = _resolve; 607 | handle = setTimeout(() => { 608 | resolve(val); 609 | }, 0); 610 | }); 611 | 612 | p.cancel = () => { 613 | clearTimeout(handle); 614 | resolve('canceled'); 615 | }; 616 | return p; 617 | } 618 | 619 | it('work with effect', done => { 620 | const id = ref(1); 621 | const promises = []; 622 | watchEffect(onCleanup => { 623 | const val = getAsyncValue(id.value); 624 | promises.push(val); 625 | onCleanup(() => { 626 | val.cancel(); 627 | }); 628 | }); 629 | waitForUpdate(() => { 630 | id.value = 2; 631 | }) 632 | .thenWaitFor(async next => { 633 | const values = await Promise.all(promises); 634 | expect(values).toEqual(['canceled', 2]); 635 | next(); 636 | }) 637 | .then(done); 638 | }); 639 | 640 | it('run cleanup when watch stops (effect)', done => { 641 | const spy = jest.fn(); 642 | const cleanup = jest.fn(); 643 | const stop = watchEffect(onCleanup => { 644 | spy(); 645 | onCleanup(cleanup); 646 | }); 647 | waitForUpdate(() => { 648 | expect(spy).toHaveBeenCalled(); 649 | stop(); 650 | }) 651 | .then(() => { 652 | expect(cleanup).toHaveBeenCalled(); 653 | }) 654 | .then(done); 655 | }); 656 | 657 | it('run cleanup when watch stops', () => { 658 | const id = ref(1); 659 | const spy = jest.fn(); 660 | const cleanup = jest.fn(); 661 | const stop = watch(id, (value, oldValue, onCleanup) => { 662 | spy(value); 663 | onCleanup(cleanup); 664 | }); 665 | 666 | expect(spy).toHaveBeenCalledWith(1); 667 | stop(); 668 | expect(cleanup).toHaveBeenCalled(); 669 | }); 670 | 671 | it('should not collect reactive in onCleanup', done => { 672 | const ref1 = ref(1); 673 | const ref2 = ref(1); 674 | watchEffect(onCleanup => { 675 | spy(ref1.value); 676 | onCleanup(() => { 677 | ref2.value = ref2.value + 1; 678 | }); 679 | }); 680 | waitForUpdate(() => { 681 | expect(spy).toBeCalledTimes(1); 682 | expect(spy).toHaveBeenLastCalledWith(1); 683 | ref1.value++; 684 | }) 685 | .then(() => { 686 | expect(spy).toBeCalledTimes(2); 687 | expect(spy).toHaveBeenLastCalledWith(2); 688 | ref2.value = 10; 689 | }) 690 | .then(() => { 691 | expect(spy).toBeCalledTimes(2); 692 | }) 693 | .then(done); 694 | }); 695 | 696 | it('work with callback ', done => { 697 | const id = ref(1); 698 | const promises = []; 699 | watch(id, (newVal, oldVal, onCleanup) => { 700 | const val = getAsyncValue(newVal); 701 | promises.push(val); 702 | onCleanup(() => { 703 | val.cancel(); 704 | }); 705 | }); 706 | id.value = 2; 707 | waitForUpdate() 708 | .thenWaitFor(async next => { 709 | const values = await Promise.all(promises); 710 | expect(values).toEqual(['canceled', 2]); 711 | next(); 712 | }) 713 | .then(done); 714 | }); 715 | }); 716 | }); 717 | --------------------------------------------------------------------------------