前两篇文章教会我们手写一个简单的响应式系统。这篇文章主要带领我们潜入vue.js的源码来看待响应式系统。首先创建一个vue application:
<div id="app">
<h1>{{ product }}</h1>
</div>
<script src="vue.js"></script>
<script>
var app = new Vue({
el: '#app',
data: {
product: "Socks"
}
})
</script>
在声明data的时候,通过getter, setter实现了响应式系统。从前两章中我们知道,在get data的时候我们增加了一个依赖,在set data的时候运行所有的依赖。
在这里我们先下载一份vue源码,在examples目录下创建一个test,html文件如下所示。然后我们将使用谷歌浏览器的debugger工具逐步深入data初始化的过程。
<div id="app">
{{ msg }}
</div>
<!-- <script src="../dist/vue.js"></script> -->
<script src="../dist/vue.js"></script>
<script>
debugger
new Vue({
el: '#app',
data: {
msg: 'hello vue'
}
})
</script>
一开始,在/src/core/instance/index.js 中我们会看到这样的代码:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' && // 判断存在node环境
!(this instanceof Vue) // Vue 构造函数不存在
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) // 调用vue原型上的init方法
}
initMixin(Vue) // 从这里开始执行
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
当我们运行let app = new Vue({...})
的时候,上述的根Vue 函数帮我们创建了一个实例。随后调用原型上定义的_init
方法,该方法在initMixin
函数中添加。在initMixin
中我们做了许多事情,但此时我们只关注initState
, 因为它与初始化data, 添加响应式密切相关,除此之外initState
方法还与初始化props
, methods
, computed
密切相关。
/src/core/instance/init.js
:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
... // 省略
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm) // defineReactive attrs and listeners
callHook(vm, 'beforeCreate') // Notice this lifecycle call hook
initInjections(vm) // resolve injections before data/props
initState(vm) // <---
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // Notice this lifecycle call hook
...
vm.$mount(vm.$options.el)
}
}
/src/core/instance/state.js
:
import {
set,
del,
observe, // 观察数据
defineReactive,
toggleObserving
} from '../observer/index'。// 引入一系列方法
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {. // data 需要是纯对象
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
...
// 观察数据,接下来找到observe方法
observe(data, true /* asRootData */)
}
/src/core/observer/index.js
:
observe方法: 为每个值创建一个观察类实例
export function observe (value: any, asRootData: ?boolean): Observer | void {
//value 是 {msg: 'hello vue'}
...
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__ // 有observer就用原来的
} else if (
...
) {
ob = new Observer(value) // 没有则新建一个
}
...
return ob
}
Observer类,可以发现此处正式定义了响应式方法defineReactive
, defineReactive
就与前两篇中写的简易响应式原理衔接上了。
/src/core/observer/index.js
:
export class Observer {
...
constructor (value: any) {
this.value = value // 例子 { msg: 'hello vue'}
this.dep = new Dep(). //
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) { // 处理data是数组的情况
...
this.observeArray(value)
} else {
this.walk(value) // 遍历每个属性
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 正式定义响应式
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // 数组的时候递归遍历知道简单数据类型
}
}
}
defineReactive方法,终于到这里啦。在这里我们终于使用到了之前讲过很多次的Object.defineProperty
, 在数据获取的时候存储依赖,在数据更新的时候重新运行依赖。
/src/core/observer/index.js
:
export function defineReactive (
obj: Object,
key: string, // "msg"
val: any, // "hello"
...
) {
const dep = new Dep()
...
// 关键api defineProperty
Object.defineProperty(obj, key, { // 为对象的每个元素设置getter, setter
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // 已经有get了则直接复用
if (Dep.target) {
// 如果target存在,则加入依赖
dep.depend()
if (childOb) { // 存在子对象时遍历
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
// 如果值一样,则返回
return
}
...
if (setter) {
...
} else {
val = newVal // 设置新值
}
childOb = !shallow && observe(newVal)
dep.notify() // 重新运行依赖中的所有方法
}
})
}
在上一段代码中我们可以看到,使用到了Dep
类,这个类前两篇的时候也出现过,是我们自己写的简易版本。VUE中真正的这个类还比较复杂,在了解这个之前还有watcher类要了解下。这个watcher做了一些什么呢?它会将需要被再次运行的匿名函数暂时记录下来(例如我们平常使用的计算属性中的方法),运行一次,然后清除它,这是使用到自身定义的get方法。还会有一个update方法,让这些watcher实例能够排队运行,因为有时候不是需要立即运行匿名函数代码并重新计算。要注意的时候对于data对象上的属性来说,watcher类的实例化发生在mount的时候,因为在渲染真实dom的时候这些属性第一次被获取,然后触发依赖收集。而对于computed来说创建计算属性的时候就要收集依赖,即实例化watcher类了。
/src/core/observer/watcher.js
:
export default class Watcher {
...
get() {
pushTarget(this) // 将Dep.target 存入watcher 对象中
...
value = this.getter.call(vm, vm) // vm 虚拟dom,即VUE实例,上面挂在创建一个根节点所需要的所有属性
...
popTarget() // 一次只存一个,用完之后清除
return value
}
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run() // shown below
} else {
queueWatcher(this) // queue this to run later
}
}
run() {
if (this.active) {
const value = this.get()
}
}
addDep(dep: Dep) { // 开始追踪一个依赖
...
this.newDeps.push(dep) // The watcher also track's deps
dep.addSub(this) // Calls back to the dep (below)
}
}
最后再来看一下dep类。可以看到订阅者中保存的是watcher类型了。这个功能就跟我们之前写的dep类基本类似。被获取的时候收集依赖,修改时触发匿名函数重新命名。
//src/core/observer/dep.js
:
export default class Dep {
...
subs: Array<Watcher>; // subs里面保存的是watcher类.
constructor () {
this.subs = [] // Our subscribers we need to keep track of
}
addSub(sub: Watcher) { // You can think of this sub Watcher as our target
this.subs.push(sub)
}
...
depend() { // <-- There's our depend function
if (Dep.target) { // If target exists
Dep.target.addDep(this) // Add this as a dependency, which ends up calling addSub function above. Pushing this watcher.
}
}
notify() { // <--- There's our notify function
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // <-- Queue and run each subscriber
}
}
}
以上是本人看了教程后对vue响应式原理的一点点简单的理解。