最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 重学Vue【Vue的patch】

    正文概述 掘金(道道里)   2021-03-05   526

    重学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仓库,有需要的自取~ 重学Vue【Vue的patch】

    附述

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

    App.vue
    <template>
      <div id="app">
       <HelloWorld />
      </div>
    </template>
    

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

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

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

    1. initComponent 是把patch的返回值赋值给 vnode.elm(HelloWorld占位符节点)

    2. insert是把HelloWorld.vue 插入到父占位符里

    此时 HelloWorld插入到App.vue结束


    起源地下载网 » 重学Vue【Vue的patch】

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元