前言
通过手写Vue2源码,更深入了解Vue; 在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅; 另外我会编写一些开发文档,阐述编码细节及实现思路; 源码地址:手写Vue2源码
为何需要watch
在第五节我们实现了数据的响应式,在渲染的时候收集依赖,在数据更新时响应式更新视图。但是只有在render中使用到的数据才会进行依赖收集,实际开发中,我们需要自己监听一些数据,进而执行一些操作。
实现思路
- 在initWatch中创建watcher
- 在watcher中执行
get()
,返回监听的数据;在此期间触发数据的getter,进行依赖收集 - 在数据改变时,触发数据的setter,派发更新,或在数组原型中派发更新
- 执行
watcher.run()
,再次执行this.getter()
,获取到最新的newValue,执行回调函数
看一下Vue中watch的用法
new Vue({
watch: {
name(newVal, oldVal) {},
'obj.name'(newVal, oldVal) {},
age: [
function(newVal, oldVal) {},
function(newVal, oldVal) {},
],
b: 'someMethod', // 直接接方法名
id: {
handler: (newVal, oldVal) => {},
immediate: true
}
}
})
可以看到,watch中,key可能是单个直接的属性,也可能是层次很深的对象属性(例如obj.a.b.c);watch[key]
的类型可能是函数、对象、数组、字符串等。
initWatch
在stateMixin(Vue)
中执行initState,在initState中执行initWatch(初始化watch)
// src/state.js
export function initState(vm) {
const opts = vm.$options;
// 初始化data
if (opts.data) {
initData(vm);
}
// 初始化watch
if (opts.watch) {
initWatch(vm);
}
}
// 初始化watch
function initWatch(vm) {
let watch = vm.$options.watch
for (let k in watch) {
const handler = watch[k]
if (Array.isArray(handler)) {
handler.forEach((handle) => {
createWatcher(vm, k, handle)
})
} else {
createWatcher(vm, k, handler)
}
}
}
// 创建watcher
function createWatcher(vm, key, handler, options = {}) {
if (typeof handler === "object") {
options = handler; //保存用户传入的对象
handler = handler.handler; //是函数
}
// 如果handler是字符串,说明是一个方法名,直接从实例中调用
if (typeof handler === "string") {
handler = vm[handler];
}
// watch 相当于调用了 vm.$watch()
return vm.$watch(key, handler, options);
}
export function stateMixin(Vue) {
Vue.prototype.$watch = function (exprOrFn, cb, options) {
const vm = this;
// user: true 表示创建的是一个用户watcher
let watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true });
if (options.immediate) {
cb(watcher.value); // 如果立刻执行
}
};
}
分析一下代码:
- initWatch其实最后执行了
return vm.$watch(key, handler, options)
,key为监听的数据,handler为回调函数,options为watch的配置(比如immediate、root) - $watch 是挂载在vue原型上的方法,它创建了一个用户watcher,它与渲染watcher的区别就是:
- 用户watcher的options中增加属性
user: true
,而渲染watcher的options为true - 用户watcher的第二个参数exprOrFn为监听的数据,而渲染watcher的exprOrFn为updateComponent方法
- 用户watcher的options中增加属性
改造Watcher
用户自定义watcher需要解决的几个问题:
- 什么时候收集依赖
- 派发更新时如何获取到newVal和oldVal,并触发回调函数
import { pushTarget, popTarget } from "./dep";
import { queueWatcher } from "./scheduler";
import { isObject } from "../util/index";
// 全局变量id 每次new Watcher都会自增
let id = 0;
export default class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.user = !!options.user; // 表示是不是用户watcher
this.id = id++; // watcher的唯一标识
this.deps = []; //存放dep的容器
this.depsId = new Set(); //用来去重dep
/**
* 1. 渲染watcher中,exprOrFn为updateComponent(),是一个函数
* 2. 在用户watcher中,exprOrFn为字符串(watch中的属性名,即监听地属性)
*/
// 如果是渲染watcher(exprOrFn为vm._update(vm._render())) 或者 computed watcher(exprOrFn为计算属性里的getter)
if (typeof exprOrFn === 'function') {
this.getter = exprOrFn
} else {
// 如果是user watcher,exprOrFn为监听的数据
this.getter = function () {
// watcher监听的数据可能是第一层 obj1,也可能是深层的某个属性 obj1.a.b,后者需要处理成 vm.obj1.a.b
let path = exprOrFn.split('.')
let obj = vm
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]
}
// 【*****】watch监听的数据一般定义在data中,当创建user watcher时会执行watcher里的get(),读取监听的数据;这个过程中就触发了数据的getter,会进行依赖收集(当前的Dep.target为user watcher(在get()中设置的),所以收集的是user watcher)
return obj
}
}
this.value = this.get();
}
// new Watcher时会执行get方法;之后数据更新时,直接手动调用get方法即可
get() {
pushTarget(this);
const res = this.getter.call(this.vm); // 如果是用户watcher,则上一次执行getter得到的值即为oldValue
popTarget();
return res;
}
// ...
run() {
// 执行getter,更新视图/获取新值
const newVal = this.getter(); // 新值
const oldVal = this.value; //老值
this.value = newVal; // newVal就成为了现在的值;为了保证下一次更新时,上一次的新值是下一次的老值
// 如果是用户watcher
if (this.user) {
if (newVal !== oldVal || isObject(newVal)) {
this.cb.call(this.vm, newVal, oldVal);
}
} else {
// 渲染watcher
this.cb.call(this.vm);
}
}
}
总结一下user watcher:
- 收集依赖过程:在创建watcher时,执行
this.get()
,会返回监听的数据(设为this.value
,即下一次更新时的oldVal),触发相关数据的依赖收集 - 更新过程:当监听的数据改变时,会派发更新给相关的watcher;当之前收集的用户watcher被通知更新时,最终会执行
run()
;在run里面执行this.getter()
(【注意】:不要执行this.get()
,因为get()
会重复收集依赖)获取到newVal、以及oldVal=this.val
,执行回调函数,传入新旧value。
问:如果监听的是
$route.path
,它并没有经过数据劫持,在创建user watcher实例,执行Watcher.get()
时应该是无法触发数据的getter,那么如何收集依赖的?当$route.path
改变时应该也不会触发数据setter,又何如派发更新?
系列文章
- 手写Vue2源码(一)—— 环境搭建
- 手写Vue2源码(二)—— 数据劫持
- 手写Vue2源码(三)—— 模板编译
- 手写Vue2源码(四)—— 初次渲染
- 手写Vue2源码(五)—— 观察者模式
- 手写Vue2源码(六)—— 异步更新及nextTick
- 手写Vue2源码(七)—— 侦听属性
- 手写Vue2源码(八)—— 计算属性
- 手写Vue2源码(九)—— 混入原理与生命周期
- 手写Vue2源码(十)—— 组件原理
- 手写Vue2源码(十一)—— diff算法
- 手写Vue2源码(十二)—— keep-alive
- 手写Vue2源码(十三)—— 全局API
- vue-router原理解析
- vuex原理解析
- vue3原理解析