From 77dae710621432217bb6b61fe949395728497637 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 26 Feb 2019 18:04:52 -0500 Subject: [PATCH] refactor: adjust files --- packages/runtime-core/src/component.ts | 2 +- .../runtime-core/src/componentInstance.ts | 104 ++++++ packages/runtime-core/src/componentOptions.ts | 61 ++++ packages/runtime-core/src/componentProxy.ts | 2 +- .../runtime-core/src/componentRenderUtils.ts | 165 +++++++++ packages/runtime-core/src/componentUtils.ts | 337 ------------------ packages/runtime-core/src/createRenderer.ts | 8 +- packages/runtime-core/src/index.ts | 5 +- packages/runtime-core/src/optional/mixins.ts | 2 +- packages/runtime-core/src/vdom.ts | 2 +- 10 files changed, 340 insertions(+), 348 deletions(-) create mode 100644 packages/runtime-core/src/componentInstance.ts create mode 100644 packages/runtime-core/src/componentRenderUtils.ts delete mode 100644 packages/runtime-core/src/componentUtils.ts diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 72d5d8731f..4cfb156957 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -11,7 +11,7 @@ import { setupWatcher } from './componentWatch' import { ReactiveEffect, DebuggerEvent, ComputedGetter } from '@vue/observer' import { nextTick } from '@vue/scheduler' import { ErrorTypes } from './errorHandling' -import { initializeComponentInstance } from './componentUtils' +import { initializeComponentInstance } from './componentInstance' import { EventEmitter, invokeListeners } from './optional/eventEmitter' import { warn } from './warning' diff --git a/packages/runtime-core/src/componentInstance.ts b/packages/runtime-core/src/componentInstance.ts new file mode 100644 index 0000000000..4e485680c7 --- /dev/null +++ b/packages/runtime-core/src/componentInstance.ts @@ -0,0 +1,104 @@ +import { VNode, MountedVNode } from './vdom' +import { Component, ComponentInstance, ComponentClass } from './component' +import { initializeState } from './componentState' +import { initializeProps } from './componentProps' +import { initializeWatch, teardownWatch } from './componentWatch' +import { initializeComputed, teardownComputed } from './componentComputed' +import { createRenderProxy } from './componentProxy' +import { resolveComponentOptionsFromClass } from './componentOptions' +import { VNodeFlags } from './flags' +import { ErrorTypes, callLifecycleHookWithHandler } from './errorHandling' +import { stop } from '@vue/observer' +import { EMPTY_OBJ } from '@vue/shared' + +let currentVNode: VNode | null = null +let currentContextVNode: VNode | null = null + +export function createComponentInstance( + vnode: VNode +): ComponentInstance { + // component instance creation is done in two steps. + // first, `initializeComponentInstance` is called inside base component + // constructor as the instance is created so that the extended component's + // constructor has access to certain properties and most importantly, + // this.$props. + // we are storing the vnodes in variables here so that there's no need to + // always pass args in super() + currentVNode = vnode + currentContextVNode = vnode.contextVNode + const Component = vnode.tag as ComponentClass + const instance = (vnode.children = new Component() as ComponentInstance) + + // then we finish the initialization by collecting properties set on the + // instance + const { + $proxy, + $options: { created, computed, watch } + } = instance + initializeState(instance, !Component.fromOptions) + initializeComputed(instance, computed) + initializeWatch(instance, watch) + instance.$slots = currentVNode.slots || EMPTY_OBJ + + if (created) { + callLifecycleHookWithHandler(created, $proxy, ErrorTypes.CREATED) + } + + currentVNode = currentContextVNode = null + return instance +} + +// this is called inside the base component's constructor +// it initializes all the way up to props so that they are available +// inside the extended component's constructor +export function initializeComponentInstance(instance: ComponentInstance) { + if (__DEV__ && currentVNode === null) { + throw new Error( + `Component classes are not meant to be manually instantiated.` + ) + } + + instance.$options = resolveComponentOptionsFromClass(instance.constructor) + instance.$parentVNode = currentVNode as MountedVNode + + // renderProxy + const proxy = (instance.$proxy = createRenderProxy(instance)) + + // parent chain management + if (currentContextVNode !== null) { + // locate first non-functional parent + while (currentContextVNode !== null) { + if ((currentContextVNode.flags & VNodeFlags.COMPONENT_STATEFUL) > 0) { + const parentComponent = (currentContextVNode as VNode) + .children as ComponentInstance + instance.$parent = parentComponent.$proxy + instance.$root = parentComponent.$root + parentComponent.$children.push(proxy) + break + } + currentContextVNode = currentContextVNode.contextVNode + } + } else { + instance.$root = proxy + } + + // beforeCreate hook is called right in the constructor + const { beforeCreate, props } = instance.$options + if (beforeCreate) { + callLifecycleHookWithHandler(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE) + } + initializeProps(instance, props, (currentVNode as VNode).data) +} + +export function teardownComponentInstance(instance: ComponentInstance) { + const parentComponent = instance.$parent && instance.$parent._self + if (parentComponent && !parentComponent._unmounted) { + parentComponent.$children.splice( + parentComponent.$children.indexOf(instance.$proxy), + 1 + ) + } + stop(instance._update) + teardownComputed(instance) + teardownWatch(instance) +} diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 05ff8dac24..97e7db1521 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -7,6 +7,8 @@ import { } from './component' import { isArray, isObject, isFunction } from '@vue/shared' import { normalizePropsOptions } from './componentProps' +import { warn } from './warning' +import { h } from './h' export type Data = Record @@ -170,6 +172,65 @@ export function resolveComponentOptionsFromClass( return options } +export function createComponentClassFromOptions( + options: ComponentOptions +): ComponentClass { + class AnonymousComponent extends Component { + static options = options + // indicate this component was created from options + static fromOptions = true + } + const proto = AnonymousComponent.prototype as any + for (const key in options) { + const value = options[key] + if (key === 'render') { + if (__COMPAT__) { + options.render = function() { + return value.call(this, h) + } + } + // so that we can call instance.render directly + proto.render = options.render + } else if (key === 'computed') { + // create computed setters on prototype + // (getters are handled by the render proxy) + for (const computedKey in value) { + const computed = value[computedKey] + const set = isObject(computed) && computed.set + if (set) { + Object.defineProperty(proto, computedKey, { + configurable: true, + set + }) + } + } + } else if (key === 'methods') { + for (const method in value) { + if (__DEV__ && proto.hasOwnProperty(method)) { + warn( + `Object syntax contains method name that conflicts with ` + + `lifecycle hook: "${method}"` + ) + } + proto[method] = value[method] + } + } else if (__COMPAT__) { + if (key === 'name') { + options.displayName = value + } else if (key === 'render') { + options.render = function() { + return value.call(this, h) + } + } else if (key === 'beforeDestroy') { + options.beforeUnmount = value + } else if (key === 'destroyed') { + options.unmounted = value + } + } + } + return AnonymousComponent as ComponentClass +} + export function mergeComponentOptions(to: any, from: any): ComponentOptions { const res: any = Object.assign({}, to) if (from.mixins) { diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 4091b7dc24..0698795d11 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -1,7 +1,7 @@ import { ComponentInstance } from './component' import { isFunction, isReservedKey } from '@vue/shared' import { warn } from './warning' -import { isRendering } from './componentUtils' +import { isRendering } from './componentRenderUtils' import { isObservable } from '@vue/observer' import { reservedMethods } from './componentOptions' diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts new file mode 100644 index 0000000000..50b5508c52 --- /dev/null +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -0,0 +1,165 @@ +import { VNode, createFragment, createTextVNode, cloneVNode } from './vdom' +import { ComponentInstance, FunctionalComponent } from './component' +import { resolveProps } from './componentProps' +import { handleError, ErrorTypes } from './errorHandling' +import { VNodeFlags, ChildrenFlags } from './flags' +import { EMPTY_OBJ, isArray, isObject } from '@vue/shared' +import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks' + +export let isRendering = false + +export function renderInstanceRoot(instance: ComponentInstance): VNode { + let vnode + const { + $options: { hooks }, + render, + $proxy, + $props, + $slots, + $attrs, + $parentVNode + } = instance + try { + setCurrentInstance(instance) + if (hooks) { + instance._hookProps = hooks.call($proxy, $props) || null + } + if (__DEV__) { + isRendering = true + } + vnode = render.call($proxy, $props, $slots, $attrs, $parentVNode) + if (__DEV__) { + isRendering = false + } + unsetCurrentInstance() + } catch (err) { + handleError(err, instance, ErrorTypes.RENDER) + } + return normalizeComponentRoot(vnode, $parentVNode) +} + +export function renderFunctionalRoot(vnode: VNode): VNode { + const render = vnode.tag as FunctionalComponent + const { 0: props, 1: attrs } = resolveProps(vnode.data, render.props) + let subTree + try { + subTree = render(props, vnode.slots || EMPTY_OBJ, attrs, vnode) + } catch (err) { + handleError(err, vnode, ErrorTypes.RENDER) + } + return normalizeComponentRoot(subTree, vnode) +} + +function normalizeComponentRoot( + vnode: any, + componentVNode: VNode | null +): VNode { + if (vnode == null) { + vnode = createTextVNode('') + } else if (!isObject(vnode)) { + vnode = createTextVNode(vnode + '') + } else if (isArray(vnode)) { + if (vnode.length === 1) { + vnode = normalizeComponentRoot(vnode[0], componentVNode) + } else { + vnode = createFragment(vnode) + } + } else { + const { el, flags } = vnode + if ( + componentVNode && + (flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT) + ) { + if (el) { + vnode = cloneVNode(vnode as VNode) + } + if (flags & VNodeFlags.COMPONENT) { + vnode.parentVNode = componentVNode + } + } else if (el) { + vnode = cloneVNode(vnode as VNode) + } + } + return vnode +} + +export function shouldUpdateComponent( + prevVNode: VNode, + nextVNode: VNode +): boolean { + const { data: prevProps, childFlags: prevChildFlags } = prevVNode + const { data: nextProps, childFlags: nextChildFlags } = nextVNode + // If has different slots content, or has non-compiled slots, + // the child needs to be force updated. + if ( + prevChildFlags !== nextChildFlags || + (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0 + ) { + return true + } + if (prevProps === nextProps) { + return false + } + if (prevProps === null) { + return nextProps !== null + } + if (nextProps === null) { + return prevProps !== null + } + const nextKeys = Object.keys(nextProps) + if (nextKeys.length !== Object.keys(prevProps).length) { + return true + } + for (let i = 0; i < nextKeys.length; i++) { + const key = nextKeys[i] + if (nextProps[key] !== prevProps[key]) { + return true + } + } + return false +} + +// DEV only +export function getReasonForComponentUpdate( + prevVNode: VNode, + nextVNode: VNode +): any { + const reasons = [] + const { childFlags: prevChildFlags } = prevVNode + const { childFlags: nextChildFlags } = nextVNode + if ( + prevChildFlags !== nextChildFlags || + (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0 + ) { + reasons.push({ + type: `slots may have changed`, + tip: `use function slots + $stable: true to avoid slot-triggered child updates.` + }) + } + const prevProps = prevVNode.data || EMPTY_OBJ + const nextProps = nextVNode.data || EMPTY_OBJ + for (const key in nextProps) { + if (nextProps[key] !== prevProps[key]) { + reasons.push({ + type: 'prop changed', + key, + value: nextProps[key], + oldValue: prevProps[key] + }) + } + } + for (const key in prevProps) { + if (!(key in nextProps)) { + reasons.push({ + type: 'prop changed', + key, + value: undefined, + oldValue: prevProps[key] + }) + } + } + return { + type: 'triggered by parent', + reasons + } +} diff --git a/packages/runtime-core/src/componentUtils.ts b/packages/runtime-core/src/componentUtils.ts deleted file mode 100644 index 0341eabf08..0000000000 --- a/packages/runtime-core/src/componentUtils.ts +++ /dev/null @@ -1,337 +0,0 @@ -import { VNodeFlags, ChildrenFlags } from './flags' -import { EMPTY_OBJ, isArray, isObject } from '@vue/shared' -import { h } from './h' -import { VNode, MountedVNode, createFragment } from './vdom' -import { - Component, - ComponentInstance, - ComponentClass, - FunctionalComponent -} from './component' -import { createTextVNode, cloneVNode } from './vdom' -import { initializeState } from './componentState' -import { initializeProps, resolveProps } from './componentProps' -import { initializeComputed, teardownComputed } from './componentComputed' -import { initializeWatch, teardownWatch } from './componentWatch' -import { - ComponentOptions, - resolveComponentOptionsFromClass -} from './componentOptions' -import { createRenderProxy } from './componentProxy' -import { - handleError, - ErrorTypes, - callLifecycleHookWithHandler -} from './errorHandling' -import { warn } from './warning' -import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks' -import { stop } from '@vue/observer' - -let currentVNode: VNode | null = null -let currentContextVNode: VNode | null = null - -export function createComponentInstance( - vnode: VNode -): ComponentInstance { - // component instance creation is done in two steps. - // first, `initializeComponentInstance` is called inside base component - // constructor as the instance is created so that the extended component's - // constructor has access to certain properties and most importantly, - // this.$props. - // we are storing the vnodes in variables here so that there's no need to - // always pass args in super() - currentVNode = vnode - currentContextVNode = vnode.contextVNode - const Component = vnode.tag as ComponentClass - const instance = (vnode.children = new Component() as ComponentInstance) - - // then we finish the initialization by collecting properties set on the - // instance - const { - $proxy, - $options: { created, computed, watch } - } = instance - initializeState(instance, !Component.fromOptions) - initializeComputed(instance, computed) - initializeWatch(instance, watch) - instance.$slots = currentVNode.slots || EMPTY_OBJ - - if (created) { - callLifecycleHookWithHandler(created, $proxy, ErrorTypes.CREATED) - } - - currentVNode = currentContextVNode = null - return instance -} - -// this is called inside the base component's constructor -// it initializes all the way up to props so that they are available -// inside the extended component's constructor -export function initializeComponentInstance(instance: ComponentInstance) { - if (__DEV__ && currentVNode === null) { - throw new Error( - `Component classes are not meant to be manually instantiated.` - ) - } - - instance.$options = resolveComponentOptionsFromClass(instance.constructor) - instance.$parentVNode = currentVNode as MountedVNode - - // renderProxy - const proxy = (instance.$proxy = createRenderProxy(instance)) - - // parent chain management - if (currentContextVNode !== null) { - // locate first non-functional parent - while (currentContextVNode !== null) { - if ((currentContextVNode.flags & VNodeFlags.COMPONENT_STATEFUL) > 0) { - const parentComponent = (currentContextVNode as VNode) - .children as ComponentInstance - instance.$parent = parentComponent.$proxy - instance.$root = parentComponent.$root - parentComponent.$children.push(proxy) - break - } - currentContextVNode = currentContextVNode.contextVNode - } - } else { - instance.$root = proxy - } - - // beforeCreate hook is called right in the constructor - const { beforeCreate, props } = instance.$options - if (beforeCreate) { - callLifecycleHookWithHandler(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE) - } - initializeProps(instance, props, (currentVNode as VNode).data) -} - -export let isRendering = false - -export function renderInstanceRoot(instance: ComponentInstance): VNode { - let vnode - const { - $options: { hooks }, - render, - $proxy, - $props, - $slots, - $attrs, - $parentVNode - } = instance - try { - setCurrentInstance(instance) - if (hooks) { - instance._hookProps = hooks.call($proxy, $props) || null - } - if (__DEV__) { - isRendering = true - } - vnode = render.call($proxy, $props, $slots, $attrs, $parentVNode) - if (__DEV__) { - isRendering = false - } - unsetCurrentInstance() - } catch (err) { - handleError(err, instance, ErrorTypes.RENDER) - } - return normalizeComponentRoot(vnode, $parentVNode) -} - -export function renderFunctionalRoot(vnode: VNode): VNode { - const render = vnode.tag as FunctionalComponent - const { 0: props, 1: attrs } = resolveProps(vnode.data, render.props) - let subTree - try { - subTree = render(props, vnode.slots || EMPTY_OBJ, attrs, vnode) - } catch (err) { - handleError(err, vnode, ErrorTypes.RENDER) - } - return normalizeComponentRoot(subTree, vnode) -} - -export function teardownComponentInstance(instance: ComponentInstance) { - const parentComponent = instance.$parent && instance.$parent._self - if (parentComponent && !parentComponent._unmounted) { - parentComponent.$children.splice( - parentComponent.$children.indexOf(instance.$proxy), - 1 - ) - } - stop(instance._update) - teardownComputed(instance) - teardownWatch(instance) -} - -function normalizeComponentRoot( - vnode: any, - componentVNode: VNode | null -): VNode { - if (vnode == null) { - vnode = createTextVNode('') - } else if (!isObject(vnode)) { - vnode = createTextVNode(vnode + '') - } else if (isArray(vnode)) { - if (vnode.length === 1) { - vnode = normalizeComponentRoot(vnode[0], componentVNode) - } else { - vnode = createFragment(vnode) - } - } else { - const { el, flags } = vnode - if ( - componentVNode && - (flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT) - ) { - if (el) { - vnode = cloneVNode(vnode as VNode) - } - if (flags & VNodeFlags.COMPONENT) { - vnode.parentVNode = componentVNode - } - } else if (el) { - vnode = cloneVNode(vnode as VNode) - } - } - return vnode -} - -export function shouldUpdateComponent( - prevVNode: VNode, - nextVNode: VNode -): boolean { - const { data: prevProps, childFlags: prevChildFlags } = prevVNode - const { data: nextProps, childFlags: nextChildFlags } = nextVNode - // If has different slots content, or has non-compiled slots, - // the child needs to be force updated. - if ( - prevChildFlags !== nextChildFlags || - (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0 - ) { - return true - } - if (prevProps === nextProps) { - return false - } - if (prevProps === null) { - return nextProps !== null - } - if (nextProps === null) { - return prevProps !== null - } - const nextKeys = Object.keys(nextProps) - if (nextKeys.length !== Object.keys(prevProps).length) { - return true - } - for (let i = 0; i < nextKeys.length; i++) { - const key = nextKeys[i] - if (nextProps[key] !== prevProps[key]) { - return true - } - } - return false -} - -// DEV only -export function getReasonForComponentUpdate( - prevVNode: VNode, - nextVNode: VNode -): any { - const reasons = [] - const { childFlags: prevChildFlags } = prevVNode - const { childFlags: nextChildFlags } = nextVNode - if ( - prevChildFlags !== nextChildFlags || - (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0 - ) { - reasons.push({ - type: `slots may have changed`, - tip: `use function slots + $stable: true to avoid slot-triggered child updates.` - }) - } - const prevProps = prevVNode.data || EMPTY_OBJ - const nextProps = nextVNode.data || EMPTY_OBJ - for (const key in nextProps) { - if (nextProps[key] !== prevProps[key]) { - reasons.push({ - type: 'prop changed', - key, - value: nextProps[key], - oldValue: prevProps[key] - }) - } - } - for (const key in prevProps) { - if (!(key in nextProps)) { - reasons.push({ - type: 'prop changed', - key, - value: undefined, - oldValue: prevProps[key] - }) - } - } - return { - type: 'triggered by parent', - reasons - } -} - -export function createComponentClassFromOptions( - options: ComponentOptions -): ComponentClass { - class AnonymousComponent extends Component { - static options = options - // indicate this component was created from options - static fromOptions = true - } - const proto = AnonymousComponent.prototype as any - for (const key in options) { - const value = options[key] - if (key === 'render') { - if (__COMPAT__) { - options.render = function() { - return value.call(this, h) - } - } - // so that we can call instance.render directly - proto.render = options.render - } else if (key === 'computed') { - // create computed setters on prototype - // (getters are handled by the render proxy) - for (const computedKey in value) { - const computed = value[computedKey] - const set = isObject(computed) && computed.set - if (set) { - Object.defineProperty(proto, computedKey, { - configurable: true, - set - }) - } - } - } else if (key === 'methods') { - for (const method in value) { - if (__DEV__ && proto.hasOwnProperty(method)) { - warn( - `Object syntax contains method name that conflicts with ` + - `lifecycle hook: "${method}"` - ) - } - proto[method] = value[method] - } - } else if (__COMPAT__) { - if (key === 'name') { - options.displayName = value - } else if (key === 'render') { - options.render = function() { - return value.call(this, h) - } - } else if (key === 'beforeDestroy') { - options.beforeUnmount = value - } else if (key === 'destroyed') { - options.unmounted = value - } - } - } - return AnonymousComponent as ComponentClass -} diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index b9331017f3..38ecc85414 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -24,14 +24,16 @@ import { VNodeChildren } from './vdom' import { ComponentInstance } from './component' +import { + createComponentInstance, + teardownComponentInstance +} from './componentInstance' import { renderInstanceRoot, renderFunctionalRoot, - createComponentInstance, - teardownComponentInstance, shouldUpdateComponent, getReasonForComponentUpdate -} from './componentUtils' +} from './componentRenderUtils' import { KeepAliveSymbol } from './optional/keepAlive' import { pushWarningContext, popWarningContext, warn } from './warning' import { resolveProps } from './componentProps' diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index edfd24ed80..56f9a4877b 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -52,7 +52,4 @@ export { VNode, Slots } from './vdom' // Internal API, for libraries or renderers that need to perform low level work export * from './componentOptions' -export { - createComponentInstance, - createComponentClassFromOptions -} from './componentUtils' +export { createComponentInstance } from './componentInstance' diff --git a/packages/runtime-core/src/optional/mixins.ts b/packages/runtime-core/src/optional/mixins.ts index 35fe31ea18..86eb6cbbc7 100644 --- a/packages/runtime-core/src/optional/mixins.ts +++ b/packages/runtime-core/src/optional/mixins.ts @@ -1,5 +1,5 @@ import { Component } from '../component' -import { createComponentClassFromOptions } from '../componentUtils' +import { createComponentClassFromOptions } from '../componentOptions' import { ComponentOptions, resolveComponentOptionsFromClass, diff --git a/packages/runtime-core/src/vdom.ts b/packages/runtime-core/src/vdom.ts index b44d2c1d77..f0fd22828b 100644 --- a/packages/runtime-core/src/vdom.ts +++ b/packages/runtime-core/src/vdom.ts @@ -4,7 +4,7 @@ import { FunctionalComponent } from './component' import { VNodeFlags, ChildrenFlags } from './flags' -import { createComponentClassFromOptions } from './componentUtils' +import { createComponentClassFromOptions } from './componentOptions' import { EMPTY_OBJ, isObject, isArray, isFunction, isString } from '@vue/shared' import { RawChildrenType, RawSlots } from './h' import { FunctionalHandle } from './createRenderer' -- 2.47.3