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

图片

题图:本身拍摄于乌镇闭包

什么是响应式Reactivity

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

需求

如今有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么作?ide

简单尝试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); // 4console.log(b); // 30b = a * 10;console.log(b); // 40

简单尝试2:

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

onAChanged(() => {
    b = a * 10;
})

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

结合view层

如今把a、b和view页面相结合,此时a对应于数据,b对应于页面。业务场景很简单,改变数据a以后就改变页面b。日志

<span class="cell b"></span>document
    .querySelector('.cell.b')
    .textContent = state.a * 10

如今创建数据a和页面b的关系,用函数包裹以后创建如下关系。code

<span class="cell b"></span>onStateChanged(() => {    document
        .querySelector(‘.cell.b’)
        .textContent = state.a * 10})

再次抽象以后以下所示。orm

<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转换对象的全部属性

  • 三、转换后的对象保留原始行为,但在get或者set操做中输出日志

示例:

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


obj.foo // 输出 getting key "foo": 123obj.foo = 234 // 输出 setting key "foo" to 234obj.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

要求以下:

  • 一、建立一个Dep类,包含两个方法:dependnotify

  • 二、建立一个autorun函数,传入一个update函数做为参数

  • 三、在update函数中调用dep.depend(),显式依赖于Dep实例

  • 四、调用dep.notify()触发update函数从新运行

示例:

const dep = new Dep()

autorun(() => {
  dep.depend()  console.log('updated')
})// 注册订阅者,输出 updateddep.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:实现响应式系统

要求以下:

  • 一、结合上述两个实例,convert()重命名为观察者observe()

  • 二、observe()转换对象的属性使之响应式,对于每一个转换后的属性,它会被分配一个Dep实例,该实例跟踪订阅update函数列表,并在调用setter时触发它们从新运行

  • 三、autorun()接收update函数做为参数,并在update函数订阅的属性发生变化时从新运行。

示例:

const state = {  count: 0}

observe(state)

autorun(() => {  console.log(state.count)
})// 输出 count is: 0state.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 = nullfunction autorun (update) {  // 包裹update函数到"wrappedUpdate"函数中,
  // "wrappedUpdate"函数执行时注册和注销自身
  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate
    update()
    activeUpdate = null
  }
  wrappedUpdate()
}

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

Job Done!!!


 图片

相关文章
相关标签/搜索