项目地址:https://github.com/vuejs/vue
迁出项目: git clone https://github.com/vuejs/vue.git
当前版本号:2.6.11
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",
6)根据第4步的配置,运行成功后会在dist目录下生成一个映射文件,方便我们写测试用例时在浏览器调试。
为了让我们能更清晰的理解Vue的工作机制和初始化流程,我们需先找到程序的入口文件。
我们可以从package.json这个配置文件中一步一步的查找,下面是我自己画的一张思维导图:
解析说明:
"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
},
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)
}
}
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')
}
先简单说一下,new Vue()初始化的执行过程:
new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()
下面这张是我根据源码画出的思维导图:
思维导图中,逐步列出了Vue初始化过程中各个核心函数方法分别做了哪些事,及各个核心函数方法源码所在的文件路径。
下面我们进行源码解析。
在入口文件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 or - 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选项,我们发现:
例子:
// render > template > el
// 创建实例
const app = new Vue({
el: '#demo',
// template: 'template',
// template: '#app',
// render(h){return h('div','render')},
data:{foo:'foo'}
})
但是,我们没有找到new Vue()的构造函数方法,那么Vue的构造函数在哪呢?通过文件头部的
import Vue from ‘./runtime/index’
发现Vue是从这里引入的,我们进入该文件看看。
查看源码发现,该文件主要做了两件事:
Vue.prototype.__patch__ = inBrowser ? patch : noop
记住,该方法就是Vue中将虚拟dom(vnode)生成真实dom的方法。
关于vnode会在后续文章中讲到。
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是从这里引入的,我们进入该文件看看。
查看源码发现,该文件主要做了两件事:
initGlobalAPI(Vue)
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是从这里引入的,我们进入该文件看看。
在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个方法看看它分别做了哪些事。
_init()方法主要任务:
主要任务:
$on()、$onco()、$off()、$emit()。
_update()、$forceUpdate()强制更新、$destory()
定义_reader()(作用:获取vdom),$nextTick()
new Vue() => _init() => $mount() => mountComponent() =>
new Watcher() => updateComponent() => render() => _update()
1)_reader()获取VNode
2)__patch__初始化和更新,将VNode转化为真实dom
1)每个响应式对象及它的key都会有一个Dep;
2)每个组件vm组件实例都会有一个reader Watcher,在用户使用computed、watch或$watch监听属性时也会对应的创建user Watcher;
3)Dep与Watcher的关系是多对多的关系,它们的关联操作是在Dep类的addDep()方法中进行的。
在new Vue()时,如果不手动调用$mount(),若options中存在el,会在初始化方法_init()中自动调用。该方法的作用是生成真实dom,渲染页面。
1)beforeCreate()不能访问props、methods、data、computed、watch。
2)created()能访问props、methods、data、computed、watch。