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 源码分析(组件渲染) #16

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

Vue 源码分析(组件渲染) #16

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

Vue 源码分析(组件渲染)

在首次渲染的时候,我们分析的是 html 标签名的渲染,没有涉及组件的渲染,现在介绍组件渲染。

Vue.component('component-a', {
  template: `<div>adsf</div>`
});

let vm = new Vue({
  el: '#app',
  data: {
    a: 10
  },
  render: function (h) {
    return h(
      'component-a'
    )
  }
});

渲染过程都要调用 _createElement 函数,如果 tag 是组件的话会调用 createComponent 函数。

createComponent

createComponent 定义在 src/core/vdom/create-component.js 文件

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

createComponent 函数主要有三个大的步骤:构造子类构造函数、安装组件钩子函数、实例化 VNode。

构造子类构造函数

const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
	Ctor = baseCtor.extend(Ctor)
}

context 是 Vue 实例 vm,baseCtor 是 Vue 构造函数。

isObject(Ctor) 是针对 App.vue 形式的对象,把 App.vue 对象转变为子类构造函数。

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

这个是处理异步组件,在介绍组件的时候已经分析了

 resolveConstructorOptions(Ctor)

resolveConstructorOptions 处理构造函数选项,定义在 src/core/instance/init.js 文件中

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

只有子类构造函数才能进入 if 语句。

const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
	...
}

superOptions 保存父构造函数的选项 options,cachedSuperOptions 保存调用 Vue.extend 函数时的父构造函数的选项 options,然后比较是否相等。这个是为了我们调用 Vue.extend 函数之后,改变了父构造函数的选项之后,好做一个判断。比如说:

  var Profile = Vue.extend({
    template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>'
  })
  Vue.mixin({
    data: function () {
      return {
        firstName: 'Walter',
        lastName: 'White',
        alias: 'Heisenberg'
      }
    }
  })
  new Profile().$mount('#app')

Vue.extend 函数会保存此时的父构造函数的选项。

Vue.extend = function (extendOptions: Object): Function {
	...
	
    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)
	
}

但是我们调用 Vue.mixin 函数改变了 Vue (父构造函数)的选项。如果父构造函数的选项改变了,需要进入 if 语句中,重新合并选项。

// check if there are any late-modified/attached options (#4976)
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)

父构造函数能改变选项,子类构造函数同样可以改变选项。根据代码注释,可以找到下面例子:

 const Test = Vue.extend({
    foo: 'Foo'
  })

  // Inject options later
  // vue-loader and vue-hot-reload-api are doing like this
  Test.options.computed = { $style: { test: 'abcde' } }
  Test.options.beforeCreate = [
    () => { console.log('Should be printed') }
  ]
  Test.options.render = function (h) {
    return h('div', '$style: ' + this.$style)
  }

  // Update super constructor's options
  Vue.mixin({})

  new Vue({
    render: h => h(Test)
  }).$mount('#app')

在调用 Vue.extend 函数之后,直接在 Test.options 添加属性。所以通过

const modifiedOptions = resolveModifiedOptions(Ctor)

查找变动的选项 modifiedOptions,最后合并到 Ctor.extendOptions 上面

extend(Ctor.extendOptions, modifiedOptions)

安装组件钩子函数

installComponentHooks(data)

installComponentHooks 函数定义在 src/core/vdom/create-component.js 文件中

function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

installComponentHooks 函数的作用是将 componentVNodeHooks 的钩子函数合并到 data.hooks 。这些钩子函数会在 patch 函数中调用。有一下钩子函数:

// inline hooks to be invoked on component VNodes during patch
const componentVNodeHooks = {
  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)
    }
  },

  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

实例化 VNode

const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  data, undefined, undefined, undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
)

实例化 VNode 就是调用 new VNode() 实现的,需要注意的是组件实例化,children 参数是 undefined。

最终会调用 patch 函数

patch

export function createPatchFunction (backend) {
	...
	
	return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
    	const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } 
      
    	...
    	
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

首次渲染遇见组件,会进入 if(isUndef(oldVnode)) 语句,调用 createElm(vnode, insertedVnodeQueue)。至于

const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
  // patch existing root node
  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} 

这段代码在虚拟 dom 的 patch 时会介绍。

createElm

createElm 函数定义在 src/platforms/web/runtime/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
    }
		
		...
		
  }

在介绍首次渲染的时候没有介绍

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}

这段代码和组件相关,这里详解介绍

createComponent

这里的 createComponent 函数和定义在* src/core/vdom/create-component.js* 文件中同名的 createComponent 函数不一样。我们约定:这里的 createComponent 函数我们称为 A 函数,另外一个称为 B 函数。

这里的 createComponent 函数定义在 src/platforms/web/runtime/patch.js 文件

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
    }
  }
}

因为在 B 函数中我们安装了组件的钩子函数,其中就有 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)
  }
}

init 钩子函数会调用 createComponentInstanceForVnode 创建一个 Vue 实例,然后调用 $mount 方法挂载子组件。

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)
}

最后一句

return new vnode.componentOptions.Ctor(options)

创建 Vue 子类构造函数的实例。vnode.componentOptions 在实例化 VNode 时候赋值。创建这个实例的时候和首次渲染的流程差不多,不同之处就是走的组件的逻辑,需要注意,在此不多介绍。

继续 A 函数,接下来会执行

initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)

initComponent 函数初始化组件,insert 函数将组件插入父元素中。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant