前言
个人的学习笔记,用于方便日后复习用,顺便炒个冷饭。。。。。
讲道理,相信大部分人都大概知道响应式的原理,就是实例化的时候,遍历data和props上的属性,通过Object.defineProperty监听,下面就写一写低配版的源码。
这一篇更新响应式和异步渲染,后面会更新watch,computed,watchEffect,set,以及vue中改写数组方法的hack方法
监听
这里就不遍历对象了,可以通过写一个ref
函数直接监听一个基础类型,
let ref = function (value) {
return Object.defineProperty({}, "value", {
get() {
console.log("get");
return value;
},
set(newValue) {
console.log("set");
value = newValue;
},
});
};
let x = ref(1);
console.log(x.value);
x.value = 2;
依赖
监听完成之后就是需要收集依赖
可以用一个Dep
类来储存不同属性各自得依赖,然后在获取数据得时候收集依赖,然后在更改值得时候,调用依赖中的方法,渲染DOM
class Dep {
constructor() {
this.deps = new Set();
}
depend() {
if (active) {
this.deps.add(active);
}
}
notify() {
this.deps.forEach((dep) => dep());
}
}
let ref = function (value) {
let dep = new Dep();
return Object.defineProperty({}, "value", {
get() {
dep.depend();
return value;
},
set(newValue) {
value = newValue;
dep.notify();
},
});
};
写一个渲染DOM的代码
let active;
//有点类似watchEffect
let onChange = function (fn) {
active = fn;
active(); //上来就要执行一次,渲染页面
active = null; //避免添加重复依赖,所以在get收集到依赖得时候,就要清空
};
onChange(() => {
let str = `hello ${x.value}`;
document.getElementById("app").innerText = str;
});
异步渲染DOM
上面的代码已经可以随着x.value的改变而响应式的更新页面了,但是会有一个问题
onChange(() => {
console.log("渲染");
let str = `hello ${x.value}`;
document.getElementById("app").innerText = str;
});
x.value = 2;
x.value = 3;
x.value = 4;
这个时候会发现,随着x的更新,DOM会渲染3次,这样对性能不太友好,所以vue中通过nextTick来异步渲染,等本轮数据全部更新完毕之后,才会渲染DOM
nextTick
let nextTick = function (cb) {
Promise.resolve().then(cb); //将渲染操作放在微任务中,异步渲染
};
调用渲染方法
let queue = []; //储存渲染DOM的操作
let queueJob = function (fn) {
if (!queue.includes(fn)) {
//因为更改的数据的依赖都是同一个,所以只会往queue中添加一个cb
//并且会在数据全部更新之后渲染DOM
queue.push(fn);
nextTick(flushJobs);
}
};
let flushJobs = function () {
while (queue.length > 0) {
let job = queue.shift(); //执行完之后删除,所以渲染时不会让之前渲染过的DOM再次渲染
job && job();
}
};
notify
此时调用notify的时候,将方法放入异步渲染队列中
notify() {
this.deps.forEach((dep) => queueJob(dep));
}
演示
可以发现,x.value
改变了三次,此时只会渲染一次了(第一个渲染是x.value=1
的时候调用的)
完整代码
let active;
let onChange = function (fn) {
active = fn;
active();
active = null;
};
let nextTick = function (cb) {
Promise.resolve().then(cb);
};
let queue = [];
let queueJob = function (fn) {
if (!queue.includes(fn)) {
queue.push(fn);
nextTick(flushJobs);
}
};
let flushJobs = function () {
while (queue.length > 0) {
let job = queue.shift();
job && job();
}
};
class Dep {
constructor() {
this.deps = new Set();
}
depend() {
if (active) {
this.deps.add(active);
}
}
notify() {
this.deps.forEach((dep) => queueJob(dep));
}
}
let ref = function (value) {
let dep = new Dep();
return Object.defineProperty({}, "value", {
get() {
dep.depend();
return value;
},
set(newValue) {
value = newValue;
dep.notify();
},
});
};
let x = ref(1);
onChange(() => {
console.log("渲染");
let str = `hello ${x.value}`;
document.getElementById("app").innerText = str;
});
x.value = 2;
x.value = 3;
x.value = 4;