资讯专栏INFORMATION COLUMN

关于VUE响应式数据的最佳解释

endiat / 1100人阅读

摘要:遍历执行其中存储的所有的匿名函数。如果我们使用一个类来管理相关依赖,这很接近的表现方式代码看起来就像下面这样你会发现现在匿名函数被储存在而不是原来的。

许多前端框架(如Angular,React,Vue)都有自己的响应式引擎。通过理解如何响应,提议提升你的开发能力并能够更高效地使用JS框架。本文中构建的响应逻辑与Vue的源码是一毛一样的!

响应系统

初见时,你会惊讶与Vue的响应系统。看看以下面这些简单代码

Price:${{price}}
Total:${{price*quantity}}
Taxes:${{totalPriceWithTax}}

更新页面上的price

重新计算pricequantity的乘积,更新页面

调用totalPriceWithTax函数并更新页面

等等,你可能会疑惑为何Vue知道price变化了,它是如何跟踪所有的变化?

这并非日常的JS编程会用到的

如果你疑惑,那么最大的问题是业务代码通常不涉及这些。举个例子,如果我运行下面代码:

let price = 5
let quantity = 2
let total = price *quantity
price = 20
console.log(`total is ${total}`)

即便我们从未使用过Vue,我们也能知道会输出10。

>> total is 10

更进一步,我们想要在price和quantity更新时

total is 40

遗憾的是,JS是一个程序,看着它它也不会变成响应式的。这时我们需要coding

难题

我们需要存储计算的total,以便在pricequantity变化时,重新运行。

解决

首先我们需要告知应用“下面我要运行的代码先保存起来,我可能在别的时间还要运行!”之后但我们更新代码中pricequantity的值时,之前存储的代码会被再次调用。

// save code

let total = price * quantity

// run code

// later on rung store code again

所以通过记录函数,可以在变量改变时多次运行:

let price = 5
let quantity = 2
let total = 0
let target = null
target = function(){
    total = price * quantity
}
record() // 稍后执行
target()

注意target 存储了一个匿名函数,不过如果使用ES6的箭头函数语法,我们可以写成这样:

target = () => {
    total = price * quantity
}

然后我们再简单滴定义一下record函数:

let storage = [] //在starage 中存放target函数
function record(){
    storage.push(target)
}

我们存储了target(上述例子中就是{ total = price * quantity }),我们在稍后会用到它,那时使用target,就可以运行我们记录的所有函数。

function target(){
    storage.forEach(run => run())
}

遍历storage执行其中存储的所有的匿名函数。在代码中我们可以这样:

price = 20
console.log(total)  //  => 10
replay()
console.log(total)  //  => 40

足够简单吧!如果你想看看目前阶段完整的代码,请看:

let price = 5
let quantity = 2
let quantity = 0
let target = null
let storage = []
function record () {
    storage.push(target)
}
function replay() {
    storage.forEach(run => run())
}
target = () => {
    total = price * quantity
}
record()
target()


price = 20
console.log(total)  // => 10
replay()
console.log(total)  // => 40
难题

功能虽然可以实现,但是代码似乎不够健壮。我们需要一个类,来维护目标列表,在需要重新执行时来通知执行。

解决

通过将所需要的方法封装成一个依赖类,通过这个类实现标准的观察者模式。
如果我们使用一个类来管理相关依赖,(这很接近VUE的表现方式)代码看起来就像下面这样:

class Dep {
    constructor(){
        this.subscribers = []
    }
    depend() {
        if(target && !this.subscribers.includes(target)){
            this.subscribers.push(target)
        }
    }
    notify() {
        this.subscribers.forEach(sub => sub())
    }
    
}

你会发现现在匿名函数被储存在subscribers而不是原来的storage。同时,现在的记录函数叫做depend而不是record,通知函数是notify而非replay。看看他们执行情况:

const dep = new Dep()
let price = 5
let quantity = 2
let quantity = 0
let target = () => {
    total = price * quantity
}
dep.depend()  //将target添加进subscribers
target()      //执行获取total


price = 20
console.log(total)  // => 10
dep.notify()
console.log(total)  // => 40

现在代码的复用性已经初见端倪,但是还有一件别扭的事,我们还需要配置与执行目标函数。

难题

以后我们会为每个变量创建一个Dep类,对此我们应该使用一个watcher函数来监听并更新数据,而非使用这样的方式:

let target = () => {
    total = price * quantity
}
dep.depend()  
target()      

期望中的代码应该是:

watcher(() => {
    total = price * quantity
})
解决 实现watcher函数

在watcher函数中我们做了下面这些事:

function watcher(myFunc){
    target = myFunc
    dep.depend() 
    target()
    target = null 
}

如你所见,watcher接受一个myFunc作为参数,将其赋值给全局变量target,并将它添加微订阅者。在执行target后,重置target为下一轮做准备!
现在只需要这样的代码

price = 20
console.log(total)  // => 10
dep.notify()
console.log(total)  // => 40

你可能会疑惑为什么target是一个全局变量的形式,而非作为一个参数传入。这个问题在结尾处会明朗起来!

难题

现在我们拥有了一个简单的Dep类,但我们真正想要的是每个变量都能拥有一个自己的Dep类。先让我们把之前讨论的特性变成一个对象吧!

let data = { price: 5,quantity: 2}

我们先假设,每个属性都有自己的Dep类:

现在我们运行

watcher(() => {
    totla = data.price * data.quantity
})

由于total需要依赖price和quantity两个变量,所以这个匿名函数需要被写入两者的subscriber数组中!
同时如果我们又有一个匿名函数,只依赖data.price,那么它仅需要被添加进price的dep的subscriber数组中

但我们改变price的值时,我们期待dep.notify()被执行。在文章的最末,我们期待能够有下面这样的输出:

>> total
10
>> price =20
>> total
40

所以现在我们需要去挂载这些属性(如quantity和price)。这样当其改变时就会触发subscriber数组中的函数。

解决 Object.defineProperty()

我们需要了解Object.defineProperty函数
ES5种提出的,他允许我们为一个属性定义getter与setter函数。在我们把它和Dep结合前,我先为你们演示一个非常基础的用法:

let data = { price: 5,quantity: 2}
Object.defineProperty(data,"price",{
    get(){
        console.log(`Getting price ${internalValue}`);
        return internalValue
    }
    set(newValue){
        console.log(`Setting price ${newValue}`);
        internalValue = newValue
    }
})
total = data.price * data.quantity  // 调用get
data.price = 20                      //  调用set

现在当我们获取并设置值时,我们可以触发通知。通过Object.keys(data)返回对象键的数组。运用一些递归,我们可以为数据数组中的所有项运行它。

let data = { price: 5,quantity: 2}
Object.keys(data).forEach((key) => {
    let internalValue = data[key]
    Object.defineProperty(data, key,{
        get(){
            console.log(`Getting ${key}:${internalValue}`);
            return internalValue
        }
        set(newValue){
            console.log(`Setting ${key} to ${newValue}`);
            internalValue = newValue
        }
    })
})
total = data.price * data.quantity  
data.price = 30     

现在你可以在控制台上看到:

Getting price: 5
Getting quantity: 20
Setting price to 30
成亲了
total = data.price * data.quantity

类似上述代码运行后,获得了price的值。我们还期望能够记录这个匿名函数。当price变化或事被赋予了一个新值(译者:感觉这是一回事)这个匿名函数就会被促发。

Get => 记住这个匿名函数,在值变化时再次执行!
Set => 值变了,快去执行刚才记下的匿名函数

就Dep而言:
Price被读 => 调用dep.depend()保存当前目标函数
Price被写 => 调用dep.notify()去执行所有目标函数

好的,现在让我们将他们合体,并祭出最后的代码。

let data = {price: 5,quantity: 2}
let target = null
class Dep {
    constructor(){
        this.subscribers = []
    }
    depend() {
        if(target && !this.subscribers.includes(target)){
            this.subscribers.push(target)
        }
    }
    notify() {
        this.subscribers.forEach(sub => sub())
    }
}

Object.keys(data).forEach((key) => {
    let internalValue = data[key]
    const  dep = new Dep()
    Object.defineProperty(data, key,{
        get(){
            dep.depend()
            return internalValue
        }
        set(newValue){
            internalValue = newValue
            dep.notify()
        }
    })
})
function watcher(myFunc){
    target = myFunc
    target();
    target = null;
}
watch(() => {
    data.total = data.price * data.quantity
})

猜猜看现在会发生什么?

>> data.total
10
>> data.price = 20
20
>> data.total
40
>> data.quantity = 3 
3
>> data.total
60

正如我们所期待的那样,pricequantity现在是响应式的了!当pricequantity更跟新时,被监听函数会被重新执行!
现在你应该可以理解Vue文档中的这张图片了吧!

看到图中紫色数据圈gettersetter吗?看起来应该很熟悉!每个组件实例都有一个watcher实例(蓝色),它从getter(红线)收集依赖项。稍后调用setter时,它会通知观察者导致组件重新渲染。下图是一个我注释后的版本。

虽然Vue实际的代码愿彼此复杂,但你现在知道了基本的实现了。

那么回顾一下

我们创建一个Dep类来收集依赖并重新运行所有依赖(notify)

watcher函数来将需要监听的匿名函数,添加到target

使用Object.defineProperty()去创建gettersetter

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96416.html

相关文章

  • 前方来报,八月最新资讯--关于vue2&3最佳文章推荐

    摘要:哪吒别人的看法都是狗屁,你是谁只有你自己说了才算,这是爹教我的道理。哪吒去他个鸟命我命由我,不由天是魔是仙,我自己决定哪吒白白搭上一条人命,你傻不傻敖丙不傻谁和你做朋友太乙真人人是否能够改变命运,我不晓得。我只晓得,不认命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出处 查看github最新的Vue...

    izhuhaodev 评论0 收藏0
  • VUE防抖与节流最佳解决方案——函数组件

    摘要:案例持续触发事件时,并不立即执行函数,当毫秒内没有触发事件时,才会延时触发一次函数。也以函数形式暴露普通插槽。这样的场景组件用函数式组件是非常方便的。相关阅读函数式组件自定义指令前言 有echarts使用经验的同学可能遇到过这样的场景,在window.onresize事件回调里触发echartsBox.resize()方法来达到重绘的目的,resize事件是连续触发的这意味着echarts...

    OldPanda 评论0 收藏0
  • React 可视化开发工具 Shadow Widget 非正经入门(之一:React 三宗罪)

    摘要:前言非正经入门是相对正经入门而言的。不过不要紧,正式学习仍需回到正经入门的方式。快速入门建议先学会用拼文写文档注册一个账号,把库到自己名下,然后用这个库写自己的博客,参见这份介绍。会用拼文写文章,相当于开发已入门三分之一了。 本系列博文从 Shadow Widget 作者的视角,解释该框架的设计要点,既作为用户手册的补充,也从更本质角度帮助大家理解 Shadow Widget 为什么这...

    dongxiawu 评论0 收藏0

发表评论

0条评论

endiat

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<