Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue的patch过程 #17

Open
daodaolee opened this issue Mar 3, 2021 · 0 comments
Open

Vue的patch过程 #17

daodaolee opened this issue Mar 3, 2021 · 0 comments

Comments

@daodaolee
Copy link
Owner

前面提到 createElement 创建了组件VNode,接着调用 vm._update ,执行 vm._patch 方法把VNode转换为真实节点,这是针对于一个普通的 VNode节点,下面看下在组件中的VNode的区别。

patch 会调用 createEml 创建元素节点,它在 src/core/vdom/patch.js 中:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  // ...
}

和普通VNode最不一样的地方就是: createComponent,因为这里的vnode是一个组件vnode,所以它在创建的时候有些不太一样:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // after calling the init hook, if the vnode is a child component
    // it should've created a child instance and mounted it. the child
    // component also has set the placeholder vnode's elm.
    // in that case we can just return the element and be done.
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

首先判断 vnode.data 有没有,keepAlive先忽略,接着判断有没有 data.hook 以及这个 hook 中有没有 init 方法,如果有就调用这个方法。

回忆一下,在创建组件 createComponent 方法里面,有一个 installComponentHooks 方法,这个方法会把上面定义的4个hook都初始化一遍(init,prepatch,insert,destroy),然后挂载到 data.hook 上,所以在上面的判断中,init 是有的,然后就执行到了 hook 上的 init 方法中:

init (vnode: VNodeWithData, hydrating: boolean): ?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
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
}

keepAlive跳过,后面有个 createComponentInstanceForVnode,这个方法返回了 vnode.componentInstance,也就是返回了vm的实例,然后调用了 $mount 方法挂载子组件。看下 createComponentInstanceForVnode, 它传入了两个参数,第一个是组件vnode,第二个是 activeInstance,后面会提到。来看下 createComponentInstanceForVnode 的定义:

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // 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)
}

可以看到传入了两个参数,第一个是组件vnode,第二个参数其实是当前vm的一个实例,定义了一个 options,有三个键, 中间的 _parentVnode 就是父vnode,它其实是一个占位vnode,一个占位节点。最后返回了一个 new vnode.componentOptions.Ctor(options),回忆一下,在创建子组件vnode的时候,用了一个 context.$options.__base,也就是 Vue.extend,扩展了一个子组件构造器 Ctor,接着在创建vnode的时候,有个参数是 { Ctor, propsData, listeners, tag, children },这里的 Ctor 就是组件构造器,那么再执行 vnode.componentOptions.Ctor 的时候其实就是执行了 Sub 的构造函数,Subsrc/core/global-api/extend.js 中,然后它执行 了 _init,这个 _init 又回到了 Vue 的初始化,因为子组件的构造器其实是继承了 Vue 的构造器,来再次看下 _init 的细节,和之前不一样的地方,在 src/core/instance/init.js

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
  // expose real self
  vm._self = vm
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')
  
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  } 
}

首先合并了 options,参数 options._isComponent 现在是true,所以执行 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
  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
  }
}

创建了一个 vm.constructor.options 对象,然后赋值给 vm.$options,接下来是重点,它把 _parentVnodeparent 传了进来,_parentVnode 就是上面的 createComponentInstanceForVnode 的参数 _parentVnode,就是占位符的vnode,parent 是当前vm的实例,也就是当前子组件的父vm实例,继续看 initInternalComponent,把 vnodeComponentOptions 里的一些参数拿出来赋值给 opts,到此 initInternalComponent 结束。所以这里做的操作就是把通过 createComponentInstanceForVnode 函数传入的参数合并到内部的 $options 里了。

接着看 _initinitLifecycle 定义在 src/core/instance/lifecycle.js,看下这个方法:

export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false

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
}

拿到了一个 options.parent,这个 parent 实际上就是 activeInstance,注意 activeInstance 在这个文件中是一个全局变量,它的赋值在 lifecycleMixin 方法中:

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

在调用 _update 的时候,赋值了 activeInstance,也就是说每次调用 _update ,就会把当前的vm实例赋值给 activeInstance

 activeInstance = vm

同时用 prevActiveInstance 来保留上一次的 activeInstance,这么做是什么意思?

这里把当前的vm给了 activeInstance,然后在当前vm实例的vnode在 patch 的过程中,把当前实例作为父vm实例,传给子组件,这样 patch 其实就是一个深度遍历,将当前激活的vm实例给 activeInstance,然后在初始化子组件的时候,将这个 activeInstance 作为parent参数传入,然后在 initLifecycle 里,就可以拿到当前激活的vm实例,然后把实例作为parent:

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

此时 parent 是vm实例,parent.$children 塞一个子组件的vm。上面代码中的 vm 是子组件,parent 是它的父组件,然后他们有一层 push 的关系,接着把 vm.$parent 赋值为父组件实例 parent,至此 initLifecycle 就把这一层父子关系给建立起来了。

继续看 _init,最后的 vm.$mount 是走不到的,因为现在的 $options 没有 el

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

所以此时 _init 返回的是一个子组件的实例,然后回到 createComponent 里面的 init 钩子,createComponentInstanceForVnode 其实就是返回了一个子组件的实例,接着:

 child.$mount(hydrating ? vnode.elm : undefined, hydrating)

手动调用了 $mount 来挂载,也就是执行之前的 Vue.prototype.$mountmountComponent 方法,接着执行 _updateupdateComponent 方法,最后调用 __patch__ 渲染VNode:

vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
 
function patch (oldVnode, vnode, hydrating, removeOnly) {
  // ...
  let isInitialPatch = false
  const insertedVnodeQueue = []

  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    // ...
  }
  // ...
}

然后调用 createElm,再来看下它的定义:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ...
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // ...

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }
    
    // ...
  } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  } else {
    vnode.elm = nodeOps.createTextNode(vnode.text)
    insert(parentElm, vnode.elm, refElm)
  }
}

这里只传了2个参数,所以 parentElmundefined。注意,这里传入的 vnode 是组件渲染的 vnode,也就是之前说的 vm._vnode,如果组件的根节点是个普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。接下来的过程就和 createComponent 一样了,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复patch,这样通过一个递归的方式就可以完整地构建了整个组件树。

此时传入的 parentElm 是空,所以对组件的插入,在 createComponent 有这么一段逻辑:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // ....
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }
    // ...
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。

@daodaolee daodaolee changed the title Vue的patch Vue的patch过程 Mar 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant