@TOC
一、前置工作
1. 获取Vue源码
项目地址:github.com/vuejs/vue
迁出项目: git clone github.com/vuejs/vue.g…
当前版本号: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这个配置文件中一步一步的查找,下面是我自己画的一张思维导图:
解析说明:
- 根据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
},
复制代码
- 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)
}
}
复制代码
- 进入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')
}
复制代码
- 所以,我们的入口文件的路径为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初始化过程中各个核心函数方法分别做了哪些事,及各个核心函数方法源码所在的文件路径。
下面我们进行源码解析。
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选项,我们发现:
- options中关于reader()、template、el的优先级是:reader() -> template -> el
- 如果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()方法主要任务:
- 合并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()
主要任务:
- 对$data和$props做响应式
- 定义全局方法$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。