八、Vue源码解读-Vue核心内容精讲

一、Vue源码解析–响应式原理

1、课程目标

  • Vue.js的静态成员和实例成员初始化过程
  • 首次渲染的过程
  • 数据响应式原理

2、准备工作

Vue源码的获取

项目地址:https://github.com/vuejs/vue

为什么分析Vue2.6? 新的版本发布后,现有项目不会升级到3.0,2.x还有很长的一段过渡期。

3.0项目地址https://github.com/vuejs/vue-next

源码目录结构(在src目录下面定义的就是源码内容):

compiler: 编译相关(主要作用:就是把模板转换成render函数,在render函数中创建虚拟DOM)
core:Vue核心库
platforms:平台相关代码,web:基于web的开发,weex是基于移动端的开发
server:SSR,服务端渲染
sfc:将.vue文件编译为js对象
shared:公共的代码

core目录是Vue的核心库,在core目录下面,也定义了很多的文件夹,下面我们先简单来看一下。

components目录下面定义的是keep-alive.js组件。

global-api:定义的是Vue中的静态方法。vue.filter,vue.extend,vue.mixin,vue.use等。

Instance:创建vue的实例,定义了Vue的构造函数,初始化,以及生命周期的钩子函数等。

observer:定义响应式机制的位置,

util:定义公共成员。

vodom:定义虚拟DOM

3、打包

这里我们来介绍一下,关于Vue源码中使用的打包方式。

打包工具Rollup

Vue.js 所使用的打包工具为Rollup,RollupWebpack更加轻量,Webpack是把所有的文件(例如:图片文件,样式等)当作模块进行打包,Rollup只处理js文件,所以Rollup更适合在Vue.js这样的库中进行使用。

Rollup打包不会生成冗余的代码,如果是Webpack打包,那么会生成一些浏览器支持模块化的代码。

以上就是WebpackRollup之间的区别。根据以上的讲解,其实我们可以总结出,Rollup更适合在库的开发中使用,Webpack更适合在项目开发中使用,所以它们各自有自己的应用场景。

下面看一下打包的步骤:

第一步:安装依赖

npm i

第二步设置:sourcemap

sourcemap是代码地图,在sourcemap中记录了打包后的代码与源码之间的对应关系。如果出错了,也会告诉我们源码中的第几行出错了。怎样设置sourcemap呢?在package.json 文件中的dev脚本中添加--sourcemap.

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",

第三步:执行dev,运行npm run dev执行打包,用的是rollup,-w 参数是监听源码文件的变化,源码文件变化后自动的重新进行打包。-c是设置配置文件,scripts/config.js就是配置文件,environment环境变量,通过后面设置的值,来打包生成不同版本的Vue.web-full-dev:web:指的是打包web平台下的,full:表示完整版,包含了编译器与运行时,dev:表示的是开发版本,不会对代码进行压缩。

web-runtime-cjs-dev: runtime:表示运行时,cjs:表示CommonJS模块。

在执行npm run dev进行打包之前,可以先来看一下dist目录,该目录下面已经有很多的js文件,这些文件针对的是不同版本的Vue.那么为了更好的看到,执行npm run dev命令后的打包效果,在这里可以将这些文件先删除掉。

4、Vue不同版本说明

https://cn.vuejs.org/v2/guide/installation.html#对不同构建版本的解释

完整版:同时包含编译器运行时版本。

什么是编译器?用来将模板字符串编译成为javascript渲染函数(render函数,render函数用来生成虚拟DOM)的代码,体积大,效率低。

		什么是运行时?用来创建`Vue`实例,渲染并处理虚拟`DOM`等的代码,体积小,效率高,基本上就是除去编译器的代码。

还有一点需要说明的是:Vue包含了不同的模块化方式。

UMD:指的是通用的模块版本,支持多种模块方式,UMD 版本可以通过 <script> 标签直接用在浏览器中

CommonJSCommonJS版本用来配合老的打包工具比如[Browserify](http://browserify.org/) 或[webpack 1](https://webpack.github.io/)

ES Module:从 2.6 开始 Vue会提供两个 ES Modules (ESM,也是ES6的模块化方式,这时标准的模块化方式,后期会使用该方式替换其它的模块化方式) 构建文件:

  • 为打包工具提供的ESM:为诸如 [webpack 2](https://webpack.js.org/)[Rollup](https://rollupjs.org/) 提供的现代打包工具。ESM 格式被设计为可以被静态分析(在编译的时候进行代码的处理也就是解析模块之间的依赖,而不是运行时),所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件 (pkg.module) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js)。
  • 为浏览器提供的 ESM(2.6+):用于在现代浏览器中通过 <script type="module"> 直接导入。

如果使用vue-cli创建的项目,默认的就是运行时版本,并且使用的是ES6的模块化方式。

同时使用vue-cli创建的项目中,有很多的.vue文件,而这些文件浏览器是不支持的,所以在打包的时候,会将这些单文件转换成js对象,在转换js对象的过程中,会将.vue文件中的template转换成render函数。

所以单文件组件在运行的时候也是不需要编译器的。

5、寻找入口文件

查看vue的源码,就需要找到对应的入口文件。

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",

可以从srcipts/config.js这个配置文件中进行查找。

该配置文件中的内容是比较多的。所以可以看一下文件的底部,底部导出了相应的内容、

如下所示:

//判断环境变量是否有`TARGET`
//如果有的话,使用`genConfig()`生成`rollup`配置文件。
if (process.env.TARGET) {
   
  module.exports = genConfig(process.env.TARGET)
} else {
   
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

下面,我们看一下genConfig方法的代码实现

getConfig方法中有一行代码如下所示:

 const opts = builds[name]

我们看到在builds这个对象中,该对象中的属性就是:环境变量的值。

由于在package.json文件中,关于dev的配置中的环境变量的值为web-full-dev.

所以下面,我们在builds对象中查找该属性对应的内容。

具体内容如下:

 // Runtime+compiler development build (Browser)
  'web-full-dev': {
   
      //表示入口文件,我们查找的就是该文件。
    entry: resolve('web/entry-runtime-with-compiler.js'),
        //出口,打包后的目标文件
    dest: resolve('dist/vue.js'),
        //模块化的方式,这里是umd
    format: 'umd',
        //打包方式,env的取值可以是开发模式或者是生产模式
    env: 'development',
        //别名,这里先不用关系
    alias: {
    he: './entity-decoder' },
        //表示的就是文件的头,打包好的文件的头部信息。
    banner
  },

web-full-dev中定义的就是在打包的时候,需要的一些配置的基本信息。

通过以上代码的注意,我们知道,web-full-dev打包的是完整版,包含了运行时与编译器。

下面我们来看一下,入口文件,入口文件的地址为web/entry-runtime-with-compiler.js, 但是问题是在scripts目录中,我们没有发现web目录,我们进入reslove方法看一下,

const aliases = require('./alias')//导入alias模块
const resolve = p => {
   
    //根据传递过来的参数,安装`/`进行分隔,然后获取第一项内容。
    //很明显这里获取的是 web
  const base = p.split('/')[0]
  //根据获取到的`web`,从aliases中获取一个值,下面看一下aliases中的内容。
  if (aliases[base]) {
   
      //aliases[base]的值:src/platforms/web
      // p的值为:web/entry-runtime-with-compiler.js
      //p.slice(base.length + 1):获取到的就是entry-runtime-with-compiler.js
      //整个返回的内容是:src/platforms/web/entry-runtime-with-compiler.js 的绝对路径并返回
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
   
    return path.resolve(__dirname, '../', p)
  }
}

aliases中的内容定义在scripts/alias.js文件,具体的代码如下:

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
   
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

通过上面的代码,我们可以看到这里是通过path.resolve获取到了当前的绝对路径,并且是在scripts目录的上一级src下面去查找platforms/web目录中的内容。

下面我们继续来看一下genConfig方法。

function genConfig (name) {
   
    //获取到了关于配置的基础信息
  const opts = builds[name]
  //config对象就是所有的配置信息
  const config = {
   
    input: opts.entry,//入口
    external: opts.external,
    plugins: [
      flow(),
      alias(Object.assign({
   }, aliases, opts.alias))
    ].concat(opts.plugins || []),
    output: {
   
      file: opts.dest,//出口
      format: opts.format,
      banner: opts.banner,
      name: opts.moduleName || 'Vue'
    },
    onwarn: (msg, warn) => {
   
      if (!/Circular/.test(msg)) {
   
        warn(msg)
      }
    }
  }

  // built-in vars
  const vars = {
   
    __WEEX__: !!opts.weex,
    __WEEX_VERSION__: weexVersion,
    __VERSION__: version
  }
  // feature flags
  Object.keys(featureFlags).forEach(key => {
   
    vars[`process.env.${
     key}`] = featureFlags[key]
  })
  // build-specific env
  if (opts.env) {
   
    vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)
  }
  config.plugins.push(replace(vars))

  if (opts.transpile !== false) {
   
    config.plugins.push(buble())
  }

  Object.defineProperty(config, '_name', {
   
    enumerable: false,
    value: name
  })
//将配置信息返回
  return config
}

6、从入口开始

通过上一小节的内容,我们已经找到了对应的入口文件src/platform/web/entry-runtime-with-compiler.js

下面我们要对入口文件进行分析,在分析的过程中,我们要解决一个问题,如下代码所示:

const vm=new Vue({
   
  el:'#app',
template:'<h3>hello template</h3>'
render(h){
   
  return h('h4','hello render')
}
})

在上面的代码中,我们在创建Vue的实例的时候,同时指定了templaterender,那么会渲染执行哪个内容?

一会我们通过查看源码来解决这个问题。

下面我们打开入口文件。

先来看一下$mount

//保留Vue实例的$mount方法
const mount = Vue.prototype.$mount;
//$mout:挂载,作用就是把生成的DOM挂载到页面中。
Vue.prototype.$mount = function (
  el?: string | Element,
  //非ssr情况下为false,ssr的时候为true
  hydrating?: boolean
): Component {
   
  //获取el选项,创建vue实例的时候传递过来的选项。
  //el就是DOM对象。
  el = el && query(el);

  /* istanbul ignore if */
  //如果el为body或者是html,并且是开发环境,那么会在浏览器的控制台
  //中输出不能将Vue的实例挂载到<html>或者是<body>标签上
  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.`
      );
    //直接返回vue的实例
    return this;
  }
  //获取options选项
  const options = this.$options;
  // resolve template/el and convert to render function
  //判断options中是否有render(在创建vue实例的时候,也就new Vue的时候是否传递了render函数)
  if (!options.render) {
   
    //没有传递render函数。获取template模板,然后将其转换成render函数
    //关于将`template`转换成render的代码比较多,目录先知道其主要作用就可以了
    let template = options.template;
    if (template) {
   
      if (typeof template === "string") {
   
          //如果是id选择器
        if (template.charAt(0) === "#") {
   
            //获取对应的DOM对象的innerHTML,作为模板
          template = idToTemplate(template);
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== "production" && !template) {
   
            warn(
              `Template element not found or is empty: ${
     options.template}`,
              this
            );
          }
        }
      } else if (template.nodeType) {
   
          //如果模板是元素,返回元素的innerHTML
        template = template.innerHTML;
      } else {
   
          //如果不是字符串,也不是元素,在开发环境中会给出警告信息,模板不合法
        if (process.env.NODE_ENV !== "production") {
   
          warn("invalid template option:" + template, this);
        }
          //返回Vue实例。
        return this;
      }
    } else if (el) {
   
        //如果选项中没有设置template模板,那么获取el的outerHTML 作为模板。
      template = getOuterHTML(el);
    }
    if (template) {
   
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== "production" && config.performance && mark) {
   
        mark("compile");
      }
//把template模板编译成render函数
      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;

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== "production" && config.performance && mark) {
   
        mark("compile end");
        measure(`vue ${
     this._name} compile`, "compile", "compile end");
      }
    }
  }
  //如果创建 Vue实例的时候,传递了render函数,这时会直接调用mount方法。
  // mount方法的作用就是渲染DOM,这块内容在下一小节会讲解到。
  return mount.call(this, el, hydrating);
};

下面代码是query方法实现的代码。

/**
 * Query an element selector if it's not an element already.
 */
export function query(el: string | Element): Element {
   
  // 如果el等于字符串,表明是选择器。
  //否则是DOM对象,直接返回
  if (typeof el === "string") {
   
    //获取对应的DOM元素
    const selected = document.querySelector(el);
    if (!selected) {
   
      //如果没有找到,判断是否为开发模式,如果是开发模式
      //在控制台打印“找不到元素”
      process.env.NODE_ENV !== "production" &&
        warn("Cannot find element: " + el);
      //这时会创建一个`div`元素返回。
      return document.createElement("div");
    }
    //返回找到的dom元素
    return selected;
  } else {
   
    return el;
  }
}

看完上面的代码后,我们就可以回答最开始的时候,提出的问题,如果传递了render函数,是不会处理template这个模板的,直接调用mount方法渲染dom

现在面临的一个问题就是$mount这个方法是在哪儿被调用的呢?

core/instance/init.js文件中,查找到如下代码:

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

通过以上代码,可以看到调用了$mount方法。

也就是在Vue._init方法中调用的。

那么_init方法是在哪被调用的呢?

core/instance/index.js文件中、

function Vue (options) {
   
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
   
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

通过以上的代码,可以看到在Vue这个方法中调用了_init方法,而Vue方法,是在创建Vue实例的时候被调用的。所以上面的Vue方法就是一个构造函数。

现在我们将以上的内容做一个总结,重点是以下三点内容。

  • el不能是body 或者是html标签
  • 如果没有render,把template转换成render函数。
  • 如果有render方法,直接调用mount挂载DOM

7、Vue的初始化过程

在这一小节中,我们需要考虑如下的一个问题。

Vue实例成员和Vue的静态成员是从哪里来的?

src/platforms/web目录下面定义的文件都是与平台有关的文件。

下面我们还是看一下开始文件:entry-runtime-with-compiler.js文件。

/* @flow */

import config from "core/config";
import {
    warn, cached } from "core/util/index";
import {
    mark, measure } from "core/util/perf";
//导入Vue的构造函数
import Vue from "./runtime/index";
import {
    query } from "./util/index";
import {
    compileToFunctions } from "./compiler/index";
import {
   
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref,
} from "./util/compat";

const idToTemplate = cached((id) => {
   
  const el = query(id);
  return el && el.innerHTML;
});
//保留Vue实例的$mount方法,方便下面重写$mount的功能
const mount = Vue.prototype.$mount;
//$mout:挂载,作用就是把生成的DOM挂载到页面中。
Vue.prototype.$mount = function (
  el?: string | Element,
  //非ssr情况下为false,ssr的时候为true
  hydrating?: boolean
): Component {
   
  //获取el选项,创建vue实例的时候传递过来的选项。
  //el就是DOM对象。
  el = el && query(el);

  /* istanbul ignore if */
  //如果el为body或者是html,并且是开发环境,那么会在浏览器的控制台
  //中输出不能将Vue的实例挂载到<html>或者是<body>标签上
  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.`
      );
    //直接返回vue的实例
    return this;
  }
  //获取options选项
  const options = this.$options;
  // resolve template/el and convert to render function
  //判断options中是否有render(在创建vue实例的时候,也就new Vue的时候是否传递了render函数)
  if (!options.render) {
   
    //没有传递render函数。获取template模板,然后将其转换成render函数
    //关于将`template`转换成render的代码比较多,目录先知道其主要作用就可以了
    let template = options.template;
    // 如果模板存在
    if (template) {
   
      //判断对应的类型如果是字符串
      if (typeof template === "string") {
   
        //如果模板是id选择器
        if (template.charAt(0) === "#") {
   
          //获取对应的DOM对象的innerHTML
          template = idToTemplate(template);
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== "production" && !template) {
   
            warn(
              `Template element not found or is empty: ${
     options.template}`,
              this
            );
          }
        }
      } else if (template.nodeType) {
   
        //如果模板是元素,返回元素的innerHTML
        template = template.innerHTML;
      } else {
   
        if (process.env.NODE_ENV !== "production") {
   
          warn("invalid template option:" + template, this);
        }
        return this;
      }
    } else if (el) {
   
      //如果模板不存在
      template = getOuterHTML(el);
    }
    if (template) {
   
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== "production" && config.performance && mark) {
   
        mark("compile");
      }
//把template模板编译成render函数
      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;

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== "production" && config.performance && mark) {
   
        mark("compile end");
        measure(`vue ${
     this._name} compile`, "compile", "compile end");
      }
    }
  }
  //如果创建 Vue实例的时候,传递了render函数,这时会直接调用mount方法。
  // mount方法的作用就是渲染DOM,这里的mount就是下面我们要看的./runtime/index文件中的$mount,只不过
   // 在当前的文件中重写了。
  return mount.call(this, el, hydrating);
};

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML(el: Element): string {
   
  if (el.outerHTML) {
   
    //如果有outerHTML属性,返回内容的HTML形式
    return el.outerHTML;
  } else {
   
    //创建div
    const container = document.createElement("div");
    //把el的内容克隆,然后追加到div中
    container.appendChild(el.cloneNode(true));
    //返回div的innerHTML
    return container.innerHTML;
  }
}
//注册Vue.compile方法,根据HTML字符串返回render函数
Vue.compile = compileToFunctions;

export default Vue;

通过查看该文件中的代码,可以发现,在该文件中并没有创建Vue的实例,关于实例的创建在如下导入的文件中。

//导入Vue的构造函数
import Vue from "./runtime/index";

通过前面的讲解,我们知道在入口文件中,最主要的方法是mount.

//保留Vue实例的$mount方法,方便下面重写$mount的功能
const mount = Vue.prototype.$mount;

在该方法中,很重要的一个操作就是将template模板,转换成render函数。

下面,我们先来看一下./runtime/index文件中的内容。

// install platform specific utils
//给Vue.config注册了方法,这些方法都是与平台相关的方法。这些方法是在Vue内部使用的。

Vue.config.mustUseProp = mustUseProp;
//是否为保留的标签,也就是说,传递过来的内容是否为HTML中特有的标签
Vue.config.isReservedTag = isReservedTag;
//是否是保留的属性,也就是说,传递过来的内容是否为HTML中特有的属性
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;

以上内容简短了解一下就可以。

下面我们继续查看如下内容:

// install platform runtime directives & components
//通过extend方法注册了与平台相关的全局的指令与组件。
//extend的作用就是将第二个参数的成员全部拷贝到第一个参数中
//那么问题是注册了哪些指令与组件呢?
extend(Vue.options.directives, platformDirectives);
extend(Vue.op
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值