从源码的角度分析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 其实就是主动触发一次更新操作,达到强制更新的目的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350