Vue源码学习之关于对Array的数据侦听实现


Posted in Javascript onApril 23, 2019

摘要

我们都知道Vue的响应式是通过Object.defineProperty来进行数据劫持。但是那是针对Object类型可以实现, 如果是数组呢? 通过set/get方式是不行的。

但是Vue作者使用了一个方式来实现Array类型的监测: 拦截器。

核心思想

通过创建一个拦截器来覆盖数组本身的原型对象Array.prototype。

拦截器

通过查看Vue源码路径vue/src/core/observer/array.js。

/**
 * Vue对数组的变化侦测
 * 思想: 通过一个拦截器来覆盖Array.prototype。
 * 拦截器其实就是一个Object, 它的属性与Array.prototype一样。 只是对数组的变异方法进行了处理。
*/

function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
   value: val,
   enumerable: !!enumerable,
   writable: true,
   configurable: true
  })
}

// 数组原型对象
const arrayProto = Array.prototype
// 拦截器
const arrayMethods = Object.create(arrayProto)

// 变异数组方法:执行后会改变原始数组的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // 缓存原始的数组原型上的方法
  const original = arrayProto[method]
  // 对每个数组编译方法进行处理(拦截)
  def(arrayMethods, method, function mutator (...args) {
   // 返回的value还是通过数组原型方法本身执行的结果
   const result = original.apply(this, args)
   // 每个value在被observer()时候都会打上一个__ob__属性
   const ob = this.__ob__
   // 存储调用执行变异数组方法导致数组本身值改变的数组,主要指的是原始数组增加的那部分(需要重新Observer)
   let inserted
   switch (method) {
    case 'push':
    case 'unshift':
     inserted = args
     break
    case 'splice':
     inserted = args.slice(2)
     break
   }
   // 重新Observe新增加的数组元素
   if (inserted) ob.observeArray(inserted)
   // 发送变化通知
   ob.dep.notify()
   return result
  })
})

关于Vue什么时候对data属性进行Observer

如果熟悉Vue源码的童鞋应该很快能找到Vue的入口文件vue/src/core/instance/index.js。

function Vue (options) {
 if (process.env.NODE_ENV !== 'production' &&
  !(this instanceof Vue)
 ) {
  warn('Vue is a constructor and should be called with the `new` keyword')
 }
 this._init(options)
}

initMixin(Vue)
// 给原型绑定代理属性$props, $data
// 给Vue原型绑定三个实例方法: vm.$watch,vm.$set,vm.$delete
stateMixin(Vue)
// 给Vue原型绑定事件相关的实例方法: vm.$on, vm.$once ,vm.$off , vm.$emit
eventsMixin(Vue)
// 给Vue原型绑定生命周期相关的实例方法: vm.$forceUpdate, vm.destroy, 以及私有方法_update
lifecycleMixin(Vue)
// 给Vue原型绑定生命周期相关的实例方法: vm.$nextTick, 以及私有方法_render, 以及一堆工具方法
renderMixin(Vue)

export default Vue

this.init()

源码路径: vue/src/core/instance/init.js。

export function initMixin (Vue: Class<Component>) {
 Vue.prototype._init = function (options?: Object) {
  // 当前实例
  const vm: Component = this
  // a uid
  // 实例唯一标识
  vm._uid = uid++

  let startTag, endTag
  /* istanbul ignore if */
  // 开发模式, 开启Vue性能检测和支持 performance.mark API 的浏览器上。
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
   startTag = `vue-perf-start:${vm._uid}`
   endTag = `vue-perf-end:${vm._uid}`
   // 处于组件初始化阶段开始打点
   mark(startTag)
  }

  // a flag to avoid this being observed
  // 标识为一个Vue实例
  vm._isVue = true
  // merge options
  // 把我们传入的optionsMerge到$options
  if (options && options._isComponent) {
   // optimize internal component instantiation
   // since dynamic options merging is pretty slow, and none of the
   // internal component options needs special treatment.
   initInternalComponent(vm, options)
  } else {
   vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
   )
  }
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
   initProxy(vm)
  } else {
   vm._renderProxy = vm
  }
  // expose real self
  vm._self = vm
  // 初始化生命周期
  initLifecycle(vm)
  // 初始化事件中心
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  // 初始化State
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
   vm._name = formatComponentName(vm, false)
   mark(endTag)
   measure(`vue ${vm._name} init`, startTag, endTag)
  }
  // 挂载
  if (vm.$options.el) {
   vm.$mount(vm.$options.el)
  }
 }
}

initState()

源码路径:vue/src/core/instance/state.js。

export function initState (vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 if (opts.props) initProps(vm, opts.props)
 if (opts.methods) initMethods(vm, opts.methods)
 if (opts.data) {
  initData(vm)
 } else {
  observe(vm._data = {}, true /* asRootData */)
 }
 if (opts.computed) initComputed(vm, opts.computed)
 if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm, opts.watch)
 }
}

这个时候你会发现observe出现了。

observe

源码路径: vue/src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
 if (!isObject(value) || value instanceof VNode) {
  return
 }
 let ob: Observer | void
 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  // value已经是一个响应式数据就不再创建Observe实例, 避免重复侦听
  ob = value.__ob__
 } else if (
  shouldObserve &&
  !isServerRendering() &&
  (Array.isArray(value) || isPlainObject(value)) &&
  Object.isExtensible(value) &&
  !value._isVue
 ) {
  // 出现目标, 创建一个Observer实例
  ob = new Observer(value)
 }
 if (asRootData && ob) {
  ob.vmCount++
 }
 return ob
}

使用拦截器的时机

Vue的响应式系统中有个Observe类。源码路径:vue/src/core/observer/index.js。

// can we use __proto__?
export const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

function protoAugment (target, src: Object) {
 /* eslint-disable no-proto */
 target.__proto__ = src
 /* eslint-enable no-proto */
}

function copyAugment (target: Object, src: Object, keys: Array<string>) {
 // target: 需要被Observe的对象
 // src: 数组代理原型对象
 // keys: const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
 // keys: 数组代理原型对象上的几个编译方法名
 // const methodsToPatch = [
 //  'push',
 //  'pop',
 //  'shift',
 //  'unshift',
 //  'splice',
 //  'sort',
 //  'reverse'
 // ]
 for (let i = 0, l = keys.length; i < l; i++) {
  const key = keys[i]
  def(target, key, src[key])
 }
}

export class Observer {
 value: any;
 dep: Dep;
 vmCount: number; // number of vms that have this object as root $data

 constructor (value: any) {
  this.value = value
  // 
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  // 如果是数组
  if (Array.isArray(value)) {
   if (hasProto) {
    // 如果支持__proto__属性(非标属性, 大多数浏览器支持): 直接把原型指向代理原型对象
    protoAugment(value, arrayMethods)
   } else {
    // 不支持就在数组实例上挂载被加工处理过的同名的变异方法(且不可枚举)来进行原型对象方法拦截
    // 当你访问一个对象的方法时候, 只有当自身不存在时候才会去原型对象上查找
    copyAugment(value, arrayMethods, arrayKeys)
   }
   this.observeArray(value)
  } else {
   this.walk(value)
  }
 }

 /**
  * Walk through all properties and convert them into
  * getter/setters. This method should only be called when
  * value type is Object.
  */
 walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
   defineReactive(obj, keys[i])
  }
 }

 /**
  * 遍历数组每一项来进行侦听变化,即每个元素执行一遍Observer()
  */
 observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
   observe(items[i])
  }
 }
}

如何收集依赖

Vue里面真正做数据响应式处理的是defineReactive()。 defineReactive方法就是把对象的数据属性转为访问器属性, 即为数据属性设置get/set。

function dependArray (value: Array<any>) {
 for (let e, i = 0, l = value.length; i < l; i++) {
  e = value[i]
  e && e.__ob__ && e.__ob__.dep.depend()
  if (Array.isArray(e)) {
   dependArray(e)
  }
 }
}


export function defineReactive (
 obj: Object,
 key: string,
 val: any,
 customSetter?: ?Function,
 shallow?: boolean
) {
 // dep在访问器属性中闭包使用
 // 每一个数据字段都通过闭包引用着属于自己的 dep 常量
 // 每个字段的Dep对象都被用来收集那些属于对应字段的依赖。
 const dep = new Dep()

 // 获取该字段可能已有的属性描述对象
 const property = Object.getOwnPropertyDescriptor(obj, key)
 // 边界情况处理: 一个不可配置的属性是不能使用也没必要使用 Object.defineProperty 改变其属性定义的。
 if (property && property.configurable === false) {
  return
 }

 // 由于一个对象的属性很可能已经是一个访问器属性了,所以该属性很可能已经存在 get 或 set 方法
 // 如果接下来会使用 Object.defineProperty 函数重新定义属性的 setter/getter
 // 这会导致属性原有的 set 和 get 方法被覆盖,所以要将属性原有的 setter/getter 缓存
 const getter = property && property.get
 const setter = property && property.set
 // 边界情况处理
 if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
 }
 // 默认就是深度观测,引用子属性的__ob__
 // 为Vue.set 或 Vue.delete 方法提供触发依赖。
 let childOb = !shallow && observe(val)
 Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
   // 如果 getter 存在那么直接调用该函数,并以该函数的返回值作为属性的值,保证属性的原有读取操作正常运作
   // 如果 getter 不存在则使用 val 作为属性的值
   const value = getter ? getter.call(obj) : val
   // Dep.target的值是在对Watch实例化时候赋值的
   if (Dep.target) {
    // 开始收集依赖到dep
    dep.depend()
    if (childOb) {
     childOb.dep.depend()
     if (Array.isArray(value)) {
      // 调用 dependArray 函数逐个触发数组每个元素的依赖收集
      dependArray(value)
     }
    }
   }
   // 正确地返回属性值。
   return value
  },
  set: function reactiveSetter (newVal) {
   // 获取原来的值
   const value = getter ? getter.call(obj) : val
   /* eslint-disable no-self-compare */
   // 比较新旧值是否相等, 考虑NaN情况
   if (newVal === value || (newVal !== newVal && value !== value)) {
    return
   }
   /* eslint-enable no-self-compare */
   if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
   }
   // #7981: for accessor properties without setter
   if (getter && !setter) return
   // 如果数据之前有setter, 那么应该继续使用该函数来设置属性的值
   if (setter) {
    setter.call(obj, newVal)
   } else {
    // 赋新值
    val = newVal
   }
   // 由于属性被设置了新的值,那么假如我们为属性设置的新值是一个数组或者纯对象,
   // 那么该数组或纯对象是未被观测的,所以需要对新值进行观测
   childOb = !shallow && observe(newVal)
   // 通知dep中的watcher更新
   dep.notify()
  }
 })
}

存储数组依赖的列表

我们为什么需要把依赖存在Observer实例上。 即

export class Observer {
  constructor (value: any) {
    ...
    this.dep = new Dep()
  }
}

首先我们需要在getter里面访问到Observer实例

// 即上述的
let childOb = !shallow && observe(val)
...
if (childOb) {
 // 调用Observer实例上dep的depend()方法收集依赖
 childOb.dep.depend()
 if (Array.isArray(value)) {
  // 调用 dependArray 函数逐个触发数组每个元素的依赖收集
  dependArray(value)
 }
}

另外我们在前面提到的拦截器中要使用Observer实例。

methodsToPatch.forEach(function (method) {
  ...
  // this表示当前被操作的数据
  // 但是__ob__怎么来的?
  const ob = this.__ob__
  ...
  // 重新Observe新增加的数组元素
  if (inserted) ob.observeArray(inserted)
  // 发送变化通知
  ob.dep.notify()
  ...
})

思考上述的this.__ob__属性来自哪里?

export class Observer {
  constructor () {
    ...
    this.dep = new Dep()
    // 在vue上新增一个不可枚举的__ob__属性, 这个属性的值就是Observer实例
    // 因此我们就可以通过数组数据__ob__获取Observer实例
    // 进而获取__ob__上的dep
    def(value, '__ob__', this)
    ...
  }
}

牢记所有的属性一旦被侦测了都会被打上一个__ob__的标记, 即表示是响应式数据。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
addRule在firefox下的兼容写法
Nov 30 Javascript
jQuery的实现原理的模拟代码 -5 Ajax
Aug 07 Javascript
js判断undefined变量类型使用typeof
Jun 03 Javascript
javaScript 动态访问JSon元素示例代码
Aug 30 Javascript
node.js中的http.get方法使用说明
Dec 14 Javascript
JavaScript获取页面上被选中文字的方法技巧
Mar 13 Javascript
Bootstrap组合上、下拉框简单实现代码
Mar 06 Javascript
JS弹窗 JS弹出DIV并使整个页面背景变暗功能的实现代码
Apr 21 Javascript
关于vue v-for循环解决img标签的src动态绑定问题
Sep 18 Javascript
如何优雅地在vue中添加权限控制示例详解
Mar 07 Javascript
layer.confirm()右边按钮实现href的例子
Sep 27 Javascript
js canvas实现五子棋小游戏
Jan 22 Javascript
vue的keep-alive中使用EventBus的方法
Apr 23 #Javascript
js继承的这6种方式!(上)
Apr 23 #Javascript
jQuery对底部导航进行跳转并高亮显示的实例代码
Apr 23 #jQuery
node.js基于socket.io快速实现一个实时通讯应用
Apr 23 #Javascript
深入浅出 Vue 系列 -- 数据劫持实现原理
Apr 23 #Javascript
vue 中 beforeRouteEnter 死循环的问题
Apr 23 #Javascript
JavaScript中十种一步拷贝数组的方法实例详解
Apr 22 #Javascript
You might like
POSIX 风格和兼容 Perl 风格两种正则表达式主要函数的类比(preg_match, preg_replace, ereg, ereg_replace)
2010/10/12 PHP
PHP对象转换为数组函数(递归方法)
2012/02/04 PHP
php之Smarty模板使用方法示例详解
2014/07/08 PHP
php中header跳转使用include包含解决参数丢失问题
2015/05/08 PHP
php实现按天数、星期、月份查询的搜索框
2016/05/02 PHP
PHP内置加密函数详解
2016/11/20 PHP
PhpStorm的使用教程(本地运行PHP+远程开发+快捷键)
2020/03/26 PHP
图片onload事件触发问题解决方法
2011/07/31 Javascript
JavaScript常用脚本汇总(一)
2015/03/04 Javascript
javascript实现五星评分功能
2015/11/10 Javascript
javascript实现dom元素可拖动
2016/03/21 Javascript
React.js入门学习第一篇
2016/03/30 Javascript
使用get方式提交表单在地址栏里面不显示提交信息
2017/02/21 Javascript
解决vue props 拿不到值的问题
2018/09/11 Javascript
使用Jenkins部署React项目的方法步骤
2019/03/11 Javascript
ios中视频的最后一桢问题解决
2019/05/14 Javascript
jQuery中DOM操作原则实例分析
2019/08/01 jQuery
layui switch 开关监听 弹出确定状态转换的例子
2019/09/21 Javascript
javascript二维数组和对象的深拷贝与浅拷贝实例分析
2019/10/26 Javascript
Python与Redis的连接教程
2015/04/22 Python
python统计cpu利用率的方法
2015/06/02 Python
Python Learning 列表的更多操作及示例代码
2018/08/22 Python
Python异常的检测和处理方法
2018/10/26 Python
pycharm下pyqt4安装及环境配置的教程
2020/04/24 Python
pyecharts在数据可视化中的应用详解
2020/06/08 Python
安装python依赖包psycopg2来调用postgresql的操作
2021/01/01 Python
美国网上鞋子零售商:Dr. Scholl’s Shoes
2017/11/17 全球购物
日本高岛屋百货购物网站:TAKASHIMAYA
2019/03/24 全球购物
个人求职简历的自我评价
2013/10/19 职场文书
2014年乡镇植树节活动方案
2014/02/28 职场文书
企业办公室岗位职责
2014/03/12 职场文书
2014年秋季开学典礼致辞
2014/08/02 职场文书
爱心捐助活动总结
2015/05/09 职场文书
《葡萄沟》教学反思
2016/02/23 职场文书
mysql 排序失效
2022/05/20 MySQL
Nginx利用Logrotate实现日志分割
2022/05/20 Servers