vue源码-深入响应式原理
前言
随着前后端分离成为Web开发的常态,Mvvm框架越来越普及。让前端开发从关注Dom,变为关注数据,提高了开发效率,降低了学习成本。同时也能有效避免低级的Dom操作错误。
在享受Mvvm框架带来的便利的同时,我们也会对它的具体实现产生兴趣。笔者认为Mvvm框架重要的有两个部分
-
数据变化的捕获,通知与响应
-
vDom对通知产生响应,并对Dom进行相应的操作
今天我们先来看一下变化的捕获,通知与响应,分为下面四个部分
-
数据变化的捕获
-
监听器的创建
-
数据变化与监听器的关联
-
变化的响应
一,数据变化的捕获
日常项目中,我们常用的与数据变化相关的,有以下三个:
-
Data: 包括定义数据模型设置的初始值,Prop传递的值
-
Watch:监听某一个值的变化进行后续业务处理
-
Computed:页面展示的值是多个值的组合变化
除了上述三个,其实vue框架本身,还有一个组件层面的变化,比如路由变化会重新渲染组件。虽然都是变化,但这四者既有联系也有区别,关系如下图
image数据变化会触发监听器,会触发组件渲染,而组件渲染的时候,会重新计算属性。那么该如何监听数据变化呢?
监听数据变化
JavaScript中监听数据变化API:Getter和Setter,先看一个简单的示例:
var user = {}
var name;
Object.defineProperty(user, 'current', {
get: function(){
console.log('获取名称')
return name
},
set:function(val){
console.log('设置名称')
name = val
}
})
user.current = '张三';
console.log(user.current);
控制台输出:
设置名称
获取名称
张三
API getter和setter就是数据劫持的基础,通过这个例子我们看到,在设置数据或获取数据的时候,我们都可以加入自己的处理逻辑。从而达到我们监听数据变化的目的。接下来我们再对比一下vue的代码实现。
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()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
同样的,Vue也是通过这种方式劫持数据,然后拦截到变化后,去通知订阅者。Vue捕获变化并发送通知的流程图如下(放大查看):
image.png-
当监听到数据变化时,我们通过Watcher来发送通知
-
当获取数据的时候,我们把Watcher加入到通知列表
什么是Watcher呢?
二,监听器的创建
Watcher的分类
imageWatcher什么时候创建的呢?先看下面这段熟悉的代码:
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>’
})
上面这块代码简单来说就是创建了一个Vue的实例。声明了一个组件App,渲染绑定的节点是#app,还有路由。
Vue实例化的处理流程(本文无关的部分略过)。
image和变化相关的有三个方法:
-
InitRender阶段,绑定组件的渲染方法
-
InitState阶段,创建数据模型,监听器,计算属性的Watcher
-
$mount阶段,创建组件Watcher
我们分别看一下三种监听器的创建
监听器Watcher
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
注意这一行:const watcher = new Watcher(vm, expOrFn, cb, options)
$watch方法也是Vue对外提供的API
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
})
我们结合Vue官网watch例子来看new Wacher的参数:
vm:Vue实例本身
expOrFn:firstName
cb:对应的函数
option:额外的参数,从上面我们也看到,有个immediate属性,如果为true就先调用一次
计算属性Watcher
计算属性也是和上面类似,但有个重要的参数,lazy为true。这就代表着,在创建的时候,并不会立即执行。
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
……略
for (const key in computed) {
……略
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
}
……略
}
……略
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
而调用的时机是在渲染组件的时候触发,然后watcher.evaluate()
组件Watcher
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
………略
let updateComponent
if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) {
updateComponent = () => {
………略
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
………略
return vm
}
当组件发生变化的时候就会触发vm._update(vm._render(), hydrating),在创建的时候也会执行一次,进行第一次渲染。具体见下图:
image.png-
Init方法中,会进行各种Watcher的创建
-
$mount中会创建组件Watcher并执行
-
组件Watcher触发渲染
-
渲染过程发现有子组件,对子组件再走一遍上面的流程
注意:上面我们说了三种Watcher的创建,计算属性的Watcher不会立即执行,而其他两个都会立即执行一次。
三,数据变化与监听器的关联
到目前为止,我们解决了变化的监听,以及观察者的创建,那么两者又是如何联系起来的呢?
再来看一下数据劫持的getter方法,我们发现只有在Dep.target(Watcher)存在的时候才建立关联
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
},
再看一下Watcher类的get方法(这个就是一个普通的方法名称,不要和getter混淆)
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
pushTarget(this):将当前watcher压入栈,同时将this赋值给Dep.target
popTarget():出栈
哪这个栈又是个什么东东呢?
Watcher Stack
组件是按树形的结构递归解析。如果不考虑出栈的情况,那么整个栈的情况如图所示:
而在实际过程中当关联设置结束后,会进行出栈操作。整个解析过程按照从根节点到子节点,也就是监听先压入栈,然后解析的时候发现栈里有监听,就会绑定。
是不是有点乱?没关系,我们再捋一遍。
-
new Watcher的时候调用其内部get方法,在这个方法中会将当前监听压入栈,并赋值给target。
-
继续向下执行,解析组件时第一次必然获取数据,这个时候就会触发数据劫持的getter,在getter里判断当前target是否有值,有值就把当前数据和Watcher进行关联,没有就忽略继续向下
-
出栈并清空target
结合上面的文字,再具体看一下这三个Watcher关联的流程
组件Watcher关联
image.png监听器Watcher关联
image.png组件Watcher和监听器Watcher的区别,是组件Watcher要进行渲染。这当然也比较好理解,监听的目的,归根结底是要渲染到页面用户才能看到变化。比如vue-router,就是利用组件Watcher进行的触发。
计算属性Watcher关联
计算属性和前面两个不同,它在创建watcher的时候,并不会触发get。
在初始化的时候创建好Watcher,渲染的时候才会触发,同时把组件Watcher也追加进订阅
image四,变化的响应
变化劫持,通知Watcher,Watcher响应具体的动作。这部分内容相对就比较简单了。唯一需要注意的是,计算属性因为没有入栈,所以它的响应会被丢弃。
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true //不执行具体动作
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
通过代码可以看到,当是lazy的时候,设置dirty=true,但并没有进行具体的操作。
我们最后再整体回顾一下开始的关系图:
image结语
数据响应式可以说是Mvvm框架的精髓,希望通过本文的描述,可以让大家更好的理解它的实现原理,只是通过文章,依然不能完全的描述透彻,细节部分还是需要去阅读源码,对照分析和研究。前端水越来越深,一起共勉。本文都是作者自己的理解,有不当之处欢迎批评指正。关于vDom的渲染部分,会在下篇文章中分享。
image.png