您现在的位置:首页 >> 前端 >> 内容

vue2.x源码解析之实例解析组件的整个映射过程

时间:2018/7/11 11:38:15 点击:

  核心提示:1.准备工作1.加入断点我们利用断点的方式,一步一步分析,,我们采用的是Runtime+Compiler版本的vue.js,所以我们将debugger插入组件DOM的时候会走createCompone...

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

  <p id="app"

    <img src="./assets/logo.png"

    <hello</hello

  </p

</template

<script

import hello from './components/HelloWorld'

export default {

  name: 'App',

  components: {

    hello

  }

}

</script

HelloWorld.vue

 <p class="hello"

    <h1{{ msg }}</h1

    <h2Essential Links</h2

    <h2Ecosystem</h2

    <ul

      <li

      </li

      <li

      </li

    </ul

  </p

export default {

  name: 'HelloWorld',

  data () {

    return {

      msg: 'Welcome to Your Vue.js App'

    }

  }

}

注释:

占位符节点: 

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

渲染VNode 

B组件的外层p, <p id="app", 

因为它有子节点,也就是children,他的children都会保留,所以拿到渲染VNode也就是根VNode就可以了,可以用它去遍历children,拿到这个子节点VNode树

2 过程

1.进入函数path

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

 }

参数: 

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

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

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

 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实例 就是p#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实例的外层元素,就是p#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)

1

2

就会执行$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组件的外层p, `<p id="app"`

    vnode = render.call(vm._renderProxy, vm.$createElement);

    。

    。

    。

     // 将渲染vnode指向 占位符vnode节点

    vnode.parent = _parentVnode;

    // 返回渲染vnode

    return vnode

}

渲染vnode 

app.vue组件的外层p, <p 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最外层的p#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组件

Tags:VU UE E2 2X 
作者:网络 来源:小毛驴的博客