本文希望可以帮助那些想吃蛋糕,但又觉得蛋糕太大而又不知道从哪下口的人们。
一、如何开始第一步
- 将源码项目
clone
下来后,按照CONTRIBUTING中的Development Setup
中的顺序,逐个执行下来
$ npm install
# watch and auto re-build dist/vue.js
$ npm run dev
- 学会看package.json文件,就像你在使用MVVM去关注它的render一样。
既然$ npm run dev
命令可以重新编译出vue.js
文件,那么我们就从scripts
中的dev
开始看吧。
"dev":"rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
如果这里你还不清楚
rollup
是做什么的,可以戳这里,简单来说就是一个模块化打包工具。具体的介绍这里就跳过了,因为我们是来看vue的,如果太跳跃的话,基本就把这次主要想做的事忽略掉了,跳跳跳不一定跳哪里了,所以在阅读源码的时候,一定要牢记这次我们的目的是什么。
注意上面指令中的两个关键词scripts/config.js
和web-full-dev
,接下来让我们看看script/config.js
这个文件。
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
回忆上面的命令,我们传入的
TARGET
是
web-full-dev
,那么带入到方法中,最终会看到这样一个
object
'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
。OK,打开这个文件后,终于看到了我们的一个目标关键词
import Vue from './runtime/index'
江湖规矩,继续往这个文件里跳,然后你就会看到:
import Vue from 'core/index'
是不是又看到了代码第一行中熟悉的关键词
Vue
import Vue from './instance/index'
打开
instance/index
后,结束了我们的第一步,已经从package.json中到框架中的文件,找到了
Vue
的定义地方。让我们再回顾下流程:
二、学会利用demo
切记,在看源码时为了防止看着看着看跑偏了,我们一定要按照代码执行的顺序看。
-
项目结构中有
examples
目录,让我们也创建一个属于自己的demo在这里面吧,随便copy一个目录,命名为demo,后面我们的代码都通过这个demo来进行测试、观察。index.html内容如下:
<!DOCTYPE html>
<html>
<head>
<title>Demo</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<template>
<span>{{text}}</span>
</template>
</div>
<script src="app.js"></script>
</body>
</html>
app.js文件内容如下:
var demo = new Vue({
el: '#demo',
data() {
return {
text: 'hello world!'
}
}
})
引入vue.js
上面demo的html中我们引入了dist/vue.js,那么window下,就会有Vue
对象,暂且先将app.js的代码修改如下:
console.dir(Vue);
如果这里你还不知道
console.dir
,而只知道console.log
,那你就亲自试试然后记住他们之间的差异吧。
从控制台我们可以看出,Vue
对象以及原型上有一系列属性,那么这些属性是从哪儿来的,做什么的,就是我们后续去深入的内容。
三、从哪儿来的
是否还记得我们在第一章中找到最终Vue
构造函数的文件?如果不记得了,就再回去看一眼吧,我们在本章会按照那个顺序倒着来看一遍Vue
的属性挂载。
instance(src/core/instance/index.js)
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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)
export default Vue
接下来我们就开始按照代码执行的顺序,先来分别看看这几个函数到底是弄啥嘞?
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
initMixin(src/core/instance/init.js)
Vue.prototype._init = function (options?: Object) {}
-
在传入的
Vue
对象的原型上挂载了_init
方法。 -
stateMixin(src/core/instance/state.js)
// Object.defineProperty(Vue.prototype, '$data', dataDef)
// 这里$data只提供了get方法,set方法再非生产环境时会给予警告
Vue.prototype.$data = undefined;
// Object.defineProperty(Vue.prototype, '$props', propsDef)
// 这里$props只提供了get方法,set方法再非生产环境时会给予警告
Vue.prototype.$props = undefined;
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function() {}
如果这里你还不知道
Object.defineProperty
是做什么的,我对你的建议是可以把对象的原型这部分好好看一眼,对于后面的代码浏览会有很大的效率提升,不然云里雾里的,你浪费的只有自己的时间而已。
eventsMixin(src/core/instance/events.js)
Vue.prototype.$on = function() {}
Vue.prototype.$once = function() {}
Vue.prototype.$off = function() {}
Vue.prototype.$emit = function() {}
lifecycleMixin(src/core/instance/lifecycle.js)
Vue.prototype._update = function() {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
renderMixin(src/core/instance/render.js)
// installRenderHelpers
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
//
Vue.prototype.$nextTick = function() {}
Vue.prototype._render = function() {}
instance
中对
Vue
的原型一波疯狂输出后,
Vue
的原型已经变成了:
如果你认为到此就结束了?答案当然是,不。让我们顺着第一章整理的图,继续回到core/index.js中。
Core(src/core/index.js)
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import {
FunctionalRenderContext
} from 'core/vdom/create-functional-component'
// 初始化全局API
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
})
Vue.version = '__VERSION__'
export default Vue
按照代码执行顺序,我们看看
initGlobalAPI(Vue)
方法内容:
// Object.defineProperty(Vue, 'config', configDef)
Vue.config = { devtools: true, …}
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive,
}
Vue.set = set
Vue.delete = delete
Vue.nextTick = nextTick
Vue.options = {
components: {},
directives: {},
filters: {},
_base: Vue,
}
// extend(Vue.options.components, builtInComponents)
Vue.options.components.KeepAlive = { name: 'keep-alive' …}
// initUse
Vue.use = function() {}
// initMixin
Vue.mixin = function() {}
// initExtend
Vue.cid = 0
Vue.extend = function() {}
// initAssetRegisters
Vue.component = function() {}
Vue.directive = function() {}
Vue.filter = function() {}
不难看出,整个Core在instance的基础上,又对Vue
的属性进行了一波输出。经历完Core后,整个Vue
变成了这样:
继续顺着第一章整理的路线,来看看runtime又对Vue
做了什么。
runtime(src/platforms/web/runtime/index.js)
这里还是记得先从宏观入手,不要去看每个方法的详细内容。可以通过
debugger
来暂停代码执行,然后通过控制台的console.dir(Vue)
随时观察Vue
的变化,
-
这里首先针对web平台,对Vue.config来了一小波方法添加。
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
向options中directives增加了
model
以及
show
指令:
// extend(Vue.options.directives, platformDirectives)
Vue.options.directives = {
model: { componentUpdated: ƒ …}
show: { bind: ƒ, update: ƒ, unbind: ƒ }
}
向options中components增加了
Transition
以及
TransitionGroup
:
// extend(Vue.options.components, platformComponents)
Vue.options.components = {
KeepAlive: { name: "keep-alive" …}
Transition: {name: "transition", props: {…} …}
TransitionGroup: {props: {…}, beforeMount: ƒ, …}
}
在原型中追加
__patch__
以及
$mount
:
// 虚拟dom所用到的方法
Vue.prototype.__patch__ = patch
Vue.prototype.$mount = function() {}
-
以及对devtools的支持。
entry(src/platforms/web/entry-runtime-with-compiler.js)
-
在entry中,覆盖了
$mount
方法。 -
挂载compile,
compileToFunctions
方法是将template
编译为render
函数
Vue.compile = compileToFunctions
小结
至此,我们完整的过了一遍在web中Vue的构造函数的变化过程:
- 通过instance对Vue.prototype进行属性和方法的挂载。
- 通过core对Vue进行静态属性和方法的挂载。
- 通过runtime添加了对platform === 'web'的情况下,特有的配置、组件、指令。
- 通过entry来为$mount方法增加编译
template
的能力。
四、做什么的
上一章我们从宏观角度观察了整个Vue构造函数的变化过程,那么我们本章将从微观角度,看看new Vue()后,都做了什么。
将我们demo中的app.js修改为如下代码:
var demo = new Vue({
el: '#demo',
data() {
return {
text: 'hello world!'
}
}
})
还记得instance/init中的Vue构造函数吗?在代码执行了
this._init(options)
,那我们就从
_init
入手,开始本章的旅途。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
// 浏览器环境&支持window.performance&非生产环境&配置了performance
if (process.env.NODE_ENV !== 'production'
&& config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
// 相当于 window.performance.mark(startTag)
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// 将options进行合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production'
&& config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
这个方法都做了什么?
- 在当前实例中,添加
_uid
,_isVue
属性。 - 当非生产环境时,用window.performance标记vue初始化的开始。
- 由于我们的demo中,没有手动处理_isComponent,所以这里会进入到else分支,将Vue.options与传入options进行合并。
- 为当前实例添加
_renderProxy
,_self
属性。 - 初始化生命周期,
initLifecycle
- 初始化事件,
initEvents
- 初始化render,
initRender
- 调用生命周期中的
beforeCreate
- 初始化注入值
initInjections
- 初始化状态
initState
- 初始化Provide
initProvide
- 调用生命周期中的
created
- 非生产环境下,标识初始化结束,为当前实例增加
_name
属性 - 根据
options
传入的el
,调用当前实例的$mount
OK,我们又宏观的看了整个_init
方法,接下来我们结合我们的demo,来细细的看下每一步产生的影响,以及具体调用的方法。
mergeOptions(src/core/util/options.js)
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options =
mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
还记得我们在第三章中,runtime对
Vue
的变更之后,options变成了什么样吗?如果你忘了,这里我们再回忆一下:
Vue.options = {
components: {
KeepAlive: { name: "keep-alive" …}
Transition: {name: "transition", props: {…} …}
TransitionGroup: {props: {…}, beforeMount: ƒ, …}
},
directives: {
model: { componentUpdated: ƒ …}
show: { bind: ƒ, update: ƒ, unbind: ƒ }
},
filters: {},
_base: ƒ Vue
}
this.constructor
传入
resolveConstructorOptions
中,因为我们的demo中没有进行继承操作,所以在
resolveConstructorOptions
方法中,没有进入if,直接返回得到的结果,就是在
runtime
中进行处理后的
options
选项。而
options
就是我们在调用
new Vue({})
时,传入的
options
。此时,mergeOptions方法变为:
vm.$options = mergeOptions(
{
components: {
KeepAlive: { name: "keep-alive" …}
Transition: {name: "transition", props: {…} …}
TransitionGroup: {props: {…}, beforeMount: ƒ, …}
},
directives: {
model: { componentUpdated: ƒ …}
show: { bind: ƒ, update: ƒ, unbind: ƒ }
},
filters: {},
_base: ƒ Vue
},
{
el: '#demo',
data: ƒ data()
},
vm
)
接下来开始调用
mergeOptions
方法。打开文件后,我们发现在引用该文件时,会立即执行一段代码:
// config.optionMergeStrategies = Object.create(null)
const strats = config.optionMergeStrategies
仔细往下看后面,还有一系列针对
strats
挂载方法和属性的操作,最终
strats
会变为:
其实这些散落在代码中的挂载操作,有点没想明白尤大没有放到一个方法里去统一处理一波?
继续往下翻,看到了我们进入这个文件的目标,那就是mergeOptions
方法:
function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
debugger;
if (process.env.NODE_ENV !== 'production') {
// 根据用户传入的options,检查合法性
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 标准化传入options中的props
normalizeProps(child, vm)
// 标准化注入
normalizeInject(child, vm)
// 标准化指令
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
因为我们这里使用了最简单的hello world
,所以在mergeOptions
中,可以直接从30行开始看,这里初始化了变量options
,32行、35行的for
循环分别根据合并策略进行了合并。看到这里,恍然大悟,原来strats
是定义一些标准合并策略,如果没有定义在其中,就使用默认合并策略defaultStrat
。
这里有个小细节,就是在循环子options时,仅合并父options中不存在的项,来提高合并效率。
让我们继续来用最直白的方式,回顾下上面的过程:
// 初始化合并策略
const strats = config.optionMergeStrategies
strats.el = strats.propsData = function (parent, child, vm, key) {}
strats.data = function (parentVal, childVal, vm) {}
constants.LIFECYCLE_HOOKS.forEach(hook => strats[hook] = mergeHook)
constants.ASSET_TYPES.forEach(type => strats[type + 's'] = mergeAssets)
strats.watch = function(parentVal, childVal, vm, key) {}
strats.props =
strats.methods =
strats.inject =
strats.computed = function(parentVal, childVal, vm, key) {}
strats.provide = mergeDataOrFn
// 默认合并策略
const defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
}
function mergeOptions (parent, child, vm) {
// 本次demo没有用到省略前面代码
...
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
怎么样,是不是清晰多了?本次的demo经过
mergeOptions
之后,变为了如下:
OK,因为我们本次是来看_init
的,所以到这里,你需要清除Vue
通过合并策略,将parent与child进行了合并即可。接下来,我们继续回到_init
对options
合并处理完之后做了什么?
initProxy(src/core/instance/proxy.js)
在merge完options后,会判断如果是非生产环境时,会进入initProxy方法。
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
带着雾水,进入到方法定义的文件,看到了Proxy
这个关键字,如果这里你还不清楚,可以看下阮老师的ES6,上面有讲。
- 这里在非生产环境时,对config.keyCodes的一些关键字做了禁止赋值操作。
- 返回了
vm._renderProxy = new Proxy(vm, handlers)
,这里的handlers
,由于我们的options中没有render,所以这里取值是hasHandler。
这部分具体是做什么用的,暂且知道有这么个东西,主线还是不要放弃,继续回到主线吧。
initLifecycle(src/core/instance/lifecycle.js)
初始化了与生命周期相关的属性。
function initLifecycle (vm) {
const options = vm.$options
// 省去部分与本次demo无关代码
...
vm.$parent = undefined
vm.$root = vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
initEvents(src/core/instance/events.js)
function initEvents (vm) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// 省去部分与本次demo无关代码
...
}
initRender(src/core/instance/render.js)
function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
vm.$slots = {}
vm.$scopedSlots = {}
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement= (a, b, c, d) => createElement(vm, a, b, c, d, true)
vm.$attrs = {}
vm.$listeners = {}
}
callHook(vm, 'beforeCreate)
调用生命周期函数beforeCreate
initInjections(src/core/instance/inject.js)
由于本demo没有用到注入值,对本次vm并无实际影响,所以这一步暂且忽略,有兴趣可以自行翻阅。
initState(src/core/instance/state.js)
本次的只针对这最简单的demo,分析
initState
,可能忽略了很多过程,后续我们会针对更复杂的demo来继续分析一波。
这里你可以先留意到几个关键词Observer
,Dep
,Watcher
。每个Observer
都有一个独立的Dep
。关于Watcher
,暂时没用到,但是请相信,马上就可以看到了。
initProvide(src/core/instance/inject.js)
由于本demo没有用到,对本次vm并无实际影响,所以这一步暂且忽略,有兴趣可以自行翻阅。
callHook(vm, 'created')
这里知道为什么在
created
时候,没法操作DOM了吗?因为在这里,还没有涉及到实际的DOM渲染。
vm.$mount(vm.$options.el)
这里前面有个if判断,所以当你如果没有在
new Vue
中的options
没有传入el
的话,就不会触发实际的渲染,就需要自己手动调用了$mount
。
这里的$mount
最终会调向哪里?还记得我们在第三章看到的compiler
所做的事情吗?就是覆盖Vue.prototype.$mount
,接下来,我们一起进入$mount
函数看看它都做了什么吧。
// 只保留与本次相关代码,其余看太多会影响视线
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
if (!options.render) {
let template = getOuterHTML(el)
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
$mount
之前,先将原有的
$mount
保留至变量
mount
中,整个覆盖后的方法是将
template
转为
render
函数挂载至
vm
的
options
,然后调用调用原有的
mount
。所以还记得
mount
来自于哪嘛?那就继续吧
runtime/index
,方法很简单,调用了生命周期中
mountComponent
。
// 依然只保留和本demo相关的内容
function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
OK,精彩的部分来了,一个
Watcher
,盘活了整个我们前面铺垫的一系列东西。打开
src/core/observer/watcher.js
,让我们看看
Watcher
的构造函数吧。为了清楚的看到
Watcher
的流程。依旧只保留方法我们需要关注的东西:
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
vm._watcher = this
vm._watchers.push(this)
this.getter = expOrFn
this.value = this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
popTarget()
this.cleanupDeps()
return value
}
- 在
Watcher
的构造函数中,本次传入的updateComponent
作为Wather
的getter
。 - 在
get
方法调用时,又通过pushTarget
方法,将当前Watcher
赋值给Dep.target
- 调用
getter
,相当于调用vm._update
,先调用vm._render
,而这时vm._render
,此时会将已经准备好的render
函数进调用。 -
render
函数中又用到了this.text
,所以又会调用text
的get
方法,从而触发了dep.depend()
-
dep.depend()
会调回Watcher
的addDep
,这时Watcher
记录了当前dep
实例。 - 继续调用
dep.addSub(this)
,dep
又记录了当前Watcher
实例,将当前的Watcher
存入dep.subs
中。 - 这里顺带提一下本次
demo
还没有使用的,也就是当this.text
发生改变时,会触发Observer
中的set
方法,从而触发dep.notify()
方法来进行update
操作。
最后这段文字太干了,可以自己通过断点,耐心的走一遍整个过程。如果没有耐心看完这段描述,可以看看笔者这篇文章100行代码带你玩vue响应式。
就这样,Vue
的数据响应系统,通过Observer
、Watcher
、Dep
完美的串在了一起。也希望经历这个过程后,你能对真正的对这张图,有一定的理解。
当然,$mount
中还有一步被我轻描淡写了,那就是这部分,将template转换为render,render实际调用时,会经历_render
, $createElement
, __patch__
, 方法,有兴趣可以自己浏览下'src/core/vdom/'目录下的文件,来了解vue
针对虚拟dom的使用。
最后
如果你喜欢,可以继续浏览笔者关于vue template转换部分的文章《Vue对template做了什么》。
原文作者:JserWang
本文来源: 掘金 如需转载请联系原作者