响应式系统(四)

前言

通过前三章我们还有Watcher(观察者)、Dep(依赖收集容器)未解

Watcher里有deps、depIds存储dep实例,这是个一(Watcher也就是观察者,这里可指$watch回调、渲染函数等)对多(dep也就是依赖,这里可以想象成数据,eg: this.a)关系
也就是一个Watcher观察了多个数据,eg: render()引用了多个data

Dep里有subs存储watcher实例,这也是一(dep也就是依赖,这里可以想象成数据,eg: this.a)对多(Watcher也就是观察者,这里可指$watch回调、渲染函数等)关系
也就是一个data用在了多个地方,eg: this.arender()引用也被计算属性comB引用

也就是一个数据在多处引用,而一个渲染函数也引用多个数据

正文

Watcher

我们知道它就是想方设法触发所依赖的数据的get从而收集依赖,这就是核心

export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {}

  get() {}

  addDep(dep: Dep) {}

  cleanupDeps() {}

  update() {}

  run() {}

  getAndInvoke(cb: Function) {}

  evaluate() {}

  depend() {}

  teardown() {}
}

理解一个函数就从参数看起就是,也就是从调用处看起

new Watcher(vm, updateComponent, noop, {
    before () {
        if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */)

这里选择renderWatcher就因为它够特殊,它接受五个参数:vm(实例对象)、expOrFn(被观察的目标,可以访问到待观察的数据即可)、cb(数据变化触发的回调)、options(选项)、isRenderWatcher(是染renderWatcher

constructor
this.vm = vm;
// 若是renderWatcher,则赋值在_watcher上
if (isRenderWatcher) {
    vm._watcher = this;
}
vm._watchers.push(this);

首先将实例对象vm赋值给this.vm,这样子每个Watcher实例都知道它属于哪个组件实例
然后判断下若是renderWatcher(组件renderWatcher)就将当前Watcher实例赋值给vm._watcher(initLifecycle初始化),这样子比如销毁组件就可以取到该组件renderWatcher实例对象(vm._watcher.teardown()
最后将当前Watcher实例对象pushvm._watchers(initState初始化)

if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.computed = !!options.computed;
    this.sync = !!options.sync;
    this.before = options.before;
} else {
    this.deep = this.user = this.computed = this.sync = false;
}

处理下options,就是只接受五个参数:

  • options.deep,用来标识是否深度观测
  • options.user,用来标识是否是用户自定义还是内部定义this.$watch就默认传入options.user = true
  • options.computed,用来标识当前观察者是否是计算属性观察者
  • options.sync,用来标识数据变化之后是否同步求值且调用回调,默认是将其放进异步队列后统一触发
  • options.before,钩子,就是数据变化之后,更新触发之前调用的(eg: beforeUpdate
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.dirty = this.computed; // for computed watchers

这个就dirty提下,也就是它在计算属性下才为真,这个代表是否已求值,true代表未求值

this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();

分为俩组用于存储当前的watcher被哪些dep收集了以及它们的id,用于防止重复收集依赖
deps、depIds是当前的依赖情况,newDeps、newDepIds是页面重新渲染之后的情况也就是数据更新之后的依赖情况(初始时为空,cleanupDeps导致的),也就是重新收集

// 传入的取值表达式,用于报错提示
this.expression =
    process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
// parse expression for getter
// 将expression转成getter
if (typeof expOrFn === "function") {
    this.getter = expOrFn;
} else {
    // 将'a.b.c'转成取值函数
    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
            );
    }
}

这个就是供Watcher取值的源头。expression就是取值表达式字符串,用于开发环境定位使用
expOrFn可传入字符串、函数,所以需要序列化:

  • 函数的话就无须处理
  • 字符串的话(a.b.c),那么就得处理下
if (this.computed) {
    this.value = undefined;
    this.dep = new Dep();
} else {
    this.value = this.get();
}

这里就是核心也就是触发求值,但是计算属性watcher普通watcher不同,普通watcher会立即求值,计算属性watcher其实会在被引用时触发其存取描述符get才求值

这就是computed惰性求值

收集依赖

很明显就是this.value = this.get()触发的,这个除了触发求值还有取到值赋值给this.value

get() {
    // 设置Dep.target
    pushTarget(this)
    let value
    const vm = this.vm
    try {
        // 触发getter用于收集依赖
        value = this.getter.call(vm, vm)
    } catch (e) {
        // ...
    } finally {
        // ...
    }
    return value
}

首先调用pushTarget(this)(源码看下文Dep),这个很明显就是设置Target也就是赋值当前观察者了
然后就是求值了value = this.getter.call(vm, vm),因为这个有是用户自定义的,所以getter不可预测,就得用try/catch
这个会触发get属性访问器,即:

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;
}

这里之前章节基本都讲过,就差dep.depend(),详解如下Dep节,我们可知其调用Watcher.addDep

addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
        // 本轮收集中还没订阅这个dep
        this.newDepIds.add(id)
        this.newDeps.push(dep)
        if (!this.depIds.has(id)) {
            // 还没有订阅这个需要订阅的dep
            dep.addSub(this)
        }
    }
}
// 例子
new Vue({
    data: {
        a: 'a'
    },
    template: `
{{a}}{{a}}
` }).$mount('#app')

数据观测之后有一堆dep,这些dep是基本固定的,这就可以根据dep.id来处理这个重复依赖问题

我们知道观测完数据之后会留下一堆的dep,而Watcher而言(比如renderWatcher)只有一个(以Watcher为视角),数据变化的话我们可以触发dep.notify()。但是这个回调是在Watcher里,我们怎么把这俩个关联起来?
这里其实就是处理重复依赖的核心逻辑,我们先判断当前Watcher是否订阅了这个dep(具体来说就是dep对应的数据,可以使data、computed)(也就是新的渲染函数是否用了数据a,最开始这个记录自然是空的,那么就将此dep.id记录在newDepIds表示这个记录了,dep记录在newDeps

这里就把Dep记录在了Watcher

然后判断下当前的Watcher是否订阅了这个dep(也就是当前渲染函数是否用了数据a,没有的话就将此观察者添加到该dep里,即dep.addSub(this)

这里就把Watcher记录在了Dep上。因为没有的话(!this.depIds.has(id))就说明这个收集订阅a的观察者的容器(dep)没有这个观察者,那么走到这步就是要收集这个观察者,所以就添加了

get() {
    // ...
    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) {
            // 若是deep观测,那么久递归读取子属性值,已达到收集子属性依赖
            traverse(value)
        }
        // 当前watcher完了之后就得置空(pop),轮到新的watcher
        popTarget()
        // 清理下订阅,比如之前订阅了a、b,现在订阅了a、c,那么得把b给清掉就这么个性能优化
        this.cleanupDeps()
    }
    return value
}

回到watcher.get(),若是报错的话判断下若是用户自定义就报下错
接下来判断下是否是深度观测,是的话就traverse(value)(看下文,其实就是遍历访问每一个属性以达到触发其存取描述符get的目的,从而收集依赖),然后popTarget(),当前watcher出栈,轮到下一个watcher
接下来就是很重要的this.cleanupDeps(),这个其实就是每轮收集之后需要清理掉旧的已经不需要的依赖

cleanupDeps() {
    let i = this.deps.length
    // 循环遍历当前订阅过的deps
    while (i--) {
        const dep = this.deps[i]
        if (!this.newDepIds.has(dep.id)) {
            // 若是新的不订阅这个曾经订阅过得dep就得删除
            // 这样子完成了dep的取消订阅
            dep.removeSub(this)
        }
    }
    // 就是更新下当前的订阅列表
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp // 这里得需要,不然this.depIds也会被clear,下同
    this.newDepIds.clear()

    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
}

首先将当前所依赖的依赖集遍历,然后判断新的Watcher里是不是需要这个dep,不需要的话就删掉就是了
然后就将newDepIds(新的)赋给depIds(当前的),然后得清空newDepIds、newDeps俩变量。

  • addDep、cleanupDeps可见newDepIds、newDeps存的是本次求值收集的depdepIds、deps存的是上一次,也就是当前。且每次最后会将newDepIds、newDeps赋值给depIds、deps,然后清空
  • depIds用于在多次求值时避免收集重复依赖,因为它存储的是当前的依赖集列表,不会被清空
  • newDepIds用于在一次求值时避免收集重复依赖,因为它每次求值之后都会被清空,它存储的是新的依赖集列表
  • 先把新的依赖情况给取到,然后就可以和当前的的比对从而去重

最后返回value,这样子就可以被赋值给this.value = this.get()

触发依赖

很明显,触发依赖是设置属性值时触发其set里的逻辑

set: function reactiveSetter(newVal) {
    // ...
    dep.notify()
}

很明显,这个就是触发依赖的关键(详见下文),而其最终导向watcher.update

update() {
    if (this.computed) {
        if (this.dep.subs.length === 0) {
            this.dirty = true
        } else {
            this.getAndInvoke(() => {
                this.dep.notify()
            })
        }
    } else if (this.sync) {
        this.run()
    } else {
        queueWatcher(this)
    }
}

这里我们先不管computed,那么余下俩个就是同步与异步,我们先不管异步,其实和同步一样的是最后都是run完成了更新操作,所以我们先看run

run() {
    if (this.active) {
        this.getAndInvoke(this.cb)
    }
}

可见run也只是判断了下当前观察者是否激活,激活的话就会调用getAndInvoke方法,且将this.cb为参数传入。这时候我们知道getAndInvoke肯定就是更新变化操作的根源了

getAndInvoke(cb: Function) {
    const value = this.get()
    if (
        value !== this.value ||
        isObject(value) ||
        this.deep
    ) {
        const oldValue = this.value
        this.value = value
        this.dirty = false
        if (this.user) {
            try {
                cb.call(this.vm, value, oldValue)
            } catch (e) {
                handleError(e, this.vm, `callback for watcher "${this.expression}"`)
            }
        } else {
            cb.call(this.vm, value, oldValue)
        }
    }
}

这个一开始就重新求值,这个也是为什么renderWatcher传入的回调时noop的原因,因为会重新求值,那么渲染函数自然也会被调用
然后就是一个if语句,三个判断条件:

  • 判断新旧值是否相等,不相等的话才执行回调
  • 是对象的话也执行回调,因为对象的话引用不变但是数据内容可能变了
  • 深度观测也执行回调,因为深度观测的话就是对象

接下来if语句内部逻辑,首先定义了oldValue来存储旧值,将前面求得新值赋值给this.value,给dirty赋值false代表已经求值了,然后就是个if语句来区分下是否是用户自定义还是内部定义的watcher。用户自定义的不可控,得用try/catch包裹,且给适当的提示报错的话。最终回调调用就是cb.call(this.vm, value, oldValue),这个也就是回调函数的参数新旧值由来

Dep

这里就是收集依赖的容器,它暴露了相关方法

其实从某种情况而言你可以将其当做数据的一个影子,因为每一个dep都有其对应的一个响应式数据,computed特殊点而已

Dep Class
let uid = 0
export default class Dep {
    static target: ?Watcher;
    id: number;
    subs: Array;

    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()
        }
    }
}

看一个类自然先从constructor看起,可见先初始化俩实例变量:id、subsidDep实例对象的唯一标识,subs是存储依赖(观察者)的数组,还有一个静态属性target用来存储当前处理的观察者实例

  • depend():首先判断Dep.target,这是因为不是每处调用都和get一样保证其有值。然后调用Dep.target.addDep(this),这个为什么不直接pushsubs,这是因为需要做依赖重复处理
  • addSub:将观察者存入subs,也就是订阅
  • removeSub:将观察者从subs删除,也就是取消订阅
  • notify:就是通知订阅者数据变更,首先取subs副本,这是因为防止更新期间subs变动,subs存储的是watcher,循环遍历调用其update()即可,watcher.update()与传入Watchercb有关联,之后再说
pushTarget、popTarget
// 当前处理的watcher
Dep.target = null
// 当前处理的watcher只能有一个,所以要是还没处理完当前的又设置Dep.target就得把后来的入栈
const targetStack = []

export function pushTarget(_target: ?Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
}
// 完了之后出栈就能拿到后来的
export function popTarget() {
    Dep.target = targetStack.pop()
}

首先定义Dep.target用于存储当前处理的watcher,因为当前处理的watcher,所以若是有另外的watcher待处理就得入栈排队,这就是targetStack
暴露俩个pushTarget、popTarget方法,用于设置Dep.target

traverse

深度观测所用,也就是递归循环遍历触发属性get

const seenObjects = new Set()

export function traverse(val: any) {
    _traverse(val, seenObjects)
    // 完了之后清空seenObjects,以备下次使用
    seenObjects.clear()
}

function _traverse(val: any, seen: SimpleSet) {
    let i, keys
    const isA = Array.isArray(val)
    // 因为深度观测,所以不能是非对象、不能被冻结、不能是VNode实例
    if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
        return
    }
    // 这里是处理循环引用情况
    // 实现val是对象且响应式,那么必然有__ob__属性
    if (val.__ob__) {
        // 我们需要一个标志来定位到这个val,就用其对应的dep的id即可
        const depId = val.__ob__.dep.id
        // 判定seenObjects里已经添加了这个,也就是已经处理过了,就return
        if (seen.has(depId)) {
            return
        }
        // 没处理的就添加上标志已经处理了
        seen.add(depId)
    }
    // 因为数组可对象取值方法不一样,所以区分下,仅此而已。
    // 目的都是val[i]、val[keys[i]]这俩触发getter尔
    if (isA) {
        i = val.length
        while (i--) _traverse(val[i], seen)
    } else {
        keys = Object.keys(val)
        i = keys.length
        while (i--) _traverse(val[keys[i]], seen)
    }
}

暴露一个traverse方法,这种很明显是得要递归的,那么_traverse也就应运而生了。它调用_traverse,传入的seenObjects用于存储dep.id来判断是否处理过
接来下看_traverse。首先判断下若是非对象、被冻结、VNode实例对象,那么自然不能深度观测,return就是了
然后判断下是否有__ob__,有的话那么必然是响应式的也就是无需处理的。因为可能有循环引用情况,那么就得把这个对象的dep.id存着,这样子下次遍历到这个对象的时候就可以判断这个对象是否已经遍历过了。不然的话可能会无限循环
最后就是递归了,这个就得区分数组还是对象,因为俩个遍历方式不一样。目的其实都是为了触发getval[i]、val[keys[i]]这个就是触发get以达到收集依赖的目的

你可能感兴趣的:(响应式系统(四))