浅析Vue响应式

Vue 响应式是什么

Vue 是一个 MVVM 的框架,即 Model-View-ViewModel,Model 与 View 之间不直接联系,而是由 ViewModel(相当于 Pipe) 去监听 Model 的变化并触发 View 改变以及监听 View 中的事件操作响应的 Model

我们来看一段简单的双向绑定的例子:

<template>
  <div id="app">
    <input
      type="text"
      v-model="msg"
      placeholder="edit me"
    >
    <h1 @click="rotate()">{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      msg: 'Welcome'
    }
  },
  methods: {
    rotate () {
      this.msg = this.msg.split('').reverse().join('')
    }
  }
}
</script>

在这个例子中,<input>和<h1>均为 view,msg 为 model,而 Vue 则是我们的 ViewModel。
Vue 提供了两个工具:

  • DOMListener:监听页面的 DOM 事件,修改 Model 中的数据

  • DataBinding:监听 Js 中的数据变化,修改 View 视图

Vue 如何实现响应式

先来了解几个名词

  1. Observer 观察者(监听器),每个可监听对象都会挂载一个观察者实例,负责订阅数据变化,通知对应的 Dep 实例

  2. Dep 消息订阅器(依赖收集器),负责依赖收集,管理 watcher,依赖收集操作在获取数据时执行(defineReactive > get)

  3. Watcher 订阅者,负责响应,Vue中有三种

    • User Watcher 组件的 watch 中定义的 watcher

      于 initWatch > createWatcher 中初始化

    • Computed Watcher 组件的 computed 中定义的 watcher

      于 initComputed 中初始化

    • Render Watcher 渲染 Watcher,只要有数据变化,最终都会由 Render Watcher 触发页面更新

      于 mountComponent 方法中 beforeMount 与 mounted 钩子之间初始化

再来看一下响应的流程

来自官网的盗图

reactive.png

稍微加工了一下,看下图


my-reactive.png

实线部分为内部实现;虚线部分为响应的流程。

结合上图,简单来说就是在 DOM 上操作数据(如 input )时,会被 Observer 中定义的 setter 函数劫持并调用 notify 函数通知消息订阅器 Dep,Dep 遍历其 subs 数组对所有的订阅者 Watcher 调用 update 函数,update 函数通过一系列操作更新 DOM

这一系列操作包括:

  1. queueWatcher 将当前 Watcher 放入待更新的 Watcher 队列中
  2. flushSchedulerQueue 依次执行队列中 Watcher 的 run 函数
  3. run 函数中const value = this.get()调用 Watcher 的 get 函数
  4. get 函数中执行value = this.getter.call(vm, vm)调用 Watcher 中定义的 expression
    • Render Watcher 中的 expression 是 function () { vm._update(vm._render(), hydrating); }
    • User Watcher 中的 expression 是用户自定义的 watch 中的函数
    • Computed Watcher 中的 expression 是用户自定义的 computed 中回调函数
  5. 执行vm._update(vm._render(), hydrating),vm._render() 返回一个新的 VNode,vm._update 中执行 vm.__patch__(prevVnode, vnode) 将VNode 渲染成真实 DOM 反应在页面上

接下来分别看这三者的实现

  • Observer

    /**
     * Observer class that is attached to each observed
     * object. Once attached, the observer converts the target
     * object's property keys into getter/setters that
     * collect dependencies and dispatch updates.
     */
    export class Observer {
      ...
      constructor(value: any) {
        ...
        def(value, '__ob__', this)
        ...
        this.walk(value);
      }
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
      ...
    }
    

    撇开具体的逻辑代码不看,Observer 类的构造函数中就做了两件事

    1. 将 Observer 的实例挂载到数据对象上def(value, '__ob__', this)
    2. 循环执行 defineReactive 方法将数据对象的所有属性变成响应式的

DefineReactive

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  ...
  const getter = property && property.get
  const setter = property && property.set
  ...
  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) {
      ...
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      ...
      dep.notify()
    }
  })
}

defineReactive 做了一下几件事

  • 实例化一个消息订阅器 new Dep()
  • 利用 Object.defineProperty 给 data 对象上的每个属性添加 getter 和 setter 以"劫持"数据操作(get / set)
    • 在 get 中调用 dep 的 depend 方法将当前 Watcher 挂载到当前 dep 上
    • 在 set 时调用 dep 的 notify 方法通知所有订阅该 dep 的 Watcher

注意: 这里的 Dep.target 表示当前正在计算的 Watcher,其具有全局唯一性。

  • Dep

    /**
     * A dep is an observable that can have multiple
     * directives subscribing to it. 
     * directives 中的通过 Watcher 订阅数据
     */
    export default class Dep {
      static target: ?Watcher;
      id: number;
      subs: Array<Watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      removeSub (sub: Watcher) {
        remove(this.subs, sub)
      }
    
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
      ...
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update() // 调用 Watcher 的 update 方法
        }
      }
    }
    

    Dep 类作为消息订阅器,只负责依赖收集(通过 depend 与 addSub 收集所有与之相关的 Watcher )和管理订阅它的所有 Watcher。

  • Watcher

    /**
     * A watcher parses an expression, collects dependencies,
     * and fires callback when the expression value changes.
     * This is used for both the $watch() api and directives.
     */
    export default class Watcher {
       ...
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm // 当前Vue实例
        if (isRenderWatcher) {
          vm._watcher = this
        }
        vm._watchers.push(this) // 添加render watcher
        // options
      ...
        // parse expression for getter 下面用到的 getter 函数就是这么来的
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
          ...
          }
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * Evaluate the getter, and re-collect dependencies.
       * 调用 this.getter 函数(user、computed、render watcher 的 callback)
       * 然后重新收集依赖
       */
      get () {
        ...
        value = this.getter.call(vm, vm)
      ...
        this.cleanupDeps() 
        ...
        return value
      }
    
      /**
       * Add a dependency to this directive.
       * 把 dep 添加到 Watcher 实例的依赖数组中
       */
      addDep (dep: Dep) {
      ...
        dep.addSub(this)
      }
    
      /**
       * Clean up for dependency collection.
       * 重新整理依赖数组(deps)
       */
      cleanupDeps () {
        ...
        this.deps = this.newDeps
      ...
      }
    
      /**
       * Subscriber interface.
       * Will be called when a dependency changes.
       * 同步 sync 直接执行 run 
       * 异步 async 则先把当前 Watcher 推入队列,在 nextTick 中通过 flushSchedulerQueue 循环执行每个 watch    的 run 方法
       */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      /**
       * Scheduler job interface.
       * Will be called by the scheduler.
       */
      run () {
        if (this.active) {
         /** 
          * 对 watcher 求值,重新收集依赖
          * watcher 的执行顺序是 user > computed > render
          * get 函数内部做了两件事:
          * 触发 watcher callback;返回当前 value 值(只有 user watcher 有返回值)
          */
          const value = this.get() 
          
          if (
            value !== this.value ||
            isObject(value) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            ...
            this.cb.call(this.vm, value, oldValue)
          ...
          }
        }
      }
    
      ...
    
      /**
       * Depend on all deps collected by this watcher.
       * 循环收集依赖
       */
      depend () {
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
    
    /**
       * Remove self from all dependencies' subscriber list.
     * 把当前 watcher 从其依赖的所有 dep 的 subs 数组中删除
       */
      teardown () {
      ...
      }
    }
    

    Watcher 中的方法不多,主要就以下几个

    • addDep、depend、cleanupDeps、teardown 主要用于管理 watcher 与 dep 的依赖关系
    • get、update、run 用于响应数据更新的操作(如更新视图等)

一些小知识

  • 由于 Vue 响应式的核心 defineReactive 是使用 ES5 的Object.defineProperty 实现的,所以不支持 IE8 以下的浏览器

  • __patch__过程关于 DOM 操作的部分都定义在 platforms > runtime > node-ops.js 中

  • Vue 不能检测到对象属性的添加和删除,需要通过 Vue.$Set(target,key,value) 去实现,set 函数中会重新执行 defineReactive 将对象变为响应式,并且调用 dep.notify 以达到更新视图的效果

    /**
     * Set a property on an object. Adds the new property and
     * triggers change notification if the property doesn't
     * already exist.
     */
    export function set (target: Array<any> | Object, key: any, val: any): any {
      ... 
      // 如果属性已经存在就直接返回
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      // 将新添加的属性变成响应式
      // 触发 notify
      const ob = (target: any).__ob__
      ...
      defineReactive(ob.value, key, val)
      ob.dep.notify()
      return val
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351