因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出。
文章的原地址:https://github.com/answershuto/learnVue。
在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助。
可能会有理解存在偏差的地方,欢迎提issue指出,共同学习,共同进步。
阅读数据绑定源码之前建议先了解一下《响应式原理》以及《依赖收集》,可以更好地理解Vue.js数据双向绑定的整个过程。
数据绑定原理前面已经讲过Vue数据绑定的原理了,现在从源码来看一下数据绑定在Vue中是如何实现的。
首先看一下Vue.js官网介绍响应式原理的这张图。
img
这张图比较清晰地展示了整个流程,首先通过一次渲染操作触发Data的getter(这里保证只有视图中需要被用到的data才会触发getter)进行依赖收集,这时候其实Watcher与data可以看成一种被绑定的状态(实际上是data的闭包中有一个Deps订阅着,在修改的时候会通知所有的Watcher观察者),在data发生变化的时候会触发它的setter,setter通知Watcher,Watcher进行回调通知组件重新渲染的函数,之后根据diff算法来决定是否发生视图的更新。
Vue在初始化组件数据时,在生命周期的beforeCreate与created钩子函数之间实现了对data、props、computed、methods、events以及watch的处理。
initData这里来讲一下initData,可以参考源码instance下的state.js文件,下面所有的中文注释都是我加的,英文注释是尤大加的,请不要忽略英文注释,英文注释都讲到了比较关键或者晦涩难懂的点。
加注释版的vue源码也可以直接通过传送门查看,这些是我在阅读Vue源码过程中加的注释,持续更新中。
initData主要是初始化data中的数据,将数据进行Oberver,监听数据的变化,其他的监视原理一致,这里以data为例。
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
)
} // proxy data on instance
const keys = Object.keys(data) const props = vm.$options.props let i = keys.length //遍历data中的数据
while (i--) {
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`,
vm
)
} else if (!isReserved(keys[i])) {
proxy(vm, `_data`, keys[i])
}
} // observe data
observe(data, true )
}其实这段代码主要做了两件事,一是将_data上面的数据代理到vm上,另一件事通过observe将所有数据变成observable。
proxy接下来看一下proxy代理。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val
} Object.defineProperty(target, key, sharedPropertyDefinition)
}这里比较好理解,通过proxy函数将data上面的数据代理到vm上,这样就可以用app.text代替app._data.text了。
observe接下来是observe,这个函数定义在core文件下oberver的index.js文件中。
export function observe (value: any, asRootdata: ?boolean): Observer | void {
if (!isObject(value)) { return
} let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
} if (asRootData && ob) {
ob.vmCount++
} return ob
}Vue的响应式数据都会有一个ob的属性作为标记,里面存放了该属性的观察器,也就是Observer的实例,防止重复绑定。
Observer接下来看一下新建的Observer。Observer的作用就是遍历对象的所有属性将其进行双向绑定。
export class {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0
def(value, '__ob__', this) if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
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], obj[keys[i]])
}
}
observeArray (items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}Observer为数据加上响应式属性进行双向绑定。如果是对象则进行深度遍历,为每一个子对象都绑定上方法,如果是数组则为每一个成员都绑定上方法。
如果是修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。但这时候出现了一个问题,?如果我们进行pop、push等操作的时候,push进去的对象根本没有进行过双向绑定,更别说pop了,那么我们如何监听数组的这些变化呢?
Vue.js提供的方法是重写push、pop、shift、unshift、splice、sort、reverse这七个数组方法。修改数组原型方法的代码可以参考observer/array.js。
import { def } from '../util/index'const arrayProto = Array.prototypeexport const arrayMethods = Object.create(arrayProto)
;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
.forEach(function (method) { // cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator () { // avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
let i = arguments.length const args = new Array(i) while (i--) {
args[i] = arguments[i]
}
const result = original.apply(this, args)
const ob = this.__ob__ let inserted switch (method) { case 'push':
inserted = args break
case 'unshift':
inserted = args break
case 'splice':
inserted = args.slice(2) break
} if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify() return result
})
})从数组的原型新建一个Object.create(arrayProto)对象,通过修改此原型可以保证原生数组方法不被污染。如果当前浏览器支持proto这个属性的话就可以直接覆盖该属性则使数组对象具有了重写后的数组方法。如果没有该属性的浏览器,则必须通过遍历def所有需要重写的数组方法,这种方法效率较低,所以优先使用第一种。
在保证不污染不覆盖数组原生方法添加监听,主要做了两个操作,第一是通知所有注册的观察者进行响应式处理,第二是如果是添加成员的操作,需要对新成员进行observe。
但是修改了数组的原生方法以后我们还是没法像原生数组一样直接通过数组的下标或者设置length来修改数组,Vue.js提供了$set()及$remove()方法。
Watcher是一个观察者对象。依赖收集以后Watcher对象会被保存在Deps中,数据变动的时候会由于Deps通知Watcher实例,然后由Watcher实例回调cb进行实图的更新。
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array;
newDeps: Array;
depIds: ISet;
newDepIds: ISet;
getter: Function;
value: any; constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) { this.vm = vm
vm._watchers.push(this) // options
if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync
} else { this.deep = this.user = this.lazy = this.sync = false
} this.cb = cb this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy 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
)
}
} this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this) let value const vm = this.vm
if (this.user) { try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
} // "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget() this.cleanupDeps() return value
}
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)
}
}
}
cleanupDeps () {
let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
} let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear()
tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0
}
update () {
if (this.lazy) { this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () { if (this.active) { const value = this.get() if (
value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) || this.deep
) { // set new value
const oldValue = this.value
this.value = value
if (this.user) { try { this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else { this.cb.call(this.vm, value, oldValue)
}
}
}
}
evaluate () { this.value = this.get() this.dirty = false
}
depend () { let i = this.deps.length while (i--) { this.deps[i].depend()
}
}
teardown () { if (this.active) { // remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
} let i = this.deps.length while (i--) { this.deps[i].removeSub(this)
} this.active = false
}
}
} Dep来看看Dep类。其实Dep就是一个发布者,可以订阅多个观察者,依赖收集之后Deps中会存在一个或多个Watcher对象,在数据变更的时候通知所有的Watcher。
export default class Dep { 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()
}
}
}// the current target watcher being evaluated.// this is globally unique because there could be only one// watcher being evaluated at any time.Dep.target = null defineReactive接下来是defineReactive。defineReactive的作用是通过Object.defineProperty为数据定义上gettersetter方法,进行依赖收集后闭包中的Deps会存放Watcher对象。触发setter改变数据的时候会通知Deps订阅者通知所有的Watcher观察者对象进行试图的更新。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function) {
const dep = new Dep() 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
let childOb = 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
if (newVal === value || (newVal !== newVal && value !== value)) { return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
} if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}现在再来看这张图是不是更清晰了呢?
作者:染陌同学
链接:https://www.jianshu.com/p/74119fd33986



