首发地址:CJWbiu's Blog

原理:

  ‘当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。’

  上面那段话是Vue官方文档中截取的,可以看到是使用Object.defineProperty实现对数据改变的监听。Vue主要使用了观察者模式来实现数据与视图的双向绑定。

  1. function initData(vm) { //将data上数据复制到_data并遍历所有属性添加代理
  2. vm._data = vm.$options.data;
  3. const keys = Object.keys(vm._data);
  4. let i = keys.length;
  5. while(i--) {
  6. const key = keys[i];
  7. proxy(vm, `_data`, key);
  8. }
  9. observe(data, true /* asRootData */) //对data进行监听
  10. }

  在第一篇数据初始化中,执行new Vue()操作后会执行initData()去初始化用户传入的data,最后一步操作就是为data添加响应式。

实现:

  在Vue内部存在三个对象:Observer、Dep、Watcher,这也是实现响应式的核心。

Observer:

  Observer对象将data中所有的属性转为getter/setter形式,以下是简化版代码,详细代码请看这里

  1. export function observe (value) {
  2. //递归子属性时的判断
  3. if (!isObject(value) || value instanceof VNode) {
  4. return
  5. }
  6. ...
  7. ob = new Observer(value)
  8. }
  9. export class Observer {
  10. constructor (value) {
  11. ... //此处省略对数组的处理
  12. this.walk(value)
  13. }
  14.  
  15. walk (obj: Object) {
  16. const keys = Object.keys(obj)
  17. for (let i = 0; i < keys.length; i++) {
  18. defineReactive(obj, keys[i]) //为每个属性创建setter/getter
  19. }
  20. }
  21. ...
  22. }
  23.  
  24. //设置set/get
  25. export function defineReactive (
  26. obj: Object,
  27. key: string,
  28. val: any
  29. ) {
  30. //利用闭包存储每个属性关联的watcher队列,当setter触发时依然能访问到
  31. const dep = new Dep()
  32. ...
  33. //如果属性为对象也创建相应observer
  34. let childOb = observe(val)
  35. Object.defineProperty(obj, key, {
  36. enumerable: true,
  37. configurable: true,
  38. get: function reactiveGetter () {
  39. if (Dep.target) {
  40. dep.depend() //将当前dep传到对应watcher中再执行watcher.addDep将watcher添加到当前dep.subs中
  41. if (childOb) { //如果属性是对象则继续收集依赖
  42. childOb.dep.depend()
  43. ...
  44. }
  45. }
  46. return value
  47. },
  48. set: function reactiveSetter (newVal) {
  49. ...
  50. childOb = observe(newVal) //如果设置的新值是对象,则为其创建observe
  51. dep.notify() //通知队列中的watcher进行更新
  52. }
  53. })
  54. }

  创建Observer对象时,为data的每个属性都执行了一遍defineReactive方法,如果当前属性为对象,则通过递归进行深度遍历。该方法中创建了一个Dep实例,每一个属性都有一个与之对应的dep,存储所有的依赖。然后为属性设置setter/getter,在getter时收集依赖,setter时派发更新。这里收集依赖不直接使用addSub是为了能让Watcher创建时自动将自己添加到dep.subs中,这样只有当数据被访问时才会进行依赖收集,可以避免一些不必要的依赖收集。

Dep:

  Dep就是一个发布者,负责收集依赖,当数据更新是去通知订阅者(watcher)。源码地址

  1. export default class Dep {
  2. static target: ?Watcher; //指向当前watcher
  3. constructor () {
  4. this.subs = []
  5. }
  6. //添加watcher
  7. addSub (sub: Watcher) {
  8. this.subs.push(sub)
  9. }
  10. //移除watcher
  11. removeSub (sub: Watcher) {
  12. remove(this.subs, sub)
  13. }
  14. //通过watcher将自身添加到dep中
  15. depend () {
  16. if (Dep.target) {
  17. Dep.target.addDep(this)
  18. }
  19. }
  20. //派发更新信息
  21. notify () {
  22. ...
  23. for (let i = 0, l = subs.length; i < l; i++) {
  24. subs[i].update()
  25. }
  26. }
  27. }

Watcher:

源码地址

  1. //解析表达式(a.b),返回一个函数
  2. export function parsePath (path: string): any {
  3. if (bailRE.test(path)) {
  4. return
  5. }
  6. const segments = path.split('.')
  7. return function (obj) {
  8. for (let i = 0; i < segments.length; i++) {
  9. if (!obj) return
  10. obj = obj[segments[i]] //遍历得到表达式所代表的属性
  11. }
  12. return obj
  13. }
  14. }
  15. export default class Watcher {
  16. constructor (
  17. vm: Component,
  18. expOrFn: string | Function,
  19. cb: Function,
  20. options?: ?Object,
  21. isRenderWatcher?: boolean
  22. ) {
  23. this.vm = vm
  24. if (isRenderWatcher) {
  25. vm._watcher = this
  26. }
  27. //对创建的watcher进行收集,destroy时对这些watcher进行销毁
  28. vm._watchers.push(this)
  29. // options
  30. if (options) {
  31. ...
  32. this.before = options.before
  33. }
  34. ...
  35. //上一轮收集的依赖集合Dep以及对应的id
  36. this.deps = []
  37. this.depIds = new Set()
  38. //新收集的依赖集合Dep以及对应的id
  39. this.newDeps = []
  40. this.newDepIds = new Set()
  41. this.expression = process.env.NODE_ENV !== 'production'
  42. ? expOrFn.toString()
  43. : ''
  44. // parse expression for getter
  45. if (typeof expOrFn === 'function') {
  46. this.getter = expOrFn
  47. } else {
  48. this.getter = parsePath(expOrFn)
  49. ...
  50. }
  51. ...
  52. this.value = this.get()
  53. }
  54.  
  55. /** * Evaluate the getter, and re-collect dependencies. */
  56. get () {
  57. pushTarget(this)
  58. let value
  59. const vm = this.vm
  60. try {
  61. value = this.getter.call(vm, vm)
  62. } catch (e) {
  63. if (this.user) {
  64. handleError(e, vm, `getter for watcher "${this.expression}"`)
  65. } else {
  66. throw e
  67. }
  68. } finally {
  69. // "touch" every property so they are all tracked as
  70. // dependencies for deep watching
  71. if (this.deep) {
  72. traverse(value)
  73. }
  74. popTarget()
  75. this.cleanupDeps() //清空上一轮的依赖
  76. }
  77. return value
  78. }
  79.  
  80. /** * Add a dependency to this directive. */
  81. addDep (dep: Dep) {
  82. const id = dep.id
  83. if (!this.newDepIds.has(id)) { //同一个数据只收集一次
  84. this.newDepIds.add(id)
  85. this.newDeps.push(dep)
  86. if (!this.depIds.has(id)) {
  87. dep.addSub(this)
  88. }
  89. }
  90. }
  91.  
  92. //每轮收集结束后去除掉上轮收集中不需要跟踪的依赖
  93. cleanupDeps () {
  94. let i = this.deps.length
  95. while (i--) {
  96. const dep = this.deps[i]
  97. if (!this.newDepIds.has(dep.id)) {
  98. dep.removeSub(this)
  99. }
  100. }
  101. let tmp = this.depIds
  102. this.depIds = this.newDepIds
  103. this.newDepIds = tmp
  104. this.newDepIds.clear()
  105. tmp = this.deps
  106. this.deps = this.newDeps
  107. this.newDeps = tmp
  108. this.newDeps.length = 0
  109. },
  110. update () {
  111. ...
  112. //经过一些优化处理后,最终执行this.get
  113. this.get();
  114. }
  115. // ...
  116. }

  依赖收集的触发是在执行render之前,会创建一个渲染Watcher:

  1. updateComponent = () => {
  2. vm._update(vm._render(), hydrating) //执行render生成VNode并更新dom
  3. }
  4. new Watcher(vm, updateComponent, noop, {
  5. before () {
  6. if (vm._isMounted) {
  7. callHook(vm, 'beforeUpdate')
  8. }
  9. }
  10. }, true /* isRenderWatcher */)

  在渲染Watcher创建时会将Dep.target指向自身并触发updateComponent也就是执行_render生成VNode并执行_updateVNode渲染成真实DOM,在render过程中会对模板进行编译,此时就会对data进行访问从而触发getter,由于此时Dep.target已经指向了渲染Watcher,接着渲染Watcher会执行自身的addDep,做一些去重判断然后执行dep.addSub(this)将自身push到属性对应的dep.subs中,同一个属性只会被添加一次,表示数据在当前Watcher中被引用。

  当_render结束后,会执行popTarget(),将当前Dep.target回退到上一轮的指,最终又回到了null,也就是所有收集已完毕。之后执行cleanupDeps()将上一轮不需要的依赖清除。当数据变化是,触发setter,执行对应Watcher的update属性,去执行get方法又重新将Dep.target指向当前执行的Watcher触发该Watcher的更新。

  这里可以看到有deps,newDeps两个依赖表,也就是上一轮的依赖和最新的依赖,这两个依赖表主要是用来做依赖清除的。但在addDep中可以看到if (!this.newDepIds.has(id))已经对收集的依赖进行了唯一性判断,不收集重复的数据依赖。为何又要在cleanupDeps中再作一次判断呢?

  1. while (i--) {
  2. const dep = this.deps[i]
  3. if (!this.newDepIds.has(dep.id)) {
  4. dep.removeSub(this)
  5. }
  6. }
  7. let tmp = this.depIds
  8. this.depIds = this.newDepIds
  9. this.newDepIds = tmp
  10. this.newDepIds.clear()
  11. tmp = this.deps
  12. this.deps = this.newDeps
  13. this.newDeps = tmp
  14. this.newDeps.length = 0

  在cleanupDeps中主要清除上一轮中的依赖在新一轮中没有重新收集的,也就是数据刷新后某些数据不再被渲染出来了,例如:

  1. <body>
  2. <div id="app">
  3. <div v-if='flag'>
  4.  
  5. </div> <div v-else>
  6.  
  7. </div>
  8. <button @click="msg1 += '1'">change</button> <button @click="flag = !flag">toggle</button> </div> <script type="text/javascript">
  9. var vm = new Vue({
  10. el: '#app',
  11. data: {
  12. flag: true,
  13. msg1: 'msg1',
  14. msg2: 'msg2'
  15. }
  16. })
  17. </script> </body>

  每次点击change,msg1都会拼接一个1,此时就会触发重新渲染。当我们点击toggle时,由于flag改变,msg1不再被渲染,但当我们点击change时,msg1发生了变化,但却没有触发重新渲染,这就是cleanupDeps起的作用。如果去除掉cleanupDeps这个步骤,只是能防止添加相同的依赖,但是数据每次更新都会触发重新渲染,又去重新收集依赖。这个例子中,toggle后,重新收集的依赖中并没有msg1,因为它不需要被显示,但是由于设置了setter,此时去改变msg1依然会触发setter,如果没有执行cleanupDeps,那么msg1的依赖依然存在依赖表里,又会去触发重新渲染,这是不合理的,所以需要每次依赖收集完毕后清除掉一些不需要的依赖。

总结:

  依赖收集其实就是收集每个数据被哪些Watcher(渲染Watcher、computedWatcher等)所引用,当这些数据更新时,就去通知依赖它的Watcher去更新。

Vue源码学习之双向绑定的更多相关文章

  1. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  2. 【Vue源码学习】依赖收集

    前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...

  3. Vue源码学习三 ———— Vue构造函数包装

    Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...

  4. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  5. 最新 Vue 源码学习笔记

    最新 Vue 源码学习笔记 v2.x.x & v3.x.x 框架架构 核心算法 设计模式 编码风格 项目结构 为什么出现 解决了什么问题 有哪些应用场景 v2.x.x & v3.x.x ...

  6. Vue2.0源码阅读笔记--双向绑定实现原理

    上一篇 文章 了解了Vue.js的生命周期.这篇分析Observe Data过程,了解Vue.js的双向数据绑定实现原理. 一.实现双向绑定的做法 前端MVVM最令人激动的就是双向绑定机制了,实现双向 ...

  7. Vue 源码学习(1)

    概述 我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家. 这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容: 寻找入口文件 在打包的过程中 Vue 发生了什么变化 在 ...

  8. 【Vue源码学习】响应式原理探秘

    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...

  9. VUE 源码学习01 源码入口

    VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...

随机推荐

  1. UILabel常见用法

    //创建一个UILabel UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(50 , 100 , 200 , 560)]; // ...

  2. Boost库之asio io_service以及run、run_one、poll、poll_one区别

    一.io_service的作用 io_servie 实现了一个任务队列,这里的任务就是void(void)的函数.Io_servie最常用的两个接口是post和run,post向任务队列中投递任务,r ...

  3. 【转】 Pro Android学习笔记(二九):用户界面和控制(17):include和merge

    目录(?)[-] xml控件代码重用include xml控件代码重用merge 横屏和竖屏landsacpe portrait xml控件代码重用:include 如果我们定义一个控件,需要在不同的 ...

  4. 使用hibernate validator出现

    1.javax.validation.UnexpectedTypeException: No validator could be found for type: java.lang.Integer ...

  5. 在VirtualBox中安装CentOS 7【转载】

    当初接触Linux的时候,因为条件限制,只能在VirtualBox虚拟机中安装Linux系统使用,由于是小白,爬了好多坑.于是决定写一篇关于在虚拟机中安装linux系统的文章.一是为了巩固自己的知识, ...

  6. numpy.zeros(shape, dtype=float, order='C')

    numpy.zeros Return a new array of given shape and type, filled with zeros. Parameters: shape : int o ...

  7. http相关理解

    http://blog.csdn.net/generon/article/details/73920945

  8. zookeeper相关知识的总结:

    一.分布式协调技术 在给大家介绍ZooKeeper之前先来给大家介绍一种技术——分布式协调技术.那么什么是分布式协调技术?那么我来告诉大家,其实分布式协调技术 主要用来解决分布式环境当中多个进程之间的 ...

  9. ubuntu下hive-0.8.1配置

    1.下载hive包wget http://labs.mop.com/apache-mirror/hive/stable/hive-0.8.1.tar.gz,并用tar -xzvf 将其解压到要安装的目 ...

  10. Appium 在 Android UI 测试中的应用

    原文地址:https://blog.coding.net/blog/Appium-Android-UI Android 测试工具与 Appium 简介 Appium 是一个 C/S 架构的,支持 An ...