Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue源码拆解(一):实现计算属性和侦听器 #10

Open
luoway opened this issue Aug 4, 2019 · 0 comments
Open

Vue源码拆解(一):实现计算属性和侦听器 #10

luoway opened this issue Aug 4, 2019 · 0 comments

Comments

@luoway
Copy link
Owner

luoway commented Aug 4, 2019

前言

Vue被称为“框架”,而jQuery被称为“库”,区别在于库解决某一类问题,而框架解决了多类问题。

想要深入了解Vue,以读“库”的方式去逐行看源码,很容易迷失在成千上万行代码里。

本文抱着拆出指定功能模块的目的来读源码,以脱离框架的方式实现功能模块。

虽然代码会与源码不同,但模块功能的实现原理是相同的,逻辑纯粹的代码更容易理解其原理。

defineProperty

Vue教程 - 深入响应式原理 - 如何追踪变化一节提到,Vue 遍历对象所有的属性,并使用 Object.defineProperty把这些属性全部转为 getter/setter

先了解defineProperty功能:MDN文档 - defineProperty

defineProperty的主要能力是定义或修改属性描述符,在Vue源码中有两种用途:

定义不可枚举的属性或方法

//下文会继续使用def函数
const def = function(obj, key, val) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: false,	//不可枚举
        writable: true,
        configurable: true
    })
}

设置getter/setter,在属性被访问和修改时通知变化

export function defineReactive(obj, key, val) {
    //...
    const property = Object.getOwnPropertyDescriptor(obj, key)
    const getter = property && property.get
    const setter = property && property.set
    //...
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            const value = getter ? getter.call(obj) : val
            //收集依赖...
            return value
        },
        set(newVal) {
            //...
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            //通知变化...
        }
    })
}

依赖收集

什么是依赖?

当变化发生的时候,要通知变化,但通知给谁呢?因此,接收通知的单位应当是变化发生前先明确的。

如果通知到所有接收通知的单位,那么在遍历进行通知时,就会有不需要通知的冗余单位,这些冗余单位仍会参与遍历并消耗性能。

性能优化考虑,应当有条件地选择接收通知的单位,这些单位称之为依赖(依赖者、订阅者,变化发生的单位称之为被依赖者、被观察者、发布者)。

如何收集依赖?

是个令人头疼的问题,无论getter还是setter执行时,传入的参数里都不包含这个发生变化的对象属性有哪些依赖。

为了解决这个问题,Vue给每个对象添加一个不可枚举的观察者(Observer),存储属于它的依赖。

observe()

Vue通过observe()函数为对象添加Observer

export function observe(value) {
    if (typeof value !== "object") return

    let ob
    if (value.hasOwnProperty("__ob__") && value.__ob__ instanceof Observer) {
      	//已有观察者,返回已存在的观察者。
        ob = value.__ob__
    } else if (
        (Array.isArray(value) ||
            Object.prototype.toString.call(value) === "[object Object]") &&
        Object.isExtensible(value)
    ) {
        ob = new Observer(value)
    }
    return ob
}

Observer

Observer类定义如下:

export class Observer {
    constructor(value) {
        this.value = value
        this.dep = new Dep()
        def(value, "__ob__", this)	//this指向当前Observer实例
      	
        if (Array.isArray(value)) {
            //省略数组方法处理
            this.observeArray(value)
        } else {
            this.observeObject(value)
        
    }
    observeArray(value) {
        value.forEach(observe)
    }
    observeObject(obj) {
        Object.keys(obj).forEach(key => defineReactive(obj, key))
    }
}

在为对象添加观察者的过程中,Vue 遍历了对象所有的属性,使用defineReactive()设置getter/setter,并递归地为嵌套对象添加观察者。

defineReactive()

export function defineReactive(obj, key, val) {
    const dep = new Dep()

    const property = Object.getOwnPropertyDescriptor(obj, key)
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
  
    let childOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
            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(newVal) {
            const value = getter ? getter.call(obj) : val
            if (newVal === value || (newVal !== newVal && value !== value)) {
                //新旧值相同,或新旧值不等于本身的特殊值,如 NaN
                return
            }
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            childOb = observe(newVal)
            //通知变化
            dep.notify()
        }
    })
}

function dependArray(value){
    value.forEach(e => {
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
            dependArray(e)
        }
    })
}

defineReactive中又定义了一个私有dep,在getter/setter中被使用,形成闭包。闭包depObserver实例的dep属性相比,两者有什么区别?

提前看下Dep类中depend()定义:

class Dep {
    //...
    static target = null
    //...
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
}

可见depend()不是一个静态方法,而是和实例上下文相关的。

defineReactive设置的setter中通知变化调用用的是闭包dep,因而真正与响应式直接相关的是闭包dep。而Observer实例的dep属性,会执行与闭包dep一样的depend()行为,但不会通知变化。

因此实例上的dep是只读的,有用的是闭包dep

Dep

下面具体看Dep类:

class Dep {
    static uid = 0
    // 当前执行的watcher。
    // 这是全局唯一的,因为同一时间只有一个watcher被执行。
    static target = null
    static targetStack = []
    static pushTarget(target) {
        Dep.targetStack.push(target)
        Dep.target = target
    }
    static popTarget() {
        Dep.targetStack.pop()
        Dep.target = Dep.targetStack[Dep.targetStack.length - 1]
    }

    constructor() {
        this.id = Dep.uid++
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    removeSub(sub) {
        const index = this.subs.indexOf(sub)
        if (index > -1) this.subs.splice(index, 1)
    }
    //收集依赖
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }
    notify() {
        // 先固定订阅者列表(避免遍历过程中数组改变)
        const subs = this.subs.slice()
        subs.forEach(sub => sub.update())
    }
}

static标志的属性和方法是Dep所有实例共享的,其中target取值为空或者Watcher实例,targetStackpushTargetpopTarget形成对多个watcher的栈操作。

Depconstructor中定义的subs属性,负责存储接收变化通知的订阅者列表,由addSub()removeSub()方法操作列表,notify()方法遍历列表通知订阅者更新。

需要理解的是depend()方法。dep.depend()执行的前提是Dep.target有真值,而Dep.target在注释中说明了真值为Watcher实例,因此执行内容是调用Watcher实例的addDep()方法。

提前看下Watcher类中addDep()定义:

class Watcher {
    constructor(){
      	//...
      	this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
      	//...
    }
    //...
    /**
     * 为当前指令添加依赖
     */
    addDep(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)
            }
        }
    }
}

addDep(dep)去重后调用dep.addSub(this),此处this指向当前Watcher实例。

综上,dep.depend()的逻辑可以理解为:

class Dep {
    //...
    depend(){
      	if(Dep.target){	//如果有正在执行的watcher
            if(!Dep.target.depIds.has(this.id)){	//避免重复添加dep
              	this.addSub(Dep.target)	//添加该watcher到订阅者列表
            }
        }
    }
}

“收集依赖”的过程小结:

当Vue通过defineReactive()设置对象属性obj.keygetter被触发时,如果存在正在执行的watcher(即Dep.target有真值),将watcher加入到defineReactive()创建的闭包dep.subs列表和obj.__ob__.dep.subs中。

效果如下:

obj.ob.dep.subs.png

问题是,什么时候Dep.target才有真值,也就是pushTarget()被传入真值并调用的时候。

Watcher

下面具体看Watcher

class Watcher {
    static uid = 0
    constructor(obj, keyOrFn, cb, options = {}) {
        this.vm = obj
        if (!obj._watchers) def(obj, "_watchers", [])
        obj._watchers.push(this)

        this.cb = cb
        this.id = ++Watcher.uid
        this.active = true
        this.deep = !!options.deep
        this.dirty = this.lazy = !!options.lazy

      	//存储当前watcher的相关依赖索引
      	//会在添加依赖时更新数组内容
      	//会在watcher.depend()方法中被使用
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()

        this.getter = typeof keyOrFn === "function" ? keyOrFn : obj => obj[keyOrFn]
        this.value = this.lazy ? undefined : this.get()
    }

    get() {
        Dep.pushTarget(this)
        let value
        const vm = this.vm
        try {
            value = this.getter.call(vm, vm)
        } catch (e) {
            console.log(e)
        } finally {
            // 对于深watch,递归触发每个属性的getters以收集依赖
            if (this.deep) {
              	//省略traverse具体实现
                traverse(value)
            }
            Dep.popTarget()
            this.cleanupDeps()
        }
        return value
    }

    /**
     * 为当前指令添加依赖
     */
    addDep(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)
            }
        }
    }

    /**
     * 订阅者接口。
     * 依赖改变时调用
     */
    update() {
        if (this.lazy) {
            this.dirty = true
        } else {
            //便于理解,这里使用同步更新
            //在Vue中默认是使用queueWatcher()异步更新
            const value = this.get()

            if (
                value !== this.value ||
                typeof value === "object" ||
                this.deep
            ) {
                const oldValue = this.value
                this.value = value
                this.cb.call(this.vm, value, oldValue)
            }
        }
    }

    /**
     * 清理依赖收集
     * 移除失效的订阅者
     */
    cleanupDeps() {
        let i = this.deps.length
        while (i--) {
            const dep = this.deps[i]
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this)
            }
        }
      	// 以下操作复用tmp变量
      	// 更新depIds、deps,并清空newDepIds、newDeps
        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
    }
    /**
     * 计算watcher的值
     * 只会对延迟watcher调用
     */
    evaluate() {
        this.value = this.get()
        this.dirty = false
    }
		//触发与当前watcher相关的所有依赖收集Dep.target
    depend() {
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }
    teardown() {
        if (this.active) {
            const index = this.vm._watchers.indexOf(this)
            if (index > -1) this.vm._watchers.splice(index, 1)

            let i = this.deps.length
            while (i--) {
                this.deps[i].removeSub(this)
            }
            this.active = false
        }
    }
}

Watcher实例化需要至少三个参数,用法如下:

new Watcher(obj, keyOrFn, cb)

其中obj是被侦听的对象;keyOrFn是被侦听属性名(侦听器watch使用)或函数(计算属性computed使用),在constructor中被转换为能够触发被侦听属性的getter的函数,并赋值给this.gettercb是变化后回调的函数。

Dep.pushTarget(this)Watcher.get()中用到,此时Dep.target有真值。

此外,get()方法还调用this.getter触发被侦听属性的gettergetter内容即执行dep.depend(),将当前watcher实例加入到被侦听对象obj的依赖列表中,完成依赖收集。

watcher.get()方法在constructor()update()evaluate()三处被使用。

  • constructor()Watcher实例化时执行;

  • update()dep.notify()通知变化时执行;

  • evaluate()尚未提到,是在实现computed计算属性中使用的。

三处的区别与联系都和this.lazy相关:

  • lazy为假

    Watcher实例化时立即执行get()收集依赖,dep.notify()通知变化时立即执行get()更新依赖,并调用回调函数cb

  • lazy为真

    Watcher实例化时不执行get()dep.notify()通知变化时将this.dirty赋值为真,直到wathcer.evalute()执行时才会执行get()收集依赖并执行computed定义的计算函数。

上述lazy两种取值,形成了两种数据响应式更新的方式,下面来具体实现这两种方式。

侦听器 - $watch

设计$watch的使用方式如下:

$watch(obj, {
    key(newVal){
      	console.log(newVal)
    }
})

$watch接收二个参数:

  • 被侦听对象obj
  • 侦听属性 - 侦听回调 键值对watch

实现$watch()函数:

function $watch(obj, watch, options){
    return Object.keys(watch).map(key => {
        const watcher = new Watcher(obj, key, watch[key], options)
        return () => {
            watcher.teardown()
        }
    })
}

计算属性 - $compute

设计$compute的使用方式如下:

$compute(obj, {
    key(){
      	const computedResult = obj
      	console.log(computedResult)
      	return computedResult
    }
})

$compute接收二个参数:

  • 将添加计算属性的对象obj
  • 计算属性 - 计算函数 键值对computed

实现$compute函数:

function $compute(obj, computed){
    Object.keys(computed).forEach(key => {
        const watcher = new Watcher(obj, computed[key], function() {}, {
            lazy: true
        })

        Object.defineProperty(obj, key, {
            get() {
                if (watcher) {
                    if (watcher.dirty) {
                      	//lazy为真,实例化、接收变化通知后dirty为真,表示需要重新计算
                        watcher.evaluate()
                    }
                  	//computed添加的属性也可以被侦听,此时Dep.target有真值
                    if (Dep.target) {
                      	//将Dep.target收集为依赖
                        watcher.depend()
                    }
                    return watcher.value
                }
                return undefined
            },
            set() {
                // 计算属性不可set
            }
        })
    })
}

至此实现了计算属性和侦听器,可以运行的完整源码

流程图

$watch

$watch函数流程

img

$watch响应更新流程

img

$compute

$compute函数流程

img

$compute取值流程

img

总结

img

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant