图解 Vue1.0 响应式系统

发布于: 2020 年 06 月 18 日
图解 Vue1.0 响应式系统

jQuery 和 Vue

jQuery

$.ajax({
//...
}).then((data)=>{
let $target = $('#target') // get
$target.text(data.name) // set
})

Vue

fetch({}).then((data)=>{
this.name = data.name
})
  1. Vue 是如何知道更新哪里?

  2. Vue 是如何知道何时该更新?

这就是 Vue 一个核心的概念:响应式系统。如下是Vue 1.0 的官方响应式图:

其实官网的图是一个简化版的,只是为了使用者大概知道原理,但是对于研究源码还不够丰富,下面是我自己补充的图:

得益于 Vue 的响应式系统,才实现上面两个功能,那 Vue 到底是如何实现的呢?Vue 是结合了 Object.defineProperty发布订阅模式 来实现响应式系统的。

Object.defineProperty:

The static method Object.defineProperty() defines a new property directly on an object, or modifies an existing property on an object, and returns the object.

在对象上添加新属性或修改已存在属性,并返回传入的对象。

语法:

Object.defineProperty(obj, prop, descriptor)

重点是 descriptor:

实例:

let obj = {};
let name = 'zhangsan'
Object.defineProperty(obj,'name',{
enumerable:true,
configurable: true,
get: function reactiveGetter () {
return name
},
set: function reactiveSetter (newVal){
console.log('我的名字更新了')
name = newVal
}
})

在浏览器控制台输入:obj.name = 'lisi',结果如下:

Vue 中的源码分析

Vue 暴露的构造函数

function Vue (options) {
this._init(options)
}

new Vue 的时候就是调用上面,实际执行的是里面的 this._init() 方法,源码如下(主要部分):

this._callHook('init')
// initialize data observation and scope inheritance.
this._initState() // 重点
// setup event system and option events.
this._initEvents()
// call created hook
this._callHook('created')

this._initState:

Vue.prototype._initState = function () {
this._initProps()
this._initMeta()
this._initMethods()
this._initData() // 重点
this._initComputed()
}

this._initData:

Vue.prototype._initData = function () {
var dataFn = this.$options.data
// 实例上挂载 this._data 属性指向配置项里的 data
var data = this._data = dataFn ? dataFn() : {}
var props = this._props
// proxy data on instance
// 把 data 代理到实例上
var keys = Object.keys(data)
var i, key
i = keys.length
while (i--) {
key = keys[i]
this._proxy(key)
}
// observe data
// 从这里开始对data属性下的值进行 getter/setter处理
observe(data, this)
}

observe:

export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
var ob
// 若已经observer则返回之前的 __ob__
if (
hasOwn(value, '__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (ob && vm) {
ob.addVm(vm)
}
return ob
}

Observer:

export function Observer (value) {
this.value = value
// 创建收集筐,这里为何要实例化一个 dep ???
this.dep = new Dep()
// 添加__ob__属性,标识数据已经被Observer观察过
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}

this.observeArray 和 this.walk 都会走到 defineReactive 方法源码如下:

export function defineReactive (obj, key, val) {
// 每个属性都有自己的收集器
var dep = new Dep()
// 获得对象上指定属性的描述符
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 依赖收集(把所有依赖此数据项的Watcher添加进数组)
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
// 添加进依赖数组
// 把 watcher 添加到收集器中
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
// 通知更新
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
// 数据没有更改
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
// 调用依赖此数据项,订阅者的更新方法
// 通知订阅我这个dep的watcher们:我更新了
dep.notify()
}
})
}

Dep.target 就是 watcher,在每次 watcher 求值之前,会把当前的 watcher 赋值给 Dep.target,求值触发了 get 方法则会把当前的 watcher 给收集到 Dep 的 subs 中,这里就回答了 Vue 是如何知道更新哪里的问题,等到重新赋值时触发 set 方法执行 dep.notify() 通知更新,这里就回答了 Vue 是如何知道何时该更新的问题 ,源码如下:

Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}

这里其实就是调用 watcher 的 update 方法,通知更新:

Watcher.prototype.update = function (shallow) {
if (this.lazy) {
this.dirty = true
} else if (this.sync || !config.async) {
this.run()
} else {
// if queued, only overwrite shallow with non-shallow,
// but not the other way around.
this.shallow = this.queued
? shallow
? this.shallow
: false
: !!shallow
this.queued = true
// record before-push error stack in debug mode
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.debug) {
this.prevError = new Error('[vue] async stack trace')
}
pushWatcher(this)
}
}

这里的 run 方法是重点:

Watcher.prototype.run = function () {
if (this.active) {
var 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; but only do so if this is a
// non-shallow update (caused by a vm digest).
((isObject(value) || this.deep) && !this.shallow)
) {
// set new value
var oldValue = this.value
this.value = value
// in debug + async mode, when a watcher callbacks
// throws, we also throw the saved before-push error
// so the full cross-tick stack trace is available.
var prevError = this.prevError
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' &&
config.debug && prevError) {
this.prevError = null
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
nextTick(function () {
throw prevError
}, 0)
throw e
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
this.queued = this.shallow = false
}
}

this.cb 就是指令的更新方法,到此跟新动作完成。

如有不对,请斧正!😁

发布于: 2020 年 06 月 18 日 阅读数: 36
用户头像

前端黑板报

关注

专注前端,但不限前端。 2017.10.17 加入

前端开发工程师

评论

发布
暂无评论
图解 Vue1.0 响应式系统