一、版本:2.5.9
二、建议
vue最重要的应该就是响应式更新了,刚开始接触vue或多或少都能从官方文档或者其他地方知道vue响应式更新依赖于Object.defineProperty()
方法,这个方法在MDN上有详细讲解,不过,如果是初学者的话,直接去看响应式更新源码还有点难度的,最好是先用项目练一遍,对vue有个相对熟悉的了解,然后可以去各大热门讲解的博客上看看人家的讲解,这样汇总一番有点底子了再去看源码实现相对轻松点。
最低级别的监听可以看我这个库:https://github.com/lizhongzhen11/obj
参考:https://segmentfault.com/a/1190000009054946
https://segmentfault.com/a/1190000004384515
三、迎接挑战吧—鬼气·九刀流·阿修罗
从github上把vueclone下来,或者直接在github上看也行。
别的先不管,直接去src/core/observer文件夹,这个明显就是vue响应式更新源码精华所在,内部共有array.js
,dep.js
,index.js
,scheduler.js
,traverse.js
,watcher.js
6个文件,先看哪一个呢?第一次看没有头绪的话就先看index.js
。
index.js
开头import
了不少文件,先不用管,往下看需要用到时再去查找不迟。而第一步就用到了arrayMethods
,该对象来自array.js
,下面同时列出array.js
中的相关代码:
1 2 3 4 5 6 7
| import { arrayMethods } from './array' const arrayKeys = Object.getOwnPropertyNames(arrayMethods) import { def } from '../util/index' const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto)
|
如上所示,arrayMethods
其实是一个Array.prototype
的实例,只不过中间经过arrayProto
过渡,一开始我还在纠结下方的代码(对数组push
等方法遍历添加到刚刚创建的实例arrayMethods
中,这里没有列出来),因为没看到下方代码有export
,感觉很奇怪,而且他代码是下面这样的,[]
前有个;
,感觉很奇怪,vue作者是不写;
的,这里出现一个;
感觉很突兀。PS:后来问了前辈,前辈解释说:在js文件合并的时候,防止前一个js文件没有;
结尾导致的错误
1
| ;['push','pop','shift','unshift','splice','sort','reverse']
|
接下来,go on!定义了一个“观察状态”变量,内部有一个是否可以覆盖的布尔属性。注释里面说不想强制覆盖冻结数据结构下的嵌套值,以避免优化失败。
1 2 3
| export const observerState = { shouldConvert: true }
|
继续往下看,来到了重头戏:Observer
类,注释中也说的明白:该类属于每个被观察的对象,observer
在目标对象的属性的getter/setters
覆盖键同时搜集依赖以及分发更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import Dep from './dep' export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } * Walk through each property 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++) { defineReactive(obj, keys[i], obj[keys[i]]) } } * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
|
构造函数里面第二步this.dep = new Dep()
,这个Dep
来自dep.js
,这时候,得需要去看看dep.js
里面相关的代码了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| let uid = 0 * A dep is an observable that can have multiple * directives subscribing to it. * dep是可观察的,可以有多个指令订阅它 */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
|
Dep
内部用到了Watcher
,而Watcher
又来自watcher.js
。先说Dep
,内部主要对Watcher
类型的数组进行增加删除以及更新维护,自己内部没有什么太多复杂的逻辑,主要还是在watcher.js
中。接下来列出watcher.js
相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| let uid = 0 * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ export default class Watcher { constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid this.active = true this.dirty = this.lazy this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' 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() }
|
上面构造函数第一个参数vm
是什么?如果一直用vue-cli
构建工具开发的话,可能没怎么注意过,其实vm
就是vue
的一个实例!!!第二个参数expOrFn
暂时还不清楚,如果是函数的话直接赋给this.getter
,否则this.getter
直接指向一个空函数,同时还发出警报,需要传递一个函数。最后,判断this.lazy
,为true
的话调用this.get()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import Dep, { pushTarget, popTarget } from './dep' * Evaluate the getter, and re-collect dependencies. * 对 getter 求值,并重新收集依赖 */ get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } export function pushTarget (_target: Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() }
|
get()
中最终会判断cthis.deep
是否为true
,如果是调用traverse(value)
,而traverse()
来自traverse.js
,其目的是把dep.id
加进去;popTarget()
是为了将之前pushTarget(this)
的target
移除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
|
cleanupDeps()
方法将旧的依赖编号与新的依赖集合编号进行对比,如果旧依赖数组中存在的编号,而新依赖集合编号中不存在,就需要删除对应编号的依赖;接下来交换新旧依赖集合编号,然后清空this.newDepIds
(其实此时该集合内保存的是旧有的依赖集合编号);随后交换新旧依赖数组,然后来了一步骚操作:this.newDeps.length = 0
,将this.newDeps
清空,比较骚。
也就是说,利用get()
方法求值后会清理依赖收集。
到了get()
可以先暂停回顾一下。这里是在Watcher
构造函数中调用的,也就是说,当new Watcher()
时就会走遍上述代码,包括调用get()
来取值。
这时候如果继续强行看完Watcher
下面的源码,会发现没什么头绪,所以依然回到index.js
中。继续研究Observer
类的构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13
| constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } }
|
构造函数中紧跟着调用了def(value, '__ob__', this)
,这个方法是干嘛的?在哪里?
通过查找发现def
方法位于util/lang.js
内,下面贴出源码:
1 2 3 4 5 6 7 8 9 10 11
| * Define a property. */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
|
def
内部调用了Object.defineProperty()
,结合Observer
构造函数的传参,可知这里给每个对象定义了一个__ob__
属性,在日常开发中,当我们打印输出时经常能看到__ob__
。
接下来进一步判断value
是不是数组,如果不是的话调用walk()
,当然要确保参数是Object
,然后遍历对象的key
并且每个调用defineReactive(obj, keys[i], obj[keys[i]])
。
看看defineReactive()
方法内部实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| 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 } const getter = property && property.get const setter = property && property.set let childOb = !shallow && 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 (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
|
响应式更新的重中之重就是首先得监听到对象属性值的改变,vue
通过defineReactive()
内部重写传入的对象属性中的set
以及get
方法,其中,js
原生的call()
也有很大的功劳。
总结
再一次看vue
源码明显比第一次看好多了,但是不断地调用其它方法,理解上还是有一定的难度,这一次阅读源码更多的就是做个笔记,写得并不好,但是留个印象,方便下次再看。