1.从打包命令开始
从打包入口开始 package.json
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
-w
= --watch
-c
= --config
就是指定配置文件为 build/config.js
build/config.js
const builds = {
//....
// Runtime+compiler development build (Browser)
'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
},
//....
}
function genConfig (name) {
const opts = builds[name]
const config = {
input: opts.entry,
external: opts.external,
plugins: [
flow(),
alias(Object.assign({}, aliases, opts.alias))
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || 'Vue'
},
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
}
}
}
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET) //传入参数 web-full-dev
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
- 这里其实就是一个打包配置文件,为了多平台打包的配置。genConfig 函数返回一个Rollup 的配置对象。
2. 寻找 Vue 的构造函数
入口文件为 web/entry-runtime-with-compiler.js:
import Vue from './runtime/index'
src/core/instance/index.js
一路下来找到 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) // 实现了_init()
stateMixin(Vue) // $data,$props,$set,$delete,$watch
eventsMixin(Vue)
lifecycleMixin(Vue) // _update()
renderMixin(Vue) // _render
export default Vue
这个文件就是定义构造函数,然后调用了 5 个 Mixin 方法给 Vue 构造函数增加原型方法。
处理后 Vue 的 prototype 上会挂载很多方法
src/core/index.js
回到上一个文件 src/core/index.js
initGlobalAPI(Vue)
initGlobalAPI
的作用是在 Vue 构造函数上挂载静态属性和方法,Vue
在经过initGlobalAPI
之后,会变成这样:
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
在往上就是 runtime/index.js
主要做了三件事:
1、覆盖Vue.config的属性,将其设置为平台特有的一些方法
2、Vue.options.directives
和Vue.options.components
安装平台特有的指令和组件
3、在Vue.prototype
上定义__patch__
和$mount
Vue会变成这样:
Vue.config.isUnknownElement = isUnknownElement
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.mustUseProp = mustUseProp
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (el,hydrating) {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
注意的是Vue.options的变化。
$mount方法:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// el=document.querySelector(el)
web-runtime-with-compiler.js
最后回到入口文件web-runtime-with-compiler.js
// 缓存 $mount 函数
const mount = Vue.prototype.$mount
// 覆盖 $mount 函数
Vue.prototype.$mount = function(){...}
//在 Vue 上挂载 compile
Vue.compile = compileToFunctions
//compileToFunctions 函数的作用,就是将模板 template 编译为 render 函数
runtime/index.js
主要是添加 web 平台特有的配置、组件和指令,web-runtime-with-compiler.js
给 Vue 的$mount方法添加compiler编译器,支持template。
3. Vue 初始化流程
let v = new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
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)
}
在调用_init()之前,还做了一个安全模式的处理,告诉开发者必须使用new操作符调用 Vue。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
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 {
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) // 初始化$parent,$root,$children,$refs
initEvents(vm) // 处理父组件传递的监听器
initRender(vm) // $slots, $scopedSlots,_c(),$createElement()
callHook(vm, 'beforeCreate')
initInjections(vm) // 获取注入数据
initState(vm) // 初始化组件中props、methods、data、computed、watch
initProvide(vm) // 提供数据
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)
}
}
_init()
方法在一开始的时候,在this
对象上定义了两个属性:_uid
和_isVue
,然后判断有没有定义options._isComponent
,在使用 Vue 开发项目的时候,我们是不会使用_isComponent
选项的,这个选项是 Vue 内部使用的,按照本节开头的例子,这里会走else
分支:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这样Vue第一步所做的事情就来了:使用策略对象合并参数选项
可以发现,Vue 使用mergeOptions
来处理我们调用 Vue 时传入的参数选项 (options),然后将返回值赋值给this.$options(vm === this)
,传给mergeOptions
方法三个参数,我们分别来看一看,首先是:resolveConstructorOptions(vm.constructor)
,我们查看一下这个方法:
export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options // 相当于let options = Vue.options
if (Ctor.super) { //处理继承
//...
}
return options
}
这个方法接收一个参数Ctor,通过传入的vm.constructor我们可以知道,其实就是Vue构造函数本身。
//Vue.options的样子
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
传给mergeOptions
方法的第二个参数是我们调用 Vue 构造函数时的参数选项,
第三个参数是vm也就是this对象,
最终运行的代码应该如下:
vm.$options = mergeOptions(
{
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
},
{
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
},
vm
)
并将最终的值赋值给实例下的$options
属性即:this.$options
。
下面的代码是_init()
方法合并完选项之后的代码:
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)
根据上面的代码,在生产环境下会为实例添加两个属性,并且属性值都为实例本身:
vm._renderProxy = vm
vm._self = vm
然后,调用了4个init*
方法分别为:initLifecycle
、initEvents
、initState
、initRender
,且在initState
前后分别回调了生命周期钩子beforeCreate
和created
,而initRender
是在created
钩子执行之后执行的,看到这里,也就明白了为什么 created
的时候不能操作 DOM 了。因为这个时候还没有渲染真正的 DOM 元素到文档中。created
仅仅代表数据状态的初始化完成。
最后在initRender
中如果有vm.$options.el
还要调用vm.$mount(vm.$options.el)
,如下:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
这就是为什么如果不传递el
选项就需要手动 mount
的原因了。
4. 数据响应式
Vue 的数据响应系统包含三个部分:Observer
、Dep
、Watcher
。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? data.call(vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
while (i--) {
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else {
proxy(vm, keys[i])
}
}
observe(data)
data.__ob__ && data.__ob__.vmCount++
}
这里会判断是不是fucntion,得到最终我们传入的data
data: {
a: 1,
b: [1, 2, 3]
}
然后是一个while
循环,循环的目的是在实例对象上对数据进行代理,这样我们就能通过this.a
来访问data.a
了,代码的处理是在proxy
函数中,该函数非常简单,仅仅是在实例对象上设置与data
属性同名的访问器属性,然后使用_data
做数据劫持,如下:
function proxy (vm: Component, key: string) {
if (!isReserved(key)) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return vm._data[key]
},
set: function proxySetter (val) {
vm._data[key] = val
}
})
}
做完数据的代理,就正式进入响应式系统
observe(data)
我们说过,数据响应系统主要包含三部分:Observer
、Dep
、Watcher
,代码分别存放在:observer/index.js
、observer/dep.js
以及observer/watcher.js
文件中
假如,我们有如下代码:
var data = {
a: 1,
b: {
c: 2
}
}
observer(data)
new Watch('a', () => {
alert(9)
})
new Watch('a', () => {
alert(90)
})
new Watch('b.c', () => {
alert(80)
})
这段代码目的是,首先定义一个数据对象data
,然后通过 observer
对其进行观测,之后定义了三个观察者,当数据有变化时,执行相应的方法,这个功能使用 Vue 的实现原来要如何去实现?其实就是在问observer
怎么写?Watch
构造函数又怎么写?接下来我们逐一实现。
首先,observer
的作用是:将数据对象 data 的属性转换为访问器属性:
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
let keys = Object.keys(data)
for(let i = 0; i < keys.length; i++){
defineReactive(data, keys[i], data[keys[i]])
}
}
}
function defineReactive (data, key, val) {
observer(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function (newVal) {
if(val === newVal){
return
}
observer(newVal)
}
})
}
function observer (data) {
if(Object.prototype.toString.call(data) !== '[object Object]') {
return
}
new Observer(data)
}
上面的代码中,我们定义了 observer
方法,该方法检测了数据 data
是不是纯 JavaScript
对象,如果是就调用Observer
类,并将data
作为参数透传。
在Observer
类中,我们使用walk
方法对数据 data
的属性循环调用defineReactive
方法,defineReactive
方法很简单,仅仅是将数据 data
的属性转为访问器属性,并对数据进行递归观测,否则只能观测数据 data 的直属子属性。这样我们的第一步工作就完成了,当我们修改或者获取 data 属性值的时候,通过get 和 set 即能获取到通知。
我们继续往下看,来看一下Watch:
new Watch('a', () => {
alert(9)
})
现在的问题是,Watch
要怎么和observer
关联?
我们看看Watch
它知道些什么,通过上面调用Watch
的方式,传递给Watch
两个参数,一个是 ‘a’ 我们可以称其为表达式,另外一个是回调函数。所以我们目前只能写出这样的代码:
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
}
}
那么要怎么关联呢,大家看下面的代码会发生什么:
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
data[exp]
}
}
多了一句data[exp]
,这句话是在干什么?是不是在获取data
下某个属性的值,比如 exp
为 ‘a’ 的话,那么data[exp]
就相当于在获取data.a
的值,那这会发生?
大家不要忘了,此时数据data
下的属性已经是访问器属性了,所以这么做的结果会直接触发对应属性的get
函数,这样我们就成功的和observer
产生了关联,但这样还不够,我们还是没有达到目的,不过我们已经无限接近了,我们继续思考看一下可不可以这样:
既然在Watch中对表达式求值,能够触发observer的get,那么可不可以在get中收集Watch中函数呢?
答案是可以的,不过这个时候我们就需要Dep
出场了,它是一个依赖收集器。
我们的思路是:data
下的每一个属性都有一个唯一的Dep
对象,在get
中收集仅针对该属性的依赖,然后在set
方法中触发所有收集的依赖,这样就搞定了,看如下代码:
class Dep {
constructor () {
this.subs = []
}
addSub () {
this.subs.push(Dep.target)
}
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
}
Dep.target = null
function pushTarget(watch){
Dep.target = watch
}
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
pushTarget(this)
data[exp]
}
}
上面的代码中,我们在Watch
中增加了pushTarget(this)
,可以发现,这句代码的作用是将Dep.target
的值设置为该 Watch
对象。在pushTarget
之后我们才对表达式进行求值,接着,我们修改defineReactive
代码如下
function defineReactive (data, key, val) {
observer(val)
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.addSub()
return val
},
set: function (newVal) {
if(val === newVal){
return
}
observer(newVal)
dep.notify()
}
})
}
如标注,新增了三句代码,我们知道,Watch
中对表达式求值会触发 get 方法,我们在 get 方法中调用了dep.addSub
,也就执行了这句代码:this.subs.push(Dep.target)
,由于在这句代码执行之前,Dep.target
的值已经被设置为一个Watch
对象了,所以最终结果就是收集了一个Watch
对象
然后在set
方法中我们调用了dep.notify
,所以当 data 属性值变化的时候,就会通过dep.notify
循环调用所有收集的 Watch 对象中的回调函数:
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
这样observer
、Dep
、Watch
三者就联系成为一个有机的整体,实现了我们最初的目标,完整的代码可以戳这里:observer-dep-watch。
这里还给大家挖了个坑,因为我们没有处理对数组的观测,由于比较复杂并且这又不是我们讨论的重点,如果大家想了解可以戳我的这篇文章:JavaScript 实现 MVVM 之我就是想监测一个普通对象的变化,另外,在 Watch 中对表达式求值的时候也只做了直接子属性的求值,所以如果 exp 的值为 ‘a.b’ 的时候,就不可以用了,Vue 的做法是使用.分割表达式字符串为数组,然后遍历一下对其进行求值,大家可以查看其源码。如下:
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
} else {
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
}
Vue 的求值代码是在src/core/util/lang.js
文件中parsePath
函数中实现的。总结一下 Vue 的依赖收集过程应该是这样的:
实际上,Vue 并没有直接在get中调用addSub
,而是调用的dep.depend
,目的是将当前的 dep
对象收集到 watch
对象中,(大家注意数据的每一个字段都拥有自己的dep对象和get方法。)
这样 Vue 就建立了一套数据响应系统,之前我们说过,按照我们的例子那样写,初始化工作只包含两个主要内容即:initData
和initRender
。现在initData
我们分析完了,接下来看一看initRender
6. 渲染
在initRender
方法中,因为我们的例子中传递了el选项,所以下面的代码会执行:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
这里,调用了$mount
方法,在还原 Vue 构造函数的时候,我们整理过所有的方法,其中$mount
方法在两个地方出现过:
1、在web-runtime.js
文件中:
Vue.prototype.$mount = function (
el
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return this._mount(el, hydrating)
}
它的作用是通过el获取相应的 DOM 元素,然后调用lifecycle.js
文件中的_mount
方法。
2、在web-runtime-with-compiler.js
文件中:
分析一下可知web-runtime-with-compiler.js
的逻辑如下:
1、缓存来自web-runtime.js
文件的$mount
方法
2、判断有没有传递render
选项,如果有直接调用来自web-runtime.js
文件的 $mount
方法
3、如果没有传递render
选项,那么查看有没有template
选项,如果有就使用compileToFunctions
函数根据其内容编译成render函数
4、如果没有template
选项,那么查看有没有el
选项,如果有就使用compileToFunctions
函数将其内容 (template = getOuterHTML(el))
编译成render
函数
5、将编译成的render
函数挂载到this.$options
属性下,并调用缓存下来的web-runtime.js
文件中的 $mount
方法
不过不管怎样,我们发现这些步骤的最终目的是生成render
函数,然后再调用lifecycle.js
文件中的_mount
方法,我们看看这个方法做了什么事情,查看_mount
方法的代码,这是简化过得:
Vue.prototype._mount = function (
el?: Element | void,
hydrating?: boolean
): Component {
const vm: Component = this
vm.$el = el
callHook(vm, 'beforeMount')
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
上面的代码很简单,该注释的都注释了,唯一需要看的就是这段代码:
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
看上去很眼熟有没有?我们平时使用 Vue 都是这样使用 watch
的:
this.$watch('a', (newVal, oldVal) => {
})
// 或者
this.$watch(function(){
return this.a + this.b
}, (newVal, oldVal) => {
})
第一个参数是 表达式或者函数,第二个参数是回调函数,第三个参数是可选的选项。
原理是 Watch 内部对表达式求值或者对函数求值从而触发数据的 get 方法收集依赖。
可是_mount
方法中使用Watcher
的时候第一个参数vm是什么鬼。我们不妨去看看源码中$watch
函数是如何实现的,根据之前还原 Vue 构造函数中所整理的内容可知:
$watch
方法是在src/core/instance/state.js文件中的stateMixin
方法中定义的,源码如下:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: Function,
options?: Object
): Function {
const vm: Component = this
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
我们可以发现,$warch
其实是对 Watche r的一个封装,内部的 Watcher 的第一个参数实际上也是vm即:Vue 实例对象,这一点我们可以在 Watcher 的源码中得到验证,observer/watcher.js
文件查看:
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object = {}
) {
}
}
可以发现真正的 Watcher 第一个参数实际上就是vm。第二个参数是表达式或者函数,然后以此类推,所以现在再来看_mount
中的这段代码:
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
忽略第一个参数vm
,也就说,Watcher
内部应该对第二个参数求值,也就是运行这个函数:
() => {
vm._update(vm._render(), hydrating)
}
所以vm._render()
函数被第一个执行,该函数在src/core/instance/render.js
中,该方法中的代码很多,下面是简化过的:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options
...
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
...
}
vnode.parent = _parentVnode
return vnode
}
_render
方法首先从vm.$options
中解构出render
函数,大家应该记得:vm.$options.render
方法是在web-runtime-with-compiler.js
文件中通过compileToFunctions
方法将template
或el
编译而来的。解构出render
函数后,接下来便执行了该方法:
vnode = render.call(vm._renderProxy, vm.$createElement)
其中使用call
指定了render
函数的作用域环境为vm._renderProxy
,这个属性在我们整理实例对象的时候知道,他是在Vue.prototype._init
方法中被添加的,即:vm._renderProxy = vm
,其实就是 Vue 实例对象本身,然后传递了一个参数:vm.$createElement
。那么render
函数到底是干什么的呢?
让我们根据上面那句代码猜一猜,我们已经知道render
函数是从template
或el
编译而来的,如果没错的话应该是返回一个虚拟 DOM 对象。我们不妨使用console.log打印一下render
函数,当我们的模板这样编写时:
<ul>
<li>{{a}}</li>
</ul>
其实了解 Vue2.x 版本的同学都知道,Vue 提供了render选项,作为template的代替方案,同时为 JavaScript 提供了完全编程的能力,下面两种编写模板的方式实际是等价的:
new Vue({
el: '#app',
data: {
a: 1
},
template: '<ul><li>{{a}}</li><li>{{a}}</li></ul>'
})
new Vue({
el: '#app',
render: function (createElement) {
createElement('ul', [
createElement('li', this.a),
createElement('li', this.a)
])
}
})
现在我们再来看我们打印的render函数:
function anonymous() {
with(this){
return _c('ul', {
attrs: {"id": "app"}
},[
_c('li', [_v(_s(a))])
])
}
}
是不是与我们自己写render
函数很像?因为 render
函数的作用域被绑定到了 Vue 实例,即:render.call(vm._renderProxy, vm.$createElement)
,所以上面代码中_c
、_v
、_s
以及变量a
相当于 Vue 实例下的方法和变量。
大家还记得诸如_c
、_v
、_s
这样的方法在哪里定义的吗?我们在整理 Vue 构造函数的时候知道,他们在src/core/instance/render.js
文件中的renderMixin
方法中定义,除了这些之外还有诸如:_l
、_m
、_o
等等。其中_l
就在我们使用v-for
指令的时候出现了。所以现在大家知道为什么这些方法都被定义在render.js
文件中了吧,因为他们就是为了构造出render
函数而存在的。
现在我们已经知道了render
函数的长相,也知道了render
函数的作用域是 Vue 实例本身即:this
(或vm
)。那么当我们执行render
函数时,其中的变量如:a
,就相当于:this.a
,我们知道这是在求值,所以_mount
中的这段代码:
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
当vm._render
执行的时候,所依赖的变量就会被求值,并被收集依赖。
按照 Vue 中watcher.js
的逻辑,当依赖的变量有变化时不仅仅回调函数被执行,实际上还要重新求值,即还要执行一遍:
() => {
vm._update(vm._render(), hydrating)
}
这实际上就做到了re-render
,因为vm._update
就是文章开头所说的虚拟 DOM 中的最后一步:patch
vm_render
方法最终返回一个vnode
对象,即虚拟 DOM
,然后作为vm_update
的第一个参数传递了过去,我们看一下vm_update
的逻辑,在src/core/instance/lifecycle.js
文件中有这么一段代码:
if (!prevVnode) {
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false ,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
vm.$el = vm.__patch__(prevVnode, vnode)
}
如果还没有prevVnode
说明是首次渲染,直接创建真实 DOM。如果已经有了prevVnode
说明不是首次渲染,
那么就采用patch
算法进行必要的 DOM 操作。这就是 Vue 更新 DOM 的逻辑。只不过我们没有将 virtual DOM 内部的实现。
现在我们理理思路,当我们写如下代码时:
new Vue({
el: '#app',
data: {
a: 1,
b: [1, 2, 3]
}
})
Vue 所做的事:
1、构建数据响应系统,使用Observer
将数据 data
转换为访问器属性;将el
编译为render
函数,render
函数返回值为虚拟 DOM
2、在_mount
中对_update
求值,而_update
又会对render
求值,render
内部又会对依赖的变量求值,收集为被求值的变量的依赖,当变量改变时,_update
又会重新执行一遍,从而做到re-render
。
到此,我们从大体流程,挑着重点的走了一遍 Vue,但是还有很多细节我们没有提及,比如:
1、将模板转为render
函数的时候,实际是先生成的抽象语法树(AST),再将抽象语法树转成的render
函数,而且这一整套的代码我们也没有提及,因为他在复杂了,其实这部分内容就是在完正则。
2、我们也没有详细的讲 Virtual DOM 的实现原理,网上已经有文章讲了,大家可以搜一搜
3、我们的例子中仅仅传递了el
,data
选项,大家知道 Vue 支持的选项很多,比如我们都没有讲到,但都是触类旁通的,比如你搞清楚了data
选项再去看computed
选项或者props
选项就会很容易,比如你知道了Watcher
的工作机制再去看watch
选项就会很容易。