VUE 响应式系统及 Watcher 的调度实现

1.响应式系统的实现

Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」。

vue 2.0中,是基于 Object.defineProperty实现的「响应式系统」。vue3 中是基于 Proxy/Reflect 来实现的,vue3的详细解析有时间再写了,本文讲的是vue2 的实现。

主要涉及属性:

enumerable,属性是否可枚举,默认 false。

configurable,属性是否可以被修改或者删除,默认 false。

get,获取属性的方法。

set,设置属性的方法。

响应式基本原理就是,在 Vue 的构造函数中,对 options 的 data 进行处理。即在初始化vue实例的时候,对data、props等对象的每一个属性都通过Object.defineProperty定义一次,在数据被set的时候,做一些操作,改变相应的视图。

class Vue {

        /* Vue构造类 */

        constructor(options) {

              this._data = options.data;

              observer(this._data);

        }

}

function observer (value) {

       if (!value || (typeof value !== 'object')) {

             return;

      }

     Object.keys(value).forEach((key) => {

             defineReactive(value, key, value[key]);

     });

}

function defineReactive (obj, key, val) {

       Object.defineProperty(obj, key, {

              enumerable: true, /* 属性可枚举 */

              configurable: true, /* 属性可被修改或删除 */

              get: function reactiveGetter () { return val; },

              set: function reactiveSetter (newVal) {

                      if (newVal === val) return;

                      cb(newVal);

               }

       });

}

实际应用中,各种系统复杂无比。假设我们现在有一个全局的对象,我们可能会在多个 Vue 对象中用到它进行展示。又或者写在data中的数据并没有应用到视图中呢,这个时候去更新视图就是多余的了。这就需要依赖收集的过程。

2.依赖收集

所谓依赖收集,就是把一个数据用到的地方收集起来,在这个数据发生改变的时候,统一去通知各个地方做对应的操作。“订阅者”在VUE中基本模式如下:

exportdefaultclass 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 () {

    // stabilize the subscriber list first

    const subs = this.subs.slice()

    for (let i = 0, l = subs.length; i < l; i++) {

      subs[i].update()

    }

  }

}

有了订阅者,再来看看Watcher的实现。源码Watcher比较多逻辑,简化后的模型如下

class Watcher{

    constructor(vm,expOrFn,cb,options){

        //传进来的对象 例如Vue

        this.vm = vm

        //在Vue中cb是更新视图的核心,调用diff并更新视图的过程

        this.cb = cb

        //收集Deps,用于移除监听

        this.newDeps = []

        this.getter = expOrFn

        //设置Dep.target的值,依赖收集时的watcher对象

        this.value =this.get()

    }

    get(){

        //设置Dep.target值,用以依赖收集

        pushTarget(this)

        const vm = this.vm

        let value = this.getter.call(vm, vm)

        return value

    }

    //添加依赖

      addDep (dep) {

          // 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖

        this.newDeps.push(dep)

        dep.addSub(this)

      }

      //更新

      update () {

        this.run()

    }

    //更新视图

    run(){

        //这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图

        console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)

    }

}

3.defineReactive详细逻辑

exportfunction defineReactive (

  obj: Object,

  key: string,

  val: any,

  customSetter?: ?Function,

  shallow?: boolean

) {

  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)

  if (property && property.configurable === false) {

    return

  }

  // cater for pre-defined getter/setters

  const getter = property && property.get

  const setter = property && property.set

  if ((!getter || setter) && arguments.length === 2) {

    val = obj[key]

  }

  let childOb = !shallow && observe(val)

  Object.defineProperty(obj, key, {

    enumerable:true,

    configurable:true,

    get:function reactiveGetter () {

      const value = getter ? getter.call(obj) : val

      if (Dep.target) {

        dep.depend()

        if (childOb) {

          childOb.dep.depend()

          if (Array.isArray(value)) {

            dependArray(value)

          }

        }

      }

      return value

    },

    set:function reactiveSetter (newVal) {

      const value = getter ? getter.call(obj) : val

      /* eslint-disable no-self-compare */

      if (newVal === value || (newVal !== newVal && value !== value)) {

        return

      }

      /* eslint-enable no-self-compare */

      if (process.env.NODE_ENV !== 'production' && customSetter) {

        customSetter()

      }

      if (setter) {

        setter.call(obj, newVal)

      }else {

        val = newVal

      }

      childOb = !shallow && observe(newVal)

      dep.notify()

    }

  })

}

所以响应式原理就是,我们通过递归遍历,把vue实例中data里面定义的数据,用defineReactive(Object.defineProperty)重新定义。每个数据内新建一个Dep实例,闭包中包含了这个 Dep 类的实例,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

4.Watcher的产生

在vue中,共有4种情况会产生Watcher:

Vue实例对象上的watcher,观测根数据,发生变化时重新渲染组件 updateComponent = () => {  vm._update(vm._render(), hydrating)} vm._watcher = new Watcher(vm, updateComponent, noop)

用户在vue对象内用watch属性创建的watcher

用户在vue对象内创建的计算属性,本质上也是watcher

用户使用vm.$watch创建的watcher

Wathcer会增减,也可能在render的时候新增。所以,必须有一个Schedule来进行Watcher的调度。部分主要代码如下:

 queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed

  // as we run existing watchers

  for (index = 0; index < queue.length; index++) {

    watcher = queue[index]

    if (watcher.before) {

      watcher.before()

    }

    id = watcher.id

    has[id] =null

    watcher.run()

    // in dev build, check and stop circular updates.

    if (process.env.NODE_ENV !== 'production' && has[id] != null) {

      circular[id] = (circular[id] || 0) + 1

      if (circular[id] > MAX_UPDATE_COUNT) {

        warn(

          'You may have an infinite update loop ' + (

            watcher.user

              ?`in watcher with expression "${watcher.expression}"`

              :`in a component render function.`

          ),

          watcher.vm

        )

        break

      }

    }

  }

Schedule 调度的作用:

去重,每个Watcher有一个唯一的id。首先,如果id已经在队列里了,跳过,没必要重复执行,如果id不在队列里,要看队列是否正在执行中。如果不在执行中,则在下一个时间片执行队列,因此队列永远是异步执行的。

排序,按解析渲染的先后顺序执行,即Watcher小的先执行。Watcher里面的id是自增的,先创建的id比后创建的id小。所以会有如下规律:

2.1、组件是允许嵌套的,而且解析必然是先解析了父组件再到子组件。所以父组件的id比子组件小。

2.2、用户创建的Watcher会比render时候创建的先解析。所以用户创建的Watcher的id比render时候创建的小。

删除Watcher,如果一个组件的Watcher在队列中,而他的父组件被删除了,这个时候也要删掉这个Watcher。

队列执行过程中,存一个对象circular,里面有每个watcher的执行次数,如果哪个watcher执行超过MAX_UPDATE_COUNT定义的次数就认为是死循环,不再执行,默认是100次。

总之,调用的作用就是管理 Watcher。

补充:

VUE2中是如何用Object.defineProperty给数组对象重新定义的呢,为什么我们直接修改数据中某项(arr[3] = 4)的时候,视图并没有响应式地变化呢。

答案是数组的响应式是不够完全的,VUE只重写了有限的方法。重写逻辑如下:

const arrayProto = Array.prototype

exportconst arrayMethods = Object.create(arrayProto)

const methodsToPatch = [

  'push',

  'pop',

  'shift',

  'unshift',

  'splice',

  'sort',

  'reverse'

]

/**

 * Intercept mutating methods and emit events

 */

methodsToPatch.forEach(function (method) {

  // cache original method

  const original = arrayProto[method]

  def(arrayMethods, method,function mutator (...args) {

    const result = original.apply(this, args)

    const ob = this.__ob__

    let inserted

    switch (method) {

      case'push':

      case'unshift':

        inserted = args

        break

      case'splice':

        inserted = args.slice(2)

        break

    }

    if (inserted) ob.observeArray(inserted)

    // notify change

    ob.dep.notify()

    return result

  })

})

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 响应式原理 Vue实现响应式的核心是利用了Object.defindProperty为对象的属性添加getter和...
    C脖子阅读 929评论 0 0
  • 本文是lhyt本人原创,希望用通俗易懂的方法来理解一些细节和难点。转载时请注明出处。文章最早出现于本人github...
    lhyt阅读 2,193评论 0 4
  • 从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页...
    grain先森阅读 894评论 2 2
  • 秋日 作者:程颢 闲来无事不从容,睡觉东窗日已红。 万物静观皆自得,四时佳兴与人同。 道通天地有形外,思入风云变态...
    江南莫之阅读 496评论 8 17
  • 开心时刻,又到了写文的时间了。这次开始小说系列的第一篇,好激动,不知道自己写的小说,会写到什么程度^^ 正题...
    七年行阅读 174评论 0 0