从源码的角度分析Vue面试题[二]
由于通过面试题分析的话并不会从头去看源码,而且我也不会写的特别细致,所以本文章适合有基础的同学看,否则某些地方可能看不太明白。我这些分析只是辅助,还是建议大家有时间的话能完整的看一看源码,毕竟多看优秀的项目才能提升自己的代码能力。
如何在子组件中访问父组件的实例?
答案:通过$parent 就可以访问到父组件的实例了,除了$parent,我们还可以通过$children 访问子组件的实例。相信这个答案各位小伙伴都知道,但是这个$parent 和$children 是通过什么方式实现的呢,或者说,Vue 内部是如何建立这种父子组件关系的?
源码解析:
如果我们写一个组件 A,初始化组件的时候都会执行 A.$mount方法,将组件进行挂载,$mount 方法实际上会执行 mountComponent
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
//...省略其他逻辑
// 创建一个组件的渲染Watcher
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
//...
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
//...省略其他逻辑
}
这个函数最关键的地方是创建了一个渲染 Watcher,其内部是执行了 vm._update(vm._render(), hydrating),_render 主要就是返回一个 vnode,就是虚拟节点,然后_update 通过这个 vnode 去 patch 组件,那么我们看一下_update 做了什么事情
export let activeInstance: any = null
//...
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
//...省略其他逻辑
vm.$el = vm.__patch__(
vm.$el,
vnode,
hydrating,
false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
//...省略其他逻辑
activeInstance = prevActiveInstance
}
这个函数外部定义了一个 activeInstance 变量,在执行_update 的时候通过const prevActiveInstance = activeInstance
将 activeInstance 保存了起来,又通过activeInstance = vm
把当前组件实例赋值给了 activeInstance,这个 activeInstance 是在函数外部定义的一个变量,其他文件也可以 import 这个变量,activeInstance 的作用稍后会讲到。我们先看下 patch,patch 过程比较复杂,我只讲和本题相关的一些逻辑,假设我们的组件 A 有一个子组件 A1,在 patch 的时候,将会执行到 A1 组件的 hook.init 函数
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)
}
},
这里执行了 createComponentInstanceForVnode 这个函数,注意传入的第二个参数是 activeInstance,这个 activeInstance 是从 mountComponent 那引入的,在 mountComponent 的时候已经把这个变量设置成了 A,注意由于这个 init 函数初始化的是 A1 组件,那么对于 A1 组件来说,activeInstance 就是它的父组件。看下 createComponentInstanceForVnode 函数
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)
}
刚才那个 activeInstance 就是这个 parent 参数,然后又放在了 options 中,执行了 new vnode.componentOptions.Ctor(options)函数,这个 vnode.componentOptions.Ctor 是组件的构造函数,是在创建 vnode 时通过 Vue.extend 创建的,关于 Vue.extend 做的事情,可查看我上一篇文章,这个 Ctor 执行了这一段函数
function VueComponent(options) {
this._init(options)
}
这里又回到了_init
Vue.prototype._init = function(options?: Object) {
//...省略其他逻辑
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
//...省略其他逻辑
initLifecycle(vm)
//...省略其他逻辑
}
组件会执行到 initInternalComponent 函数
export function initInternalComponent(
vm: Component,
options: InternalComponentOptions
) {
const opts = (vm.$options = Object.create(vm.constructor.options))
// doing this because it's faster than dynamic enumeration.
const parentVnode = options._parentVnode
opts.parent = options.parent // 父Vnode,activeInstance
opts._parentVnode = parentVnode // 占位符Vnode
opts._parentElm = options._parentElm
opts._refElm = options._refElm
const vnodeComponentOptions = parentVnode.componentOptions // componentOptions是createComponent时候传入new Vnode()的
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
}
}
这里面有一段逻辑是 opts.parent = options.parent,就是把 options.parent 这个属性赋值给了 vm.$options,这里options.parent就是createComponentInstanceForVnode中的parent,也就是activeInstance,那么现在vm.$options.parent 就是 activeInstance。
执行完这些后再回到_init 中,我们看到下面还有一段函数 initLifecycle(vm)
export function initLifecycle(vm: Component) {
const options = vm.$options
// locate first non-abstract parent
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
}
这里拿到了 vm.$options.parent,通过刚才的 initInternalComponent 我们可以知道,这个 parent 其实就是 activeInstance,然后又通过 parent.$children.push(vm)往parent的$children 中添加自己,又通过 vm.$parent = parent将parent赋值给$parent,所以这个时候,A1 的$parent就是A,A的$children 里面也会有 A1,执行完这些后,再回到最开始的 init 方法
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)
}
},
这个 init 方法又执行了$mount 方法,接着又是 mountComponent > _update
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
//...省略其他逻辑
vm.$el = vm.__patch__(
vm.$el,
vnode,
hydrating,
false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
//...省略其他逻辑
activeInstance = prevActiveInstance
}
注意这里仍然是在 A 的子组件 A1 中,_update 会执行 A1 的 patch,如果 A1 有子组件的话,就又会重复上面的操作,整个过程其实是一个递归的过程,每次递归都会对 activeInstance 赋值,最终在 patch 完成之后,会通过activeInstance = prevActiveInstance
将 activeInstance 恢复,这样就可以确保 patch 过程中的 activeInstance 是父组件实例,patch 完成之后也不会影响其他逻辑,这样 Vue 就建立了层层的父子级关系。
vue 怎么实现强制刷新组件?
答案:Vue 提供了一个 API:$forceUpdate,可以强制渲染组件。
源码解析:Vue 内部会监听组件 data,并自动收集依赖,在属性发生变化时自动通知更新,触发组件的重新渲染。一般情况下,我们是不需要关心这个过程的,但有时我们想强制刷新视图,就要使用$forceUpdate 这个函数了,那这个函数做了什么事情呢,我们先来看下组件挂载过程的 mountComponent 函数
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
//省略其他逻辑...
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
//省略其他逻辑...
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
//省略其他逻辑...
}
我们只看和本题相关的逻辑,这里创建了一个 updateComponent 函数,然后又将 updateComponent 参数实例化了一个 Watcher,这个 updateComponent 函数的细节我就不展开讲了,总之我们在更新组件属性的后,Vue 内部也会执行到这个函数,使视图重新渲染。还有这个 Watcher 类比较多,我就不贴出来占地方了,与本题相关的关键的一个地方是下面这个
// class Watcher
if (isRenderWatcher) {
vm._watcher = this
}
将组件实例的_watcher 属性指向了 watcher 本身。这样我们就可以通过 vm._watcher 访问到 watcher 了。除了上面这些操作之外,Vue 原型上还定义了$forceUpdate 方法
Vue.prototype.$forceUpdate = function() {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
可以看到这个 forceUpdate 其实就是执行了 watcher 的 update,watcher.update()最终会执行到 updateComponent 函数,从而触发一次更新。
可以看到,$forceUpdate 其实就是主动触发一次更新操作,达到强制更新的目的。
发表评论 (审核通过后显示评论):