此文已由作者吴维伟授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

Vue实例在初始化时,可以接受以下几类数据:

  • 模板

  • 初始化数据

  • 传递给组件的属性值

  • computed

  • watch

  • methods

Vue 根据实例化时接受的数据,在将数据和模板转化成DOM节点的同时,分析其依赖的数据。在特定数据改变时,自动在下一个周期重新渲染DOM节点

本文主要分析Vue是如何进行依赖收集的。

Vue中,与依赖收集相关的类有:

Dep : 一个订阅者的列表类,可以增加或删除订阅者,可以向订阅者发送消息

Watcher : 订阅者类。它在初始化时可以接受getter, callback两个函数作为参数。getter用来计算Watcher对象的值。当Watcher被触发时,会重新通过getter计算当前Watcher的值,如果值改变,则会执行callback.

对初始化数据的处理

对于一个Vue组件,需要一个初始化数据的生成函数。如下:

export default {
    data () {        
    return {           
     text: 'some texts',       
          arr: [],           
           obj: {}
        }
    }
}

Vue为数据中的每一个key维护一个订阅者列表。对于生成的数据,通过Object.defineProperty对其中的每一个key进行处理,主要是为每一个key设置get, set方法,以此来为对应的key收集订阅者,并在值改变时通知对应的订阅者。部分代码如下:

  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   let childOb = 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 = observe(newVal)
      dep.notify()
    }
  })

每一key都有一个订阅者列表 const dep = new Dep()

在为key进行赋值时,如果值发生了改变,则会通知所有的订阅者 dep.notify()

在对key进行取值时,如果Dep.target有值,除正常的取值操作外会进行一些额外的操作来添加订阅者。大多数时间里,Dep.target的值都为null,只有订阅者在进行订阅操作时,Dep.target才有值,为正在进行订阅的订阅者。此时进行取值操作,会将订阅者加入到对应的订阅者列表中。

订阅者在进行订阅操作时,主要包含以下3个步骤:

  • 将自己放在Dep.target上

  • 对自己依赖的key进行取值

  • 将自己从Dep.target移除

在执行订阅操作后,订阅者会被加入到相关key的订阅者列表中。

针对对象和数组的处理

如果为key赋的值为对象:

  • 会递归地对这个对象中的每一key进行处理

如果为key赋的值为数组:

  • 递归地对这个数组中的每一个对象进行处理

  • 重新定义数组的push,pop,shift,unshift,splice,sort,reverse方法,调用以上方法时key的订阅者列表会通知订阅者们“值已改变”。如果调用的是push,unshift,splice方法,递归处理新增加的项

对模板的处理

Vue将模板处理成一个render函数。需要重新渲染DOM时,render函数结合Vue实例中的数据生成一个虚拟节点。新的虚拟节点和原虚拟节点进行对比,对需要修改的DOM节点进行修改。

订阅者

订阅者在初始化时主要接受2个参数getter, callback。getter用来计算订阅者的值,所以其在执行时会对订阅者所有需要订阅的key进行取值。订阅者的订阅操作主要是通过getter来实现。

部分代码如下:

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)    let value
    const vm = this.vm    if (this.user) {      try {        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {      value = this.getter.call(vm, vm)
    }    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()    this.cleanupDeps()    return value
  }

主要步骤:

  • 将自己放在Dep.target上(pushTarget(this))

  • 执行getter(this.getter.call(vm, vm))

  • 将自己从Dep.target移除(popTarget())

  • 清理之前的订阅(this.cleanupDeps())

此后,订阅者在依赖的key的值发生变化会得到通知。获得通知的订阅者并不会立即被触发,而是会被加入到一个待触发的数组中,在下一个周期统一被触发。

订阅者在被触发时,会执行getter来计算订阅者的值,如果值改变,则会执行callback.

负责渲染DOM的订阅者

Vue实例化后都会生成一个用于渲染DOM的订阅者。此订阅者在实例化时传入的getter方法为渲染DOM的方法。

部分代码如下:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
} vm._watcher = new Watcher(vm, updateComponent, noop)

vm._render()结合模板和数据,计算出虚拟DOM vm._update()根据虚拟DOM渲染真实的DOM节点

此订阅者在初始化时就会进行订阅操作。实例化时传入的getter为updateComponent。其中的vm._render()在执行时一定会对所有依赖的key进行取值,能完成对依赖的key的订阅。同时vm._update()完成了第一次DOM渲染。当前依赖的key的值发生变化,订阅者被触发时,作为getter的updateComponent会重新执行,重新渲染DOM。因为getter返回的值一直为undefined,所以此订阅者中的callback并没有被用到,于是传入了一个空函数noop作为callback

对computed的处理

通过computed可以定义一组计算属性,通过计算属性可以将一些复杂的计算过程抽离出来,保持模板的简单和清晰。

代码示例:

export default {
    data () {        return {            text: 'some texts',            arr: [],            obj: {}
        }
    },    computed: {        key1: function () {            return this.text + this.arr.length
        }
    }
}

在定义一个计算属性时,需要定义一个key和一个计算方法。

Vue在对computed进行处理时,会为每一个计算属性生成一个lazy状态的订阅者。普通的订阅者在实例化和触发时会执行getter来计算自身的值和进行订阅操作。而lazy状态的订阅者在上述情况下只会将自身置为dirty状态,不进行其它操作。在订阅者执行自身的evaluate方法时,会清除自身的dirty状态并执行getter来计算自身的值和进行订阅。

Vue在为计算属性生成订阅者时的示例代码如下:

const computedWatcherOptions = { lazy: true }// create internal watcher for the computed property.watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)

传入的getter为自定义的计算方法,callback为空函数。(lazy状态的订阅者永远都没有机会执行callback)

Vue 在自身实例上为指定key定义get方法,使可以通过Vue实例获取计算属性的值。

部分代码如下:

function createComputedGetter (key) {  return function computedGetter () {    const watcher = this._computedWatchers && this._computedWatchers[key]    if (watcher) {      if (watcher.dirty) {
        watcher.evaluate()
      }      if (Dep.target) {
        watcher.depend()
      }      return watcher.value
    }
  }
}

在对计算属性定义的key进行取值时,会首先获取之前生成好的订阅者。只有订阅者处于dirty状态时,才会执行evaluate计算订阅者的值。所以为计算属性定义的计算方法只有在对计算属性的key进行取值并且计算属性依赖的key曾经改变时才会执行。

假如对上文定义的计算属性key1进行取值

vm.key1; //第一次取值,自定义计算方法执行vm.key1; //第二次取值,依赖的key的值没有变化,自定义计算方法不会执行vm.text = '' //改变计算属性依赖的key的值,计算属性对应的订阅者会进入dirty状态,自定义计算方法不会执行vm.key1; //第三次取值,计算属性依赖的key的值发生了变化并且对计算属性进行取值,自定义的计算方法执行
订阅计算属性值的变化

计算属性的key不会维护一个订阅者列表,也不能通过计算属性的set方法在触发所有订阅者。(计算属性不能被赋值)。一个订阅者执行订阅操作来订阅计算属性值的变化其实是订阅了计算属性依赖的key的值的变化。 在计算属性的get方法中

if (Dep.target) {    watcher.depend()}

如果有订阅者来订阅计算属性的变化,计算属性会将自己的订阅复制到正在进行订阅的订阅者上。watcher.depend()的作用就是如此。

例如:

//初始化订阅者watcher, 依赖计算属性key1var watcher = new Watcher(function () {    return vm.key1
}, noop) vm.text = '' //计算属性key1依赖的text的值发生变化,watcher会被触发

对watch的处理

Vue实例化时可以传入watch对象,来监听某些值的变化。 例如:

export default {
    watch: {        'a.b.c': function (val, oldVal) {            console.log(val)            console.log(oldVal)
        }
    }
}

Vue 会为watch中的每一项生成一个订阅者。订阅者的getter通过处理字符串得到。如'a.b.c'会被处理成

function (vm) {    var a = vm.a    var b = a.b    var c = b.c    return c
}

处理字符串的源码如下:

/**
 * Parse simple path.
 */const bailRE = /[^\w.$]/export function parsePath (path: string): any {  if (bailRE.test(path)) {    return
  }  const segments = path.split('.')  return function (obj) {    for (let i = 0; i < segments.length; i++) {      if (!obj) return
      obj = obj[segments[i]]
    }    return obj
  }
}

订阅者的callback为定义watch时传入的监听函数。当订阅者被触发时,如果订阅者的值发生变化,则会执行callback。callback执行时会传入变化后的值,变化前的值作为参数。

网易云免费体验馆,0成本体验20+款云产品!

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 == vs === in Javascript

Vue 依赖收集原理分析的更多相关文章

  1. Spring依赖注入原理分析

    在分析原理之前我们先回顾下依赖注入的概念: 我们常提起的依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念.具体含义是:当某个角色( ...

  2. 三、vue依赖收集

    Vue 会把普通对象变成响应式对象,响应式对象 getter 相关的逻辑就是做依赖收集,这一节我们来详细分析这个过程 Dep Dep 是整个 getter 依赖收集的核心,它的定义在 src/core ...

  3. Vue依赖收集引发的问题

    问题背景 在我们的项目中有一个可视化配置的模块,是通过go.js生成canvas来实现的.但是,我们发现这个模块在浏览器中经常会引起该tab页崩溃.开启chrome的任务管理器一看,进入该页面内存和c ...

  4. vue双向绑定原理分析

    当我们学习angular或者vue的时候,其双向绑定为我们开发带来了诸多便捷,今天我们就来分析一下vue双向绑定的原理. 简易vue源码地址:https://github.com/jiangzhenf ...

  5. Vue双向数据绑定原理分析(转)

    add by zhj: 目前组里使用的是前端技术是jQuery + Bootstrap,后端使用的Django,Flask等,模板是在后端渲染的.前后端没有分离,这种做法有几个缺点 1. 模板一般是由 ...

  6. Spring学习笔记——Spring依赖注入原理分析

    我们知道Spring的依赖注入有四种方式,各自是get/set方法注入.构造器注入.静态工厂方法注入.实例工厂方法注入 以下我们先分析下这几种注入方式 1.get/set方法注入 public cla ...

  7. Vue 双向数据绑定原理分析 以及 Object.defineproperty语法

    第三方精简版实现 https://github.com/luobotang/simply-vue Object.defineProperty 学习,打开控制台分别输入以下内容调试结果 userInfo ...

  8. 网站统计中的数据收集原理及实现(share)

    转载自:http://blog.codinglabs.org/articles/how-web-analytics-data-collection-system-work.html 网站数据统计分析工 ...

  9. Spark原理分析目录

    1 Spark原理分析 -- RDD的Partitioner原理分析 2 Spark原理分析 -- RDD的shuffle简介 3 Spark原理分析 -- RDD的shuffle框架的实现概要分析 ...

随机推荐

  1. IDEA中Thrift插件配置

    方法一:直接在IDEA界面中配置 打开IDEA的插件中心,搜索 Thrift 即可安装 方法二:手动下载Thrift插件安装 有时像在IDEA中安装Lombok插件一样,有时由于网络原因,方法一不奏效 ...

  2. firefox os 开发模拟器1.4版本号安装开发具体解释

    首先在使用firefox os 模拟器的时候必须先下载firefox 浏览器,这个是众多web开发人员必备的工具,下载地址firefox 浏览器 .在最新的官方版本号是1.5版的模拟器,可是如今还不是 ...

  3. ubuntu如何修改root密码

    安装完Ubuntu后忽然意识到没有设置root密码,不知道密码自然就无法进入根用户下.到网上搜了一下,原来是这麽回事.Ubuntu的默认root密码是随机的,即每次开机都有一个新的root密码.我们可 ...

  4. 键值对集合Dictionary<K,V>根据索引提取数据

    Dictionary<K,V>中ToList方法返回 List<KeyValuePair<K,V>>定义可设置检索的键/值对

  5. Webservice WCF WebApi 前端数据可视化 前端数据可视化 C# asp.net PhoneGap html5 C# Where 网站分布式开发简介 EntityFramework Core依赖注入上下文方式不同造成内存泄漏了解一下? SQL Server之深入理解STUFF 你必须知道的EntityFramework 6.x和EntityFramework Cor

    Webservice WCF WebApi   注明:改编加组合 在.net平台下,有大量的技术让你创建一个HTTP服务,像Web Service,WCF,现在又出了Web API.在.net平台下, ...

  6. iOS_40_核心动画

    核心动画之CATransition转场动画 终于效果图: 核心动画之CAKeyFrameAnimation,图标抖动效果 终于效果图: 核心动画之CAAnimationGroup(动画组) 终于效果图 ...

  7. cf 215 C. Crosses yy题

    链接:http://codeforces.com/problemset/problem/215/C C. Crosses time limit per test 2 seconds memory li ...

  8. DOM编程 --《高性能JavaScript》

    1.重绘和重排 浏览器下载完页面的所有组件 —— HTML标记,CSS,JavaScript,图片,会解析并生成两个内部数据结构. DOM树 表示页面结构 渲染树(CSS) 表示DOM节点如何显示 当 ...

  9. 基本SCTP套接字编程常用函数

    sctp_bindx函数:允许SCTP套接字捆绑一个特定地址子集 #include <netinet/sctp.h> // 若成功返回0,出错返回-1 int sctp_bindx(int ...

  10. python 2: 解决python中的plot函数的图例legend不能显示中文问题

     问题: 图像标题.横纵坐标轴的标签都能显示中文名字,但是图例就是不能显示中文,怎么解决呢?  解决: plt.figure() plt.title(u'训练性能', fontproperties=f ...