文章目录
许多javascript框架(比如:Angular,React,以及Vue)都有它们自己的响应式引擎。我们如果理解了响应式的原理,搞清楚它们是怎么工作的,我们可以提高自己的编程技巧,更有效的使用JavaScript框架。下面的视频和内容,将会帮助我们实现一个与Vue一样的响应式引擎。
响应式系统
当我们第一次看见Vue响应式系统工作的时候,感觉它就像充满了魔法一样。 现在我们举一个简单的例子
<div id="app">
<div> Price :${{price}} </div>
<div> Total:${{price * quantity}} </div>
<div> Taxes: ${{totalPriceWithTax}} </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script>
var vm = new Vue({
el:"#app",
data:{
price:5.00,
quantity:2
},
computed:{
totalPriceWithTax(){
return this.price * this.quantity * 1.03
}
}
})
</script>
不知道为什么,当price 发生变化的时候,Vue就知道自己需要做三件事情:
- 更新页面上price的值
- 计算表达式 price*quantity 的值,更新页面
- 调用totalPriceWithTax 函数,更新页面
老乡等一等,我清楚你现在有很多疑惑,当price改变的时候,Vue怎么知道需要更新什么东西?它是怎么记录这一切的。
这根本就不是Javascript程序通常的工作方式呀
如果我们不明白其中的套路的话,最大的问题在于,我们发现所编写的程序不是按照这种方式工作的。现在举一个例子
let price = 5
let quantity = 2
let total = price * quantity // 10 right ?
price = 20
console.log(`total is ${total}`)
如果我们没有使用Vue的话,输出的结果是10.
>> total is 10
在Vue中,不管是price还是quantity发生变化的时候,total都会更新数据,我们期待的结果是
>> total is 40
很悲剧的是,Javascript 是程序性的(procedural)(此处无法找一个合适的中文),而不是响应式的,这在现实生活中,不能够产生。为了使 total具有响应式。我们需要使用javascript做一些事情。()
关键点
我们需要保存计算total 的方式,当price 或 quantity 发生变化的时候,重新运行一下。
解决方案
首先我们需要一些方式来告诉我们的应用,“我将要运行的代码,我存储在某一个地方,有时候(当数据发生改变的时候),我需要你再运行一遍”。然后我们运行代码,如果price 或 quantity 更新的时候,再一次运行存储好的代码。
我们可以通过存储一个函数来实现这个功能,当我们需要的时候,再执行一次
let price = 5
let quantity = 2
let total = 0
let target = null
target = function () {
total = price * quantity
}
record() // Remember this in case we want to run it later
target() // Also go ahead and run it
需要注意的是,我们把匿名函数存储在target变量中,然后调用record 函数 使用ES6的的箭头函数语法,我们也可以如下写法
target = () => { total = price * quantity }
record 函数的定义很简单
let storage = [] // We'll store our target functions in here
function record () { // target = () => { total = price * quantity }
storage.push(target)
}
因为现在我们存储了target (total = price * quantity) ,所以我们之后可以去运行它。我们可以创建一个 replay 函数来运行所有存储的target
function replay () {
storage.forEach(run => run())
}
上面的代码执行了我们存储在storage 数组中的所有的匿名函数
继续我们之前的代码
price = 20
console.log(total) // =>10
replay()
console.log(total) // => 40
是不是发现就是这么简单?下面是整个代码
let price = 5
let quantity = 2
let total = 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
未解决的问题
如果我们使用上面的代码,我们发现代码无法重复利用。我们可以定义一个类来存储一系列targets ,当我们要再运行这些targets的时候,我们再去通知这些targets.
创建Dependency 类
我们可以将这些封装在了一个类中,其实这个类实现了一个观察者模式,所以,如果我们创建一个类,来管理我们的依赖,实现的代码如下
class Dep { // Stands for dependency
constructor() {
this.subscribers = [] // The targets that are dependent ,and should be
// run when notify() is called
}
depend() { // This replaces our record function
if(target && !this.subscribers.includes(target)) {
// Only if there is a target & it's not already subscribed
this.subscribers.push(target)
}
}
notify() { // Replaces our replay function
this.subscribers.forEach(sub => sub()) // Run our targets , or observers
}
}
- 1.我们使用 subscribers 代替了 storage 来存储我们的匿名函数
- 2. 我们使用depend函数 代替了record函数
- 3. 我们使用了notify函数代替了replay函数
对应的代码如下
const dep = new Dep()
let price = 5
let quantity = 2
let total = 0
let target = () => { total= price * quantity }
dep.depend() // Add this target to our subscribers
target() // Run it to get the total
console.log(total) // =>10 .. The right number
price = 20
console.log(total) // =>10 .. No longer the right number
dep.notify() // Run the subscirbers
console.log(total) // => 40 .. Now the right nubmer
上述的代码也达到了同样的效果,也解决了代码重复利用的问题了。但是上述的代码是最优的吗?我们在设置,运行 target 的时候是否觉得别扭。
碰到的问题
将来我们将为每一个变量建立一个Dep 对象。把那些需要被监听起来的匿名函数封装起来,估计会更好。我们可以建立一个watcher 函数满足这种需求。
我们可以将
target = () => { total = price * quantity }
dep.depend()
target()
使用下列代码代替
watcher(()=>{
total = price * quantity
})
Watcher 函数的实现
在watcher 函数中,我们干了这些事情
function watcher(myFunc) {
target = myFunc //Set as the active target
dep.depend() // Add the active target as a dependency
target() // Call the target
target = null // Reset the target
}
现在当我们运行下列代码的时候
price = 20
console.log(total)
dep.notify()
console.log(total)
也许大家会有个疑问,为什么我们要把target设置为一个全局变量?而不是作为一个参数传进函数 。这么做的原因,在这篇文章结束的时候,你将会发现设置为全局变量的好处。
碰到的问题
我现在有个一个Dep 类,但是我们真正想要的是,为每一个变量都分配一个自己的Dep. 如下面的对象
let data = {price:5, quantity:2}
我们需要每一个属性都有自己的Dep 类,如下图所示
现在当我运行
watcher(() => {
total = data.price * data.quantity
})
因为使用到了data.price ,那我们肯定想要 price 的依赖类 能够 把匿名函数(存储在target中)保存在subscriber 数组中(通过调用dep.depend()方法),此外在这个匿名函数中还是使用了data.quantity ,那么同样的操作也需要在 quantity的dep 类中进行,如下图所示
如果我们还有宁外一个匿名函数,该匿名函数我们只用到了data.price , 那么我们只需要把这个匿名函数保存到price属性对应的依赖类中。
什么时候,我想要调用price dep 类中 dep.notify()呢?当price发生改变的时候,想要去调用对应dep.notify。在该文章的结束的时候,我们就能够实现以下的功能
>> total
10
>> price = 20 // when this gets run it will need to call notify() on the price
>> total
40
我们需要找到方法hook住对象的属性(如 price 或quantity),当有地方使用到他们的时候,我们能够把对应target保存到对应的subscriber数组中,当属性值发生变化的时候,我们能够运行保存在sunscriber中的函数,那利用什么方法能够做到呢?
Object.defineProperty()
我们需要了解在es5中实现的Object.defineProperty() 的功能。这个方法能够允许我们为一个属性定义 setter 和getter 方法。我现在给大家展示一个简单的例子
let data = { price:5 , quantity: 2}
Object.defineProperty(data, 'price', { // For just the price property
get() { // Create a get method
console.log('I was accessed')
},
set(newVal) { // Create a set method
console.log('I was changed')
}
})
data.price // This calls get()
data.price = 20 // this.call set()
我们会发现,打印了两行日志,但是我们并没有get 或 get 任何值。 我们期待get()能返回一个值,set() 更新一个值. 我们现在来新增一个变量 internalValue 存储我们当前的price值
let data = {price; 5, quantity: 2}
let internalValue = data.price // Our intital value
Object.defineProperty(data, 'price', { // For just the price property
get() { // Create a get method
console.log(`Getting price: ${internalValue}`)
return internalValue
},
set(newVal) { // Create a set method
console.log(`Setting price to ${newVal}`
internalValue = newVal
}
})
total = data.price * data.quantity // This call get()
data.price = 20 // This calls set()
现在我们的get和set能够正常工作了,你来猜猜console打出的是什么
现在在获取属性值,更新属性值的时候,会收到对应的通知。我们遍历对象的所有属性,那么可以为所有的属性添加上对应的钩子。遍历对象我们可以通过Object.keys(data) 来实现。
let data = {price:5, quantity: 2}
Object.keys(data).forEach(key => { // We're running this for each item in data now
let internalValue = data[key]
Object.defineProperty(data, key, {
get() {
console.log(`Getting ${key}: ${internalValue}`)
return internalValue
},
set(newVal) {
console.log(`Setting ${key} to : ${newVal}` )
internalValue = newVal
}
})
})
total = data.price * data.quantity
data.price = 20
现在所有的属性都有getters 和setters ,在控制台上,输出为
把上述的两个想法结合起来
total = data.price * data.quantity
当代码运行起来的时候,在获取price的值得时候,我们想要price记住匿名函数(target),此外当price设置成一个新值的时候,能够触发这个匿名函数运行一遍。所有我们可以设想成这样
Get => 存储这个匿名函数,如果属性值改变的时候,我们再次运行这个匿名函数。
Set => 运行保存的匿名函数,因为我们的值刚刚改变
就 Dep 类而言
Price accesed (get) => 调用dep.depend() 来保存当前target
Price set => 调用与price有关的dep.notify() ,运行所有的targets
结合以上两个点子,完整的代码如下
let data = { price: 5, quantity: 2 }
let target = null
// This is exactly the same Dep class
class Dep {
constructor() {
this.subscribers = []
}
depend() {
if (target && !this.subscribers.includes(target)) {
//Only if there is a target & it's not already subscribed
this.subscribers.push(target)
}
}
notify() {
this.subscribers.forEach(sub => sub())
}
}
// Go through each of our data properties
Object.keys(data).forEach(key => {
let internalValue = data[key]
// Each property gets a dependency instance
const dep = new Dep()
Object.defineProperty(data, key, {
get() {
dep.depend() // <-- Remember the target we're running
return internalValue
},
set(newVal) {
internalValue = newVal
dep.notify() // <-- Re-run stored functions
}
})
})
//My watcher no longer calls dep.depend,
//since that gets called from inside our get method.
function watcher(myFunc) {
target = myFunc
target()
target = null
}
watcher(() => {
data.total = data.price * data.quantity
})
现在我们运行的时候,我们看看console中打印的日志
这样真的实现我们想要的结果,price 和quantity真正的做到了响应式的了 ,无论是price还是quantity发生改变 target都能发生改变。
现在我们就能明白官方文档的中响应式原理图。
从图中我们可以看到紫色圆形区域的Data ,与上面的所讲解的内容很像啊。每一个组件都有一个watcher实例(蓝色的圆形区域) 在getter的时候去收集依赖(红色的虚线)当setter被调用的时候,去通知watcher, 触发组件重新渲染。下面的图是我添加了注释的响应式原理图
这样就更加清晰明了。Vue实现这个更复杂,但是我们现在明白了基本原理。
我们学到了什么
- 怎么创建一个 收集依赖,运行所有依赖的Dep 类
- 怎么创建一个 wacher 来管理需要运行的代码,这个代码被添加到target上作为依赖
- 怎么使用Object.defineProperty()来创建getters和setters
接下来学什么呢?
如果你喜欢和我一起学习,那么我们接下来学习使用Proxies来实现响应式
发表评论