前言
通过手写Vue2源码,更深入了解Vue; 在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅; 另外我会编写一些开发文档,阐述编码细节及实现思路; 源码地址:手写Vue2源码
问题分析
在上一节,页面初次渲染,我们调用了vm._update(vm._render())
将VNode渲染到页面上;但是有一个问题:如果有一个异步方法修改了data,页面是不会响应式更新的,与数据驱动视图思想不符。本文将采用观察者模式,定义Watcher和Dep,完成依赖收集和派发更新,从而实现渲染更新。
思考一下怎么实现页面响应式更新?
- 在修改数据时,我们需要知道哪些组件使用到该数据,并触发所有相关组件的视图更新;
- 在执行
vm._render()
,获取数据时,我们需要给数据收集所有使用到该数据的组件;
流程分析
大概实现流程如下:
- 在渲染组件时,实例化一个渲染watcher(后续利用watcher触发视图更新,以及绑定watcher与数据的关系)
- 在触发数据的getter时,收集依赖(一个数据可能对应多个watcher,同时一个watcher可能对应多个数据)
- 在触发数据的setter时,通知watcher更新视图(需要进行异步渲染优化)
在收集依赖时我们要做两件事:
- 给数据收集相关watcher,目的是当数据变化时,通知watcher更新视图
- 给watcher收集相关数据,目的是为了在组件更新的时候进行【依赖清除】,对没有用到的deps进行清除,避免该dep修改时进行无意义的重新渲染
为了实现这个功能,我们增加了一个中间者——dep:
- dep和数据一一对应
- dep必须唯一(一个watcher中可存放的多个dep,但dep必须唯一)
- dep作为一个中间者,具体实现流程为:做数据劫持时实例化一个dep ——> 触发getter时通知dep收集watcher(
dep.depend(data)
)——> depend中通知watcher收集dep(Dep.target.addDep(this)
,Dep.target
在创建watcher时指向当前watcher)——> 在watcher中收集dep,并通知dep收集watcher(dep.addSub(this)
)
Vue观察者模式:
- watcher就是观察者,它需要订阅数据的变化,当数据变化时,通知它去更新视图
- dep就是被观察者,dep作为一个中间者的身份,实现了:在数据getter中进行依赖收集,在Dep中通知watcher收集相关dep,在Watcher中通知Dep收集相关watcher
- 当数据变化时,dep相关watcher【观察者】自动更新视图
创建渲染watcher
暂时只分析渲染watcher
(computed watcher
和user watcher
后续章节再分析)。
组件渲染时会执行mountComponent()
,在该方法中实例化一个渲染watcher。
// src/lifecycle.js
export function mountComponent(vm, el) {
let updateComponent = () => {
vm._update(vm._render());
};
// 每个组件渲染的时候,都会创建一个watcher,并执行updateComponent;true表示是渲染Watcher
new Watcher(
vm,
updateComponent,
() => {
console.log('视图更新了')
callHook(vm, "beforeUpdate");
},
true
);
}
实例化dep、收集依赖、通知更新
在数据劫持时,实例化一个独一无二的dep,在setter中收集依赖,在setter中派发更新。 直接看代码:
// src/observer/index.js
function defineReactive(data, key, value) {
observe(value);
let dep = new Dep() // 为每个属性创建一个独一无二的dep
Object.defineProperty(data, key, {
get() {
// Dep.target指向当前渲染watcher,只在渲染时存在,渲染完删除
// 这里的判断是为了保证只在渲染时才进行依赖收集
if(Dep.target) {
dep.depend() // 收集依赖
}
return value;
},
set(newVal) {
if (newVal === value) return;
observe(newVal);
value = newVal;
dep.notify(); // 通知dep存放的watchers去更新--派发更新
},
});
}
实现Dep
Dep实现的功能:
- 唯一性
- subs数组存放watchers
- 通知watchers更新 —— notify()
- 通知watcher收集dep —— depend()
- 在dep中收集watcher —— addSub(watcher)
- 创建自身的target属性,用来保存当前watcher
具体实现:
// src/observer/dep.js
/**
* 1. 每个属性我都给他分配一个dep(一对一的关系),一个dep可以存放多个watcher(一个属性可能对应多个watcher)
* 2. 一个watcher中还可以存放多个dep(一个watcher可能对应多个属性,而dep与属性一一对应)
* 3. dep具有唯一性
*/
let id = 0; // 给dep添加一个标识,保证它的唯一性
export default class Dep {
constructor() {
this.id = id++;
this.subs = []; // 用来存放watcher
}
// 将dep实例放到watcher中
depend() {
// 如果当前存在watcher
if (Dep.target) {
// Dep.target即当前watcher,是在new Watcher时设置的
Dep.target.addDep(this); // this为dep实例(与属性一一对应),即把自身dep实例存放在watcher里面
}
}
// 依次执行subs里面的watcher更新方法
notify() {
this.subs.forEach((watcher) => watcher.update());
}
// 把watcher加入到dep实例的subs容器(因为一个dep可能对应多个watcher)
addSub(watcher) {
this.subs.push(watcher);
}
}
/**
* targetStack定义在全局,为整个项目所有watcher
* Dep.target定义在Dep自身而非prototype上,无法被实例继承,标识当前的watcher,具有唯一性
*/
// 栈结构用来存众多watcher
const targetStack = [];
// Dep.target 为 dep 当前所对应的watcher(即栈顶的watcher),默认为null
Dep.target = null;
export function pushTarget(watcher) {
targetStack.push(watcher);
Dep.target = watcher; // Dep.target指向当前watcher
}
export function popTarget() {
targetStack.pop(); // 当前watcher出栈 拿到上一个watcher
Dep.target = targetStack[targetStack.length - 1];
}
实现watcher
Dep实现的功能:
- 实现页面渲染 ——
get()
,在get()
调用this.getter()
,即调用第二个形参(即vm._update(vm._render())
);在get()
中配置Dep.target
,进行入栈和出栈操作,保证渲染时的Dep.target
指向当前watcher; - 页面更新 —— update(),需要进行异步渲染优化,后续再完善,暂时直接调用
this.get()
- 收集dep ——
addDep(dep)
,要确保dep的唯一性,另外在该方法中还要通知dep收集watcher
具体实现:
// src/observer/watcher
export default class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.deps = []; //存放dep的容器
this.depsId = new Set(); //用来去重dep
this.getter = exprOrFn;
this.get();
}
// new Watcher时会执行get方法;之后数据更新时,直接手动调用get方法即可
get() {
// 把当前watcher放到全局栈,并设置Dep.target(无法继承,具唯一性)为当前watcher
pushTarget(this);
/**
* 执行exprOrFn,如果watcher是渲染watcher,则exprOrFn为vm._update(vm._render())
* 在执行render函数的时候,获取变量会触发属性的getter(定义在对象数据劫持中),在getter中进行依赖收集
*/
const res = this.getter.call(this.vm);
// 执行完getter就把当前watcher删掉,以防止用户在methods/生命周期中访问data属性时进行依赖收集(数据劫持时会判断Dep.target是否存在)
popTarget(); // 在调用方法之后把当前watcher实例从全局watcher栈中移除,设置Dep.target为新的栈顶watcher
return res;
}
/**
* 1. 保证dep唯一,因为在render过程中,同一属性可能被多次调用,只需收集一次依赖即可;另外初始渲染收集过的dep,在更新时也不用再次收集(通过dep的id来判断)
* 2. 将dep放到watcher中的deps数组中
* 3. 在dep实例中添加watcher
*/
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
// 将dep放到watcher中的deps数组中
this.deps.push(dep);
console.log('watcher.deps------------', this.deps)
// 直接调用dep的addSub方法 把自己watcher实例添加到dep的subs容器里面
dep.addSub(this);
}
}
// 更新当前watcher相关的视图
update() {
console.log('watcher.update:更新视图')
this.get()
// toDO... 如果短时间内同一watcher执行了多次update,我们希望先将watcher缓存下来,等一会儿一起更新
}
}
数组的依赖收集
// src/observer/index.js
import { arrayMethods } from "./array";
import Dep from "./dep";
class Observer {
// 通过new命令生成class实例时,会自动调用constructor(),即会执行this.walk(data)方法
constructor(data) {
this.value = data
this.dep = new Dep(); // 给data添加一个dep,收集data整体的一个dep(主要用于数组的依赖收集)
if (Array.isArray(data)) {
// 数组响应式处理
// 重写数组的原型方法,将data原型指向重写后的对象
data.__proto__ = arrayMethods;
// 如果数组中的数据是对象,需要监控对象的变化
this.observeArray(data);
} else {
// 对象响应式处理
this.walk(data);
}
}
observeArray(data) {
data.forEach((item) => {
observe(item);
});
}
}
function defineReactive(data, key, value) {
let childOb = observe(value); // 【关键】递归,劫持对象中所有层级的所有属性
let dep = new Dep() // 为每个属性创建一个独一无二的dep
Object.defineProperty(data, key, {
get() {
if(Dep.target) {
dep.depend()
// 如果属性的值依然是一个数组/对象,则对该 数组/对象 整体进行依赖收集
if(childOb) {
childOb.dep.depend(); // 让对象和数组也记录watcher
// 如果数据结构类似 {a:[1,2,[3,4,[5,6]]]} 这种数组多层嵌套,数组包含数组的情况,那么我们访问a的时候,只是对第一层的数组进行了依赖收集
// 里面的数组因为没访问到,所以无法收集依赖,但是如果我们改变了a里面的第二层数组的值,是需要更新页面的,所以需要对数组递归进行依赖收集
if (Array.isArray(value)) {
// 如果内部还是数组
dependArray(value); // 遍历 + 递归数组,对数组不同层级的所有数组元素进行依赖收集
}
}
}
return value;
},
});
}
// 遍历递归收集数组依赖
function dependArray(value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i];
// 对每一项进行依赖收集
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
// 【递归】如果数组里面还有数组,就递归去收集依赖
dependArray(e);
}
}
}
总结:
- 如果一个对象的属性值为对象/数组,通过
childOb.dep.depend()
对该 对象/数组 整体进行收集依赖(childOb.dep是在new Observer(value)
时添加的) - 如果数组的子元素依然有数组,即多层数组嵌套的情况,则采用遍历加递归的方式,对所有数组及子元素进行依赖收集(递归observe数组元素时,无法对深层数组进行依赖收集,因为深层数组无法走到defineReactive这一步)
- 执行
initData()
——>return Observer(data)
,只是进行数据劫持,依赖收集是在触发getter时进行的
数组派发更新
数组的响应式主要是通过重写原型方法,所以数组的派发更新也在原型中进行:
// src/observer/array.js
methods.forEach(method => {
arrayMethods[method] = function(...args) {
const result = oldArrayPrototype[method].call(this,...args) // this指向调用该方法的data(即经过响应式处理的数组)
// 对于数组中新增的元素,也需要进行监控
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case "splice":
inserted = args.slice(2);
default:
break;
}
// inserted是个数组,需要调用observeArray来监测
if (inserted) ob.observeArray(inserted);
// 数组派发更新;dep是在new Observer(data)时添加的
ob.dep.notify()
return result
}
})
核心思想:在new Observer(data)
时,往data添加 dep
、__ob__
属性以及收集依赖,在原型上通过 this.__ob__.dep.notify()
派发更新。
系列文章
- 手写Vue2源码(一)—— 环境搭建
- 手写Vue2源码(二)—— 数据劫持
- 手写Vue2源码(三)—— 模板编译
- 手写Vue2源码(四)—— 初次渲染
- 手写Vue2源码(五)—— 观察者模式
- 手写Vue2源码(六)—— 异步更新及nextTick
- 手写Vue2源码(七)—— 侦听属性
- 手写Vue2源码(八)—— 计算属性
- 手写Vue2源码(九)—— 混入原理与生命周期
- 手写Vue2源码(十)—— 组件原理
- 手写Vue2源码(十一)—— diff算法
- 手写Vue2源码(十二)—— keep-alive
- 手写Vue2源码(十三)—— 全局API
- vue-router原理解析
- vuex原理解析
- vue3原理解析