从vue问世到现在,所有的vue开发者应该对vue的底层原理都模模糊糊的有一些了解,Object.defineProperty getter/setter,发布订阅之类的概念都能说出一二来,可问题是大部分的coder就真的只是模模糊糊的了解一二,练习vue两年半,到头来还是只能说出个双向绑定来。
众所周知vue是一个非常好用的mvvm框架,我们所关注的双向绑定发生在view和viewModel的交互过程中,支持这个过程就是vue本身的数据劫持模式,粗略的描述就是vue通过Object.defineProperty方法重写了一个普通js对象的get,set方法,使其在被访问时收集依赖,在被设置时执行回调。同时vue将遍历所有的dom节点,解析指令,在这个过程中由于挂载在this上的属性已经是响应式数据(reactive)这些变量在Compile的过程中被访问时便会新生成一个watcher实例放入这个属性特有的dep实例中(闭包),当该属性被set的时候便会遍历(notify)这个属性的dep实例,执行里面每个watcher实例中保存的回调方法。
几乎每个vue coder对这个过程都是倒背如流,可在实际开发中发现并没有什么卵用,因为这只是一个很粗粝的描述和理解,里面还有很多的细节和彼此间关联是大部分coder都影影绰绰模模糊糊的,只有对这些细节大部分都有所了解了,才能在实际开发中对项目有整体的把控感。
之所以从watcher对象开始,是因为在之前的概述中可以发现watcher对象其实是连接Observer和Compile的关键,从watch,computed属性,再到vuex可以说vue的一整套数据监听和链路追踪系统都和watcher相关,
主要源码端解析
构造部分
主要构造参数都通过注释有所说明,需要注意的是this.getter的值可以看到:
this.getter = expOrFn
对于初始化用来渲染视图的watcher来说,expOrFn就是render方法,对于computed来说就是表达式,对于watch才是key,所以这边需要判断是字符串还是函数,如果是函数则返回函数,如果是字符串则返回parsePath(expOrFn)方法,所以不管传入的expOrFn是什么最终返回的都是一个方法,这样子做的目的是为了方便watcher内的get()方法调用
构造部分源码
constructor (
vm: Component, // 组件实例对象
expOrFn: string | Function, // 要观察的表达式
cb: Function, // 当观察的表达式值变化时候执行的回调
options?: ?Object, // 给当前观察者对象的选项
isRenderWatcher?: boolean // 标识该观察者实例是否是渲染函数的观察者
) {
// 每一个观察者实例对象都有一个 vm 实例属性,该属性指明了这个观察者是属于哪一个组件的
this.vm = vm
if (isRenderWatcher) {
// 只有在 mountComponent 函数中创建渲染函数观察者时这个参数为真
// 组件实例的 _watcher 属性的值引用着该组件的渲染函数观察者
vm._watcher = this
}
vm._watchers.push(this)
// options
// deep: 当前观察者实例对象是否是深度观测
// 平时在使用 Vue 的 watch 选项或者 vm.$watch 函数去观测某个数据时,
// 可以通过设置 deep 选项的值为 true 来深度观测该数据。
// user: 用来标识当前观察者实例对象是 开发者定义的 还是 内部定义的
// 无论是 Vue 的 watch 选项还是 vm.$watch 函数,他们的实现都是通过实例化 Watcher 类完成的
// sync: 告诉观察者当数据变化时是否同步求值并执行回调
// before: 可以理解为 Watcher 实例的钩子,当数据变化之后,触发更新之前,
// 调用在创建渲染函数的观察者实例对象时传递的 before 选项。
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
// cb: 回调
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()
: ''
// 检测了 expOrFn 的类型
// this.getter 函数终将会是一个函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
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():
主要是通过访问属性来达到收集依赖的目的,在我们进行依赖收集时,data已经完成响应式数据的过程。我们知道在属性被访问时触发该属性的get方法将全局变量Dep.target放入该属性独有的deps对象中,所以不管是在哪种场景下进行依赖收集时都会新建一个watcher实例。其中pushTarget(this) 将该实例赋值到全局变量Dep.target this.getter.call(vm, vm) 用来访问该属性触发该属性的get方法将该watcher实例收集进其deps中
/**
* 求值: 收集依赖
* 求值的目的有两个
* 第一个是能够触发访问器属性的 get 拦截器函数
* 第二个是能够获得被观察目标的值
*/
get () {
// 推送当前Watcher实例到Dep.target
pushTarget(this)
let value
// 缓存vm
const vm = this.vm
try {
// 获取value,this.getter被处理成了方法
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
// 递归地读取被观察属性的所有子属性的值
// 这样被观察属性的所有子属性都将会收集到观察者,从而达到深度观测的目的。
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
addDep:
将自身添加进属性所属的deps实例,newDepIds的存在是为了避免重复收集依赖
/**
* 记录自己都订阅过哪些Dep
*/
addDep (dep: Dep) {
const id = dep.id
// newDepIds: 避免在一次求值的过程中收集重复的依赖
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id) // 记录当前watch订阅这个dep
this.newDeps.push(dep) // 记录自己订阅了哪些dep
if (!this.depIds.has(id)) {
// 把自己订阅到dep
dep.addSub(this)
}
}
}
updata()
调用run方法,run方法执行回调函数完成数据更新,this.cb即为watcher实例生成时传入的回调函数
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
// 指定同步更新
this.run()
} else {
// 异步更新队列
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
// 对比新值 value 和旧值 this.value 是否相等
// 是对象的话即使值不变(引用不变)也需要执行回调
// 深度观测也要执行
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) {
// 意味着这个观察者是开发者定义的,所谓开发者定义的是指那些通过 watch 选项或 $watch 函数定义的观察者
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)
}
}
}
}
大致了解了watcher实例后,再分析watcher在不同场景所扮演的角色,
页面数据更新
也就是所谓的双向绑定,在vue遍历dom节点时{{msg}}内的数据被访问时生成watcher将其收集进msg的dep中当msg变更时触发回调函数更新视图,这一块也是被说臭了的一块,就不细说,网上一大把
$watch属性
官网:watch 选项提供了一个更通用的方法,来响应数据的变化
源码位置: vue/src/core/instance/state.js
根据我们之前的分析要响应数据的变化那么就需要将变化(回调函数) 这个动作当作 依赖(watcher实例) 被 响应数据(data属性)所收集进他的deps
通过源码可以看到initWatch方法遍历option内的watch属性,然后依次调用createWatcher方法然后通过调用
vm.$watch(expOrFn, handler, options)
来生成watcher实例。
例: 以一个watch属性的通常用法为例
data: {
question: '',
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
回调方法 balabalabala...
}
},
原理解析
在之前的watcher实例所需的构造参数了解到watcher所需的必要参数,此时生成watcher实例的expOrFn为watch属性的key也就是例子中的question,cb为watch属性的handler也就是后面的回调方法,在watcher的get方法中我们得知expOrFn将会作为getter被访问,
......
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
.....
value = this.getter.call(vm, vm)
.....
又因为key为data的属性作为响应式数据在被访问时触发其get方法将收集这个**依赖(当前watcher实例)**也就是Dep.target也就是当前的watcher(watcher在get方法时会将Dep.target设置成当前属性),当该属性被修改时调用dep的notify方法再调用每个watcher的updata方法执行回调就可达到watch属性的目的,所以watch属性可以被总结为依赖更新触发回调
源码注释
// 遍历vue实例中的watch属性
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
// $watch 方法允许我们观察数据对象的某个属性,当属性变化时执行回调
// 接受三个参数: expOrFn(要观测的属性), cb, options(可选的配置对象)
// cb即可以是一个回调函数, 也可以是一个纯对象(这个对象要包含handle属性。)
// options: {deep, immediate}, deep指的是深度观测, immediate立即执行回掉
// $watch()本质还是创建一个Watcher实例对象。
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
// vm指向当前Vue实例对象
const vm: Component = this
if (isPlainObject(cb)) {
// 如果cb是一个纯对象
return createWatcher(vm, expOrFn, cb, options)
}
// 获取options
options = options || {}
// 设置user: true, 标示这个是由用户自己创建的。
options.user = true
// 创建一个Watcher实例
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// 如果immediate为真, 马上执行一次回调。
try {
// 此时只有新值, 没有旧值, 在上面截图可以看到undefined。
// 至于这个新值为什么通过watcher.value, 看下面我贴的代码
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 返回一个函数,这个函数的执行会解除当前观察者对属性的观察
return function unwatchFn () {
// 执行teardown()
watcher.teardown()
}
}
computed属性
官网:你可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 vm.reversedMessage 依赖于 vm.message,因此当 vm.message 发生改变时,所有依赖 vm.reversedMessage 的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。
就像watch属性可以理解为依赖变更触发回调,computed可以理解为依赖变更求值,而且将该属性挂载在当前实例上可以通过this访问并返回最新值。
原理解析:
依赖收集
如下源码所示,在方法initComputed中将遍历vue实例的computed属性,然后为computed内的每个属性都实例化一个watcher对象
....
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
....
此时computed属性的表达式将作为watcher实例的expFcn,之前说过expFcn将会作为getter被访问,
......
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
.....
value = this.getter.call(vm, vm)
.....
可以看到此时的expFcn是一个方法,当这个方法被执行时方法内如果有data属性(响应式数据)则这些数据的get方法将会被依次触发,此时全局属性Dep.target还是当前的watcher实例也就是说当前的watcher将作为这些data属性的依赖之一被保存在被闭包的deps属性中
触发更新
当computed属性所依赖的data属性被修改时执行dep.notify方法依次遍历deps内的watcher实例执行各自的uptate方法,
最终在run方法内调用get方法将watcher的value属性更新为最新值
run () {
if (this.active) {
const value = this.get()
...
this.value = value
.....
}
}
又因为data属性被修改,触发vue的render()方法,更新视图,访问computed属性,而在defineComputed方法内我们可以看到在被重写了get和set方法的computed属性都被挂载到了当前vue实例上,而此时computed属性被访问触发了被重写了的get方法,从而调用createComputedGetter方法最终返回已经是最新值的watcher.value
部分相关源码
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
function createComputedGetter (key) {
return function computedGetter () {
....
return watcher.value
.....
}
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 遍历vue实例中的computed属性,此时每个computed属性的表达式将作为watcher实例的expFcn
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
// 如果computed属性不属于vue实例,则d
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
//
export function defineComputed (
target: any,// 当前vue实例
key: string,// computed属性
userDef: Object | Function// computed表达式,若是对象则为对象内的get方法
) {
// 重写get set 方法
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 生成属性的get方法,当computed属性被访问时通过watcher实例返回最新的数据
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
}
}
}
官网:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
这里不介绍vuex的各种概念官网上说了
Vuex 背后的基本思想,借鉴了 Flux、Redux 和 The Elm Architecture
这些概念的详细说明,出门左拐,这里只是基于vue数据链路追踪的角度来详细解释vuex的相关源码实现
先回忆一下vuex在vue项目中的使用,我们经常是在main.js中引入配置好的vuex对象然后将其当作vue的构造参数之一传入vue的构造方法,然后所有的vue实例都将拥有一个$store属性指向同一状态空间也就是我们说的单一状态树
如:
new Vue({
el: '#app',
router,
store,
components: { App },
template: ' '
})
这个操作的支持是因为在vue实例的全局beforeCreate钩子中混入vuex对象
Vue.mixin({ beforeCreate: vuexInit })
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 全局混入store属性
Vue.mixin({ beforeCreate: vuexInit })
} else {
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
function vuexInit () {
const options = this.$options
// 如果当前实例有传入store对象则取当前若无则像上取,确保每个实例都有store属性
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
从上述源码可知项目中的所有组件实例都拥有指向同一状态空间的能力,那么如果只是做全局属性那么一个普通的js对象即可,用vuex的目的是因为他可以完美的和vue本身的数据链路追踪无缝切合,如同拥有了一个全局的data属性,无论是收集依赖触发页面更新,监听数据变化等data属性能做的他都能做,那和data如此相像的就只有data本身了,所以vuex的store本身其实就是一个vue实例,在不考虑其他因素的前提下,只做全局响应式 数据共享,那么在vue的原型链上挂载一个vue实例一样可以做到相同的效果,使用过vuex的coder知道我们往往需要从 store 中的 state 中派生出一些状态,而不是简单的直接访问state
以官网实例来说
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
当我们访问this.$store.doneTodos时将会返回最新的todos中done为true的数据,同理当todos发生变化时(set被触发)doneTodos也会随着更新依赖,这么看是不是和我们之前看的computed一模一样,可以从以下的源码看到resetStoreVM方法中将遍历每个getter并生成computed作为参数传入到vue的构造方法中,由此可以这样理解当store中的state内数据被修改时相当于vue实例的data属性被修改触发其set方法更新依赖,当getter内的数据被访问时相对于computed属性被访问返回最新的依赖数据
部分源码
/**
* forEach for object
* 遍历对象
*/
function forEachValue (obj, fn) {
Object.keys(obj).forEach(function (key) { return fn(obj[key], key); });
}
// 遍历vuex 内每个module内的getter属性
module.forEachGetter(function (getter, key) {
var namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
{
console.error(("[vuex] duplicate getter key: " + type));
}
return
}
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
};
}
/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
/* 存放之前的vm对象 */
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
/* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
/* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
Vue.config.silent = true
/* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
/* 使能严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
/* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
我们再通过几个常用的vuex用法来巩固理解
例1:页面视图更新
如下代码所示当组件B触发点击事件更新 $store.count 时,组件A视图将会随之更新,因为在组件A compile 时
count作为响应式数据将count属性的get方法将收集用于更新页面的watcher实例作为依赖,当组件B修改count时触发set方法更新依赖
component A
<div id="app">
{{ $store.count }}
</div>
<script>
export default {
}
component B
<div id="app">
<div @click="$store.count+=1"></div>
</div>
<script>
export default {
methods:{
}
}
</script>
实例2:监听store数据变化
在业务开发过程中我们经常会遇到需要监听store数据变化的情况,我们一般的做法是在这个vue实例中将需要观测的state作为computed属性return出来,让其挂载到当前this实例中,然后通过watch属性观测其变化
在下面的实例中可以看到当$store.count变化时将触发watch属性中的回调方法,
由之前分析可知,当组件实例化中将在方法initComputed内遍历其computed属性,为每一个compted属性生成一个内部watcher实例,s_count的表达式将作为watcher的getter被访问,又因为store.count做为store内部vue实例的响应式数据,当其被访问时将会收集这个watcher作为依赖,同时s_count将被挂载到当前this实例上。同时在initWatch方法中当,将会为watch属性s_count生成一个内部watcher实例,this实例上的s_count属性(被computed挂载将作为watcher的getter被访问,s_count将作为该this实例生成的新属性收集这个watcher作为依赖,s_count的表达式将作为回调方法(cb) 传入watcher实例中,当store.count发生变化时触发其set方法,调用dep.notify依次触发依赖更新,s_count将获取到最新值,s_count更新后触发当前实例中的依赖再依次更新,从而触发回调方法
可以看到这个操作的关键在于当前组件实例中computed的内部watcher被vuex内的vue实例中的响应数据当作依赖被收集,这是两个不同的vue实例进行数据流通的关键
<div id="app">
{{ $store.count }}
</div>
<script>
export default {
watch:{
s_count:{
callBack balabalala......
}
}
computed:{
s_count:{
return $store.count
}
}
}
</script>
抛开所有细节来说整个vue的数据链路追踪系统其实就是一套简单的观察者模式,我们在开发中根据业务需求创建数据间的依赖关系,vue所做的其实就是简单的收集依赖关系然后触发更新。很简单的东西,可往往很多coder都理不清晰,在于网上的相关资料大都良莠不齐,很多人都喜欢写一些什么vue双向绑定的实现, 模仿vue实现双向绑定之类的东西,大多就是通过Object.defineProperty()重写了get ,set。这种文章实现起来容易,看起来也爽,五分钟的光景感觉自己就是尤大了, 其实到头来还是一团雾水,归根结底还是要阅读源码,配合大神们的注释和官网解释看效果更佳