vue响应式原理

 约 23 分钟

建立反应系统

在本课程中,我们将使用与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/

阅读 442

推荐阅读
目录