Vue双向绑定原理以及原生JS实现

通过Object.defineProperty()方法给car定义了一个price属性,并把这个属性的读和写分别使用get()set()进行拦截,每当该属性进行读或写操作的时候就会出发get()set()。如下图:

可以看到,car已经可以主动告诉我们它的属性的读写情况了,这也意味着,这个car的数据对象已经是“可观测”的了。

为了把car的所有属性都变得可观测,我们可以编写如下两个函数:

/**   * 把一个对象的每一项都转化成可观测对象   * @param { Object } obj 对象   */    function observable (obj) {        if (!obj || typeof obj !== ‘object’) {            return;        }        let keys = Object.keys(obj);        keys.forEach((key) =>{            defineReactive(obj,key,obj[key])        })        return obj;    }/**`     `* 使一个对象转化成可观测对象`     `* @param { Object } obj 对象`     `* @param { String } key 对象的key`     `* @param { Any } val 对象的某个key的值`     `*/`    `function defineReactive (obj,key,val) {`        `Object.defineProperty(obj, key, {`            `get(){`                console.log(${key}属性被读取了);                `return val;`            `},`            `set(newVal){`                console.log(${key}属性被修改了);``                val = newVal;            }        })    `}

现在,我们就可以这样定义car:

let car = observable({        ‘brand’:‘BMW’,        ‘price’:3000    })

car的两个属性都变得可观测了。

完成了数据的’可观测’,即我们知道了数据在什么时候被读或写了,那么,我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。

现在,我们需要创建一个依赖收集容器,也就是消息订阅器Dep,用来容纳所有的“订阅者”。订阅器Dep主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。

创建消息订阅器Dep:

class Dep {        constructor(){            this.subs = []        },        //增加订阅者        addSub(sub){            this.subs.push(sub);        },        //判断是否增加订阅者        depend () {            if (Dep.target) {                this.addSub(Dep.target)            }        },

//通知订阅者更新        notify(){            this.subs.forEach((sub) =>{``sub.update()            })        }    }``Dep.target = null;

有了订阅器,再将defineReactive函数进行改造一下,向其植入订阅器:

function defineReactive (obj,key,val) {        let dep = new Dep();        Object.defineProperty(obj, key, {            get(){                dep.depend();                ``console.log(KaTeX parse error: Expected 'EOF', got '}' at position 59: …;`            `}̲,`            `…{key}属性被修改了);``                dep.notify()                    //数据变化通知所有订阅者            }        })    }

从代码上看,我们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一 的Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。

我们将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。

到此,订阅器Dep设计完毕,接下来,我们设计订阅者Watcher.

订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中,那该如何添加呢?我们已经知道监听器Observer是在get函数执行了添加订阅者Wather的操作的,所以我们只要在订阅者Watcher初始化的时候出发对应的get函数去执行添加订阅者操作即可,那要如何触发get的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了Object.defineProperty( )进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者Watcher的实现如下:

class Watcher {        constructor(vm,exp,cb){            this.vm = vm;            this.exp = exp;            this.cb = cb;            this.value = this.get();  // 将自己添加到订阅器的操作        },

update(){            let value = this.vm.data[this.exp];            let oldVal = this.value;            if (value !== oldVal) {                this.value = value;                this.cb.call(this.vm, value, oldVal);            },        get(){            Dep.target = this;  // 缓存自己            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数            Dep.target = null;  // 释放自己            return value;        }    }

过程分析:

订阅者Watcher 是一个 类,在它的构造函数中,定义了一些属性:

  • **vm:**一个Vue的实例对象;

  • **exp:**是node节点的v-modelv-on:click等指令的属性值。如v-model="name"exp就是name;

  • **cb:**是Watcher绑定的更新函数;

当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行:

Dep.target = this;  // 缓存自己

实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行了:

let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数

在这个过程中会对 vm 上的数据访问,其实就是为了触发数据对象的getter

每个对象值的 getter都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target 恢复成上一个状态,即:

Dep.target = null;  // 释放自己

因为当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。

update()函数是用来当数据发生变化时调用Watcher自身的更新函数进行更新的操作。先通过let value = this.vm.data[this.exp];获取到最新的数据,然后将其与之前get()获得的旧数据进行比较,如果不一样,则调用更新函数cb进行更新。

至此,简单的订阅者Watcher设计完毕。

完成以上工作后,我们就可以来真正的测试了。

index.html

```````    ` Document`````    `

`````    ```

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+点击【学习资料】即可免费领取!

档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!有需要的程序猿(媛)可以帮忙点赞+点击【学习资料】即可免费领取!

[外链图片转存中…(img-89qcGDUP-1713573419538)]

[外链图片转存中…(img-HfQ8b4Oj-1713573419539)]

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值