vue2的双向数据绑定

89 阅读2分钟
class Vue {
    constructor(options) {
        this.$data = options.data
            // 调用数据劫持的方法
        Observe(this.$data)

        // 属性代理
        Object.keys(this.$data).forEach((key) => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newValue) {
                    this.$data[key] = newValue
                },
            })
        })

        // 调用模板编译的函数
        Compile(options.el, this)
    }
}

// 定义一个数据劫持的方法
function Observe(obj) {
    // 这是递归的终止条件
    if (!obj || typeof obj !== 'object') return
    const dep = new Dep()

    // 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
    Object.keys(obj).forEach((key) => {
        // 当前被循环的 key 所对应的属性值
        let value = obj[key]
            // 把 value 这个子节点,进行递归
        Observe(value)
            // 需要为当前的 key 所对应的属性,添加 getter 和 setter
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
                // 就被放到了 dep.subs 这个数组中了
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set(newVal) {
                value = newVal
                Observe(value)
                    // 通知每一个订阅者更新自己的文本
                dep.notify()
            },
        })
    })
}

// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
    // 获取 el 对应的 DOM 元素
    vm.$el = document.querySelector(el)

    // 创建文档碎片,提高 DOM 操作的性能(创建一处内存)
    const fragment = document.createDocumentFragment()

    while ((childNode = vm.$el.firstChild)) {
        fragment.appendChild(childNode)
    }

    // 进行模板编译
    replace(fragment)

    vm.$el.appendChild(fragment)

    // 负责对 DOM 模板进行编译的方法
    function replace(node) {
        // 定义匹配插值表达式的正则
        const regMustache = /\{\{\s*(\S+)\s*\}\}/

        // 证明当前的 node 节点是一个文本子节点,需要进行正则的替换
        if (node.nodeType === 3) {
            // 注意:文本子节点,也是一个 DOM 对象,如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
            const text = node.textContent
                // 进行字符串的正则匹配与提取
            const execResult = regMustache.exec(text)
                // console.log(execResult)
            if (execResult) {
                const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
                node.textContent = text.replace(regMustache, value)
                    // 在这个时候,创建 Watcher 类的实例         为了实现以后再次更新时候如何更新。把他作为一个方法存进去
                new Watcher(vm, execResult[1], (newValue) => {
                    // 至关重要
                    node.textContent = text.replace(regMustache, newValue)
                })
            }
            // 终止递归的条件
            return
        }

        // 判断当前的 node 节点是否为 input 输入框
        if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
            // 得到当前元素的所有属性节点
            const attrs = Array.from(node.attributes)
            const findResult = attrs.find((x) => x.name === 'v-model')
            if (findResult) {
                // 获取到当前 v-model 属性的值   v-model="name"    v-model="info.a"
                const expStr = findResult.value

                const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)

                node.value = value

                // 创建 Watcher 的实例
                new Watcher(vm, expStr, (newValue) => {
                    node.value = newValue
                })

                // 监听文本框的 input 输入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可
                node.addEventListener('input', (e) => {
                    const keyArr = expStr.split('.')
                    const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
                    const leafKey = keyArr[keyArr.length - 1]
                    obj[leafKey] = e.target.value
                })
            }
        }

        // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
        node.childNodes.forEach((child) => replace(child))
    }
}

// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
    constructor() {
        // 今后,所有的 watcher 都要存到这个数组中
        this.subs = []
    }

    // 向 subs 数组中,添加 watcher 的方法
    addSub(watcher) {
        this.subs.push(watcher)
    }

    // 负责通知每个 watcher 的方法
    notify() {
        this.subs.forEach((watcher) => watcher.update())
    }
}

// 订阅者的类
class Watcher {
    // cb 回调函数中,记录着当前 Watcher 如何更新自己的文本内容
    //    但是,只知道如何更新自己还不行,还必须拿到最新的数据,
    //    因此,还需要在 new Watcher 期间,把 vm 也传递进来(因为 vm 中保存着最新的数据)
    // 除此之外,还需要知道,在 vm 身上众多的数据中,哪个数据,才是当前自己所需要的数据,
    //    因此,必须在 new Watcher 期间,指定 watcher 对应的数据的名字
    constructor(vm, key, cb) { //vm为了拿最新的数据;cb为了如何更新自己,key为了知到在vm上取哪个值
        this.vm = vm
        this.key = key
        this.cb = cb

        // ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
        Dep.target = this //就是一个单纯的自定义属性
        key.split('.').reduce((newObj, k) => newObj[k], vm)
        Dep.target = null
    }

    // watcher 的实例,需要有 update 函数,从而让发布者能够通知我们进行更新!
    update() {
        const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm) //从vm上取到新值    触发了数据劫持
        this.cb(value)
    }
}