学习 vue 源码 -- 响应式原理

概述

由于刚开始学习 vue 源码,而且水平有限,有理解或表述的不对的地方,还请不吝指教。

vue 主要通过 Watcher、Dep 和 Observer 三个类来实现响应式视图。另外还有一个 scheduler 来进行调度,本次暂时不做讨论。

Watcher 和 Dep 是订阅者和发布者的关系,每个 Watcher 可以订阅多个 Dep,而每个 Dep 也可以被多个 Watcher 订阅。当 Observer 监听的数据发生改变时,相应的 Dep 就会触发其订阅者 Watcher 更新视图。

下面通过一个简单的例子来说明其实现流程和原理:

  <div id="app">
{{someVar}}
</div> <script type="text/javascript">
new Vue({
el: '#app', data: {
someVar: 'init'
}, mounted(){
setTimeout(() => this.someVar = 'changed', 3000)
} })
</script>

页面初始会显示 "init" 字符串,3秒钟之后,会更新为 "changed" 字符串。

为了便于理解,将整个流程分为三个阶段:

  1. 初始化data,这个阶段 vue 通过 Observer 监听 data 对象,并将普通的 someVar 属性代理为 get\set 属性
  2. 初次挂载el,这个阶段 vue 使用默认的 someVar 数据渲染视图,并将 watcher 添加到 dep 的订阅者列表
  3. someVar 更改触发视图更新,这个阶段 someVar 被赋予了新值,vue 根据 watcher 和 dep 的订阅关系触发视图的更新

下面我们来逐步分析这三个阶段的流程。

第一阶段

主要流程

new Vue(options) => vm._init(options) => initState(vm) => initData(vm) => observe(data) => new Observer(data) => defineReactive(data, key, value)

说明

初始化操作会监听 data 对象,对其每一个属性调用 defineReactive() 方法,将其改造为响应式属性,代码如下(去掉了不影响表述主流程的代码,以便能更清晰的抓住重点):

/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any
) {
const dep = new Dep() Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { dep.depend()
    return val
  },
set: function reactiveSetter (newVal) {
if (newVal === val || (newVal !== newVal && val !== val)) {
return
} val = newVal
dep.notify()
}
})
}

可以看到,当 get() 方法执行的时候,会调用 dep.depend() ;当 set() 方法执行时,会调用 dep.notify()。后面我们会看到这两个方法的作用。

第二阶段

主要流程

vm.$mount(el) => mountComponent(vm, el) => new Watcher(vm, updateComponent) => watcher.get() => updateComponent() => vm._update(vm._render())

说明

vm._render() 调用 vm.$options.render() 方法生成 vnode,vm._update() 方法根据 vnode 对视图做更新。

vm.$options.render() 方法是在 $mount() 方法中生成的,生成后的代码如下:

(function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v("\n         " + _s(someVar) + "\n ")])
}
})

可以看到,代码中会使用到 vm.someVar 属性,而该属性最终会代理到之前定义的响应式属性上,从而调用其 get() 方法,进而调用 dep.depend() 方法将 watcher 添加到订阅者列表。

dep.depend() => Dep.target.addDep(dep) => dep.addSub(watcher) => dep.subs.push(watcher)

dep.subs 就是 dep 的订阅者列表,通过这个流程,就建立起了 dep 和 watcher 之间的订阅关系。

其中,Dep.target 就是当前的 watcher,因为在 watcher.get() 方法执行时,有如下流程:

watcher.get() => pushTarget(watcher) => Dep.target = watcher

第三阶段

3秒之后,vm.someVar 被赋予了新的值,从而最终会调用到响应式属性的 set() 方法,进而调用 dep.notify(),触发 watcher 更新视图。

主要流程

dep.notify() => watcher.update() => watcher.run() => watcher.get() => watcher.getter.call(vm, vm) == updateComponent() => vm._update(vm._render())

说明

watcher 的 getter() 方法就是第二阶段 new Watcher(vm, updateComponent) 中的 updateComponent 方法,可以看到,通过这个流程,视图得到了更新。

以上就构成了一个响应式视图模型,其核心是利用 defineProperty() 方法将普通属性转换为带有钩子的 set\get 属性,从而实现了数据监听。

上一篇:Dev GridControl GridView 属性大全[中文解释]


下一篇:一起学习vue源码 - Object的变化侦测