From c54aa43fa738461c9f16a444942ddd8a558bebaa Mon Sep 17 00:00:00 2001 From: Luxiaosong <2878354193@qq.com> Date: Fri, 21 Feb 2020 18:21:24 +0800 Subject: [PATCH 01/21] chore: use 'const' instead of 'let' (#755) [ci skip] --- packages/compiler-core/src/parse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index f856d06027d..dbadeff771b 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -694,7 +694,7 @@ function parseAttributeValue( if (!match) { return undefined } - let unexpectedChars = /["'<=`]/g + const unexpectedChars = /["'<=`]/g let m: RegExpExecArray | null while ((m = unexpectedChars.exec(match[0])) !== null) { emitError( From 5fac65589b4455b98fd4e2f9eb3754f0acde97bb Mon Sep 17 00:00:00 2001 From: guaijie <30885718+guaijie@users.noreply.github.com> Date: Fri, 21 Feb 2020 18:38:07 +0800 Subject: [PATCH 02/21] fix(reactivity): should trigger all effects when array length is mutated (#754) --- packages/reactivity/__tests__/effect.spec.ts | 24 ++++++++++++++++++++ packages/reactivity/src/effect.ts | 5 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index 369cda983ff..3ce0748b539 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -737,6 +737,30 @@ describe('reactivity/effect', () => { expect(fnSpy).toHaveBeenCalledTimes(1) }) + it('should trigger all effects when array lenght is set 0', () => { + const observed: any = reactive([1]) + let dummy, record + effect(() => { + dummy = observed.length + }) + effect(() => { + record = observed[0] + }) + expect(dummy).toBe(1) + expect(record).toBe(1) + + observed[1] = 2 + expect(observed[1]).toBe(2) + + observed.unshift(3) + expect(dummy).toBe(3) + expect(record).toBe(3) + + observed.length = 0 + expect(dummy).toBe(0) + expect(record).toBeUndefined() + }) + it('should handle self dependency mutations', () => { const count = ref(0) effect(() => { diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index f4cac6d4edc..03326426d81 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -174,8 +174,9 @@ export function trigger( } const effects = new Set() const computedRunners = new Set() - if (type === TriggerOpTypes.CLEAR) { - // collection being cleared, trigger all effects for target + if (type === TriggerOpTypes.CLEAR || (key === 'length' && isArray(target))) { + // collection being cleared or Array length mutation + // trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) From 189a0a3b1944dcb52b846a6053d8aeceb29b23a4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Feb 2020 11:27:08 +0100 Subject: [PATCH 03/21] chore: use undefined for TS 3.8 compat --- packages/runtime-dom/__tests__/modules/style.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-dom/__tests__/modules/style.spec.ts b/packages/runtime-dom/__tests__/modules/style.spec.ts index 28eac3b426b..08667275081 100644 --- a/packages/runtime-dom/__tests__/modules/style.spec.ts +++ b/packages/runtime-dom/__tests__/modules/style.spec.ts @@ -21,7 +21,7 @@ describe(`module style`, () => { it('remove if falsy value', () => { const el = document.createElement('div') - patchStyle(el, { color: 'red' }, { color: null }) + patchStyle(el, { color: 'red' }, { color: undefined }) expect(el.style.cssText.replace(/\s/g, '')).toBe('') }) From 1b9b235663b75db040172d2ffbee1dd40b4db032 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Feb 2020 13:10:13 +0100 Subject: [PATCH 04/21] fix(compiler-dom): properly stringify class/style bindings when hoisting static strings --- .../transforms/transformElement.spec.ts | 4 ++-- .../__snapshots__/index.spec.ts.snap | 5 +--- packages/compiler-dom/__tests__/index.spec.ts | 2 +- .../transforms/stringifyStatic.spec.ts | 8 +++---- .../transforms/transformStyle.spec.ts | 17 ++++--------- .../src/transforms/stringifyStatic.ts | 16 ++++++++++--- .../src/transforms/transformStyle.ts | 5 ++-- .../compiler-ssr/__tests__/ssrElement.spec.ts | 4 ++-- .../compiler-ssr/__tests__/ssrVShow.spec.ts | 12 +++------- .../src/helpers/ssrRenderAttrs.ts | 18 ++------------ packages/shared/src/normalizeProp.ts | 24 ++++++++++++++++++- 11 files changed, 57 insertions(+), 58 deletions(-) diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index b62a27a7557..cc3cbd480a5 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -623,7 +623,7 @@ describe('compiler: element transform', () => { test(`props merging: style`, () => { const { node } = parseWithElementTransform( - `
`, + `
`, { nodeTransforms: [transformStyle, transformElement], directiveTransforms: { @@ -646,7 +646,7 @@ describe('compiler: element transform', () => { elements: [ { type: NodeTypes.SIMPLE_EXPRESSION, - content: `_hoisted_1`, + content: `{"color":"green"}`, isStatic: false }, { diff --git a/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap b/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap index b429387fd6d..40e14d51f18 100644 --- a/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap @@ -2,9 +2,6 @@ exports[`compile should contain standard transforms 1`] = ` "const _Vue = Vue -const { createVNode: _createVNode } = _Vue - -const _hoisted_1 = {} return function render(_ctx, _cache) { with (_ctx) { @@ -14,7 +11,7 @@ return function render(_ctx, _cache) { _createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]), _createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]), _createVNode(\\"div\\", null, \\"test\\"), - _createVNode(\\"div\\", { style: _hoisted_1 }, \\"red\\"), + _createVNode(\\"div\\", { style: {\\"color\\":\\"red\\"} }, \\"red\\"), _createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */) ], 64 /* STABLE_FRAGMENT */)) } diff --git a/packages/compiler-dom/__tests__/index.spec.ts b/packages/compiler-dom/__tests__/index.spec.ts index d40bf36fe2c..db43ce905cf 100644 --- a/packages/compiler-dom/__tests__/index.spec.ts +++ b/packages/compiler-dom/__tests__/index.spec.ts @@ -5,7 +5,7 @@ describe('compile', () => { const { code } = compile(`
test
-
red
+
red
`) expect(code).toMatchSnapshot() diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index d1095d3f425..cc8b53f245d 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -77,8 +77,8 @@ describe('stringify static html', () => { test('serliazing constant bindings', () => { const { ast } = compileWithStringify( - `
${repeat( - `{{ 1 }} + {{ false }}`, + `
${repeat( + `{{ 1 }} + {{ false }}`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT )}
` ) @@ -89,8 +89,8 @@ describe('stringify static html', () => { callee: CREATE_STATIC, arguments: [ JSON.stringify( - `
${repeat( - `1 + false`, + `
${repeat( + `1 + false`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT )}
` ) diff --git a/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts b/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts index 89121c10a26..6eee9f8ba38 100644 --- a/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts @@ -26,17 +26,8 @@ function transformWithStyleTransform( } describe('compiler: style transform', () => { - test('should transform into directive node and hoist value', () => { - const { root, node } = transformWithStyleTransform( - `
` - ) - expect(root.hoists).toMatchObject([ - { - type: NodeTypes.SIMPLE_EXPRESSION, - content: `{"color":"red"}`, - isStatic: false - } - ]) + test('should transform into directive node', () => { + const { node } = transformWithStyleTransform(`
`) expect(node.props[0]).toMatchObject({ type: NodeTypes.DIRECTIVE, name: `bind`, @@ -47,7 +38,7 @@ describe('compiler: style transform', () => { }, exp: { type: NodeTypes.SIMPLE_EXPRESSION, - content: `_hoisted_1`, + content: `{"color":"red"}`, isStatic: false } }) @@ -71,7 +62,7 @@ describe('compiler: style transform', () => { }, value: { type: NodeTypes.SIMPLE_EXPRESSION, - content: `_hoisted_1`, + content: `{"color":"red"}`, isStatic: false } } diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index a2efa56eec0..a4aa7745f8e 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -14,7 +14,10 @@ import { isString, isSymbol, escapeHtml, - toDisplayString + toDisplayString, + normalizeClass, + normalizeStyle, + stringifyStyle } from '@vue/shared' // Turn eligible hoisted static trees into stringied static nodes, e.g. @@ -84,8 +87,15 @@ function stringifyElement( } } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { // constant v-bind, e.g. :foo="1" + let evaluated = evaluateConstant(p.exp as SimpleExpressionNode) + const arg = p.arg && (p.arg as SimpleExpressionNode).content + if (arg === 'class') { + evaluated = normalizeClass(evaluated) + } else if (arg === 'style') { + evaluated = stringifyStyle(normalizeStyle(evaluated)) + } res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml( - evaluateConstant(p.exp as ExpressionNode) + evaluated )}"` } } @@ -151,7 +161,7 @@ function evaluateConstant(exp: ExpressionNode): string { if (c.type === NodeTypes.TEXT) { res += c.content } else if (c.type === NodeTypes.INTERPOLATION) { - res += evaluateConstant(c.content) + res += toDisplayString(evaluateConstant(c.content)) } else { res += evaluateConstant(c) } diff --git a/packages/compiler-dom/src/transforms/transformStyle.ts b/packages/compiler-dom/src/transforms/transformStyle.ts index 3c232db4357..765fab778e9 100644 --- a/packages/compiler-dom/src/transforms/transformStyle.ts +++ b/packages/compiler-dom/src/transforms/transformStyle.ts @@ -17,12 +17,11 @@ export const transformStyle: NodeTransform = (node, context) => { node.props.forEach((p, i) => { if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) { // replace p with an expression node - const exp = context.hoist(parseInlineCSS(p.value.content, p.loc)) node.props[i] = { type: NodeTypes.DIRECTIVE, name: `bind`, arg: createSimpleExpression(`style`, true, p.loc), - exp, + exp: parseInlineCSS(p.value.content, p.loc), modifiers: [], loc: p.loc } @@ -45,5 +44,5 @@ function parseInlineCSS( tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim()) } }) - return createSimpleExpression(JSON.stringify(res), false, loc) + return createSimpleExpression(JSON.stringify(res), false, loc, true) } diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index c99d13c543a..c81d6ccb10e 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -101,7 +101,7 @@ describe('ssr: element', () => { expect( getCompiledString(`
`) ).toMatchInlineSnapshot( - `"\`
\`"` + `"\`
\`"` ) }) @@ -184,7 +184,7 @@ describe('ssr: element', () => { ) ).toMatchInlineSnapshot(` "\`
\`" `) }) diff --git a/packages/compiler-ssr/__tests__/ssrVShow.spec.ts b/packages/compiler-ssr/__tests__/ssrVShow.spec.ts index 2738bc22617..8a99a52c7d3 100644 --- a/packages/compiler-ssr/__tests__/ssrVShow.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVShow.spec.ts @@ -16,11 +16,9 @@ describe('ssr: v-show', () => { .toMatchInlineSnapshot(` "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\") - const _hoisted_1 = {\\"color\\":\\"red\\"} - return function ssrRender(_ctx, _push, _parent) { _push(\`
\`) }" @@ -48,11 +46,9 @@ describe('ssr: v-show', () => { ).toMatchInlineSnapshot(` "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\") - const _hoisted_1 = {\\"color\\":\\"red\\"} - return function ssrRender(_ctx, _push, _parent) { _push(\`
\`) @@ -69,12 +65,10 @@ describe('ssr: v-show', () => { "const { mergeProps: _mergeProps } = require(\\"vue\\") const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\") - const _hoisted_1 = {\\"color\\":\\"red\\"} - return function ssrRender(_ctx, _push, _parent) { _push(\` | undefined +): string { + let ret = '' + if (!styles) { + return ret + } + for (const key in styles) { + const value = styles[key] + const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key) + if ( + isString(value) || + (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey)) + ) { + // only render valid values + ret += `${normalizedKey}:${value};` + } + } + return ret +} + export function normalizeClass(value: unknown): string { let res = '' if (isString(value)) { From 9547c2b93d6d8f469314cfe055960746a3e3acbe Mon Sep 17 00:00:00 2001 From: hareku Date: Fri, 21 Feb 2020 22:44:13 +0900 Subject: [PATCH 05/21] fix(sfc): inherit parent scopeId on child rooot (#756) --- .../__tests__/helpers/scopeId.spec.ts | 23 ++++++++++++++++++- .../runtime-core/src/componentRenderUtils.ts | 7 ++++++ .../__tests__/renderToString.spec.ts | 2 +- .../__tests__/ssrRenderAttrs.spec.ts | 8 +++++++ .../src/helpers/ssrRenderAttrs.ts | 4 +++- 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts index 08f7b1e7d50..b14d3acbc44 100644 --- a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts +++ b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts @@ -17,6 +17,27 @@ describe('scopeId runtime support', () => { expect(serializeInner(root)).toBe(`
`) }) + test('should attach scopeId to components in parent component', () => { + const Child = { + __scopeId: 'child', + render: withChildId(() => { + return h('div') + }) + } + const App = { + __scopeId: 'parent', + render: withParentId(() => { + return h('div', [h(Child)]) + }) + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe( + `
` + ) + }) + test('should work on slots', () => { const Child = { __scopeId: 'child', @@ -41,7 +62,7 @@ describe('scopeId runtime support', () => { // - scopeId from parent // - slotted scopeId (with `-s` postfix) from child (the tree owner) expect(serializeInner(root)).toBe( - `
` + `
` ) }) }) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 145a3697c5c..6b3be672792 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -39,6 +39,7 @@ export function renderComponentRoot( ): VNode { const { type: Component, + parent, vnode, proxy, withProxy, @@ -102,6 +103,11 @@ export function renderComponentRoot( if (vnodeHooks !== EMPTY_OBJ) { result = cloneVNode(result, vnodeHooks) } + // inherit scopeId + const parentScopeId = parent && parent.type.__scopeId + if (parentScopeId) { + result = cloneVNode(result, { [parentScopeId]: '' }) + } // inherit directives if (vnode.dirs != null) { if (__DEV__ && !isElementRoot(result)) { @@ -127,6 +133,7 @@ export function renderComponentRoot( result = createVNode(Comment) } currentRenderingInstance = null + return result } diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index 2ac929ac1c4..c8bb79c8746 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -562,7 +562,7 @@ describe('ssr: renderToString', () => { } expect(await renderToString(h(Parent))).toBe( - `
slot
` + `
slot
` ) }) }) diff --git a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts index 87521a49aae..38ca843c4db 100644 --- a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts +++ b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts @@ -26,6 +26,14 @@ describe('ssr: renderAttrs', () => { ).toBe(` id="foo" title="bar"`) }) + test('empty value attrs', () => { + expect( + ssrRenderAttrs({ + 'data-v-abc': '' + }) + ).toBe(` data-v-abc`) + }) + test('escape attrs', () => { expect( ssrRenderAttrs({ diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts index ef07bac30f1..d17c0dd11d6 100644 --- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts +++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts @@ -53,7 +53,9 @@ export function ssrRenderDynamicAttr( if (isBooleanAttr(attrKey)) { return value === false ? `` : ` ${attrKey}` } else if (isSSRSafeAttrName(attrKey)) { - return ` ${attrKey}="${escapeHtml(value)}"` + return value === '' + ? ` ${attrKey}` + : ` ${attrKey}="${escapeHtml(value)}"` } else { console.warn( `[@vue/server-renderer] Skipped rendering unsafe attribute name: ${attrKey}` From 014acc13e95133ef022ad051683a39e8f15388fb Mon Sep 17 00:00:00 2001 From: hareku Date: Fri, 21 Feb 2020 22:44:41 +0900 Subject: [PATCH 06/21] test(reactivity): test for ref without init value (#746) --- packages/reactivity/__tests__/ref.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index b822e801bbf..84f1c8dff5a 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -33,6 +33,17 @@ describe('reactivity/ref', () => { expect(dummy).toBe(2) }) + it('should work without initial value', () => { + const a = ref() + let dummy + effect(() => { + dummy = a.value + }) + expect(dummy).toBe(undefined) + a.value = 2 + expect(dummy).toBe(2) + }) + it('should work like a normal property when nested in a reactive object', () => { const a = ref(1) const obj = reactive({ From 932de431b1f8ec218daa95d471a5b1deab92720a Mon Sep 17 00:00:00 2001 From: Reed Jones Date: Fri, 21 Feb 2020 06:44:58 -0700 Subject: [PATCH 07/21] chore: typo (#744) [ci skip] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a5353cb1e..540b2a70d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ ### Code Refactoring -* **watch:** adjsut watch API behavior ([9571ede](https://github.com/vuejs/vue-next/commit/9571ede84bb6949e13c25807cc8f016ace29dc8a)) +* **watch:** adjust watch API behavior ([9571ede](https://github.com/vuejs/vue-next/commit/9571ede84bb6949e13c25807cc8f016ace29dc8a)) ### Features From 9882788cb358cb2cd1bd39d30513c46387897e49 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2020 13:46:21 +0000 Subject: [PATCH 08/21] build(deps-dev): bump typescript from 3.7.5 to 3.8.2 Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.7.5 to 3.8.2. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/commits) Signed-off-by: dependabot-preview[bot] --- yarn.lock | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 804622e3b1d..074f742af49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6241,7 +6241,12 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.7.0, typescript@~3.7.2: +typescript@^3.7.0: + version "3.8.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a" + integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ== + +typescript@~3.7.2: version "3.7.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== From 33622d63600ba0f18ba4dae97bda882c918b5f7d Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Feb 2020 15:05:16 +0100 Subject: [PATCH 09/21] perf(reactivity): only trigger all effects on Array length mutation if new length is shorter than old length --- packages/reactivity/src/baseHandlers.ts | 25 +++--------- packages/reactivity/src/collectionHandlers.ts | 39 ++++--------------- packages/reactivity/src/effect.ts | 25 ++++++++++-- 3 files changed, 34 insertions(+), 55 deletions(-) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 43b6df903bd..3bc03e0a7f6 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -91,20 +91,10 @@ function createSetter(isReadonly = false, shallow = false) { const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { - /* istanbul ignore else */ - if (__DEV__) { - const extraInfo = { oldValue, newValue: value } - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key, extraInfo) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key, extraInfo) - } - } else { - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key) - } + if (!hadKey) { + trigger(target, TriggerOpTypes.ADD, key, value) + } else if (hasChanged(value, oldValue)) { + trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result @@ -116,12 +106,7 @@ function deleteProperty(target: object, key: string | symbol): boolean { const oldValue = (target as any)[key] const result = Reflect.deleteProperty(target, key) if (result && hadKey) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.DELETE, key, { oldValue }) - } else { - trigger(target, TriggerOpTypes.DELETE, key) - } + trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result } diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index cb90cf0e3c2..af4ca05d17f 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -51,12 +51,7 @@ function add(this: SetTypes, value: unknown) { const hadKey = proto.has.call(target, value) const result = proto.add.call(target, value) if (!hadKey) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.ADD, value, { newValue: value }) - } else { - trigger(target, TriggerOpTypes.ADD, value) - } + trigger(target, TriggerOpTypes.ADD, value, value) } return result } @@ -69,20 +64,10 @@ function set(this: MapTypes, key: unknown, value: unknown) { const hadKey = proto.has.call(target, key) const oldValue = proto.get.call(target, key) const result = proto.set.call(target, key, value) - /* istanbul ignore else */ - if (__DEV__) { - const extraInfo = { oldValue, newValue: value } - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key, extraInfo) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key, extraInfo) - } - } else { - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key) - } + if (!hadKey) { + trigger(target, TriggerOpTypes.ADD, key, value) + } else if (hasChanged(value, oldValue)) { + trigger(target, TriggerOpTypes.SET, key, value, oldValue) } return result } @@ -96,12 +81,7 @@ function deleteEntry(this: CollectionTypes, key: unknown) { // forward the operation before queueing reactions const result = proto.delete.call(target, key) if (hadKey) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.DELETE, key, { oldValue }) - } else { - trigger(target, TriggerOpTypes.DELETE, key) - } + trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result } @@ -117,12 +97,7 @@ function clear(this: IterableCollections) { // forward the operation before queueing reactions const result = getProto(target).clear.call(target) if (hadItems) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.CLEAR, void 0, { oldTarget }) - } else { - trigger(target, TriggerOpTypes.CLEAR) - } + trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget) } return result } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index 03326426d81..c0f4707d83d 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -165,7 +165,9 @@ export function trigger( target: object, type: TriggerOpTypes, key?: unknown, - extraInfo?: DebuggerEventExtraInfo + newValue?: unknown, + oldValue?: unknown, + oldTarget?: Map | Set ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { @@ -174,7 +176,12 @@ export function trigger( } const effects = new Set() const computedRunners = new Set() - if (type === TriggerOpTypes.CLEAR || (key === 'length' && isArray(target))) { + if ( + type === TriggerOpTypes.CLEAR || + (key === 'length' && + isArray(target) && + (newValue as number) < (oldValue as number)) + ) { // collection being cleared or Array length mutation // trigger all effects for target depsMap.forEach(dep => { @@ -196,7 +203,19 @@ export function trigger( } } const run = (effect: ReactiveEffect) => { - scheduleRun(effect, target, type, key, extraInfo) + scheduleRun( + effect, + target, + type, + key, + __DEV__ + ? { + newValue, + oldValue, + oldTarget + } + : undefined + ) } // Important: computed effects must be run first so that computed getters // can be invalidated before any normal effects that depend on them are run. From 627b9df4a293ae18071009d9cac7a5e995d40716 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Feb 2020 17:45:42 +0100 Subject: [PATCH 10/21] fix(types): improve ref typing, close #759 --- packages/reactivity/src/ref.ts | 4 ++-- test-dts/ref.test-d.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 test-dts/ref.test-d.ts diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 65e1b8dd016..b36b3308b39 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -23,12 +23,12 @@ export interface Ref { const convert = (val: T): T => isObject(val) ? reactive(val) : val -export function isRef(r: Ref | T): r is Ref +export function isRef(r: Ref | unknown): r is Ref export function isRef(r: any): r is Ref { return r ? r._isRef === true : false } -export function ref(value: T): T +export function ref(value: T): T extends Ref ? T : Ref export function ref(value: T): Ref export function ref(): Ref export function ref(value?: unknown) { diff --git a/test-dts/ref.test-d.ts b/test-dts/ref.test-d.ts new file mode 100644 index 00000000000..ad8c22fef61 --- /dev/null +++ b/test-dts/ref.test-d.ts @@ -0,0 +1,16 @@ +import { expectType } from 'tsd' +import { Ref, ref } from './index' +import { isRef } from '@vue/reactivity' + +function foo(arg: number | Ref) { + // ref coercing + const coerced = ref(arg) + expectType>(coerced) + + // isRef as type guard + if (isRef(arg)) { + expectType>(arg) + } +} + +foo(1) From 775a7c2b414ca44d4684badb29e8e80ff6b5d3dd Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Feb 2020 17:48:39 +0100 Subject: [PATCH 11/21] refactor: preserve refs in reactive arrays BREAKING CHANGE: reactive arrays no longer unwraps contained refs When reactive arrays contain refs, especially a mix of refs and plain values, Array prototype methods will fail to function properly - e.g. sort() or reverse() will overwrite the ref's value instead of moving it (see #737). Ensuring correct behavior for all possible Array methods while retaining the ref unwrapping behavior is exceedinly complicated; In addition, even if Vue handles the built-in methods internally, it would still break when the user attempts to use a 3rd party utility functioon (e.g. lodash) on a reactive array containing refs. After this commit, similar to other collection types like Map and Set, Arrays will no longer automatically unwrap contained refs. The usage of mixed refs and plain values in Arrays should be rare in practice. In cases where this is necessary, the user can create a computed property that performs the unwrapping. --- .../reactivity/__tests__/reactive.spec.ts | 64 ---------- .../__tests__/reactiveArray.spec.ts | 111 ++++++++++++++++++ packages/reactivity/__tests__/ref.spec.ts | 22 ++-- packages/reactivity/src/baseHandlers.ts | 23 ++-- packages/reactivity/src/reactive.ts | 5 +- packages/reactivity/src/ref.ts | 5 +- 6 files changed, 137 insertions(+), 93 deletions(-) create mode 100644 packages/reactivity/__tests__/reactiveArray.spec.ts diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index 1b18dc8055a..5d8876843b1 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -26,51 +26,6 @@ describe('reactivity/reactive', () => { expect(Object.keys(observed)).toEqual(['foo']) }) - test('Array', () => { - const original = [{ foo: 1 }] - const observed = reactive(original) - expect(observed).not.toBe(original) - expect(isReactive(observed)).toBe(true) - expect(isReactive(original)).toBe(false) - expect(isReactive(observed[0])).toBe(true) - // get - expect(observed[0].foo).toBe(1) - // has - expect(0 in observed).toBe(true) - // ownKeys - expect(Object.keys(observed)).toEqual(['0']) - }) - - test('cloned reactive Array should point to observed values', () => { - const original = [{ foo: 1 }] - const observed = reactive(original) - const clone = observed.slice() - expect(isReactive(clone[0])).toBe(true) - expect(clone[0]).not.toBe(original[0]) - expect(clone[0]).toBe(observed[0]) - }) - - test('Array identity methods should work with raw values', () => { - const raw = {} - const arr = reactive([{}, {}]) - arr.push(raw) - expect(arr.indexOf(raw)).toBe(2) - expect(arr.indexOf(raw, 3)).toBe(-1) - expect(arr.includes(raw)).toBe(true) - expect(arr.includes(raw, 3)).toBe(false) - expect(arr.lastIndexOf(raw)).toBe(2) - expect(arr.lastIndexOf(raw, 1)).toBe(-1) - - // should work also for the observed version - const observed = arr[2] - expect(arr.indexOf(observed)).toBe(2) - expect(arr.indexOf(observed, 3)).toBe(-1) - expect(arr.includes(observed)).toBe(true) - expect(arr.includes(observed, 3)).toBe(false) - expect(arr.lastIndexOf(observed)).toBe(2) - expect(arr.lastIndexOf(observed, 1)).toBe(-1) - }) - test('nested reactives', () => { const original = { nested: { @@ -97,25 +52,6 @@ describe('reactivity/reactive', () => { expect('foo' in original).toBe(false) }) - test('observed value should proxy mutations to original (Array)', () => { - const original: any[] = [{ foo: 1 }, { bar: 2 }] - const observed = reactive(original) - // set - const value = { baz: 3 } - const reactiveValue = reactive(value) - observed[0] = value - expect(observed[0]).toBe(reactiveValue) - expect(original[0]).toBe(value) - // delete - delete observed[0] - expect(observed[0]).toBeUndefined() - expect(original[0]).toBeUndefined() - // mutating methods - observed.push(value) - expect(observed[2]).toBe(reactiveValue) - expect(original[2]).toBe(value) - }) - test('setting a property with an unobserved value should wrap with reactive', () => { const observed = reactive<{ foo?: object }>({}) const raw = {} diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts new file mode 100644 index 00000000000..b61116e28b1 --- /dev/null +++ b/packages/reactivity/__tests__/reactiveArray.spec.ts @@ -0,0 +1,111 @@ +import { reactive, isReactive, toRaw } from '../src/reactive' +import { ref, isRef } from '../src/ref' +import { effect } from '../src/effect' + +describe('reactivity/reactive/Array', () => { + test('should make Array reactive', () => { + const original = [{ foo: 1 }] + const observed = reactive(original) + expect(observed).not.toBe(original) + expect(isReactive(observed)).toBe(true) + expect(isReactive(original)).toBe(false) + expect(isReactive(observed[0])).toBe(true) + // get + expect(observed[0].foo).toBe(1) + // has + expect(0 in observed).toBe(true) + // ownKeys + expect(Object.keys(observed)).toEqual(['0']) + }) + + test('cloned reactive Array should point to observed values', () => { + const original = [{ foo: 1 }] + const observed = reactive(original) + const clone = observed.slice() + expect(isReactive(clone[0])).toBe(true) + expect(clone[0]).not.toBe(original[0]) + expect(clone[0]).toBe(observed[0]) + }) + + test('observed value should proxy mutations to original (Array)', () => { + const original: any[] = [{ foo: 1 }, { bar: 2 }] + const observed = reactive(original) + // set + const value = { baz: 3 } + const reactiveValue = reactive(value) + observed[0] = value + expect(observed[0]).toBe(reactiveValue) + expect(original[0]).toBe(value) + // delete + delete observed[0] + expect(observed[0]).toBeUndefined() + expect(original[0]).toBeUndefined() + // mutating methods + observed.push(value) + expect(observed[2]).toBe(reactiveValue) + expect(original[2]).toBe(value) + }) + + test('Array identity methods should work with raw values', () => { + const raw = {} + const arr = reactive([{}, {}]) + arr.push(raw) + expect(arr.indexOf(raw)).toBe(2) + expect(arr.indexOf(raw, 3)).toBe(-1) + expect(arr.includes(raw)).toBe(true) + expect(arr.includes(raw, 3)).toBe(false) + expect(arr.lastIndexOf(raw)).toBe(2) + expect(arr.lastIndexOf(raw, 1)).toBe(-1) + + // should work also for the observed version + const observed = arr[2] + expect(arr.indexOf(observed)).toBe(2) + expect(arr.indexOf(observed, 3)).toBe(-1) + expect(arr.includes(observed)).toBe(true) + expect(arr.includes(observed, 3)).toBe(false) + expect(arr.lastIndexOf(observed)).toBe(2) + expect(arr.lastIndexOf(observed, 1)).toBe(-1) + }) + + test('Array identity methods should be reactive', () => { + const obj = {} + const arr = reactive([obj, {}]) + + let index: number = -1 + effect(() => { + index = arr.indexOf(obj) + }) + expect(index).toBe(0) + arr.reverse() + expect(index).toBe(1) + }) + + describe('Array methods w/ refs', () => { + let original: any[] + beforeEach(() => { + original = reactive([1, ref(2)]) + }) + + // read + copy + test('read only copy methods', () => { + const res = original.concat([3, ref(4)]) + const raw = toRaw(res) + expect(isRef(raw[1])).toBe(true) + expect(isRef(raw[3])).toBe(true) + }) + + // read + write + test('read + write mutating methods', () => { + const res = original.copyWithin(0, 1, 2) + const raw = toRaw(res) + expect(isRef(raw[0])).toBe(true) + expect(isRef(raw[1])).toBe(true) + }) + + test('read + indentity', () => { + const ref = original[1] + expect(ref).toBe(toRaw(original)[1]) + expect(original.indexOf(ref)).toBe(1) + }) + }) +}) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index 84f1c8dff5a..dc0cca6c8c3 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -49,23 +49,20 @@ describe('reactivity/ref', () => { const obj = reactive({ a, b: { - c: a, - d: [a] + c: a } }) let dummy1: number let dummy2: number - let dummy3: number effect(() => { dummy1 = obj.a dummy2 = obj.b.c - dummy3 = obj.b.d[0] }) const assertDummiesEqualTo = (val: number) => - [dummy1, dummy2, dummy3].forEach(dummy => expect(dummy).toBe(val)) + [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val)) assertDummiesEqualTo(1) a.value++ @@ -74,8 +71,6 @@ describe('reactivity/ref', () => { assertDummiesEqualTo(3) obj.b.c++ assertDummiesEqualTo(4) - obj.b.d[0]++ - assertDummiesEqualTo(5) }) it('should unwrap nested ref in types', () => { @@ -95,15 +90,14 @@ describe('reactivity/ref', () => { expect(typeof (c.value.b + 1)).toBe('number') }) - it('should properly unwrap ref types nested inside arrays', () => { + it('should NOT unwrap ref types nested inside arrays', () => { const arr = ref([1, ref(1)]).value - // should unwrap to number[] - arr[0]++ - arr[1]++ + ;(arr[0] as number)++ + ;(arr[1] as Ref).value++ const arr2 = ref([1, new Map(), ref('1')]).value const value = arr2[0] - if (typeof value === 'string') { + if (isRef(value)) { value + 'foo' } else if (typeof value === 'number') { value + 1 @@ -131,8 +125,8 @@ describe('reactivity/ref', () => { tupleRef.value[2].a++ expect(tupleRef.value[2].a).toBe(2) expect(tupleRef.value[3]()).toBe(0) - tupleRef.value[4]++ - expect(tupleRef.value[4]).toBe(1) + tupleRef.value[4].value++ + expect(tupleRef.value[4].value).toBe(1) }) test('isRef', () => { diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 3bc03e0a7f6..11e127d5642 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -16,20 +16,21 @@ const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true) const readonlyGet = /*#__PURE__*/ createGetter(true) const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) -const arrayIdentityInstrumentations: Record = {} +const arrayInstrumentations: Record = {} ;['includes', 'indexOf', 'lastIndexOf'].forEach(key => { - arrayIdentityInstrumentations[key] = function( - value: unknown, - ...args: any[] - ): any { - return toRaw(this)[key](toRaw(value), ...args) + arrayInstrumentations[key] = function(...args: any[]): any { + const arr = toRaw(this) as any + for (let i = 0, l = (this as any).length; i < l; i++) { + track(arr, TrackOpTypes.GET, i + '') + } + return arr[key](...args.map(toRaw)) } }) function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { - if (isArray(target) && hasOwn(arrayIdentityInstrumentations, key)) { - return Reflect.get(arrayIdentityInstrumentations, key, receiver) + if (isArray(target) && hasOwn(arrayInstrumentations, key)) { + return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) if (isSymbol(key) && builtInSymbols.has(key)) { @@ -40,7 +41,8 @@ function createGetter(isReadonly = false, shallow = false) { // TODO strict mode that returns a shallow-readonly version of the value return res } - if (isRef(res)) { + // ref unwrapping, only for Objects, not for Arrays. + if (isRef(res) && !isArray(target)) { return res.value } track(target, TrackOpTypes.GET, key) @@ -79,7 +81,7 @@ function createSetter(isReadonly = false, shallow = false) { const oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) - if (isRef(oldValue) && !isRef(value)) { + if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } @@ -94,6 +96,7 @@ function createSetter(isReadonly = false, shallow = false) { if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { + debugger trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index f97a1e6f2d4..5f8aa579725 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -9,7 +9,7 @@ import { mutableCollectionHandlers, readonlyCollectionHandlers } from './collectionHandlers' -import { UnwrapRef, Ref } from './ref' +import { UnwrapRef, Ref, isRef } from './ref' import { makeMap } from '@vue/shared' // WeakMaps that store {raw <-> observed} pairs. @@ -50,6 +50,9 @@ export function reactive(target: object) { if (readonlyValues.has(target)) { return readonly(target) } + if (isRef(target)) { + return target + } return createReactiveObject( target, rawToReactive, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index b36b3308b39..351dd1ddb5d 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -29,7 +29,6 @@ export function isRef(r: any): r is Ref { } export function ref(value: T): T extends Ref ? T : Ref -export function ref(value: T): Ref export function ref(): Ref export function ref(value?: unknown) { if (isRef(value)) { @@ -83,8 +82,6 @@ function toProxyRef( } as any } -type UnwrapArray = { [P in keyof T]: UnwrapRef } - // corner case when use narrows type // Ex. type RelativePath = string & { __brand: unknown } // RelativePath extends object -> true @@ -94,7 +91,7 @@ type BaseTypes = string | number | boolean export type UnwrapRef = { cRef: T extends ComputedRef ? UnwrapRef : T ref: T extends Ref ? UnwrapRef : T - array: T extends Array ? Array> & UnwrapArray : T + array: T object: { [K in keyof T]: UnwrapRef } }[T extends ComputedRef ? 'cRef' From 0c672019422b6a5dbc5fdb43f00a8a998a40dc18 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 22 Feb 2020 03:54:20 +0100 Subject: [PATCH 12/21] chore: fix test type --- packages/runtime-core/__tests__/apiTemplateRef.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/apiTemplateRef.spec.ts b/packages/runtime-core/__tests__/apiTemplateRef.spec.ts index 8ff6b2c59e1..68b14dd2b28 100644 --- a/packages/runtime-core/__tests__/apiTemplateRef.spec.ts +++ b/packages/runtime-core/__tests__/apiTemplateRef.spec.ts @@ -142,7 +142,7 @@ describe('api: template refs', () => { foo: ref(null), bar: ref(null) } - const refKey: Ref = ref('foo') + const refKey = ref('foo') as Ref const Comp = { setup() { From e9024bf1b7456b9cf9b913c239502593364bc773 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 22 Feb 2020 04:39:32 +0100 Subject: [PATCH 13/21] feat(reactivity): expose unref and shallowRef --- packages/reactivity/__tests__/ref.spec.ts | 31 ++++++++++++++++++++- packages/reactivity/src/index.ts | 2 +- packages/reactivity/src/ref.ts | 20 +++++++++++-- packages/runtime-core/src/componentProxy.ts | 9 +++--- packages/runtime-core/src/index.ts | 2 ++ test-dts/ref.test-d.ts | 6 ++-- 6 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index dc0cca6c8c3..6620f7fd9ce 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -1,5 +1,14 @@ -import { ref, effect, reactive, isRef, toRefs, Ref } from '../src/index' +import { + ref, + effect, + reactive, + isRef, + toRefs, + Ref, + isReactive +} from '../src/index' import { computed } from '@vue/runtime-dom' +import { shallowRef, unref } from '../src/ref' describe('reactivity/ref', () => { it('should hold a value', () => { @@ -129,6 +138,26 @@ describe('reactivity/ref', () => { expect(tupleRef.value[4].value).toBe(1) }) + test('unref', () => { + expect(unref(1)).toBe(1) + expect(unref(ref(1))).toBe(1) + }) + + test('shallowRef', () => { + const sref = shallowRef({ a: 1 }) + expect(isReactive(sref.value)).toBe(false) + + let dummy + effect(() => { + dummy = sref.value.a + }) + expect(dummy).toBe(1) + + sref.value = { a: 2 } + expect(isReactive(sref.value)).toBe(false) + expect(dummy).toBe(2) + }) + test('isRef', () => { expect(isRef(ref(1))).toBe(true) expect(isRef(computed(() => 1))).toBe(true) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 040dd8c673d..21a9eae8f8c 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -1,4 +1,4 @@ -export { ref, isRef, toRefs, Ref, UnwrapRef } from './ref' +export { ref, unref, shallowRef, isRef, toRefs, Ref, UnwrapRef } from './ref' export { reactive, isReactive, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 351dd1ddb5d..f88631130e7 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -31,10 +31,22 @@ export function isRef(r: any): r is Ref { export function ref(value: T): T extends Ref ? T : Ref export function ref(): Ref export function ref(value?: unknown) { + return createRef(value) +} + +export function shallowRef(value: T): T extends Ref ? T : Ref +export function shallowRef(): Ref +export function shallowRef(value?: unknown) { + return createRef(value, true) +} + +function createRef(value: unknown, shallow = false) { if (isRef(value)) { return value } - value = convert(value) + if (!shallow) { + value = convert(value) + } const r = { _isRef: true, get value() { @@ -42,7 +54,7 @@ export function ref(value?: unknown) { return value }, set value(newVal) { - value = convert(newVal) + value = shallow ? newVal : convert(newVal) trigger( r, TriggerOpTypes.SET, @@ -54,6 +66,10 @@ export function ref(value?: unknown) { return r } +export function unref(ref: T): T extends Ref ? V : T { + return isRef(ref) ? (ref.value as any) : ref +} + export function toRefs( object: T ): { [K in keyof T]: Ref } { diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 9e451110523..a69d33da140 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -13,7 +13,8 @@ import { isRef, isReactive, Ref, - ComputedRef + ComputedRef, + unref } from '@vue/reactivity' import { warn } from './warning' import { Slots } from './componentSlots' @@ -84,8 +85,6 @@ const enum AccessTypes { OTHER } -const unwrapRef = (val: unknown) => (isRef(val) ? val.value : val) - export const PublicInstanceProxyHandlers: ProxyHandler = { get(target: ComponentInternalInstance, key: string) { // fast path for unscopables when using `with` block @@ -115,7 +114,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { case AccessTypes.DATA: return data[key] case AccessTypes.CONTEXT: - return unwrapRef(renderContext[key]) + return unref(renderContext[key]) case AccessTypes.PROPS: return propsProxy![key] // default: just fallthrough @@ -125,7 +124,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { return data[key] } else if (hasOwn(renderContext, key)) { accessCache![key] = AccessTypes.CONTEXT - return unwrapRef(renderContext[key]) + return unref(renderContext[key]) } else if (type.props != null) { // only cache other properties when instance has declared (this stable) // props diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index b26c9a8f987..8629d07f418 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -3,6 +3,8 @@ export const version = __VERSION__ export { ref, + unref, + shallowRef, isRef, toRefs, reactive, diff --git a/test-dts/ref.test-d.ts b/test-dts/ref.test-d.ts index ad8c22fef61..e1323fef1fb 100644 --- a/test-dts/ref.test-d.ts +++ b/test-dts/ref.test-d.ts @@ -1,6 +1,5 @@ import { expectType } from 'tsd' -import { Ref, ref } from './index' -import { isRef } from '@vue/reactivity' +import { Ref, ref, isRef, unref } from './index' function foo(arg: number | Ref) { // ref coercing @@ -11,6 +10,9 @@ function foo(arg: number | Ref) { if (isRef(arg)) { expectType>(arg) } + + // ref unwrapping + expectType(unref(arg)) } foo(1) From 76c7f5426919f9d29a303263bc54a1e42a66e94b Mon Sep 17 00:00:00 2001 From: guaijie <30885718+guaijie@users.noreply.github.com> Date: Sat, 22 Feb 2020 12:17:30 +0800 Subject: [PATCH 14/21] perf(effect): optimize effect trigger for array length mutation (#761) --- packages/reactivity/src/effect.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index c0f4707d83d..010aea0020a 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -176,17 +176,18 @@ export function trigger( } const effects = new Set() const computedRunners = new Set() - if ( - type === TriggerOpTypes.CLEAR || - (key === 'length' && - isArray(target) && - (newValue as number) < (oldValue as number)) - ) { - // collection being cleared or Array length mutation + if (type === TriggerOpTypes.CLEAR) { + // collection being cleared // trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) + } else if (key === 'length' && isArray(target)) { + depsMap.forEach((dep, key) => { + if (key === 'length' || key >= (newValue as number)) { + addRunners(effects, computedRunners, dep) + } + }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { From b36a76fe2382ebd0faff293387a1799c8eb0d11a Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 22 Feb 2020 05:19:22 +0100 Subject: [PATCH 15/21] chore: remove debugger [ci skip] --- packages/reactivity/src/baseHandlers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 11e127d5642..0aaeade83e4 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -96,7 +96,6 @@ function createSetter(isReadonly = false, shallow = false) { if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { - debugger trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } From 99a2e18c9711d3d1f79f8c9c59212880efd058b9 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 22 Feb 2020 08:19:10 +0100 Subject: [PATCH 16/21] feat(runtime-core): add watchEffect API BREAKING CHANGE: replae `watch(fn, options?)` with `watchEffect` The `watch(fn, options?)` signature has been replaced by the new `watchEffect` API, which has the same usage and behavior. `watch` now only supports the `watch(source, cb, options?)` signautre. --- .../__tests__/apiSetupContext.spec.ts | 6 +- .../runtime-core/__tests__/apiWatch.spec.ts | 57 ++++++++++++++----- .../__tests__/components/Suspense.spec.ts | 5 +- .../__tests__/errorHandling.spec.ts | 15 ++--- packages/runtime-core/src/apiWatch.ts | 15 +++++ packages/runtime-core/src/index.ts | 2 +- .../vue/examples/composition/commits.html | 4 +- .../vue/examples/composition/todomvc.html | 4 +- 8 files changed, 77 insertions(+), 31 deletions(-) diff --git a/packages/runtime-core/__tests__/apiSetupContext.spec.ts b/packages/runtime-core/__tests__/apiSetupContext.spec.ts index bc3c2065034..8f7ab078bbe 100644 --- a/packages/runtime-core/__tests__/apiSetupContext.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupContext.spec.ts @@ -6,7 +6,7 @@ import { render, serializeInner, nextTick, - watch, + watchEffect, defineComponent, triggerEvent, TestElement @@ -55,7 +55,7 @@ describe('api: setup context', () => { const Child = defineComponent({ setup(props: { count: number }) { - watch(() => { + watchEffect(() => { dummy = props.count }) return () => h('div', props.count) @@ -88,7 +88,7 @@ describe('api: setup context', () => { }, setup(props) { - watch(() => { + watchEffect(() => { dummy = props.count }) return () => h('div', props.count) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index ef05452fd33..796b2cbff50 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1,4 +1,12 @@ -import { watch, reactive, computed, nextTick, ref, h } from '../src/index' +import { + watch, + watchEffect, + reactive, + computed, + nextTick, + ref, + h +} from '../src/index' import { render, nodeOps, serializeInner } from '@vue/runtime-test' import { ITERATE_KEY, @@ -13,10 +21,10 @@ import { mockWarn } from '@vue/shared' describe('api: watch', () => { mockWarn() - it('watch(effect)', async () => { + it('effect', async () => { const state = reactive({ count: 0 }) let dummy - watch(() => { + watchEffect(() => { dummy = state.count }) expect(dummy).toBe(0) @@ -117,10 +125,10 @@ describe('api: watch', () => { expect(dummy).toMatchObject([[2, true], [1, false]]) }) - it('stopping the watcher', async () => { + it('stopping the watcher (effect)', async () => { const state = reactive({ count: 0 }) let dummy - const stop = watch(() => { + const stop = watchEffect(() => { dummy = state.count }) expect(dummy).toBe(0) @@ -132,11 +140,32 @@ describe('api: watch', () => { expect(dummy).toBe(0) }) + it('stopping the watcher (with source)', async () => { + const state = reactive({ count: 0 }) + let dummy + const stop = watch( + () => state.count, + count => { + dummy = count + } + ) + + state.count++ + await nextTick() + expect(dummy).toBe(1) + + stop() + state.count++ + await nextTick() + // should not update + expect(dummy).toBe(1) + }) + it('cleanup registration (effect)', async () => { const state = reactive({ count: 0 }) const cleanup = jest.fn() let dummy - const stop = watch(onCleanup => { + const stop = watchEffect(onCleanup => { onCleanup(cleanup) dummy = state.count }) @@ -187,7 +216,7 @@ describe('api: watch', () => { const Comp = { setup() { - watch(() => { + watchEffect(() => { assertion(count.value) }) return () => count.value @@ -221,7 +250,7 @@ describe('api: watch', () => { const Comp = { setup() { - watch( + watchEffect( () => { assertion(count.value, count2.value) }, @@ -263,7 +292,7 @@ describe('api: watch', () => { const Comp = { setup() { - watch( + watchEffect( () => { assertion(count.value) }, @@ -363,14 +392,14 @@ describe('api: watch', () => { expect(spy).toHaveBeenCalledTimes(3) }) - it('warn immediate option when using effect signature', async () => { + it('warn immediate option when using effect', async () => { const count = ref(0) let dummy - // @ts-ignore - watch( + watchEffect( () => { dummy = count.value }, + // @ts-ignore { immediate: false } ) expect(dummy).toBe(0) @@ -388,7 +417,7 @@ describe('api: watch', () => { events.push(e) }) const obj = reactive({ foo: 1, bar: 2 }) - watch( + watchEffect( () => { dummy = [obj.foo, 'bar' in obj, Object.keys(obj)] }, @@ -423,7 +452,7 @@ describe('api: watch', () => { events.push(e) }) const obj = reactive({ foo: 1 }) - watch( + watchEffect( () => { dummy = obj.foo }, diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index a530532f077..6a115e39c25 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -9,6 +9,7 @@ import { nextTick, onMounted, watch, + watchEffect, onUnmounted, onErrorCaptured } from '@vue/runtime-test' @@ -163,7 +164,7 @@ describe('Suspense', () => { // extra tick needed for Node 12+ deps.push(p.then(() => Promise.resolve())) - watch(() => { + watchEffect(() => { calls.push('immediate effect') }) @@ -265,7 +266,7 @@ describe('Suspense', () => { const p = new Promise(r => setTimeout(r, 1)) deps.push(p) - watch(() => { + watchEffect(() => { calls.push('immediate effect') }) diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts index 6396c6b35a4..f6907949b1f 100644 --- a/packages/runtime-core/__tests__/errorHandling.spec.ts +++ b/packages/runtime-core/__tests__/errorHandling.spec.ts @@ -7,7 +7,8 @@ import { watch, ref, nextTick, - defineComponent + defineComponent, + watchEffect } from '@vue/runtime-test' import { setErrorRecovery } from '../src/errorHandling' import { mockWarn } from '@vue/shared' @@ -241,7 +242,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'ref function') }) - test('in watch (effect)', () => { + test('in effect', () => { const err = new Error('foo') const fn = jest.fn() @@ -257,7 +258,7 @@ describe('error handling', () => { const Child = { setup() { - watch(() => { + watchEffect(() => { throw err }) return () => null @@ -268,7 +269,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'watcher callback') }) - test('in watch (getter)', () => { + test('in watch getter', () => { const err = new Error('foo') const fn = jest.fn() @@ -298,7 +299,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'watcher getter') }) - test('in watch (callback)', async () => { + test('in watch callback', async () => { const err = new Error('foo') const fn = jest.fn() @@ -332,7 +333,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'watcher callback') }) - test('in watch cleanup', async () => { + test('in effect cleanup', async () => { const err = new Error('foo') const count = ref(0) const fn = jest.fn() @@ -349,7 +350,7 @@ describe('error handling', () => { const Child = { setup() { - watch(onCleanup => { + watchEffect(onCleanup => { count.value onCleanup(() => { throw err diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index da3df3033f9..1a2ecb5ec2f 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -71,6 +71,14 @@ export type StopHandle = () => void const invoke = (fn: Function) => fn() +// Simple effect. +export function watchEffect( + effect: WatchEffect, + options?: BaseWatchOptions +): StopHandle { + return doWatch(effect, null, options) +} + // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} @@ -110,6 +118,13 @@ export function watch( // watch(source, cb) return doWatch(effectOrSource, cbOrOptions, options) } else { + // TODO remove this in the next release + __DEV__ && + warn( + `\`watch(fn, options?)\` signature has been moved to a separate API. ` + + `Use \`watchEffect(fn, options?)\` instead. \`watch\` will only ` + + `support \`watch(source, cb, options?) signature in the next release.` + ) // watch(effect) return doWatch(effectOrSource, null, cbOrOptions) } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 8629d07f418..634d53b424b 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -17,7 +17,7 @@ export { markNonReactive } from '@vue/reactivity' export { computed } from './apiComputed' -export { watch } from './apiWatch' +export { watch, watchEffect } from './apiWatch' export { onBeforeMount, onMounted, diff --git a/packages/vue/examples/composition/commits.html b/packages/vue/examples/composition/commits.html index 270662fe386..711cbc67039 100644 --- a/packages/vue/examples/composition/commits.html +++ b/packages/vue/examples/composition/commits.html @@ -22,7 +22,7 @@

Latest Vue.js Commits