说明
【vue.js 设计与实现】 霍春阳 学习笔记
接上文
- 响应系统设计—Part 1(init)
- 响应系统设计—Part 2(bug修复)
- 响应系统设计—Part 3(支持嵌套Effect)
- 响应系统设计—Part 4(支持调度执行)
- 响应系统设计—Part 5(支持computed属性)
实现—Version 0.6(支持watch)
watch的作用是什么?
通过watch监听响应式对象,对象发生变更时,执行回调
如何监听对象发生变更?
- 可以通过一个读取对象的
effect来实现,对象所有的属性变更都需要,所以要遍历读取对象的属性 - 也允许开发者自定义只监听的对象的某个属性,此时传入一个读取属性的函数即可
监听到变更后,怎执行回调?
可以通过Part4中讲到的调度执行来支持,当副作用函数有传入scheduler时,就会触发scheduler执行而不是副作用函数执行。
1. init
function watch(source, callback) {
// 1. 读取对象属性的函数
let getter;
if(typeof source === 'function'){
// 1.1 自定义要监听的属性
getter = source;
}else {
// 1.2 遍历所有属性
getter = () =>{
traverse(source);
};
}
//2. effect注册getter,建立联系;执行回调的scheduler函数
effect(getter,
{
scheduler: function() {
console.log('调度执行callback');
callback();
}
}
);
}
function traverse(value, seen = new Set()) {
if(typeof value !== 'object' || value === null || seen.has(value)) {
return;
}
// 数据遍历后,存起来,表示已读取,避免循环引用引起死循环
seen.add(value);
// 假设value是个对象,其他数据结构暂不考虑
for(const key in value){
traverse(value[key], seen);
}
return value;
}
2. callack的参数
我们在使用callback时,还需要拿到对象(某个属性)的新值和旧值,如何获取勒?
在Part5中,我们用到了lazy属性,它使得在注册Effect时,不执行副作用函数。
if(typeof source === 'function'){
// ...
}else {
// 增加return
getter = () =>{
return traverse(source);
};
}
// 监听的响应式对象变更前后的值
let newValue;
let oldValue;
const effectFn = effect(getter,
{
lazy: true, // 注册时不执行getter
scheduler: function() {
console.log('调度执行callback');
newValue = effectFn();
callback(newValue, oldValue);
// 这次的新值,是下一次的旧值
oldValue = newValue;
}
}
);
// 第一次读取,执行getter建立联系,并得到初始化的旧值
oldValue = effectFn();
如果
watch的是响应式对象
因为是对象,
oldValue和newValue都是proxyData的引用,读取属性值时自然相同。
3. 立即执行watch
我们使用watch监听时,默认情况下是watch的回调只会在响应式数据发生变化时才执行。在vue.js中,还可以配置immediate: true来指定回调立即执行,如何实现勒?
/* watch函数增加options参数 */
function watch(source, callback, options = {}) {
// 读取对象属性的函数
let getter;
// 省略getter部分逻辑
let newValue;
let oldValue;
/* 将scheduler中的逻辑独立出来,以便单独调用 */
const job = function() {
newValue = effectFn();
console.log('调度执行callback');
callback(newValue, oldValue);
// 这次的新值,是下一次的旧值
oldValue = newValue;
};
const effectFn = effect(
getter,
{
lazy: true, // 注册时不执行getter
scheduler: function() {
job();
}
}
);
/*
* 如果声明了立即执行,则立即执行job
* 第一次执行,此时的oldValue一定是undefined,也符合预期
*/
if(options.immediate) {
job();
}else{
// 第一次读取,执行getter建立联系,并得到初始化的旧值
oldValue = effectFn();
}
}
4.过期的副作用
开发中watch中可能会处理一些异步的情况,常见的就是发请求了。
如下:
/**
* 以下是演示用例
*/
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 2,
});
// 发送请求,控制响应的时机
let time = 500;
function request(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, time);
});
}
window.onload = function() {
watch(
proxyData,
function(newValue) {
// 使用name属性值,方便观察响应是第几次请求的结果
request(newValue.name).then((data) => {
const str = 'name=' + data;
console.log('更新html为:' + str);
document.body.innerHTML = str;
});
// 使后续的请求能更快的响应
time = time - 200;
}
);
proxyData.name = '鼠标键盘';
setTimeout(() => {
proxyData.name = '收纳包';
}, 50);
}
过程:
- 修改
name为【鼠标键盘】 - 触发
watch发送第一次请求 - 50ms后修改
name为【收纳包】 - 触发
watch发送第二次请求 - 收到第二次请求的响应,修改html为【收纳包】
- 收到第一次请求的响应,修改html为【鼠标键盘】
很明显,这个结果不是我们期望的。我们总是希望最后发出的请求的响应是有效的。 如何支持勒?
1)watch函数的callback增加第三个参数
function watch(source, callback, options = {}) {
//... getter
let newValue;
let oldValue;
/** 新增:清理函数,由回调传入 */
let cleanUp;
/** 新增:callback的第三个参数 */
function onInvalidate(fn) {
console.log('\n注册cleanUp\n\n')
cleanUp = fn;
}
const job = function() {
newValue = effectFn();
/** 新增:执行callback前先清理 */
if(cleanUp) {
console.log('执行cleanUp')
cleanUp();
}
console.log('调度执行callback');
/* 新增:传入onInvalidate参数 */
callback(newValue, oldValue, onInvalidate);
// 这次的新值,是下一次的旧值
oldValue = newValue;
};
//... 注册Effect等
}
2)watch使用部分,callback中增加对onInvalidate的使用
window.onload = function() {
watch(
proxyData,
function(newValue, _, onInvalidate) {
/*新增: 标记本次回调已过期*/
let expired = false;
let flag = newValue.name;
onInvalidate(() => {
console.log('expired 过期, ' + flag);
expired = true;
});
request(flag).then((data) => {
console.log('收到响应,expired: ' , expired);
/*新增:未过期才赋值 */
if(!expired){
const str = 'name=' + data;
console.log('更新html为:' + str);
document.body.innerHTML = str;
}
});
time = time - 200;
}
);
proxyData.name = '鼠标键盘';
setTimeout(() => {
proxyData.name = '收纳包';
}, 50);
}
满足期望:
- 修改
name为【鼠标键盘】 - 触发
watch,执行jobcleanUp为空- 执行
callback回调。运行onInvalidate注册了cleanUp,随后发送了第一次请求
- 50ms后修改
name为【收纳包】 - 触发
watch,执行job- 有
cleanUp,执行cleanUp使上一次副作用函数中的expired为true - 执行
callback,注册cleanUp,随后发送第二次请求
- 有
- 收到第二次请求的响应,修改html为【收纳包】
- 收到第一次请求的响应,副作用函数内的
expired=true,不赋值
5. 完整代码
let activeEffect = null;
// 执行副作用函数
const effectStack = [];
/**
* 注册副作用函数
* @param {Function} effectFn
* @param {Object} options // 新增
* @param {Function} [options.scheduler] 副作用函数执行调度器
* @param {Boolean} [options.lazy] 延迟执行
*/
function effect(effectFn, options = {}) {
/* effectFn对象,增加deps属性 */
const formatEffectFn = () => {
/* start-清理关联的set */
for(let i = 0; i < formatEffectFn.deps.length; i++){
const effectSet = formatEffectFn.deps[i];
effectSet.delete(formatEffectFn);
}
// 重置数组
formatEffectFn.deps.length = 0;
/* end */
// 一定要放在清理set之后,因为执行effectFn会重新建立关联
activeEffect = formatEffectFn;
effectStack.push(formatEffectFn);
const result = effectFn();
effectStack.pop();
/* effectFn执行后,重置 */
activeEffect = effectStack[effectStack.length - 1];
return result;
};
formatEffectFn.deps = [];
formatEffectFn.raw = effectFn;
formatEffectFn.options = options;
/* 如果是延迟执行,此时不执行副作用函数 */
if(!options.lazy) {
formatEffectFn();
}
return formatEffectFn;
}
// 存储副作用函数
const bucket = new WeakMap();
/**
* 收集target对象key的副作用函数
* @param {Object} target
* @param {String|Symbol} key
* @return {void}
*/
function track(target, key){
if(!activeEffect) return;
let targetMap = bucket.get(target);
if(!targetMap){
targetMap = new Map();
bucket.set(target, targetMap);
}
let effectSet = targetMap.get(key);
if(!effectSet){
effectSet = new Set();
targetMap.set(key, effectSet);
}
effectSet.add(activeEffect);
/* effect中关联和它相关的set */
activeEffect.deps.push(effectSet);
}
/**
* 触发target对象key的副作用函数执行
* @param {Object} target
* @param {String|Symbol} key
* @return {void}
*/
function trigger(target, key) {
console.log('触发trigger');
const targetMap = bucket.get(target);
if(!targetMap) return;
const effectSet = targetMap.get(key);
if(!effectSet) return;
const effectsToRun = new Set(effectSet);
effectsToRun.forEach(effectFn => {
/* 守卫条件 */
if(effectFn !== activeEffect){
console.log('取出effectFn');
if(effectFn.options.scheduler) {
console.log('effectFn执行由调度器来控制');
effectFn.options.scheduler(effectFn);
}else{
effectFn();
}
}else{
console.log('不执行effectFn');
}
});
}
/**
* 创建响应式对象
* @param {*} obj
* @returns
*/
function reactive(obj){
return new Proxy(obj, {
get(target, key) {
// console.log(`触发get,key = ${key}`);
/* 存入副作用函数 */
track(target, key);
return target[key];
},
set(target, key, newVal) {
console.log(`触发set,key = ${key}`);
target[key] = newVal;
// 取出并执行副作用函数
trigger(target, key);
return true;
}
});
}
/**
* 注册计算属性
* @param {*} getter 计算属性对象是的getter函数
*/
function computed(getter) {
let isDirty = true;
let val;
// lazy: 注册Effect时,不执行副作用函数
const effectFn = effect(getter, {
lazy: true,
/* 调度器中(依赖发生改变触发trigger,执行调度器),重置isDirty */
scheduler: function() {
isDirty = true;
/** 计算试行依赖的响应式对象发生变更时,手动调用obj的trigger */
trigger(obj, 'value');
}
});
const obj = {
get value() {
if(isDirty){
val = effectFn();
isDirty = false;
}else{
console.log('计算属性不需要重新计算');
}
console.log('读取属性value');
/* 读取value是,手动调用track函数,追踪obj */
track(obj, 'value');
return val;
}
}
return obj;
}
function watch(source, callback, options = {}) {
// 1. 读取对象属性的函数
let getter;
if(typeof source === 'function'){
// 1.1 自定义要监听的属性
getter = source;
}else {
// 1.2 遍历所有属性
getter = () => {
return traverse(source);
};
}
let newValue;
let oldValue;
/** 新增:清理函数,由回调传入 */
let cleanUp;
/** 新增:callback的第三个参数 */
function onInvalidate(fn) {
console.log('\n注册cleanUp\n\n')
cleanUp = fn;
}
const job = function() {
newValue = effectFn();
/** 新增:执行callback前先清理 */
if(cleanUp) {
console.log('执行cleanUp')
cleanUp();
}
console.log('调度执行callback');
/* 新增:传入onInvalidate参数 */
callback(newValue, oldValue, onInvalidate);
// 这次的新值,是下一次的旧值
oldValue = newValue;
};
const effectFn = effect(
getter,
{
lazy: true, // 注册时不执行getter
scheduler: function() {
job();
}
}
);
if(options.immediate) {
job();
}else{
// 第一次读取,执行getter建立联系,并得到初始化的旧值
oldValue = effectFn();
}
}
function traverse(value, seen = new Set()) {
if(typeof value !== 'object' || value === null || seen.has(value)) {
return;
}
// 数据遍历后,存起来,表示已读取,避免循环引用引起死循环
seen.add(value);
// 假设value是个对象,其他数据结构暂不考虑
for(const key in value){
traverse(value[key], seen);
}
return value;
}
/**
* 以下是演示用例
*/
const proxyData = reactive({
name: '牛牛手办',
price: 10,
count: 2,
});
let time = 500;
function request(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, time);
});
}
window.onload = function() {
watch(
proxyData,
function(newValue, _, onInvalidate) {
/*新增: 标记本次回调已过期*/
let expired = false;
let flag = newValue.name;
onInvalidate(() => {
console.log('expired 过期, ' + flag);
expired = true;
});
request(flag).then((data) => {
console.log('收到响应,expired: ' , expired);
/*新增:未过期才赋值 */
if(!expired){
const str = 'name=' + data;
console.log('更新html为:' + str);
document.body.innerHTML = str;
}
});
time = time - 200;
}
);
proxyData.name = '鼠标键盘';
setTimeout(() => {
proxyData.name = '收纳包';
}, 50);
}