从源码来分析vue挂载原理

简介

再讲挂载原理之前呢,先介绍下vue的四种不同的构建版本
在这里插入图片描述
可以看出,排除环境问题之外,构建版本有两种,一种完整版,一种运行时版本,那么两种有什么区别的?简单的说,完整版包含编译器运行时版本不包含编译器,需要特别强调的是,运行时版本完整版的体积小30%左右,另外vue-cli默认是运行时的版本,当然版本可以根据具体情况自由选择,那么vue-cli3更改版本如下所示:

// vue.config.js
module.exports = {
    configureWebpack: {
        resolve: {
            alias: {
                'vue$': 'vue/dist/vue.esm.js'
            }
        }
    },
}

那么什么时候需要编译器呢?如下代码所示

// 如果传入一个字符串给template选项,这时候就需要编译器
new Vue({
	template:<div>hi</div>
})

// 不需要编译器
new Vue({
	render: c => c (xx)
})

OK,科普完毕,下面就进入咱们今天的主题vue挂载方式及原理

vue挂载方式

相信大家都知道,VUE主要挂载有两种方式,一种是el选项,一种是$mount,但实际上,无论我们在实例化VUE时是否设置了el选项,想让VUE实例具有关联的DOM元素,只有vm.$mount方法这一种途径,如源码所示:

      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }

那么,具体方式大概有以下方式

	//1.el选项->template
	new Vue({
	    el:"#app",
	    template:"<div>哈哈哈</div>"
	})
	//2.el选项->render
	new Vue({
	    el:"#app",
	    render:c=>c(App)
	})
	//3. $mount ->template选项
	new Vue({
    	template:"<div>哈哈哈</div>"
	}).$mount("#app")
	//4. $mount ->render
	new Vue({
    	render:c=>c(App)
	}).$mount("#app")

接下来,就从源码来分析下vm.$mount的原理。

vue挂载原理

在文章开头的时候已经介绍过,vue构建方式分为完整版运行时,所以$mount的实现原理也会不同,首先请看以下代码

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

这段代码就是运行时的$mount,具体逻辑先不讲,后续会讲,下面先讲下完整版的$mount的方法

完整版vm.$mount的实现原理

ok,先贴上源码

  var mount = Vue.prototype.$mount;
  Vue.prototype.$mount = function (
    el,
    hydrating
  ) {
    el = el && query(el);

    /* istanbul ignore if */
    if (el === document.body || el === document.documentElement) {
      warn(
        "Do not mount Vue to <html> or <body> - mount to normal elements instead."
      );
      return this
    }

    var options = this.$options;
    // resolve template/el and convert to render function
    if (!options.render) {
      var template = options.template;
      if (template) {
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
            /* istanbul ignore if */
            if (!template) {
              warn(
                ("Template element not found or is empty: " + (options.template)),
                this
              );
            }
          }
        } else if (template.nodeType) {
          template = template.innerHTML;
        } else {
          {
            warn('invalid template option:' + template, this);
          }
          return this
        }
      } else if (el) {
        template = getOuterHTML(el);
      }
      if (template) {
        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile');
        }

        var ref = compileToFunctions(template, {
          outputSourceRange: "development" !== 'production',
          shouldDecodeNewlines: shouldDecodeNewlines,
          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        }, this);
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns;
        options.render = render;
        options.staticRenderFns = staticRenderFns;

        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile end');
          measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
        }
      }
    }
    return mount.call(this, el, hydrating)
  };

可以看出,完整版是在运行时的基础上利用了函数劫持,将原型上的$mount方法保存在mount属性上,这里的$mount的方法就是上面该章开头贴的那段代码,以便后续使用,然后在原型上重新定义一个$mount方法,通过这种方式就可以在执行原有方法之前新增一些功能,那么接下来让我看下他加了哪些功能?
1.首先解析el,就是传入的被挂载对象

 el = el && query(el);
  function query (el) {
    if (typeof el === 'string') {
      var selected = document.querySelector(el);
      if (!selected) {
        warn(
          'Cannot find element: ' + el
        );
        return document.createElement('div')
      }
      return selected
    } else {
      return el
    }
  }

以上代码可以看出,首先必须要传的,然后去找这个元素,如果没有找到,发出警告,然后新建一个div,如果找到直接返回。

2.接下来,判断el是否是html或者body,如果是发出警告,并直接返回vue实例

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

3.在接下来就是完整版运行时的区别了,那就是判断是否有render函数,如果没有,就把template/el编译成render函数

   var options = this.$options;
   if (!options.render) {
    // 编译过程  
   }
   return mount.call(this, el, hydrating)

注意,以上的$options是实例化的时候传入的所有参数,这个值是在初始化的时候赋值的,通过这个条件可以明显看出如果给出render选项,那么template是无效的,下面就看下如果tempale有效,它是怎么处理的
4.首先,如源码所示

	  var template = options.template;
      if (template) {
        	if (typeof template === 'string') {
	          	if (template.charAt(0) === '#') {
		            template = idToTemplate(template);
		            /* istanbul ignore if */
		            if (!template) {
		              warn(
		                ("Template element not found or is empty: " + (options.template)),
		                this
		              );
		            }
		          }
		        } else if (template.nodeType) {
		          template = template.innerHTML;
		        } else {
		          {
		            warn('invalid template option:' + template, this);
		          }
		        return this
	        }
      } else if (el) {
        template = getOuterHTML(el);
      }

判断用户是否提供template选项,也就是挂载模板,如果提供了,又如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板。常用的技巧是用<script type="x-template">包含模板,如果template是字符串,但不是以#号开头的,就说明template是用户设置的模板,不需要改进,直接使用即可。如果template选项不是字符串,而是一个DOM元素,则使用DOM元素的innerHTML作为模板,如果template选择既不是字符串又不是DOM元素,那么VUE会发出警告,提示template选项无效。

如果没有提供,那么就通过getOuterHTML方法从用户提供的el选项获取模板,getOuterHTML逻辑如下

  function getOuterHTML (el) {
    if (el.outerHTML) {
      return el.outerHTML
    } else {
      var container = document.createElement('div');
      container.appendChild(el.cloneNode(true));
      return container.innerHTML
    }
  }

上面逻辑就不说了,相信大家都能看懂,然后有了template,怎么去解析呢?
5.解析模板

    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile');
      }

      var ref = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines: shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this);
      var render = ref.render;
      var staticRenderFns = ref.staticRenderFns;
      options.render = render;
      options.staticRenderFns = staticRenderFns;

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end');
        measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
      }
    }

这里重点是在compileToFunctions,这里我只简单提一下,因为关于模板编译需要单独抽出来,单独写一章了,这里compileToFunctions主要是把模板编译成渲染函数并设置到this.$options上,而模板编译,总共有三部分组成,分别是解析器优化器代码生成器解析器主要是将模板解析成AST优化器主要是遍历AST标记静态节点,这样再虚拟DOM中更新节点时,就不会重新渲染它了,而代码生成器就是把AST转化为代码字符串,从而转换成渲染函数。

讲到这里就是完整版vm.$mount的原理所有逻辑,下面讲下运行时的逻辑。

只包含运行时版本的vm.$mount的原理

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

1.首先将id转换为DOM元素,这里之前有讲过,在这不做讲解
2.mountComponent函数,它的作用就是将vue实例绑定到DOM元素上,并实现持续性,也就是说每当数据变化,依然可以渲染到指定的DOM元素中。

function mountComponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        );
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        );
      }
    }
  }
  callHook(vm, 'beforeMount');

  var updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = function () {
      var name = vm._name;
      var id = vm._uid;
      var startTag = "vue-perf-start:" + id;
      var endTag = "vue-perf-end:" + id;

      mark(startTag);
      var vnode = vm._render();
      mark(endTag);
      measure(("vue " + name + " render"), startTag, endTag);

      mark(startTag);
      vm._update(vnode, hydrating);
      mark(endTag);
      measure(("vue " + name + " patch"), startTag, endTag);
    };
  } else {
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  hydrating = false;

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, 'mounted');
  }
  return vm
}

首先判断是否存在渲染函数,如果不存在,则判断template,这时的template必须是id选择器,因为运行时不包含编译阶段,只能是dom元素,如果是生产环境,则生成一个默认的渲染函数,也就是返回一个注释类型的VNode节点,并发出警告~
然后触发beforeMount生命周期钩子,钩子出发后则真正的开始执行挂载,因为是持续性挂载,所以重点在于watcher的使用,需要注意的是这句代码

 vm._update(vm._render(), hydrating);

_update作用是调用虚拟DOM的patch方法进行新旧对比,而_render则是生成一个新的VNode节点数,那么vm._update(vm._render(), hydrating);的作用就很明显了,就是将新的VNode旧的VNode进行对比并更新DOM,简单的说就是执行了渲染操作~

okok,到这里咱们算真正的分享完了,有什么不对的地方,非常欢迎大家指教哦~谢谢大家

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值