Skip to content

Commit

Permalink
数据劫持
Browse files Browse the repository at this point in the history
  • Loading branch information
wuyawei committed Apr 7, 2019
1 parent 81a5af1 commit e2d89dd
Showing 1 changed file with 226 additions and 6 deletions.
232 changes: 226 additions & 6 deletions vue/mvvm/1.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

但是对于刚接触或者了解不多的同学来说,可能还会感到困惑:为什么不能检测到对象属性的添加或删除?为什么不支持通过索引设置数组成员?相信看完本期文章,你一定会豁然开朗。

本文会针对整个响应式原理一步步深入,这样就算是刚接触的同学也能跟着看懂。当然,如果你已经对 `Object.defineProperty` 做数据劫持有一些认识和了解,大可以 **直接前往实现部分 [MVVM](#MVVM)**
本文会结合 Vue 源码,针对整个响应式原理一步步深入,这样就算是刚接触的同学也能跟下来。当然,如果你已经对响应式原理有一些认识和了解,大可以 **直接前往实现部分 [MVVM](#MVVM)**

**文章仓库 [🍹🍰 fe-code](https://github.com/wuyawei/fe-code),欢迎 star**
**文章仓库和源码都在 [🍹🍰 fe-code](https://github.com/wuyawei/fe-code),欢迎 star**

Vue 官方的响应式原理图镇楼。

Expand Down Expand Up @@ -79,13 +79,13 @@ setTimeout(_ => {
},1000);
```
当然 Vue 也 提供了 `vm.$set( target, key, value )` 方法来解决特定情况下添加属性的操作,但是我们这里不太适用。
## Object.defineProperty
> Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。— MDN
## Vue 响应式原理
前面我们讲了两个具体例子,举了易犯的错误以及解决办法,但是我们依然只知道应该这么去做,而不知道为什么要这么去做。

Vue 的数据劫持依赖于 `Object.defineProperty`,所以也正是因为它的某些特性,才引起这个问题。不了解这个属性的同学看这里 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
### 简单实现
### Object.defineProperty 基础实现
> Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。— MDN
看一个基础的数据劫持的栗子,这也是响应式最根本的依赖。
``` javascript
function defineReactive(obj, key, val) {
Expand Down Expand Up @@ -125,7 +125,227 @@ arr[0] = 'oh nanana'; // set
```
那么 Vue 为什么不这么处理呢?尤大官方回答是性能问题。关于这个点更详细的分析,各位可以移步 [Vue为什么不能检测数组变动?](https://segmentfault.com/a/1190000015783546)
### Vue 源码实现
> 以下代码 Vue 版本为:2.6.10。
#### observer
我们知道了数据劫持的基础实现,顺便再看看 Vue 源码是如何做的。
``` javascript
// observer/index.js
// 数据劫持前的判断方法
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 是否是对象或者虚拟dom
return
}
let ob: Observer | void
// 判断是否有 __ob__ 属性,有的话代表有 Observer 实例,直接返回,没有就创建 Observer
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
) {
ob = new Observer(value) // 创建Observer
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

// Observer 实例
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep() // 给 Observer 添加 Dep 实例
this.vmCount = 0
// 为被劫持的对象添加__ob__属性,指向自身 Observer 实例。作为是否 Observer 的唯一标识。
def(value, '__ob__', this)
if (Array.isArray(value)) { // 判断是否是数组
if (hasProto) { // 判断是否支持__proto__属性,用来处理数组方法
protoAugment(value, arrayMethods) // 继承
} else {
copyAugment(value, arrayMethods, arrayKeys) // 拷贝
}
this.observeArray(value) // 劫持数组成员
} else {
this.walk(value) // 劫持对象
}
}

walk (obj: Object) { // 只有在值是 Object 的时候,才用此方法
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 数据劫持方法
}
}

observeArray (items: Array<any>) { // 如果是数组,则调用 observe 处理数组成员
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // 依次处理数组成员
}
}
}
```
上面需要注意的是 `__ob__` 属性,避免重复创建。然后 Vue 将对象和数组分开处理,数组只深度监听了对象成员,这也是之前说的导致不能直接操作索引的原因。但是数组的一些方法是可以正常响应的,比如 push、pop 等,这便是因为上述判断响应对象是否是数组时,做的处理,我们来看看具体代码。
``` javascript
// observer/index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

// export function observe 省略部分代码
if (Array.isArray(value)) { // 判断是否是数组
if (hasProto) { // 判断是否支持__proto__属性,用来处理数组方法
protoAugment(value, arrayMethods) // 继承
} else {
copyAugment(value, arrayMethods, arrayKeys) // 拷贝
}
this.observeArray(value) // 劫持数组成员
}
// ···

// 直接继承 arrayMethods
function protoAugment (target, src: Object) {
target.__proto__ = src
}
// 依次拷贝数组方法
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}

// util/lang.js def 方法长这样,用来给对象添加属性
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
```
可以看到关键点在 `arrayMethods`上,我们再继续看:
``` javascript
// observer/array.js
import { def } from '../util/index'

const arrayProto = Array.prototype // 存储数组原型上的方法
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__ // 拿到该数组的 ob 实例
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2) // splice 接收的前两个参数是下标
break
}
if (inserted) ob.observeArray(inserted) // 原数组的新增部分需要重新 observe
// notify change
ob.dep.notify() // 手动发布
return result
})
})
```
由此可见,Vue 重写了部分数组方法,并且在调用这些方法时,做了手动发布。但是 Vue 的数据劫持部分我们还没有看到,在第一部分的 observer 函数的代码中,有一个 defineReactive 方法,我们来看看:
``` javascript
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() // 实例一个 Dep 实例

const property = Object.getOwnPropertyDescriptor(obj, key) // 获取对象自身属性
if (property && property.configurable === false) { // 没有属性或者属性不可写就没必要劫持了
return
}

// 兼容预定义的 getter/setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val) // 需要深度Watch就再次监听val,从 observe 开始,走完一套流程
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // 执行预设的getter获取值
if (Dep.target) { // 依赖收集的关键
dep.depend() // 依赖收集
if (childOb) { // 如果有深度属性,则添加同样的依赖
childOb.dep.depend()
if (Array.isArray(value)) { // value 是数组的话调用数组的方法
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 原有值和新值比较,值一样则不做处理
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) { // 执行预设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() // 如果数组成员有 __ob__,则添加依赖
if (Array.isArray(e)) { // 数组成员还是数组,递归调用
dependArray(e)
}
}
}
```
#### Dep
在上面的分析中,我们弄懂了 Vue 的数据劫持以及数组方法重写,但是又有了新的疑惑。
## Proxy
## MVVM
### 概念

0 comments on commit e2d89dd

Please sign in to comment.