Vue2 如何实现监听对象的变化

423 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

1. js如何实现监听对象属性变化

// 普通对象
const data = {}

// 把普通对象变为可监听的对象,利用Object.defineProperty
function defineReactive(data, key, value) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            console.log('取值了')
            return value
        },
        set(newV) {
            console.log('赋值了')
            // 拦截赋值
            document.getElementById('test').innerText = newV
            value = newV
        }
    })
}

// 把data的name属性变为可监听的
defineReactive(data, 'name', '')

// 赋值
data.name = 'liyajie' // 打印: 赋值了

// 取值
console.log(data.name) // 打印:取值了

这样我们就可以对取值和赋值做一层拦截,比如我们可以在赋值的时候修改dom,以达到绑定的效果,这样我们就只需要关注数据对象了

2. 对依赖进行收集,统一管理

  • 收集依赖:当我们使用对象中属性的时候,说明这个时候是依赖这个属性的

  • 触发依赖:当我们赋值的时候,是需要对取值的地方最一次通知,也就是需要触发一次依赖

  • 也就是说我们需要在getter中进行依赖收集,在setter中触发依赖

<template>
    <div>{{ name }}</div>
</template>

如上vue的模板中使用到了name,则回触发响应的getter,同时也会触发依赖收集,下次触发setter的时候触发依赖,更新name

  1. 如果我们把依赖用函数来表示,那触发依赖的时候我们只需要触发这个函数即可
// 改进
function defineReactive(data, key, value) {
    const dep = [] // 依赖列表
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 收集依赖
            console.log('1.收集依赖')
            dep.push(function(newV, oldV) {
                console.log('执行了依赖', newV, oldV)
            })
            return value
        },
        set(newV) {
            if (value === newV) {
                return
            }
            // 触发依赖
            for(let i = 0, l = dep.length; i < l; i++) {
                console.log('触发了依赖')
                dep[i](newV, value)
            }
            value = newV
        }
    })
}
const data = {}
defineReactive(data, 'name', '') // 变为可监听对象
console.log(data.name) // 打印:1.收集依赖
data.name = 'liyajie' // print: 触发了依赖  执行了依赖

升级之后新增了dep进行存放依赖,在set触发的时候循环dep执行依赖

3. 独立Dep类

上面写法有点耦合,将Dep封装成独立类,对defineReactive再改造一下

  • Dep类具有的功能
    • 依赖收集
    • 依赖触发
    • 依赖移除
// Dep
class Dep {
    constructor() {
        this.subs = [] // 存放依赖列表
    }
    // 添加依赖
    addSub(sub) {
        this.subs.push(sub)
    }
    // 移除依赖
    removeSub(sub) {
        remove(this.subs, sub)
    }
    // 快速收集依赖
    depend() {
        // 假设我们把依赖放到window.target
        if (window.target) {
            this.addSub(window.target)
        }
    }
    // 触发依赖(通知)
    notify() {
        const subs = this.subs.slice()
        for(let i = 0, len = subs.length; i < len; i++) {
            subs[i].update() // 看到这里也就能明白,window.target中有一个update方法
        }
    }
}

// 借助该方法移除依赖
function remove(arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item)
        if (index > -1) {
            arr.splice(index, 1)
        }
    }
}

改造defineReactive

function defineReactive(data, key, value) {
    const dep = new Dep() // 依赖列表
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get() {
            // 收集依赖
            console.log('1.收集依赖')
            dep.depend() // 收集
            return value
        },
        set(newV) {
            if (value === newV) {
                return
            }
            value = newV
            // 触发依赖
            dep.notify()
        }
    })
}

再次改造后的defineReactive已经把依赖全部放到了Dep中

4. 依赖到底是啥,有什么作用 Watcher

只要我们用到数据的地方都是依赖,有的是在模板中,有的也有可能是用户自定义的watcher,这时就得将这些情况进行统一管理,收集只收集它,通知也只通知它,他就是Watcher,把他理解为中介即可,用来做统一分发处理

  • watcher经典使用方式
// watcher常用的方式
vm.$watch('a.b.c', function(newV, oldV){
  // 执行
})

先来实现一下a.b.c这种方式读取属性

// source vue/core/util/lang.js
const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
function parsePath(path) {
    if (bailRE.test(path)) {
        return
    }
    return function(obj) {
        const segments = path.split('.')
        // 循环读取属性
        for(let i = i; i < segments.length; i++) {
            if (!obj) return
            obj = obj[segments[i]]
        }
        return obj
    }
}

4.1 实现Watcher

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    // getter是用来获取a.b.c的值
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }

  // 获取最新value
  get() {
    window.target = this
    // 执行getter就是执行parsePath的返回值函数
    let value = this.getter.call(this.vm, this.vm)
    window.target = null
    return value
  }

  // 执行回调
  update() {
    // 缓存oldValue
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

// 使用
const data = {
    person: {
        name: '小李子'
    }
}
new Watcher(data, 'person.name', function(newV, oldV){
    console.log('newV=',newV,'oldV=', oldV)
})

5. 实现整个对象的深层监听

上面我们已经实现了一个对象属性的监听,现在我们要实现整个对象的深层监听,利用递归遍历整个对象属性,让每个属性都可监听

class Observer {
    constructor(value) {
        this.value = value
        if (!Array.isArray(value)) {
            this.walk(value)
        }
    }
    
    walk(obj) {
        const keys = Object.keys(obj)
        for(let i = 0, len = keys.length; i < len; i++) {
            defineReactive(obj, keys[i], obj[keys[i]])
        }
    }
}

function defineReactive(data, key, value) {
    if (typeof value === 'object') {
        // 如果是对象,则需要设置其属性为getter/setter
        new Observer(value)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get() {
            // 依赖收集
            dep.depend()
            return value
        },
        set(newValue) {
            if (newValue === value) {
                return
            }
            value = newValue
            // 循环执行依赖列表
            dep.notify()
        }
    })
}

6. 总结

  1. defineReactive: 将对象变为getter/setter形式
  2. Depnew Dep() 收集依赖、通知依赖
  3. Observernew Observer(data) 将对象深度遍历为getter/setter形式
  4. Watchernew Watcher(data, 'a.b', function(newV, oldV){}) 监听属性变化,初始化的时候取值,取值会触发属性的getter,触发后依赖自动收集(就是把Watcher收集了起来),当属性变化的时候,执行通知依赖(就是触发Watcher的update),也就是他的回调函数,或者更新模板

感谢:深入浅出Vue.js