相信看完这篇文章的你,跟我的想法是一样的。
当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
为了实现数据变化影响视图,vue采用了观察者模式,将数据和页面渲染关联起来。通过dep收集依赖,当数据变化时,通知对应watcher更新视图。
具体是怎么实现的呢?
1.创建渲染 watcher 并初始化
我们将更新视图的功能封装了一个watcher
export function lifecycleMixin() {
Vue.prototype._update = function (vnode) {}
}
export function mountComponent(vm, el) {
vm.$el = el;
let updateComponent = () => {
// 将虚拟节点 渲染到页面上
vm._update(vm._render());
}
new Watcher(vm, updateComponent, () => {}, true);
}
class Watcher {
// vm,updateComponent,()=>{ console.log('更新视图了')},true
constructor(vm,exprOrFn,cb,options){
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.id = id++;
// 默认应该让exprOrFn执行 exprOrFn 方法做了什么是? render (去vm上了取值)
this.getter = exprOrFn;
this.deps = [];
this.depsId = new Set();
this.get(); // 默认初始化 要取值
}
get(){ // 稍后用户更新 时 可以重新调用getter方法
// defineProperty.get, 每个属性都可以收集自己的watcher
// 我希望一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
pushTarget(this); // Dep.target = watcher
this.getter(); // render() 方法会去vm上取值 vm._update(vm._render)
popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了
}
update(){ // vue中的更新操作是异步的
// 每次更新时 this
queueWatcher(this); // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
}
run(){ // 后续要有其他功能
this.get();
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this)
}
}
}
2.在渲染时存储 watcher
调用 dep 的 pushTarget 方法存储 watcher
class Watcher{
// ...
get(){
pushTarget(this);//调用dep的pushTarget方法 Dep.target = watcher储存当前的watcher
this.getter();
popTarget();
}
}
let id = 0;
class Dep{
constructor(){
this.id = id++;
}
}
let stack = [];
export function pushTarget(watcher){
Dep.target = watcher;
stack.push(watcher);
}
export function popTarget(){
stack.pop();
Dep.target = stack[stack.length-1];
}
export default Dep;
3.对象的依赖收集
- 在vue中页面渲染时使用的属性,需要进行依赖收集 ,收集对象的渲染
watcher - 取值时,给每个属性都加了个
dep属性,用于存储这个渲染watcher(同一个watcher会对应多个dep)。watcher的get方法调用render,render方法去vm上取值,取值就调用了defineProperty的get方法。
dep.depend()=> 通知dep存放watcher=>Dep.target.addDep()=> 通知watcher存放dep实现双向存储。
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if(Dep.target){ // 如果取值时有watcher
dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新
}
});
Dep 实现
每个 dep 有一个 id 唯一标示,向watcher中添加dep可以通过id去重。
用 dep 做关系的收集,每个属性都分配一个 dep,dep 可以来存放 watcher。
let id = 0;
class Dep{ // 每个属性我都给他分配一个dep,dep可以来存放watcher, watcher中还要存放这个dep
constructor(){
this.id = id++;
this.subs = []; // 用来存放watcher的
}
depend(){
// Dep.target dep里要存放这个watcher,watcher要存放dep 多对多的关系
if(Dep.target){
Dep.target.addDep(this);
}
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
Dep.target = null; // 一份
export function pushTarget(watcher) {
Dep.target = watcher;
}
export function popTarget(){
Dep.target = null
}
export default Dep
watcher实现
watcher 中存放 dep(一个属性一个 dep,多个属性多个 dep)
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
let id = 0;
class Watcher {
// vm,updateComponent,()=>{ console.log('更新视图了')},true
constructor(vm,exprOrFn,cb,options){
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.id = id++;
// 默认应该让exprOrFn执行 exprOrFn 方法做了什么是? render (去vm上了取值)
this.getter = exprOrFn;
this.deps = [];
this.depsId = new Set();
this.get(); // 默认初始化 要取值
}
get(){ // 稍后用户更新 时 可以重新调用getter方法
// defineProperty.get, 每个属性都可以收集自己的watcher
// 我希望一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
pushTarget(this); // Dep.target = watcher
this.getter(); // render() 方法会去vm上取值 vm._update(vm._render)
popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了 比如说 我在 vue 的外面可以, vm.xxx 这个值 不需要收集 watcher
}
update(){ // vue中的更新操作是异步的
// 每次更新时 this
queueWatcher(this); // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
}
run(){ // 后续要有其他功能
this.get();
}
addDep(dep){
let id = dep.id;
//watcher在添加dep的时候做了去重
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this)
}
}
}
4.数组的依赖收集
- 给数组的
__ob__添加一个dep,用来存放watcher - 当调用数组变异方法时,调用
dep.notify()来通知watcher更新 - 如果是数组嵌套数组,通过
dependArray()递归收集watcher
this.dep = new Dep(); // 专门为数组设计的
if (Array.isArray(value)) {
value.__proto__ = arrayMethods;
this.observeArray(value);
} else {
this.walk(value);
}
function defineReactive(data, key, value) {
let childOb = observe(value);
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend(); // 收集数组依赖
}
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify();
}
})
}
arrayMethods[method] = function (...args) {
// ...
ob.dep.notify()
return result;
}
递归收集数组依赖
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend(); // 收集数组依赖
if(Array.isArray(value)){ // 如果内部还是数组
dependArray(value);// 不停的进行依赖收集
}
}
}
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let current = value[i];
current.__ob__ && current.__ob__.dep.depend();
if (Array.isArray(current)) {
dependArray(current)
}
}
}
Vue异步更新之nextTick
每改一次数据就更新一下视图,这样操作显然是不合理的。所以vue的视图更新是异步的,多次调用update,先将watcher缓存在queue中,根据id去重,再一起更新。
1.实现队列机制
update(){
queueWatcher(this);
}
import { nextTick } from "../utils";
let queue = [];
let has = {}; // 做列表的 列表维护存放了哪些watcher
function flushSchedulerQueue(){
for(let i =0 ; i < queue.length; i++){
queue[i].run(); // vm.name = 123?
}
queue = [];
has = {};
pending = false;
}
let pending = false;
// 要等待同步代码执行完毕后 才执行异步逻辑
export function queueWatcher(watcher) {
// 当前执行栈中代码执行完毕后,会先清空微任务,在清空宏任务, 我希望尽早更新页面
const id = watcher.id;
if (has[id] == null) {
queue.push(watcher);
has[id] = true;
// 开启一次更新操作 批处理 (防抖)
if(!pending){
nextTick(flushSchedulerQueue, 0);
pending = true;
}
}
}
2.nextTick实现原理
const callbacks = [];
function flushCallbacks() {
callbacks.forEach((cb) => cb());
waiting = false;
}
//nextTick防抖 异步
//vue2考虑了兼容性问题
//1.支持promise,promise.then
//2.mutationObserver 监控文本的变化,文本内容变化执行flushCallbacks
//3.setImmediate
//4.setTimeout
//vue3不再考虑兼容性问题
let timerFn = () => {};
if (Promise) {
timerFn = () => {
Promise.resolve().then(flushCallbacks);
};
} else if (MutationObserver) {
let textNode = document.createTextNode(1);
let observe = new MutationObserver(flushCallbacks);
observe.observe(textNode, {
characterData: true,
});
timerFn = () => {
textNode.textContent = 3;
};
} else if (setImmediate) {
timerFn = () => {
setImmediate(flushCallbacks);
};
} else {
timerFn = () => {
setTimeout(flushCallbacks);
};
}
let waiting = false;
export function nextTick(cb) {
callbacks.push(cb);
if (!waiting) {
timerFn(flushCallbacks, 0);
waiting = false;
}
}
小结
到这里,我们就实现了依赖收集的原理。
响应式数据时,对于数组和对象的处理分开两种不同的方式。 现在依赖的收集也是。
你觉得绕吗吗吗?