在开始之前,阅读源码你需要有扎实的基本功,还要有耐心,能把握全局,不要扣细节!
一、先看看目录
├── build --------------------------------- 构建相关的文件
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放使用Vue开发的的例子
├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/)
├── package.json -------------------------- 项目依赖
├── test ---------------------------------- 包含所有测试文件
├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码
│ ├──platforms --------------------------- 包含平台相关的代码
│ │ ├──web ----------------------------- 包含了不同构建的包的入口文件
│ │ | ├──entry-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意
│ │ | ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器
│ ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
│ │ ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
│ │ ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
│ │ ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
│ ├── core ------------------------------ 存放通用的,平台无关的代码
│ │ ├── observer ---------------------- 反应系统,包含数据观测的核心代码
│ │ ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码
│ │ ├── instance ---------------------- 包含Vue构造函数设计相关的代码
│ │ ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码
│ │ ├── components -------------------- 包含抽象出来的通用组件
│ ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码
│ ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│ ├── shared ---------------------------- 包含整个代码库通用的代码复制代码
二、Vue 的构造函数是什么样的
使用 new
操作符来调用 Vue
,Vue
是一个构造函数,了解了目录结构,下面来看下下入口文件
打开package.json
当我们运行npm run dev,看看干了啥,rollup也是类似webpack的打包工具,根据
TARGET=web-full-dev
打开入口文件找到了web/entry-runtime-with-compiler.js
依照以上查找路径,我们找到了Vue构造函数
定义了构造函数,引入依赖,调用初始化函数,最后导出Vue
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue复制代码
打开这五个文件,找到相应的方法,你会发现,这些方法的作用,就是在 Vue 的原型 prototype 上挂载方法或属性
1. 先进入 initMixin(Vue),在prototype上挂载
Vue.prototype._init = function (options) {}
复制代码
2. 进入 stateMixin(Vue),在prototype上挂载
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
复制代码
3. 进入eventsMixin(Vue),在prototype上挂载
Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
复制代码
4.进入lifecycleMixin(Vue),在prototype上挂载
Vue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroy 复制代码
5. 最后进入renderMixin(Vue),在prototype上挂载
Vue.prototype.$nextTick
Vue.prototype._render
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners
复制代码
根据上面的查找路径下一步到src/core/index.js
引入依赖,在Vue上挂载静态方法和属性
import { initGlobalAPI } from './global-api/index' import { isServerRendering } from 'core/util/env' initGlobalAPI(Vue) Object.defineProperty(Vue.prototype, '$isServer', { get: isServerRendering }) Object.defineProperty(Vue.prototype, '$ssrContext', { get () { /* istanbul ignore next */ return this.$vnode && this.$vnode.ssrContext } }) Vue.version = '__VERSION__' export default Vue复制代码
进入initGlobalAPI(Vue),在Vue上挂载静态属性和方法
Vue.config Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: { KeepAlive },
directives: {},
filters: {},
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
复制代码
接着挂载
Vue.prototype.$isServer
Vue.version = '__VERSION__'
复制代码
根据上面的查找路径下一步到runtime/index.js,安装平台特有的工具
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// 安装平台特定的 指令 和 组件
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount复制代码
根据上面的查找路径最后一步到web/entry-runtime-with-compiler.js
- 缓存来自
web-runtime.js
文件的$mount
函数,const mount = Vue.prototype.$mount - 在 Vue 上挂载
compile
然后覆盖覆盖了Vue.prototype.$mount
- Vue.compile = compileToFunctions 就是将模板
template
编译为render函数。
到这整个Vue构造函数就还原了
三、通过一个例子说明整个流程
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Vue.js grid component example</title>
</head>
<body>
<div id="app">
<ol>
<li v-for="todo in todos">
{{ todo.text }}
</li>
</ol>
<Child></Child>
</div>
</body>
</html>
复制代码
grid.js
let Child = {
data: function() {
return {child: '你好哈'}
},
template: '<div>{{child}}</div>'
}
new Vue({
el: '#app',
data: {
todos: [
{text: '学习 JavaScript'},
{text: '学习 Vue'},
{text: '整个牛项目'}]
},
components: {'Child': Child}
})复制代码
想要知道Vue都干了什么,首先查看构造函数function Vue (options) { this._init(options) }
- new Vue({//传入上面的内容 }) ,首先进入Vue.prototype._init,构造函数第一个挂载的方法
Vue.prototype._init = function (options) {
const vm= this
vm._uid = uid++
let startTag, endTag
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._renderProxy = vm
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
复制代码
_init()
方法在一开始的时候,在 this
对象上定义了两个属性:_uid
和 _isVue
,然后判断有没有定义 options._isComponent
这里会走 else
分支,也就是这段代码:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)mergeOptions使用策略模式合并传入的options和Vue.options复制代码
mergeOptions使用策略模式合并传入的options和Vue.options合并后的代码结构, 可以看到通过合并策略components,directives,filters继承了全局的, 这就是为什么全局注册的可以在任何地方使用,因为每个实例都继承了全局的, 所以都能找到
initLifecycle
、
initEvents
、
initRender、initState
,且在
initState
前后分别回调了生命周期钩子
beforeCreate
和
created,
看到这里,也就明白了为什么 created 的时候不能操作DOM了。因为这个时候还没有渲染真正的DOM元素到文档中。
created
仅仅代表数据状态的初始化完成。
重点看下initState()
initState (vm) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}复制代码
通过initData看Vue的数据响应系统,由于只是传了data,所以执行initData(vm)
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
observe(data, true /* asRootData */)
}
复制代码
proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}复制代码
取出data中的key,进行循环,通过proxy代理,可以直接通过 this.属性 访问data中的值,在实例对象上对数据进行代理,这样我们就能通过this.todos 来访问 data.todos 了 接下啦,正式的数据响应系统observe(data, true /* asRootData */),将数据通过Object.defineProperty进行get,set处理,使得数据响应
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value)
} else {
this.walk(value)
}
}
walk(obj: Object) {
const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
复制代码
在 Observer
类中,我们使用 walk
方法对数据data的属性循环调用 defineReactive
方法,defineReactive
方法很简单,仅仅是将数据data的属性转为访问器属性,并对数据进行递归观测,否则只能观测数据data的直属子属性。这样我们的第一步工作就完成了,当我们修改或者获取data属性值的时候,通过 get
和 set
即能获取到通知。
function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
const dep = new Dep()
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
复制代码
- 其中 let childOb = !shallow && observe(val),进行递归调用,将所有data数据包含子集进行get set化,进行响应
- 其中在Observe类中,如果属性是数组,会进行改造
if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) }export const arrayMethods = Object.create(arrayProto) ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) }) 复制代码
initData(vm)执行完成,相应系统就完成了,这时候执行callHook(vm, 'created') 触发created,继续回到_init(),执行到 vm.$mount(vm.$options.el)
Vue.prototype._init = function (options) {
const vm= this
vm._uid = uid++
let startTag, endTag
vm._isVue = true
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
vm._renderProxy = vm
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}复制代码
进入$mount会先获取挂载el节点,然后先判断有没有传入render方法,没有在去找有没有传入template,
Vue.prototype.$mount = function (
el,
hydrating
){
el = el && query(el)
const options = this.$options
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
}
} 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) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
复制代码
有了模板经过compileToFunctions,将模板编译为ast语法树,经过静态优化, 最后处理成render函数,本例中的render函数,使用了with(this), 将this作用域提前, {{ todo.text }} 所以我们可以直接在模板中使用属性,不加this! 当然加上this也是可以的 解除了我的很多迷惑,为什么模板中可以不加this(react中的render是要使用this的), 其中的v-for转换成_l,根据之前的Vue.prototype._l = renderList
function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('ol', _l((this.todos), function(todo) {
return _c('li', [_v("\n " + _s(todo.text) + "\n ")])
})), _v(" "), _c('child')], 1)
}
}复制代码
- 生成了render函数后接着进入mountComponent,
-
首先调用了beforeMount函数,
- 接着执行vm._watcher = new Watcher(vm, updateComponent, noop)
- 最后callHook(vm, 'mounted'),执行mounted,所以在mounted执行之前就已经挂载到dom上
所以重点 vm._watcher = new Watcher(vm, updateComponent, noop)
function mountComponent (
vm,
el,
hydrating
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
复制代码
查看Watcher代码
class Watcher {
constructor (
vm,
expOrFn,
cb,
options
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
evaluate () {
this.value = this.get()
this.dirty = false
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
复制代码
执行构造函数由于 this.lazy=false; this.value = this.lazy ? undefined : this.get();
执行get方法 其中pushTarget(this),给Dep.target添加静态属性this(当前new Watcher()实例 )
function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
return value
}
复制代码
接着执行 this.getter.call(vm, vm)复制代码
this.getter就是
updateComponent = () => {
vm._update(vm._render(), hydrating)
} 复制代码
- 开始先调用vm._render()
Vue.prototype._render = function (){
const vm = this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
let vnode = render.call(vm._renderProxy, vm.$createElement)
return vnode
}
复制代码
开始执行之前编译好的render函数了,在执行render函数时,通过获取todos属性等,触发相应
function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('ol', _l((this.todos), function(todo) {
return _c('li', [_v("\n " + _s(todo.text) + "\n ")])
})), _v(" "), _c('child')], 1)
}
}复制代码
的get方法,这个时候Dep.target已经存在静态属性,Watcher实例了
所以相应的dep实例就会收集对应的Watcher实例了
复制代码
执行完之后返回vnode,
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
其中vm._render()执行render函数返回vnode作为参数
接下来执行vm._update
这是首次渲染,所以执行
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false,
vm.$options._parentElm,
vm.$options._refElm
)复制代码
Vue.prototype._update = function (vnode, hydrating) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false,
vm.$options._parentElm,
vm.$options._refElm
)
vm.$options._parentElm = vm.$options._refElm = null
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
复制代码
vm.__patch__( vm.$el, vnode, hydrating, false, vm.$options._parentElm, vm.$options._refElm ) 根据vnode中树,创建对应元素,插入到父节点中,通过对vnode递归循环创建所有子节点 插入到父节点中 其中如果遇到子元素是组件,例如本例中Child,会创建对应VueComponent的实例,执行 和new Vue()一样的流程
如果还没有 prevVnode
说明是首次渲染,直接创建真实DOM。如果已经有了 prevVnode
说明不是首次渲染,那么就采用 patch
算法进行必要的DOM操作。这就是Vue更新DOM的逻辑。只不过我们没有将 virtual DOM 内部的实现。
当改变属性值时,会触发对应的属性的set方法,由于之前执行render的时候触发了get,收集了对应的Watcher,所以改变值时触发set,通知之前收集的Watcher实例执行,重新计算render方法进行patch操作
最后盗取一张图:
写了半天,实在写不下去了,以后有好的语言,再来整理吧!