欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

vue2.x源码解析六——组件化--4.实例解析组件的整个映射过程

程序员文章站 2022-05-13 21:41:17
...

1.准备工作

1.加入断点

我们利用断点的方式,一步一步分析,,我们采用的是Runtime+Compiler版本的vue.js,所以我们将debugger

插入组件DOM的时候会走createComponent函数

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
   ...
      if (isDef(vnode.componentInstance)) {
        debugger
       ...
      }
  }

path的时候

 return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    debugger
 }

1.2 例子

我们的例子为

目录:
|-main.js ——– 写Vue实例 ( 用A.vue代替)
|-app.vue ——– 组件 (用B.vue代替)
|-HelloWorld.vue —— 组件 (用C.vue代替)

main.js
使用app.vue组件

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

app.vue
HelloWorld.vue会插入到app.vue中

<template>
  <div id="app">
    <img src="./assets/logo.png">
    <hello></hello>
  </div>
</template>

<script>
import hello from './components/HelloWorld'
export default {
  name: 'App',
  components: {
    hello
  }
}
</script>

HelloWorld.vue

 <div class="hello">
    <h1>{{ msg }}</h1>
    <h2>Essential Links</h2>
    <h2>Ecosystem</h2>
    <ul>
      <li>
      </li>
      <li>
      </li>
    </ul>
  </div>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Welcome to Your Vue.js App'
    }
  }
}

注释:

占位符节点:
渲染B组件的时候,B组件中引入了组件,那么 <hello></hello>就是占位符节点,同理A使用B组件的时候也会有占位符节点

渲染VNode
B组件的外层div, <div id="app">,
因为它有子节点,也就是children,他的children都会保留,所以拿到渲染VNode也就是根VNode就可以了,可以用它去遍历children,拿到这个子节点VNode树

2 过程

1.进入函数path

 return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm){
 }

参数:
oldVnode :div#app 原生的DOM节点,就是我们vue实例话的时候的 div id=“app”

vnode : vue-component-4-App 就是我们的组件app.vue

2

因为是最开始的path,所以 isRealElement 设置为true

3

 oldVnode = emptyNodeAt(oldVnode);

将oldVnode 转化为VNode

4.第一次执行createElm做挂载

也就是对app.vue进行挂载

function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode);
    }

    vnode.isRootInsert = !nested; // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

会执行createComponent这个方法

5.

执行createComponent这个方法

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
      var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
      // 这里会为true,执行hook中的init方法
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */, parentElm, refElm);
      }
      // 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)) {
        debugger
        initComponent(vnode, insertedVnodeQueue);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
      }
    }
  }

vnode是app.vue组件,组件中是有data和hook钩子的,所以会执行执行hook中的init方法

6.

componentVNodeHooks

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 {
            //child是一个vnode实例
          const child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode, // 当前组件的vnode
            activeInstance // 当前的vue实例 就是div#app,也就是当前组件的父vue实例
          )
          //调用 $mount 方法挂载子组件
          child.$mount(hydrating ? vnode.elm : undefined, hydrating)
        }
      },

     prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
     },

     insert (vnode: MountedComponentVNode) {
     },

     destroy (vnode: MountedComponentVNode) {
     }
}

这是会自动给组件添加的hook,执行的就是这个hook种的init方法,会走else,就会走createComponentInstanceForVnode方法

7.

进入createComponentInstanceForVnode方法

function createComponentInstanceForVnode (
  vnode, // 
  parent, // 
  parentElm,
  refElm
) {
 // 定义options
  var options = {
    _isComponent: true,
    parent: parent, // vue实例
    _parentVnode: vnode, // 这里就是站位父VNode,也就是app.vue的占位符
    _parentElm: parentElm || null, // vue实例的外层元素,就是div#app的外层,这里是body
    _refElm: refElm || null
  };
  // check inline-template render functions
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  // 最后就会走到这个构造器
  return new vnode.componentOptions.Ctor(options)
}

最后就会走到构造器

8.

上面进入到了子构造器

 return new vnode.componentOptions.Ctor(options)

然后就会执行构造函数

var Sub = function VueComponent (options) {
      this._init(options);
    };

Sub是继承于Vue,所以会有跟Vue一样的原型方法,就会走_init函数

9

 Vue.prototype._init = function (options) {
  // 因为是组件,所以合并options会走这里
   if (options && options._isComponent) {
      initInternalComponent(vm, options);
    } else {
    }

 // 初始化生命周期
 initLifecycle(vm);

 //vm.$options就是B组件,B组件是没有el的,所以不会执行,回跳出init函数,去执行$mount
  if (vm.$options.el) {
      vm.$mount(vm.$options.el);
   }
 }

1.合并options

因为是组件,所以合并options会initInternalComponent函数

function initInternalComponent (vm, options) {
  // 返回一个空对象
  var opts = vm.$options = Object.create(vm.constructor.options);

  var parentVnode = options._parentVnode;
  opts.parent = options.parent;    // Vue实例
  opts._parentVnode = parentVnode; // 占位符VNode
  opts._parentElm = options._parentElm;
  opts._refElm = options._refElm;

  // 将占位符VNode的一些属性赋值给opts
  var 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;
  }
}

2. 初始化生命周期

 initLifecycle(vm);

建立组件实例和new Vue的实例的父子关系

function initLifecycle (vm) {
  var options = vm.$options; // B组件实例

  // locate first non-abstract parent
  var parent = options.parent; // new Vue的实例
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent;
    }
    // 向 new Vue的实例中push组件实例
    parent.$children.push(vm);
  }
  // 组件的$parent指向new Vue的实例
  vm.$parent = parent;
  vm.$root = parent ? parent.$root : vm;

 。。。
}

10

会跳回到 6节 执行

  //调用 $mount 方法挂载子组件
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)

就会执行$mount方法

var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
  el,
  hydrating
) {
 // 挂载的真实DOM
  el = el && query(el);

  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      "Do not mount Vue to <html> or <body> - mount to normal elements instead."
    );
    return this
  }

// 组件实例
  var options = this.$options;

 // 这里的render函数是有的,因为组件会被vue-loader转化为对象,对象会有render渲染函数
  if (!options.render) {
  }

  //最终执行的是Vue原生的mount
  return mount.call(this, el, hydrating)
};

因为采用的是Runtime+Compiler版本的vue.js,所以没有直接走原生的mount方法,但是最终执行的是Vue原生的mount

11.

Vue.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

发现他执行的是mountComponent方法

12

function mountComponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el;
  ...

  var updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else {
    updateComponent = function () {
     // vm._update,vm._render()
      vm._update(vm._render(), hydrating);
    };
  }
// 去监控执行 vm._update
  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
  hydrating = false;

 ...
  return vm
}

会生成渲染watcher,去监控执行 vm._update,也就是path,我们先看一下vm._render()

13

上面的vm._render()
将子元素都生成VNode

Vue.prototype._render = function () {
    vm.$vnode = _parentVnode; // 将占位符vnode赋值给$vnode

    // 调用render去生成渲染vnode,就是app组件的外层div, `<div id="app">`
    vnode = render.call(vm._renderProxy, vm.$createElement);
    。
    。
    。
     // 将渲染vnode指向 占位符vnode节点
    vnode.parent = _parentVnode;
    // 返回渲染vnode
    return vnode
}

渲染vnode
app.vue组件的外层div, <div id="app">,
因为它有子节点,也就是children,他的children都会保留,所以拿到渲染VNode也就是根VNode就可以了,可以用它去遍历children,拿到这个子节点VNode树

返回 返回渲染vnode后, vm._update就会去调用渲染vnode

14

当前为B组件实例实例化的过程,此时的activeInstance自然是最外层的vue实例,但是我们会将B组件实例赋值给activeInstance

Vue.prototype._update = function (vnode, hydrating) {
    //保存activeInstance,为Vue,因为我们是B组件的实例化过程,activeInstance是Vue的实例化过程
    var prevActiveInstance = activeInstance; 
    // activeInstance 保存当前实例,是B组件实例
    activeInstance = vm; 


    // B组件实例的_vnode去保留渲染vnode
     vm._vnode = vnode;

    // 这是就回去执行子组件的patch,也就是B组件的初始化,将行子组件的patch结果赋值给B组件实例.$el
    if (!prevVnode) {
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
      vm.$options._parentElm = vm.$options._refElm = null;
    } else {
      // 渲染B组件的内容,再次调用__patch__方法
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
}

当我们patch完最外层,就会返回B组件的占位符vnode,占位符vnod执行整个B组件初始化过程中才会去渲染子组件
接着执行patch方法

15

对B组件的子组件进行patch

 return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm){
 }

参数:
oldVnode :undefined,因为B组件没有绑定元素呢
vnode : 就是我们的B组件(app.vue)的渲染vnode,里面包含着子VNode

接着就会执行createElm方法,进行挂载

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

16

 function createElm (
    vnode,     //B组件(app.vue)的渲染vnode
    insertedVnodeQueue,
    parentElm, 
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode);
    }

    vnode.isRootInsert = !nested; // for transition enter check
    // 这个时候vnode是渲染vnode,也就是app.vue最外层的div#app,并不是组件,所以不会走这里
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    // 拿到app.vue的data
    var data = vnode.data;
    // 拿到app.vue的所有子节点,第三个子节点是我们helloWorld.vue组件
    var children = vnode.children;
    var tag = vnode.tag;
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++;
        }
        if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          );
        }
      }

    // 给渲染VNode.elm创建一个DOM,
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode);
      setScope(vnode);

      /* istanbul ignore if */
      // 执行 createChildren,子组件插入到app.vue的占位符中
      {
         //   createChildren的时候回递归的执行createElm方法,遇到我们的helloWorld组件的时候就会再次执行createComponent这个方法
        createChildren(vnode, children, insertedVnodeQueue);
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue);
        }
        insert(parentElm, vnode.elm, refElm);
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--;
      }
    } 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);
    }
  }

17

子节点插入父节点,父节点插入爷爷节点

总结

patch的总体过程:
createComponents (返回为true)–> 子组件初始化 (_init –>createComponentInstanceForVnode –>initLifecycle初始化 –> mount挂载)–> 子组件render(生成渲染vnode) –> 子组件patch –> 遍历子组件的渲染VNode –> 如果发现子组件中还有组件就递归的调用 createComponents

activeInstance为当前**的vm实例,会作为子组件的parent传入,因为如果有多层组件的套用,是需要深层遍历的

vm.$vnode是组件的占位符节点

vm._vnode是渲染VNode

嵌套组件的插入顺序是先子后父。
A.vue 调用 B.vue 调用 C.vue
判断 A中有B组件 –> 生成B组件占位VNode –> 生成B组件渲染VNode –> 遍历B组件渲染VNode –> 发现组件C –> 生成C组件占位VNode –> 生成C组件渲染VNode –> 遍历C组件渲染VNode –> 将生成的DOM节点插入 C组件 —> 将C组件插入B组件 –> B组件插入A组件