vue2源码浅读(三):数据响应式的原理与实现

2023-10-14 19:59

本文主要是介绍vue2源码浅读(三):数据响应式的原理与实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

vue2源码浅读(三):数据响应式

  • 众所周知,vue2响应式核心原理是数据劫持,即采用的是Object.defineProperty,关于Object.defineProperty,请移步这里数据劫持(一)由浅入深理解Object.defineProperty

数据响应式入口

  • 紧接着上一篇,在initState中初始化data,调用了initData()方法:
  • src\core\instance\state.js
function initData (vm) {let data = vm.$options.data// 获取到用户传入的options 取到data属性, data的写法如果是函数形式 那么就使用.call指向传入的vm 拿到返回值 // 如果不是函数形式就是对象了不处理, 将数据绑定到data 和 vm._data(绑定到_data统一管理)上data = vm._data = typeof data === 'function'? getData(data, vm): data || {}if (!isPlainObject(data)) {data = {}process.env.NODE_ENV !== 'production' && warn('data functions should return an object:\n' +'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}const keys = Object.keys(data)const props = vm.$options.propsconst methods = vm.$options.methodslet i = keys.lengthwhile (i--) {const key = keys[i]// 检查data里的数据是否与methods里的方法名冲突if (process.env.NODE_ENV !== 'production') {if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm)}}// 检查data里的数据是否与props里的数据冲突if (props && hasOwn(props, key)) {process.env.NODE_ENV !== 'production' && warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm)} else if (!isReserved(key)) {// 如果不是保留字段,将data里的属性代理到vm实例上,使得可以通过this获取proxy(vm, `_data`, key)}}// 响应式处理第一步observe(data, true /* asRootData */)
}
  • vue 设计, 不希望访问 _ 开头的数据,_ 开头的数据是私有数据
  • 那么,经过proxy后, data挂载vm的实例上,访问app.xxx就相当于访问app._data.xxx
const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]}sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val}Object.defineProperty(target, key, sharedPropertyDefinition)
}
  • 找到observe方法,这里才是响应式处理入口
  • src\core\observer\index.js
function observe (value, asRootData) {if (!isObject(value) || value instanceof VNode) {return}let ob;//已经被做过的响应式处理,数据会有__ob__属性,且__ob__为Observer实例,直接返回ob   if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {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的响应式数据分为两类:对象和数组
  • 遍历对象的所有属性,并为每个属性设置getter和setter,以便将来的获取和设置,如果属性的值也是对象,则递归为属性值上的每个key设置getter和setter
  • 接下来进入正题, Observer 类的具体实现
  • src\core\observer\index.js
class Observer {value;dep;vmCount: number; // number of vms that have this object as root $dataconstructor (value) {this.value = value// 实例化一个dep,即Observer都会有一个Dep依赖this.dep = new Dep()this.vmCount = 0//添加'__ob__'def(value, '__ob__', this)// 数组需要单独处理if (Array.isArray(value)) {if (hasProto) {// 如果是现代的浏览器,复杂类型数据有原型,调用arrayMethods,重写相关方法,具体见下边protoAugment(value, arrayMethods)} else {// 如果是老旧浏览器,没有原型,直接给数组上的方法给重写替换掉copyAugment(value, arrayMethods, arrayKeys)}// 调用数组观察方法this.observeArray(value)} else {// 对象的响应式处理this.walk(value)}}// 对象响应式处理:遍历所有属性,并为每个属性添加geeter/setter。walk (obj: Object) {const keys = Object.keys(obj)for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])}}// 数组观察方法observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}}
}

数组响应式处理

  • 众所周知,Object.defineProperty不能监听数组的变化,具体为使用push、unshift、pop、shift、splice, sort, revers,是触发不了set的。
  • 通过重写数组操作方法实现数组监听
  • src\core\observer\array.js

// 获取数组的原型Array.prototype
const arrayProto = Array.prototype// 创建一个空对象arrayMethods,并将arrayMethods的原型指向arrayProto
export 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) {// 执行原始行为的逻辑const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}// 新加入的元素做响应式处理if (inserted) ob.observeArray(inserted)// 内部的dep去通知更新ob.dep.notify()return result})
})

对象响应式处理

  • 在Observer类里边,walk方法里调用了defineReactive方法
  • 获取数据时:在dep中添加相关的watcher
  • 设置数据时:再由dep通知相关的watcher去更新
  • src\core\observer\index.js
function defineReactive (obj,key,valcustomSetter,shallow) {const dep = new Dep()// 每个key都会实例化一个Dep// 获取属性相关描述const property = Object.getOwnPropertyDescriptor(obj, key)// configurable为false时,该属性相关的配置不能再被更改,也不能被删除if (property && property.configurable === false) {return}const getter = property && property.getconst setter = property && property.setif ((!getter || setter) && arguments.length === 2) {val = obj[key]}let childOb = !shallow && observe(val)// 拦截对obj[key]的获取和设置Object.defineProperty(obj, key, {enumerable: true,configurable: true,// 拦截对obj[key]的获取操作get: function reactiveGetter () {// 获取obj[key]的值const value = getter ? getter.call(obj) : val// 依赖收集// 如果存在,则说明此次调用触发者是一个Watcher实例if (Dep.target) {// 依赖关系的创建,建立dep和Dep.target之间的依赖关系(把dep添加到watcher中,也将watcher添加到dep中)dep.depend()if (childOb) {// 建立是ob内部的dep和Dep.target之间的依赖关系,也就是嵌套对象的依赖收集childOb.dep.depend()if (Array.isArray(value)) {// 如果是数组,数组内部的所有项都需要做依赖收集处理dependArray(value)}}}return value},// 拦截对obj[key]的设置操作set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val// 如果新值和老值相等则不做处理 直接返回if (newVal === value || (newVal !== newVal && value !== value)) {return}// 如果setter不存在,说明只能获取不能设置,也直接返回if (getter && !setter) return// 设置为新的值if (setter) {setter.call(obj, newVal)} else {val = newVal}// 对新值也做响应式处理childOb = !shallow && observe(newVal)// 通知更新dep.notify()}})
}
  • 收集对数组元素的依赖项
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)}}
}

依赖管理Dep类

  • 接着找到依赖管理Dep类,主要实现提供依赖收集 ( depend ) 和 派发更新 ( notify ) 的功能。
  • 每个在页面上使用了的数据都会有一个Dep 类,主要访问属性的时候 get 方法会收集对应的 watcher
  • 在获取数据的时候知道自己(Dep)依赖的watcher都有谁,同时在数据变更的时候通知自己(Dep)依赖的这些watcher去执行对应update,以在页面多组件情况下实现局部渲染。
  • src\core\observer\dep.js
class Dep {static target: ?Watcher;id: number;subs: Array<Watcher>;constructor () {this.id = uid++// subs用于存放依赖this.subs = []}// 在dep中添加watcheraddSub (sub: Watcher) {this.subs.push(sub)}// 删除dep中的watcherremoveSub (sub: Watcher) {remove(this.subs, sub)}// 在watcher中添加depdepend () {if (Dep.target) {Dep.target.addDep(this)}}// 遍历dep的所有watcher 然后执行他们的update notify () {const subs = this.subs.slice()if (process.env.NODE_ENV !== 'production' && !config.async) {subs.sort((a, b) => a.id - b.id)}for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}Dep.target = null
const targetStack = []
// 渲染阶段,访问页面上的属性变量时,给对应的 Dep 添加 watcher
export function pushTarget (target: ?Watcher) {targetStack.push(target)Dep.target = target
}
// 访问结束后删除
export function popTarget () {targetStack.pop()Dep.target = targetStack[targetStack.length - 1]
}

观察者watcher

  • 页面访问的属性,将该属性的watcher 关联到该属性的 dep 中
  • 同时, 将 dep 也存储 watcher 中. ( 互相引用的关系 )
  • 在 Watcher 调用 get 方法的时候, 将当前 Watcher 放到全局, 在 get 之前结束的时候(之后), 将这个 全局的 watcher 移除
  • src\core\observer\watcher.js
class Watcher {vm: Component;expression: string;cb: Function;id: number;deep: boolean;user: boolean;lazy: boolean;sync: boolean;dirty: boolean;active: boolean;deps: Array<Dep>;newDeps: Array<Dep>;depIds: SimpleSet;newDepIds: SimpleSet;before: ?Function;getter: Function;value: any;constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {this.vm = vmif (isRenderWatcher) {vm._watcher = this}// // 当前Watcher添加到vue实例上vm._watchers.push(this)// 参数配置,options默认falseif (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.syncthis.before = options.before} else {this.deep = this.user = this.lazy = this.sync = false}this.cb = cbthis.id = ++uid // uid for batchingthis.active = truethis.dirty = this.lazy // for lazy watchersthis.deps = []this.newDeps = []this.depIds = new Set()this.newDepIds = new Set()this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''// 如果exporfn是函数的话,就会把这个函数赋值给getterif (typeof expOrFn === 'function') {this.getter = expOrFn} else {// 如果不是函数是字符串的话,会调用parsePath方法,// parsePath方法会把我们传入的path节分为数组,通过patch来访问到我们的对象。this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = noopprocess.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}// Watcher不止会监听Observer,还会直接把值计算出来放在this.value上this.value = this.lazy? undefined: this.get()}get () {// 将Dep的target添加到targetStack,同时Dep的target赋值为当前watcher对象pushTarget(this)let valueconst vm = this.vmtry {// 去访问我们给属性重写的 get 方法,添加 watcher 依赖value = this.getter.call(vm, vm)} catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)} else {throw e}} finally {if (this.deep) {traverse(value)}// update执行完成后,弹出target,防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖popTarget()this.cleanupDeps()}return value}// 添加依赖addDep (dep: Dep) {const id = dep.idif (!this.newDepIds.has(id)) {// watcher添加它和dep的关系this.newDepIds.add(id)this.newDeps.push(dep)if (!this.depIds.has(id)) {// dep添加它和watcher的关系dep.addSub(this)}}}//  清理依赖项收集cleanupDeps () {let i = this.deps.lengthwhile (i--) {const dep = this.deps[i]if (!this.newDepIds.has(dep.id)) {dep.removeSub(this)}}let tmp = this.depIdsthis.depIds = this.newDepIdsthis.newDepIds = tmpthis.newDepIds.clear()tmp = this.depsthis.deps = this.newDepsthis.newDeps = tmpthis.newDeps.length = 0}// 更新update () {if (this.lazy) {// 懒执行,computedthis.dirty = true} else if (this.sync) {// 同步执行this.run()} else {// 将watcher放到watcher队列中queueWatcher(this)}}
// 更新视图run () {if (this.active) {const value = this.get()if (value !== this.value ||isObject(value) ||this.deep) {const oldValue = this.valuethis.value = valueif (this.user) {const info = `callback for watcher "${this.expression}"`invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)} else {this.cb.call(this.vm, value, oldValue)}}}}// 懒执行的watcher会调用该方法evaluate () {this.value = this.get()this.dirty = false}
// 依赖这个观察者收集的所有depsdepend () {let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}// // 从所有依赖项的订阅者列表中把自己删除teardown () {if (this.active) {if (!this.vm._isBeingDestroyed) {remove(this.vm._watchers, this)}let i = this.deps.lengthwhile (i--) {this.deps[i].removeSub(this)}this.active = false}}
}

在这里插入图片描述

梳理整个过程

  • 在initState方法中,首先是用户传入参数有没有data,没有则observe(vm._data = {}, true /* asRootData */),将一个空的data代理到vm实例上,有则调用initData方法。
  • 在initData方法中,主要遍历data中的属性,通过proxy(vm,_data, key),并将其代理到vm实例上,然后对data进行观察observe(data, true /* asRootData */);
  • 在observe方法中,主要判断传入的value有没有__ob__属性,有则说明已经被观察过了,没有则进行new Observer(value)
  • 在Observer类中,主要实现了对数组和对象的响应式处理。
    • 首先是调用 def(value, '__ob__', this)给value添加__ob__属性;
    • 判断value如果是数组,再判断当前环境
      • 如果是现代的浏览器,复杂类型数据有原型,调用arrayMethods,重写相关方法
        • 将数组原型指向空对象上,对push、unshift、pop、shift、splice, sort, revers方法重写覆盖。具体还是执行原来的行为逻辑,主要是拦截对其做响应式处理:调用 ob.dep.notify()通知更新,如果是新插入的元素调用ob.observeArray(inserted)。
      • 如果是老旧浏览器,没有原型,直接给数组上的方法给重写替换掉
      • 最后调用observeArray方法,对每个数组元素进行observe
    • 判断value如果是对象
    • 遍历对象,对每个键调用defineReactive(obj, keys[i])方法,做响应式处理
  • 在defineReactive(obj, keys[i])方法中,首先对key都会实例化一个Dep。
    • 在get方法中,收集依赖,把dep添加到watcher中,也将watcher添加到dep中dep.depend()
      • 如果有子对象,则进行嵌套对象的依赖收集childOb.dep.depend()
      • 如果是数组,数组内部的所有项都需要做依赖收集 dependArray(value)
    • 在set方法中,如果新值和老值相等则不做处理直接返回,否则赋新值,且对新值做响应式处理,dep.notify()通知更新
  • 在Dep类中,访问属性的时候 get 方法会收集对应的 watcher,在数据变更的时候通知自己(Dep)依赖的这些watcher去执行对应update,实现局部渲染。
  • Watcher类中,将Dep也存储 watcher 中,形成相互引用的关系,提供了update方法
    • Watcher中 不止会监听Observer,还会直接把值计算出来放在this.value上。其中get方法主要应用于computed计算和watch监听。

这篇关于vue2源码浅读(三):数据响应式的原理与实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/212817

相关文章

详谈redis跟数据库的数据同步问题

《详谈redis跟数据库的数据同步问题》文章讨论了在Redis和数据库数据一致性问题上的解决方案,主要比较了先更新Redis缓存再更新数据库和先更新数据库再更新Redis缓存两种方案,文章指出,删除R... 目录一、Redis 数据库数据一致性的解决方案1.1、更新Redis缓存、删除Redis缓存的区别二

Redis事务与数据持久化方式

《Redis事务与数据持久化方式》该文档主要介绍了Redis事务和持久化机制,事务通过将多个命令打包执行,而持久化则通过快照(RDB)和追加式文件(AOF)两种方式将内存数据保存到磁盘,以防止数据丢失... 目录一、Redis 事务1.1 事务本质1.2 数据库事务与redis事务1.2.1 数据库事务1.

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur

C#实现文件读写到SQLite数据库

《C#实现文件读写到SQLite数据库》这篇文章主要为大家详细介绍了使用C#将文件读写到SQLite数据库的几种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下... 目录1. 使用 BLOB 存储文件2. 存储文件路径3. 分块存储文件《文件读写到SQLite数据库China编程的方法》博客中,介绍了文

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

Oracle Expdp按条件导出指定表数据的方法实例

《OracleExpdp按条件导出指定表数据的方法实例》:本文主要介绍Oracle的expdp数据泵方式导出特定机构和时间范围的数据,并通过parfile文件进行条件限制和配置,文中通过代码介绍... 目录1.场景描述 2.方案分析3.实验验证 3.1 parfile文件3.2 expdp命令导出4.总结