一、阅读准备
阅读Vue.js
代码前,需要准备:
- 仓库代码,方便加注释和多段关键代码可以同时阅读
- 打包后未压缩的代码,方便打断点,看代码执行情况
做好以上准备后,我们写一个demo
:
<!DOCTYPE html>
<html>
<head>
<title>learn Vue</title>
</head>
<body>
<div id="app">
<div class="title">hello vue.js</div>
<div class="content">{{count}}</div>
<input type="text" v-model="count">
<div @click="handleClick" style="cursor: pointer;">点击我</div>
<div v-for="item in array" :key="item">{{item}}</div>
</div>
<script src="vue.js" type="text/javascript"></script>
<!-- <script src="https://unpkg.com/vue@next"></script> -->
<script>
// debugger
var app = new Vue({
el: '#app',
data: {
count: 1,
array: [1, 2, 3]
},
methods: {
handleClick() {
this.count = +this.count + 1;
}
},
});
</script>
</body>
</html>
用 Chrome 打开这个demo
后,我们在new Vue
前打一个断点,就可以看到Vue
是如何执行的了。
二、响应式原理
2.1 代码阅读
根据第一个断点找到了Vue
的入口function Vue()
,然后从断点进入Vue.prototype._init
,代码如下:
Vue.prototype._init = function (options?: Object) {
// 这里删去了无关代码
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
// 执行 beforeCreate 钩子
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
// 执行 created 钩子
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
通过Vue
生命周期图得知Vue
是在breforeCreate
和created
这两个钩子之间完成reactivity
,于是我们进入initState(vm)
方法,代码如下:
function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
通过上述代码不难得知Vue
在initData
中完成了对options.data
的改造,使其具有响应式功能,于是我们进入initData
:
function initData (vm: Component) {
/**** begin: 从vm中取出data对象 ****/
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
)
}
/**** end ****/
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
// 判断 data 中是否有和 props 和 methods 重名的属性
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
// 使用 vm 代理 vm._data
// 通过 vm[prop] 访问 vm._data[prop]
proxy(vm, `_data`, key)
}
}
// observe data
// 在这里使用观察者模式对 data 进行封装
observe(data, true /* asRootData */)
}
上述代码完成了(1)把data
从vm.data
中取出来转为对象;(2)对props
, methods
属性进行检查,看是否和data
属性冲突;(3)调用observe
对data
进行响应式封装。下面我们找到observe
的代码:
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果存在 __ob__ ,则返回已存在的观察者
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
上面代码的注释已经很清楚的讲述了这个函数的用处:为传入的value
(这里传入的是vm.data
)创建一个observer
对象,如果value
已经有observer
了,那就返回已有的observer
实例。这里又调用了Observer
构造函数,代码如下:
class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 把 observer 挂载到 value.__ob__ 上
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组方法重写,支持响应式
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 递归 observe 数组, 所以直接修改数组的元素值是具有响应性质的
this.observeArray(value)
} else {
// 让 vm._data上的每个属性都支持响应式
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
生成一个observer
挂载到value.__ob__
上,如果value
为数组,那么重写该数组的原型上的方法。最后,遍历value
上的属性,使其具有响应性。具体实现通过defineReactive
,代码如下:
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 属性的依赖也是保存在闭包中
const dep = new Dep()
// 获取 obj[key] 的属性描述对象,如果该属性不允许配置,那么直接返回
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 如果是深度reactive,还要 observe obj[key]
let childOb = !shallow && observe(val)
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
// 如果要 set 的新的值和原值相等,那么不需要 reactive
// 第二个条件是为了避免 NaN !== NaN
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
}
// 修改了val后,需要修改深度监听的的 observer
childOb = !shallow && observe(newVal)
// 通知所有的 watcher update
dep.notify()
}
})
}
这里是响应式的关键,修改对象属性的setter
和getter
,在setter
执行时,先调用对象属性原本的setter
,然后通知该属性的所有的watcher
做更新。
2.2 流程梳理
看了上面的代码后,脑子里就一个字:绕!所以我决定,这里在对实现响应式的流程做一个梳理。这里用文字对流程进行描述:
-
reactive
的入口是Vue.prototype._init
里调用的initState(vm)
。 - 在
initState
中调用initData
。主要把data
对象从data
函数中取出来挂载到vm._data
上,并使用vm
代理vm._data
;检查props
和methods
中是否有属性名和data
属性名冲突;调用observe(data)
。 -
observe
调用new Observer(value)
(这里的value
是被观察对象)生成一个Observer
对象挂载到value.__ob__
上。 -
Observer
构造函数中重写了数组原型链的方法使其支持reactive
,然后调用walk
遍历vm._data
执行defineReactive
,另外,使用new Dep
生成一个依赖对象挂载到value.__ob__.dep
上用来保存依赖。 -
defineReactive
里声明了一个闭包变量dep
这个变量是真正保存属性watcher
的,在调用属性getter
的时候,如果当时存在Dep.target
,就会让Dep.target
把自身加入到前面申明的闭包变量dep.sub
中。 - 当属性值发生改变时,会调用
dep.notify
通知sub
中的所有watcher
让其更新。
总结
- 真正实现观察者模式的是
Dep
和Watcher
,如果能改下名字,那就更好了。。。 - 因为调用
defineReactive
时默都是深响应,data
中每一个属性都会递归转为setter
和getter
,着实有点占用性能。 - 从
Observer
开始实现响应式。 - 在
created
后,vm._data
初始化完成,适合请求数据。 - 模板渲染的时候,会通过调用属性的
getter
添加依赖。
网友评论