vue响应式原理

vue响应式原理

vue框架 中最核心的就是 vue的响应式 ,通过对vuedata数据的变更实现页面效果的重新渲染。但在实际开发中经常会有人发现明明更改了对应数据的值,但是vue却没有重新渲染。明明说好了是响应式的,但为什么有的数据可以通过响应式实现,而有的只能通过vue.set方法实现

官方文档的流程图

在此基础上,我们根据源码更细一步划分出watcher和data之间的部分,即Depobserver

总体架构简介

在Vue源码内,Dep类作为依赖,Watcher类则用来收集依赖和通知依赖重新求值。对于在实例化时传入的数据,使用工厂函数defineReactive令其响应式。而在实例后再通过Vue.set/vm.$set添加的响应式数据,则需要借助Observer类来使其成为响应式数据,最后也是通过defineReactive实现响应式。

对于每个响应式数据,会有两个Dep实例,第一个是在defineReactive中的闭包遍历,用途显而易见。而第二个Dep则在响应式数组的__ob__属性值中,这个值是Observer实例,其实例属性dep是Dep实例,在执行Vue.set/vm.$set添加响应式数据后,会通知依赖更新。

在讲defineReactive之前,先讲一下这些辅助类的实现和用处。

Dep

我们都知道,Vue响应式的实现,会在getter中收集响应式数据的依赖,在setter中通知依赖数据更新,重新计算数据然后来更新视图。在Vue内部,使用Dep实例表示依赖,让我们看一下Dep类是怎么定义的。

Dep有两个实例属性,一个静态属性。静态属性targetWatcher实例,功能是重新求值和通知视图更新,下文我们会讲到。实例属性id是Dep实例的唯一标识,无需多说;属性subs是Watcher实例数组,用于收集Watcher实例,当依赖更新时,这些Watcher实例就会重新求值。

 static target: ?Watcher;
 id: number;
 subs: Array;
​
 constructor () {
 this.id = uid++
 this.subs = []
 }
​
 addSub (sub: Watcher) {
 this.subs.push(sub)
 }
​
 removeSub (sub: Watcher) {
 remove(this.subs, sub)
 }
​
 depend () {
 if (Dep.target) {
 Dep.target.addDep(this)
 }
 }
​
 notify () {
 // stabilize the subscriber list first
 const subs = this.subs.slice()
 for (let i = 0, l = subs.length; i < l; i++) {
 subs[i].update()
 }
 }
}```

方法`addSub`用于添加`Watcher`实例到`subs`中,方法`removeSub`用于从`subs`移除`Watcher`实例。

方法`depond`会在收集依赖的时候调用,实际上执行了Watcher的实例方法`addDep`,在`addDep`内除了调用dep实例的`addSup`方法外,还做了避免重复收集Watcher实例的工作。这个方法会在Vue为响应式数据设置的自定义getter中执行。

`notify`方法则遍历`subs`,执行Watcher实例方法update来重新求值。这个方法会在Vue为响应式数据设置的自定义setter中执行。

有人可能有疑问,`target`是静态属性,那不是每个实例的target都一样的?实际上,重新求值的操作在Watcher实例方法`get`内实现。在get方法内,会先调用`pushTarget`来更新`Dep.target`,使其指向当前Watcher实例,之前的``Dep.target`会被保存`targetStack`末尾(相当于入栈操作),完成操作后会执行`popTarget`函数,从`targetStack`取出最后一个元素来还原`Dep.target`(相当于出栈操作)。

```Dep.target = null
const targetStack = []
​
export function pushTarget (_target: ?Watcher) {
 if (Dep.target) targetStack.push(Dep.target)
 Dep.target = _target
}
​
export function popTarget () {
 Dep.target = targetStack.pop()
}```

## Watcher

当依赖更新时,Watcher类会重新求值,并可能触发重渲染。

```constructor (
 vm: Component,
 expOrFn: string | Function,
 cb: Function,
 options?: ?Object,
 isRenderWatcher?: boolean
 ) {
 this.vm = vm
 // 与渲染相关的watcher
 if (isRenderWatcher) {
 vm._watcher = this
 }
 vm._watchers.push(this)
 // options
 if (options) {
 this.deep = !!options.deep
 this.user = !!options.user
 this.computed = !!options.computed
 this.sync = !!options.sync
 this.before = options.before
 } else {
 this.deep = this.user = this.computed = this.sync = false
 }
 this.cb = cb
 this.id = ++uid // uid for batching
 this.active = true
 this.dirty = this.computed // for computed watchers
 this.deps = []
 this.newDeps = []
 this.depIds = new Set()
 this.newDepIds = new Set()
 this.expression = process.env.NODE_ENV !== 'production'
 ? expOrFn.toString()
 : ''
 // parse expression for getter
 if (typeof expOrFn === 'function') {
 this.getter = expOrFn
 } else {
 this.getter = parsePath(expOrFn)
 if (!this.getter) {
 this.getter = function () {}
 process.env.NODE_ENV !== 'production' && warn(
 `Failed watching path: "${expOrFn}" ` +
 'Watcher only accepts simple dot-delimited paths. ' +
 'For full control, use a function instead.',
 vm
 )
 }
 }
 if (this.computed) {
 this.value = undefined
 this.dep = new Dep()
 } else {
 this.value = this.get()
 }
 }```

构造函数接受五个参数,`vm`是挂载的Component实例;`expOrFn`是观察的属性,当是字符串时表示属性名,是函数时会被当成属性的get方法;`cb`是属性更新后执行的回调函数;`options`是配置项;`isRenderWatcher`表示当前实例是否与渲染相关。

在构造函数内,先将实例属性`vm`指向传入的Component实例vm,如果当前Watcher实例与渲染相关,会将其保存在`vm._watcher`中。接着将当前实例添加到`vm._watchers`中,同时根据传入的配置项`options`初始化实例属性。实例属性`getter`是监听属性的getter函数,如果`expOrFn`是函数,直接赋值,否则会调用`parsePath`来获取属性的getter。

`parsePath`内部会先使用正则来判断属性名,如果有除数字、字母、`.`和`$`以外的字符时视为非法属性名,直接返回,所以属性只能是以`.`分隔的属性。如果属性名合法,则`parsePath`返回一个闭包函数,调用时会传入`vm`,即`obj`是`vm`的引用,这个闭包函数最终的目的是从vm实例里获取属性。

```const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
 if (bailRE.test(path)) {
 return
 }
 const segments = path.split('.')
 return function (obj) {
 for (let i = 0; i < segments.length; i++) {
 if (!obj) return
 obj = obj[segments[i]]
 }
 return obj
 }
}```

初始化完成之后,如果不是计算属性相关的Watcher实例,会调用实例方法`get`求值。

### get方法

执行getter方法求值,完成依赖收集的过程。

方法开始时,执行`pushTarget(this)`,将`Dep.target`指向当前Watcher实例。然后执行`getter`收集依赖,最后将`Dep.target`复原,并执行`cleanDeps`遍历`deps`。在每次求值之后,都会调用`cleanupDeps`方法重置依赖,具体如何重置,稍后再讲。

实际上,`Dep.target`指向的实例是即将要收集的目标。

`getter`的执行,除了会获取值外,还会触发在`defineReactive`中为属性设置的getter,完成依赖的收集。

``` get () {
 pushTarget(this)
 let value
 const vm = this.vm
 try {
 value = this.getter.call(vm, vm)
 } catch (e) {
 if (this.user) {
 handleError(e, vm, `getter for watcher "${this.expression}"`)
 } else {
 throw e
 }
 } finally {
 // "touch" every property so they are all tracked as
 // dependencies for deep watching
 if (this.deep) {
 traverse(value)
 }
 popTarget()
 this.cleanupDeps()
 }
 return value
 }```

### addDep

`addDep`的功能是将当前Watcher实例添加到传入的Dep实例属性subs数组里去。

`addDep`接受一个Dep实例作为参数,如果 `dep.id` 没有在集合 `newDepIds` 之中,则添加。如果不在集合 `depIds` 中,则将当前实例添加到 `dep.subs` 中。 简单来说,这里的操作会避免重复收集依赖,这也是不直接调用`dep.addSub(Dep.target)`的原因。

``` addDep (dep: Dep) {
 const id = dep.id
 if (!this.newDepIds.has(id)) {
 this.newDepIds.add(id)
 this.newDeps.push(dep)
 if (!this.depIds.has(id)) {
 dep.addSub(this)
 }
 }
 }```

从这里可以看出来Dep实例和Watcher实例会相互引用。Dep实例将Watcher实例保存在实例属性`subs`中,在响应式属性调用`setter`时,执行`notify`方法,通知Watcher实例重新求值。

Watcher实例将Dep实例保存在集合`newDeps`,目的是避免重复收集依赖,同时会执行Dep实例方法`addDep`,将当前Watcher实例添加到Dep实例属性`subs`中。

### cleanupDeps

对于Watcher来说,每次求值的依赖并不一定与上一次的相同,在每次执行`get`之后,都会调用`cleanupDeps`来重置收集的依赖。Watcher有四个实例属性用于记录依赖,分别是`newDeps/newDepIds`与`deps/depIds`。`newDeps`与`deps`是保存依赖的数组,`newDepIds`与`depIds`是保存依赖Id的集合。记录上一次求值依赖的属性是`deps/depIds`,记录下一次求值依赖的属性是`newDeps/newDepIds`(执行`cleanupDeps`时已经调用过`getter`重新求值了,所以说是上一次求值,下一次指的是下一次调用`get`的时候)。

``` cleanupDeps () {
 let i = this.deps.length
 while (i--) {
 const dep = this.deps[i]
 if (!this.newDepIds.has(dep.id)) {
 dep.removeSub(this)
 }
 }
 // 交换depIds和newDepIds
 let tmp = this.depIds
 this.depIds = this.newDepIds
 this.newDepIds = tmp
 this.newDepIds.clear()
 // 交换deps和newDeps
 tmp = this.deps
 this.deps = this.newDeps
 this.newDeps = tmp
 this.newDeps.length = 0
 }```

首先遍历`deps`,如果此次求值的依赖在下一次求值中并不存在,则需要调用`removeSub`方法,从`subs`数组中移除当前Watcher实例。

接着交换`newDeps/newDepIds`与`deps/depIds`,并清空交换后的`newDeps/newDepIds`。

### update

Dep类的`notify`方法用于通知观察者重新求值,该方法内部实际是遍历`subs`数组,执行Watcher的`update`方法。

update 方法定义如下。当实例与计算属性相关时,xxx。如果不是计算属性相关时,判断是否需要同步触发,同步触发时调用`run`,否则执行`queueWatcher(this)`,交由调度模块统一调度。

``` update () {
 if (this.computed) {
 if (this.dep.subs.length === 0) {
 this.dirty = true
 } else {
 this.getAndInvoke(() => {
 this.dep.notify()
 })
 }
 } else if (this.sync) {
 this.run()
 } else {
 queueWatcher(this)
 }
 }```
总的来说,vue的数据响应式实现主要分成2个部分:

1.  把数据转化为getter和setter

2.  建立watcher并收集依赖

第一部分是上图中`data`、`observer`、`dep`之间联系的建立过程,第二部分是`watcher`、`dep`的关系建立

### 响应式原理整合

在生成vue实例时,为对传入的data进行遍历,使用`Object.defineProperty`把这些属性转为`getter/setter`.

`Object.defineProperty` 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

每个vue实例都有一个watcher实例,它会在实例渲染时记录这些属性,并在setter触发时重新渲染。

`Vue 无法检测到对象属性的添加或删除`

`Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value)方法向嵌套对象添加响应式属性。`

### 声明响应式属性

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值。

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的属性。

### 异步更新队列

vue更新dom时是异步执行的

数据变化、更新是在主线程中同步执行的;在侦听到数据变化时,watcher将数据变更存储到异步队列中,当本次数据变化,即主线成任务执行完毕,异步队列中的任务才会被执行(已去重)。

如果你在js中更新数据后立即去操作DOM,这时候DOM还未更新;vue提供了nextTick接口来处理这样的情况,它的参数是一个回调函数,会在本次DOM更新完成后被调用。

使用方法:

*   1.在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

```Vue.component('example', {
 template: '{{ message }}',
 data: function () {
 return {
 message: '未更新'
 }
 },
 methods: {
 updateMessage: function () {
 this.message = '已更新'
 console.log(this.$el.textContent) // => '未更新'
 this.$nextTick(function () {
 console.log(this.$el.textContent) // => '已更新'
 })
 }
 }
})```

*   2.因为 `$nextTick()` 返回一个 `Promise` 对象,所以你可以使用新的 [ES2016 async/await](https://links.jianshu.com/go?to=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FJavaScript%2FReference%2FStatements%2Fasync_function) 语法完成相同的事情:

```methods: {
 updateMessage: async function () {
 this.message = '已更新'
 console.log(this.$el.textContent) // => '未更新'
 await this.$nextTick()
 console.log(this.$el.textContent) // => '已更新'
 }
}```

你可能感兴趣的:(vue响应式原理)