vue响应式原理一

构建一个响应式系统

本节课,我们将使用vue源码中相似的技能,来构建一个简单的响应式系统。这会让你更好的理解vue的设计模式,也会让你对wathcer 和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的值
  • 重新计算price*quantity 这个表达式的值,并且在网页上更新
  • 调用计算属性totalPriceWithTax, 重新计算并且更新网页

我知道你很疑惑,vue是如何知道price已经改变了,需要更新,它是怎样追踪这一切的呢?JaveScript语言本身并不支持响应式功能。比如:

    let price = 5
    let quantity = 2
    let total = price * quantity  // 10 right?
    price = 20   // price 改变
    console.log(`total is ${total}`) // total并不会变化

以上例子中,price发生变化时,并不会通知total,因此total的值并不会变化。为了让total的值跟随price或者quantity而自动更新,我们需要使用JS来做一些处理。
**

问题一

**
我们需要将如何计算total的过程保存下来,这样当price和quantity改变的时候,我们能够重新运行计算total的过程。

解决方法

首先,我们需要告诉程序,计算total的过程需要被保存下来,我之后会再次运行它。

    let price = 5
    let quantity = 2
    let total = 0
    let target = null   // 保存计算total的过程
    let storage = []    // 运行程序保存在这
    
    target = () => { total = price * quantity }
    
    function record () {  
        storage.push(target) 
    }  // 将计算total的过程保存为一个函数,以便之后调用
    
    function replay () {
        storage.forEach(run => run())
    }   // 重新运行storage中的每一个target
    record()  // 保存target
    target() // 再次计算total
    
    price = 20  // 重新赋值 price
    console.log(total) //  10 total不变
    replay()   // 重新计算tota
    console.log(total) //  40 total更新了

问题二

当我们的需求变的更为复杂的时候,或许一个类能够更好的维持变量以及运行函数的保存,也能让程序变得更自动化。

解决方法:一个依赖类

我们可以根据这些行为封装一个自己的类,使用标准的编程观察者模式。所以我们创建了一个js的类来管理所有的依赖(这也是vue的做法),如下:

    class Dep { // Dep,dependency 依赖的简写
      constructor () {
        this.subscribers = [] // 订阅者- 所有的运行函数将被保存在这里
      }
      depend() {  // 取代我们上述的 record 方法
        if (target && !this.subscribers.includes(target)) {
          // 当target存在且不在subscribers 中的时候,保存
           this.subscribers.push(target)
        } 
      }
      notify() {  
        this.subscribers.forEach(sub => sub()) // 重新运行subscribers中的每一个target函数
      }
    }

现在我们来创建一个实例:

    const dep = new Dep()
  
    let price = 5
    let quantity = 2
    let total = 0
    let target = () => { total = price * quantity }
    dep.depend() // 将total函数添加到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()       // 运行subscribers 中的total
    console.log(total) // => 40  .. Now the right number

现在代码看上去更加规范,以及能被复用。唯一觉得奇怪的是,每次price改变之后,需要手动运行notify函数来改变total,还不算真正的响应式。

问题三

之后,对于每个变量,都会有一个dep实例,如果能将这些需要被重新执行的匿名函数封装在一起会更好。我们使用watcher方法来管理这些行为。将以下代码:

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

替换为:

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

解决方法:使用一个watcher函数

在watcher函数里面,我们做一些简单的事情:

    function watcher(myFunc) {
      target = myFunc // 赋值匿名函数
      dep.depend()       // 存入匿名函数
      target()           // 调用匿名函数
      target = null      // 重置targer,方便下次赋值匿名函数
    }

如上所示,watcher函数接收myFunc作为参数,并且赋值给全局变量target,然后调用depend方法将target存入subscriber,随后调用target,最后重置它。现在我们运行一下代码:

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

看到这里你也许会疑惑,为什么要将myFunc赋值给全局变量target,而不是直接存入subscriber,在文章末尾我们将看到这么做的原因。

问题四

我们现在有一个简单的Dep class (依赖类),我们希望每个变量都有它自己的dep实例,现在我们将这些变量都集合成对象属性的形式:

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

现在我们想象所有的变量(price, quantity)都有它自己的dep class:
在这里插入图片描述
然后我们运行:

    watcher(() => {
      total = data.price * data.quantity
    }  // watcher 中将myfunc存入subscribers, 并执行一次

当price的值被获取时,属于price的依赖类 (dep class) 会将匿名函数 (target) 存入订阅者数组 (subscribers) 中;当quantity的值被获取时,属于quantity的依赖类会将匿名函数存入订阅者数组中。
在这里插入图片描述
如果有一个匿名函数只用到了price属性,我们可以把它添加到price属性专有的依赖类里,如下图所示:
在这里插入图片描述
那我们什么时候会需要运行notify方法呢?当price被重新赋值时。最终,我想在控制台实现以下效果:

>> total
10
>> price = 20  // 重新赋值price时,total的值自动改变
>> total
40

我们需要对price, quantity这样的属性做一些绑定。当这些值被获取的时候,匿名函数会被存入订阅者数组中,当这些值被重新赋值的时候,匿名函数会被再次执行。

解决方法: Object.defineProperty()

我们需要使用到ES5的Object.defineProperty()方法。这个方法允许我们为对象的属性定义getter,setter方法。在我将它使用到我们的依赖类(dep class)之前,先来看看Object.defineProperty()的基础用法:

    let data = { price: 5, quantity: 2 }
    
    Object.defineProperty(data, 'price', {  // For just the price property
    
        get() {  // 当price被获取的时候,执行这里
          console.log(`I was accessed`)
        },
        
        set(newVal) {  // 当price被赋值的时候,执行这里
          console.log(`I was changed`)
        }
    })
    data.price // I was accessed
    data.price = 20  // I was changed

如你所见,它会打印出你定义的两行。但上述例子中的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()

现在get 和set 方法能正常运行了。当属性值被获取,或者重新设置时,我们能收到通知了。如果使用遍历的话,我们能对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

现在,每个属性都有了自己的get, set方法。
上述案例主要用到了两种想法:

  1. 讲生成变量(total)的过程保存为一个函数,以便日后调用。
  2. 使用Object.defineProperty()方法追踪变量的获取和修改。

合并两个想法

    total = data.price * data.quantity

当以上代码执行,或者是获取到price值的时候,我们希望price记住这个方法。这样,当price改变,或者被重新赋值时,就会自动触发这个函数执行,因为它知道,这个函数是依赖于price的。所以你可以这样来记:

  • Get => 记住这个匿名函数,我们会在属性值改变的时候再次运行它。
  • Set => 执行刚刚保存的匿名函数,属性响应式改变。

在依赖类(dep class)中表现如下:

  • Price accessed (get) => 调用dep.depend()来保存当前的target
  • Price set =>调用dep.notify()来运行所有依赖于price的target

以下是拼接之后,最终的响应式系统代码:

    let data = { price: 5, quantity: 2 }
    let target = null
    
    // 步骤一:构建自己的依赖类
    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())
      }
    }
    // 步骤二:使用 Object.defineProperty 追踪属性被获取或者更新
    // 遍历数据中的每一个变量,生成对应的依赖类
    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() //存入target中保存的匿名函数
          return internalValue
        },
        set(newVal) {
          internalValue = newVal
          dep.notify() // 运行所有依赖于该属性的匿名函数
        }
      })
    })
    
    // 步骤三:watch需要响应式属性,此处是total
    // 我们不再需要运行dep.depend, 因为方法内部自己已经调用了
    function watcher(myFunc) {
      target = myFunc
      target()
      target = null
    }
    watcher(() => {
      data.total = data.price * data.quantity
    })  // total是需要根据price,quantity更新的变量,需要被watch

现在我们实现了我们想要的功能,改变price或者quantity,total会随之响应改变:
在这里插入图片描述

来讲讲vue

现在能理解一点vue文档中的下列插图了:
在这里插入图片描述
我们可以看到,包含data的紫色圆圈中有我们熟悉的get, set方法。对于每一个组件实例,我们都有一个watcher实例(蓝色圈圈),来收集所有getter触发的依赖。当随后getter被执行,它会通知watcher, 引起组件的重新渲染。当然,vue的底层原理远比我们今天演示的案例要复杂得多,但是现在我们知道了基本原理。在接下来的课程中我们会更深入的理解vue, 探索其源码是否有用到这种模式。

课程总结

  1. 如何创建一个依赖类 (dep class), 用以收集依赖和重新执行依赖。
  2. 如何创建一个观察者(watcher),用以管理我们之后会再次运行的代码(依赖)。
  3. 如何使用Object.defineProperty()来创建setter和getter。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值