Vue 实例挂载方法($mount)的实现

Posted 十木禾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue 实例挂载方法($mount)的实现相关的知识,希望对你有一定的参考价值。

在 Vue 的 _init 方法中已经回调了beforeCreatecreated这两个生命周期钩子,在此之后就进行了实例的挂载

    if (vm.$options.el)  // 挂载实例
      vm.$mount(vm.$options.el);
    

在挂载函数中,将要进行 beforeMountmounted 的回调。


在不同的平台下对于 $mount 函数的实现是有差异的,下面考虑 web 平台的 runtime-with-compiler 版本 , 其在web平台下的定义如下(src/platforms/web/runtime/index.js)

import  mountComponent  from 'core/instance/lifecycle';

Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component 
  el = el && inBrowser ? query(el) : undefined;
  
  return mountComponent(this, el, hydrating);
;

$mount函数的参数中,第一个为我们属性的el, 第二个参数为服务端渲染有关,在patch函数中用到,这里可以忽略。

但是在调用这个$mount函数的时候,首先调用的是不同版本下的$mount函数,然后在该函数中再调用相应平台的$mount函数,如下在 runtime-with-compiler 版本中$mount函数如下(src/platforms/web/entry-runtime-with-compiler.js)

import Vue from './runtime/index';

const mount = Vue.prototype.$mount; // 缓存 上面的 $mount 方法

Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component 
  el = el && query(el);

  // 不能挂载到 body 和 html
  if (el === document.body || el === document.documentElement)      
    return this;
  

  const options = this.$options;

  if (!options.render)  // 如果没有 render 函数
  	// ... 将 render 函数添加到 options 上
      const  render, staticRenderFns  = compileToFunctions(template, 
        outputSourceRange : process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters        : options.delimiters,
        comments          : options.comments,
      , this);

      options.render = render;
      options.staticRenderFns = staticRenderFns;
    // ...
  
  
  return mount.call(this, el, hydrating);
;

可知该函数主要干了三件事

  1. 由于挂载之后会替换被挂载的对象,所以限制不能挂载到 bodyhtml
  2. 如果当前Vue实例没有 render() 函数(写template等),则通过编译等手段,将render函数添加到 options
  3. 调用在代码开头我们先缓存的$mount方法,该方法就是web平台下的方法。

web平台下的$mount方法里面主要就是调用了mountComponent() 方法,接下来我们的核心就是该方法了


'core/instance/lifecycle.js 文件中我们找到了该方法的定义,删掉一些非重点代码后如下

export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component 
  vm.$el = el;
  if (!vm.$options.render)  
  	// 不是重点,该处主要是用来对没有 render 函数下的一些错误提示
  
  callHook(vm, 'beforeMount'); // 回调 beforeMount , 开始准备挂载实例

  // 声明 更新组件 的函数 (源代码中有关performance配置不是重点,故省略) 
  const updateComponent = updateComponent = () => 
     vm._update(vm._render(), hydrating);
  ;

  // new 一个 Watcher [isRenderWatcher]
  new Watcher(vm, updateComponent, noop, 
    before() 
      if (vm._isMounted && !vm._isDestroyed) 
        callHook(vm, 'beforeUpdate');
      
    ,
  , true /* isRenderWatcher */);
  hydrating = false;

  // Vue 的根实例的 mounted 回调在这里执行
  if (vm.$vnode == null) 
    vm._isMounted = true;
    callHook(vm, 'mounted');
  
  
  return vm;

上面的代码中主要干了如下三件事

  1. 回调 beforeMount
  2. 生成 updateComponent 方法,该方法将 vnode 渲染为真实的DOM
  3. new 一个 Watcher ,并在该 Watcher在调用updateComponent方法
  4. 回调 mounted

对于 updateComponent方法较为复杂,其内部主要调用_update()vnode渲染为浏览器上显示的真实DOM

我们考虑如下两个问题

1. Watcher 中如何调用 updateComponent方法

Watcher 函数的构造函数接受如下的参数

constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  )

在上面的代码中,updateComponent()方法作为第二个参数传递过来,即构造函数中的expOrFn

往下看会看到

    if (typeof expOrFn === 'function') 
      this.getter = expOrFn;
    

也就是说updateComponent()方法被设置为了getter()方法

看到构造函数的最后

    this.value = this.lazy
      ? undefined
      : this.get();

其中 lazy 属性的值在前面被设置为了 false

	this.lazy = !!options.lazy; // 我们 options 中没有 lazy 属性

这也就是说,咋i构造函数的末尾会调用this.get(),而在this.get()

    const vm = this.vm;

    try 
      value = this.getter.call(vm, vm);
    

我们看到调用了getter()方法,也就是调用了updateComponent()方法。

2. 为什么根实例的$vnode为空

initRender()函数中有如下代码

const parentVnode = vm.$vnode = options._parentVnode;

也就是说 当前实际的 $vnode 值为其父节点的vnode
而根实例没有父节点,故其$vnode值就为空了,所以会执行

 if (vm.$vnode == null) 
    vm._isMounted = true;
    callHook(vm, 'mounted');
  

那么子节点的mounted回调是在那里执行的呢?
path()(core/vdom/patch.js) 函数中有如下代码

  function invokeInsertHook(vnode, queue, initial) 
    if (isTrue(initial) && isDef(vnode.parent)) 
      vnode.parent.data.pendingInsert = queue;
    
    else 
      for (let i = 0; i < queue.length; ++i) 
        queue[i].data.hook.insert(queue[i]); // 这里
      
    
  

在循环queue的时候,调用了 insert()方法,该方法为 VNodeHooks,其在componentVNodeHooks(core/vdom/create-component.js)中声明,代码如下

const componentVNodeHooks = 
  insert(vnode: MountedComponentVNode) 
    const  context, componentInstance  = vnode;

    if (!componentInstance._isMounted) 
      componentInstance._isMounted = true;
      callHook(componentInstance, 'mounted'); // 这里
    
    if (vnode.data.keepAlive) 
      if (context._isMounted) 
        queueActivatedComponent(componentInstance);
      
      else 
        activateChildComponent(componentInstance, true /* direct */);
      
    
  ,

由于 path() 方法在 _update()函数中调用,这部不再重点说明。

下节我们将来说说render()_update() 方法的实现


欢迎关注微信公众号,会有最新的文章哦~: 子曰思鱼

以上是关于Vue 实例挂载方法($mount)的实现的主要内容,如果未能解决你的问题,请参考以下文章

vue $mount挂载实例方法

vue $mount挂载实例方法

Vue源码分析 : Vue实例挂载

vue之$mount

Vue2.0 第四季第2节 实例方法

[Vue]实例化Vue时的两种挂载方式el与$mount