. 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([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
src/core/vdom/patch.js
patchVnode()。
copy function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
if (isDef (vnode.elm ) && isDef (ownerArray)) {
vnode = ownerArray[index] = cloneVNode (vnode)
}
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
}
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 (process.env .NODE_ENV !== 'production' ) {
checkDuplicateKeys (ch)
}
if (isDef (oldVnode.text )) nodeOps.setTextContent (elm, '' )
addVnodes (elm, null , ch, 0 , ch.length - 1 , insertedVnodeQueue)
} else if (isDef (oldCh)) {
removeVnodes (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)
}
}
javascript
src/core/vdom/patch.js
updateChildren()。diff 算法核心逻辑。
copy 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
const canMove = !removeOnly
if (process.env .NODE_ENV !== 'production' ) {
checkDuplicateKeys (newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef (oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef (oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode (oldStartVnode, newStartVnode)) {
patchVnode (oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode (oldEndVnode, newEndVnode)) {
patchVnode (oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode (oldStartVnode, newEndVnode)) {
patchVnode (oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore (parentElm, oldStartVnode.elm , nodeOps.nextSibling (oldEndVnode.elm ))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode (oldEndVnode, newStartVnode)) {
patchVnode (oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
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)) {
createElm (newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm , false , newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode (vnodeToMove, newStartVnode)) {
patchVnode (vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore (parentElm, vnodeToMove.elm , oldStartVnode.elm )
} else {
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 (oldCh, oldStartIdx, oldEndIdx)
}
}
javascript
Vue2 diff 核心算法各分支逻辑可以参考下图。
头头比较
尾尾比较
头尾比较
尾头比较
乱序比较
3. vue 已经存在数据劫持,为什么还要使用虚拟 DOM 和 diff 算法
Vue 通过数据劫持确实可以在数据发生变化时,通知视图渲染。但是如果给每个属性都添加 watcher,会产生大量的 watcher 从而降低性能。而且粒度过细也会导致更新不精确的问题,所以 Vue 采用了组件级的 watcher 配置 diff 来检测差异。
使用虚拟 DOM 配合 diff 算法可以对比差异进行更新 DOM,可以减少对真实 DOM 的操作。
4. Vue 中 key 的作用和原理
Vue 在 patch 过程中通过 key 可以判断两个虚拟节点是否是相同节点(可以复用老节点)。
src/code/vdom/patch.js
copy 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 )
)
)
)
}
javascript
没有 key 会导致更新的时候出现问题(不设置 key 时,key 默认是 undefined)。
尽量不要采用索引作为 key。
比如做一个 todoList,使用 unshift 添加任务到数组中,如果默认选中第一项,这是添加 N个 元素默认选中的都是第一项。
如果使用索引作为 key,会导致后续节点的值都需要更新。如果不使用索引,新节点只会执行一次插入操作。
5. 谈谈你对 vue 组件化的理解
组件的特点:高内聚、低耦合、单向数据流;
降低更新范围,只重新渲染变化的组件(一个组件对应一个 watcher);
组件化开发能大幅提高应用开发效率、测试性、复用性等;
常用组件化技术:属性、自定义事件、插槽等。
6. Vue 组件渲染流程
主要分为三部分。创建组件虚拟节点 -> 创建组件的真实节点 -> 插入到页面中。
7. Vue 组件更新流程
属性更新时会触发 patchVnode 方法 -> 组件虚拟节点会调用 prepatch 钩子 -> 更新属性 -> 组件更新。
组件更新有两种途径。内部数据更新时,组件会更新。传入的响应式属性更新时,组件也会更新。
这里主要分析父节点数据改变,触发 patch 过程从而导致子组件更新的流程。
8. Vue 中异步组件原理
创建方式
vue 有 3 种创建异步组件的方式。
普通异步组件
copy Vue .component ('HelloWorld' , function (resolve, reject ) {
require (['./components/HelloWorld' ], resolve);
})
javascript
Promise 异步组件
copy Vue .component (
'HelloWorld' ,
() => import ('./components/HelloWorld.vue' )
)
javascript
高级异步组件
copy const AsyncComp = ( ) => ({
component : import ('./components/HelloWorld.vue' ),
loading : LoadingComp ,
error : ErrorComp ,
delay : 200 ,
timeout : 3000
});
Vue .component ('HelloWorld' , AsyncComp );
javascript
执行流程分析
这里以普通异步组件的创建为例。
默认渲染异步占位符节点 -> 组件加载完毕后调用 foreceUpdate 强制更新。
9. 函数组件的优势及原理
特性
函数式组件无状态、无生命周期、无 this,所以性能也高。
正常组件是一个类继承了 Vue,函数式组件就是普通的函数,没有 new 的过程,也没有初始化过程。
执行流程分析
copy Vue .component ('func' , {
functional : true ,
render (h) {
return h ('div' , 'yueluo' );
}
});
javascript
src/core/vdom/create-component.js
copy 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
if (isObject (Ctor )) {
Ctor = baseCtor.extend (Ctor )
}
if (isTrue (Ctor .options .functional )) {
return createFunctionalComponent (Ctor , propsData, data, context, children)
}
installComponentHooks (data)
return vnode
}
javascript
10. Vue 组件传值的方式及区别
传值方式
props
和 $emit
父组件向子组件传递数据是通过 prop 传递的,子组件向父组件传递数据是通过 $emit
触发事件来做到的;
$parent
,$children
获取当前组件的父组件和当前组件的子组件;
$attrs
和 $listeners
。Vue 2.4 开始提供 $attrs
和 $listeners
来解决这个问题;
父组件中通过 provide
来提供变量,然后在子组件通过 inject
来注入变量;
$refs
获取实例;
eventBus
平级组件数据传递。这种情况下可以使用中央事件总线的方式;
vuex
状态管理;
。。。
原理分析
为了方便理解,先看下图。下图标注了常见的几种传值方式的源码所在位置。
props 实现原理
copy <child-component a="1" b="2" c="3" @event01 @event02 @click.native ></child-component>
javascript
src/core/vdom/create-component
copy export function createComponent (
Ctor : Class <Component > | Function | Object | void ,
data : ?VNodeData ,
context : Component ,
children : ?Array <VNode >,
tag?: string
): VNode | Array <VNode > | void {
const propsData = extractPropsFromVNodeData (data, Ctor , tag)
const listeners = data.on
data.on = data.nativeOn
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
)
return vnode
}
javascript
src/core/instance/init.js
copy export function initMixin (Vue : Class <Component >) {
Vue .prototype ._init = function (options?: Object ) {
const vm : Component = this
if (options && options._isComponent ) {
initInternalComponent (vm, options)
} else {
vm.$options = mergeOptions (
resolveConstructorOptions (vm.constructor ),
options || {},
vm
)
}
vm._self = vm
initLifecycle (vm)
initEvents (vm)
initRender (vm)
callHook (vm, 'beforeCreate' )
initInjections (vm)
initState (vm)
initProvide (vm)
callHook (vm, 'created' )
if (vm.$options .el ) {
vm.$mount(vm.$options .el )
}
}
}
javascript
copy export function initInternalComponent (vm : Component , options : InternalComponentOptions ) {
const opts = vm.$options = Object .create (vm.constructor .options )
const parentVnode = options._parentVnode
opts.parent = options.parent
opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
if (options.render ) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
javascript
src/core/instance/state.js
copy export function initState (vm : Component ) {
vm._watchers = []
const opts = vm.$options
if (opts.props ) initProps (vm, opts.props )
if (opts.methods ) initMethods (vm, opts.methods )
if (opts.data ) {
initData (vm)
} else {
observe (vm._data = {}, true )
}
if (opts.computed ) initComputed (vm, opts.computed )
if (opts.watch && opts.watch !== nativeWatch) {
initWatch (vm, opts.watch )
}
}
javascript
copy function initProps (vm : Component , propsOptions : Object ) {
const propsData = vm.$options .propsData || {}
const props = vm._props = {}
const keys = vm.$options ._propKeys = []
const isRoot = !vm.$parent
if (!isRoot) {
toggleObserving (false )
}
for (const key in propsOptions) {
keys.push (key)
const value = validateProp (key, propsOptions, propsData, vm)
if (process.env .NODE_ENV !== 'production' ) {
const hyphenatedKey = hyphenate (key)
if (isReservedAttribute (hyphenatedKey) ||
config.isReservedAttr (hyphenatedKey)) {
warn (
`"${hyphenatedKey} " is a reserved attribute and cannot be used as component prop.` ,
vm
)
}
defineReactive (props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn (
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key} "` ,
vm
)
}
})
} else {
defineReactive (props, key, value)
}
if (!(key in vm)) {
proxy (vm, `_props` , key)
}
}
toggleObserving (true )
}
javascript
$on
、$event
基本流程和 props 一致。内部使用订阅模式进行实现。
src/core/instance/init.js
copy export function initInternalComponent (vm : Component , options : InternalComponentOptions ) {
opts._parentListeners = vnodeComponentOptions.listeners
}
javascript
src/core/instance/event.js
copy export function initEvents (vm : Component ) {
vm._events = Object .create (null )
vm._hasHookEvent = false
const listeners = vm.$options ._parentListeners
if (listeners) {
updateComponentListeners (vm, listeners)
}
}
javascript
copy function add (event, fn) {
target.$on(event, fn)
}
function remove (event, fn) {
target.$off(event, fn)
}
function createOnceHandler (event, fn) {
const _target = target
return function onceHandler () {
const res = fn.apply (null , arguments )
if (res !== null ) {
_target.$off(event, onceHandler)
}
}
}
export function updateComponentListeners (
vm : Component ,
listeners : Object ,
oldListeners : ?Object
) {
target = vm
updateListeners (listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
javascript
copy export function updateListeners (
on : Object ,
oldOn : Object ,
add : Function ,
remove : Function ,
createOnceHandler : Function ,
vm : Component
) {
let name, def, cur, old, event
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent (name)
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, vm)
}
if (isTrue (event.once )) {
cur = on[name] = createOnceHandler (event.name , cur, event.capture )
}
add (event.name , cur, 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 )
}
}
}
javascript
$parent
、$children
src/core/vdom/create-component.js
copy init (vnode : VNodeWithData , hydrating : boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance ._isDestroyed &&
vnode.data .keepAlive
) {
const mountedNode : any = vnode
componentVNodeHooks.prepatch (mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode (
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined , hydrating)
}
}
javascript
组件初始化时会调用此方法。
copy export function createComponentInstanceForVnode (
vnode : any,
parent : any
): Component {
const options : InternalComponentOptions = {
_isComponent : true ,
_parentVnode : vnode,
parent
}
const inlineTemplate = vnode.data .inlineTemplate
if (isDef (inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions .Ctor (options)
}
javascript
src/core/instance/lifecycle.js
构建父子关系。vm.parent === vm.$parent。
copy export function initLifecycle (vm : Component ) {
const options = vm.$options
let parent = options.parent
if (parent && !options.abstract ) {
while (parent.$options .abstract && parent.$parent ) {
parent = parent.$parent
}
parent.$children .push (vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
javascript
$attrs
、$listeners
copy <child-component a="1" b="2" c="3" ></child-component> => $vnode.data .attrs = { a : 1 , b : 2 }
javascript
src/core/instance/render.js
copy export function initRender (vm : Component ) {
vm._vnode = null
vm._staticTrees = null
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots (options._renderChildren , renderContext)
vm.$scopedSlots = emptyObject
vm._c = (a, b, c, d ) => createElement (vm, a, b, c, d, false )
vm.$createElement = (a, b, c, d ) => createElement (vm, a, b, c, d, true )
const parentData = parentVnode && parentVnode.data
if (process.env .NODE_ENV !== 'production' ) {
defineReactive (vm, '$attrs' , parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn (`$attrs is readonly.` , vm)
}, true )
defineReactive (vm, '$listeners' , options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn (`$listeners is readonly.` , vm)
}, true )
} else {
defineReactive (vm, '$attrs' , parentData && parentData.attrs || emptyObject, null , true )
defineReactive (vm, '$listeners' , options._parentListeners || emptyObject, null , true )
}
}
javascript
provide、inject
src/core/instance/inject.js
copy export function initProvide (vm : Component ) {
const provide = vm.$options .provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call (vm)
: provide
}
}
javascript
copy export function initInjections (vm : Component ) {
const result = resolveInject (vm.$options .inject , vm)
if (result) {
toggleObserving (false )
Object .keys (result).forEach (key => {
if (process.env .NODE_ENV !== 'production' ) {
defineReactive (vm, key, result[key], () => {
warn (
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key} "` ,
vm
)
})
} else {
defineReactive (vm, key, result[key])
}
})
toggleObserving (true )
}
}
export function resolveInject (inject : any, vm : Component ): ?Object {
if (inject) {
const result = Object .create (null )
const keys = hasSymbol
? Reflect .ownKeys (inject)
: Object .keys (inject)
for (let i = 0 ; i < keys.length ; i++) {
const key = keys[i]
if (key === '__ob__' ) continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn (source._provided , provideKey)) {
result[key] = source._provided [provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call (vm)
: provideDefault
} else if (process.env .NODE_ENV !== 'production' ) {
warn (`Injection "${key} " not found` , vm)
}
}
}
return result
}
}
javascript
$ref
src/core/vdom/modules/ref.js
DOM 创建更新删除时会调用一些方法,继而执行该钩子。
copy 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 )
}
}
javascript
copy 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 ) {
refs[key].push (ref)
}
} else {
refs[key] = ref
}
}
}
javascript
11. $attrs
是为了解决什么问题出现的
$attrs 主要的作用就是实现批量传递数据。
适用于不需要显式传递数据的场景。比如多个组件传递数据,中间的组件不需要数据,只起到传递的作用。
copy v-bind="$attrs" 、v-on="$listeners"
javascript
provide/inject 更适合应用在插件中,主要是实现跨级数据传递。