自己写一个Vue2的变化侦测

53 阅读2分钟

1.基调

根据《认知天性》的介绍,如果你想让自己有创造力并且记忆深刻的话,在做题的时候就不要去看标准答案,应该给自己一定的困难,在自己弄出一些东西之后再去比对答案,或许你会弄出一些奇奇怪怪的东西,但这确实可能成为你未来灵感的源泉。根据《学习的方法》,不要迷信权威,他因为各种原因必然会犯各种错误,这是无法避免的,不要觉得一个很成熟的东西有很多大牛弄过了,他就是完美无缺的,或许里面充满了各种妥协呢。

2.什么是VUE?

在我的理解中,vue是一个响应式的框架,如果我的页面上的东西不需要变动,那么写html和css就好了,没有必要去折腾什么框架,但是如果页面要变化,昨天油价9元一升,今天就10元了,要改起来就麻烦了,那么有没有办法我给他传多少他油价就显示多少,就像雇一个员工放块白板写上今日油价:9元,明天变了把白板字擦了换了,就行了,我的加油站没有必要拆了重建,这就是vue的作用了。

3.变化侦测

首先,我们雇一个员工,他要做的是油价变了就喊一声告诉所有人,然后要会改白板上的字,于是用上了js里的Object.defineProperty()

let oil = {}
let val = 9
Object.defineProperty(oil,'price',{
    get(){
        console.log("告诉你今日油价")
        return val
    },
    set(newVal){
        console.log("油价更新了")
        val = newVal
        
    }
})

整合一下形成一个Observe类

export class Observer {
    constructor(value) {
        this.value = value
        this.walk(value)//遍历一遍,把对象转成响应式
    }
    walk(obj) {
        const keys = Object.keys(obj)
        for(let i=0; i<keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
}
//val参数代表对象的某个key的值
function defineReactive(obj,key,val) {
    //如果只有两个参数,val默认 == obj[key]
    if(arguments.length === 2) {
        val = obj[key]
    }
    if(typeof(val)  === 'object') {
        new Observer(val)
    }
    Object.defineProperty(obj, key, {
        enumerable: true,//表示是否可以通过delete删除并重新定义,改为访问器属性
        configurable: true,//表示是否可以通过for-in迭代
        get(){
            console.log(`${key}被读取了`)
            return val
        },
        set(newVal){
            if(val === newVal) return
            console.log(`${key}被修改了`)
            val = newVal
        }
    })
}

好了,现在对象可以观测了,里面还有很多需要考虑的细节,但这并不是现在需要考虑的问题,把全貌看完再细化细节可能更好。我们的目标是看到最高处,现在拿简易木架搭了第一层阶梯,我们应该做的是继续搭简易阶梯上去,没必要把每一层都浇上钢筋水泥,那太慢了,等看到最高处的风景,想给每一级搭大理石还是金属装饰那都没问题。

4.收集依赖

当数据改变了之后,页面怎么知道到的哪些地方需要修改呢?我们可以做一个目录,哪个地方用了这个数据,就告诉这个地方改一下,引用一句话:在getter中收集依赖,在setter中通知依赖更新。可以用一个数组来做目录,有东西调用了get方法就在目录中加上是哪个对象调用的,set了一个新值就把目录中各个依赖的对象更新一下。下面构建一个Dep类,我们为每一个对象都弄一个Dep,里面有依赖数组和添加,删除,更新依赖的方法。

export default class Dep {
    constructor() {
        this.subs = []
    }
    depend() {
        if(window.target) {
            this.addSub(window.target)
        }
    }
    //添加依赖的方法
    addSub(sub) {
        this.subs.push(sub)
        console.log("添加了依赖,现在的依赖有:",this.subs)
    }
    //移除依赖
    removeSub(sub) {
        remove(this.subs, sub)
    }
    //在源码中remove函数被单独放在了一个util文件夹内,但这里我们先不考虑
    remove(arr,item) {
        if(arr.length) {
            const index = arr.indexOf(item)
            if(index > -1) {
                return arr.splice(index, 1)
            }
        }
    }
    notify() {
        const subs = this.subs.slice()
        for(let i=0; i<subs.length; i++) {
            subs[i].update()//具体如何更新的下面说
        }
    }

}

好了,接下来更新一下observer类

export class Observer {
    constructor(value) {
        this.value = value
        this.walk(value)//遍历一遍,把对象转成响应式
    }
    walk(obj) {
        const keys = Object.keys(obj)
        for(let i=0; i<keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
}
//val参数代表对象的某个key的值
function defineReactive(obj,key,val) {
    //如果只有两个参数,val默认 == obj[key]
    if(arguments.length === 2) {
        val = obj[key]
    }
    if(typeof(val)  === 'object') {
        new Observer(val)
    }
    const dep = new Dep()//实例化Dep类,新建一个Dep类型的dep实例
    Object.defineProperty(obj, key, {
        enumerable: true,//表示是否可以通过delete删除并重新定义,改为访问器属性
        configurable: true,//表示是否可以通过for-in迭代
        get(){
            console.log(`${key}被读取了`)
            //收集依赖
            dep.depend()
            return val
        },
        set(newVal){
            if(val === newVal) return
            console.log(`${key}被修改了`)
            val = newVal
            //被set了新值就通知依赖更新
            dep.notify()
        }
    })
}

5.更新依赖

好了,现在对象变成响应式的了,而且建了依赖数组,一旦有变动就会通知依赖更新,那么依赖是怎么更新的呢?于是我们弄了一个Watcher类,谁用到了数据,我们就给它实例化一个watcher,里面有更新的方法。

export class Watcher {
    constructor(vm,expOrFn,cb) {
        this.vm = vm
        this.cb = cb
        this.getter = parsePath(expOrFn) //获取路径,在源码里单独放在工具类util里
        //Watcher被实例化的时候就调用get()
        this.value = this.get()
    }
    get() {
        //this是一个实例化后的watcher对象,这里先把this保存下来,换成Dep.target也可以
        window.target = this
        const vm = this.vm
        let value = this.getter.call(vm, vm)
        window.target = undefined
        return value
    }
    update() {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    },
    
    parsePath (path) {
        const bailRE = /[^\w.$]/
        if (bailRE.test(path)) {
            return
        }
        const segments = path.split('.')
        return function (obj) {
            for (let i = 0; i < segments.length; i++) {
                if (!obj) return
                obj = obj[segments[i]]
            }
            return obj
        }
    }
}

好了,简易的Object的变化侦测就完成了。

参考资料:
github.com/vuejs/vue/t…
vue-js.com/learn-vue/
segmentfault.com/a/119000004…
ustbhuangyi.github.io/vue-analysi…