学习vue2双向数据绑定原理

start

  • 学习vue2双向数据绑定原理
  • 时间仓促,代码纯手写,若有错误欢迎指出。

1. 简单的模拟一下双向数据绑定

    var obj = {
        a: 1
    }

    obj.a = 20
    console.log('打印它', obj.a)

比如我有一个对象obj,它有一个属性a.我可以设置它的值,也可以获取它的值。没毛病,这种代码我们肯定没少见过。

vue双向数据绑定实现的是什么效果?数据改变了页面改变,页面改变了数据也跟着改变。

那如果想实现它这个效果,该怎么做呢?

简易的模拟一下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue双向数据绑定原理学习 by_tomato----1</title>
</head>

<body>
    <input type="text" id="inp">
    <div id="box"></div>
    
    <script>
        var obj = {
            a: 1
        }
        // 值改变了,我们去修改页面
        document.getElementById('inp').value = obj.a
        document.getElementById('box').innerText = obj.a

        obj.a = 'tomato'
        // 值改变了,我们去修改页面
        document.getElementById('inp').value = obj.a
        document.getElementById('box').innerText = obj.a

        document.getElementById('inp').addEventListener('input', function (e) {
            console.log(e.target.value)

            // 页面input值改变了,我们去修改数据
            obj.a = e.target.value
            // 数据改变了,再去修改页面
            document.getElementById('box').innerText = obj.a
        })
    </script>
</body>

</html>

1.*页面改变* => *改变数据*

我们可以给例如input框添加一个输入事件,当input框的内容改变了,我们修改一下数据。

2.*数据改变* => *改变页面*

每次操作数据的时候,手动更新DOM。

3.效果

可以自己创建一个html页面,复制我的代码运行一下,就会发现基本实现了双向绑定的一个效果。

简易版的弊端: 不适合处理复杂的数据

我上面的代码,只是操作一个数据,稍微还好。但是如果数据量大,结构更复杂,每次数据改变都要手动更新DOM,我裂开了。
其次我并不能精确的掌握数据改变的时机。

2. 初次接触Object.defineProperty

由上面简易版本的弊端,我在想,怎么去监听数据的改变比较合适呢?

这个时候就需要介绍一下 Object.defineProperty 这个方法。

  • 翻译一下define Property英译:定义属性。
  • 注意单词要记住怎么读怎么写,最好是要可以熟练盲写出来。
  • 详情可参考 mdn官方文档

英译的意思是,定义属性,其实它的作用就是给属性进行配置。
方法的细节,可以自行查看文档,今天的主角不是它,我这里就直接列一下它使用案例。

Object.defineProperty 基础使用

    var obj = {
        a: 1
    }

    var temp
    Object.defineProperty(obj, 'a', {
        // 注意事项: value 和 get/set 这两对属性,不可以同时使用,会报错。
        // value:'3',
        get: function () {
            console.log('get获取值' + 'a')
            return temp
        },
        set: function (newValue) {
            console.log('set 设置值' + 'a')
            temp = newValue
        }
    })

    obj.a = 2
    // set 设置值a


    console.log('你好呀', obj.a)
    // get获取值a
    // 你好呀 2

我遇到的问题1

    // get/set属性,可以用es6的简写方式,第一次遇到可能会觉得有些生疏,其实两个写法都是一样的。
    get() {
        console.log('get获取值' + 'a')
        return temp
    },
    set(newValue) {
        console.log('set 设置值' + 'a')
        temp = newValue
    }

我遇到的问题2

我最开始接触这个get,set,我不明白,加了这两个东西有什么用呢,这样做了不是和之前一样,去读取值,设置值嘛。

但是你仔细想想,这个地方,我能在get的时候打印get获取值a,set的时候打印set 设置值a。那么我在console的地方,是不是可以加其他的代码?

3.给多个元素添加get set

再上述的代码中,我们只给obj中的a属性添加了get和set属性。但是如果我们obj中不仅仅只有一个属性呢,这个时候我们就需要去遍历我们的obj,把每个属性都添加上get和set。

为了方便调用,我们把上面的代码放在一个名为defineReactive的函数中:

    function defineReactive(obj, key, data) {
        Object.defineProperty(obj, key, {
            get() {
                console.log('get获取值' + key)
                return data
            },
            set(newValue) {
                console.log('set 设置值' + key)
                data = newValue
            }
        })
    }

我遇到的问题1

首先知道参数是什么意思。分别是:对象名。属性名。属性值。

我遇到的问题2

它这个地方data用的非常好!最初我们是使用的一个temp变量,来传递值的。

  • 现在是形参有一个data,就相当于在当前作用域定义了一个名为data的变量;

  • 且data还能接收外部的传参;

  • 再者,data会被 get/set函数使用,这就形成了一个闭包,当get/set执行,data会常驻于内存中。


好了不说问题,继续往下思考

开始遍历obj对象,遍历对象有很多方法,我这里就采用for in

    function defineReactive(obj, key, data) {
        Object.defineProperty(obj, key, {
            get: function () {
                console.log('get获取值' + key)
                return data
            },
            set: function (newValue) {
                console.log('set 设置值' + key)
                data = newValue
            }
        })
    }

    
    // 这里!
    for (const key in obj) {
        if (Object.hasOwnProperty.call(obj, key)) {
            defineReactive(obj,key,obj[key])
        }
    }

遍历这里我又遇到问题了1

Object.hasOwnProperty.call(obj, key)很眼熟,它是干什么的!不知道。解释一下:在编辑器中打for in会直接提示这些代码,那么这个方法有什么用,我这里解释一下:

》这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。

大白话说就是判断一个属性是不是这个对象自己的,而不是原型链上的,防止遍历到原型链上的属性。

4.如果对象中的属性也是对象呢?

    // 例如 d属性 它是对象。我们给a,b,c,d都添加了get set,但是m没有被添加。
    var obj = {
        a: 1,
        b: 2,
        c: 3,
        d: {
            m: 3
        }
    }

这个时候就需要处理一下属性值为对象的情况,改动如下:

1.首先把遍历对象的方法放在一个函数里面,函数叫observe

2.然后每次添加get,set之前,对每个属性值都observe一下。

3.除此之外,observe之前判断一下传入的值是不是对象,不是对象直接return。

4.其次对设置的新值,也observe一下

完整修改如下: ps:修改的步骤已经一一对应了

    var obj = {
        a: 1,
        b: 2,
        c: 3,
        d: {
            m: 3
        }
    }


    function defineReactive(obj, key, data) {
        observe(obj[key]) // 步骤 2
        Object.defineProperty(obj, key, {
            get: function () {
                console.log('get获取值' + key)
                return data
            },
            set: function (newValue) {
                if(data === newValue) return
                console.log('set 设置值' + key)
                data = newValue
                observe(data) // 步骤 4
            }
        })
    }

    function observe(obj) { // 步骤 1
        if (!obj || typeof obj !== 'object') return // 步骤 3
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                defineReactive(obj, key, obj[key])
            }
        }
    }

    observe(obj)

    obj.d.m = 333
    console.log(obj.d.m)
   

我遇到的问题1

步骤3用的非常巧妙,既保证了每个属性都会被递归执行observe,又排除了不是对象的属性。如果没有绕过弯,需要捋一下这里的逻辑。

我遇到的问题2:

又来复习js基础知识啦, 数据类型的判断:

  1. typeof一般是用来判断基础数据类型的;

  2. es6之前,typeof永不报错,返回的都是对应数据类型的,小写字符串形式;

  3. typeof是连在一起写的,中间没有驼峰。

  4. 有几个特殊情况 typeof null =》‘object’ typeof 函数 =》 ‘function’

》 所以啊 !obj || typeof obj !== 'obj',!obj用来排除null的情况,typeof obj !== 'obj’排除不是对象的情况(这个地方函数也会被排除。)

我遇到的问题3:

执行obj.d.m = 333,输出结果为:

image.png

执行console.log(obj.d.m),输出结果为:

image.png

执行上面两行代码,都会打印 “get获取值d ”,一开始以为我的代码写的有问题,但是仔细想想,这个地方没错,就是先读取的obj.d,再去获取d中的属性

5.发布者dep

上面的代码写完了,我们给对象中的每一个属性都添加了get set (暂时我们先不考虑数组的情况)。

那么后续该怎么做呢?其实我就是卡在了这个地方,看了很多讲解这个地方的博客文章,好像都没有说原因,直接拿出来一些没见过的单词,导致初次理解这个地方的我很懵。

没关系

我说说我个人理解的思路,思路很重要,建议多读几遍!

1.我们既然给obj对象的所有属性添加了get set,为了方便我们操作,我们是不是应该在get的时候,记录一下使用了我这个变量。

2.在set的时候,告诉所有使用了这个变量的 ,你应该刷新dom了。


  function defineReactive(obj, key, data) {
        observe(obj[key]) 


        // 1. 来一个数组存储,我们的数据
        var arr=[]

        Object.defineProperty(obj, key, {
            get: function () {

                // 2. 使用的时候 把这个 `谁` 保存起来 。(不知道数组存什么,别着急后面会说,暂时我们存一个对象来表示)
                arr.push({
                    name:"谁"
                })
                console.log('get获取值' + key)
                return data
            },
            set: function (newValue) {
                console.log('set 设置值' + key)
                data = newValue
                observe(data)

                // 3. 值改变的时候,通知 arr中 所有的 `谁` 去更新dom等操作
                arr.forEach(item=>{
                    console.log(item)
                })
            }
        })
    }

先不考虑谁是什么,我们把上面代码优化一下,专业的事情专业的人做,收集依赖 交给Dep

    // 1. 定义一个构造函数
    function Dep() {
        // 2. new 出来的实例对象要有一个属性来存放 谁使用了数据
        this.subs = []
    }

    // 3. new出来的实例对象,有一个方法addSubs :可以收集 `谁` (这里也可以成为收集依赖)
    Dep.prototype.addSubs = function (item) {
        this.subs.push(item)
    }

    // 4. new出来的实例对象,有一个方法addSubs :可以收集 `谁` (这里也可以成为收集依赖)
    Dep.prototype.notify = function (item) {
        this.subs.forEach(item => {
            console.log(item)
        })
    }


     function defineReactive(obj, key, data) {
        observe(obj[key])
        
        // 1
        var dep=new Dep()

        Object.defineProperty(obj, key, {
            get: function () {
                // 2
                dep.addSubs({name:'谁'})
                console.log('get获取值' + key)
                return data
            },
            set: function (newValue) {
                console.log('set 设置值' + key)
                data = newValue
                observe(data)
                // 3
                dep.notify()
            }
        })
    }

这个dep对象,它在get中收集了依赖,在set中发布了改变信息,这个 dep 就是发布者。

现在我们收集的依赖是{name:'谁'}。收集它肯定没有意义。那这个数据怎么去定义?。

6. 订阅者 watcher

怎么理解?首先,什么东西会用到我们的数据,仔细想想,其实本质上是页面会拿到我们的数据用来展示。所以数据来源就是我们页面。(当然我这里表达可能不是很准确,后续彻底理解虚拟dom再作更改)

    // 1.定义一个构造函数
    function Watcher(vm, exp, callback) {

        // 2. 对象   可以理解为一个对象  var obj= {a:{b:{c:1}}}
        this.vm = vm

        // 3. 表达式 可以理解为一个表达式 例如 a.b.c
        this.exp = exp

        // 5.所以  this.vm[this.exp] 可以理解为 obj.a.b.c (当然直接这样 obj['a.b.c'] 可能无法读取,这里找个工具函数 parsePath 转换一下格式)

        // 4. 回调函数
        this.callback = callback


        // 7.定义一个 value 存储 vm中本身的值
        // this.value= parsePath(this.vm,this.exp)

        // 8.但是 第7步 除了存储数据,还要有其他的事情要做,我们把它抽离到 getValue 一个方法中
        this.value = this.getValue()
    }

    Watcher.prototype.getValue = function () {
        let value = parsePath(this.vm, this.exp)
        return value // 9. 切记return
    }

    // 10. 我们在编写dep的时候呢,我们这个对象可以做点什么。所以需要有一个 “做点什么的方法”
    Watcher.prototype.updata = function () {
        console.log('做点什么')

        // 11. 做点什么呢?仔细想想,可以吧最新的值更新到 this.value上
        this.value =  parsePath(this.vm, this.exp)
        
        // 12.其次可以执行callback函数
        this.callback()
    }


7.传递Watcher的实例

  1. 什么时候传递呢?在new Watcher()的时候传递,详情见下面的代码注释; 步骤13-17
  2. 我们Watcher构造函数写好了,怎么传递给到dep.addSubs() 方法中呢,可以放在全局变量上直接传递,确保唯一。
  3. 其次优化一下我们的callback函数,模仿vue的watch实现一下,支持两个参数,一个最新的值,一个旧值,修改一下callback的this指向为 this.vm。
    var obj = {
        a: 1,
        b: 2,
        c: 3,
        d: {
            m: 3
        }
    }

    function defineReactive(obj, key, data) {
        observe(obj[key])

        var dep = new Dep()

        Object.defineProperty(obj, key, {
            get: function () {
                // 15. 在get函数判断一下 如果 window.tomato存在,就存一下我们这个实例
                if (window.tomato) {
                    dep.addSubs(window.tomato)
                }
                console.log('get获取值' + key)
                return data
            },
            set: function (newValue) {
                console.log('set 设置值' + key)
                data = newValue
                observe(data)

                dep.notify()
            }
        })
    }


    function Dep() {
        this.subs = []
    }
    Dep.prototype.addSubs = function (item) {
        this.subs.push(item)
    }
    Dep.prototype.notify = function () {
        this.subs.forEach(item => {
            console.log(item)
        })
    }


    // 1.定义一个构造函数
    function Watcher(vm, exp, callback) {
        console.log(this)

        // 2. 对象   可以理解为一个对象  var obj= {a:{b:{c:1}}}
        this.vm = vm

        // 3. 表达式 可以理解为一个表达式 例如 a.b.c
        this.exp = exp

        // 5.所以  this.vm[this.exp] 可以理解为 obj.a.b.c (当然直接这样 obj['a.b.c'] 可能无法读取,这里找个工具函数 parsePath 转换一下格式)

        // 4. 回调函数
        this.callback = callback


        // 7.定义一个 value 存储 vm中本身的值
        // this.value= parsePath(this.vm,this.exp)
        // 8.但是 第7步 除了存储数据,还要有其他的事情要做,我们把它抽离到 getValue 一个方法中
        this.value = this.getValue()
    }

    Watcher.prototype.getValue = function () {
        /* 
           13.
           + 首先我们给 数据所有属性 加上 get set  ==》  observe(obj)
           + 当我们new Watcher的时候 就会执行构造函数Watcher中的代码。 ==》 new Watcher()
           + 执行到 this.getValue() 的时候,就会调用这个getValue函数  ==》 getValue()

           + ***然后执行到 parsePath(this.vm, this.exp)  这个时候就会读取数据 就会触发parsePath(this.vm, this.exp)的 get函数
        */


        // 14. 在读取数据之前,存储当前的this 当前的this就是 Watcher的实例,存储到哪里呢?随意全局的变量下是可以的,我这里为了区分就存储在了 tomato变量下,可以换成其他变量
        window.tomato = this

        let value = parsePath(this.vm, this.exp)

        // 16.为了防止重复塞入数据,清空全局变量 window.tomato
        window.tomato = null

        return value // 9. 切记return
    }

    // 10. 我们在编写dep的时候呢,我们这个对象可以做点什么。所以需要有一个 “做点什么的方法”
    Watcher.prototype.updata = function () {
        let oldValue = this.value

        // 11. 做点什么呢?仔细想想,可以吧最新的值更新到 this.value 上
        this.value = parsePath(this.vm, this.exp)

        // 12.其次可以执行callback函数  
        // this.callback()

        // 17.优化一下callback
        callback.call(this.vm, this.value, oldValue)


    }

    // 6. 转换的工具函数
    function parsePath(obj, expression) {
        const segments = expression.split('.')
        for (let key of segments) {
            if (!obj) return
            obj = obj[key]
        }
        return obj
    }

    function observe(obj) {
        if (!obj || typeof obj !== 'object') return
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                defineReactive(obj, key, obj[key])
            }
        }
    }

    observe(obj)


    var wq = new Watcher(obj, 'd.m', function () {
        console.log('传入一个函数')
    })


    obj.d.m = 333
    console.log(obj.d.m)

8.完整代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue2 双向数据绑定</title>
</head>

<body>
    </div>
    <script>
        var obj = {
            a: 10,
            b: {
                c: "tomato"
            }
        }

        function defineReactive(obj, key, data) {
            observe(obj[key])

            let dep = new Dep()
            Object.defineProperty(obj, key, {
                get: function () {
                    console.log('get方法执行了,读取字段:' + key,)
                    dep.depend()
                    return data
                },
                set: function (newValue) {
                    console.log('set方法执行了,设置字段:' + key)
                    data = newValue
                    dep.notify()

                }
            })
        }


        function observe(object) {
            if (!obj || typeof object !== 'object') return

            for (const key in object) {
                if (Object.hasOwnProperty.call(object, key)) {
                    defineReactive(object, key, object[key])
                }
            }
        }


        function Dep() {
            this.subs = []
        }
        Dep.prototype.addSubs = function (item) {
            this.subs.push(item)
        }
        Dep.prototype.notify = function () {
            this.subs.forEach(item => {
                console.log(item)
            })
        }
        Dep.prototype.depend = function () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }


        function Watcher(data, exp, callback) {
            this.data = data
            this.exp = exp
            this.callback = callback
            this.value = this.getValue()
        }
        Watcher.prototype.getValue = function () {
            Dep._target = this
            let value = parsePath(this.data, this.exp)
            Dep._target = null
            return value
        }
        Watcher.prototype.update = function () {
            let oldVal = this.value
            this.value = parsePath(this.data, this.exp)
            this.callback.call(this.data, oldVal, this.value)
        }


        function parsePath(obj, expression) {
            const segments = expression.split('.')
            for (let key of segments) {
                if (!obj) return
                obj = obj[key]
            }
            return obj
        }

        observe(obj)

        var wq = new Watcher(obj, 'b.c', function (oldVal, newVal) {
            console.log('旧的', oldVal, '新的', newVal)
        })

        // obj.b.c=123
        // console.dir(obj)
    </script>
</body>

</html>

end

  • 后续实际过完vue2的源码之后,再来继续完善这方面的笔记。
  • 其次剩余 数组待处理,class写法待处理。
  • 在这 watcher到底处理什么数据 这个有待学习。
  • 最后,加油啦小番茄。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lazy_tomato

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值