前言
最近看了下vue的源码(2.0版),决定就核心部分-响应式原理做一个沉淀,欢迎感兴趣的小伙伴阅读
源码解析
上图是官方给的流程图,为了方便理解,我将从以下顺序进行分析
- Data部分,Vue对data做了什么
- 为什么要进行依赖收集,又是怎么收集的
- Watcher是什么,在其中起了什么作用
- 组件的re-render是在什么时候怎么触发的
1.双向绑定
在看源码之前我们就知道,Vue是MVVM的框架,能将数据做到双向绑定,它对data
的每个属性都用Object.defineProperty
定义了getter
和setter
,但人家肯定不是这一句话就写完的,具体是怎么实现的呢?
data初始化
让我们打开Vue的源码,翻到vue/src/core/instance
,选择这个目录是因为里面的代码基本都是一些初始化的操作,里面一定会有对data
的初始化
根据程序员的直觉,我们先看看index.js
,可以看到里面主要执行了几个方法
// index.js
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
复制代码
然后我们看下第一个函数initMixin
,根据引入目录,我们在init.js
中找到了这个函数,看到在里面又执行了几个方法
// init.js
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化时间
initRender(vm) // 初始化render
callHook(vm, 'beforeCreate') // 触发beforeCreate
initInjections(vm) // resolve injections before data/props
initState(vm) // 上面是before,下面是after,中间是什么?
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 触发created
复制代码
根据命名和注释,我猜data
的初始化应该是在initState
函数中
果然!在state.js
里的initState
中幸运的找到了initData
函数!不亏是大框架层级真深。。。
// state.js
export function initState (vm: Component) {
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) // here~
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
除了初始化data
,这个函数里还处理了props
、methods
、computed
和watch
,这里不多解释,感兴趣的可以自己去看看
这次我们重点看initData
:
// state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, 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
)
}
// 上面代码的意思是要求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]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// 上面代码意思method、data、props里字段命名不能冲突
observe(data, true /* asRootData */)
}
复制代码
可以看出,其实重点的操作在最后的observe
函数
observer
顺着observe
的引入目录我们来到vue/src/core/observer
,在index.js
中找到了这个函数本体
// index.js
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
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
或者返回已存在的Observer
Observer
是啥?又干了什么事?不着急,慢慢往下看
刚说到new
了下Observer
,所以我们直接看Observer
类的构造函数
// index.js
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
复制代码
可以看出,我们的data
通过observe
改名为value
然后传到constructor
最终被做了这样的处理:数组的话执行observerArray
函数,不是数组则执行walk
函数,而这两个函数源码就放在了构造函数下面很容易就找到了!
// index.js
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
// 把data每一项都definRective
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 将数组遍历,把每一项再observe,最终还是相当于每一项都definRective
observe(items[i])
}
}
复制代码
所以最终都会到达defineReactive
函数:
// index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
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
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
// 定义getter和setter
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
复制代码
getter和setter
这个函数看起来很长,其实可以归纳如下:
// 1.创建了个dep
const dep=new Dep()
Object.defineProperty(obj, key, {
get: function reactiveGetter () {
// 2.判断target存在,就执行depend(),这里就是收集依赖
if (Dep.target) {
dep.depend()
}
return value
},
set: function reactiveSetter (newVal) {
val = newVal
// 3.通知依赖更新
dep.notify()
}
}
复制代码
这样是不是就简单多啦,所以可以看出Vue
的data
就是在这里被定义getter
和setter
的
当我们对data
执行get
操作时就会触发getter
,执行set
操作时就会触发setter
思考1:如果我动态往data里新加一个属性a,操作a是否会触发getter和setter?
答案:不会 cn.vuejs.org/v2/api/#Vue…
思考2:在Vue中我们用push方法向data的某个数组变量中增加一个属性,会触发setter吗?结论是会的,Vue对数组方法做了特殊处理,想知道时怎么处理的话就去源码(observer/array.js)中寻找答案吧~
2.观察者模式
Dep
我们注意到getter
和setter
中有个出现频率很高的词——dep
。那么dep
是什么,又是怎么通过它进行依赖收集呢?
顺着引入路径,我们来到observer/dep.js
,defineReactive
归纳后我标注的1、2、3三个步骤的操作分别对应如下:
// dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
// 1.new Dep()时创建了空的subs,这是用来存依赖的数组
constructor () {
this.id = uid++
this.subs = []
}
// 2.dep.depend() 执行了target的addDep()
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 3.dep.notify()对subs数组里每个项执行了update,也就是通知每个依赖更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
复制代码
这么一看,好像没有向依赖数组subs
存入内容的操作,因为depend
函数也并没有对subs
做类似push
的行为,收集依赖
的步骤在哪呢?
好奇的小伙伴肯定注意到了target
,这又是啥,从哪来的,是做啥的?
其实这是理解依赖收集的关键,通过在dep.js
中搜索‘target’我们在上面代码第一行看到关于target
类型的定义是一个Watcher
类,然后在最下面搜到这样的代码:
// dep.js
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
复制代码
target
应该就是在这里被注入灵魂的,注释上说Dep.target
必须保持全局有且只有一个,这也解释了这段代码的功能,也是为什么在依赖收集时可以直接判断Dep.target
那这俩函数pushTarget
和popTarget
在哪调用的呢?而Watcher
,根据命名和平时的使用,我们猜测,Watcher
应该是个能观察到数据变化的工具,先去Watcher
里看看吧
Watcher–观察者
带着疑惑我们打开了observer/watcher.js
,果然在里面找到了pushTarget
和popTarget
的调用!它在一个名叫get
的函数里
// watcher.js
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 1.赋值target Dep.target = this
pushTarget(this)
let value
const vm = this.vm
try {
// 2.执行this.getter
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// 3.收集完成后删掉target
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
复制代码
也就是说,target
被赋值了this
,也就是当前的Watcher
那this.getter
是什么呢,我们在Watcher
的构造函数中找到了他
// watcher.js
// expOrFn是创建Watcher时传进来的,要观察的表达式,这里把表达式转成了getter函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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.getter
相当于是观察内容的的赋值逻辑,怎么理解呢:
举例我创建了一个Watcher
观察一个属性a
,而a
的赋值逻辑为data.b + data.c
,那么this.getter
就相当于执行了data.b + data.c
接着在构造函数中我们也幸运的找到了上面提到的get
函数的调用位置
// watcher.js
this.value = this.lazy
? undefined
: this.get()
复制代码
依赖收集
现在我们已经解开了大部分谜题,为了方便理解,我们模拟一下整个流程,还是刚才的例子,我们有一个变量a
,而a
的赋值逻辑涉及了data.b
和data.c
现在我们new Watcher()
把它传入,然后走到了构造函数,执行this.get()
,get()
会创建target=Watcher
并执行this.getter()
,而this.getter()
就相当于执行a
的赋值逻辑,就会 “触碰” 到data.b
和data.c
,因为data
所有属性都在初始化的时候被定义了getter
和setter
,当被 “触碰” 就会触发data.b
和data.c
的getter
前方高能
也就是执行
// observe.js
// 创建Watcher与data的桥梁dep(记住是data.b或data.c的dep,跟a无关)
const dep=new Dep()
// getter:
if (Dep.target) {
// target有吗?有的,刚才get()里创建了,值为当前Watcher
dep.depend()
}
复制代码
执行
// observe.js
dep.depend()
复制代码
等于
// dep.js
Dep.target.addDep(this)
复制代码
也就是执行Watcher
里的addDep
:
// watcher.js
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)
}
}
}
复制代码
等于
// watcher.js
dep.addSub(this)
复制代码
等于
// dep.js
addSub (sub: Watcher) {
this.subs.push(sub)
}
复制代码
就是这样,兜兜转转,最终data.b
和data.c
的subs
里成功放入了a
的Watcher
,也就完成了依赖收集的步骤!
依赖更新
依赖收集完了,Watcher存好了,怎么更新的呢?
假设现在data.b
或data.c
被set
了新的值,那么就会触发他们的setter
:
继续高能
也就是执行
// observe.js
// setter:
dep.notify()
复制代码
也就是
// dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs are not sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
// here~ 这里subs里只有一个,就是a的Watcher
subs[i].update()
}
}
复制代码
也就是
// watcher.js
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
复制代码
默认会走到this.run()
,其他两种模式这次就先不做解释啦
run () {
if (this.active) {
const value = this.get()
// 下面是一些value变化后回调逻辑
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
复制代码
约等于
this.value=this.get()
复制代码
get
函数刚刚有解释哦,里面会执行a
的赋值逻辑,a
就顺利被更新了!
至此,整条响应式链路就理清楚了!
组件的响应式
当然a、b、c都是我的举例,实际组件的响应式和例子也很相似。
首先每一个组件就相当于例子中的a,都会配一个Watcher
,只不过而组件的赋值逻辑有一个专属函数,也就是渲染函数render
render
执行过程中就会生成DOM
,而DOM
中所需要的data
就相当于例子中的b、c
一样会被 “触碰” ,然后就像例子中一样,往所有data
的依赖中放入组件的Watcher
当data
变化时就会触发Watcher
的执行赋值逻辑re-render
,然后组件就成功被更新啦~
思考:这部分源码在哪里呢?试着找到然后研究下?
总结
- Watcher的作用印证了我们的猜想,它就是一个观察者,可以观察一个数据变化
- Dep则是一个订阅者,主要的作用就是收集依赖也就是收集观察者Watcher和通知观察者更新
- Observer的主要作用是使用
Object.defineProperty
方法对Data
的每一个子属性定义getter
和setter
,然后劫持他们的get
和set
操作
——————————————end——————————————
才疏学浅,大佬轻喷~