关于vue.js:译自官方课程双向数据绑定的最佳解释

47次阅读

共计 11423 个字符,预计需要花费 29 分钟才能阅读完成。


highlight: vs2015

译自这篇文章:Build a Reactivity System,在我的了解上加工了那么一丢丢🤏

还有这篇文章(其实都是一篇文章,然而这个多了几张图)

建设响应零碎

在本课中,咱们将应用与 Vue 源代码中雷同的技术构建一个简略的响应零碎。这将使您更好地了解 Vue.js 及其设计模式,并让您相熟 观察者 watcher 和 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 怎么晓得要更新什么,它又是怎么跟踪所有内容的?

JavaScript 编程通常不是这样工作的

如果您不分明,那么咱们必须解决的一个大问题是,编程 (programming) 通常不会以这种响应式的形式工作:\
例如,如果我运行以下代码:

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)的,不是响应性的(原词:reactive),所以在现实生活中这不起作用。为了使 total 变得更具响应性,咱们必须用 JavaScript 做些操作,使事件体现得不同。

❗️问题

首先咱们须要 保留下来 计算 总数 total办法(函数),以便在 价格 price数量 quantity 发生变化时从新运行。

✅ 解决方案

首先,咱们须要一些办法来通知咱们的应用程序,“我行将运行的代码,存储一下这个代码,我可能须要你在另一个工夫运行它。”\
而后咱们要运行代码,如果 价格 price 数量 quantity变量失去更新,请再次运行存储的代码。

译者:咱们可能遇到两种场景,一种是创立函数之后立刻运行,还有一种就是定义之后过一阵子再运行这个函数。下图就是这两种场景的演示

不论是立刻运行还是要一会儿运行,代码要保留在一个容器外面,而后从容器外面拿出函数运行。

storage 中译是“存储”

下图的 storage 就是保留代码的“容器”

咱们能够通过记录这个函数 (原词:by recording the function) 来做到这一点,这样咱们就能够再次运行它。

译者:他说的记录这个函数,就是我说的,要先存储代码,而后拿进去运行,存储这个动作就是记录

依照咱们下面总结的逻辑就是:

  1. 先定义变量、初始化函数(定义函数)
  2. 而后是要记录函数,
  3. 运行这个函数(必定是记录函数的同时执行函数)

补充一点 :函数定义的时候,天然会被保留在内存,然而咱们所说的【容器】并不是内存,\
因为三四个函数(能够是更多)保留在内存,咱们会一个一个调用,\
咱们所想的【容器】是,把这个三四个函数保留在【数组】里,能够通过循环数组实现数组里所有函数的运行,而不必一个一个调用

/* 1. 定义变量、函数 */
let price = 5      // 价格
let quantity = 2   // 数量
let total = 0      // 总价:价格 * 数量
let target = null  // 存储了【总价 = 价格 * 数量】计算式 的函数

target = function () {total = price * quantity}

/* 2. 记录函数 */
record() // 记录这个函数,如果咱们想稍后运行它
         // 译者:record 函数 的外部逻辑就是 将 target 函数 保留到【容器】里
         
/* 3. 运行函数 */
target() // 同时也运行它

请留神,咱们在 target 变量中存储匿名函数,而后调用 record 函数。应用 ES6 箭头语法,我也能够写成:

译者:下面这句话意思:就是应用函数表达式的形式申明一个 target 函数

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

record函数的定义也很简略:

let storage = [] // 咱们将把 target 函数保留在这个数组里

function record () { // target = () => {total = price * quantity}
  storage.push(target)
}

译者:record() 外面的 storage 数组就是我下面说的【容器】,这个外面将寄存很多函数,大略相似这个模式:

storage = [fun1(),fun2(),fun3()...]
storage[0] === fun1() // true

咱们正在存储 target 函数,(target = ()=>{ total = price * quantity}),以便于咱们能够稍后运行它。

咱们也能够应用一个 replay 函数来运行咱们记录的所有内容。

译者:这就是用 replay() 函数遍历数组,运行保留在容器外面的函数们

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 = []     // 存储 target 的数组

    // 定义 record 函数:将 target 存储在 storage 数组中,以便在适合的机会拿进去运行
function record () {storage.push(target)
}

    // 定义 replay 函数:运行存储在 storage 中的每一个函数
function replay () {storage.forEach(run => run())
}

    // 定义 target 函数:寄存【总价格 = 价格 * 数量】的函数
target = () => { total = price * quantity}

record()  // 向 storage 数组中寄存一个 target 函数
target()  // 运行一次 target 函数 -- 计算 总价格 total

price = 20  // 批改一次 价格 price 的值

console.log(total) // => 10 
    // 输入一下总价格 total,因为价格尽管批改了,// 然而在下面运行 target 函数的时候,total 曾经被赋值,// 下面那一行仅仅批改了价格 price,然而没有从新运行 target 函数,所以 total 的值没有扭转。replay() 
    // 运行存储在 storage 数组中的所有函数,// 此时数组中仅有一个元素,元素的值是 target 函数,// 换句话说,storage 中仅仅保留着一个 target 函数,// 所以这里就相当于运行一次 target()
    
console.log(total) // => 40

译者:如果下面看不太懂,我能够简化一下:\
因为数组中仅仅存储了一个 target(仅仅须要记录一个函数一次),所以下面那个情景,能够等价替换为:

 let price = 5
let quantity = 2
let total = 0
let target = null
// 去掉了 storage 数组
// 去掉了 record 和 replay 函数

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

target()

price = 20
console.log(total) // => 10
target() // 将 replay() 替换为 target
console.log(total) // => 40

译者:到这里了解 recordreplay 函数的作用了吗?\
record:将想要一会儿调用的函数存储在 storage 数组中 \
replay:遍历 storage,将 存储在 storage 数组中的函数,挨个运行

❗️问题

咱们能够依据须要,持续记录 (recording) 指标函数们,但最好有一个更弱小的解决方案来扩大咱们的应用程序。兴许是一个类,负责保护指标列表(译者:这里说的应该就是那个寄存函数的【容器】),当咱们须要它们从新运行时会收到告诉。

✅ 解决方案:依赖类(A Dependency Class)

咱们能够开始解决此问题的一种办法是 把这种行为封装到它本人的类中 ,这是一个实现规范编程观察者模式的 依赖类 。\
因而,如果咱们创立一个 JavaScript 类来治理咱们的依赖项(这更靠近 Vue 解决事物的形式),那么它可能看起来像这样:

译者:下面的意思就是,咱们能够把 把函数记录到数组 运行数组里的函数 这种行为封装到一个类外面,这个类就是观察者模式外面的 依赖类(Dep)

另外我也不晓得 Vue 处理事务的形式是什么,可能是指观察者模式?

补充说一些货色:

  • 订阅器 :在这篇文章中有一张图,监听器 Observer 和订阅者 Watcher两头还有一个订阅器 Dep,这个订阅器 Dep就是下面实现的这个类。
  • 依赖:指保留在 subscribers 数组外面的函数们。

    1. Vue 文档里写到:组件渲染的时候,会将“接触”过的数据 property 记录为依赖。
    2. 一个属性,以 price 为例,所有用到 price 的中央都被记录为依赖,能够是函数target,也能够是组件中的<div> {{price}} </div>
    3. 一组依赖,是一组所有用到 price 属性的中央。所以说,一个属性会 new 一个订阅器 Dep,订阅器外面保留有price 属性的一组依赖
  • 订阅:依赖如果在 subscribers 数组外面,就是被订阅了,如果没在,就是没有被订阅。

这篇文章写到:\
“公布订阅者”模式,数据变动为“发布者”,依赖对象为“订阅者”。\
音讯订阅器 Dep,用来包容所有的“订阅者”。订阅器 Dep 次要负责收集订阅者,而后当数据变动的时候后执行对应订阅者的更新函数。

OK,咱们来看,这是 target 函数

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

为什么 target 函数会成为订阅者?\
首先看 target 是做什么的:是计算 total 的值的,而 total 的值依靠于 price 和 quantity 计算而来,所以 target 函数就要订阅:“如果 price 或者 quantity 发生变化,你就要告诉我!我来更新 total 的值”

class Dep { // 订阅器,保留依赖(依赖的汇合)constructor () {this.subscribers = [] // 依赖的指标(原句:The targets that are dependent),// 应该在调用 notify() 的时候运行(译者:指的是这个外面保留的函数们,会在 notify()外面被遍历运行)}
  depend() {  // 这替换了 record() 函数
    if (target && !this.subscribers.includes(target)) {
      // 只有在有 target 并且尚未订阅的状况下
      this.subscribers.push(target)
    } 
  }
  notify() {  // 这替换了 replay() 函数
    this.subscribers.forEach(sub => sub()) // 运行咱们的 targets() 或观察者 observers()。}
}

请留神,咱们当初将匿名函数存储在 subscribers 中,而不是 storage。\
咱们当初调用 depend() 而不是咱们的 record() 函数,\
咱们当初应用 notify() 而不是replay()

【中途插播】译者有话说:

  • depend()「中译:依赖」用于增加依赖,notify()「中译:告诉」用于告诉订阅者 Watcher(??)以便后续告诉依赖更新(执行【subscribers 中的函数等】)

要让它运行:

const dep = new Dep()
    
let price = 5
let quantity = 2
let total = 0
let target = () => { total = price * quantity}
dep.depend() // 将 target() 存储在 subscribers 数组中 
target()  // 执行 target() 函数 失去 total

console.log(total) // => 10 .. 正确的数字(后果)price = 20
console.log(total) // => 10 .. 不是正确的数字了
dep.notify()       // 运行 subscribers 数组中寄存的函数
console.log(total) // => 40  .. 当初又是正确的后果了

它依然能运行,当初咱们的代码感觉更可重用。惟一依然感觉有点奇怪的是 target() 的设置和运行。

❗️问题

未来咱们将为每个变量设置一个 Dep 类,封装 创立须要监督更新的匿名函数 的行为会很好。兴许 watcher() 函数可能是为了关照这种行为。

译者有话说:

对于翻译:and it’ll be nice to encapsulate the behavior of creating anonymous functions that need to be watched for updates.

谷歌百度是下面:封装创立须要监督更新的匿名函数的行为会很好 \
感觉有些拗口,大略意思我了解的是:封装一个匿名函数的行为很好,这个匿名函数须要被监督,监督是为了更新(数据)。

所以不要这样调用:

// target 的三步操作
let target = () => { total = price * quantity} // 定义 target() 函数,返回 total 值
dep.depend() // 将 target() 存储在 subscribers 数组中 
target()  // 执行 target() 函数 失去 total

(这只是下面的代码)

咱们能够改为调用:

watcher(() => {total = price * quantity // 译者:在 watcher 中失去 total 值} )

// 等价于
// let target = () => { total = price * quantity}
// watcher(target)

译者有话说:就是将之前的这三行再次封装、更加精简

让 target 函数作为 watcher 的参数。在 watcher 外面实现对 target 的三步操作。

✅ 解决方案:观察者函数(A Watcher Function)

在咱们的 Watcher 函数中,咱们能够做一些简略的事件:

function watcher(myFunc) {
  target = myFunc    // 设置为流动指标(Set as the active target)// 译者:因为在【dep.depend()】中,是将 target 函数 push 进依赖数组中的
                     
  dep.depend()       // 将 target 增加为依赖项(增加进入依赖数组 ---subscribers)target()           // 运行 target 指标函数 
  target = null      // 重置 target 指标函数 
                     // 译者:我也不分明为什么要重置,然而临时先往下看吧
}

如您所见,观察者函数(watcher())承受一个 myFunc 参数,将其设置为咱们的全局 target 属性,调用 dep.depend() 将咱们的 target 增加为订阅者(增加进入依赖数组 subscribers),调用target 函数并重置target

当初,当咱们运行以下命令时:

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

您可能想晓得为什么咱们 将 target 实现为全局变量 ,而不是在须要时将其传递给咱们的函数。\
这是有充沛理由的,这将在咱们文章的结尾变得不言而喻。

❗️问题

咱们只有一个 Dep class,但咱们真正想要的是每个变量都有本人的订阅器Dep。\
让我在进一步钻研之前将变量化为属性(原句:Let me move things into properties before we go any further.)

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

让咱们假如咱们的每个属性(price 和 quantity)都有本人的外部 Dep 类。

当初让咱们运行:

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

因为拜访了 data.price 值,我心愿 price 属性的 Dep 类将咱们的匿名函数(存储在 target 中)。通过调用 dep.depend()推送到它的订阅者数组(subscriber array)

因为拜访了 data.quantity,我还心愿 quantity 属性 Dep 类将此匿名函数(存储在 target 中)推送到其 订阅者数组 subscriber array 中。

译者有话说:每一个属性都会有一个 订阅者 watcher,来监督它的状态变动,同一个工夫只会有一个订阅者,避免的就是两个人同时批改一个数据的抵触。

咱们之前说到,每一个属性都会 new 一个订阅器 Dep,订阅器外面保留着所有用到这个属性的 函数 、或者 html 中{{这个属性}} 的中央

上面这段意思就是,心愿在每一个属性被扭转的时候调用一次 dep.notify(),那咱们有没有什么办法能监听数据的扭转呢?

如果我有另一个只拜访 data.price 的匿名函数,我心愿将其推送到 price 属性 Dep 类。

我心愿什么时候对 price 的订阅者调用 dep.notify()?我心愿在设定 price 时调用它们。在文章完结时,我心愿可能进入控制台并执行以下操作:

>> total 
10 
>> price = 20 // When this gets run it will need to call notify() on the price 
                // 当它运行时,它须要在 price(的 Dep 类上,即通过订阅器) 上调用 notify()
>> total 
40

咱们须要一些办法来绑定(hook)数据属性(如 pricequantity),\
以便当它 被拜访时 ,咱们能够将target 保留到咱们的订阅者数组(subscriber array)中,\
当它 扭转时,运行存储在订阅者数组中的函数。

✅ 解决方案: Object.defineProperty()

咱们须要理解 Object.defineProperty() 函数,它是纯 ES5 JavaScript。它容许咱们为属性定义 getter 和 setter 函数。在我向您展现咱们将如何在咱们的 Dep 类中应用它之前,让我向您展现十分根本的用法。

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

Object.defineProperty(data, 'price', {  // 只有 price

    get() {  // 创立一个 get 办法
      console.log(`I was accessed`)
    },

    set(newVal) {  // 创立一个 set 办法
      console.log(`I was changed`)
    }
})
data.price // 这里调用 get()
data.price = 20  // 这里调用 set()

如您所见,它只打印了(logs)两行。然而,它实际上并没有获取或设置任何值,因为咱们适度应用(over-rode)了该性能。咱们当初把它加上 (译者:指的是把获取值、设置值的操作加上)get() 冀望返回一个值,而 set() 依然须要更新一个值,所以让咱们增加一个 internalValue 变量来存储咱们以后的price 值。

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

let internalValue = data.price // 咱们的初始值。Object.defineProperty(data, 'price', {  // 只操纵价格属性

    get() {  // 创立一个 get 办法
      console.log(`Getting price: ${internalValue}`)
      return internalValue
    },

    set(newVal) {  // 创立一个 set 办法
      console.log(`Setting price to: ${newVal}` )
      internalValue = newVal
    }
})
total = data.price * data.quantity  // 这里调用 get()
data.price = 20  // 这里调用 set()

当初咱们的 get 和 set 工作失常,你认为控制台会打印什么?

所以咱们有方法在获取和设置值时失去告诉。通过一些递归,咱们能够对数据数组中的所有我的项目运行它,对吧?

仅供参考:Object.keys(data) 返回对象键的数组。

    let data = {price: 5, quantity: 2}
    
    Object.keys(data).forEach(key => { // 咱们当初正在为数据中的每个属性(item)运行它
      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

当初所有都有(译者:对象中每一个属性都绑定了) getter 和 setter,咱们在管制台上看到了这一点。

把下面的想法放在一起

total = data.price * data.quantity

当这样一段代码运行并 获取(gets) price 的值时,咱们心愿 price 记住这个匿名函数(target)。这样,如果 price 发生变化或 (设置 sets) 为新值,它将触发此函数从新运行,因为它晓得这行代码(this line)依赖于它。所以你能够这样想。

Get => 记住这个匿名函数,当咱们的值扭转时咱们会再次运行它。

Set => 运行保留的匿名函数,咱们的值刚刚扭转。

或者在咱们的 Dep Class 的状况下:

Price accessed 获取价格 (get) => 调用 dep.depend() 保留以后指标

Price set 设置价格 => 在价格上调用 dep.notify(),从新运行所有指标

让咱们联合这两个想法,并运行咱们的最终代码。

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

// 这是完全相同的 Dep 类
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.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() // <-- 记住咱们正在运行的指标
      return internalValue
    },
    set(newVal) {
      internalValue = newVal
      dep.notify() // <-- 从新运行存储的函数}
  })
})

// 我的观察者不再调用 dep.depend,
// 因为它是从咱们的 get 办法外部调用的。function watcher(myFunc) {
  target = myFunc
  target()
  target = null
}

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

当初试试看咱们的控制台会产生什么。

正是咱们所心愿的!价格和数量的确是响应性的!每当价格或数量的值更新时,咱们的代码就会从新运行。

译者有话说:

订阅器 Dep是在“前方大本营”实现的,(相对而言)watcher是裸露在“明面儿”上的,dep 收集依赖,watcher 调用依赖,同一个工夫只有一个 watcher,也是为了避免同时操作一个数据引起的抵触。

注:都是我认为,心愿大家也有本人的思考!

跳转到 Vue

Vue 文档中的这个插图当初应该开始有意义了。

你看到带有 getter 和 setter 的丑陋的紫色 Data 圆圈了吗?它应该看起来很相熟!每个组件实例都有一个 watcher 实例(蓝色),它从 getter(红线)收集依赖项。当稍后调用 setter 时,它会 notify 告诉 导致组件从新渲染的观察者。这是带有我本人正文的图像。

译者注:这张图在这个页面拿的:JavaScript 响应性的最佳解释

译者有话说:

按我的了解捋一遍:

首先 Vue 在刚开始渲染的时候,就会用数据劫持(Object.defineProperty())来从新定义一下属性的 getter、setter 办法(数据被获取的时候触发的函数、数据被扭转的时候触发的函数)

每一个属性都会有一个订阅器 Dep,订阅器外面保留着所有用到这个属性的中央(就是属性的依赖)。

渲染之初,会获取组件哪 data 外面的每个属性,触发属性的 getter 函数,从而触发订阅器的【dep.depend()】函数,将属性的依赖保留进订阅器外面。

当属性被扭转的时候就会触发 setter 函数,在 setter 函数外面,告诉【dep.notify()】所有依赖,更新内容。

渲染结束之后,页面稳固,如果数据被扭转,则触发 watcher 函数,用上面这段代码举例,调用 () => { data.total = data.price * data.quantity},而后total 触发setter,告诉 watcher,从新渲染数据,实现绑定。

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

(对于 watcher 我忽然感觉有点云里雾里,为什么是说 setter 告诉 watcher,然而看代码反而是告诉订阅器 Dep)

是的,这当初不是更有意义了吗?

显然 Vue 如何在幕后做到这一点更简单,但你当初晓得了基础知识。在下一课中,咱们将深刻理解 Vue,看看咱们是否能够在源代码中找到这种模式。

所以咱们学了什么?

  • 如何创立一个收集依赖项(depend())并从新运行所有依赖项(notify())的 Dep 类
  • 如何创立一个 观察者 来治理咱们正在运行的代码,这可能须要增加(target())作为依赖项。
  • 如何应用 Object.defineProperty() 创立 getter 和 setter。

正文完
 0