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 相关文章推荐
js控制frameSet示例
Sep 10 Javascript
js监听滚动条滚动事件使得某个标签内容始终位于同一位置
Jan 24 Javascript
javascript实现鼠标拖动改变层大小的方法
Apr 30 Javascript
jquery实现鼠标拖拽滑动效果来选择数字的方法
May 04 Javascript
js过滤HTML标签完整实例
Nov 26 Javascript
Bootstrap框架结合jQuery仿百度换肤功能实例解析
Sep 17 Javascript
Javascript使用uploadify来实现多文件上传
Nov 16 Javascript
ES2015 Symbol 一种绝不重复的值
Dec 25 Javascript
javascript表单正则应用
Feb 04 Javascript
javascript填充默认头像方法
Feb 22 Javascript
react-native动态切换tab组件的方法
Jul 07 Javascript
前后端如何实现登录token拦截校验详解
Sep 03 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
php基础知识:类与对象(3) 构造函数和析构函数
2006/12/13 PHP
PHP中$_SERVER的详细参数与说明
2008/07/29 PHP
如何用php生成扭曲及旋转的验证码图片
2013/06/07 PHP
php实现姓名根据首字母排序的类与方法(实例代码)
2018/05/16 PHP
20款超赞的jQuery插件 Web开发人员必备
2011/02/26 Javascript
jquery调用asp.net 页面后台的实现代码
2011/04/27 Javascript
JavaScript高级程序设计 阅读笔记(二十) js错误处理
2012/08/14 Javascript
JavaScript实现为指定对象添加多个事件处理程序的方法
2015/04/17 Javascript
js+css实现的圆角边框TAB选项卡滑动门代码分享(2款)
2015/08/26 Javascript
jQuery使用each方法与for语句遍历数组示例
2016/06/16 Javascript
jQuery实现简单的网页换肤效果示例
2016/09/18 Javascript
Vue实现双向数据绑定
2017/05/03 Javascript
javascript实现固定侧边栏
2021/02/09 Javascript
python实现根据窗口标题调用窗口的方法
2015/03/13 Python
Python 模拟购物车的实例讲解
2017/09/11 Python
Python3.x爬虫下载网页图片的实例讲解
2018/05/22 Python
python 寻找list中最大元素对应的索引方法
2018/06/28 Python
Django教程笔记之中间件middleware详解
2018/08/01 Python
Python时间序列处理之ARIMA模型的使用讲解
2019/04/02 Python
python 修改本地网络配置的方法
2019/08/14 Python
如何用Python 加密文件
2020/09/10 Python
快速解决pymongo操作mongodb的时区问题
2020/12/05 Python
详解CSS3的perspective属性设置3D变换距离的方法
2016/05/23 HTML / CSS
使用html5新特性轻松监听任何App自带返回键的示例
2018/03/13 HTML / CSS
临床医学大学生求职信
2013/09/28 职场文书
母亲追悼会答谢词
2014/01/27 职场文书
优秀管理者获奖感言
2014/02/17 职场文书
行政主管职责范本
2014/03/07 职场文书
环保建议书
2014/03/12 职场文书
小学学雷锋活动总结
2014/04/25 职场文书
安全在我心中演讲稿
2014/09/01 职场文书
小学生学习保证书
2015/02/26 职场文书
初一语文教学反思
2016/03/03 职场文书
MySQL 8.0 驱动与阿里druid版本兼容问题解决
2021/07/01 MySQL
Vue图片裁剪组件实例代码
2021/07/02 Vue.js
德生TECSUN S-2000使用手册文字版
2022/05/10 无线电