vue2源码解析(一) - new Vue()的初始化过程

一、前置工作

1. 获取Vue源码

项目地址:https://github.com/vuejs/vue
迁出项目: git clone https://github.com/vuejs/vue.git
当前版本号:2.6.11

2. Vue源码项目文件结构

2.1 项目根目录结构说明

根目录

2.2 核心代码目录说明

核心代码目录

3. 调试环境搭建

1)安装依赖: npm i
2)若速度较慢,安装phantom.js时即可终止
3)安装rollup: npm i -g rollup
4)修改package.json配置文件中的dev脚本,添加sourcemap

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

5)运行开发命令: npm run dev
运行成功提示

6)根据第4步的配置,运行成功后会在dist目录下生成一个映射文件,方便我们写测试用例时在浏览器调试。
生成映射文件

二、寻找项目运行入口文件

为了让我们能更清晰的理解Vue的工作机制和初始化流程,我们需先找到程序的入口文件。
我们可以从package.json这个配置文件中一步一步的查找,下面是我自己画的一张思维导图:
寻找项目入口文件思维导图
解析说明:

  1. 根据package.json文件的dev配置项,找到scripts/config.js文件中的web-full-dev配置项:
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev",
'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
},
  1. web/entry-runtime-with-compiler.js为项目打包的入口文件路径,其中web为别名,我们需要找到web对应真实路径。进入resolve()方法:
const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}
  1. 进入scripts/alias.js文件,我们找到了web对应的路径是src/platforms/web
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')
}
  1. 所以,我们的入口文件的路径为src/platforms/web/entry-runtime-with-compiler.js
    注意;此文件是带编译器的版本,是为了方便我们更清晰的了解整个Vue工作机制,在我们日常工作中使用的webpack是不带编译器的版本,而是通过额外注入的vue-loader实现的

三、new Vue()的初始化过程解析

1. 思维导图

先简单说一下,new Vue()初始化的执行过程:
new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()

下面这张是我根据源码画出的思维导图:
Vue初始化思维导图
思维导图中,逐步列出了Vue初始化过程中各个核心函数方法分别做了哪些事,及各个核心函数方法源码所在的文件路径。
下面我们进行源码解析。

2. 源码解析

2.1 扩展$mount()方法

入口文件src/platforms/web/entry-runtime-with-compiler.js中,实现了对$mount()的扩展。

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

  /* istanbul ignore if */
  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
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          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) {
        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')
      }

      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')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

扩展方法主要是实现对我们new Vue()创建实例时,处理传入的options中可能存在的template或el选项,我们发现:

  1. options中关于reader()、template、el的优先级是:reader() -> template -> el
  2. 如果reader()不存在,则会将template或el转化成html模板字符串,再转化成reader()

例子:

// render > template > el
// 创建实例
const app = new Vue({
    el: '#demo',
    // template: '<div>template</div>',
    // template: '#app',
    // render(h){return h('div','render')},
    data:{foo:'foo'}
})

但是,我们没有找到new Vue()的构造函数方法,那么Vue的构造函数在哪呢?通过文件头部的

import Vue from ‘./runtime/index’

发现Vue是从这里引入的,我们进入该文件看看。

2.2 src/platforms/web/runtime/index.js

查看源码发现,该文件主要做了两件事:

2.2.1 定义__patch__方法。
Vue.prototype.__patch__ = inBrowser ? patch : noop

记住,该方法就是Vue中将虚拟dom(vnode)生成真实dom的方法。

关于vnode会在后续文章中讲到。

2.2.2 实现$mount()。
Vue.prototype.$mount = function (...)

在$mount()中会调用mountComponent()方法(该方法是在src/core/instance/lifecycle.js中定义的)。

在mountComponent()中会创建一个Watcher,由此可知,每当我们new Vue()创建Vue组件实例时,都会新建一个Watcher。特别提一下,组件对应reader Watcher,一个组件只有一个;当用户使用computed、watch和$watch时,有几个属性就有几个user Watcher。Watcher与Dep的关系是多对多的关系。

这里提出一个问题:$mount()在何时调用?这个问题我们会在下面源码中找出答案。

但是,我们还没有找到new Vue()的构造函数方法,那么Vue的构造函数在哪呢?通过文件头部的

import Vue from ‘core/index’

发现Vue是从这里引入的,我们进入该文件看看。

2.3 src/core/index.js

查看源码发现,该文件主要做了两件事:

2.3.1 初始化全局API
initGlobalAPI(Vue)
2.3.2 配置ssr服务器渲染等
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

这部分我们暂不研究,不是我们这次的核心内容。知道这个文件做了这两件事就可以了。

通过文件头部的

import Vue from ‘./instance/index’

发现Vue是从这里引入的,我们进入该文件看看。

2.4 Vue的初始化

2.4.1 Vue的初始化构造函数方法

src/core/instance/index.js这里我们终于找到了Vue的初始化构造函数方法_init()。

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)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

实际上,_init()方法是定义在下方的initMixin(Vue)中。我们依次进入下面的5个方法看看它分别做了哪些事。

2.4.2 初始化相关选项
2.4.2.1. initMixin()定义_init()方法

_init()方法主要任务:

  1. 合并new Vue()中的options
    合并全局组件transition、transitionGroup、keepAlive和自定义组件;合并过滤器filters、自定义指令directive等。
  2. initLifecycle(vm)
    初始化$parent、$root、$children、$refs等。
  3. initEvents(vm)
    初始化事件监听。组件间的事件都是由自身分发和监听,所以这里子组件会接收父组件的事件监听器进行处理。
  4. initRender(vm)
    初始化$solt、$scopedSolts;定义vm.$createElement(),该方法就是reader()的参数h;定义$arrts和$listeners的响应式。
  5. callHook(vm, ‘beforeCreate’)
    执行钩子函数beforeCreate()。由以上可知,此钩子只能访问以上属性和方法,不能访问props、methods、data、computed、watch。
  6. initState(vm)
    依次初始化options中的props、methods、data、computed、watch,对数据做响应式处理。具体的响应式处理,请看上面的思维导图,里面有相对详细的介绍说明。
  7. 当Vue()的options存在el时会调用vm.$mount()。
  8. callHook(vm, ‘created’)
    执行钩子函数created()。此钩子能访问props、methods、data、computed、watch。
2.4.2.2 stateMixin()

主要任务:

  1. 对$data和$props做响应式
  2. 定义全局方法$set()、$delete()、$watch()。这3个方法的具体实现是在src/core/observer/index.js中实现的。
2.4.2.3 eventMixin()定义事件监听方法

$on()、$onco()、$off()、$emit()。

2.4.2.4 lifecycleMixin()定义生命周期钩子函数

_update()、$forceUpdate()强制更新、$destory()

2.4.2.5 readerMixin()

定义_reader()(作用:获取vdom),$nextTick()

四、总结

1. new Vue()初始化的执行过程

new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()

2. _reader()和__patch__的作用

1)_reader()获取VNode
2)__patch__初始化和更新,将VNode转化为真实dom

3. Dep与Watcher的关系

1)每个响应式对象及它的key都会有一个Dep;
2)每个组件vm组件实例都会有一个reader Watcher,在用户使用computed、watch或$watch监听属性时也会对应的创建user Watcher;
3)Dep与Watcher的关系是多对多的关系,它们的关联操作是在Dep类的addDep()方法中进行的。

4. $mount()的调用

在new Vue()时,如果不手动调用$mount(),若options中存在el,会在初始化方法_init()中自动调用。该方法的作用是生成真实dom,渲染页面。

5. 使用生命周期钩子函数的注意事项

1)beforeCreate()不能访问props、methods、data、computed、watch。
2)created()能访问props、methods、data、computed、watch。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值