vue最重要的应该就是响应式更新了,刚开始接触vue或多或少都能从官方文档或者其余地方知道vue响应式更新依赖于Object.defineProperty()
方法,这个方法在MDN上有详细讲解,不过,若是是初学者的话,直接去看响应式更新源码还有点难度的,最好是先用项目练一遍,对vue有个相对熟悉的了解,而后能够去各大热门讲解的博客上看看人家的讲解,这样汇总一番有点底子了再去看源码实现相对轻松点。 最低级别的监听能够看我这个库:https://github.com/lizhongzhen11/obj 参考:https://segmentfault.com/a/1190000009054946 https://segmentfault.com/a/1190000004384515vue
从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
中的相关代码:java
// index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// array.js
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文件没有;
结尾致使的错误react
;['push','pop','shift','unshift','splice','sort','reverse']
复制代码
接下来,go on!定义了一个“观察状态”变量,内部有一个是否能够覆盖的布尔属性。注释里面说不想强制覆盖冻结数据结构下的嵌套值,以免优化失败。git
export const observerState = {
shouldConvert: true
}
复制代码
继续往下看,来到了重头戏:Observer
类,注释中也说的明白:该类属于每一个被观察的对象,observer
在目标对象的属性的getter/setters
覆盖键同时搜集依赖以及分发更新。es6
import Dep from './dep'
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)
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
里面相关的代码了:github
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 () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 更新 Watcher 数组中的数据
}
}
}
复制代码
Dep
内部用到了Watcher
,而Watcher
又来自watcher.js
。先说Dep
,内部主要对Watcher
类型的数组进行增长删除以及更新维护,本身内部没有什么太多复杂的逻辑,主要仍是在watcher.js
中。接下来列出watcher.js
相关代码:vue-cli
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 // 直接在vue 页面里打印 this 能够找到_watcher属性
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.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 // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set() // es6语法,相似java Set集合,不会添加剧复数据
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
// 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()
}
复制代码
上面构造函数第一个参数vm
是什么?若是一直用vue-cli
构建工具开发的话,可能没怎么注意过,**其实vm
就是vue
的一个实例!!!**第二个参数expOrFn
暂时还不清楚,若是是函数的话直接赋给this.getter
,不然this.getter
直接指向一个空函数,同时还发出警报,须要传递一个函数。最后,判断this.lazy
,为true
的话调用this.get()
方法:express
import Dep, { pushTarget, popTarget } from './dep'
/** * Evaluate the getter, and re-collect dependencies. * 对 getter 求值,并从新收集依赖 */
get () {
pushTarget(this) // 至关于 Dep.target = 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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps() // 清理deps,为了依赖收集
}
return value
}
// dep.js
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
移除。segmentfault
/** * 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() // newDepIds 是Set类型,能够经过clear()清空
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
复制代码
cleanupDeps()
方法将旧的依赖编号与新的依赖集合编号进行对比,若是旧依赖数组中存在的编号,而新依赖集合编号中不存在,就须要删除对应编号的依赖;接下来交换新旧依赖集合编号,而后清空this.newDepIds
(其实此时该集合内保存的是旧有的依赖集合编号);随后交换新旧依赖数组,而后来了一步骚操做:this.newDeps.length = 0
,将this.newDeps
清空,比较骚。api
也就是说,利用
get()
方法求值后会清理依赖收集。 到了get()
能够先暂停回顾一下。这里是在Watcher
构造函数中调用的,也就是说,当new Watcher()
时就会走遍上述代码,包括调用get()
来取值。
这时候若是继续强行看完Watcher
下面的源码,会发现没什么头绪,因此依然回到index.js
中。继续研究Observer
类的构造函数。
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
内,下面贴出源码:
/** * 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()
方法内部实现:
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 // 缓存对象属性内的get方法
const setter = property && property.set // 缓存对象属性内的set方法
let childOb = !shallow && observe(val) // observe(val)尝试返回一个 observer实例,若是 !shallow === true 那么 childOb === ob
// 其实也能够理解为, childOb === val.__ob__
Object.defineProperty(obj, key, { // 这里开始是真正的核心所在,其实就是从新对象的get、set方法,方便监听
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // getter 存在的话就调用原生的 get 方法取值,不然用传进来的值
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()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal) // childOb === newVal.__ob__
dep.notify() // 内部调用了 watcher.js 里面的 uodate(),内部又调用了 run(),run()里面设置值,其中还用到了watcher队列
}
})
}
复制代码
响应式更新的重中之重就是首先得监听到对象属性值的改变,
vue
经过defineReactive()
内部重写传入的对象属性中的set
以及get
方法,其中,js
原生的call()
也有很大的功劳。
再一次看vue
源码明显比第一次看好多了,可是不断地调用其它方法,理解上仍是有必定的难度,这一次阅读源码更多的就是作个笔记,写得并很差,可是留个印象,方便下次再看。