Vue源码解析02-数据响应式

mac2024-05-06  33

Vue源码解析02-数据响应式

开篇之前先了解几个相关概念

MVC模式

模式简介

MVC的全称是Model(模型)-View(视图)-Controller(控制器)

Model:这是数据层,存储项目所需的数据。Model的作用是返回或者更新数据。在应用中常用于数据库存储数据。

View:视图层,用于想用户显示数据。View本身不显示任何数据,而是Controller或者Model让View显示数据。

Controller:控制层,MVC的核心部分。控制器相当于用户和系统的链接,接收用户的输入,处理完成后,要求Model更新数据。

MVP模式

MVP模式将Controller更名为Presenter,同时改变了通讯方向。全称是Model(模型)-Presenter(呈现器)-View(视图)。

MVP中的数据通信均是双向的。

Model:数据层。

View:视图层。

Presenter:Presenter层。Pressenter作为View和Model的中间层起到了桥梁的作用。Presenter从Model层获取数据通过接口发送给View层展示。View层将用户操作发送给Presenter,借由Presenter将数据发送给Model进行数据更新。

MVVM模式

MVVP模式将Presenter更名为View-Model(对应MVC中的C-Controller),基本上于MVP模式一致。但是MVVM采用的是双向数据绑定,View的变动自动反应到ViewModel上。

Model:数据层。

View:视图层。

ViewModel:在vue中指的是vue的实例对象,是一个公开公共属性和命令的抽象view;

View中的变化会自动更新到ViewModel,ViewModel的变化也会自动反应到View视图中。这种自动更新是通过vue中的Observer观察者实现的。

Vue的双向数据绑定原理

数据双向绑定的意思是view的变化能反应到ViewModel中,ViewModel中的变化能同步更新View视图。

Vue双向数据绑定的原理是数据劫持+订阅发布模式实现

数据劫持

数据劫持指的是在访问或者修改某个属性时,通过Object.defineProperty()或者Proxy对象拦截这个行为,扩展额外的操作和行为然后返回结果。

Vue2中使用的是Object.defineProperty(),Vue3中使用的是Proxy对象的方式。

订阅发布模式

定义:对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有与该对象产生依赖关系的对象都会接收到通知。

优点:耦合性低,便于维护

缺点:创建订阅者可能会消耗一定时间和内存,但是订阅事件不一定会发生,订阅者则会一直存在于内存中。

Vue的双向数据绑定源码解析

源码分析入口点

在之前的源码分析中我们知道,Vue的构造函数中实现了一个_init()方法,这个方法是用来初始化一些选项的:

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') } //这里初始化了Vue传入的选项 this._init(options) }

上面的_init(options)方法的实现在src/core/instance/init.js中的initMixin(Vue)中,分析其中的源码,我们可以根据方法名称做一下简单的判断:

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 */ 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 vm._isVue = true // merge 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) // 初始化渲染器$slots scopedSlots、_c、$createElement initRender(vm) // 调用生命周期钩子函数 callHook(vm, 'beforeCreate') // 获取注入的数据 initInjections(vm) // resolve injections before data/props // 初始化状态props、methods、data、computed、watch 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) } // 如果存在el则执行$mount if (vm.$options.el) { vm.$mount(vm.$options.el) } } }

通过分析initMixin(Vue)我们可以得到一个大概的结果,那就是在Vue初始化的时候我们完成了生命周期、事件、渲染器、状态的初始化,同时还获取了祖先组件注入的数据,同时为后代组件的注入提供了数据。 其中initState(vm)方法则是对数据还有其他东西的一些初始化操作,跳入该方法中查看一下其内容src/core/instance/state.js

我们逐行分析一下该方法的实现把: export function initState (vm: Component) { // 在当前实例中创建了一个watcher的空数组 vm._watchers = [] // 保存了当前Vue实例的选项options const opts = vm.$options // 如果选项中的props存在的化则初始化props if (opts.props) initProps(vm, opts.props) // 如果选项中methods存在则初始化方法 if (opts.methods) initMethods(vm, opts.methods) // 如果选项中的data存在则进行data的初始化操作 // data的处理,响应化处理 if (opts.data) { // 一般情况下我们初始化Vue实例的时候都会传入data,所以大部分情况是走这个方法的 // 初始化data initData(vm) } else { // 数据响应化 observe(vm._data = {}, true /* asRootData */) } // 初始化computed if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { // 初始化watch initWatch(vm, opts.watch) } }

初始化数据initData

分析上面这段代码我们可以知道,在initState(vm)中是对数据进行了初始化的(先考虑传入data的情况),那么我们继续顺着代码往下看:

function initData (vm: Component) { // 取出data let data = vm.$options.data // 判断data是否为方法,如果是一个方法,则处理完毕后返回 data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} //如果data不是一个object则警告 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 ) } // proxy data on instance // 分别取出data的key,选项中的props、methods const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length // 进行各种判断是否重复之类的警告,不是核心代码,不过多赘述 while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } 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)) { proxy(vm, `_data`, key) } } // observe data // 数据的响应化,遍历开始 observe(data, true /* asRootData */) } 上面代码,我们发现了一个核心的方法:observe(),这个方法曾在initState()中出现过,所以,我们初始化数据的方法归根结底会落在observe()上面代码,我们发现了一个核心的方法:observe

observe:返回一个Observer

废话不多说,直接上代码:from src/core/observer/index.js

//该方法,接收了vue实例中的data数据,和一个boolean,返回了一个Observer(观察者)实例 export function observe (value: any, asRootData: ?boolean): Observer | void { //如果data数据不是一个对象或者是一个虚拟domVNode,直接结束 if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 如果当前对象已经存在observer则返回 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 已经存在的Observer则会保存在value.__ob__中 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++ } // 无论结果如何,最终都会返回一个observer return ob } 通过分析上述代码我们可以得出一个结论**observe()**的作用就是返回一个Observer的实例。

Observer:判断data类型做处理

所以接下来我们要看一下Observer的构造方法完成了什么事情fromsrc/core/observer/index.js

//Observer的构造方法 // 接收传入的Vue中的data数据 constructor (value: any) { this.value = value // 新建了一个Dep的实例,这个Dep是用来做依赖收集的,后面会用到 this.dep = new Dep() //当前的vmCount数量 this.vmCount = 0 // 给当前data对象定义了一个__ob__属性 def(value, '__ob__', this) // 判断当前对象是否为数组,如果是数组单独处理, if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else {// 如果不是数组的话 //普通对象则用walk遍历 this.walk(value) } } walk (obj: Object) { // 拿出obj(data对象)的key const keys = Object.keys(obj) // 对所有的key进行遍历 for (let i = 0; i < keys.length; i++) { // 实现数据响应式 // 传入data对象和当前key,进行响应化处理 defineReactive(obj, keys[i]) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } 分析Observer的构造函数,我们知道了Observer的核心功能:判断data对象是不是一个数组,根据判断进行不同的响应化处理

当data为对象时

当data为对象时,walk()方法遍历data的key,执行defineReactive()方法,下面看一下defineReactive方法的实现,在src/core/observer/index.js中 export function defineReactive ( obj: Object,// 接收一个object,就是vue实例的data key: string,// data的key val: any, customSetter?: ?Function, shallow?: boolean ) { // 这里传进来的是data对象,所以没有一个data对象就有一个dep与之对应 const dep = new Dep() // 查看对象上是否有该属性,如果没有则停止执行 const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 获取属性的getter和setter // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 如果当前值是一个对象,则递归调用 let childOb = !shallow && observe(val) // 定义数据的拦截 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 依赖收集,这里的Dep.target是watcher的实例 if (Dep.target) { dep.depend()// 追加依赖关系,简单来说就是将watcher加入到dep中,但是实际操作要复杂一点 // 如果childOb存在,说明该属性是一个对象 if (childOb) { // 继续追加依赖 childOb.dep.depend() // 如果是数组,继续处理 if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 如果getter存在则调用getter否则返回当前val const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ // 如果新值和老值相同 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存在则执行setter执行更新,否则用新值覆盖老值 if (setter) { setter.call(obj, newVal) } else { // 更新本地的val val = newVal } // 如果新传入的val是一个数组的化,则递归进行响应话处理observe childOb = !shallow && observe(newVal) // 通知更新 dep.notify() } }) }

简单总结一下defineReactive() ♣ 通过Object.defineProperty方法对data属性的set和get方法进行数据劫持 ♣ 创建Dep实例,每有一个data属性则有一个dep与之对应 ♣ 扩展了data属性的get方法,将Dep.target静态属性中的watcher加入到dep实例中(依赖收集过程) ♣ 扩展了data属性中的set方法,当数据被更新时,执行dep.notify()方法通知数据更新

所以我们顺着代码看一下依赖收集的过程和通知更新的方法

依赖收集

依赖收集的过程通过**dep.depend()**完成,我们来看一下它的实现from src\core\observer\dep.js,由于代码较多,我们只粘贴关键代码

export default class Dep{ static target: ?Watcher;// 静态属性中的watcher实例 id: number; subs: Array<Watcher>;// 维护了一个watcher数组 depend () { // 这里的Dep.target是watcher的实例, if (Dep.target) { // 建立和watcher之间的关系,将当前Dep实例加入watcher中 Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { // 通知watcher进行数据更新 subs[i].update() } } } // 将当前watcher实例赋值到Dep.target的静态属性上 export function pushTarget (target: ?Watcher) { targetStack.push(target) // 将watcher实例赋值到Dep的静态属性上 Dep.target = target } 总结dep的作用:维护了一个watcher数组,实现了watcher的增加删除和通知更新

上面代码中的Dep.target.addDep(this),是将当前的Dep实例加入到了watcher实例中,这里有一个细节:理论上是每有一个Data则有一个Dep,当同一个Data多次被调用的时候,只需要创建多个watcher对其进行监听,然后Dep进行依赖收集,通知watcher更新,所以理论上Dep和watcher是一对多的关系. 但是上面的代码是将Dep实例添加到了Wtacher中,所以这就形成了多对多的关系.出现这种情况是因为真正使用的时候,有的时候一个组件

Watcher的实现

这里的Watcher主要是讲Render Watcher,组件实例化的时候会产生一个Watcher的实例,在组件$mount过程中的mountComponent()方法中new Watcher: 这里只粘贴部分核心代码

// 定义组件更新函数 // _render()执行可以获得虚拟dom,VNode // _update()将虚拟DOM转换为真实DOM updateComponent = () => { vm._update(vm._render(), hydrating) } // 创建Watcher实例 //vm:当前vue实例 //updateComponent:组件更新函数 // noop: // {}:回调函数 //true:是否是浏览器的watcher new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)

上面的updateComponent()函数调用了Vue._render函数,最终会调用data属性的get函数,最终完成依赖收集。

Watcher对象的实现:

只粘贴部分核心代码

constructor(){ // watcher创建的时候会执行当前watcher实例的get函数,这样会出发依赖收集的过程 this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. */ get () { //将当前watcher实例赋值到Dep.target静态属性中 pushTarget(this) let value // 当前vue实例 const vm = this.vm try { // getter函数是上面的updateComponent()函数,会触发依赖收集过程 value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } // 清空Dep.target静态属性中的watcher实例 popTarget() this.cleanupDeps() } return value } /** * Add a dependency to this directive. */ //依赖收集 addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { // 维护了一个depid映射关系 this.newDepIds.add(id) this.newDeps.push(dep) // 如果当前dep里面没有watcher,则将该watcher加入到dep中建立联系 if (!this.depIds.has(id)) { // dep中维护了一个watcher的数组 // 将当前watcher加入到dep中的watcher数组中,实现dep对watcher的收集 dep.addSub(this) } } } /** * Subscriber interface. * Will be called when a dependency changes. */ // 实现数据更新 update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { // 将当前watcher实例push到更新队列中实现数据的更新 queueWatcher(this) } } 总结watcher的作用:解析传入的updateComponent更新函数并进行依赖收集。每个组件都会有一个Watcher与之对应,数值变化会触发更新函数进行重新渲染。

当data为数组时,进行数组响应化处理

根据Observer的构造方法得知,当data为数组时

constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 // 给当前对象定义一个__ob__属性 def(value, '__ob__', this) // 判断当前data对象是否为数组 if (Array.isArray(value)) { if (hasProto) { // 覆盖数组的原型方法 protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } // 如果是数组则进行数组响应化的处理 this.observeArray(value) } else { //普通对象则用walk遍历 this.walk(value) } } // 数组响应化处理 observeArray (items: Array<any>) { //遍历数组 for (let i = 0, l = items.length; i < l; i++) { // 取出数组的每一项进行响应化处理 observe(items[i]) } }

上述代码的核心功能主要是,protoAugment()方法扩展了当前data数组的原型方法,arrayMethods 直接上核心代码

//src/core/observer/array.js: const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method // 取出数组的原型方法 const original = arrayProto[method] // 拦截,添加额外行为 // arrayMethods:数组的原型对象,定义特殊方法 def(arrayMethods, method, function mutator (...args) { // 执行原先的任务 const result = original.apply(this, args) // 额外任务:通知更新 // 从this.__ob__中取出观察者 const ob = this.__ob__ let inserted // 以下三个操作需要额外处理 // 如果是新添加的元素,还需要额外的做响应化的处理 switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 如果inserted存在说明元素是新添加的,额外响应化的处理 if (inserted) ob.observeArray(inserted) // notify change // 核心:添加通知更新方法,每一个ob中都有一个dep和这个对象或者数组对应 ob.dep.notify() return result }) }) 总结一下数组的响应化实现:通过扩展了数组原型的七个方法,实现了数组每一项的响应化,从而实现数组的响应化。

♣ 注意,通过上面的代码我们可以看出,只有通过扩展的这七个方法才能实现数组的响应化:pop、push、shift、unshift、splice、sort、reverse

总结

因为是自己边分析源码边写的一些东西,所以可能有点乱。为了捋清思路做了张图片,聊胜于无吧:

源码加载运行流程

下面附上一张官方数据响应化的工作流程图:

图片来源:https://vuejs.org/v2/guide/reactivity.html

响应式的基本机制:

通过Object.defineProperty()进行数据劫持,扩展对象属性的set和get方法watcher执行getter方法触发对象属性的get方法进行依赖收集输入写入时触发对象属性的set方法,dep发布通知,watcher进行数据更新

附上一张我理解的数据响应化流程图:

以上,根据自己对源码的理解,和网上一些大神的分析整理出来的,如有不对的地方,欢迎各位大神指正。

最新回复(0)