乐趣区

vue响应式原理

建立反应系统

在本课程中,我们将使用与 Vue 源代码中发现的非常相同的技术来构建一个简单的反应系统。这将使您更好地了解 Vue.js 及其设计模式,并使您熟悉观察者和 Dep 类。

反应系统

当您第一次看到 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>

Vue 以某种方式只知道如果 price 发生更改,它应该做三件事:

  1. 更新 price 我们网页上的值。
  2. 重新计算相乘的表达式 price * quantity,然后更新页面。
  3. totalPriceWithTax 再次调用该函数并更新页面。

但是,等等,我听说您想知道,Vue 如何知道 price 更改时要更新的内容,以及如何跟踪所有内容?

这不是 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 中,我们希望 total 随时更新 price 或 quantity 更新。我们想要:

total is 40

不幸的是,JavaScript 是程序性的,不是反应性的,因此在现实生活中不起作用。为了使 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 功能来运行我们记录的所有内容。

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

这将遍历我们存储在存储阵列中的所有匿名函数,并执行每个匿名函数。

然后在我们的代码中,我们可以:

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

问题
我们可以根据需要继续记录目标,但是最好有一个可以随我们的应用扩展的更强大的解决方案。也许是一类负责维护目标列表的类,当我们需要它们重新运行时会得到通知。

解决方案:依赖项类
我们可以开始解决这个问题的方法之一是通过封装这种行为到自己的类,一个依赖类,它实现了标准的编程观察者模式。

因此,如果我们创建一个 JavaScript 类来管理我们的依赖关系(与 Vue 处理事物的方式更接近),它可能看起来像这样:

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.
  }
}

请注意,而不是 storage 我们现在将匿名函数存储在中 subscribers。 record 现在我们调用 depend而不是我们的函数,现在我们使用 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 subscribers 
console.log(total) // => 40  .. Now the right number

它仍然有效,现在我们的代码更可重用了。唯一仍然感到有些奇怪的是设置和运行 target。

问题
将来,我们将为每个变量提供一个 Dep 类,很好地封装创建需要监视更新的匿名函数的行为。也许有一个 watcher 功能可能是为了照顾这种行为。

因此,与其调用:

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

(这只是上面的代码)

相反,我们可以致电:

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

解决方案:观察者功能
在 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
}

如您所见,该 watcher 函数接受一个 myFunc 参数,将其设置为我们的全局 target 属性,调用 dep.depend() 以将我们的目标添加为订户,调用该 target 函数并重置 target。

现在,当我们运行以下命令时:

price = 20
console.log(total)
dep.notify()      
console.log(total) 


您可能想知道为什么我们将其实现 target 为全局变量,而不是在需要时将其传递给函数。这是有充分的理由的,这将在本文结尾处变得显而易见。

问题
我们只有一个 Dep class,但我们真正想要的是每个变量都具有自己的 Dep。让我先将内容移入属性,然后再继续。

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

让我们假设一下,每个属性(price 和 quantity)都有自己的内部 Dep 类。

现在,当我们运行时:

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

由于该 data.price 值已被访问(因此),因此我希望该 price 属性的 Dep 类将我们的匿名函数(存储在中 target)推入其用户数组(通过调用 dep.depend())。由于 data.quantity 已访问,因此我也希望 quantity 属性 Dep 类将该匿名函数(存储在中 target)推送到其订户数组中。

如果我有另一个匿名函数 data.price 可以访问 just,我希望将其仅推送到 price 属性 Dep 类。

我什么时候想 dep.notify()被 price 的订阅者调用?我希望它们在 price 设置时被调用。在文章结尾,我希望能够进入控制台并执行以下操作:

>> total
10
>> price = 20  // When this gets run it will need to call notify() on the price
>> total
40

我们需要某种方法来挂钩数据属性(例如 price 或 quantity),以便在访问数据属性时将其保存 target 到我们的订户数组中,并在更改它后运行存储在我们的订户数组中的函数。

解决方案:Object.defineProperty()
我们需要学习 Object.defineProperty()函数,它是普通的 ES5 JavaScript。它允许我们为属性定义 getter 和 setter 函数。Lemme 向您展示了非常基本的用法,然后向您展示了如何在 Dep 类中使用它。

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 calls set()

如您所见,它仅记录两行。然而,实际上它并不 get 和 set 任何值,因为我们过度乘坐的功能。让我们现在将其添加回去。get()期望返回一个值,并且 set()仍然需要更新一个值,因此让我们添加一个 internalValue 变量来存储当前 price 值。

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

let internalValue = data.price // Our initial 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 calls get() 
data.price = 20  // This calls set()

现在,我们的获取和设置工作正常,您认为将打印到控制台什么?

因此,当我们获取并设置值时,我们有一种方法可以得到通知。通过一些递归,我们可以对数据数组中的所有项目运行此命令,对吗?

仅供参考,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

现在,所有内容都有吸气剂和吸气剂,我们在控制台上看到了。

将两个想法放在一起

total = data.price * data.quantity

当这样的一段代码运行并获得的值时 price,我们想 price 记住这个匿名函数(target)。这样,如果 price 被更改或设置为新值,它将触发此函数重新运行,因为它知道此行依赖于此行。所以您可以这样想。

Get => 请记住该匿名函数,当我们的值更改时,我们将再次运行它。

Set => 运行保存的匿名函数,我们的值刚刚更改。

或就我们的 Dep Class 而言

价格获取(获取)=> 调用 dep.depend()以保存当前价格 target

设置价格 => 调用 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})

现在来看一下我们玩游戏时控制台中会发生什么。

正是我们所希望的!两者 price 和 quantity 的确是反应性的!每当 price 或 quantity 更新值时,我们的总代码就会重新运行。

参考
https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system/

退出移动版