写点什么

每日一题之请描述 Vue 组件渲染流程

作者:bb_xiaxia1998
  • 2022-10-28
    浙江
  • 本文字数:6597 字

    阅读完需:约 22 分钟

组件化是 Vue, React 等这些框架的一个核心思想,通过把页面拆成一个个高内聚、低耦合的组件,可以极大程度提高我们的代码复用度,同时也使得项目更加易于维护。所以,本文就来分析下组件的渲染流程。我们通过下面这个例子来进行分析:


<div id="demo">  <comp></comp></div><script>  Vue.component('comp', {    template: '<div>I am comp</div>',  })  const app = new Vue({    el: '#demo',  })</script>
复制代码


这里我们分为两步来分析:组件声明、组件创建及渲染

组件声明

首先,我们看下 Vue.component 是什么东西,它的声明在 core/global-api/assets.js


export function initAssetRegisters(Vue: GlobalAPI) {  // ASSET_TYPES是数组:['component','directive','filter']  ASSET_TYPES.forEach((type) => {    Vue[type] = function (      id: string,      definition: Function | Object    ): Function | Object | void {      if (!definition) {        return this.options[type + 's'][id]      } else {        /* istanbul ignore if */        if (process.env.NODE_ENV !== 'production' && type === 'component') {          validateComponentName(id)        }        // 组件声明相关代码        if (type === 'component' && isPlainObject(definition)) {          definition.name = definition.name || id          // _base是Vue          // Vue.extend({})返回组件构造函数          definition = this.options._base.extend(definition)        }        if (type === 'directive' && typeof definition === 'function') {          definition = {bind: definition, update: definition}        }        // 注册到components选项中去        // 在Vue原始选项上添加组件配置,将来其他组件继承,它们都有这些组件注册        this.options[type + 's'][id] = definition        return definition      }    }  })}
复制代码


这里 this.options._base.extend(definition) 调用的其实就是 Vue.extend(definition)


Vue.extend = function (extendOptions: Object): Function {  extendOptions = extendOptions || {}  const Super = this  const SuperId = Super.cid  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})  if (cachedCtors[SuperId]) {    return cachedCtors[SuperId]  }
const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) }
const Sub = function VueComponent(options) { this._init(options) } Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ Sub.options = mergeOptions(Super.options, extendOptions) Sub['super'] = Super
// For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) }
// allow further extension/mixin/plugin usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use
// create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub }
// 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)
// cache constructor cachedCtors[SuperId] = Sub return Sub}
复制代码


这里我们可以理解为返回了一个名叫 VueComponent 的构造函数且继承了 Vue。所以,这里的组件定义完成后 Vue 就会变成这样:


{  ...  options: {    components: {      comp: function VueComponent() {}    }  }  ..}
复制代码

组件创建及挂载

我们知道 Vue 中的模板最后会变编译成 render 函数,比如上面例子最终的 render 函数会如下所示:


render() {  with (this) {return _c('div',{attrs:{"id":"demo"}},[_c('comp')],1)}}
复制代码


这里 _c 的定义可以在 core/instance/render.js 中找到:


vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
复制代码


所以 _c('comp') 最终还是调用了 createElement (core/vdom/create-element.js) 这个方法:


export function createElement (  context: Component,  tag: any,  data: any,  children: any,  normalizationType: any,  alwaysNormalize: boolean): VNode | Array<VNode> {  ...  return _createElement(context, tag, data, children, normalizationType)}
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number): VNode | Array<VNode> { ... } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 自定义组件 vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } ...}
复制代码


这里我们只看自定义组件的相关逻辑,发现最后调用了 createComponent (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 {  ...
// 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 )
return vnode}
复制代码


这里我们跳过其他的代码,先看看 installComponentHooks


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    }  }}
复制代码


这里会在 data.hook 上挂载一些 hooks,如果用户也传了相同的 hooks 则会进行合并。这个 hooks 又是啥呢:参考 前端vue面试题详细解答


const componentVNodeHooks = {  // 实例化和挂载  init(vnode: VNodeWithData, hydrating: boolean): ?boolean {    if (      vnode.componentInstance && // 实例已经存在      !vnode.componentInstance._isDestroyed && // 未被销毁      vnode.data.keepAlive // 被标记为keepAlive    ) {      // kept-alive components, treat as a patch      // 对于缓存组件,只需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 */) } } },}
复制代码


这里有四个 hooks ,看他们的名字就知道他们会在对应的操作去执行。比如 init 会在组件初始化的时候执行,这个后面碰到了再说。我们继续看 createComponent


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


export default class VNode {  ...  constructor(    tag?: string,    data?: VNodeData,    children?: ?Array<VNode>,    text?: string,    elm?: Node,    context?: Component,    componentOptions?: VNodeComponentOptions,    asyncFactory?: Function  ) {    ...    this.componentOptions = componentOptions  }
// DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child(): Component | void { return this.componentInstance }}
复制代码


这里初始化了一个 VNode 并进行了返回,到这里 _c('comp') 的任务就完成了。可以看到我们的自定义组件的构造函数在这一步并没有执行,仅仅只是挂载到了 componentOptions 属性上。那他什么时候执行呢?别急,我们接着往下走。


当根组件的 render 执行完后,会执行 vm._update 进行组件的更新,然后会调用 __patch__,我们顺藤摸瓜最终来到 core/vdom/patch.js


return function patch(oldVnode, vnode, hydrating, removeOnly) {      ...      // create new node      createElm(        vnode,        insertedVnodeQueue,        // extremely rare edge case: do not insert if old element is in a        // leaving transition. Only happens when combining transition +        // keep-alive + HOCs. (#4590)        oldElm._leaveCb ? null : parentElm,        nodeOps.nextSibling(oldElm)      )      ...  return vnode.elm}
复制代码


然后会走到 createElm


function createElm(    vnode,    insertedVnodeQueue,    parentElm,    refElm,    nested,    ownerArray,    index  ) {  ...      } else {        createChildren(vnode, children, insertedVnodeQueue);        if (isDef(data)) {          // 事件、属性等等初始化          invokeCreateHooks(vnode, insertedVnodeQueue);        }        // 插入节点        insert(parentElm, vnode.elm, refElm);      }  ...
复制代码


注意到这里的 vnode<div id="demo"></div> 这个元素的,所以会走到 createChildren


function createChildren(vnode, children, insertedVnodeQueue) {  if (Array.isArray(children)) {    if (process.env.NODE_ENV !== 'production') {      checkDuplicateKeys(children)    }    for (let i = 0; i < children.length; ++i) {      createElm(        children[i],        insertedVnodeQueue,        vnode.elm,        null,        true,        children,        i      )    }  } else if (isPrimitive(vnode.text)) {    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))  }}
复制代码


这里最后又回到了 createElm,不过此时的 vnode 就是自定义组件了,会走到这里:


  function createElm(    vnode,    insertedVnodeQueue,    parentElm,    refElm,    nested,    ownerArray,    index  ) {    ...    // 自定义组件创建    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {      return;    }
复制代码


function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {  let i = vnode.data  if (isDef(i)) {    // 缓存的情况    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive    // 前面安装的钩子在这里用到了,执行了 init,自定义组件实例化    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)      // 插入dom      insert(parentElm, vnode.elm, refElm)      if (isTrue(isReactivated)) {        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)      }      return true    }  }}
复制代码


注意到这里会执行 i.init 方法,该方法上文已经说过,会实例化组件对象,然后进行 $mount。而执行 $mount 最终又会走到 patch 方法,并最终执行 createElm


function patch(oldVnode, vnode, hydrating, removeOnly) {  ...  if (isUndef(oldVnode)) {    // empty mount (likely as component), create new root element    isInitialPatch = true;    createElm(vnode, insertedVnodeQueue);  }  ...}
复制代码


执行该方法又会递归的将自定义组件内的 vnode 渲染成真实的 dom,最后通过 insert 方法将整颗 dom 树插入到父元素之中。到这里自定义组件的渲染过程就结束了


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
每日一题之请描述Vue组件渲染流程_Vue_bb_xiaxia1998_InfoQ写作社区