当前位置: 首页 > news >正文

【翻译】构建响应式系统-vue

声明

本文是对于Build a Reactivity System的翻译

目标读者

使用过vue,并且对于vue实现响应式的原理感兴趣的前端童鞋。

正文

本教程我们将使用一些简单的技术(这些技术你在vue源码中也能看到)来创建一个简单的响应式系统。这可以帮助你更好地理解Vue以及Vue的设计模式,同时可以让你更加熟悉watchers和Dep class.

响应式系统

当你第一次看到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 * quatity,并且将计算后的值更新到页面上。
  • 调用totalPriceWithTax函数并更新页面。

但是等一下,当price改变的时候vue是怎么知道要更新哪些东西的?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中我们想要total的值可以随着price或者quantity值的改变而改变,我们想要:

>> total is 40
复制代码

不幸的是,JavaScript本身是非响应式的。为了让total具备响应式,我们需要使用JavaScript来让事情表现的有所不同。

问题

我们需要先保存total的计算过程,这样我们才能在price或者quantity改变的时候重新执行total的计算过程。

解决方案

首先,我们需要告诉我们的应用,“这段我将要执行的代码,保存起来,后面我可能需要你再次运行这段代码。”这样当price或者quantity改变的时候,我们可以再次运行之前保存起来的代码(来更新total)。

我们可以通过将total的计算过程保存成函数的形式来做,这样后面我们能够再次执行它。

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())
}
复制代码

replay函数会遍历所有我们存储在storage中的匿名函数然后挨个执行这些匿名函数。 紧接着,我们可以这样用replay函数:

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
复制代码

问题

上面的代码虽然也能工作,但是可能并不好。或许可以抽取个class,这个class负责维护targets列表,然后在我们需要重新运行targets列表的时候接收通知(并执行targets列表中的所有匿名函数)。

解决方案:A Dependency Class

一种解决方案是我们可以把这种行为封装进一个类里面,一个实现了普通观察者模式的Dependency Class

所以,如果我们创建个JavaScript类来管理我们的依赖的话,代码可能长成下面这样:

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。以下是Dep类的用法:

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
复制代码

上面的代码和之前的代码功能上是一致的,但是代码看起来更具有复用性(Dep类可以复用)。唯一看起来有点奇怪的地方就是设置和执行target的地方。

问题

后面我们会为每个变量创建个Dep实例,同时如果可以将创建匿名函数的逻辑封装起来的话就更好了,或许我们可以用个watcher函数来做这件事情。

所以我们不用通过调用以下代码来收集依赖

target = () => { total = price * quantity }
dep.depend() 
target() 
复制代码

而是通过调用watcher函数来收集依赖(是不是赶脚代码清晰很多?):

watcher(() => {
  total = price * quantity
})
复制代码

解决方案:A Watcher Function

Watcher fucntion的定义如下:

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加到subscribers列表中,紧接着调用target函数,然后重置target变量。

现在如果我们运行以下代码:

price = 20
console.log(total)
dep.notify()      
console.log(total) 
复制代码
>> 10
>> 40
复制代码

你可能会想为什么要把target作为一个全局变量,而不是在需要的时候传入函数。别捉急,这么做自然有这么做的道理,看到本教程结尾就阔以一目了然啦。

问题

现在我们有了个简单的Dep class,但是我们真正想要的是每个变量都拥有自己的dep实例,在继续后面的教程之前让我们先把变量变成某个对象的属性:

let data = { price: 5, quantity: 2 }
复制代码

让我们先假设下data上的每个属性(pricequantity)都拥有自己的dep实例。

这样当我们运行:

watcher(() => {
  total = data.price * data.quantity
})
复制代码

因为data.price的值被访问了,我想要price的dep实例可以将上面的匿名函数收集到自己的subscribers列表里面。data.quantity也是如此。

如果这时候有个另外的匿名函数里面用到了data.price,我也想这个匿名函数被加到price自带的dep类里面。

问题来了,我们什么时候调用pricedep.notify()呢?当price被赋值的时候。在这篇文章的结尾我希望能够直接进入console做以下的事情:

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

要实现以上意图,我们需要能够在data的所有属性被访问或者被赋值的时候执行某些操作。当data下的属性被访问的时候我们就把target加入到subscribers列表里面,当data下的属性被重新赋值的时候我们就触发notify()执行所有存储在subscribes列表里面的匿名函数。

解决方案:Object.defineProperty()

我们需要学习下Object.defineProperty()函数是怎么用的。defineProperty函数允许我们为属性定义getter和setter函数,在我使用defineProperty函数之前先举个非常简单的例子:

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()
复制代码
>> I was accessed
>> I was changed
复制代码

正如你所看到的,上面的代码仅仅打印两个log。然而,上面的代码并不真的get或者set任何值,因为我们并没有实现,下面我们加上。

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()
复制代码
Getting price: 5
Setting price to: 20
复制代码

所以通过defineProperty函数我们可以在get和set值的时候收到通知(就是我们可以知道什么时候属性被访问了,什么时候属性被赋值了),我们可以用Object.keys来遍历data上所有的属性然后为它们添加getter和setter属性。

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
复制代码

现在data上的每个属性都有getter和setter了。

把这两种想法放在一起

total = data.price * data.quantity
复制代码

当上面的代码运行并且getprice的值的时候,我们想要price记住这个匿名函数(target)。这样当price变动的时候,可以触发执行这个匿名函数。

  • Get => Remember this anonymous function, we’ll run it again when our value changes.
  • Set => Run the saved anonymous function, our value just changed.

或者:

  • Price accessed (get) => call dep.depend() to save the current target
  • Price set => call dep.notify() on price, re-running all the 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
})
复制代码

在控制台看下:

就像我们想的一样!这时候 pricequantity都是响应式的!当 pricequantity更新的时候我们的total都会及时地更新。

看下Vue

下面的图现在应该看起来有点感觉了:

你看见紫色的带有getter和setter的Data圆圈没?是不是看起来很熟悉!每个component实例都拥有一个 watcher实例用于通过getters和setters来收集依赖。当某个setter后面被调用的时候,它会通知相应地watcher从而导致组件重新渲染。下面是我添加了一些标注的图:
Yeah!现在你对Vue的响应式有所了解了没? 很显然,Vue内部实现会比这个更加复杂,但是通过这篇文章你知道了一些基础知识。在下个教程里面我们会深入Vue内部,看看源码里面的实现是否和我们的类似。

我们学到了什么?

  • 如何创建一个可以同来收集依赖(depend)并执行所有依赖(notify)的Dep class
  • 如何创建watcher来管理我们当前正在执行的代码(target)
  • 如何使用Object.defineProperty()来创建getters和setters。

转载于:https://juejin.im/post/5c8b0490e51d457975771199

相关文章:

  • 程序是什么?如何理解编程的本质?
  • centos7.5+cobbler2.8.4实战图文攻略--2019持续更新
  • Node.js设计模式读书笔记(2)
  • 物流行业如何选择手持终端
  • CH2906 武士风度的牛(算竞进阶习题)
  • 2014年蓝桥杯部分题目与解答
  • 重拾 ObjC 自动释放池
  • 监听JS对象属性变化 Object.defineProperty Proxy 记录
  • 读ios开发有感——建立APP开发体系
  • 回归
  • Kubernetes — 重新认识Docker容器
  • 专业术语------扫盲
  • 实验1
  • nunjucks模版引擎入门
  • git flow常用命令
  • 【402天】跃迁之路——程序员高效学习方法论探索系列(实验阶段159-2018.03.14)...
  • 【RocksDB】TransactionDB源码分析
  • es6(二):字符串的扩展
  • IP路由与转发
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • jquery cookie
  • oschina
  • sessionStorage和localStorage
  • springMvc学习笔记(2)
  • Vue.js-Day01
  • vue和cordova项目整合打包,并实现vue调用android的相机的demo
  • 测试如何在敏捷团队中工作?
  • 将回调地狱按在地上摩擦的Promise
  • 面试题:给你个id,去拿到name,多叉树遍历
  • 前端面试总结(at, md)
  • 日剧·日综资源集合(建议收藏)
  • 如何使用 OAuth 2.0 将 LinkedIn 集成入 iOS 应用
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • Redis4.x新特性 -- 萌萌的MEMORY DOCTOR
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (Note)C++中的继承方式
  • (pojstep1.3.1)1017(构造法模拟)
  • (zhuan) 一些RL的文献(及笔记)
  • (二十三)Flask之高频面试点
  • (四)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (一)插入排序
  • (转) Face-Resources
  • (转)AS3正则:元子符,元序列,标志,数量表达符
  • (转)母版页和相对路径
  • .Net 8.0 新的变化
  • .NET开源全面方便的第三方登录组件集合 - MrHuo.OAuth
  • @property括号内属性讲解
  • [ 云计算 | AWS ] 对比分析:Amazon SNS 与 SQS 消息服务的异同与选择
  • [CSAWQual 2019]Web_Unagi ---不会编程的崽
  • [hdu2196]Computer树的直径
  • [IMX6DL] CPU频率调节模式以及降频方法
  • [Java性能剖析]Sun JDK基本性能剖析工具介绍
  • [Linux打怪升级之路]-信号的保存和递达