【vue】用图告诉你响应式原理

前言

若是本身去实现数据驱动的模式,如何解决一下几个问题:html

  • 经过什么手段去知道个人数据变了?
  • 经过什么东西去同步更新视图?

数据劫持——obvserver

咱们须要知道数据的获取和改变,数据劫持是最基础的手段。在Obeserver中,咱们能够看到代码以下:vue

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
    },
    set: function reactiveSetter (newVal) {
      // ...
    }
  })
复制代码

经过Object.defineProperty这个方法,咱们能够在数据发生改变或者获取的时候,插入一些自定义操做。同理,vue也是在这个方法中作依赖收集和派发更新的。react

绑定和更新视图——watcher

从初始化开始,咱们渲染视图的时候,便会生成一个watcher,他是监视视图中参数变化以及更新视图的。代码以下:设计模式

// 在mount的生命钩子中
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true /* isRenderWatcher */)
复制代码

固然,咱们能够保留疑问:数组

  • watcher是怎么去更新视图的
  • 数据又是怎么和watcher联动起来的

具体的绑定和更新的流程,咱们到后续的依赖收集中讲解。app

咱们先来说讲响应式系统中涉及到的设计模式。异步

发布订阅模式

在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另外一方面向订阅者发布事件;订阅者须要从事件通道订阅事件oop

以此避免发布者和订阅者之间产生依赖关系 post

vue的响应式流程

vue的响应式系统借鉴了数据劫持和发布订阅模式。性能

Vue用Dep做为一个中间者,解藕了Observer和Watcher之间的关系,使得二者的职能更加明确。

那具体是如何来完成依赖收集和订阅更新的呢?

依赖收集过程

  • 依赖收集的流程

举个例子

<div id="app">
    {{ message }}
    {{ message1 }}
    <input type="text" v-model="message">
    <div @click="changeMessage">改变message</div>        
</div>
复制代码
var app = new Vue({
    el: '#app',
    data: {
        message: '1',
        message1: '2',
    },
    methods: {
        changeMessage() {
            this.message = '2'
        }
    },
    watch: {
        message: function(val) {
            this.message1 = val
        }
    }
})
复制代码

依赖收集流程图:

如何看懂这个依赖收集流程?关键在watcher代码中:

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      // 省略
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
复制代码

调用的这个this.getter有两种,一种是key值的getter方法,还有一种是expOrFn,好比mounted中传入的updateComponent。

  • 如何防止重复收集

咱们不妨想一想什么才算是重复收集了?

笔者想到一种状况:就是dep数组中,出现了多个同样的watcher。

好比renderWatch就容易被重复收集,由于咱们在html模版中,会重复使用data中的某个变量。那他是如何去重的呢?

一、只有watch在执行get时,触发的取数操做,才会被收集

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        // ...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // ...
      dep.notify()
    }
  })
复制代码

当只有Dep.target这个存在的时候才进行依赖收集。Dep.target这个值只有在watcher执行get方法的时候才会存在。

二、在dep.depend的时候会判断watch的id

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}
复制代码
addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
 }
复制代码

咱们会发现,在depend过程当中,会有一个newDepIds去记录已经存入的dep的id,当一个watcher已经被该dep存过期,便再也不会进行依赖收集操做。

派发更新过程

收集流程讲完了,不妨在听听更新流程。

  • 订阅更新的流程 老例子
<div id="app">
    {{ message }}
    {{ message1 }}
    <input type="text" v-model="message">
    <div @click="changeMessage">改变message</div>        
</div>
复制代码
var app = new Vue({
    el: '#app',
    data: {
        message: '1',
        message1: '2',
    },
    methods: {
        changeMessage() {
            this.message = '3'
        }
    },
    watch: {
        message: function(val) {
            this.message1 = val
        }
    }
})
复制代码

依赖收集的最终结果:

当触发click事件的时候,便会触发订阅更新流程。

订阅更新流程图:

当renderWatch执行更新的时候,回去调用beforeUpdate生命钩子,而后执行patch方法,进行视图的变动。

  • 如何防止重复更新

如何去防止重复更新呢?renderWatch会被不少dep进行收集,若是视图屡次渲染,会形成性能问题。

其实问题的关在在于——queueWatcher

在queueWatcher中有两个操做:去重和异步更新。

function queueWatcher (watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    queue.push(watcher)
    // ...
    if (!waiting) {
      waiting = true
      // ...
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

其实queueWatcher很简单,将全部watch收集到一个数组当中,而后去重。

这样至少能够避免renderWatch频繁更新。

好比上述例子中的,message和message1都有一个renderWatch,可是只会执行一次。

异步更新也能够保证当一个事件结束以后,才会触发视图层的更新,也能防止renderWatch重复更新

结尾

文章讲述了响应式流程的缘由,代码细节并未深刻,若是喜欢了解源码的,能够翻看笔者其余的文章:

Observer源码解析

Watcher源码解析(未完成)

相关文章
相关标签/搜索