watch
的核心就是观测一个响应式数据,当数据变化时通知并执行回调 (那也就是说它本身就是一个effect
)
watch
的本质就是effect
内部会对用户填写的数据进行依赖收集
watch
监控普通对象的时候 对象的属性发生变化 是不会触发的因为 对象的引用地址没有发生变化
watch
等价于effect
内部会保存老值和新值 调用方法
使用场景
1、监控对象 可以监控数据变化 数据变化了就重新执行
监控对象无法区分前后的新值和老值
const state= reactive({flag: true, name: 'lyp',address:{num: 6}, age: 30})
// 监测一个响应式值的变化
watch(state,(oldValue,newValue)=>{
console.log(oldValue,newValue) // 两个值一样
})
setTimeout(() => {
state.name ='jdlyp'
// 也可以触发watch
// state.address.num='10000'
}, 1000);
2、可以去监控一个函数,函数的返回值就是老值 更新后获取新值
不能
直接写成state.address.num
const state= reactive({flag: true, name: 'lyp',address:{num: 6}, age: 30})
watch(()=>state.address.num,(oldValue,newValue)=>{ // 监测一个响应式值的变化
console.log(oldValue,newValue)
})
setTimeout(() => {
state.address.num=10000
}, 1000);
3、连续触发watch时需要清理之前的watch操作 达到以最后一次返回结果为准的目的
当用户在输入框中输入的时候 我们根据输入的内容返回结果 (ajax等异步)
实现步骤
- 1)第一次调用watch的时候 传入一个取消的回调
- 2)第二次调用watch的时候执行上一次传入的回调
onCleanup
是 vue源码提供给用户的钩子用户传递给
onCleanup
的函数会由vue源码在下一次
自动调用
const state= reactive({flag: true, name: 'lyp',address:{num: 6}, age: 30})
let i = 2000;
// 模拟ajax 实现 第一次比第二次返回的晚
function getData(timer){
return new Promise((resolve,reject)=>{
setTimeout(() => {
resolve(timer)
}, timer);
})
}
// 每次数据变化 都会执行watch的回调函数
// 每次都会形成一个私有作用域 传入的onCleanup函数 执行改变的是上一个私有作用域的clear值
// onCleanup 是 vue源码提供给用户的钩子
watch(()=>state.age,async (newValue,oldValue,onCleanup)=>{
let clear = false;
// 将 终止的调函数 给到vue源码中的cleanup(也就是传递给下一层)
// 终止函数的调用会有vue源码自动执行
onCleanup(()=>{
clear = true;
})
i-=1000;
let r = await getData(i); // 第一次执行1s后渲染1000, 第二次执行0s后渲染0, 最终应该是0
if(!clear){document.body.innerHTML = r;}
},{flush:'sync'}); // {flush:'sync'} 表示同步执行
state.age = 31;
state.age = 32;
代码实现
- 1、如果是响应式对象,循环一遍属性, 取一遍值来收集依赖,将source 包装成箭头函数赋值给 getter
- 2、如果是函数则让函数作为getter即可
- 3、创建effect 监控自己构造的getter函数 数据变化后重新执行job 然后获取新值
- 4、运行保存老值 run 的时候 让getter执行 也就是source执行 或者循环source
- 5、需要立即执行,则立刻执行任务
- 6、回调时传入onCleanup函数 将钩子暴露给用户
- 7、保存用户传入的参数为cleanup
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive'
}
export function isReactive(value){
return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
// 遍历的是对象的话 考虑对象中有循环引用的问题
function traverse(value,seen = new Set()){
if(!isObject(value)){ // 不是对象就不再递归了
return value
}
// 如果循环过就不再考虑 直接返回上次的对象就行 解决 object= {a:object} 的问题
if(seen.has(value)){
return value;
}
seen.add(value);
for(const k in value){ // 递归访问属性用于依赖收集
traverse(value[k],seen)
}
return value
}
// source 是用户传入的对象 cb就是对应的用户回调 immediate是否立即执行一次回调
export function watch(source,cb,{immediate} = {} as any){
let getter;
// 1、如果是响应式对象 循环一遍属性
if(isReactive(source)){
// 对用户传入的数据循环一遍来收集effect 只需要循环一遍就好
//(递归循环,只要循环就会访问对象的每一个属性,在effect中 访问属性的时候 会进行依赖收集)
// 包装成effect对应的fn, 函数内部进行遍历达到依赖收集的目的
getter = () => traverse(source)
console.log(getter)
}else if(isFunction(source)){
getter = source // 2、如果是函数则让函数作为getter即可
}
let oldValue;
let cleanup;
let onCleanup = (fn) =>{
cleanup = fn;// 7、保存用户传入的参数为cleanup
}
const job = () =>{
// 值变化时再次运行effect函数,获取新值
const newValue = effect.run();
// 第一次没有 下次watch执行前调用上次注册的回调
if(cleanup) cleanup();
// 6、回调时传入onCleanup函数 将钩子暴露给用户
cb(newValue,oldValue,onCleanup);
oldValue = newValue
}
// 3、创建effect 监控自己构造的getter函数 数据变化后重新执行job 然后获取新值
const effect = new ReactiveEffect(getter,job)
if(immediate){ // 5、需要立即执行,则立刻执行任务
job();
}
// 4、运行保存老值 run 的时候 让getter执行 也就是source执行 或者循环source
oldValue = effect.run();
}
分步讲解
监测响应式对象
function traverse(value,seen = new Set()){
if(!isObject(value)){
return value
}
if(seen.has(value)){
return value;
}
seen.add(value);
for(const k in value){ // 递归访问属性用于依赖收集
traverse(value[k],seen)
}
return value
}
export function isReactive(value){
return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
export function watch(source,cb){
let getter;
if(isReactive(source)){ // 如果是响应式对象
getter = () => traverse(source)// 包装成effect对应的fn, 函数内部进行遍历达到依赖收集的目的
}
let oldValue;
// job就是 Effect的调度函数
const job = () =>{
const newValue = effect.run(); // 值变化时再次运行effect函数,获取新值
cb(newValue,oldValue);
oldValue = newValue
}
const effect = new ReactiveEffect(getter,job) // 创建effect
oldValue = effect.run(); // 运行保存老值
}
监测函数
export function watch(source,cb){
let getter;
if(isReactive(source)){ // 如果是响应式对象
getter = () => traverse(source)
}else if(isFunction(source)){
getter = source // 如果是函数则让函数作为fn即可
}
// ...
}
watch中回调执行时机
export function watch(source,cb,{immediate} = {} as any){
const effect = new ReactiveEffect(getter,job) // 创建effect
if(immediate){ // 需要立即执行,则立刻执行任务
job();
}
oldValue = effect.run();
}
watch中cleanup实现
连续触发watch时需要清理之前的watch操作
onCleanup 是 vue源码提供给用户的钩子
用户传递给onCleanup的函数会由vue源码自动调用
// 使用
const state = reactive({ flag: true, name: 'lyp', age: 30 })
let i = 2000;
function getData(timer){
return new Promise((resolve,reject)=>{
setTimeout(() => {
resolve(timer)
}, timer);
})
}
watch(()=>state.age,async (newValue,oldValue,onCleanup)=>{
let clear = false;
onCleanup(()=>{ // 利用钩子函数将 取消的回调传给下一层
clear = true;
})
i-=1000;
let r = await getData(i); // 第一次执行1s后渲染1000, 第二次执行0s后渲染0, 最终应该是0
if(!clear){document.body.innerHTML = r;}
},{flush:'sync'});
state.age = 31;
state.age = 32;
// 源码实现
let cleanup;
let onCleanup = (fn) =>{
cleanup = fn; // 保存用户的终止函数
}
const job = () =>{
const newValue = effect.run();
if(cleanup) cleanup(); // 第一次没有 下次watch执行前调用上次注册的回调
cb(newValue,oldValue,onCleanup); // 调用用户回调传入onCleanup函数 暴露钩子
oldValue = newValue
}