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

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组件

猜你喜欢

转载自blog.csdn.net/haochangdi123/article/details/80931328