前端日问,巩固基础,不打烊!!!
源码系列一直是小柒想总结的部分,之后也会逐步更新。
发布者
,主要作用调用defineReactive函数,在defineReactive函数中使用Object。defineProperty方法对对象的每一个属性添加get与set方法调度中心/ 订阅器
,作用是收集观察者watcher,以及通知watcher进行更新。每个对象的属性都有自己的订阅器dep实例,用(dep.subs)存放所有订阅了该属性的watcher对象,当数据变化时,会遍历(dep.subs),通知所有的watcher,执行update方法。观察者/订阅者
。分为三种类型:渲染watcher、计算属性watcher、侦听器watcher。三种watcher的顺序: `计算属性watcher --> 侦听器watcher --> 渲染watcher 。原因:尽可能保证,在更新组件视图时,computed属性已经是最新的值,如果 渲染watcher 排在 计算属性watcher 前面,就会导致页面更新的时候 computed 值为旧数据。在beforeCreate钩子函数后,我们就可以拿到props、data等数据,看一下代码如何实现:
// src/core/instance/state.js
export function initState(vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 初始化props
if (opts.methods) initMethods(vm, opts.methods) // 初始化methods
if (opts.data) initData(vm) // 初始化data
if (opts.computed) initComputed(vm, opts.computed) // 初始化computed
if (opts.watch) initWatch(vm, opts.watch) // 初始化watch
}
}
可以看到:props
、methods
、data
、computed
、watcher
都依次初始化。
我们看一下initData
方法的具体实现:
// src/core/instance/state.js
function initData(vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
observe(data, true /* asRootData */) // 给data做响应式处理
}
看到observe
字眼有没有很激动。在这个方法里执行了observe(data, true)
,看看这个方法的具体实现:
// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
let ob: Observer | void
ob = new Observer(value)
return ob
}
直接返回了Observer
构造函数的实例ob
,并且将data
作为参数传递。了解new
原理的你一定知道,new一个实例,将会执行一遍其构造函数。
那我们看看这个Observer
构造函数里做了什么:
// src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
value: any;
this.dep = new Dep()
def(value, '__ob__', this) // def方法保证不可枚举
this.walk(value)
}
// 遍历对象的每一个属性并将它们转换为getter/setter
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) { // 把所有可遍历的对象响应式化
defineReactive(obj, keys[i])
}
}
}
export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
const dep = new Dep() // 在每个响应式键值的闭包中定义一个dep对象
// 如果之前该对象已经预设了getter/setter则将其缓存,新定义的getter/setter中会将其执行
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 // 如果原本对象拥有getter方法则执行
if (Dep.target) { // 如果当前有watcher在读取当前值
dep.depend() // 那么进行依赖收集,dep.addSub
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val // 先getter
if (newVal === value || (newVal !== newVal && value !== value)) { // 如果跟原来值一样则不管
return
}
if (setter) { setter.call(obj, newVal) } // 如果原本对象拥有setter方法则执行
else { val = newVal }
dep.notify() // 如果发生变更,则通知更新,调用watcher.update()
}
})
}
constructor
函数中,先给每个实例添加了一个dep
实例,然后遍历对象(这里就是上面的参数data)的每一个属性,并且执行defineReactive(obj, keys[i])
。defineReactive
函数,给每个属性定义了一个dep
对象,并且给每个属性都添加get
和set
方法,分别用于收集依赖(dep.depend())
与触发依赖(dep.notify())
。当Dep.target
存在时,才会收集依赖。(Dep.target就是一个watcher)上面的代码中多次用到Dep
,对于依赖收集
这个词也不陌生, 简单来说,收集依赖就是收集watcher。如何收集,我们接着看:
// src/core/observer/dep.js
let uid = 0 // Dep实例的id,为了方便去重
export class Dep {
static target: ?Watcher; // 全局属性Dep.target
id: number;
subs: Array<Watcher>; // 存储watcher的数组
constructor () {
this.id = uid++
this.subs = [] // 用来收集watcher
}
// 添加一个watcher到subs数组中
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除一个watcher
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 调用watcher实例的addDep方法
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知数组中的每一个watcher实例进行更新
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
//待处理的观察者队列
const targetStack = [];
export function pushTarget(_target) {
//如果当前有正在处理的观察者,将他压入待处理队列
if(Dep.target) {
targetStack.push(Dep.target);
}
//将Dep.target指向需要处理的观察者
Dep.target = _target;
}
export function popTarget() {
//将Dep.target指向栈顶的观察者,并将他移除队列
Dep.target = targetStack.pop();
}
Dep中首先定义了一个全局属性target,只能Dep类自己调用,创建这个Dep类时,Dep.taget == null。Dep.target 只是一个标记,存储是watcher实例。
Dep中提供了下面几个方法:
Watcher
实例对象,并将其添加到subs
数组中Watcher
实例对象,只是从subs
数组中移除这个对象Dep.target
存在时(也就是当前Watcher实例存在时),调用该实例的addDep
方法。addDep功能下面在说。watcher
实例,执行其update
方法,完成更新操作。代码最后还暴露了两个方法pushTarget
与popTarget
。 Dep.target 的值会在调用 pushTarget 和 popTarget 时被赋值,值为当前 watcher 实例对象。
大家现在应该还有疑惑?Watcher是什么?它与Dep是什么关系? watcher实例的addDep方法功能是什么?看看Watcher类的实现:Watcher的代码比较多,小柒省略部分代码:
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function, // expOrFn可以是字符串或者函数 , watch 的属性名称
cb: Function, // 回调函数
options?: ?Object,
isRenderWatcher?: boolean // 是否是渲染watcher标志位,Vue 初始化时,为true
){
this.vm = vm
if(isRenderWatcher) {
vm._watcher = this
}
vm._watcher.push(this)
if(options) {
this.deep = !!options.deep; //是否启用深度监听
this.user = !!options.user; //主要用于错误处理,侦听器 watcher的 user为true,其他基本为false
this.lazy = !!options.lazy; //惰性求值,当属于计算属性watcher时为true
this.sync = !!options.sync; //标记为同步计算,三大类型暂无
}else {
this.deep = this.user = this.lazy = this.sync = false;
}
// 初始化各种属性
this.cb = cb; //观察者的回调 ,除了侦听器 watcher外,其他大多为空函数
this.id = ++uid$1; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
// 解析expOrfn,赋值给this.getter
// 当是渲染watcher时,expOrfn是updateComonent,即重新渲染执行render(_update)
// 当是计算watcher时,expOrFn是计算属性的计算方法
// 当是侦听器watcher时,expOrFn是watch属性的名字,this.cb就是watch的handler属性
this.expression = expOrFn.toString();
// 上面说了expOrFn可以是字符串,可以是函数:
// 什么时候会是字符串,例如我们正常使用的时候,watch: { x: fn }, Vue内部会将 `x` 这个key 转化为字符串
// 什么时候会是函数,其实 Vue 初始化时,就是传入的渲染函数 new Watcher(vm, updateComponent, ...);
//对于渲染watcher和计算watcher来说,expOrFn的值是一个函数,可以直接设置getter
//对于侦听器watcher来说,expOrFn是watch属性的名字,会使用parsePath函数,该函数返回watch属性的值(即返回一个函数)
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
// 省略一丢丢代码...
}
// 这里调用了this.get(),也就意味着 new Watcher 时会调用 this.get()
// this.lazy 是修饰符,除非用户自己传入,不然都是 false。可以先不管它
this.value = this.lazy
? undefined
: this.get()
}
// 取值操作
get (){
// 将Dep.target设置为当前的watcher实例,并将 Dep.target入栈 ,存入targetStack数组中
pushTarget(this)
// 省略部分代码...
try {
// 这里执行了 this.getter,获取到 属性的初始值
// 如果是初始化时 传入的 updateComponent 函数,这个时候会返回 udnefined
value = this.getter.call(vm, vm)
} catch (e) {
// 省略部分代码...
} finally {
// 省略部分代码...
// 出栈 targetStack数组移除当前的watcher
popTarget()
// 省略部分代码...
}
// 返回属性的值
return value
}
// 这里再回顾一下
// dep.depend 方法,会执行 Dep.target.addDep(dep) 其实也就是 watcher.addDep(dep)
// watcher.addDep(dep) 会执行 dep.addSub(watcher)
// 将当前 watcher 实例 添加到 dep 的 subs 数组 中,也就是收集依赖
// dep.depend 和 这个 addDep 方法,有好几个 this, 可能有点绕。
addDep (dep: Dep) {
const id = dep.id
// 下面两个 if 条件都是去重的作用,我们可以暂时不考虑它们
// 只需要知道,这个方法 执行 了 dep.addSub(this)
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 将当前 watcher 实例添加到 dep 的 subs 数组中
dep.addSub(this)
}
}
}
// 派发更新
update () {
// 三种watcher,只有计算属性watcher的lazy设置了true,表示启动惰性求值
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// this.sync 为true表示立即执行 this.run; 不是三大类型,会走下面的queueWatcher
this.run()
} else {
// queueWatcher 内部也是执行的 watcher实例的 run 方法,只不过内部调用了 nextTick 做性能优化。
// 它会将当前 watcher 实例放入一个队列,在下一次事件循环时,遍历队列并执行每个 watcher实例的run() 方法
queueWatcher(this)
}
}
// 在update执行后,会执行run
run () {
if (this.active) {
// 获取新的属性值
const value = this.get()
if (
// 如果新值不等于旧值
value !== this.value ||
// 如果新值是一个 引用 类型,那么一定要触发回调
// 举个例子,如果旧值本来就是一个对象,
// 在新值内,我们只改变对象内的某个属性值,那新值和旧值本身还是相等的
// 也就是说,如果 this.get 返回的是一个引用类型,那么一定要触发回调
isObject(value) ||
// 是否深度 watch
this.deep
) {
// 设置新值
const oldValue = this.value
this.value = value
// this.user 是一个标志符,如果开发者添加的 watch 选项,这个值默认为 true
// 如果是用户自己添加的 watch ,就加一个 try catch。方便用户调试。否则直接执行回调。
if (this.user) {
try {
// 触发回调,并将 新值和旧值 作为参数
// 这也就是为什么,我们写 watch 时,可以这样写: function (newVal, oldVal) { // do }
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)
}
}
}
}
// 省略部分代码...
// 以下是 Watcher 类的其他方法
cleanUpDeps() { }
evaluate() { }
depend() { }
teardown() { }
}
上面的主要代码注释部分解释的很清楚了。
数据的dep添加到自己的deps中
,方便其他地方使用。