背景
一直以来我对vue中的watch
和computed
都一知半解的,知道一点(例如:watch
和computed
的本质都是new Watcher
,computed
有缓存,只有调用的时候才会执行,也只有当依赖的数据变化了,才会再次触发...),然后就没有然后了。
也看了很多大佬写的文章,一大段一大段的源码列出来,着实让我这个菜鸡看的头大,自然也就不想看了。最近,我又开始学习vue源码,才真正理解了它们的实现原理。
data() {
return {
msg: 'hello guys',
info: {age:'18'},
name: 'FinGet'
}
}
复制代码
watcher 观察者
watcher
是什么?侦听器?它就是个类class
!
class Watcher{
constructor(vm,exprOrFn,callback,options,isRenderWatcher){
}
}
复制代码
vm
vue实例exprOrFn
可能是字符串或者回调函数(有点懵就往后看,现在它不重要)options
各种配置项(配置啥,往后看)isRenderWatcher
是否是渲染Wathcer
initState
Vue 初始化中 会执行一个 initState
方法,其中有大家最熟悉的initData
,就是Object.defineProperty
数据劫持。
export function initState(vm) {
const opts = vm.$options;
// vue 的数据来源 属性 方法 数据 计算属性 watch
if(opts.props) {
initProps(vm);
}
if(opts.methods) {
initMethod(vm);
}
if(opts.data) {
initData(vm);
}
if(opts.computed){
initComputed(vm);
}
if(opts.watch) {
initWatch(vm, opts.watch);
}
}
复制代码
在数据劫持中,Watcher
的好基友Dep
(被观察者)出现了,Dep
就是为了把Watcher
存起来。
在观察者模式中,被观察者需要把所有要观察它的观察者收集起来,这样在自身改变时,就可以通知所有的观察者作出响应。
在vue2中,我们为每一个数据(属性),创建 一个 Dep 实例,也就是说,每一个属性都是一个被观察者,需要观察它的可能是(渲染watcher,计算属性watcher,自定义watcher),所以我们就把这些watcher都存起来
dep.append
,在数据被修改时,通知所有的watcher作出响应dep.notify
function defineReactive(data, key, val) {
let dep = new Dep();
Object.defineProperty(data, key, {
get(){
if(Dep.target) {
dep.depend(); // 收集依赖
}
return val;
},
set(newVal) {
if(newVal === val) return;
val = newVal;
dep.notify(); // 通知执行
}
})
}
复制代码
当
initData
的时候,Dep.target
啥也不是,所以收集了个寂寞。target
是绑在Dep
这个类上的,不是实例上的。
但是当$mount
之后,就不一样了。至于$mount
中执行的什么compile
、generate
、render
、patch
、diff
都不是本文关注的,不重要,绕过!
你只需要知道一件事:会执行下面的代码
new Watcher(vm, updateComponent, () => {}, {}, true); // true 表示他是一个渲染watcher
复制代码
updateComponent
就是更新哈,不计较具体执行,它现在就是个会更新页面的回调函数,它会被存在Watcher
的getter
中。它对应的就是最开始那个exprOrFn
参数。
嘿嘿嘿,这个时候就不一样了:
- 渲染页面就是调用了你定义的数据(别杠,定义了没调用),就会走
get
。 - 上面
new Watcher
的时候调用一个方法把这个实例放到Dep.target
上,再次走到get
中就可以收集依赖了
pushTarget(watcher) {
Dep.target = watcher;
}
复制代码
这两件事正好凑到一起,那么 dep.depend()
就干活了。
所以到这里可以明白一件事,所有的
data
中定义的数据,只要被调用,它都会收集一个渲染watcher
,也就是数据改变,执行set
中的dep.notify
就会执行渲染watcher
下图就是定义了msg
、info
、name
三个数据,它们都有个渲染Watcher
:
眼尖的小伙伴应该看到了msg
中还有两个watcher
,一个是用户定义的watch
,另一个也是用户定义的watch
。啊,当然不是啦,vue
是做了去重的,不会有重复的watcher
,正如你所料,另一个是computed watcher
;
用户watch
我们一般是这样使用watch的:
watch: {
msg(newVal, oldVal){
console.log('my watch',newVal, oldVal)
}
// or
msg: {
handler(newVal, oldVal) {
console.log('my watch',newVal, oldVal)
},
immediate: true
}
}
复制代码
这里会执行一个initWatch
,一顿操作之后,就是提取出exprOrFn
(这个时候它就是个字符串了(msg))、handler
、options
,这就和Watcher
莫名的契合了,然后就顺理成章的调用了vm.$watch
方法。
Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
options.user = true; // 标记为用户watcher
// 核心就是创建个watcher
const watcher = new Watcher(this, exprOrFn, cb, options);
if(options.immediate){
cb.call(vm,watcher.value)
}
}
复制代码
来吧,避免不了看看这段代码(本来粘贴了好长一段,但说了大白话,我就把和这段关系不大的给删减了):
class Watcher{
constructor(vm,exprOrFn,callback,options,isRenderWatcher){
this.vm = vm;
this.callback = callback;
this.options = options;
if(options) {
this.user = !!options.user;
}
this.id = id ++;
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn; // 将传过来的回调函数 放到getter属性上
} else {
// 当exprOrFn 是个字符串的时候,就需要去取值(返回一个取值函数 闭包)
this.getter = parsePath(exprOrFn);
// 如果getter不存在 会赋值一个 函数
if (!this.getter) {
this.getter = (() => {});
}
}
this.value = this.get();
}
get(){
pushTarget(this); // 把当前watcher 存入dep中
let result = this.getter.call(this.vm, this.vm); // 渲染watcher的执行 这里会走到observe的get方法,然后存下这个watcher
popTarget(); // 再置空 当执行到这一步的时候 所以的依赖收集都完成了,都是同一个watcher
return result;
}
}
复制代码
// 这个就是拿来把msg的值取到,取到的就是oldVal
function parsePath(path) {
if (!path) {
return
}
var segments = path.split('.');
return function(obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
复制代码
大家可以看到,new Watcher
会执行一下get
方法,当是渲染Watcher就会渲染页面,执行一次updateComponent
,当它是用户Watcher就是执行parsePath
中的返回的方法,然后得到一个值this.value
也就是oldVal
。
嘿嘿嘿,既然取值了,那又走到了msg
的get
里面,这个时候dep.depend()
又干活了,用户Watcher就存进去了。
当msg
改变的时候,这过程中还有一些骚操作,不重要哈,最后会执行一个run
方法,调用回调函数,把newValue
和oldValue
传进去:
// dep
notify() {
this.subs.forEach(watcher => watcher.update())
}
复制代码
// watch
update(){
if (this.lazy) {
// 计算属性 需要更新
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
// 异步更新 多次修改同一个值 只更新一次
queueWatcher(this);
}
// queueWatcher(this);
// this.get()
}
run(){
let oldValue = this.value;
// 再执行一次就拿到了现在的值,会去重哈,watcher不会重复添加
let newValue = this.get();
this.value = newValue;
if(this.user && oldValue != newValue) {
// 是用户watcher, 就调用callback 也就是 handler
this.callback(newValue, oldValue)
}
}
复制代码
computed
computed: {
c_msg() {
return this.msg + 'computed'
}
// or
c_msg: {
get() {
return this.msg + 'computed'
},
set() {}
}
},
复制代码
computed
有什么特点:
- 调用的时候才会执行
- 有缓存
- 依赖改变时会重新计算
调用的时候执行,我怎么知道它在调用?嘿嘿嘿,Object.defineProperty
不就是干这事的嘛,巧了不是。
依赖的数据改变时会重新计算,那就需要收集依赖了。还是那个逻辑,调用了this.msg
-> get
-> dep.depend()
。
function initComputed(vm) {
let computed = vm.$options.computed;
const watchers = vm._computedWatchers = {};
for(let key in computed) {
const userDef = computed[key];
// 获取get方法
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// 创建计算属性watcher lazy就是第一次不调用
watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
defineComputed(vm, key, userDef)
}
}
复制代码
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: () => {},
set: () => {}
}
function defineComputed(target, key, userDef) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
} else {
sharedPropertyDefinition.get = createComputedGetter(userDef.get);
sharedPropertyDefinition.set = userDef.set;
}
// 使用defineProperty定义 这样才能做到使用才计算
Object.defineProperty(target, key, sharedPropertyDefinition)
}
复制代码
下面这一段最重要,上面的看一眼就好,上面做的就是把get
方法找出来,用Object.defineProperty
绑定一下。
class Watcher{
constructor(vm,exprOrFn,callback,options,isRenderWatcher){
...
this.dirty = this.lazy;
// lazy 第一次不执行
this.value = this.lazy ? undefined : this.get();
...
}
update(){
if (this.lazy) {
// 计算属性 需要更新
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this); // 这就是个陪衬 现在不管它
}
}
evaluate() {
this.value = this.get();
this.dirty = false;
}
}
复制代码
缓存就在这里,执行get
方法会拿到一个返回值this.value
就是缓存的值,在用户Watcher中,它就是oldValue
,写到这里的时候,对尤大神的佩服,又加深一层。🐂🍺plus!
function createComputedGetter(key) {
return function computedGetter() {
// this 指向vue 实例
const watcher = this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) { // 如果dirty为true
watcher.evaluate();// 计算出新值,并将dirty 更新为false
}
// 如果依赖的值不发生变化,则返回上次计算的结果
return watcher.value
}
}
}
复制代码
watcher
的update
是什么时候调用的?也就是数据更新调用dep.notify()
,dirty
就需要变成true
,但是计算属性还是不能马上计算,还是需要在调用的时候才计算,所以在update
的时候只是改了dirty
的状态!然后下次调用的时候就会重新计算。
依赖数据修改 -> 通知更新后 -> 重置 脏数据标志位 -> 页面读取 computed 时(渲染watcher执行)再更新值(重新计算)。
class Dep {
constructor() {
this.id = id ++;
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
depend() {
Dep.target.addDep(this);
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
复制代码
这里写一个initComputed
的伪代码:
function initComputed(vm) {
const computed = vm.$options.computed;
const watchers = {};
for (const key in computed) {
const userDef = computed[key];
const getter = typeof userDef === 'function' ? userDef : userDef.get;
// 1. 为计算属性创建一个watcher
watchers[key] = new Watcher(vm, getter, () => {}, {lazy: true});
// 2. 为计算属性创建一个代理(因为我们要监听计算属性在哪里调用,调用的时候是否需要重新计算)
Object.defineProperty(vm, key, {
get: () => {
// 3. 访问计算属性时,触发watcher的get方法
const watcher = watchers[key];
// 脏数据标志位 当依赖数据变化,调用对应的watcher.update 重置了
// dirty,这样再次调用了compouted的该属性,就会重新计算
// 一般情况下,依赖数据的改变,会导致页面刷新(渲染watcher.update),在template中
// 使用的计算属性也就会再次触发get,然后重新计算
if (watcher.dirty) {
watcher.evaluate();
}
// 如果依赖的值不发生变化,则返回上次计算的结果
return watcher.value;
}
});
}
}
复制代码
vue2异步更新
总结
watch
和computed
本质都是Watcher
,都被存放在Dep
中,当数据改变时,就执行dep.notify
把当前对应Dep
实例中存的Watcher
都run
一下,这样执行了渲染Watcher
页面就刷新了;- 每一个数据都有自己的
Dep
,如果他在模版中被调用,那它一定有一个渲染Watcher
; initData
时,是没有Watcher
可以收集的;- 发现没有,渲染
Watcher
和Computed
中,exprOrFn
都是函数,用户Watcher
中都是字符串。
文章中的代码是简略版的,还有很多细枝末节的东西没说,不重要也只是针对本文不重要,大家可以去阅读源码更深入的理解。