重学Vue【Vue的patch】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


前面提到 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 的插入顺序是先子后父。


下面附一张自己整理的图,还有一张更大的图在github仓库,有需要的自取~

网络异常,图片无法展示
|

附述


由于我在这一块花费了好多时间,所以在附上一点自己的理解。

App.vue
<template>
  <div id="app">
   <HelloWorld />
  </div>
</template>
复制代码

id为app作根,是一个渲染vnode,里面有个children,children里有个child就是: ,这个 就是 HelloWorld.vue 文件的 _parentVnode,也就是占位符vnode。

真正渲染的是 HelloWorld.vue 文件,调用它的 render 渲染出一个 渲染vnode,这个渲染vnode的_parentVnode 就是  这个占位符vnode。

vm.$vnode = __parentVnode = vnode._parent

vm._vnode = vnode

vm._vnode.parent = vm.$vnode

update方法执行下面两个方法:

  1. initComponent 是把patch的返回值赋值给 vnode.elm(HelloWorld占位符节点)
  2. insert是把HelloWorld.vue 插入到父占位符里

此时 HelloWorld插入到App.vue结束

vm._vnode是渲染vnode,vm.$vnode是占位vnode

目录
相关文章
|
5天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
5天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
5天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
10天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
11天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
11天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
11天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
10天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
12天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
10天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉