diff options
Diffstat (limited to 'node_modules/vue/src/core/vdom')
16 files changed, 2073 insertions, 0 deletions
diff --git a/node_modules/vue/src/core/vdom/create-component.js b/node_modules/vue/src/core/vdom/create-component.js new file mode 100644 index 00000000..89578224 --- /dev/null +++ b/node_modules/vue/src/core/vdom/create-component.js @@ -0,0 +1,257 @@ +/* @flow */ + +import VNode from './vnode' +import { resolveConstructorOptions } from 'core/instance/init' +import { queueActivatedComponent } from 'core/observer/scheduler' +import { createFunctionalComponent } from './create-functional-component' + +import { + warn, + isDef, + isUndef, + isTrue, + isObject +} from '../util/index' + +import { + resolveAsyncComponent, + createAsyncPlaceholder, + extractPropsFromVNodeData +} from './helpers/index' + +import { + callHook, + activeInstance, + updateChildComponent, + activateChildComponent, + deactivateChildComponent +} from '../instance/lifecycle' + +import { + isRecyclableComponent, + renderRecyclableComponentTemplate +} from 'weex/runtime/recycle-list/render-component-template' + +// inline hooks to be invoked on component VNodes during patch +const componentVNodeHooks = { + init ( + vnode: VNodeWithData, + hydrating: boolean, + parentElm: ?Node, + refElm: ?Node + ): ?boolean { + if ( + vnode.componentInstance && + !vnode.componentInstance._isDestroyed && + vnode.data.keepAlive + ) { + // kept-alive components, treat as a patch + const mountedNode: any = vnode // work around flow + componentVNodeHooks.prepatch(mountedNode, mountedNode) + } else { + const child = vnode.componentInstance = createComponentInstanceForVnode( + vnode, + activeInstance, + parentElm, + refElm + ) + child.$mount(hydrating ? vnode.elm : undefined, hydrating) + } + }, + + prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { + const options = vnode.componentOptions + const child = vnode.componentInstance = oldVnode.componentInstance + updateChildComponent( + child, + options.propsData, // updated props + options.listeners, // updated listeners + vnode, // new parent vnode + options.children // new children + ) + }, + + insert (vnode: MountedComponentVNode) { + const { context, componentInstance } = vnode + if (!componentInstance._isMounted) { + componentInstance._isMounted = true + callHook(componentInstance, 'mounted') + } + if (vnode.data.keepAlive) { + if (context._isMounted) { + // vue-router#1212 + // During updates, a kept-alive component's child components may + // change, so directly walking the tree here may call activated hooks + // on incorrect children. Instead we push them into a queue which will + // be processed after the whole patch process ended. + queueActivatedComponent(componentInstance) + } else { + activateChildComponent(componentInstance, true /* direct */) + } + } + }, + + destroy (vnode: MountedComponentVNode) { + const { componentInstance } = vnode + if (!componentInstance._isDestroyed) { + if (!vnode.data.keepAlive) { + componentInstance.$destroy() + } else { + deactivateChildComponent(componentInstance, true /* direct */) + } + } + } +} + +const hooksToMerge = Object.keys(componentVNodeHooks) + +export function createComponent ( + Ctor: Class<Component> | Function | Object | void, + data: ?VNodeData, + context: Component, + children: ?Array<VNode>, + tag?: string +): VNode | Array<VNode> | void { + if (isUndef(Ctor)) { + return + } + + const baseCtor = context.$options._base + + // plain options object: turn it into a constructor + if (isObject(Ctor)) { + Ctor = baseCtor.extend(Ctor) + } + + // if at this stage it's not a constructor or an async component factory, + // reject. + if (typeof Ctor !== 'function') { + if (process.env.NODE_ENV !== 'production') { + warn(`Invalid Component definition: ${String(Ctor)}`, context) + } + return + } + + // async component + let asyncFactory + if (isUndef(Ctor.cid)) { + asyncFactory = Ctor + Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) + if (Ctor === undefined) { + // return a placeholder node for async component, which is rendered + // as a comment node but preserves all the raw information for the node. + // the information will be used for async server-rendering and hydration. + return createAsyncPlaceholder( + asyncFactory, + data, + context, + children, + tag + ) + } + } + + data = data || {} + + // resolve constructor options in case global mixins are applied after + // component constructor creation + resolveConstructorOptions(Ctor) + + // transform component v-model data into props & events + if (isDef(data.model)) { + transformModel(Ctor.options, data) + } + + // extract props + const propsData = extractPropsFromVNodeData(data, Ctor, tag) + + // functional component + if (isTrue(Ctor.options.functional)) { + return createFunctionalComponent(Ctor, propsData, data, context, children) + } + + // extract listeners, since these needs to be treated as + // child component listeners instead of DOM listeners + const listeners = data.on + // replace with listeners with .native modifier + // so it gets processed during parent component patch. + data.on = data.nativeOn + + if (isTrue(Ctor.options.abstract)) { + // abstract components do not keep anything + // other than props & listeners & slot + + // work around flow + const slot = data.slot + data = {} + if (slot) { + data.slot = slot + } + } + + // install component management hooks onto the placeholder node + installComponentHooks(data) + + // return a placeholder vnode + const name = Ctor.options.name || tag + const vnode = new VNode( + `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, + data, undefined, undefined, undefined, context, + { Ctor, propsData, listeners, tag, children }, + asyncFactory + ) + + // Weex specific: invoke recycle-list optimized @render function for + // extracting cell-slot template. + // https://github.com/Hanks10100/weex-native-directive/tree/master/component + /* istanbul ignore if */ + if (__WEEX__ && isRecyclableComponent(vnode)) { + return renderRecyclableComponentTemplate(vnode) + } + + return vnode +} + +export function createComponentInstanceForVnode ( + vnode: any, // we know it's MountedComponentVNode but flow doesn't + parent: any, // activeInstance in lifecycle state + parentElm?: ?Node, + refElm?: ?Node +): Component { + const options: InternalComponentOptions = { + _isComponent: true, + parent, + _parentVnode: vnode, + _parentElm: parentElm || null, + _refElm: refElm || null + } + // check inline-template render functions + const inlineTemplate = vnode.data.inlineTemplate + if (isDef(inlineTemplate)) { + options.render = inlineTemplate.render + options.staticRenderFns = inlineTemplate.staticRenderFns + } + return new vnode.componentOptions.Ctor(options) +} + +function installComponentHooks (data: VNodeData) { + const hooks = data.hook || (data.hook = {}) + for (let i = 0; i < hooksToMerge.length; i++) { + const key = hooksToMerge[i] + hooks[key] = componentVNodeHooks[key] + } +} + +// transform component v-model info (value and callback) into +// prop and event handler respectively. +function transformModel (options, data: any) { + const prop = (options.model && options.model.prop) || 'value' + const event = (options.model && options.model.event) || 'input' + ;(data.props || (data.props = {}))[prop] = data.model.value + const on = data.on || (data.on = {}) + if (isDef(on[event])) { + on[event] = [data.model.callback].concat(on[event]) + } else { + on[event] = data.model.callback + } +} diff --git a/node_modules/vue/src/core/vdom/create-element.js b/node_modules/vue/src/core/vdom/create-element.js new file mode 100644 index 00000000..c5cde2c6 --- /dev/null +++ b/node_modules/vue/src/core/vdom/create-element.js @@ -0,0 +1,160 @@ +/* @flow */ + +import config from '../config' +import VNode, { createEmptyVNode } from './vnode' +import { createComponent } from './create-component' +import { traverse } from '../observer/traverse' + +import { + warn, + isDef, + isUndef, + isTrue, + isObject, + isPrimitive, + resolveAsset +} from '../util/index' + +import { + normalizeChildren, + simpleNormalizeChildren +} from './helpers/index' + +const SIMPLE_NORMALIZE = 1 +const ALWAYS_NORMALIZE = 2 + +// wrapper function for providing a more flexible interface +// without getting yelled at by flow +export function createElement ( + context: Component, + tag: any, + data: any, + children: any, + normalizationType: any, + alwaysNormalize: boolean +): VNode | Array<VNode> { + if (Array.isArray(data) || isPrimitive(data)) { + normalizationType = children + children = data + data = undefined + } + if (isTrue(alwaysNormalize)) { + normalizationType = ALWAYS_NORMALIZE + } + return _createElement(context, tag, data, children, normalizationType) +} + +export function _createElement ( + context: Component, + tag?: string | Class<Component> | Function | Object, + data?: VNodeData, + children?: any, + normalizationType?: number +): VNode | Array<VNode> { + if (isDef(data) && isDef((data: any).__ob__)) { + process.env.NODE_ENV !== 'production' && warn( + `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + + 'Always create fresh vnode data objects in each render!', + context + ) + return createEmptyVNode() + } + // object syntax in v-bind + if (isDef(data) && isDef(data.is)) { + tag = data.is + } + if (!tag) { + // in case of component :is set to falsy value + return createEmptyVNode() + } + // warn against non-primitive key + if (process.env.NODE_ENV !== 'production' && + isDef(data) && isDef(data.key) && !isPrimitive(data.key) + ) { + if (!__WEEX__ || !('@binding' in data.key)) { + warn( + 'Avoid using non-primitive value as key, ' + + 'use string/number value instead.', + context + ) + } + } + // support single function children as default scoped slot + if (Array.isArray(children) && + typeof children[0] === 'function' + ) { + data = data || {} + data.scopedSlots = { default: children[0] } + children.length = 0 + } + if (normalizationType === ALWAYS_NORMALIZE) { + children = normalizeChildren(children) + } else if (normalizationType === SIMPLE_NORMALIZE) { + children = simpleNormalizeChildren(children) + } + let vnode, ns + if (typeof tag === 'string') { + let Ctor + ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) + if (config.isReservedTag(tag)) { + // platform built-in elements + vnode = new VNode( + config.parsePlatformTagName(tag), data, children, + undefined, undefined, context + ) + } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { + // component + vnode = createComponent(Ctor, data, context, children, tag) + } else { + // unknown or unlisted namespaced elements + // check at runtime because it may get assigned a namespace when its + // parent normalizes children + vnode = new VNode( + tag, data, children, + undefined, undefined, context + ) + } + } else { + // direct component options / constructor + vnode = createComponent(tag, data, context, children) + } + if (Array.isArray(vnode)) { + return vnode + } else if (isDef(vnode)) { + if (isDef(ns)) applyNS(vnode, ns) + if (isDef(data)) registerDeepBindings(data) + return vnode + } else { + return createEmptyVNode() + } +} + +function applyNS (vnode, ns, force) { + vnode.ns = ns + if (vnode.tag === 'foreignObject') { + // use default namespace inside foreignObject + ns = undefined + force = true + } + if (isDef(vnode.children)) { + for (let i = 0, l = vnode.children.length; i < l; i++) { + const child = vnode.children[i] + if (isDef(child.tag) && ( + isUndef(child.ns) || (isTrue(force) && child.tag !== 'svg'))) { + applyNS(child, ns, force) + } + } + } +} + +// ref #5318 +// necessary to ensure parent re-render when deep bindings like :style and +// :class are used on slot nodes +function registerDeepBindings (data) { + if (isObject(data.style)) { + traverse(data.style) + } + if (isObject(data.class)) { + traverse(data.class) + } +} diff --git a/node_modules/vue/src/core/vdom/create-functional-component.js b/node_modules/vue/src/core/vdom/create-functional-component.js new file mode 100644 index 00000000..efed801d --- /dev/null +++ b/node_modules/vue/src/core/vdom/create-functional-component.js @@ -0,0 +1,136 @@ +/* @flow */ + +import VNode, { cloneVNode } from './vnode' +import { createElement } from './create-element' +import { resolveInject } from '../instance/inject' +import { normalizeChildren } from '../vdom/helpers/normalize-children' +import { resolveSlots } from '../instance/render-helpers/resolve-slots' +import { installRenderHelpers } from '../instance/render-helpers/index' + +import { + isDef, + isTrue, + hasOwn, + camelize, + emptyObject, + validateProp +} from '../util/index' + +export function FunctionalRenderContext ( + data: VNodeData, + props: Object, + children: ?Array<VNode>, + parent: Component, + Ctor: Class<Component> +) { + const options = Ctor.options + // ensure the createElement function in functional components + // gets a unique context - this is necessary for correct named slot check + let contextVm + if (hasOwn(parent, '_uid')) { + contextVm = Object.create(parent) + // $flow-disable-line + contextVm._original = parent + } else { + // the context vm passed in is a functional context as well. + // in this case we want to make sure we are able to get a hold to the + // real context instance. + contextVm = parent + // $flow-disable-line + parent = parent._original + } + const isCompiled = isTrue(options._compiled) + const needNormalization = !isCompiled + + this.data = data + this.props = props + this.children = children + this.parent = parent + this.listeners = data.on || emptyObject + this.injections = resolveInject(options.inject, parent) + this.slots = () => resolveSlots(children, parent) + + // support for compiled functional template + if (isCompiled) { + // exposing $options for renderStatic() + this.$options = options + // pre-resolve slots for renderSlot() + this.$slots = this.slots() + this.$scopedSlots = data.scopedSlots || emptyObject + } + + if (options._scopeId) { + this._c = (a, b, c, d) => { + const vnode = createElement(contextVm, a, b, c, d, needNormalization) + if (vnode && !Array.isArray(vnode)) { + vnode.fnScopeId = options._scopeId + vnode.fnContext = parent + } + return vnode + } + } else { + this._c = (a, b, c, d) => createElement(contextVm, a, b, c, d, needNormalization) + } +} + +installRenderHelpers(FunctionalRenderContext.prototype) + +export function createFunctionalComponent ( + Ctor: Class<Component>, + propsData: ?Object, + data: VNodeData, + contextVm: Component, + children: ?Array<VNode> +): VNode | Array<VNode> | void { + const options = Ctor.options + const props = {} + const propOptions = options.props + if (isDef(propOptions)) { + for (const key in propOptions) { + props[key] = validateProp(key, propOptions, propsData || emptyObject) + } + } else { + if (isDef(data.attrs)) mergeProps(props, data.attrs) + if (isDef(data.props)) mergeProps(props, data.props) + } + + const renderContext = new FunctionalRenderContext( + data, + props, + children, + contextVm, + Ctor + ) + + const vnode = options.render.call(null, renderContext._c, renderContext) + + if (vnode instanceof VNode) { + return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options) + } else if (Array.isArray(vnode)) { + const vnodes = normalizeChildren(vnode) || [] + const res = new Array(vnodes.length) + for (let i = 0; i < vnodes.length; i++) { + res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options) + } + return res + } +} + +function cloneAndMarkFunctionalResult (vnode, data, contextVm, options) { + // #7817 clone node before setting fnContext, otherwise if the node is reused + // (e.g. it was from a cached normal slot) the fnContext causes named slots + // that should not be matched to match. + const clone = cloneVNode(vnode) + clone.fnContext = contextVm + clone.fnOptions = options + if (data.slot) { + (clone.data || (clone.data = {})).slot = data.slot + } + return clone +} + +function mergeProps (to, from) { + for (const key in from) { + to[camelize(key)] = from[key] + } +} diff --git a/node_modules/vue/src/core/vdom/helpers/extract-props.js b/node_modules/vue/src/core/vdom/helpers/extract-props.js new file mode 100644 index 00000000..f2a41661 --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/extract-props.js @@ -0,0 +1,75 @@ +/* @flow */ + +import { + tip, + hasOwn, + isDef, + isUndef, + hyphenate, + formatComponentName +} from 'core/util/index' + +export function extractPropsFromVNodeData ( + data: VNodeData, + Ctor: Class<Component>, + tag?: string +): ?Object { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + const propOptions = Ctor.options.props + if (isUndef(propOptions)) { + return + } + const res = {} + const { attrs, props } = data + if (isDef(attrs) || isDef(props)) { + for (const key in propOptions) { + const altKey = hyphenate(key) + if (process.env.NODE_ENV !== 'production') { + const keyInLowerCase = key.toLowerCase() + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + tip( + `Prop "${keyInLowerCase}" is passed to component ` + + `${formatComponentName(tag || Ctor)}, but the declared prop name is` + + ` "${key}". ` + + `Note that HTML attributes are case-insensitive and camelCased ` + + `props need to use their kebab-case equivalents when using in-DOM ` + + `templates. You should probably use "${altKey}" instead of "${key}".` + ) + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false) + } + } + return res +} + +function checkProp ( + res: Object, + hash: ?Object, + key: string, + altKey: string, + preserve: boolean +): boolean { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key] + if (!preserve) { + delete hash[key] + } + return true + } else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey] + if (!preserve) { + delete hash[altKey] + } + return true + } + } + return false +} diff --git a/node_modules/vue/src/core/vdom/helpers/get-first-component-child.js b/node_modules/vue/src/core/vdom/helpers/get-first-component-child.js new file mode 100644 index 00000000..f8649dd8 --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/get-first-component-child.js @@ -0,0 +1,15 @@ +/* @flow */ + +import { isDef } from 'shared/util' +import { isAsyncPlaceholder } from './is-async-placeholder' + +export function getFirstComponentChild (children: ?Array<VNode>): ?VNode { + if (Array.isArray(children)) { + for (let i = 0; i < children.length; i++) { + const c = children[i] + if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) { + return c + } + } + } +} diff --git a/node_modules/vue/src/core/vdom/helpers/index.js b/node_modules/vue/src/core/vdom/helpers/index.js new file mode 100644 index 00000000..320cf4c4 --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/index.js @@ -0,0 +1,9 @@ +/* @flow */ + +export * from './merge-hook' +export * from './extract-props' +export * from './update-listeners' +export * from './normalize-children' +export * from './resolve-async-component' +export * from './get-first-component-child' +export * from './is-async-placeholder' diff --git a/node_modules/vue/src/core/vdom/helpers/is-async-placeholder.js b/node_modules/vue/src/core/vdom/helpers/is-async-placeholder.js new file mode 100644 index 00000000..6c4a6596 --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/is-async-placeholder.js @@ -0,0 +1,5 @@ +/* @flow */ + +export function isAsyncPlaceholder (node: VNode): boolean { + return node.isComment && node.asyncFactory +} diff --git a/node_modules/vue/src/core/vdom/helpers/merge-hook.js b/node_modules/vue/src/core/vdom/helpers/merge-hook.js new file mode 100644 index 00000000..0bb96e8b --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/merge-hook.js @@ -0,0 +1,38 @@ +/* @flow */ + +import VNode from '../vnode' +import { createFnInvoker } from './update-listeners' +import { remove, isDef, isUndef, isTrue } from 'shared/util' + +export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}) + } + let invoker + const oldHook = def[hookKey] + + function wrappedHook () { + hook.apply(this, arguments) + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove(invoker.fns, wrappedHook) + } + + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]) + } else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook + invoker.fns.push(wrappedHook) + } else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]) + } + } + + invoker.merged = true + def[hookKey] = invoker +} diff --git a/node_modules/vue/src/core/vdom/helpers/normalize-children.js b/node_modules/vue/src/core/vdom/helpers/normalize-children.js new file mode 100644 index 00000000..a4622ef4 --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/normalize-children.js @@ -0,0 +1,89 @@ +/* @flow */ + +import VNode, { createTextVNode } from 'core/vdom/vnode' +import { isFalse, isTrue, isDef, isUndef, isPrimitive } from 'shared/util' + +// The template compiler attempts to minimize the need for normalization by +// statically analyzing the template at compile time. +// +// For plain HTML markup, normalization can be completely skipped because the +// generated render function is guaranteed to return Array<VNode>. There are +// two cases where extra normalization is needed: + +// 1. When the children contains components - because a functional component +// may return an Array instead of a single root. In this case, just a simple +// normalization is needed - if any child is an Array, we flatten the whole +// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep +// because functional components already normalize their own children. +export function simpleNormalizeChildren (children: any) { + for (let i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children) + } + } + return children +} + +// 2. When the children contains constructs that always generated nested Arrays, +// e.g. <template>, <slot>, v-for, or when the children is provided by user +// with hand-written render functions / JSX. In such cases a full normalization +// is needed to cater to all possible types of children values. +export function normalizeChildren (children: any): ?Array<VNode> { + return isPrimitive(children) + ? [createTextVNode(children)] + : Array.isArray(children) + ? normalizeArrayChildren(children) + : undefined +} + +function isTextNode (node): boolean { + return isDef(node) && isDef(node.text) && isFalse(node.isComment) +} + +function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> { + const res = [] + let i, c, lastIndex, last + for (i = 0; i < children.length; i++) { + c = children[i] + if (isUndef(c) || typeof c === 'boolean') continue + lastIndex = res.length - 1 + last = res[lastIndex] + // nested + if (Array.isArray(c)) { + if (c.length > 0) { + c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`) + // merge adjacent text nodes + if (isTextNode(c[0]) && isTextNode(last)) { + res[lastIndex] = createTextVNode(last.text + (c[0]: any).text) + c.shift() + } + res.push.apply(res, c) + } + } else if (isPrimitive(c)) { + if (isTextNode(last)) { + // merge adjacent text nodes + // this is necessary for SSR hydration because text nodes are + // essentially merged when rendered to HTML strings + res[lastIndex] = createTextVNode(last.text + c) + } else if (c !== '') { + // convert primitive to vnode + res.push(createTextVNode(c)) + } + } else { + if (isTextNode(c) && isTextNode(last)) { + // merge adjacent text nodes + res[lastIndex] = createTextVNode(last.text + c.text) + } else { + // default key for nested array children (likely generated by v-for) + if (isTrue(children._isVList) && + isDef(c.tag) && + isUndef(c.key) && + isDef(nestedIndex)) { + c.key = `__vlist${nestedIndex}_${i}__` + } + res.push(c) + } + } + } + return res +} diff --git a/node_modules/vue/src/core/vdom/helpers/resolve-async-component.js b/node_modules/vue/src/core/vdom/helpers/resolve-async-component.js new file mode 100644 index 00000000..99ce5ce4 --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/resolve-async-component.js @@ -0,0 +1,140 @@ +/* @flow */ + +import { + warn, + once, + isDef, + isUndef, + isTrue, + isObject, + hasSymbol +} from 'core/util/index' + +import { createEmptyVNode } from 'core/vdom/vnode' + +function ensureCtor (comp: any, base) { + if ( + comp.__esModule || + (hasSymbol && comp[Symbol.toStringTag] === 'Module') + ) { + comp = comp.default + } + return isObject(comp) + ? base.extend(comp) + : comp +} + +export function createAsyncPlaceholder ( + factory: Function, + data: ?VNodeData, + context: Component, + children: ?Array<VNode>, + tag: ?string +): VNode { + const node = createEmptyVNode() + node.asyncFactory = factory + node.asyncMeta = { data, context, children, tag } + return node +} + +export function resolveAsyncComponent ( + factory: Function, + baseCtor: Class<Component>, + context: Component +): Class<Component> | void { + if (isTrue(factory.error) && isDef(factory.errorComp)) { + return factory.errorComp + } + + if (isDef(factory.resolved)) { + return factory.resolved + } + + if (isTrue(factory.loading) && isDef(factory.loadingComp)) { + return factory.loadingComp + } + + if (isDef(factory.contexts)) { + // already pending + factory.contexts.push(context) + } else { + const contexts = factory.contexts = [context] + let sync = true + + const forceRender = () => { + for (let i = 0, l = contexts.length; i < l; i++) { + contexts[i].$forceUpdate() + } + } + + const resolve = once((res: Object | Class<Component>) => { + // cache resolved + factory.resolved = ensureCtor(res, baseCtor) + // invoke callbacks only if this is not a synchronous resolve + // (async resolves are shimmed as synchronous during SSR) + if (!sync) { + forceRender() + } + }) + + const reject = once(reason => { + process.env.NODE_ENV !== 'production' && warn( + `Failed to resolve async component: ${String(factory)}` + + (reason ? `\nReason: ${reason}` : '') + ) + if (isDef(factory.errorComp)) { + factory.error = true + forceRender() + } + }) + + const res = factory(resolve, reject) + + if (isObject(res)) { + if (typeof res.then === 'function') { + // () => Promise + if (isUndef(factory.resolved)) { + res.then(resolve, reject) + } + } else if (isDef(res.component) && typeof res.component.then === 'function') { + res.component.then(resolve, reject) + + if (isDef(res.error)) { + factory.errorComp = ensureCtor(res.error, baseCtor) + } + + if (isDef(res.loading)) { + factory.loadingComp = ensureCtor(res.loading, baseCtor) + if (res.delay === 0) { + factory.loading = true + } else { + setTimeout(() => { + if (isUndef(factory.resolved) && isUndef(factory.error)) { + factory.loading = true + forceRender() + } + }, res.delay || 200) + } + } + + if (isDef(res.timeout)) { + setTimeout(() => { + if (isUndef(factory.resolved)) { + reject( + process.env.NODE_ENV !== 'production' + ? `timeout (${res.timeout}ms)` + : null + ) + } + }, res.timeout) + } + } + } + + sync = false + // return in case resolved synchronously + return factory.loading + ? factory.loadingComp + : factory.resolved + } +} diff --git a/node_modules/vue/src/core/vdom/helpers/update-listeners.js b/node_modules/vue/src/core/vdom/helpers/update-listeners.js new file mode 100644 index 00000000..11c5a2cd --- /dev/null +++ b/node_modules/vue/src/core/vdom/helpers/update-listeners.js @@ -0,0 +1,83 @@ +/* @flow */ + +import { warn } from 'core/util/index' +import { cached, isUndef, isPlainObject } from 'shared/util' + +const normalizeEvent = cached((name: string): { + name: string, + once: boolean, + capture: boolean, + passive: boolean, + handler?: Function, + params?: Array<any> +} => { + const passive = name.charAt(0) === '&' + name = passive ? name.slice(1) : name + const once = name.charAt(0) === '~' // Prefixed last, checked first + name = once ? name.slice(1) : name + const capture = name.charAt(0) === '!' + name = capture ? name.slice(1) : name + return { + name, + once, + capture, + passive + } +}) + +export function createFnInvoker (fns: Function | Array<Function>): Function { + function invoker () { + const fns = invoker.fns + if (Array.isArray(fns)) { + const cloned = fns.slice() + for (let i = 0; i < cloned.length; i++) { + cloned[i].apply(null, arguments) + } + } else { + // return handler return value for single handlers + return fns.apply(null, arguments) + } + } + invoker.fns = fns + return invoker +} + +export function updateListeners ( + on: Object, + oldOn: Object, + add: Function, + remove: Function, + vm: Component +) { + let name, def, cur, old, event + for (name in on) { + def = cur = on[name] + old = oldOn[name] + event = normalizeEvent(name) + /* istanbul ignore if */ + if (__WEEX__ && isPlainObject(def)) { + cur = def.handler + event.params = def.params + } + if (isUndef(cur)) { + process.env.NODE_ENV !== 'production' && warn( + `Invalid handler for event "${event.name}": got ` + String(cur), + vm + ) + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur) + } + add(event.name, cur, event.once, event.capture, event.passive, event.params) + } else if (cur !== old) { + old.fns = cur + on[name] = old + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name) + remove(event.name, oldOn[name], event.capture) + } + } +} diff --git a/node_modules/vue/src/core/vdom/modules/directives.js b/node_modules/vue/src/core/vdom/modules/directives.js new file mode 100644 index 00000000..639982fa --- /dev/null +++ b/node_modules/vue/src/core/vdom/modules/directives.js @@ -0,0 +1,119 @@ +/* @flow */ + +import { emptyNode } from 'core/vdom/patch' +import { resolveAsset, handleError } from 'core/util/index' +import { mergeVNodeHook } from 'core/vdom/helpers/index' + +export default { + create: updateDirectives, + update: updateDirectives, + destroy: function unbindDirectives (vnode: VNodeWithData) { + updateDirectives(vnode, emptyNode) + } +} + +function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) { + if (oldVnode.data.directives || vnode.data.directives) { + _update(oldVnode, vnode) + } +} + +function _update (oldVnode, vnode) { + const isCreate = oldVnode === emptyNode + const isDestroy = vnode === emptyNode + const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context) + const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) + + const dirsWithInsert = [] + const dirsWithPostpatch = [] + + let key, oldDir, dir + for (key in newDirs) { + oldDir = oldDirs[key] + dir = newDirs[key] + if (!oldDir) { + // new directive, bind + callHook(dir, 'bind', vnode, oldVnode) + if (dir.def && dir.def.inserted) { + dirsWithInsert.push(dir) + } + } else { + // existing directive, update + dir.oldValue = oldDir.value + callHook(dir, 'update', vnode, oldVnode) + if (dir.def && dir.def.componentUpdated) { + dirsWithPostpatch.push(dir) + } + } + } + + if (dirsWithInsert.length) { + const callInsert = () => { + for (let i = 0; i < dirsWithInsert.length; i++) { + callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode) + } + } + if (isCreate) { + mergeVNodeHook(vnode, 'insert', callInsert) + } else { + callInsert() + } + } + + if (dirsWithPostpatch.length) { + mergeVNodeHook(vnode, 'postpatch', () => { + for (let i = 0; i < dirsWithPostpatch.length; i++) { + callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode) + } + }) + } + + if (!isCreate) { + for (key in oldDirs) { + if (!newDirs[key]) { + // no longer present, unbind + callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy) + } + } + } +} + +const emptyModifiers = Object.create(null) + +function normalizeDirectives ( + dirs: ?Array<VNodeDirective>, + vm: Component +): { [key: string]: VNodeDirective } { + const res = Object.create(null) + if (!dirs) { + // $flow-disable-line + return res + } + let i, dir + for (i = 0; i < dirs.length; i++) { + dir = dirs[i] + if (!dir.modifiers) { + // $flow-disable-line + dir.modifiers = emptyModifiers + } + res[getRawDirName(dir)] = dir + dir.def = resolveAsset(vm.$options, 'directives', dir.name, true) + } + // $flow-disable-line + return res +} + +function getRawDirName (dir: VNodeDirective): string { + return dir.rawName || `${dir.name}.${Object.keys(dir.modifiers || {}).join('.')}` +} + +function callHook (dir, hook, vnode, oldVnode, isDestroy) { + const fn = dir.def && dir.def[hook] + if (fn) { + try { + fn(vnode.elm, dir, vnode, oldVnode, isDestroy) + } catch (e) { + handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`) + } + } +} diff --git a/node_modules/vue/src/core/vdom/modules/index.js b/node_modules/vue/src/core/vdom/modules/index.js new file mode 100644 index 00000000..727c8646 --- /dev/null +++ b/node_modules/vue/src/core/vdom/modules/index.js @@ -0,0 +1,7 @@ +import directives from './directives' +import ref from './ref' + +export default [ + ref, + directives +] diff --git a/node_modules/vue/src/core/vdom/modules/ref.js b/node_modules/vue/src/core/vdom/modules/ref.js new file mode 100644 index 00000000..aa9bdcfc --- /dev/null +++ b/node_modules/vue/src/core/vdom/modules/ref.js @@ -0,0 +1,45 @@ +/* @flow */ + +import { remove, isDef } from 'shared/util' + +export default { + create (_: any, vnode: VNodeWithData) { + registerRef(vnode) + }, + update (oldVnode: VNodeWithData, vnode: VNodeWithData) { + if (oldVnode.data.ref !== vnode.data.ref) { + registerRef(oldVnode, true) + registerRef(vnode) + } + }, + destroy (vnode: VNodeWithData) { + registerRef(vnode, true) + } +} + +export function registerRef (vnode: VNodeWithData, isRemoval: ?boolean) { + const key = vnode.data.ref + if (!isDef(key)) return + + const vm = vnode.context + const ref = vnode.componentInstance || vnode.elm + const refs = vm.$refs + if (isRemoval) { + if (Array.isArray(refs[key])) { + remove(refs[key], ref) + } else if (refs[key] === ref) { + refs[key] = undefined + } + } else { + if (vnode.data.refInFor) { + if (!Array.isArray(refs[key])) { + refs[key] = [ref] + } else if (refs[key].indexOf(ref) < 0) { + // $flow-disable-line + refs[key].push(ref) + } + } else { + refs[key] = ref + } + } +} diff --git a/node_modules/vue/src/core/vdom/patch.js b/node_modules/vue/src/core/vdom/patch.js new file mode 100644 index 00000000..9f3bcc65 --- /dev/null +++ b/node_modules/vue/src/core/vdom/patch.js @@ -0,0 +1,787 @@ +/** + * Virtual DOM patching algorithm based on Snabbdom by + * Simon Friis Vindum (@paldepind) + * Licensed under the MIT License + * https://github.com/paldepind/snabbdom/blob/master/LICENSE + * + * modified by Evan You (@yyx990803) + * + * Not type-checking this because this file is perf-critical and the cost + * of making flow understand it is not worth it. + */ + +import VNode, { cloneVNode } from './vnode' +import config from '../config' +import { SSR_ATTR } from 'shared/constants' +import { registerRef } from './modules/ref' +import { traverse } from '../observer/traverse' +import { activeInstance } from '../instance/lifecycle' +import { isTextInputType } from 'web/util/element' + +import { + warn, + isDef, + isUndef, + isTrue, + makeMap, + isRegExp, + isPrimitive +} from '../util/index' + +export const emptyNode = new VNode('', {}, []) + +const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] + +function sameVnode (a, b) { + return ( + a.key === b.key && ( + ( + a.tag === b.tag && + a.isComment === b.isComment && + isDef(a.data) === isDef(b.data) && + sameInputType(a, b) + ) || ( + isTrue(a.isAsyncPlaceholder) && + a.asyncFactory === b.asyncFactory && + isUndef(b.asyncFactory.error) + ) + ) + ) +} + +function sameInputType (a, b) { + if (a.tag !== 'input') return true + let i + const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type + const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type + return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) +} + +function createKeyToOldIdx (children, beginIdx, endIdx) { + let i, key + const map = {} + for (i = beginIdx; i <= endIdx; ++i) { + key = children[i].key + if (isDef(key)) map[key] = i + } + return map +} + +export function createPatchFunction (backend) { + let i, j + const cbs = {} + + const { modules, nodeOps } = backend + + for (i = 0; i < hooks.length; ++i) { + cbs[hooks[i]] = [] + for (j = 0; j < modules.length; ++j) { + if (isDef(modules[j][hooks[i]])) { + cbs[hooks[i]].push(modules[j][hooks[i]]) + } + } + } + + function emptyNodeAt (elm) { + return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) + } + + function createRmCb (childElm, listeners) { + function remove () { + if (--remove.listeners === 0) { + removeNode(childElm) + } + } + remove.listeners = listeners + return remove + } + + function removeNode (el) { + const parent = nodeOps.parentNode(el) + // element may have already been removed due to v-html / v-text + if (isDef(parent)) { + nodeOps.removeChild(parent, el) + } + } + + function isUnknownElement (vnode, inVPre) { + return ( + !inVPre && + !vnode.ns && + !( + config.ignoredElements.length && + config.ignoredElements.some(ignore => { + return isRegExp(ignore) + ? ignore.test(vnode.tag) + : ignore === vnode.tag + }) + ) && + config.isUnknownElement(vnode.tag) + ) + } + + let creatingElmInVPre = 0 + + function createElm ( + vnode, + insertedVnodeQueue, + parentElm, + refElm, + nested, + ownerArray, + index + ) { + if (isDef(vnode.elm) && isDef(ownerArray)) { + // This vnode was used in a previous render! + // now it's used as a new node, overwriting its elm would cause + // potential patch errors down the road when it's used as an insertion + // reference node. Instead, we clone the node on-demand before creating + // associated DOM element for it. + vnode = ownerArray[index] = cloneVNode(vnode) + } + + vnode.isRootInsert = !nested // for transition enter check + if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { + return + } + + const data = vnode.data + const children = vnode.children + const tag = vnode.tag + if (isDef(tag)) { + if (process.env.NODE_ENV !== 'production') { + if (data && data.pre) { + creatingElmInVPre++ + } + if (isUnknownElement(vnode, creatingElmInVPre)) { + warn( + 'Unknown custom element: <' + tag + '> - did you ' + + 'register the component correctly? For recursive components, ' + + 'make sure to provide the "name" option.', + vnode.context + ) + } + } + + vnode.elm = vnode.ns + ? nodeOps.createElementNS(vnode.ns, tag) + : nodeOps.createElement(tag, vnode) + setScope(vnode) + + /* istanbul ignore if */ + if (__WEEX__) { + // in Weex, the default insertion order is parent-first. + // List items can be optimized to use children-first insertion + // with append="tree". + const appendAsTree = isDef(data) && isTrue(data.appendAsTree) + if (!appendAsTree) { + if (isDef(data)) { + invokeCreateHooks(vnode, insertedVnodeQueue) + } + insert(parentElm, vnode.elm, refElm) + } + createChildren(vnode, children, insertedVnodeQueue) + if (appendAsTree) { + if (isDef(data)) { + invokeCreateHooks(vnode, insertedVnodeQueue) + } + insert(parentElm, vnode.elm, refElm) + } + } else { + createChildren(vnode, children, insertedVnodeQueue) + if (isDef(data)) { + invokeCreateHooks(vnode, insertedVnodeQueue) + } + insert(parentElm, vnode.elm, refElm) + } + + if (process.env.NODE_ENV !== 'production' && data && data.pre) { + creatingElmInVPre-- + } + } else if (isTrue(vnode.isComment)) { + vnode.elm = nodeOps.createComment(vnode.text) + insert(parentElm, vnode.elm, refElm) + } else { + vnode.elm = nodeOps.createTextNode(vnode.text) + insert(parentElm, vnode.elm, refElm) + } + } + + function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { + let i = vnode.data + if (isDef(i)) { + const isReactivated = isDef(vnode.componentInstance) && i.keepAlive + if (isDef(i = i.hook) && isDef(i = i.init)) { + i(vnode, false /* hydrating */, parentElm, refElm) + } + // after calling the init hook, if the vnode is a child component + // it should've created a child instance and mounted it. the child + // component also has set the placeholder vnode's elm. + // in that case we can just return the element and be done. + if (isDef(vnode.componentInstance)) { + initComponent(vnode, insertedVnodeQueue) + if (isTrue(isReactivated)) { + reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) + } + return true + } + } + } + + function initComponent (vnode, insertedVnodeQueue) { + if (isDef(vnode.data.pendingInsert)) { + insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert) + vnode.data.pendingInsert = null + } + vnode.elm = vnode.componentInstance.$el + if (isPatchable(vnode)) { + invokeCreateHooks(vnode, insertedVnodeQueue) + setScope(vnode) + } else { + // empty component root. + // skip all element-related modules except for ref (#3455) + registerRef(vnode) + // make sure to invoke the insert hook + insertedVnodeQueue.push(vnode) + } + } + + function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) { + let i + // hack for #4339: a reactivated component with inner transition + // does not trigger because the inner node's created hooks are not called + // again. It's not ideal to involve module-specific logic in here but + // there doesn't seem to be a better way to do it. + let innerNode = vnode + while (innerNode.componentInstance) { + innerNode = innerNode.componentInstance._vnode + if (isDef(i = innerNode.data) && isDef(i = i.transition)) { + for (i = 0; i < cbs.activate.length; ++i) { + cbs.activate[i](emptyNode, innerNode) + } + insertedVnodeQueue.push(innerNode) + break + } + } + // unlike a newly created component, + // a reactivated keep-alive component doesn't insert itself + insert(parentElm, vnode.elm, refElm) + } + + function insert (parent, elm, ref) { + if (isDef(parent)) { + if (isDef(ref)) { + if (ref.parentNode === parent) { + nodeOps.insertBefore(parent, elm, ref) + } + } else { + nodeOps.appendChild(parent, elm) + } + } + } + + function createChildren (vnode, children, insertedVnodeQueue) { + if (Array.isArray(children)) { + if (process.env.NODE_ENV !== 'production') { + checkDuplicateKeys(children) + } + for (let i = 0; i < children.length; ++i) { + createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) + } + } else if (isPrimitive(vnode.text)) { + nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) + } + } + + function isPatchable (vnode) { + while (vnode.componentInstance) { + vnode = vnode.componentInstance._vnode + } + return isDef(vnode.tag) + } + + function invokeCreateHooks (vnode, insertedVnodeQueue) { + for (let i = 0; i < cbs.create.length; ++i) { + cbs.create[i](emptyNode, vnode) + } + i = vnode.data.hook // Reuse variable + if (isDef(i)) { + if (isDef(i.create)) i.create(emptyNode, vnode) + if (isDef(i.insert)) insertedVnodeQueue.push(vnode) + } + } + + // set scope id attribute for scoped CSS. + // this is implemented as a special case to avoid the overhead + // of going through the normal attribute patching process. + function setScope (vnode) { + let i + if (isDef(i = vnode.fnScopeId)) { + nodeOps.setStyleScope(vnode.elm, i) + } else { + let ancestor = vnode + while (ancestor) { + if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) { + nodeOps.setStyleScope(vnode.elm, i) + } + ancestor = ancestor.parent + } + } + // for slot content they should also get the scopeId from the host instance. + if (isDef(i = activeInstance) && + i !== vnode.context && + i !== vnode.fnContext && + isDef(i = i.$options._scopeId) + ) { + nodeOps.setStyleScope(vnode.elm, i) + } + } + + function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) { + for (; startIdx <= endIdx; ++startIdx) { + createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx) + } + } + + function invokeDestroyHook (vnode) { + let i, j + const data = vnode.data + if (isDef(data)) { + if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode) + for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode) + } + if (isDef(i = vnode.children)) { + for (j = 0; j < vnode.children.length; ++j) { + invokeDestroyHook(vnode.children[j]) + } + } + } + + function removeVnodes (parentElm, vnodes, startIdx, endIdx) { + for (; startIdx <= endIdx; ++startIdx) { + const ch = vnodes[startIdx] + if (isDef(ch)) { + if (isDef(ch.tag)) { + removeAndInvokeRemoveHook(ch) + invokeDestroyHook(ch) + } else { // Text node + removeNode(ch.elm) + } + } + } + } + + function removeAndInvokeRemoveHook (vnode, rm) { + if (isDef(rm) || isDef(vnode.data)) { + let i + const listeners = cbs.remove.length + 1 + if (isDef(rm)) { + // we have a recursively passed down rm callback + // increase the listeners count + rm.listeners += listeners + } else { + // directly removing + rm = createRmCb(vnode.elm, listeners) + } + // recursively invoke hooks on child component root node + if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) { + removeAndInvokeRemoveHook(i, rm) + } + for (i = 0; i < cbs.remove.length; ++i) { + cbs.remove[i](vnode, rm) + } + if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) { + i(vnode, rm) + } else { + rm() + } + } else { + removeNode(vnode.elm) + } + } + + function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { + let oldStartIdx = 0 + let newStartIdx = 0 + let oldEndIdx = oldCh.length - 1 + let oldStartVnode = oldCh[0] + let oldEndVnode = oldCh[oldEndIdx] + let newEndIdx = newCh.length - 1 + let newStartVnode = newCh[0] + let newEndVnode = newCh[newEndIdx] + let oldKeyToIdx, idxInOld, vnodeToMove, refElm + + // removeOnly is a special flag used only by <transition-group> + // to ensure removed elements stay in correct relative positions + // during leaving transitions + const canMove = !removeOnly + + if (process.env.NODE_ENV !== 'production') { + checkDuplicateKeys(newCh) + } + + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (isUndef(oldStartVnode)) { + oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left + } else if (isUndef(oldEndVnode)) { + oldEndVnode = oldCh[--oldEndIdx] + } else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) + oldStartVnode = oldCh[++oldStartIdx] + newStartVnode = newCh[++newStartIdx] + } else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) + oldEndVnode = oldCh[--oldEndIdx] + newEndVnode = newCh[--newEndIdx] + } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) + canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) + oldStartVnode = oldCh[++oldStartIdx] + newEndVnode = newCh[--newEndIdx] + } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) + canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) + oldEndVnode = oldCh[--oldEndIdx] + newStartVnode = newCh[++newStartIdx] + } else { + if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) + idxInOld = isDef(newStartVnode.key) + ? oldKeyToIdx[newStartVnode.key] + : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) + if (isUndef(idxInOld)) { // New element + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) + } else { + vnodeToMove = oldCh[idxInOld] + if (sameVnode(vnodeToMove, newStartVnode)) { + patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) + oldCh[idxInOld] = undefined + canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) + } else { + // same key but different element. treat as new element + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) + } + } + newStartVnode = newCh[++newStartIdx] + } + } + if (oldStartIdx > oldEndIdx) { + refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm + addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) + } else if (newStartIdx > newEndIdx) { + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) + } + } + + function checkDuplicateKeys (children) { + const seenKeys = {} + for (let i = 0; i < children.length; i++) { + const vnode = children[i] + const key = vnode.key + if (isDef(key)) { + if (seenKeys[key]) { + warn( + `Duplicate keys detected: '${key}'. This may cause an update error.`, + vnode.context + ) + } else { + seenKeys[key] = true + } + } + } + } + + function findIdxInOld (node, oldCh, start, end) { + for (let i = start; i < end; i++) { + const c = oldCh[i] + if (isDef(c) && sameVnode(node, c)) return i + } + } + + function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { + if (oldVnode === vnode) { + return + } + + const elm = vnode.elm = oldVnode.elm + + if (isTrue(oldVnode.isAsyncPlaceholder)) { + if (isDef(vnode.asyncFactory.resolved)) { + hydrate(oldVnode.elm, vnode, insertedVnodeQueue) + } else { + vnode.isAsyncPlaceholder = true + } + return + } + + // reuse element for static trees. + // note we only do this if the vnode is cloned - + // if the new node is not cloned it means the render functions have been + // reset by the hot-reload-api and we need to do a proper re-render. + if (isTrue(vnode.isStatic) && + isTrue(oldVnode.isStatic) && + vnode.key === oldVnode.key && + (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) + ) { + vnode.componentInstance = oldVnode.componentInstance + return + } + + let i + const data = vnode.data + if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { + i(oldVnode, vnode) + } + + const oldCh = oldVnode.children + const ch = vnode.children + if (isDef(data) && isPatchable(vnode)) { + for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) + if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) + } else if (isDef(ch)) { + if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) + } else if (isDef(oldCh)) { + removeVnodes(elm, oldCh, 0, oldCh.length - 1) + } else if (isDef(oldVnode.text)) { + nodeOps.setTextContent(elm, '') + } + } else if (oldVnode.text !== vnode.text) { + nodeOps.setTextContent(elm, vnode.text) + } + if (isDef(data)) { + if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) + } + } + + function invokeInsertHook (vnode, queue, initial) { + // delay insert hooks for component root nodes, invoke them after the + // element is really inserted + if (isTrue(initial) && isDef(vnode.parent)) { + vnode.parent.data.pendingInsert = queue + } else { + for (let i = 0; i < queue.length; ++i) { + queue[i].data.hook.insert(queue[i]) + } + } + } + + let hydrationBailed = false + // list of modules that can skip create hook during hydration because they + // are already rendered on the client or has no need for initialization + // Note: style is excluded because it relies on initial clone for future + // deep updates (#7063). + const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key') + + // Note: this is a browser-only function so we can assume elms are DOM nodes. + function hydrate (elm, vnode, insertedVnodeQueue, inVPre) { + let i + const { tag, data, children } = vnode + inVPre = inVPre || (data && data.pre) + vnode.elm = elm + + if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) { + vnode.isAsyncPlaceholder = true + return true + } + // assert node match + if (process.env.NODE_ENV !== 'production') { + if (!assertNodeMatch(elm, vnode, inVPre)) { + return false + } + } + if (isDef(data)) { + if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */) + if (isDef(i = vnode.componentInstance)) { + // child component. it should have hydrated its own tree. + initComponent(vnode, insertedVnodeQueue) + return true + } + } + if (isDef(tag)) { + if (isDef(children)) { + // empty element, allow client to pick up and populate children + if (!elm.hasChildNodes()) { + createChildren(vnode, children, insertedVnodeQueue) + } else { + // v-html and domProps: innerHTML + if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) { + if (i !== elm.innerHTML) { + /* istanbul ignore if */ + if (process.env.NODE_ENV !== 'production' && + typeof console !== 'undefined' && + !hydrationBailed + ) { + hydrationBailed = true + console.warn('Parent: ', elm) + console.warn('server innerHTML: ', i) + console.warn('client innerHTML: ', elm.innerHTML) + } + return false + } + } else { + // iterate and compare children lists + let childrenMatch = true + let childNode = elm.firstChild + for (let i = 0; i < children.length; i++) { + if (!childNode || !hydrate(childNode, children[i], insertedVnodeQueue, inVPre)) { + childrenMatch = false + break + } + childNode = childNode.nextSibling + } + // if childNode is not null, it means the actual childNodes list is + // longer than the virtual children list. + if (!childrenMatch || childNode) { + /* istanbul ignore if */ + if (process.env.NODE_ENV !== 'production' && + typeof console !== 'undefined' && + !hydrationBailed + ) { + hydrationBailed = true + console.warn('Parent: ', elm) + console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children) + } + return false + } + } + } + } + if (isDef(data)) { + let fullInvoke = false + for (const key in data) { + if (!isRenderedModule(key)) { + fullInvoke = true + invokeCreateHooks(vnode, insertedVnodeQueue) + break + } + } + if (!fullInvoke && data['class']) { + // ensure collecting deps for deep class bindings for future updates + traverse(data['class']) + } + } + } else if (elm.data !== vnode.text) { + elm.data = vnode.text + } + return true + } + + function assertNodeMatch (node, vnode, inVPre) { + if (isDef(vnode.tag)) { + return vnode.tag.indexOf('vue-component') === 0 || ( + !isUnknownElement(vnode, inVPre) && + vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()) + ) + } else { + return node.nodeType === (vnode.isComment ? 8 : 3) + } + } + + return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { + if (isUndef(vnode)) { + if (isDef(oldVnode)) invokeDestroyHook(oldVnode) + return + } + + let isInitialPatch = false + const insertedVnodeQueue = [] + + if (isUndef(oldVnode)) { + // empty mount (likely as component), create new root element + isInitialPatch = true + createElm(vnode, insertedVnodeQueue, parentElm, refElm) + } else { + const isRealElement = isDef(oldVnode.nodeType) + if (!isRealElement && sameVnode(oldVnode, vnode)) { + // patch existing root node + patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) + } else { + if (isRealElement) { + // mounting to a real element + // check if this is server-rendered content and if we can perform + // a successful hydration. + if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { + oldVnode.removeAttribute(SSR_ATTR) + hydrating = true + } + if (isTrue(hydrating)) { + if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { + invokeInsertHook(vnode, insertedVnodeQueue, true) + return oldVnode + } else if (process.env.NODE_ENV !== 'production') { + warn( + 'The client-side rendered virtual DOM tree is not matching ' + + 'server-rendered content. This is likely caused by incorrect ' + + 'HTML markup, for example nesting block-level elements inside ' + + '<p>, or missing <tbody>. Bailing hydration and performing ' + + 'full client-side render.' + ) + } + } + // either not server-rendered, or hydration failed. + // create an empty node and replace it + oldVnode = emptyNodeAt(oldVnode) + } + + // replacing existing element + const oldElm = oldVnode.elm + const parentElm = nodeOps.parentNode(oldElm) + + // create new node + createElm( + vnode, + insertedVnodeQueue, + // extremely rare edge case: do not insert if old element is in a + // leaving transition. Only happens when combining transition + + // keep-alive + HOCs. (#4590) + oldElm._leaveCb ? null : parentElm, + nodeOps.nextSibling(oldElm) + ) + + // update parent placeholder node element, recursively + if (isDef(vnode.parent)) { + let ancestor = vnode.parent + const patchable = isPatchable(vnode) + while (ancestor) { + for (let i = 0; i < cbs.destroy.length; ++i) { + cbs.destroy[i](ancestor) + } + ancestor.elm = vnode.elm + if (patchable) { + for (let i = 0; i < cbs.create.length; ++i) { + cbs.create[i](emptyNode, ancestor) + } + // #6513 + // invoke insert hooks that may have been merged by create hooks. + // e.g. for directives that uses the "inserted" hook. + const insert = ancestor.data.hook.insert + if (insert.merged) { + // start at index 1 to avoid re-invoking component mounted hook + for (let i = 1; i < insert.fns.length; i++) { + insert.fns[i]() + } + } + } else { + registerRef(ancestor) + } + ancestor = ancestor.parent + } + } + + // destroy old node + if (isDef(parentElm)) { + removeVnodes(parentElm, [oldVnode], 0, 0) + } else if (isDef(oldVnode.tag)) { + invokeDestroyHook(oldVnode) + } + } + } + + invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) + return vnode.elm + } +} diff --git a/node_modules/vue/src/core/vdom/vnode.js b/node_modules/vue/src/core/vdom/vnode.js new file mode 100644 index 00000000..88e80cec --- /dev/null +++ b/node_modules/vue/src/core/vdom/vnode.js @@ -0,0 +1,108 @@ +/* @flow */ + +export default class VNode { + tag: string | void; + data: VNodeData | void; + children: ?Array<VNode>; + text: string | void; + elm: Node | void; + ns: string | void; + context: Component | void; // rendered in this component's scope + key: string | number | void; + componentOptions: VNodeComponentOptions | void; + componentInstance: Component | void; // component instance + parent: VNode | void; // component placeholder node + + // strictly internal + raw: boolean; // contains raw HTML? (server only) + isStatic: boolean; // hoisted static node + isRootInsert: boolean; // necessary for enter transition check + isComment: boolean; // empty comment placeholder? + isCloned: boolean; // is a cloned node? + isOnce: boolean; // is a v-once node? + asyncFactory: Function | void; // async component factory function + asyncMeta: Object | void; + isAsyncPlaceholder: boolean; + ssrContext: Object | void; + fnContext: Component | void; // real context vm for functional nodes + fnOptions: ?ComponentOptions; // for SSR caching + fnScopeId: ?string; // functional scope id support + + constructor ( + tag?: string, + data?: VNodeData, + children?: ?Array<VNode>, + text?: string, + elm?: Node, + context?: Component, + componentOptions?: VNodeComponentOptions, + asyncFactory?: Function + ) { + this.tag = tag + this.data = data + this.children = children + this.text = text + this.elm = elm + this.ns = undefined + this.context = context + this.fnContext = undefined + this.fnOptions = undefined + this.fnScopeId = undefined + this.key = data && data.key + this.componentOptions = componentOptions + this.componentInstance = undefined + this.parent = undefined + this.raw = false + this.isStatic = false + this.isRootInsert = true + this.isComment = false + this.isCloned = false + this.isOnce = false + this.asyncFactory = asyncFactory + this.asyncMeta = undefined + this.isAsyncPlaceholder = false + } + + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + get child (): Component | void { + return this.componentInstance + } +} + +export const createEmptyVNode = (text: string = '') => { + const node = new VNode() + node.text = text + node.isComment = true + return node +} + +export function createTextVNode (val: string | number) { + return new VNode(undefined, undefined, undefined, String(val)) +} + +// optimized shallow clone +// used for static nodes and slot nodes because they may be reused across +// multiple renders, cloning them avoids errors when DOM manipulations rely +// on their elm reference. +export function cloneVNode (vnode: VNode): VNode { + const cloned = new VNode( + vnode.tag, + vnode.data, + vnode.children, + vnode.text, + vnode.elm, + vnode.context, + vnode.componentOptions, + vnode.asyncFactory + ) + cloned.ns = vnode.ns + cloned.isStatic = vnode.isStatic + cloned.key = vnode.key + cloned.isComment = vnode.isComment + cloned.fnContext = vnode.fnContext + cloned.fnOptions = vnode.fnOptions + cloned.fnScopeId = vnode.fnScopeId + cloned.isCloned = true + return cloned +} |
