vue3-yuanma/packages/runtime-core/src/componentUtils.ts

339 lines
9.6 KiB
TypeScript
Raw Normal View History

import { VNodeFlags, ChildrenFlags } from './flags'
import { EMPTY_OBJ, isArray, isObject } from '@vue/shared'
2018-09-20 11:19:25 +08:00
import { h } from './h'
import { VNode, MountedVNode, createFragment } from './vdom'
import {
Component,
ComponentInstance,
ComponentClass,
FunctionalComponent
} from './component'
2018-09-19 23:35:38 +08:00
import { createTextVNode, cloneVNode } from './vdom'
import { initializeState } from './componentState'
import { initializeProps, resolveProps } from './componentProps'
import { initializeComputed, teardownComputed } from './componentComputed'
2018-09-19 23:35:38 +08:00
import { initializeWatch, teardownWatch } from './componentWatch'
import {
ComponentOptions,
resolveComponentOptionsFromClass
} from './componentOptions'
2018-09-19 23:35:38 +08:00
import { createRenderProxy } from './componentProxy'
import {
handleError,
ErrorTypes,
callLifecycleHookWithHandler
} from './errorHandling'
2018-10-12 05:21:13 +08:00
import { warn } from './warning'
2018-10-29 07:15:18 +08:00
import { setCurrentInstance, unsetCurrentInstance } from './experimental/hooks'
2018-09-19 23:35:38 +08:00
let currentVNode: VNode | null = null
2018-10-16 00:41:18 +08:00
let currentContextVNode: VNode | null = null
export function createComponentInstance<T extends Component>(
vnode: VNode
2018-10-09 23:37:24 +08:00
): ComponentInstance {
// component instance creation is done in two steps.
// first, `initializeComponentInstance` is called inside base component
2018-10-12 02:24:55 +08:00
// 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
2018-10-16 00:41:18 +08:00
currentContextVNode = vnode.contextVNode
const Component = vnode.tag as ComponentClass
2018-10-14 09:13:56 +08:00
const instance = (vnode.children = new Component() as ComponentInstance)
2018-10-17 07:10:08 +08:00
// then we finish the initialization by collecting properties set on the
// instance
2018-10-17 07:10:08 +08:00
const {
$proxy,
$options: { created, computed, watch }
} = instance
initializeState(instance, !Component.fromOptions)
2018-10-17 07:10:08 +08:00
initializeComputed(instance, computed)
initializeWatch(instance, watch)
instance.$slots = currentVNode.slots || EMPTY_OBJ
2018-10-17 07:10:08 +08:00
if (created) {
callLifecycleHookWithHandler(created, $proxy, ErrorTypes.CREATED)
}
2018-10-17 07:10:08 +08:00
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
2018-09-19 23:35:38 +08:00
// renderProxy
const proxy = (instance.$proxy = createRenderProxy(instance))
// parent chain management
if (currentContextVNode !== null) {
2018-10-11 01:13:27 +08:00
// 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
2018-10-11 01:13:27 +08:00
}
2018-09-19 23:35:38 +08:00
} else {
instance.$root = proxy
}
// beforeCreate hook is called right in the constructor
2018-10-17 07:10:08 +08:00
const { beforeCreate, props } = instance.$options
if (beforeCreate) {
callLifecycleHookWithHandler(beforeCreate, proxy, ErrorTypes.BEFORE_CREATE)
2018-09-19 23:35:38 +08:00
}
2018-10-17 07:10:08 +08:00
initializeProps(instance, props, (currentVNode as VNode).data)
2018-09-19 23:35:38 +08:00
}
2018-10-29 07:15:18 +08:00
export let isRendering = false
2018-10-09 23:37:24 +08:00
export function renderInstanceRoot(instance: ComponentInstance): VNode {
2018-09-24 08:30:26 +08:00
let vnode
const {
$options: { hooks },
render,
$proxy,
$props,
$slots,
$attrs,
$parentVNode
} = instance
2018-09-24 08:30:26 +08:00
try {
2018-10-29 07:15:18 +08:00
setCurrentInstance(instance)
if (hooks) {
instance._hookProps = hooks.call($proxy, $props) || null
2018-10-29 07:15:18 +08:00
}
if (__DEV__) {
isRendering = true
}
vnode = render.call($proxy, $props, $slots, $attrs, $parentVNode)
2018-10-29 07:15:18 +08:00
if (__DEV__) {
isRendering = false
}
unsetCurrentInstance()
} catch (err) {
handleError(err, instance, ErrorTypes.RENDER)
2018-09-24 08:30:26 +08:00
}
return normalizeComponentRoot(vnode, $parentVNode)
2018-09-19 23:35:38 +08:00
}
export function renderFunctionalRoot(vnode: VNode): VNode {
const render = vnode.tag as FunctionalComponent
const [props, 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)
}
2018-10-09 23:37:24 +08:00
export function teardownComponentInstance(instance: ComponentInstance) {
2018-09-27 05:10:34 +08:00
if (instance._unmounted) {
return
}
2018-09-19 23:35:38 +08:00
const parentComponent = instance.$parent && instance.$parent._self
2018-09-27 05:10:34 +08:00
if (parentComponent && !parentComponent._unmounted) {
2018-09-19 23:35:38 +08:00
parentComponent.$children.splice(
parentComponent.$children.indexOf(instance.$proxy),
1
)
}
teardownComputed(instance)
teardownWatch(instance)
}
function normalizeComponentRoot(
2018-09-19 23:35:38 +08:00
vnode: any,
componentVNode: VNode | null
2018-09-19 23:35:38 +08:00
): VNode {
if (vnode == null) {
vnode = createTextVNode('')
} else if (!isObject(vnode)) {
2018-09-19 23:35:38 +08:00
vnode = createTextVNode(vnode + '')
} else if (isArray(vnode)) {
2018-09-26 05:49:47 +08:00
if (vnode.length === 1) {
vnode = normalizeComponentRoot(vnode[0], componentVNode)
2018-09-26 05:49:47 +08:00
} else {
vnode = createFragment(vnode)
}
2018-09-19 23:35:38 +08:00
} else {
2018-09-27 05:10:34 +08:00
const { el, flags } = vnode
2018-09-19 23:35:38 +08:00
if (
componentVNode &&
(flags & VNodeFlags.COMPONENT || flags & VNodeFlags.ELEMENT)
) {
if (el) {
vnode = cloneVNode(vnode as VNode)
2018-09-19 23:35:38 +08:00
}
if (flags & VNodeFlags.COMPONENT) {
vnode.parentVNode = componentVNode
}
2018-09-27 05:10:34 +08:00
} else if (el) {
vnode = cloneVNode(vnode as VNode)
2018-09-19 23:35:38 +08:00
}
}
return vnode
}
export function shouldUpdateComponent(
prevVNode: VNode,
nextVNode: VNode
2018-09-19 23:35:38 +08:00
): 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
}
2018-09-19 23:35:38 +08:00
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
2018-09-19 23:35:38 +08:00
}
}
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
}
2018-09-19 23:35:38 +08:00
}
export function createComponentClassFromOptions(
options: ComponentOptions
): ComponentClass {
2018-10-09 23:37:24 +08:00
class AnonymousComponent extends Component {
2018-10-10 08:22:29 +08:00
static options = options
// indicate this component was created from options
static fromOptions = true
2018-09-19 23:35:38 +08:00
}
2018-10-09 23:37:24 +08:00
const proto = AnonymousComponent.prototype as any
2018-09-19 23:35:38 +08:00
for (const key in options) {
const value = options[key]
if (key === 'render') {
2018-10-10 08:22:29 +08:00
if (__COMPAT__) {
options.render = function() {
return value.call(this, h)
2018-10-09 23:37:24 +08:00
}
}
// so that we can call instance.render directly
proto.render = options.render
2018-10-10 08:22:29 +08:00
} else if (key === 'computed') {
// create computed setters on prototype
// (getters are handled by the render proxy)
2018-10-10 08:22:29 +08:00
for (const computedKey in value) {
const computed = value[computedKey]
const set = isObject(computed) && computed.set
if (set) {
Object.defineProperty(proto, computedKey, {
configurable: true,
set
})
}
2018-10-10 08:22:29 +08:00
}
} else if (key === 'methods') {
2018-09-20 11:43:27 +08:00
for (const method in value) {
2018-10-09 23:37:24 +08:00
if (__DEV__ && proto.hasOwnProperty(method)) {
2018-10-12 05:21:13 +08:00
warn(
2018-10-09 23:37:24 +08:00
`Object syntax contains method name that conflicts with ` +
`lifecycle hook: "${method}"`
)
}
proto[method] = value[method]
2018-09-20 11:43:27 +08:00
}
} 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
}
2018-09-20 11:43:27 +08:00
}
2018-09-19 23:35:38 +08:00
}
2018-10-09 23:37:24 +08:00
return AnonymousComponent as ComponentClass
2018-09-19 23:35:38 +08:00
}