文章目录
Vue 实例新建流程
Vue源码目录结构说明
- src
- compiler
解析模板生成AST和render函数
- core
- components
目前只有keep-alive组件
- global-api
向Vue对象注入全局方法:Vue.use(),Vue.extend()等
- instance
向Vue实例对象注入方法:this.$emit(),this.$forceUpdate()等
- observer
实现data与Watch对象的依赖收集与更新
- util
工具类
- vdom
Vdom有关方法
- components
- entries
Vue 不同类型源码入口
- platforms
- server
- sfc
- shared
- compiler
这次主要我们用到的目录只有:global-api,instance,observer三个。主要介绍Vue实例化时初始数据和计算属性时的源码具体内容。
initData初始化数据
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
...
// a flag to avoid this being observed
vm._isVue = true
...
// 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')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
当我们在 new Vue()
时首先调用得就是 _init()
方法。它主要初始化生命周期,事件,渲染相关数据,调用 beforeCreate
钩子,初始化provide/injections相关数据,初始化data相关数据,调用 created
钩子。最后调用 $mount()
方法挂载到对应的DOM元素上。
今天我们主要看一下初始化数据相关的部分。也就是 _init()
方法里面的 initState()
函数。
// 初始化数据
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 1. 初始化参数
if (opts.methods) initMethods(vm, opts.methods) // 2、初始化方法
if (opts.data) {
// 3、初始化数据
initData(vm)
} else {
observe(vm._data = {
}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) // 4、初始化计算属性
if (opts.watch) initWatch(vm, opts.watch) // 5、初始化监听属性
}
这个 initState()
代码很短,要做的事情也很清楚,就是初始化了 props
参数,methods
方法,data
数据,computed
计算属性,watch
监听属性。这次我们主要关注 initData()
方法和 initComputed()
方法。了解Vue依赖收集与双向绑定的完整流程。
所以,首先我们先看一下 initData()
方法。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {
}
...
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
while (i--) {
if (props && hasOwn(props, keys[i])) {
// 判断props和data里的属性是否有重复
process.env.NODE_ENV !== 'production' && warn(
`The data property "${
keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(keys[i])) {
// 判断data属性不以$或_开头
proxy(vm, `_data`, keys[i]) // 将this.XXX代理到this._data.XXX
}
}
// observe data
observe(data, true /* asRootData */) //
}
function getData (data: Function, vm: Component): any {
try {
return data.call(vm)
} catch (e) {
handleError(e, vm, `data()`)
return {
}
}
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
initData()
方法主要做了三件事:
第一件事:一般定义 data
属性都是一个函数,返回的对象才是具体的数据。所以首先判断 data
是不是函数,如果是就通过 getData()
方法运行 data
方法得到返回的数据,否则就返回 data || {}
。
并且将 data
函数返回的结果赋值给 vm._data
和自己。vm._data
就是保存Vue对象运行数据的属性。以后如果数据发生变化,修改得也是这里的数据。vm.$option.data
是参数里面的原始数据,不能修改。
第二件事:一般我们访问Vue对象里面的数据是使用 this.XXX
这种形式。而现在数据保存在 this._data
里面的,所以使用 proxy()
函数,将 this.XXX
代理到 this._data.XXX
上去。
不仅如此,在代理之后,还检查了 data
里面的属性是否与 props
里面的属性重名。如果重名,则认为些属性是参数,data
里面的重名属性将不会被代理。
第三件事,就是运行 observer()
方法,下面会讲解这个方法具体做了什么。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value)) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 是否已经存在__ob__观察者对象
ob = value.__ob__
} else if (
observerState.shouldConvert && // ???
!isServerRendering() && // 不是服务器渲染
(Array.isArray(value) || isPlainObject(value)) && // data必须是数组或者简单对象
Object.isExtensible(value) && // data对象必须是可扩展的(可以添加额外属性)
!value._isVue //
) {
ob = new Observer(value) // 初始化data.__ob__属性
}
if (asRootData && ob) {
ob.vmCount++ // 根对象
}
return ob
}
这个方法的关键在 ob = new Observer(value)
这一行。为数据对象添加一个__ob__属性,而这个属性就是一个Observer对象的实例。下面主要是看一下Observer类的构造函数。
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this) // 等同于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]])
}
}
}
实例化Observer对象的时候,首先初始化了一些变量value(要观察的对象),dep和vmCount。然后将自己赋值给 value.__ob__
属性。最后,判断value的类型,如果是数组就走 observeArray()
方法,如果是其它,就走 walk()
方法。
我们先看一下其它类型,主要是对象走的 walk()
方法,很简单,就是遍历对象所有属性,然后每个属性调用 defineReactive()
方法。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get // 暂存属性之前的get和set修饰器方法
const setter = property && property.set
let childOb = 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 (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal) // 触发依赖的重新收集
dep.notify() // 触发所有依赖监听器的更新
}
})
}
但是,这没有Watch对象的新建流程,所以,我们现在了解的依赖收集是不完整的。接下来,我们以计算属性的Watch对象新建为例子,让Observer,Dep和Watch对象之间的关系变得清楚明白。
initComputed初始化计算属性
// 初始化数据
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 1. 初始化参数
if (opts.methods) initMethods(vm, opts.methods) // 2、初始化方法
if (opts.data) {
// 3、初始化数据
initData(vm)
} else {
observe(vm._data = {
}, true /* asRootData */)
}
> if (opts.computed) initComputed(vm, opts.computed) // <-- 4、初始化计算属性
if (opts.watch) initWatch(vm, opts.watch) // 5、初始化监听属性
}
还是再看 initState()
这个方法,我们可以看到初始化计算属性紧跟在初始化数据之后。
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
let getter = typeof userDef === 'function' ? userDef : userDef.get // 计算属性是函数或者拥有get/set属性的对象
if (process.env.NODE_ENV !== 'production') {
if (getter === undefined) {
warn(
`No getter function has been defined for computed property "${
key}".`,
vm
)
getter = noop
}
}
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions) // 一个计算属性对应一个Watcher对象
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
而在 initComputed()
方法 ,首先创建一个存放所有计算属性Watch对象的 vm._computedWatchers
属性。然后在循环中将每个计算属性新建一个Watcher对象。
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
...
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {
}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${
expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
}
在新建Watch对象的构造函数里面,首先把自己保存到了 vm._watchers
数组里面。这个数组保存了Vue实例所有的Watcher对象。然后将 this.getter = expOrFn
这个expOrFn函数就是 initComputed
方法里面的userDef或者userDef.get,就是用户自己定义计算属性时手写的方法,或者手写计算对象里面的get函数。最后对this.value进行赋值,这个this.value就是Watcher的返回值。也就是计算属性的返回值。
接下来我们就看看调用的this.get()方法如何得到计算属性的结果。
get () {
pushTarget(this)
let value
const vm = this.vm
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${
this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
// dep.js里面导出的方法
export function pushTarget (_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
在get方法里面首先调用了pushTarget方法,这个方法是在dep.js文件里面,主要作用就是把当前正在实例化的Watcher对象赋值到Dep.target这个全局变量里面。
之后进行了一个判断,但是无论是真还是假,都会运行语句 value = this.getter.call(vm, vm)
运行this.getter方法,就是直接运行程序员写的计算属性方法,得到方法的返回值。而在运行这个方法时就会触发data对象里面的Observer的get方法。从而触发Watcher对象的依赖收集。
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) {
...
}
}
在get方法里面会先判断当时Dep.target这个全局变量有没有值,很明显,在我们实例化Watcher对象的构造函数里面正好为Dep.target赋值了,所以最后运行 dep.depend()
方法。这个方法也是依赖收集的重点。
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
...
depend () {
if (Dep.target) {
Dep.target.addDep(this) // <-- 1、watcher与dep产生联系
}
}
addSub (sub: Watcher) {
this.subs.push(sub) // <-- 4、将watcher添加进subs数组
}
}
export default class Watcher {
...
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep) // <-- 2、将dep添加进newDeps数组
if (!this.depIds.has(id)) {
dep.addSub(this) // <-- 3、调用dep的添加方法
}
}
}
}
在dep.depend()这个方法主要就是运行一件事,就是Dep.target.addDep(this),Dep.target就是当前正在实例化的Watcher对象,所以这个语句也就是watcher.addDep(this),这个this就是计算依赖data当前属性的里面的Dep对象。Watcher对象与Dep对象终于产生了联系。
而在addDep方法里面首先将dep对象添加到了watcher对象里面的newDeps数组里面,然后调用了dep.addSub(this)方法,将watcher对象添回到dep对象里面的subs数组。
这里我们可以发现,dep对象和watcher对象是双向引用的,分别有一个数组进行保存。这样dep在属性set方法调用时,可以通知自己影响的数组里面所有的watcher更新。而watcher也知道自己依赖哪些dep对象。
而Observer对象主要是为属性代理get和set方法提供的载体。dep对象和watcher对象才是属性与视图建立的双向联系。
可能的依赖关系:
属性<–依赖–视图
属性<–依赖–计算属性<–依赖–视图
属性<–依赖–计算属性<–依赖–计算属性<–依赖–视图
视图也是由Watcher对象建立与data属性的依赖关系的。在Vue实例化后面,我们可以看到。
defineComputed 定义计算属性
计算属性也有可能被其它属性或者视图依赖,所以计算属性也应该与data里面的属性一样,也被代理才对。所以让我们继续把计算属性的逻辑看完。
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef) // <-- 计算属性后面的逻辑
}
新建Watcher对象之后,计算属性通过watcher建立与data属性的依赖关系。我们看一下之后调用 defineComputed()
方法。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (target: any, key: string, userDef: Object | Function) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
这个defineComputed方法主要功能就是在vm代理出一个计算属性让用户可以通过this.XXX访问到计算属性。其中sharedPropertyDefinition定义的计算属性的修饰器,其中定义属性可枚举,可配置,set和get。我们可以看到当计算属性为函数时get是通过createComputedGetter方法定义的,而set为空函数。不是函数时则通过userDef.get和userDef.set定义。
下面我们重点说说这个createComputedGetter方法。
function createComputedGetter (key) {
return function computedGetter () {
// 返回计算属性get代理方法
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// ???
watcher.evaluate()
}
if (Dep.target) {
watcher.depend() // <-- 计算属性也可以触发watcher依赖收集
}
return watcher.value
}
}
}
export default class Watcher {
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend() // <-- watcher调用自己的dep依赖数组进行其它watcher的依赖收集
}
}
}
可以看到createComputedGetter方法返回的是代理get方法,返回watcher.value的值。一个计算属性就有一个watcher对象。甚至可以说watcher才是计算属性的本体。因为计算,更新的逻辑都在watcher里面。计算属性本身只是watcher的壳而已。
在这个方法里面watcher对象与data里面的dep起到了相同的作用,都可以进行依赖收集。不过我们看到watcher.depend()并不直接收集,因为他也依赖其它属性,所以它直接调用自己deps数组进行收集 this.deps[i].depend()
,让自己依赖的属性直接去收集。
也就是说watcher对象之间是没有直接依赖关系的,依赖被转移到了dep这一层。但是要注意,获取值的时候,计算属性还是通过自己依赖的计算获取新的值,并不能直接通过dep得到值。
dep可以通知依赖自己的watcher,watcher并不一定直接去得到dep属性的新值,也可能通过其它watcher得到新值。这样说的话,在set方法调用时,触发更新的watcher的先后顺序就成了重中之重。
因为如果依赖其它计算属性的watcher先更新,那它得到的计算属性此时还没有更新,得到的value值是缓存之前的值。这样watcher实际上是没有更新的。
这个问题我们在依赖更新的时候再讨论吧。下面是简单的计算属性和data依赖图。
data属性(dep)<--依赖--计算属性(先更新)
^ ^
| |
依赖<<<<<<转移<<<<<<<依赖(假)
| |
计算属性(后更新) -------
Vue 数据属性依赖收集流程总结
vm._data = {
firstName: {
__ob__: Observer {
dep: Dep {
...
subs: Watcher [] // 依赖此项属性的所有监听器数组
}
},
// 代理set方法
set firstName: function {
...
dep.notify() // 触发所有监听器的更新
},
// 代理get方法
get firstName: function {
...
if(Dep.target) {
// 创建Watcher监听器此项有值
dep.depend() // 进行依赖(监听器)收集
...
}
return value
}
}
}
数组直接赋值不能更新原因
data() {
return {
a: [
{
name: 'hello'
}
]
}
}
// 方法1 只修改数组对象里面的属性
this.a[0].name = 'world' // 修改一开始就存在的属性
this.$set(this.a[0], "age", '15') // 添加不存在的属性
// 方法2 修改数组整个对象
this.a[0] = {
name: 'world'} // <-- 直接修改不生效
this.$set(this.a, 0, {
name: 'world'}) // 修改数组对象
this.a.splice(0, 1, {
name: 'world'}) // 另一种修改数组对象
this.a.push({
name: 'world'}) // 添加数组对象
this.a.splice(0, 0, {
name: 'world'}) // 另一种添加数组对象
// 方法3
this.$forceUpdate() // 直接手动刷新