xingbofeng.github.io
xingbofeng.github.io copied to clipboard
Vue源码学习笔记之observer与变异方法
这一篇主要是相对src/core/observer/
这一块进行剖析。主要作用为MVVM
框架在数据层面的观察,之后通知DOM
刷新的部分。
文档里面对于这一部分的原理是这么说的:
受现代JavaScript
的限制(以及废弃 Object.observe
),Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter
转化过程,所以属性必须在 data 对象上存在才能让 Vue
转换它,这样才能让它是响应的。
所以observer
需要做的事情是:
遍历一个对象/数组
是否应该遍历它?
虽然我们希望数据都是响应式的。然而,在某些情况下(v-for
循环中或props
传递的数据),我们并不希望人为改变其中的数据(通常Vue
会给出一个警告)。因而需要在遍历一个对象之前,设定这个状态是否应该被响应式触发:
/**
* By default, when a reactive property is set, the new value is
* also converted to become reactive. However when passing down props,
* we don't want to force conversion because the value may be a nested value
* under a frozen data structure. Converting it would defeat the optimization.
*/
export const observerState = {
shouldConvert: true,
isSettingProps: false
}
如何遍历它?
遍历对象/数组通常采用的是递归遍历,这一点众所周知。在Observer
里有两个方法:walk
和observeArray
。他们分别用于遍历对象和数组。
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
为何我们要遍历它?
文档里面对这一部分解释得很清楚:
把一个普通 JavaScript
对象传给 Vue
实例的 data
选项,Vue
将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter
。Object.defineProperty
是仅 ES5
支持,且无法 shim 的特性,这也就是为什么 Vue
不支持 IE8
以及更低版本浏览器的原因。
变异方法
什么是变异方法
之前初学Vue
的时候踩过一个坑,相信这个坑,所有初学Vue
读者也都经历过:更改数组的某一项,并不会触发视图更新?这是为什么?
文档上边对此解释为变异方法。
其实Vue
所做的事情是改写数组的push
、pop
等方法,让他们在执行之后通知Vue
,因而如果不使用变异方法
进行数组更新,这样的改变是不会被Vue
所监听得到的!在使用数组的变异方法时,除了触发本方法,还会触发一个回调:通知Vue
,我已更新!
在src/core/observer/index.js
最开始引入数组的变异方法:
import { arrayMethods } from './array'
变异方法的改写
下面来分析变异方法的改写:
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
/**
* Intercept mutating methods and emit events
*/
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// 在这里拿到数组的原型
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// 之后对传入的参数放入args数组内
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
我们这里要做到的是:对于数组每个元素的变化,我们都要做到让它是可响应的,这一点是至关重要的。
而对于数组的变异方法push
、pop
、shift
、unshift
、splice
、sort
、reverse
。只有push
、unshift
和splice
是有新的元素添加入了原数组。因此我们需要对其做特殊处理!
特殊处理就是:对于每一项,我们都对它执行observeArray
方法,使得Vue
能够响应它自身的变化(也就是通过Object.defineProperty
为之添加getter/setter
方法)。
对于splice
方法,其添加的参数在第二位。因而inserted = args.slice(2)
。
至此,变异方法的分析大概就到这里。
如何做到响应?
Observer的constructor
我们先分析这样一段代码:
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
Observer
的作用是使得传入的对象/数组是相应的,这样我们才能够去实现Vue
的VM
与Model
之间的双向绑定。
如何observer一个数组?
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
下面来看observe方法:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value)) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe
先对传入的数组作一个判断:如果不是引用类型,则返回,如果其原型上已有__ob__
实例(即其已经被observe
过了),则返回。否则就去做递归,使得其每一子项都是可观察的:ob = new Observer(value)
。
如何observer一个对象?
在Vue
执行this.walk(value)
时,会对其对象每一项进行递归遍历,并对每一项执行defineReactive(obj, keys[i], obj[keys[i]])
,使得对象是可响应的。
下面来看defineReactive
的实现方式,由于量比较大,直接贴一段我写过注释的代码:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
// Dep有两个属性:id和subs(一个数组,用于存放它的观察者),
// 通过发布 — 订阅模式实现双向绑定:
// dep为发布者,他的subs属性用来存放了订阅他的观察者
const dep = new Dep()
// getOwnPropertyDescriptor返回指定对象上一个自有属性对应的属性描述符。
//(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性,如getter/setter)
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果它已经被赋予了getter/setter,或者它是一个不可响应的数据(如props中的数据),则直接返回
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 之后我们为其创建getter/setter
const getter = property && property.get
const setter = property && property.set
// 递归遍历,并对每一项执行defineReactive,使得对象是可响应的。
let childOb = observe(val)
// 定义其原型上的get和set函数,这是Vue设计思想的关键:使之可相应,同时通知视图层的刷新
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 为何要判断是否具有getter/setter?
// 这里如果没有getter,其实这个值是一个基本类型,我们直接返回这个值就好
// 否则我们执行其原型上的getter
const value = getter ? getter.call(obj) : val
// target其实是Vue内部的数组改变触发的getter,如果不是Vue内部数据改变导致的(如手动的DOM刷新)
// 这时直接返回value就好
if (Dep.target) {
// 执行depend的目的是将其添加到订阅dep的观察者中
// 一旦此数据改变,getter到这个数据的Dep.target也知晓了这一个变化!
// 发布这个改变,订阅dep的观察者也会改变!
dep.depend()
if (childOb) {
// 如果其为对象,则让其也depend
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 */
// 判断是否相等,如果相等我们根本没必要浪费性能去触发一次setter!
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
// 如果是生产环境则触发另一种setter(不是dev环境,节省性能)
customSetter()
}
if (setter) {
// 如果其子元素有setter(不是基本类型),则触发setter
setter.call(obj, newVal)
} else {
// 是基本类型返回就好
val = newVal
}
// 观察其子元素
childOb = observe(newVal)
// notify通知订阅这个数据的元素也要发生改变!
dep.notify()
}
})
}
总结
其实整个过程也正如文档里面所说的,递归,为每个引用类型添加getter/setter,对于数组的profill
是为其添加变异方法来进行相应其数据的变化。对于对象我们只能做直接替换!(不能做某一项的改变,我们要刷一个对象的方式,文档里也有写到,只能类似于redux
改变数据的方式,使用Object.assign
,当然你也可以使用immutable
)。
对于getter/setter
,同样也需要对每一项进行递归的发布 - 订阅其主要为依赖于Dep
对象的发布 - 订阅模式,对于getter
,一旦订阅到这一个变化,还会去发布一个自身已经改变的状态给订阅其的数据。即源码中的dep.depend()
。对于setter
,一旦一个数据触发其set
方法,Vue
便会发布消息,通知订阅这个数据的元素也要发生改变。即源码中的dep.notify()
请问 let ob: Observer | void 这段函数 是什么意思???有些不明白这个写法
splice 其添加的参数在第二位? 不应该是 第三位才是添加的元素么