Vue 进阶系列之响应式原理及实现

栏目: JavaScript · 发布时间: 5年前

内容简介:Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。

Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。

需求

现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?

简单尝试1:

let a = 3;
let b = a * 10;
console.log(b); // 30
复制代码

乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。

a = 4;
console.log(a); // 4
console.log(b); // 30
b = a * 10;
console.log(b); // 40
复制代码

简单尝试2:

将a和b的关系定义在函数内,那么在改变a之后执行这个函数,b的值就会改变。伪代码如下。

onAChanged(() => {
    b = a * 10;
})
复制代码

所以现在的问题就变成了如何实现 onAChanged 函数,当a改变之后自动执行 onAChanged ,请看后续。

结合view层

现在把a、b和view页面相结合,此时a对应于数据,b对应于页面。业务场景很简单,改变数据a之后就改变页面b。

<span class="cell b"></span>

document
    .querySelector('.cell.b')
    .textContent = state.a * 10
复制代码

现在建立数据a和页面b的关系,用函数包裹之后建立以下关系。

<span class="cell b"></span>

onStateChanged(() => {
    document
        .querySelector(‘.cell.b’)
        .textContent = state.a * 10
})
复制代码

再次抽象之后如下所示。

<span class="cell b">
    {{ state.a * 10 }}
</span>

onStateChanged(() => {
    view = render(state)
})
复制代码

view = render(state) 是所有的页面渲染的高级抽象。这里暂不考虑 view = render(state) 的实现,因为需要涉及到DOM结构及其实现等一系列技术细节。这边需要的是 onStateChanged 的实现。

实现

实现方式是通过 Object.defineProperty 中的 gettersetter 方法。具体使用方法参考如下链接。

MDN之Object.defineProperty

需要注意的是 getset 函数是存取描述符, valuewritable 函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。

实例1:实现 convert() 函数

要求如下:

obj
Object.defineProperty

示例:

const obj = { foo: 123 }
convert(obj)


obj.foo // 输出 getting key "foo": 123
obj.foo = 234 // 输出 setting key "foo" to 234
obj.foo // 输出 getting key "foo": 234
复制代码

在了解 Object.definePropertygettersetter 的使用方法之后,通过修改 getset 函数就可以实现 onAChangedonStateChanged

实现:

function convert (obj) {

  // 迭代对象的所有属性
  // 并使用Object.defineProperty()转换成getter/setters
  Object.keys(obj).forEach(key => {
  
    // 保存原始值
    let internalValue = obj[key]
    
    Object.defineProperty(obj, key, {
      get () {
        console.log(`getting key "${key}": ${internalValue}`)
        return internalValue
      },
      set (newValue) {
        console.log(`setting key "${key}" to: ${newValue}`)
        internalValue = newValue
      }
    })
  })
}
复制代码

实例2:实现 Dep

要求如下:

  • 1、创建一个 Dep 类,包含两个方法: dependnotify
  • 2、创建一个 autorun 函数,传入一个 update 函数作为参数
  • 3、在 update 函数中调用 dep.depend() ,显式依赖于 Dep 实例
  • 4、调用 dep.notify() 触发 update 函数重新运行

示例:

const dep = new Dep()

autorun(() => {
  dep.depend()
  console.log('updated')
})
// 注册订阅者,输出 updated

dep.notify()
// 通知改变,输出 updated
复制代码

首先需要定义 autorun 函数,接收 update 函数作为参数。因为调用 autorun 时要在 Dep 中注册订阅者,同时调用 dep.notify() 时要重新执行 update 函数,所以 Dep 中必须持有 update 引用,这里使用变量 activeUpdate 表示包裹update的函数。

实现代码如下。

let activeUpdate = null 

function autorun (update) {
  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate    // 引用赋值给activeUpdate
    update()                        // 调用update,即调用内部的dep.depend
    activeUpdate = null             // 绑定成功之后清除引用
  }
  wrappedUpdate()                   // 调用
}
复制代码

wrappedUpdate 本质是一个闭包, update 函数内部可以获取到 activeUpdate 变量,同理 dep.depend() 内部也可以获取到 activeUpdate 变量,所以 Dep 的实现就很简单了。

实现代码如下。

class Dep {

  // 初始化
  constructor () {          
    this.subscribers = new Set()
  }

  // 订阅update函数列表
  depend () {
    if (activeUpdate) {     
      this.subscribers.add(activeUpdate)
    }
  }

  // 所有update函数重新运行
  notify () {              
    this.subscribers.forEach(sub => sub())
  }
}
复制代码

结合上面两部分就是完整实现。

实例3:实现响应式系统

要求如下:

  • 1、结合上述两个实例, convert() 重命名为观察者 observe()
  • 2、 observe() 转换对象的属性使之响应式,对于每个转换后的属性,它会被分配一个 Dep 实例,该实例跟踪订阅 update 函数列表,并在调用 setter 时触发它们重新运行
  • 3、 autorun() 接收 update 函数作为参数,并在 update 函数订阅的属性发生变化时重新运行。

示例:

const state = {
  count: 0
}

observe(state)

autorun(() => {
  console.log(state.count)
})
// 输出 count is: 0

state.count++
// 输出 count is: 1
复制代码

结合实例1和实例2之后就可以实现上述要求, observe 中修改 obj 属性的同时分配 Dep 的实例,并在 get 中注册订阅者,在 set 中通知改变。 autorun 函数保存不变。 实现如下:

class Dep {

  // 初始化
  constructor () {          
    this.subscribers = new Set()
  }

  // 订阅update函数列表
  depend () {
    if (activeUpdate) {     
      this.subscribers.add(activeUpdate)
    }
  }

  // 所有update函数重新运行
  notify () {              
    this.subscribers.forEach(sub => sub())
  }
}

function observe (obj) {

  // 迭代对象的所有属性
  // 并使用Object.defineProperty()转换成getter/setters
  Object.keys(obj).forEach(key => {
    let internalValue = obj[key]

    // 每个属性分配一个Dep实例
    const dep = new Dep()

    Object.defineProperty(obj, key, {
    
      // getter负责注册订阅者
      get () {
        dep.depend()
        return internalValue
      },

      // setter负责通知改变
      set (newVal) {
        const changed = internalValue !== newVal
        internalValue = newVal
        
        // 触发后重新计算
        if (changed) {
          dep.notify()
        }
      }
    })
  })
  return obj
}

let activeUpdate = null

function autorun (update) {

  // 包裹update函数到"wrappedUpdate"函数中,
  // "wrappedUpdate"函数执行时注册和注销自身
  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate
    update()
    activeUpdate = null
  }
  wrappedUpdate()
}
复制代码

结合Vue文档里的流程图就更加清晰了。

Vue 进阶系列之响应式原理及实现

Job Done!!!

本文内容参考自VUE作者尤大的付费视频


以上所述就是小编给大家介绍的《Vue 进阶系列之响应式原理及实现》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入理解 Flask

深入理解 Flask

[美]Jack Stouffer / 苏丹 / 电子工业出版社 / 2016-7-1 / 79.00

Flask 是一种具有平缓学习曲线和庞大社区支持的微框架,利用它可以构建大规模的web应用。学习上手Flask非常轻松,但要深入理解却并不容易。 本书从一个简单的Flask应用开始,通过解决若干实战中的问题,对一系列进阶的话题进行了探讨。书中使用MVC(模型-视图-控制器)架构对示例应用进行了转化重构,以演示如何正确地组织应用代码结构。有了可扩展性强的应用结构之后,接下来的章节使用Flask......一起来看看 《深入理解 Flask》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码