feat(sfc): scopeId runtime support

This commit is contained in:
Evan You 2019-12-16 13:33:10 -05:00
parent 04e11187b9
commit 69c9dbc825
7 changed files with 71 additions and 6 deletions

View File

@ -5,7 +5,7 @@ module.exports = {
__TEST__: true, __TEST__: true,
__VERSION__: require('./package.json').version, __VERSION__: require('./package.json').version,
__BROWSER__: false, __BROWSER__: false,
__BUNDLER__: false, __BUNDLER__: true,
__RUNTIME_COMPILE__: true, __RUNTIME_COMPILE__: true,
__FEATURE_OPTIONS__: true, __FEATURE_OPTIONS__: true,
__FEATURE_SUSPENSE__: true __FEATURE_SUSPENSE__: true

View File

@ -0,0 +1,34 @@
// SFC scoped style ID management.
// These are only used in esm-bundler builds, but since exports cannot be
// conditional, we can only drop inner implementations in non-bundler builds.
export let currentScopeId: string | null = null
const scopeIdStack: string[] = []
export function pushScopeId(id: string) {
if (__BUNDLER__) {
scopeIdStack.push((currentScopeId = id))
}
}
export function popScopeId() {
if (__BUNDLER__) {
scopeIdStack.pop()
currentScopeId = scopeIdStack[scopeIdStack.length - 1] || null
}
}
export function withScopeId(id: string): <T extends Function>(fn: T) => T {
if (__BUNDLER__) {
return ((fn: Function) => {
return function(this: any) {
pushScopeId(id)
const res = fn.apply(this, arguments)
popScopeId()
return res
}
}) as any
} else {
return undefined as any
}
}

View File

@ -82,6 +82,7 @@ export { toString } from './helpers/toString'
export { toHandlers } from './helpers/toHandlers' export { toHandlers } from './helpers/toHandlers'
export { renderSlot } from './helpers/renderSlot' export { renderSlot } from './helpers/renderSlot'
export { createSlots } from './helpers/createSlots' export { createSlots } from './helpers/createSlots'
export { pushScopeId, popScopeId, withScopeId } from './helpers/scopeId'
export { setBlockTracking, createTextVNode, createCommentVNode } from './vnode' export { setBlockTracking, createTextVNode, createCommentVNode } from './vnode'
// Since @vue/shared is inlined into final builds, // Since @vue/shared is inlined into final builds,
// when re-exporting from @vue/shared we need to avoid relying on their original // when re-exporting from @vue/shared we need to avoid relying on their original

View File

@ -63,7 +63,7 @@ export interface RendererOptions<HostNode = any, HostElement = any> {
key: string, key: string,
value: any, value: any,
oldValue: any, oldValue: any,
isSVG: boolean, isSVG?: boolean,
prevChildren?: VNode<HostNode, HostElement>[], prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null, parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary<HostNode, HostElement> | null, parentSuspense?: SuspenseBoundary<HostNode, HostElement> | null,
@ -83,6 +83,7 @@ export interface RendererOptions<HostNode = any, HostElement = any> {
parentNode(node: HostNode): HostElement | null parentNode(node: HostNode): HostElement | null
nextSibling(node: HostNode): HostNode | null nextSibling(node: HostNode): HostNode | null
querySelector(selector: string): HostElement | null querySelector(selector: string): HostElement | null
setScopeId(el: HostNode, id: string): void
} }
export type RootRenderFunction<HostNode, HostElement> = ( export type RootRenderFunction<HostNode, HostElement> = (
@ -189,7 +190,8 @@ export function createRenderer<
setElementText: hostSetElementText, setElementText: hostSetElementText,
parentNode: hostParentNode, parentNode: hostParentNode,
nextSibling: hostNextSibling, nextSibling: hostNextSibling,
querySelector: hostQuerySelector querySelector: hostQuerySelector,
setScopeId: hostSetScopeId
} = options } = options
const internals: RendererInternals<HostNode, HostElement> = { const internals: RendererInternals<HostNode, HostElement> = {
@ -368,7 +370,9 @@ export function createRenderer<
const tag = vnode.type as string const tag = vnode.type as string
isSVG = isSVG || tag === 'svg' isSVG = isSVG || tag === 'svg'
const el = (vnode.el = hostCreateElement(tag, isSVG)) const el = (vnode.el = hostCreateElement(tag, isSVG))
const { props, shapeFlag, transition } = vnode const { props, shapeFlag, transition, scopeId } = vnode
// props
if (props != null) { if (props != null) {
for (const key in props) { for (const key in props) {
if (isReservedProp(key)) continue if (isReservedProp(key)) continue
@ -378,6 +382,19 @@ export function createRenderer<
invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode) invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
} }
} }
// scopeId
if (__BUNDLER__ && scopeId !== null) {
hostSetScopeId(el, scopeId)
const treeOwnerId = parentComponent && parentComponent.type.__scopeId
// vnode's own scopeId and the current patched component's scopeId is
// different - this is a slot content node.
if (treeOwnerId != null && treeOwnerId !== scopeId) {
hostSetScopeId(el, treeOwnerId + '::slot')
}
}
// children
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string) hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {

View File

@ -21,6 +21,7 @@ import { DirectiveBinding } from './directives'
import { SuspenseImpl } from './components/Suspense' import { SuspenseImpl } from './components/Suspense'
import { TransitionHooks } from './components/BaseTransition' import { TransitionHooks } from './components/BaseTransition'
import { warn } from './warning' import { warn } from './warning'
import { currentScopeId } from './helpers/scopeId'
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as { export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
__isFragment: true __isFragment: true
@ -90,6 +91,7 @@ export interface VNode<HostNode = any, HostElement = any> {
props: VNodeProps | null props: VNodeProps | null
key: string | number | null key: string | number | null
ref: string | Ref | ((ref: object | null) => void) | null ref: string | Ref | ((ref: object | null) => void) | null
scopeId: string | null // SFC only
children: NormalizedChildren<HostNode, HostElement> children: NormalizedChildren<HostNode, HostElement>
component: ComponentInternalInstance | null component: ComponentInternalInstance | null
suspense: SuspenseBoundary<HostNode, HostElement> | null suspense: SuspenseBoundary<HostNode, HostElement> | null
@ -246,6 +248,7 @@ export function createVNode(
props, props,
key: (props !== null && props.key) || null, key: (props !== null && props.key) || null,
ref: (props !== null && props.ref) || null, ref: (props !== null && props.ref) || null,
scopeId: currentScopeId,
children: null, children: null,
component: null, component: null,
suspense: null, suspense: null,
@ -296,6 +299,7 @@ export function cloneVNode<T, U>(
: vnode.props, : vnode.props,
key: vnode.key, key: vnode.key,
ref: vnode.ref, ref: vnode.ref,
scopeId: vnode.scopeId,
children: vnode.children, children: vnode.children,
target: vnode.target, target: vnode.target,
shapeFlag: vnode.shapeFlag, shapeFlag: vnode.shapeFlag,

View File

@ -38,5 +38,9 @@ export const nodeOps = {
nextSibling: (node: Node): Node | null => node.nextSibling, nextSibling: (node: Node): Node | null => node.nextSibling,
querySelector: (selector: string): Element | null => querySelector: (selector: string): Element | null =>
doc.querySelector(selector) doc.querySelector(selector),
setScopeId(el: Element, id: string) {
el.setAttribute(id, '')
}
} }

View File

@ -228,6 +228,10 @@ function querySelector(): any {
throw new Error('querySelector not supported in test renderer.') throw new Error('querySelector not supported in test renderer.')
} }
function setScopeId(el: TestElement, id: string) {
el.props[id] = ''
}
export const nodeOps = { export const nodeOps = {
insert, insert,
remove, remove,
@ -238,5 +242,6 @@ export const nodeOps = {
setElementText, setElementText,
parentNode, parentNode,
nextSibling, nextSibling,
querySelector querySelector,
setScopeId
} }