从源码解析vue的响应式原理
栏目: JavaScript · 发布时间: 6年前
内容简介:vue官方对响应式原理的解释:深入响应式原理总结下官方的描述,大概分为一下几点: 然鹅,官方的介绍只是一个大致的流程,我们还是不知道vue到底是怎样给data的每个属性设置getter、setter方法?对象属性和数组属性的实现又有什么不同?怎样实现依赖的收集和依赖的触发? 想要搞清楚这些,不得不看一波源码了。下面,请跟我从vue源码分析vue的响应式原理
vue官方对响应式原理的解释:深入响应式原理
总结下官方的描述,大概分为一下几点:
- 组件实例有自己的watcher对象,用于记录数据依赖
- 组件中的data的每个属性都有自己的getter、setter方法,用于收集依赖和触发依赖
- 组件渲染过程中,调用data中的属性的getter方法,将依赖收集至watcher对象
- data中的属性变化,会调用setter中的方法,告诉watcher有依赖发生了变化
- watcher收到依赖变化的消息,重新渲染虚拟dom,实现页面响应
然鹅,官方的介绍只是一个大致的流程,我们还是不知道vue到底是怎样给data的每个属性设置getter、setter方法?对象属性和数组属性的实现又有什么不同?怎样实现依赖的收集和依赖的触发? 想要搞清楚这些,不得不看一波源码了。下面,请跟我从vue源码分析vue的响应式原理
--- 下面我要开始我的表演了---
实例初始化阶段
vue源码的 instance/init.js 中是初始化的入口,其中初始化分为下面几个步骤:
//初始化生命周期 initLifecycle(vm) //初始化事件 initEvents(vm) //初始化render initRender(vm) //触发beforeCreate事件 callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props //初始化状态,!!!此处划重点!!! initState(vm) initProvide(vm) // resolve provide after data/props //触发created事件 callHook(vm, 'created') 复制代码
其中划重点的 initState() 方法中进行了 props、methods、data、computed以及watcher的初始化。在instance/state.js中可以看到如下代码。
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options //初始化props if (opts.props) initProps(vm, opts.props) //初始化methods if (opts.methods) initMethods(vm, opts.methods) //初始化data!!!再次划重点!!! if (opts.data) { initData(vm) } else { //即使没有data,也要调用observe观测_data对象 observe(vm._data = {}, true /* asRootData */) } //初始化computed if (opts.computed) initComputed(vm, opts.computed) //初始化watcher if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } 复制代码
划重点的initData()方法中进行了data的初始化。代码依旧在instance/state.js中可以看到。initData()方法代码如下(删节版)。
/* 初始化data */ function initData (vm: Component) { //判断data是否是一个对象 if (!isPlainObject(data)) { ... } //判断data中的属性是否和method重名 if (methods && hasOwn(methods, key)) { ... } //判断data中的属性是否和props重名 if (props && hasOwn(props, key)) { ... } //将vm中的属性转至vm._data中 proxy(vm, `_data`, key) //调用observe观测data对象 observe(data, true /* asRootData */) } 复制代码
initData()函数中除了前面一系列对data的判断之外就是数据的代理和observe方法的调用。其中数据代 proxy(vm, `_data`, key)
作用是将vm的属性代理至vm._data上,例如:
//代码如下 const per = new VUE({ data:{ name: 'summer', age: 18, } }) 复制代码
当我们访问 per.name
时,实际上访问的是 per._data.name
而下面一句 observe(data, true /* asRootData */)
才是响应式的开始。
小结
总结一下初始化过程大概如下图
响应式阶段
observe函数的代码在observe/index.js,observe是一个工厂函数,用于为对象生成一个Observe实例。而真正将对象转化为响应式对象的是observe工厂函数返回的Observe实例。
Observe构造函数
Observe构造函数代码如下(删减版)。
export class Observer { constructor (value: any) { //对象本身 this.value = value //依赖收集器 this.dep = new Dep() this.vmCount = 0 //为对象添加__ob__属性 def(value, '__ob__', this) //若对象是array类型 if (Array.isArray(value)) { ... } else { //若对象是object类型 ... } } 复制代码
从代码分析,Observe构造函数做了三件事:
- 为对象添加
__ob__
属性,__ob__
中包含value数据对象本身、dep依赖收集器、vmCount。数据经过这个步骤以后的变化如下:
//原数据 const data = { name: 'summer' } //变化后数据 const data = { name: 'summer', __ob__: { value: data, //data数据本身 dep: new Dep(), //dep依赖收集器 vmCount: 0 } } 复制代码
- 若对象是array类型,则进行array类型操作
- 若对象是object类型,则进行object类型操作
数据是object类型
当数据是object类型时,调用了一个walk方法,在walk方法中遍历数据的所有属性,并调用defineReactive方法。defineReactive方法的代码仍然在observe/index.js中,删减版如下:
export function defineReactive (...) { //dep存储依赖的变量,每个属性字段都有一个属于自己的dep,用于收集属于该字段的依赖 const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } //缓存原有的get、set方法 const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } // 为每个属性创建childOb,并且对每个属性进行observe递归 let childOb = !shallow && observe(val) //为属性加入getter/setter方法 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { ... }, set: function reactiveSetter (newVal) { ... }) } 复制代码
defineReactive方法主要做了以下几件事:
__ob__
经过defineReactive处理的数据变化如下, 每个属性都有自己的dep、childOb、getter、setter,并且每个object类型的属性都有 __ob__
//原数据 const data = { user: { name: 'summer' }, other: '123' } //处理后数据 const data = { user: { name: 'summer', [name dep,] [name childOb: undefined] name getter,//引用name dep和name childOb name setter,//引用name dep和name childOb __ob__:{data, user, vmCount} }, [user dep,] [user childOb: user.__ob__,] user getter,//引用user dep和user childOb user setter,//引用user dep和user childOb other: '123', [other dep,] [other childOb: undefined,] other getter,//引用other dep和other childOb other setter,//引用other dep和other childOb __ob__:{data, dep, vmCount} } 复制代码
刚刚讲到defineReactive函数的最后一步是每一个属性都加上getter、setter方法。那么getter和setter函数到底做了什么呢?
getter方法中:
getter函数内部代码如下:
get: function reactiveGetter () { //调用原属性的get方法返回值 const value = getter ? getter.call(obj) : val //如果存在需要被收集的依赖 if (Dep.target) { /* 将依赖收集到该属性的dep中 */ dep.depend() if (childOb) { //每个对象的obj.__ob__.dep中也收集该依赖 childOb.dep.depend() //如果属性是array类型,进行dependArray操作 if (Array.isArray(value)) { dependArray(value) } } } return value }, 复制代码
getter方法主要做了两件事:
- 调用原属性的get方法返回值
- 收集依赖
- Dep.target表示一个依赖,即观察者,大部分情况下是一个依赖函数。
- 如果存在依赖,则收集依赖到该属性的dep依赖收集器中
- 如果存在childOb(即属性是对象或者数组),则将该依赖收集到childOb也就是
__ob__
的依赖收集器__ob__.dep
中,这个依赖收集器在使用$set 或 Vue.set 给属性对象添加新属性时触,也就是说Vue.set 或 Vue.delete 会触发__ob__.dep
中的依赖。 - 如果属性的值是数组,则调用dependArray函数,将依赖收集到数组中的每一个对象元素的
__ob__.dep
中。确保在使用$set 或 Vue.set时,数组中嵌套的对象能正常响应。代码如下:
//数据 const data = { user: [ { name: 'summer' } ] } // 页面显示 {{user}} <Button @click="addAge()">addAge</Button> //addAge方法,为数组中的嵌套对象添加age属性 change2: function(){ this.$set(this.user[0], 'age', 18) } 复制代码
//dependArray函数 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) } } } 复制代码
//转化后数据 const data = { user: [ { name: 'summer', __ob__: {user[0], dep, vmCount} } __ob__: {user, dep, vmCount} ] } 复制代码
dependArray的作用就是将user的依赖收集到它内部的user[0]对象的 __ob__.dep
中,使得进行addAge操作时,页面可以正常的响应变化。
setter方法中:
setter函数内部代码如下:
set: function reactiveSetter (newVal) { // 为属性设置正确的值 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() } if (setter) { setter.call(obj, newVal) } else { val = newVal } //由于属性的值发生了变化,则为属性创建新的childOb,重新observe childOb = !shallow && observe(newVal) //在set方法中执行依赖器中的所有依赖 dep.notify() } }) 复制代码
setter方法主要做了三件事:
- 为属性设置正确的值
- 由于属性的值发生了变化,则为属性创建新的childOb,重新observe
- 执行依赖器中的所有依赖
数据是纯对象类型的处理讲完了,下面看下数据是array类型的操作。
数据是array类型
observer/index.js中对array处理的部分:
if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment //拦截修改数组方法 augment(value, arrayMethods, arrayKeys) //递归观测数组中的每一个值 this.observeArray(value) } 复制代码
当数据类型是array类型时
- 使用protoAugment方法为数据指定构造函数
__proto
为arrayMethods,出于兼容性考虑如果浏览器不支持__proto__
,则使用arrayMethods重写数组数据中的所有相关方法。 - 递归观测数组中的每一个值
arrayMethods拦截修改数组方法
arrayMethods中的定义在observe/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] //拦截修改数组的方法,当修改数组方法被调用时触发数组中的__ob__.dep中的所有依赖 def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } //对新增元素使用observeArray进行观测 if (inserted) ob.observeArray(inserted) //触发__ob__.dep中的所有依赖 ob.dep.notify() return result }) }) 复制代码
在arrayMethods中做了如下几件事:
__ob__.dep
observeArray递归观测数组中的每一项
observeArray代码如下:
observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } 复制代码
在observeArray方法,对数组中的所有属性进行observe递归。然而这里有一个问题就是无法观测数组中的所有非Object的基本类型。observe方法的第一句就是
if (!isObject(value) || value instanceof VNode) { return } 复制代码
也就是说数组中的非Object类型的值是不会被观测到的,如果有数据:
const data = { arr: [{ test: 0 }, 1, 2], } 复制代码
此时如果改变arr[0].test=3可以被触发响应,而改变arr[1]=4不能触发响应,因为observeArray观测数据中的每一项时,observe(arr[0])是一个观测一个对象可以被观测。observe(arr[1])时观测一个基本类型数据,不可以被观测。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- flask 源码解析6:响应
- 从源码解析vue的响应式原理-响应式的整体流程
- Vue 源码(一):响应式原理
- vue响应式系统源码解析
- Vue 源码解读-数据响应系统
- Vue源码分析系列五: 响应式原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Data Structures and Algorithms
Alfred V. Aho、Jeffrey D. Ullman、John E. Hopcroft / Addison Wesley / 1983-1-11 / USD 74.20
The authors' treatment of data structures in Data Structures and Algorithms is unified by an informal notion of "abstract data types," allowing readers to compare different implementations of the same......一起来看看 《Data Structures and Algorithms》 这本书的介绍吧!