前两篇文章教会我们手写一个简单的响应式系统。这篇文章主要带领我们潜入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>
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' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
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)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
...
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 = {}
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(data, true )
}
/src/core/observer/index.js :
observe方法: 为每个值创建一个观察类实例
export function observe (value: any, asRootData: ?boolean): Observer | void {
...
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} 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
this.dep = new Dep().
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
...
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,
val: any,
...
) {
const dep = new 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
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
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)
...
value = this.getter.call(vm, vm)
...
popTarget()
return value
}
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run() {
if (this.active) {
const value = this.get()
}
}
addDep(dep: Dep) {
...
this.newDeps.push(dep)
dep.addSub(this)
}
}
最后再来看一下dep类。可以看到订阅者中保存的是watcher类型了。这个功能就跟我们之前写的dep类基本类似。被获取的时候收集依赖,修改时触发匿名函数重新命名。 //src/core/observer/dep.js :
export default class Dep {
...
subs: Array<Watcher>;
constructor () {
this.subs = []
}
addSub(sub: Watcher) {
this.subs.push(sub)
}
...
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
以上是本人看了教程后对vue响应式原理的一点点简单的理解。
|