阅读源码的仓库:github.com/AlanLee97/r…
思维导图
记录了Vue应用初始化时基本的调用链路
示例demo
以todomvc为例
源码文件位置:github.com/AlanLee97/r…
app.js
/* eslint-disable no-undef */
// Full spec-compliant TodoMVC with localStorage persistence
// and hash-based routing in ~150 lines.
// localStorage persistence
var STORAGE_KEY = 'todos-vuejs-2.0'
var todoStorage = {
fetch: function () {
var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
todos.forEach(function (todo, index) {
todo.id = index
})
todoStorage.uid = todos.length
return todos
},
save: function (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
}
// visibility filters
var filters = {
all: function (todos) {
return todos
},
active: function (todos) {
return todos.filter(function (todo) {
return !todo.completed
})
},
completed: function (todos) {
return todos.filter(function (todo) {
return todo.completed
})
}
}
// eslint-disable-next-line no-debugger
debugger
// app Vue instance
var app = new Vue({
// app initial state
props: {
hello: {
type: String,
default: 'hello todomvc'
}
},
data: {
count: 0,
todos: todoStorage.fetch(),
newTodo: '',
editedTodo: null,
visibility: 'all',
currentTodoItem: {}
},
// watch todos change for localStorage persistence
watch: {
todos: {
handler: function (todos) {
todoStorage.save(todos)
},
deep: true
}
},
// computed properties
// https://vuejs.org/guide/computed.html
computed: {
filteredTodos: function () {
return filters[this.visibility](this.todos)
},
remaining: function () {
return filters.active(this.todos).length
},
allDone: {
get: function () {
return this.remaining === 0
},
set: function (value) {
this.todos.forEach(function (todo) {
todo.completed = value
})
}
}
},
filters: {
pluralize: function (n) {
return n === 1 ? 'item' : 'items'
}
},
// methods that implement data logic.
// note there's no DOM manipulation here at all.
methods: {
setCurrent(item) {
// this.count += 1
this.currentTodoItem = item
// setTimeout(() => {
// console.log('alan->count', this.count)
// })
},
addTodo: function () {
var value = this.newTodo && this.newTodo.trim()
if (!value) {
return
}
this.todos.push({
id: todoStorage.uid++,
title: value,
completed: false
})
this.newTodo = ''
},
removeTodo: function (todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
},
editTodo: function (todo) {
this.beforeEditCache = todo.title
this.editedTodo = todo
},
doneEdit: function (todo) {
if (!this.editedTodo) {
return
}
this.editedTodo = null
todo.title = todo.title.trim()
if (!todo.title) {
this.removeTodo(todo)
}
},
cancelEdit: function (todo) {
this.editedTodo = null
todo.title = this.beforeEditCache
},
removeCompleted: function () {
this.todos = filters.active(this.todos)
}
},
// a custom directive to wait for the DOM to be updated
// before focusing on the input field.
// https://vuejs.org/guide/custom-directive.html
directives: {
'todo-focus': function (el, binding) {
if (binding.value) {
el.focus()
}
}
}
})
// handle routing
function onHashChange () {
var visibility = window.location.hash.replace(/#/?/, '')
if (filters[visibility]) {
app.visibility = visibility
} else {
window.location.hash = ''
app.visibility = 'all'
}
}
window.addEventListener('hashchange', onHashChange)
onHashChange()
// mount
// 后置挂载元素
// mountComponent -> updateComponent(更新时 beforeUpdate) / new Watcher(初始化时), 观察vm,vm变化->执行updateComponent
// mounted
app.$mount('.todoapp')
console.log('alan-> app', app)
window.appVue = app
index.html
<!doctype html>
<html data-framework="vue">
<head>
<meta charset="utf-8">
<title>Vue.js • TodoMVC</title>
<link rel="stylesheet" href="https://unpkg.com/todomvc-app-css@2.0.4/index.css">
<script src="https://unpkg.com/director@1.2.8/build/director.js"></script>
<style>[v-cloak] { display: none; }</style>
</head>
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo"
autofocus autocomplete="off"
placeholder="What needs to be done?"
v-model="newTodo"
@keyup.enter="addTodo">
</header>
<section class="main" v-show="todos.length" v-cloak>
<input class="toggle-all" type="checkbox" v-model="allDone">
<ul class="todo-list">
<li v-for="todo in filteredTodos"
class="todo"
:key="todo.id"
:class="{ completed: todo.completed, editing: todo == editedTodo }"
@click="setCurrent(todo)">
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo(todo)"></button>
</div>
<input class="edit" type="text"
v-model="todo.title"
v-todo-focus="todo == editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)">
</li>
</ul>
</section>
<footer class="footer" v-show="todos.length" v-cloak>
<span class="todo-count">
<strong>{{ remaining }}</strong> {{ remaining | pluralize }} left
</span>
<ul class="filters">
<li><a href="#/all" :class="{ selected: visibility == 'all' }">All</a></li>
<li><a href="#/active" :class="{ selected: visibility == 'active' }">Active</a></li>
<li><a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Written by <a href="http://evanyou.me">Evan You</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script>
// for testing
if (navigator.userAgent.indexOf('PhantomJS') > -1) localStorage.clear()
</script>
<!-- Delete ".min" for console warnings in development -->
<script src="../../dist/vue.js"></script>
<script src="app.js"></script>
</body>
</html>
index.html为入口
分析
初始化Vue
在本示例demo中,通过script引入vue.js
<script src="../../dist/vue.js"></script>
这时会先执行这个脚本的代码,引入的vue.js的代码,对应的源码文件是src/core/index.js
,这是入口文件,这里开始初始化Vue
import Vue from './instance/index' // 导入Vue,会先执行这个脚本中的代码
import { initGlobalAPI } from './global-api/index'
// ...
initGlobalAPI(Vue)
// ...
export default Vue
这里又从src/instance/index.js
里导入了Vue,导入这个文件时,会先执行这个文件里的同步代码块,主要是给Vue.prototype上挂载一些实例方法/属性( $xxx
)和私有方法/属性( _xxx
) ,同步代码块的逻辑如下:
- 定义Vue函数
function Vue (options) { // 定义Vue函数,开发者new Vue()时,才会执行_init()
// 开发者new Vue()时,才会执行
// 里面做的操作:初始化生命周期、事件收集对象、渲染需要的一些属性、beforeCreate/created、状态(props,data,computed,watch)、provide/inject、执行挂载$mount
this._init(options)
}
初始化实例API(vm.$xxx / vm._xxx)
- 做一些初始化
// 外部导入这个文件时,会先执行一下代码
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
- initMixin(Vue),初始化,给_init赋值一个初始化的函数
Vue.prototype._init = function(options) { ... }
-
stateMixin(Vue),设置状态,在Vue.prototype上挂了实例API方法/属性
$data
$props
$set
$delete
$watch
-
eventsMixin(Vue),初始化事件,这里就是实现一个发布订阅模式,在Vue.prototype上挂上四个方法
$on
$once
$off
$emit
-
lifecycleMixin(Vue),初始化一部分生命周期,在Vue.prototype上挂了三个方法:
-
_update
_update
在执行时判断有没有前一个VNode节点,如果没有,则是初始化流程,通过__patch__()
方法得到渲染成真实DOM的元素,赋值给$el,否则就是走更新节点的流程。
-
$forceUpdate
-
$destroy
-
-
renderMixin(Vue),初始化渲染,安装渲染助手installRenderHelpers,挂载两个方法
$nextTick
_render
初始化全局API(Vue.xxx)
执行完src/instance/index.js
里的同步代码,再回来执行剩下的代码,也就是执行initGlobalAPI(Vue)
,再初始化全局API,这里会给Vue构造器函数上挂上一些公有API(Vue.xxx),挂载的有:
-
util
-
set
-
del
-
nextTick
-
observable
-
options
-
还让options.component继承了内置组件builtInComponents
- KeepAlive
- Transition
-
还做了一些初始化
- initUse,初始化插件
- initMixin,初始化mixins
- initExtend,初始化继承
- initAssetRegisters
最后导出Vue函数
以上,引入Vue.js文件时完成了Vue自身的初始化,接下来就是根据开发者提供的options,new Vue()开始初始化组件。
【备注】
挂载实例方法与挂载在构造器上的全局方法的区别
- 挂载实例方法:Vue.prototype.xxx = xxx
- 挂载在构造器全局方法:Vue.xxx = xxx
区别在于
- 挂载在prototype上的方法/属性,通过new关键字new出来的对象可以直接方法prototype里的方法,如const vm = new Vue(); vm.xxx
- 挂载在构造器上的方法/属性,只能通过构造器来访问,或者通过实例.construtor访问,如Vue.xxx,vm.constructor.xxx
new Vue(options)
调用链路示意图
我们写的组件的代码都是写在options里的,当new Vue的时候,会调用Vue构造函数里的_init
方法,_init
方法根据options初始化组件:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
// merge options
if (options && options._isComponent) {
// ...
} else {
// 合并选项,并挂载到$options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ...
initLifecycle(vm) // 初始化生命周期:$parent,$children,$refs,_watcher,_isMounted,_isDestroyed,_isBeingDestroyed等一些属性
initEvents(vm) // 初始化事件收集对象_events,初始化父组件的监听器
initRender(vm) // 初始化$slots,$scopedSlots,$createElement,响应式$attrs,$listeners
callHook(vm, 'beforeCreate') // 执行beforeCreate
initInjections(vm) // resolve injections before data/props // 初始化inject
initState(vm) // 初始化props,data,computed,watch
initProvide(vm) // resolve provide after data/props // 初始化 provide,原理:把options.provide挂载到vm._provide
callHook(vm, 'created') // 执行created
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
- mergeOptions,合并选项,并挂载到$options
- initLifecycle(vm),初始化生命周期:
$parent
,$children
,$refs
,_watcher
,_isMounted
,_isDestroyed
,_isBeingDestroyed
等一些属性 - initEvents(vm),初始化事件收集对象_events,初始化父组件的监听器
- initRender(vm),初始化
$slots
,$scopedSlots
,$createElement
,响应式$attrs
,$listeners
- callHook(vm, 'beforeCreate'),执行
beforeCreate
- initInjections(vm),初始化
inject
- initState(vm) ,初始化
props
,data
,computed
,watch
- initProvide(vm),初始化 provide,原理:把
options.provide
挂载到vm._provide
- callHook(vm, 'created'),执行
created
- vm.options.el),挂载元素,如果有
$options.el
这里比较重要的是initState方法,具体看下initState里面做了什么:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 调用defineReactive将props定义成响应式
if (opts.methods) initMethods(vm, opts.methods) // 将methods的函数平铺到vm
if (opts.data) {
initData(vm) // 将data定义成响应式
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) // 初始化computed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch) // 初始化watch,createWatcher->vm.$watch
}
}
通过上面的代码,可以知道,主要是初始化props,data,computed,watch,我们主要看下初始化data
将data转成响应式
调用链路示意图
initData(vm)的逻辑
- 调用observe方法
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
}
// ...
let i = keys.length
while (i--) {
// ...
// 略过这里的代码,主要是做一些属性名有没有被props,methods占用的检测
}
// observe data,主要是要关注observe方法
observe(data, true /* asRootData */)
}
- observe方法new Observer
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 这里是重要的地方
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
- Observer初始化时遍历data对象,调用defineReactive方法
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value // data
this.dep = new Dep() // 初始化一个依赖
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// ...
} else {
// 在这里遍历data
this.walk(value)
}
}
// 遍历所有属性并将它们转换为getter/setter。此方法只在值类型为Object时调用。
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 通过defineReactive方法
defineReactive(obj, keys[i])
}
}
// ...
}
- defineReactive方法调用Object.defineProperty,修改对象的属性描述符,get/set,加入依赖的处理,get时,收集依赖;set时,依赖触发更新
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 获取属性值得时候,收集依赖
dep.depend()
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// ...
val = newVal
// 在更新值得时候,触发依赖通知更新
dep.notify()
}
})
}
通过上面的代码链路调用,已经将data的属性完成响应式。
小总结
简单总结new Vue的操作,就是根据开发者的options初始化组件,初始化props、data、methods、computed、watch;将打它转为响应式,以实现数据变化时,能够更新视图。
其中我们比较关注的是data如何初始化,主要通过遍历data的属性,使用Object.defineProperty
,修改对象的属性描述符get/set,以实现响应式效果。
当然还有props、methods、watch等的初始化,这个我们后期再分析。
挂载元素
调用链路
- 执行app.$mount('.todoapp'),挂载元素,进入到
src\platforms\web\entry-runtime-with-compiler.js
入口文件,里面的主要逻辑:
- 先缓存一份原
$mount
方法为mount
,主要是要包装一下原来的$mount
方法,加入编译函数compileToFunctions
,将template
转成render
方法,并把render
方法挂载会options
上 - 执行
mount
方法(原Vue.prototype.$mount
)
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// ...
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)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
// 调用compileToFunctions方法,将模板转换成render函数,挂载到options
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
}
}
//
return mount.call(this, el, hydrating)
}
- 再调用mount方法,其实就是Vue.prototype.$mount,在
src\platforms\web\runtime\index.js
定义的函数,实际调用mountComponent函数
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
- mountComponent方法
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
// ...
}
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// ... 省略做一些标记mark代码
const vnode = vm._render()
vm._update(vnode, hydrating)
// ...
}
} else {
// 这里是核心逻辑,把vm._render()执行结果VNode作为参数传递给_update执行
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 我们设它为vm._watcher在观察器的构造函数中,因为观察器的初始补丁可能调用$forceUpdate(例如在子组件的挂载钩子中),它依赖于vm._watchr已经定义
// 这里是最核心的逻辑:把updateComponent更新函数放到Watcher的回调中进行监听,如果vm的数据有更新,则执行updateComponent函数,更新视图。
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
mountComponent主要做了两个操作
- 一是定义一个更新组件的函数
updateComponent
,里面把_render()渲染函数执行结果VNode作为参数放到_update()更新函数里执行更新操作 - 二是把更新函数放到Watcher的回调中进行监听,如果vm的数据有更新,则执行updateComponent函数,更新视图。
小总结:
$mount挂载元素,核心逻辑就是:
- 把template转成渲染函数
_render
,再调用挂载组件函数mountComponent
,封装一个更新组件的函数updateComponent
,调用更新函数vm._update
,用_render
函数的结果VNode作为参数 - 再
new Watcher
观察vm的变化,把updateComponent
函数作为回调函数,当vm的数据有更新时,则执行updateComponent
函数,_update
更新视图(更新视图涉及到diff,path更新,这里不做分析)。
总结
简易流程总结
- 引入Vue.js时,实例方法/属性和公共方法/属性
- 挂载实例方法/属性,给Vue的prototype挂载一些全局实例方法,set`等
- 挂载公共方法/属性,给Vue构造器上挂载一些公用方法/属性,Vue.xxx,如
Vue.set()
等
- 开发者在
new Vue(options)
的时候,执行vm._init(options)
初始化方法
- 初始化状态
-
- 将
props
,data
定义成响应式,读取属性时收集依赖,属性更新时通知更新 - 将
methods
,computed
(computed需使用Watcher观测)平铺到vm上 - 初始化
watch
- 将
- 执行生命周期函数
- 挂载元素,执行
vm.$mount()
,并监听通过new Watcher
监听自身数据的变化,以_update
作为回调函数,当数据变动时执行更新函数进行更新视图。
官方解释:
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
后记
前面分析的过程中出现了Observer
、Dep
、Watcher
;那么,它们三者之间的关系是什么呢
如图所示:
以Observer
为入口,实例化时,会实例化一个Dep
类用来收集依赖,和通知更新,Dep
里通过subs
,收集Watcher
实例,Watcher
中存放了回调函数队列,当执行run()
时,则执行完回调函数队列,视图更新函数就存放在这个回调队列函数中。
Watcher
也可以单独使用,例如,computed
就是单独使用Watcher
实现,组件自身也会new一个Watcher
用于观测自身的数据变化,组件中的watch
选项,也是单独new的Watcher
。
所以,Watcher
在Vue中起到了至关重要的作用,绝大部分功能底层都是由它来实现的。