vue 的响应式原理主要基于:数据劫持、依赖收集和异步更新,经过对象劫持来作 依赖的收集 和 数据变化的侦测,经过维持一个队列来异步更新视图。javascript
在 JavaScript 中,对象做为一种 key/value 键值对的形式存在,而对对象的基本会有 增、删、改、查 的基本操做:html
const dog = {
name: 'dog',
single: false,
girlFriend: 'charm',
house: 'villa'
}
delete dog.house
dog.single = true
dog.character = 'easy'
delete dog.girlFriend
console.log(dog.name)
复制代码
而咱们说的对象劫持,就是但愿劫持这些对对象的操做方法,那么先来看看vue
主要是经过 Object.defineProperty
方法来实现。举个小例子:“若是一个穷小子,有房子就有女友,房子没了,那么女友也就没了”java
const dog = {
name: 'dog',
single: true,
girlFriend: null
}
_house = null
Object.defineProperty(dog, 'house', {
configurable: true,
get: () => _house,
set: (house) => {
if (house) {
_house = house
dog.girlFriend = 'charm'
dog.single = false
} else {
_house = ''
dog.girlFriend = null
dog.single = true
}
}
})
dog.house = 'villa'
// {
// name: 'dog',
// single: false,
// girlFriend: 'charm'
// }
dog.house = null
// {
// name: 'dog',
// single: true,
// girlFriend: null
// }
复制代码
从上面数据劫持的例子能够看出,经过数据劫持,能够经过拦截某个属性的修改操做,进而去处理这个改变应该触发的其余值、状态的更新。这个就很是适合处理:数据驱动视图更新。react
Object.defineProperty 没法作到新增属性的拦截:数组
const dog = {}
dog.name = 'dog'
复制代码
vue 官网中也提到:app
Vue 没法检测 property 的添加或移除。因为 Vue 会在初始化实例时对 property 执行 getter/setter 转化,因此 property 必须在
data
对象上存在才能让 Vue 将它转换为响应式的。例如:domvar vm = new Vue({ data: () => ({ a: 1 }) }) // `vm.a` 是响应式的 vm.b = 2 // `vm.b` 是非响应式的 复制代码
由于没法劫持到 b 这个新增属性,因此即便视图中已经引用了 b ,视图也不会进行响应式的修改。 Vue 组件实例中提供了 Vue.set
方法来解决对象新增属性的问题。异步
Object.defineProperty 没法感知到已有属性的删除:async
const dog = {}
Object.defineProperty(dog, 'name', {
configurable: true,
get () { return 'dog' },
set (value) { console.log(value) }
})
console.log(dog.name) // 'dog'
delete dog.name
console.log(dog.name) // undefined
复制代码
defineProperty 的 set 描述符并不能劫持到 delete 操做。因此在 vue 中,会专门提供一个 Vue.delete
方法来删除一个属性。
const dogs = []
Object.defineProperty(dogs, 0, {
configurable: true,
get: () => 'easy',
set: console.log
})
Object.defineProperty(dogs, 1, {
configurable: true,
get: () => 'poor',
set: console.log
})
dogs.length // 2
dogs[0] // 'easy'
dogs[1] // 'poor'
复制代码
看起来经过 Object.defineProperty 配置的数组元素表现正常,那么试一试操做数组的方法:
dogs.push('newdog') // ['easy', 'poor', 'newdogs']
dogs.unshift('newdog2')
// easy
// newdog2
// 4
// ['easy', 'poor', 'poor', 'newdogs']
复制代码
从这个打印输出来看 push 方法没有问题,可是 unshift 方法却不符合预期;unshift 方法的内部应该是首先将第一个元素赋值给第二,第二个赋值给第三个,以此类推,而后将 newdog2 复制给第一个元素。不过这从原理上说得通的,毕竟咱们只是对 index 为 0 和 1 的属性作拦截。
MDN 官网上提到 Object.assign 方法在执行的时候,仅仅调用属性的 getter、setter 方法,因此在执行过程当中,属性描述符会丢失:
const dog = {}
Object.defineProperty(dog, 'name', {
configurable: true,
enumerable: true,
get () {
return 'dog'
},
set (value) {
console.log(value)
}
})
const dogBackup = Object.assign({}, dog) // { name: 'dog' }
dogBackup.name = 'dogBackup' // { name: 'dogBackup' }
复制代码
Proxy 用于定义基本操做的自定义行为(如属性查找、赋值、枚举、函数调用等);上述 Object.defineProperty 的局限均可以经过 Proxy 来解决:
const dog = {
name: 'dog',
single: true,
girlFriend: null,
house: null
}
const proxyDog = new Proxy(dog, {
get (target, prop, receiver) {
// 拦截查找操做
return Reflect.get(target, prop, receiver)
},
set (target, prop, value, receiver) {
// 拦截新增属性
if (!Reflect.has(target, prop)) {
throw TypeError('Unknown type ' + prop)
}
// 拦截赋值操做
if (prop === 'house') {
if (value) {
Reflect.set(target, 'girlFriend', 'charm', receiver)
Reflect.set(target, 'single', false, receiver)
} else {
Reflect.set(target, 'girlFriend', null, receiver)
Reflect.set(target, 'single', true, receiver)
}
}
return Reflect.set(target, prop, value, receiver)
},
deleteProperty (target, prop) {
// 拦截删除操做
if (prop === 'house') {
Reflect.set(target, 'girlFriend', null)
Reflect.set(target, 'single', true)
}
return Reflect.deleteProperty(target, prop)
}
})
复制代码
const dogs = []
var proxyDog = new Proxy(dogs, {
apply (targetFun, ctx, args) {
// 拦截方法调用
return Reflect.apply(targetFun, ctx, args)
}
})
proxyDog.push('easy')
复制代码
理解 Vue2 的响应式原理,能够从三个角度:变化侦测机制、收集依赖、异步更新 来探究:
在实例化 vue 组件的时候,就会对组件的 props、data 进行 defineReactive
,这个方法是对 Object.defineProperty 的封装,主要作很核心的几件事情:
这是很重要的一点,对象的每一个属性都会实例化一个 Dep 类,经过这个类来收集依赖,以及通知更新
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
})
复制代码
这里问题就来了,咱们正常取值 data.name
或者 this.name
,会触发 reactiveGetter
,可是这时候 Dep.target
确定是不存在的,只有当 Dep.target
存在的时候才进行依赖收集:dep.depend()
。那么何时 Dep.target
才存在呢?dep.depend()
方法作了些什么?
Object.defineProperty({
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 判断是否须要更新
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
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 通知更新
dep.notify()
}
})
复制代码
reactiveSetter 所作的事情就比较简单,主要作了两件事:1. 判断值是否发生变化;2. 通知依赖更新
在收集依赖的过程当中,提出了两个问题:
问题1:Dep.target 何时存在?
首先 target 的类型是一个 Watcher
实例,在一个 vue 组件实例化的时候,会建立一个渲染 watcher ,渲染 watcher 是一个非惰性 watcher,实例化的的时候会当即将 Dep.target 设置成本身;
而在模版编译的时候,即 vm._render
函数执行,经过 with
的方式定义做用域为当前的组件:
with(this){return ${code}}
复制代码
with 语法一般在模版引擎使用,这样在模版编译的时候访问变量时的做用域,都是 with 指定的做用域,这样就能够触发对象属性的 getter
方法了。
Dep.target 存在的条件能够认为是:须要数据来驱动更新;这在 Vue 中体如今:
- 视图渲染
- 计算属性
- $watch 方法
问题2:dep.depend
方法作了什么事情?
在对象属性的 getter 触发时,调用 dep.depend()
方法:
class Dep {
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
复制代码
记得我刚开始接触 Vue 的时候,有人问我:vue 数据驱动视图更新是同步的仍是异步的?若是是异步,怎么实现的?
很显然应该是异步的,在一个事件循环中屡次执行数据更新操做,最终 dom 应该只须要渲染一次便可
<template>
<div>index</div>
</template>
<script> export default { data () { return { index: 0 } }, mounted () { for (let i=0; i<100; i++) { this.index = i } } } </script>
复制代码
在数据更新时会触发 dep.notify
,这会将 watcher 放入队列等待下一次事件循环
// observer/index.js
function defineReactive () {
// ...
Object.defineProperty({
set () {
// ...
dep.notify()
}
})
}
// dep.js
class Dep {
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++) {
subs[i].update()
}
}
}
// watcher.js
class Watcher {
// ...
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
复制代码
queueWatcher 会将当前 watcher push 到更新队列中,而后开始异步更新,以及更新后调触发响应的生命周期事件
function queueWatcher (watcher) {
nextTick(flushSchedulerQueue)
}
function flushSchedulerQueue () {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
// ...
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
复制代码
// TODO