vue的双向绑定和依赖收集

Posted zhaowj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue的双向绑定和依赖收集相关的知识,希望对你有一定的参考价值。

在掘金上买了一个关于解读vue源码的小册,因为是付费的,所以还比较放心

在小册里看到了关于vue双向绑定和依赖收集的部分,总感觉有些怪怪的,然后就自己跟着敲了一遍。 敲完后,发现完全无法运行,  坑啊,  写书人完全没有测试过。

然后自己完善代码, 越写越发现坑, 问题有些大。。。。。。

最后自己重新实现了一遍,代码较多。 用到观察订阅者模式实现依赖收集, Object.defineProperty() 实现双向绑定

/*
    自己写的代码, 实现vue的双向绑定和依赖收集
    场景: 多个子组件用到父组件data中的数据, 当父组件data中的此数据发生改变时, 
    所有依赖它的 子组件全部更新
    通常子组件的从父组件中拿取的数据不允许发生改变
*/

    //订阅者 Dep
    //一个订阅者只管理一个数据
    class Dep {
        constructor () {
            this.subs = []    //存放vue组件
        }
        addSubs (sub) {
            this.subs.push(sub)
            console.log(add watcher: , sub._name)
        }
        notify () {
            this.subs.forEach( sub => {    //通知vue组件更新
                sub.update()
            })
        }
    }

    //监听者
    //一个vue实例包含一个Watcher实例
    class Watcher {
        // 在实例化Watcher时, 将Dep的target指向此实例, 在依赖收集中使用
        // 因为依赖收集是在组件初始化时触发的, 而数据变更后视图相应变更是在初始化后
        // 所以让Dep.target指向此实例, 当此vue实例初始化完成后, 再指向下一个正在初始化的vue实例完成依赖收集
        constructor (name) {
            Dep.target = this
            this._name = name
        }
        update () {
            // 这里模拟视图更新
            // 其实还应该让子组件的props相应值与父组件更新的数据同步
            console.log("子组件视图更新了..." + this._name)
        }
    }

    //对data中的数据设置读写监听, 并且创建订阅者, 用于收集子组件的依赖和发布
    function defineReactive (obj, key, value) {

        // 对vue实例中data对象的每一个属性都 设置一个订阅者Dep
        let dep = new Dep()

        // 第二个vue实例的监听 覆盖了第一个vue实例的监听, 因为引用的obj是同一个
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            get () {    
            // 在读此属性时, 将当前 watcher 对象收集到此属性的 dep 对象中
            // 在实例化vue时将Dep.target指向当前Watcher
            // get()依赖收集的时候是vue组件初始化的时候, set()是在初始化后
                if (dep.subs.indexOf(Dep.target) === -1) {
                    dep.addSubs(Dep.target)
                }
                //return obj[key]     此写法报错 提示栈溢出 原因是无限调用get()
                return value
            },
            set (newVal) {    // 此属性改变时, 通知所有视图更新
                if (newVal !== value) {
                    value = newVal
                    dep.notify()    
                }
            }
        })
    }

    //接收一个对象作为参数, 将该对象的所有属性调用defineReactive设置读写监听
    function observer (obj) {
        if (!obj || (typeof obj !== object)) {
            return 
        }
        Object.keys(obj).forEach( key => {
            defineReactive(obj, key, obj[key])
        }) 
    }

    // 构造函数, 监听 配置options中的data()方法返回的对象的所有属性 的读写
    class Vue {
        constructor (options) {
            this._name = options.name
            this._data = options.data
            // 每个vue组件都是一个vue实例, 在一个页面中有多个vue实例
            // 在初始化该vue实例时, new一个Watcher对象, 使Dep.target指向此实例
            new Watcher(options.name)
            // 给data中的数据挂载读写监听
            observer(this._data)
            //模拟vue解析template过程, 获取从父组件传递过来的props
            //在这里进行依赖收集
            this._props = options.props ? getProps() : {}
            // 实例化该组件的子组件
            this._children = options.render ? (options.render() || {}) : {}
        }
    }

    // 父组件数据
    let data = {
        first: "hello",
        second: world,
        third: [啦啦啦]
    }

    let times = 0
    // 第一次调用返回的是第一个子组件的从父组件继承的数据(vue中props属性的值)
    // 第二次调用返回的是第二个子组件的从父组件继承的数据(vue中props属性的值)
    function getProps () {
        times++
        if (times == 1) {
            
            let obj = {first: "", second: ""}
            Object.keys(obj).forEach( key => {
                // 如果是对象, 则进行深拷贝
                // 这里使用到了父组件的数据, 触发依赖收集
                if (data[key] instanceof Object) {
                    obj[key] = JSON.parse(JSON.stringify(data[key]))
                } else {
                    obj[key] = data[key]
                } 
            })
            return obj

        } else if (times == 2) {

            let obj = {first: "", third: ""}
            Object.keys(obj).forEach( key => {
                if (data[key] instanceof Object) {
                    obj[key] = JSON.parse(JSON.stringify(data[key]))
                } else {
                    obj[key] = data[key]
                } 
            })
            return obj
        }    
    }

     let vue_root = new Vue({
         name: vue_root,
         data,
         //模拟编译template和实例化vue的过程 
         //在编译父组件 并且传递参数给子组件时, 将子组件的 watcher 添加进父组件的 dep
         render () {
             let vue_1 = new Vue({
                 name: vue_1,
                 data: {},
                 props: true,
                 render () {}
             }) 
             let vue_2 = new Vue({
                 name: vue_2,
                 data: {},
                 props: true,
                 render () {}
             }) 
             return {
                 vue_1,
                 vue_2
             }
         }
     })
    console.log(vue_root)
     vue_root._data.first = hello hello    // vue_1 和 Vue_2 都依赖此数据, 都更新
     vue_root._data.third =  "aaa"            // 只有 vue_2 依赖到了此数据, 更新

 

以上是关于vue的双向绑定和依赖收集的主要内容,如果未能解决你的问题,请参考以下文章

vue的数据双向绑定的实现

vuex源码分析(二)——双向数据绑定

Vue.js单向绑定和双向绑定实例分析

剖析Vue原理&实现双向绑定MVVM

vue数据双向绑定原理

vue.js源码学习-双向绑定之Array