返回

Vue 实例挂载 – 内幕揭秘

前端

迫于菜🐶 - Vue.js 源码(五)

缘起

前面四章中,我们跟随 Vue.js 技术揭秘 , 对 Vue.js 的源码部分又有了进一步的了解,今天一起来学习 Vue 实例挂载是怎么一回事。

Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义。因为 $mount 这个方法的对象有多种可能,比如组件、根实例、全局 API 等,这个对象的类型不一样,最后调用的函数也不一样。所以先从 $mount 这个方法开始。

$mount 方法

从其源码中可以知道,$mount 这个方法所调用的最终函数是 vm.$el && vm._isMounted ? vm._update(undefined, false) : vm.__patch__(vm._render(), hydrate) 这个函数。

而这个函数,就是我们前面提到的 Vue.js 中更新函数。Vue 的更新主要分为两种:初次渲染和后续的更新。初次渲染 ,顾名思义,就是 Vue.js 将组件首次渲染到页面上的过程,后续更新 ,则是指组件内部数据发生变化后,Vue.js 将组件重新渲染到页面上的过程。

初次渲染时,hydrate 这个参数的值为 false,后续更新时,hydrate 这个参数的值为 true,这就好比给 Vue.js 一个提示,告诉它这次的更新是初次渲染,还是后续更新。

初次渲染

vm.$el && vm._isMounted ? vm._update(undefined, false) : vm.__patch__(vm._render(), hydrate)

后续更新

vm.$el && vm._isMounted ? vm._update(undefined, false) : vm.__patch__(vm._render(), true)

初次渲染

初次渲染时,hydrate 这个参数的值为 false,代表这是一个初次渲染的过程,所以会调用 Vue.prototype.__patch__ 这个方法,这个方法的最终调用函数是 Vue.prototype.$forceUpdate

// 源码位置:packages/runtime-dom/src/component.js
Vue.prototype.__patch__ = function __patch__(
  patch,
  options,
  hydrating = false,
  removeOnly = false
) {
  if (options && options.before) {
    options.before.call(this)
  }
  const prevVNode = this._vnode
  const prevVNodeNormalized = this.$vnode && this.$vnode.normalized
  const ownerReactInstance = this.reactInstance
  if (this._isMounted) {
    // 更新的 patch
    while (
      patch &&
      patch.componentInstance &&
      !patch.componentInstance._inactive
    ) {
      // protect inactive instance
      patch = patch.componentInstance._vnode
    }
    if (
      // 更新
      this._vnode !== prevVNode ||
      prevVNodeNormalized !== vnodeNormalized
    ) {
      if (this._vnode) {
        this._vnode = normalizeVNode(this._vnode, prevVNodeNormalized)
      }

      const prevChildren = this.$scopedSlots
        ? this.$scopedSlots(prevVNode)
        : prevVNode.children
      const parentVNode = this.$vnode && this.$vnode.parent
      const defaultSlot = parentVNode && parentVNode.children[0]

      const $slots = this.$slots
      const $scopedSlots = this.$scopedSlots
      const passedSlots = {}

      let name, slot, container

      if ($slots) {
        for (name in $slots) {
          if ($slots[name]) {
            slot = $slots[name] && $slots[name].byVal
              ? $slots[name]
              : cloneVNode($slots[name])
            if (!slot.length) {
              if (!defaultSlot) {
                slot = createEmptyVNode()
              } else if (slot === getKeepAliveChild(defaultSlot)) {
                passedSlots[name] = slot
              }
            }
            passedSlots[name] = slot
          }
        }
      }

      if ($scopedSlots) {
        for (name in $scopedSlots) {
          if ($scopedSlots[name]) {
            slot = $scopedSlots[name] && $scopedSlots[name].byVal
              ? $scopedSlots[name]
              : cloneVNode($scopedSlots[name])
            if (!slot.length) {
              if (!defaultSlot) {
                slot = createEmptyVNode()
              } else if (slot === getKeepAliveChild(defaultSlot)) {
                passedSlots[name] = slot
              }
            }
            container = createVNode(
              'span',
              { slot: name },
              slot,
              false,
              hydrating
            )
            passedSlots[name] = container.children
          }
        }
      }

      if (defaultSlot) {
        // 这里其实会把当前组件的插槽与父组件的插槽做个融合,然后最终合并成一个组件的插槽传递给组件的渲染函数

        // 这里的意思是,父组件没有为当前组件提供插槽,那么就用当前组件自己的插槽
        // 如果有提供插槽,那么就把父组件的插槽内容放到当前组件的插槽中
        passedSlots.default = defaultSlot.children
      }

      if (options && options.renderUpdated) {
        // beforeUpdate 钩子
        options.renderUpdated.call(this, prevVNode, this._vnode)
      }

      this.$scopedSlots = $scopedSlots
      this.$slots = $slots

      if (parentVNode) {
        const slotContent = parentVNode.children[0]
        if (this.$scopedSlots && slotContent && slotContent.hasScopedRef) {
          // TODO: 有点没看懂
          this.$scopedSlots = createScopedSlots(parentVNode, this.$scopedSlots)
        }
      }
      // 更新过程
      this._update(this._vnode, prevVNode, passedSlots, hydrating, removeOnly)
      if (this._vnode) {
        this._vnode = prevVNode
      }
      if (this._vnode && this._vnode.parent && this._vnode.parent._children) {
        // 更新后的 vnode 如果有 parent,并且 parent 中有 _children 属性(意味着 parent 是一个组件)
        // 那么就需要更新 parent 中的 _children 属性,因为 _children 属性中存储的是组件的子组件
        this._updateParentListeners()
      }
    } else {
      if (this._vnode && this._vnode.dynamicChildren) {
        // 检查动态子组件,因为这些子组件在被重新渲染时可能已经改变了
        const dynamicChildren = this._vnode.dynamicChildren
        const oldDynamicChildren = this._prevVNode.dynamicChildren
        const count = dynamicChildren.length
        for (let i = 0; i < count; i++) {
          const vnode = dynamicChildren[i]
          const oldVnode = oldDynamicChildren[i]
          if (vnode && oldVnode && vnode.ref === oldVnode.ref) {
            continue
          }
          const parent = vnode.parent
          if (parent) {
            // 这里重新收集动态组件子组件的依赖
            parent.refs
              ? parent._refs.remove(oldVnode)
              : parent._children.remove(oldVnode)
            this._refs
              ? this._refs.remove(oldVnode)
              : this._children.remove(oldVnode)
            adoptVNode(vnode, parent)
            this._children.push(vnode)
            if (vnode.dynamic) {
              this._dynamicComponents[vnode.ref] = vnode
            }
          }
        }
      }
    }
  } else {
    // 初始化渲染
    // 这里会执行一些 生命周期钩子 相关的逻辑
    if (this.$vnode) {
      this.$vnode = patch
    }
    this._vnode = patch
    this._vnodeNormalized = normalizeVNode(patch)
    if (this.$scopedSlots) {
      this._scopeId = 'data-v-' +