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技巧--转义符&quot;\&quot;的妙用
Jan 09 Javascript
IE8 下的Js错误HTML Parsing Error...
Aug 14 Javascript
JavaScript实现班级随机点名小应用需求的具体分析
May 12 Javascript
javascript记住用户名和登录密码(两种方式)
Aug 04 Javascript
js鼠标点击图片切换效果代码分享
Aug 26 Javascript
微信小程序 弹框和模态框实现代码
Mar 10 Javascript
JavaScript实现移动端轮播效果
Jun 06 Javascript
基于zepto.js实现手机相册功能
Jul 11 Javascript
手淘flexible.js框架使用和源代码讲解小结
Oct 15 Javascript
vue获取data数据改变前后的值方法
Nov 07 Javascript
基于vue-cli3+typescript的tsx开发模板搭建过程分享
Feb 28 Javascript
es6函数中的作用域实例分析
Apr 18 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读取XML值的代码(推荐)
2011/01/01 PHP
谨慎使用PHP的引用原因分析
2012/09/06 PHP
php实现微信发红包
2015/12/05 PHP
php从数据库读取数据,并以json格式返回数据的方法
2018/08/21 PHP
Aster vs Newbee BO5 第一场2.19
2021/03/10 DOTA
ASP SQL防注入的方法
2008/12/25 Javascript
理解Javascript_09_Function与Object
2010/10/16 Javascript
定时器(setTimeout/setInterval)调用带参函数失效解决方法
2013/03/26 Javascript
javascript去除字符串中所有标点符号和提取纯文本的正则
2014/06/07 Javascript
node.js中的fs.readdirSync方法使用说明
2014/12/17 Javascript
Javascript与jQuery方法的隐藏与显示
2015/01/19 Javascript
jQuery插件EnPlaceholder实现输入框提示文字
2015/06/05 Javascript
JavaScript数组的栈方法与队列方法详解
2016/05/26 Javascript
微信小程序前端源码逻辑和工作流
2016/09/25 Javascript
JS实现获取来自百度,Google,soso,sogou关键词的方法
2016/12/21 Javascript
javascript实现页面滚屏效果
2017/01/17 Javascript
vue.js-div滚动条隐藏但有滚动效果的实现方法
2018/03/03 Javascript
webpack+vuex+axios 跨域请求数据的示例代码
2018/03/06 Javascript
微信小程序云开发实现增删改查功能
2019/05/17 Javascript
后台使用freeMarker和前端使用vue的方法及遇到的问题
2019/06/13 Javascript
分享一款超好用的JavaScript 打包压缩工具
2020/04/26 Javascript
python爬虫教程之爬取百度贴吧并下载的示例
2014/03/07 Python
在win和Linux系统中python命令行运行的不同
2016/07/03 Python
flask-restful使用总结
2018/12/04 Python
Python面向对象之继承和多态用法分析
2019/06/08 Python
pytorch 使用单个GPU与多个GPU进行训练与测试的方法
2019/08/19 Python
Python2与Python3的区别点整理
2019/12/12 Python
Pycharm和Idea支持的vim插件的方法
2020/02/21 Python
Python实现链表反转的方法分析【迭代法与递归法】
2020/02/22 Python
解决pytorch多GPU训练保存的模型,在单GPU环境下加载出错问题
2020/06/23 Python
保安自我鉴定范文
2013/12/08 职场文书
客服部班长工作责任制
2014/02/25 职场文书
21岁生日感言
2014/02/27 职场文书
大学生通用个人自我评价
2014/04/27 职场文书
报表员工作失误检讨书范文
2014/09/19 职场文书
如何正确理解python装饰器
2021/06/15 Python