从源码解析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源码分析系列五: 响应式原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
UNIX编程艺术
Eric S. Raymond / 姜宏、何源、蔡晓俊 / 电子工业出版社 / 2011-1 / 69.00元
本书主要介绍了Unix系统领域中的设计和开发哲学、思想文化体系、原则与经验,由公认的Unix编程大师、开源运动领袖人物之一Eric S. Raymond倾力多年写作而成。包括Unix设计者在内的多位领域专家也为本书贡献了宝贵的内容。本书内容涉及社群文化、软件开发设计与实现,覆盖面广、内容深邃,完全展现了作者极其深厚的经验积累和领域智慧。一起来看看 《UNIX编程艺术》 这本书的介绍吧!