1 概述
vue 的响应式原理次要基于:数据劫持、依赖收集和异步更新,通过对象劫持来做 依赖的收集 和 数据变动的侦测,通过维持一个队列来异步更新视图。
1.2 什么是对象劫持?
在 JavaScript 中,对象作为一种 key/value 键值对的模式存在,而对对象的根本会有 增、删、改、查 的基本操作:
const dog = { name: 'dog', single: false, girlFriend: 'charm', house: 'villa'}delete dog.housedog.single = truedog.character = 'easy'delete dog.girlFriendconsole.log(dog.name)
而咱们说的对象劫持,就是心愿劫持这些对对象的操作方法,那么先来看看
1.3 怎么劫持?
次要是通过 Object.defineProperty
办法来实现。举个小例子:“如果一个穷小子,有房子就有女朋友,房子没了,那么女朋友也就没了”
const dog = { name: 'dog', single: true, girlFriend: null}_house = nullObject.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// }
1.4 为什么要劫持?
从下面数据劫持的例子能够看出,通过数据劫持,能够通过拦挡某个属性的批改操作,进而去解决这个扭转应该触发的其余值、状态的更新。这个就非常适合解决:数据驱动视图更新。
2 深刻
2.1 Object.defineProperty 的局限
2.1.1 不能检测新增、删除属性操作
Object.defineProperty 无奈做到新增属性的拦挡:
const dog = {}dog.name = 'dog'
vue 官网中也提到:
Vue 无奈检测 property 的增加或移除。因为 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在
data
对象上存在能力让 Vue 将它转换为响应式的。例如:var vm = new Vue({ data: () => ({ a: 1 })})// `vm.a` 是响应式的vm.b = 2// `vm.b` 是非响应式的
因为无奈劫持到 b 这个新增属性,所以即便视图中曾经援用了 b ,视图也不会进行响应式的批改。 Vue 组件实例中提供了 Vue.set
办法来解决对象新增属性的问题。
Object.defineProperty 无奈感知到已有属性的删除:
const dog = {}Object.defineProperty(dog, 'name', { configurable: true, get () { return 'dog' }, set (value) { console.log(value) }})console.log(dog.name) // 'dog'delete dog.nameconsole.log(dog.name) // undefined
defineProperty 的 set 描述符并不能劫持到 delete 操作。所以在 vue 中,会专门提供一个 Vue.delete
办法来删除一个属性。
2.1.2 不能检测数组
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 // 2dogs[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 的属性做拦挡。
2.1.3. 留神应用 Object.assign
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' }
2.2 降级计划 Proxy
Proxy 用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等);上述 Object.defineProperty 的局限都能够通过 Proxy 来解决:
2.2.1 拦挡对象的基本操作
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) }})
2.2.2 拦挡数组原型上的办法
const dogs = []var proxyDog = new Proxy(dogs, { apply (targetFun, ctx, args) { // 拦挡办法调用 return Reflect.apply(targetFun, ctx, args) }})proxyDog.push('easy')
2.3 响应式原理 - Object.defineProperty 在 Vue2 中的使用
了解 Vue2 的响应式原理,能够从三个角度:变动侦测机制、收集依赖、异步更新 来探索:
2.3.1 变动侦测机制 - 数据劫持
在实例化 vue 组件的时候,就会对组件的 props、data 进行 defineReactive
,这个办法是对 Object.defineProperty 的封装,次要做很外围的几件事件:
- 实例化一个依赖治理类 Dep
这是很重要的一点,对象的每个属性都会实例化一个 Dep 类,通过这个类来收集依赖,以及告诉更新
- 通过 Object.defineProperty 劫持 getter ,收集依赖
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 劫持 setter ,告诉依赖更新
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. 告诉依赖更新
2.3.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) } }}
2.3.3 异步更新
记得我刚开始接触 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.jsfunction defineReactive () { // ... Object.defineProperty({ set () { // ... dep.notify() } })}// dep.jsclass 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.jsclass 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') } }}
3 Proxy 在 Vue3 中的使用
// TODO