Vue源码学习记录之手写vm.$mount方法

目录
  • 一、概述
  • 二、使用方式
  • 三、完整版vm.$mount的实现原理
  • 四、只包含运行时版本的vm.$mount的实现原理

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

一、概述

在我们开发中,经常要用到Vue.extend创建出Vue的子类来构造函数,通过new 得到子类的实例,然后通过$mount挂载到节点,如代码:

<div id="mount-point"></div>
<!-- 创建构造器 -->
var Profile = Vue.extend({
 template:'<p>{{firstName}} {{lastName}} aka{{alias}}</p>',
 data:function(){
  return{
   firstName:'Walter',
   lastName:'White',
   alias:'Heisenberg'
  }
 }
})
<!-- 创建Profile实例,并挂载到一个元素上 -->
new Profile().$mount('#mount-point');

$mount方法是怎么实现的,篇文章就来讲一下

二、使用方式

vm.$mount( [elementOrSelector] )

(1)参数

{ Element | string } [elementOrSelector]

(2)返回值

  vm,即实例本身。

(3)用法

1、如果Vue.js实例在实例化时没有收到el选项,则它处于“未挂载”状态,没有关联的DOM元素。

2、可以使用vm.$mount手动挂载一个未挂载的实例。

3、如果没有提供elementOrSelector参数,模板将被渲染为文档之外的元素,并且必须使用原生DOM的API把它插入文档中。

4、这个方法返回实例自身,因而可以链式调用其他实例方法。

(4)例子

var MyComponent = Vue.extend({
 template:'<div>Hello!</div>',
})
<!-- 创建并挂载到#app(会替换#app) -->
new MyComponent().$mount('#app');
<!-- 创建并挂载到#app(会替换#app) -->
new MyComponent().$mount({el:'#app'});
<!-- 创建并挂载到#app(会替换#app) -->
var component = new MyComponent().$mount();
document.getElementById('app').appendChild(component.$el);

1、在不同的构建版本中,vm.$mount的表现都不一样。其差异主要体现在完整版(vue.js)和只包含运行时版本(vue.runtime.js)之间。

2、完整版和只包含运行时版本之间的差异在于是否有编译器,而是否有编译器的差异主要在于vm.$mount方法的表现形式。

3、在只包含运行时的构建版本中,vm.mount的作用会稍有不同,它首先会检查template或el选项所提供的模板是否已经转换成渲染函数(render函数)。如果没有,则立即进入编译过程,将模板编译成渲染函数,完成之后再进入挂载与渲染的流程中。

4、只包含运行时版本的vm.$mount没有编译步骤,它会默认实例上已经存在渲染函数,如果不存在,则会设置一个。并且,这个渲染函数在执行时会返回一个空节点的VNode,以保证执行时不会因为函数不存在而报错。同时如果是开发环境下运行,Vue.js会触发警告,提示我们当前使用的是只包含运行时的版本,会让我们提供渲染函数,或者去使用完整的构建版本。

5、从原理的角度来讲,完整版和只包含运行时版本之间是包含关系,完整版包含只包含运行时版本。

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

(1)实现代码

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
 <!-- 做些什么 -->
 return mount.call(this,el);
}

1、将Vue原型上的$mount方法保存在mount中,以便后续使用。

2、然后Vue原型上的$mount方法被一个新的方法覆盖了。新方法中会调用原始的方法,这种做法通常被称为函数劫持。(看源码的同学可能发现了,vue多处用了函数劫持的做法,例如:对数组实现监听的时候...)

3、通过函数劫持,可以在原始功能上新增一些其他功能。上面代码中,vm.$mount的原始方法就是mount的核心功能,而在完整版中需要将编译功能新增到核心功能上去。

(2)由于el参数支持元素类型或者字符串类型的选择器,所以第一步是通过el获取DOM元素。

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
    el = el && query(el);
    return mount.call(this,el);
}

使用query获取DOM元素

function query(el){
    if(typeof el === 'string'){
        const selected = document.querySelector(el);
        if(!selected){
            return document.createElement('div');
        }
        return selected;
    }else{
        return el;
    }
}

1、如果el是字符串,则使用doucment.querySelector获取DOM元素,如果获取不到,则创建一个空的div元素。

2、如果el不是字符串,那么认为它是元素类型,直接返回el(如果执行vm.$mount方法时没有传递el参数,则返回undefined)

(3)编译器

1、首先判断Vue.js实例中是否存在渲染函数,只有不存在时,才会将模板编译成渲染函数。

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
 el = el && query(el);
 const options = this.$options;
 if(!options.render){
  <!-- 将模板编译成渲染函数并赋值给options.render -->
 }
    return mount.call(this,el);
}

2、在实例化Vue.js时,会有一个初始化流程,其中会向Vue.js实例上新增一些方法,这里的this.$options就是其中之一,它可以访问到实例化Vue.js时用户设置的一些参数,例如tempalte和render。

3、如果在实例化Vue.js时给出了render选项,那么template其实是无效的,因为不会进入模板编译的流程,而是直接使用render选项中提供的渲染函数。

4、Vue.js在官方文档的template选项中也给出了相应的提示。如果没有render选项,那么需要获取模板并将模板编译成渲染函数(render函数)赋值给render选项。

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
 el = el && query(el);
 const options = this.$options;
 if(!options.render){
  <!-- 新增获取模板相关逻辑 -->
  let template = options.template;
  if(template){

  }else if(el){
   template = getOuterHTML(el);
  }
 }
    return mount.call(this,el);
}

5、从选项中取出template选项,也就是取出用户实例化Vue.js时设置的模板。如果没有取到,说明用户没有设置tempalte选项。那么使用getOuterHTML方法从用户提供的el选项中获取模板。

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

6、getOuterHTML方法会返回参数中提供的DOM元素的HTML字符串。

7、整体逻辑

如果用户没有通过template选项设置模板,那么会从el选项中获取HTML字符串当作模板。如果用户提供了template选项,那么需要对它进一步解析,因为这个选项支持很多种使用方式。template选项可以直接设置成字符串模板,也可以设置为以#开头的选择符,还可以设置成DOM元素。

8、从不同的格式中将模板解析出来

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
 el = el && query(el);
 const options = this.$options;
 if(!options.render){
  <!-- 新增获取模板相关逻辑 -->
  let template = options.template;
  if(template){
   if(typeof tempalte === 'string'){
    if(tempalte.charAt(0) === "#"){
     template = idToTemplate(tempalte);
    }
   }else if(tempalte.nodeType){
    template = template.innerHTML;
   }else{
    if(process.env.NODE_ENV !== 'production'){
     warn('invalid template option:'+tempalte,this);
    }
    return this;
   }
  }else if(el){
   template = getOuterHTML(el);
  }
 }
    return mount.call(this,el);
}

9、如果tempalte是字符串并且以#开头,则它将被用作选择符。通过选择符获取DOM元素后,会使用innerHTML作为模板。

10、使用idToTemplate方法从选择符中获取模板。idToTemplate使用选择符获取DOM元素之后,将它的innerHTML作为模板。

function idToTemplate(id){
 const el = query(id);
 return el && el.innerHTML;
}

11、如果template是字符串,但不是以#开头,就说明template是用户设置的模板,不需要进行任何处理,直接使用即可。

12、如果template选项的类型不是字符串,则判断它是否是一个DOM元素,如果是,则使用DOM元素的innerHTML作为模板。如果不是,只需要判断它是否具备nodeType属性即可。

13、如果tempalte选项既不是字符串,也不是DOM元素,那么Vue.js会触发警告,提示用户template选项是无效的。

14、获取模板之后,下一步是将模板编译成渲染函数,通过执行compileToFunctions函数可以将模板编译成渲染函数并设置到this.options上。

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function(el){
 el = el && query(el);
 const options = this.$options;
 if(!options.render){
  <!-- 新增获取模板相关逻辑 -->
  let template = options.template;
  if(template){
   if(typeof tempalte === 'string'){
    if(tempalte.charAt(0) === "#"){
     template = idToTemplate(tempalte);
    }
   }else if(tempalte.nodeType){
    template = template.innerHTML;
   }else{
    if(process.env.NODE_ENV !== 'production'){
     warn('invalid template option:'+tempalte,this);
    }
    return this;
   }
  }else if(el){
   template = getOuterHTML(el);
  }
  <!-- 新增编译相关逻辑 -->
  if(tempalte){
   const { render } = compileToFunctions(
    template,
    {...},
    this
   )
   options.render = render;
  }
 }
    return mount.call(this,el);
}

15、将模板编译成代码字符串并将代码字符串转换成渲染函数的过程是在compileToFunctions函数中完成的,其内部实现如下

function compileToFunctions(template,options,vm){
 options = extend({},options);
 <!-- 检查缓存 -->
 const key = options.delimiters
 ? String(options.delimiters)+tempalte
 :template;
 if(cache[key]){
  return cache[key];
 }
 <!-- 编译 -->
 const compiled = compile(template,options);
 <!-- 将代码字符串转换为函数 -->
 const res = {};
 res.render = createFunction(compiled.render);
 return (cache[key] = res)
}
function createFunction(code){
 return new Function(code);
}

1)首先,将options属性混合到空对象中,其目的是让options称为可选参数。

2)检查缓存中是否已经存在编译后的模板。如果模板已经被编译,就会直接返回缓存中的结果,不会重复编译,保证不做无用功来提升性能。

3)调用compile函数来编译模板,将模板编译成代码字符串并存储在compiled中的render属性中。

4)调用createFunction函数将代码字符串转换成函数。其实现原理箱单简单,使用new Function(code)就可以完成。

5)在代码字符串被new Function(code)转换成函数之后,当调用函数时,代码字符串会被执行。例如

const code = 'console.log("Hello Berwin")';
const render = new Function(code);
render();//Hello Berwin

6)最后,将渲染函数返回给调用方。

16、当通过compileToFunctions函数得到渲染函数之后,将渲染函数设置到this.$options上。

四、只包含运行时版本的vm.$mount的实现原理

(1)只包含运行时版本的vm.mount方法的核心功能。实现如下

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

1、$mount方法将ID转换为DOM元素后,使用mountComponent函数将Vue.js实例挂载到DOM元素上。

2、将实例挂载到DOM元素上指的是将模板渲染到指定的DOM元素中,而且是持续性的,以后当数据(状态)发生变化时,依然可以渲染到指定的DOM元素中。

3、实现这个功能需要开启watcher。

watcher将持续观察模板中用到的所有数据(状态),当这些数据(状态)被修改时它将得到通知,从而进行渲染操作。这个过程回持续到实例被销毁。

export function mountComponent(vm,el){
 if(!vm.$options.render){
  vm.$options.render = createEmptyVNode;
  if(process.env.NODE_ENV !== 'production'){
   <!-- 在开发环境发出警告 -->
  }
 }
}

4、mountComponent方法会判断实例上是否存在渲染函数。如果不存在,则设置一个默认的渲染函数createEmptyVNode,该渲染函数执行后,会返回一个注释类型的VNode节点。

5、事实上,如果在mountComponent方法中发现实例上没有渲染函数,则会将el参数指定页面中的元素节点替换成一个注释节点,并且在开发环境下在浏览器的控制台中给出警告。

(2)Vue.js实例在不同的阶段会触发不同的生命周期钩子,在挂载实例之前会触发beforeMount钩子函数。

export function mountComponent(vm,el){
 if(!vm.$options.render){
  vm.$options.render = createEmptyVNode;
  if(process.env.NODE_ENV !== 'production'){
   <!-- 在开发环境发出警告 -->
  }
  callHook(vm,'beforeMount')
 }
}

1、钩子函数触发后,将执行真正的挂载操作。挂载操作与渲染类似,不同的是渲染指的是渲染一次,而挂载指的是持续性渲染。挂载之后,每当状态发生变化时,都会进行渲染操作。

(3)mountComponent具体实现

export function mountComponent(vm,el){
 if(!vm.$options.render){
  vm.$options.render = createEmptyVNode;
  if(process.env.NODE_ENV !== 'production'){
   <!-- 在开发环境发出警告 -->
  }
  <!-- 触发生命周期钩子 -->
  callHook(vm,'beforeMount');
  <!-- 挂载 -->
  vm._watcher = new Watcher(vm,()=>{
   vm._update(vm._render())
  },noop);
  <!-- 触发生命周期钩子 -->
  callHook(vm,'mounted');
  return vm;
 }
}

1、vm._update作用:调用虚拟DOM中的patch方法来执行节点的比对与渲染操作。

2、vm._render作用:执行渲染函数,得到一份新的VNode节点树。

3、vm._update(vm._render())作用:先调用渲染函数得到一份最新的VNode节点树,然后通过vm._update方法对最新的VNode和上一次渲染用到的旧VNode进行对比并更新DOM节点。简单来说,就是执行了渲染操作。

(4)挂载是持续性的,而持续性的关键就在于new Watcher这行代码。

1、Watcher的第二个参数支持函数,并且当它是函数时,会同时观察函数中所读取的所有Vue.js实例上的响应式数据。

2、当watcher执行函数时,函数中所读取的数据都将会触发getter去全局找到watcher并将其收集到函数的依赖列表中。即,函数中读取的所有数据都将被watcher观察。这些数据中的任何一个发生变化时,watcher都将得到通知。

3、当数据发生变化时,watcher会一次又一次地执行函数进入渲染流程,如此反复,这个过程会持续到实例被销毁。

4、挂载完毕后,会触发mounted钩子函数。

如果不懂watcher,其实可以去掉看,就简单很多

export function mountComponent(vm,el){
 if(!vm.$options.render){
  vm.$options.render = createEmptyVNode;
  if(process.env.NODE_ENV !== 'production'){
   <!-- 在开发环境发出警告 -->
  }
  <!-- 触发生命周期钩子 -->
  callHook(vm,'beforeMount');
  <!-- 挂载 -->

   vm._update(vm._render())

  <!-- 触发生命周期钩子 -->
  callHook(vm,'mounted');
  return vm;
 }
}

这样,是不是很容易理解了。整个mountComponent,一句关键代码:vm._update(vm._render()),表示通过执行vm._render()得到VNode,再把VNode传入vm._update()vm._update()得功能是 将传入的VNode 变成 真实Dom渲染到页面。

简单地总结一下:

$mount()的思路就是, 判断 用户传入的option有没有render函数,

1.有的话就走运行时版本,

2.没有的话就自动生成render函数,然后在执行运行时版本(其实这就是编译时版本,比运行时版本多了异步生成render函数的步骤)。

执行运行时版本的时候,

通过render()获得Vnode把Vnode传入_update() 实现渲染

到此这篇关于Vue源码学习记录之手写vm.$mount方法 的文章就介绍到这了,更多相关vue vm.$mount方法 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 每天学点Vue源码之vm.$mount挂载函数

    在vue实例中,通过$mount()实现实例的挂载,下面来分析一下$mount()函数都实现了什么功能. $mount函数执行位置 _init这个私有方法是在执行initMixin时候绑定到Vue原型上的. $mount函数是如如何把组件挂在到指定元素 $mount函数定义位置 $mount函数定义位置有两个: 第一个是在src/platforms/web/runtime/index.js 这里的$mount是一个public mount method.之所以这么说是因为Vue有很多构建版本,

  • vue中的.$mount('#app')手动挂载操作

    在Vue构造函数时,需要配置一个el属性,如果没有没有el属性时,可以使用.$mount('#app')进行挂载. 配置了el属性: new Vue({ el:"#app", router }); 如果没有配置el属性,可以使用手动挂载$mount("#app") new Vue({ router }).$mount('#app'); var vm = new Vue({ router }); vm.$mount('#app'); 补充知识:Vue手动挂载组件$mo

  • Vue $mount实战之实现消息弹窗组件

    之前的项目一直在使用Element-UI框架,element中的Notification.Message组件使用时不需要在html写标签,而是使用js调用.那时就很疑惑,为什么element ui使用this.$notify.this.$message就可以实现这样的功能? 1.实现消息弹窗组件的几个问题 如何在任何组件中使用this.$message就可以显示消息? 如何将消息的dom节点插入到body中? 同时出现多个消息弹窗时,消息弹窗的z-index如何控制? 2.效果预览 3.代码实现

  • 解析vue中的$mount

    本文主要是带领大家分析$mount. $mount所做的工作从大体来讲主要分为3步: 1.如果你的option里面没有 render 函数,那么,通过 compileToFunctions 将HTML模板编译成可以生成VNode的Render函数. 2.new 一个 Watcher 实例,触发 updateComponent 方法. 3.生成vnode,经过patch,把vnode更新到dom上. 由于篇幅有限,这里先说前两步,第三步下篇说. 好,下面具体的说.首先,我们来到 $mount 函数

  • vue $mount 和 el的区别说明

    两者在使用效果上没有任何区别,都是为了将实例化后的vue挂载到指定的dom元素中. 如果在实例化vue的时候指定el,则该vue将会渲染在此el对应的dom中,反之,若没有指定el,则vue实例会处于一种"未挂载"的状态,此时可以通过$mount来手动执行挂载. 注:如果$mount没有提供参数,模板将被渲染为文档之外的的元素,并且你必须使用原生DOM API把它插入文档中. 例如: var MyComponent = Vue.extend({ template: '<div&g

  • Vue源码学习记录之手写vm.$mount方法

    目录 一.概述 二.使用方式 三.完整版vm.$mount的实现原理 四.只包含运行时版本的vm.$mount的实现原理 这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.概述 在我们开发中,经常要用到Vue.extend创建出Vue的子类来构造函数,通过new 得到子类的实例,然后通过$mount挂载到节点,如代码: <div id="mount-point"></div> <!-- 创建构造器 --> var Profile =

  • Vue源码学习之响应式是如何实现的

    目录 前言 一.一个响应式系统的关键要素 1.如何监听数据变化 2.如何进行依赖收集--实现 Dep 类 3.数据变化时如何更新--实现 Watcher 类 二.虚拟 DOM 和 diff 1.虚拟 DOM 是什么? 2.diff 算法--新旧节点对比 三.nextTick 四.总结 前言 作为前端开发,我们的日常工作就是将数据渲染到页面➕处理用户交互.在 Vue 中,数据变化时页面会重新渲染,比如我们在页面上显示一个数字,旁边有一个点击按钮,每次点击一下按钮,页面上所显示的数字会加一,这要怎么

  • Vue源码学习之数据初始化

    目录 初始化数据 创建Vue实例 构造函数扩展方法 初始化状态 调用initData方法对数据进行代理 初始化数据 环境搭建:菜鸟学Vue源码第一步之rollup环境搭建步 响应式数据的核心就是,数据变化了可以监听到数据变化了,数据的取值和更改值可以监测到,首先第一步需要创建一个Vue实例 创建Vue实例 //dist/index.html //用Vue创造一个实例 const vm = new Vue({ data(){ return { name:'i东东', age:18 } } }) 创

  • vue源码学习之Object.defineProperty对象属性监听

    本文介绍了vue源码学习之Object.defineProperty对象属性监听,分享给大家,具体如下: 参考版本 vue源码版本:0.11 相关 vue实现双向数据绑定的关键是 Object.defineProperty ,让我们先来看下这个函数. 在MDN上查看有关Object.defineProperty的解释. 我们先从最简单的开始: let a = {'b': 1}; Object.defineProperty(a, 'b', { enumerable: false, configur

  • 详解Vue源码学习之双向绑定

    原理 当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter.Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器. 上面那段话是Vue官方文档中截取的,可以看到是使用Object.defineProperty实现对数据改变的监听.Vue主要使用了观

  • Vue源码学习之关于对Array的数据侦听实现

    摘要 我们都知道Vue的响应式是通过Object.defineProperty来进行数据劫持.但是那是针对Object类型可以实现, 如果是数组呢? 通过set/get方式是不行的. 但是Vue作者使用了一个方式来实现Array类型的监测: 拦截器. 核心思想 通过创建一个拦截器来覆盖数组本身的原型对象Array.prototype. 拦截器 通过查看Vue源码路径vue/src/core/observer/array.js. /** * Vue对数组的变化侦测 * 思想: 通过一个拦截器来覆盖

  • Vue源码学习之初始化模块init.js解析

    我们看到了VUE分了很多模块(initMixin()stateMixin()eventsMixin()lifecycleMixin()renderMixin()),通过使用Mixin模式,都是使用了JavaScript原型继承的原理,在Vue的原型上面增加属性和方法.我们继续跟着this._init(options)走,这个一点击进去就知道了是进入了init.js文件是在initMixin函数里面给Vue原型添加的_init方法.首先来从宏观看看这个init文件,可以看出主要是导出了两个函数:i

  • 详解Vue源码学习之callHook钩子函数

    Vue实例在不同的生命周期阶段,都调用了callHook方法.比如在实例初始化(_init)的时候调用callHook(vm, 'beforeCreate')和callHook(vm, 'created'). 这里的"beforeCreate","created"状态并非随意定义,而是来自于Vue内部的定义的生命周期钩子. var LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mount

  • vue源码学习之Object.defineProperty 对数组监听

    上一篇中,我们介绍了一下defineProperty 对对象的监听,这一篇我们看下defineProperty 对数组的监听 数组的变化 先让我们了解下Object.defineProperty()对数组变化的跟踪情况: var a={}; bValue=1; Object.defineProperty(a,"b",{ set:function(value){ bValue=value; console.log("setted"); }, get:function(

  • 手写Vue源码之数据劫持示例详解

    源代码: 传送门 Vue会对我们在data中传入的数据进行拦截: 对象:递归的为对象的每个属性都设置get/set方法 数组:修改数组的原型方法,对于会修改原数组的方法进行了重写 在用户为data中的对象设置值.修改值以及调用修改原数组的方法时,都可以添加一些逻辑来进行处理,实现数据更新页面也同时更新. Vue中的响应式(reactive): 对对象属性或数组方法进行了拦截,在属性或数组更新时可以同时自动地更新视图.在代码中被观测过的数据具有响应性 创建Vue实例 我们先让代码实现下面的功能:

随机推荐