四.渲染更新原理
我们已经实现了将数据渲染到页面上,但是当我们改变数据的时候,页面并不会自动更新,这里我们将实现页面自动更新。 -> 观察者模式:定义 Watcher 和 Dep 完成依赖收集和派发更新 从而实现渲染更新
观察者模式
我们可以给模板中的属性 增加一个收集器dep
页面渲染的时候,我们将渲染逻辑封装到watcher中 [渲染逻辑:vm._update(vm.render())],然后当dep发生变化时,就会通知watcher,然后就会调用watcher中已经封装的渲染逻辑。
让dep记住这个watcher即可,稍后属性变化了可以找到对应的dep中存放的watcher进行重新渲染
每个属性有一个dep(属性就是被观察者),watcher就是观察者(属性变化了会通知观察者来更新)
-
每一个组件都有一个watcher,watcher中放了一个组件中的很多属性dep,并且我们给每个组件赋一个id标识
-
每一个属性有一个dep属性,里面存放了这个属性的watcher,可能一个属性的dep中存放了很多个watcher,因为这个属性在很多地方都用到了
-
当这个组件的属性没有被渲染时,dep和watcher是不会被收集的
为什么要组件化?
复用,方便维护,局部更新
一.在渲染时:watcher和dep双向收集
首先,我们需要创建一个watcher的类,参数要传入vm,还有更新视图时的渲染逻辑(渲染逻辑是个函数)
还记得以前我们初次渲染视图时会调用:
//组件的挂载
//挂载可以执行render函数产生虚拟节点 虚拟DOM,根据虚拟DOM产生真实DOM,插入到el元素中
export function mountComponent(vm,el){
vm.$el = el;
// 1.调用render方法产生虚拟节点 虚拟DOM
// vm._render()
// 2.根据虚拟DOM产生真实DOM
// vm._update(vm._render())
//这里就是我们视图每次渲染更新的逻辑,我们将它封装在watcher中
const updatedComponent = () => {
vm._update(vm._render())
}
// !!! 渲染逻辑放在watcher中
new Watcher(vm, updatedComponent,true) //true用于表示是一个渲染watcher,因为计算属性也有watcher
}
那么我们就在每次创建视图时,都要创建一个watcher,并将渲染逻辑放在watcher中。
封装一个watcher类:
let id = 0; //我们给每一个watcher加上一个唯一的id
class Watcher{
constructor(vm,fn){
this.id = id++;
this.renderWatcher = options; //true表示是一个渲染watcher(因为计算属性也有watcher,那是一个计算watcher)
this.getter = fn; //getter意味着调用这个函数可以发生取值操作
this.get();
}
get(){
this.getter();
}
}
此时我们已经封装好了watcher类,现在需要封装一个dep类,每一个属性都有一个dep。
let id = 0;
class Dep{
constructor(){
this.id = id++;
}
}
export default Dep;
接下来我们需要将watcher和dep联系起来,使得在进行属性劫持的时候,watcher可以增加它的dep,dep也可以联系到它对应的watcher。
当我们挂载组件(即渲染视图)时,会创建watcher,那就意味着我们会执行watcher中的逻辑:get()函数,
那么我们给Dep增加一个静态属性,属性值为this,相当于我们现在已经记录了当前watcher
get(){
Dep.target = this; //绑定当前watcher
this.getter();
}
完整代码:
let id = 0; //我们给每一个watcher加上一个唯一的id
class Watcher{
constructor(vm,fn){
this.id = id++;
this.renderWatcher = options; //true表示是一个渲染watcher(因为计算属性也有watcher,那是一个计算watcher)
this.getter = fn; //getter意味着调用这个函数可以发生取值操作
this.get();
}
get(){
Dep.target = this;//!!! 绑定当前watcher
this.getter();
Dep.target = null; //当前watcher已经渲染完毕,那就不需要再绑定当前的watcher,而去绑定其他正在渲染的watcher
}
}
然后回到我们对属性劫持的代码:
let dep = new Dep();//每一个属性都增加一个dep
Object.defineProperty(data, key, {
get() {
if(Dep.target){
// 如果取值时有watcher
dep.depend(); //让dep 保存当前watcher
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新
}
});
那么我们就去实现Dep上的depend()方法:
给dep增加一个存放watcher的数组subs,并且编写depend()方法。
let id = 0;
class Dep{
constructor(){
this.id = id++;
this.subs = [];//存放当前dep的所有watcher
}
depend(){
this.subs.push(Dep.target);//将当前watcher加入数组
}
}
Dep.target = null;
export default Dep;
现在我们可以实现dep增加对应的watcher,但是我们还需要让watcher增加对应的dep.
那么我们先将dep的depend()方法进行更改:
class Dep{
constructor(){
this.id = id++;
this.subs = [];//存放当前dep的所有watcher
}
depend(){
Dep.target.addDep(this); //让watcher记住dep, Dep.target 就是当前watcher
}
}
目前watcher记住dep,
同时对应某个dep被调用多次,watcher只收集一次;
那么也给watcher增加一个存放dep的数组,也实现一个收集dep的方法addDep():
class Watcher{
constructor{
//...
this.deps = []; 存放该组件watcher的dep数组
this.depsId = new Set(); //对dep进行去重,只收集一次某个dep
}
addDep(dep){
//让当前watcher收集它的dep
let id = dep.id; //拿到该dep的id
if(!this.depsId.has(id)){
//如果一个属性在视图上进行多次渲染,但是只需要收集一次
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); //让dep记住watcher
}
}
}
上面的addDep方法中,我们实现了收集dep,并且在方法的最后一行dep.addSub(this);
,让dep记住watcher。
那我们回到Dep去实现addSub()方法:使得dep记住watcher
class Dep(){
//...
depend(){
//我们希望dep和watcher实现双向收集
Dep.target.addDep(this); //Dep.target指的是当前的watcher,然后调用watcher的addDep方法
}
addSub(watcher){
this.subs.push(watcher); //当前dep收集当前watcher
}
}
现在,我们在执行渲染的get的收集操作已经实现,当执行set时我们需要通知对应的dep更新dep.notify();
。
let dep = new Dep();//每一个属性都增加一个dep
Object.defineProperty(data, key, {
get() {
if(Dep.target){
// 如果取值时有watcher
dep.depend(); //让dep 保存当前watcher
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新
}
});
回到dep增加方法:
notify(){
//通知该属性的所有watcher都要进行更新
this.subs.forEach(watcher => watcher.update());
}
watcher:
class Watcher{
//...
update(){
this.get();
}
}
总结
到现在,我们已经实现了dep和watcher的收集,以及值改变时的更新操作。
梳理:
-
首先,我们会渲染当前组件,那么就会创建一个watcher,此时就会给Dep增加一个静态属性target,即
Dep.target
-
然后,会调用watcher中存放的渲染逻辑,然后就会进行数据劫持,此时会对属性进行遍历,使用Object.defineProperty设置每个属性的getter和setter。
-
在setter和getter之前,我们需要给每个属性创建一个Dep类的实例
-
在getter时,可以实现dep和watcher的双向收集
-
在setter时,会调用dep的notify方法,通知该dep的所有watcher进行更新
二.异步更新视图
上面我们实现了在数据发生变化时,执行setter(),调用dep的notify方法,通知该dep的所有watcher进行更新。
但是对于每一个变更值的操作都会进行一次更新,这样比较浪费性能。
我们需要做到,即使进行多次变更值的操作,我们只会进行一次视图的更新 -> 异步更新视图
- 我们将所有watcher先收集到队列中,并且进行去重
- 当所有同步代码执行完毕后,再执行watcher的更新
class Watcher{
//...
update(){
//我们需要实现异步更新:即多次修改值只会最终执行一次视图更新
//实现方案:使用异步更新 -> 事件环
// this.get(); //重新渲染
queueWatcher(this); //把当前的watcher暂存起来
}
run(){
// console.log('更新视图只执行一次')
this.get(); //重新渲染
}
}
let queue = [];
let has = {
}; //实现去重
let pending = false; //防抖:多次修改值,只进行一轮刷新
function flushSchedulerQueue(){
//执行所有要进行的更新视图操作
let flushQueue = queue.slice(0);
queue = [];
has = {
};
pending = false
flushQueue.forEach(q => q.run())
}
function queueWatcher(watcher){
const id = watcher.id;
if(!has[id]){
queue.push(watcher);
has[id] = true;
//不管我们的update执行多少次,但是最终只执行一轮刷新操作
if(!pending){
//setTimeout(flushSchedulerQueue,0);
//原来我们是使用setTimeout实现异步更新,但是其实底层我们可以也可以使用promise来实现异步更新
//vue为了统一,实现了自己的异步更新方法:nextTick
nextTick(flushSchedulerQueue);
pending = true;
}
}
}
let callbacks = [];
let waiting = false;
function flushCallbacks(){
waiting = false;
let cbs = callbacks.slice(0);
callbacks = []
cbs.forEach(cb => cb());
}
export function nextTick(cb){
//会先调用户写的nextTick还是内部的nextTick?? 不一定,谁放在前面就先执行谁
callbacks.push(cb);
if(!waiting){
// timerFunc();
Promise.resolve().then(flushCallbacks)
waiting = true;
}
}
三.数组更新原理
之前我们实现的都是对对象的监测和更新。
在实现对数组的更新之前,我们还需要对作为属性值时每个对象和数组进行劫持。
这样就可以实现,当某个属性的属性值是对象或数组时,我们修改对象/数组中的某一项的值,视图依然可以监测到。
因为Object.defineProperty只能监测到已存在的属性,所以在这里我们新增属性是无效的。
const vm = new Vue({
data: {
arr: [1,2,3,{
a:1}], //给数组本身增加dep(即你的属性值是数组),如果数组新增了某一项,我可以触发dep更新
a: {
a:1} //给对象也增加dep,如果后续用户增加了属性,我可以触发dep更新
}
})
那么回到数据劫持(Object.defineProperty)的函数中,我们给Observer实例本身增加dep属性:
class Observer{
constructor(data){
//给数组本身增加dep,如果数组新增了某一项,就要触发dep更新
//给对象增加dep,如果后序用户增加了属性,也要触发dep更新
//要给每个对象都增加收集功能
this.dep = new Dep()
}
}
并且我们进行数据劫持的时候,也需要对对象/数组本身实现依赖收集:
// 数据劫持
// 执行get时会进行dep和watcher的收集,在执行set时,让dep更新
export function defineReactive(target, key, value){
let childOb = observe(value); //递归进行:如果属性是对象那就继续进行代理
//childOb对象就是Observer实例,即我们的属性值(是个对象或数组)
//childOb中就有一个dep属性 用来收集依赖
let dep = new Dep(); //每一个属性都增加一个dep
Object.defineProperty(target, key, {
get(){
//取值的时候 执行get
if(Dep.target){
dep.depend(); //让这个属性的收集器记住当前的watcher
if(childOb){
//如果childOb存在,那就证明:当前遍历的是是Observer实例(即属性值:对象/数组)
childOb.dep.depend();//让数组和对象本身也实现依赖收集
}
}
return value
},
set(newValue){
//修改的时候,会执行set
//...
}
})
}
当我们进行属性值修改时,进行通知更新:
ob.dep.notify() //!!! 数组变化,通知对应的watcher进行更新
//ob就是我们的Observer实例,它身上就有dep属性,那么就通知更新
完整代码:
methods.forEach(method => {
//arr.push(1,2,3)
newArrayProto[method] = function(...args){
//这里重写了数组的方法
const result = oldArrayProto[method].call(this,...args) //内部调用原来的方法
//我们需要对新增的数据再次进行劫持
let inserted; //该变量存放新增的数据值
// 根据newArrayProto的被引用位置:我们可以得知,这里的this应该是 class Observe的实例对象
let ob = this.__ob__;//这里的ob就是Observe实例,并且它身上有对数组进行观测的observeArray方法
switch(method){
case 'push':
case 'unshift': //arr.unshift(1,2,3)
inserted = args;
break;
case 'splice': //arr.splice(0,1,{a:1})
inserted = args.slice(2);
default:
break;
}
// console.log('新增的数组的数据:',inserted);
if(inserted){
//对新增的内容再次进行观测
//既然要对新增的数据进行观测:那么还是要调用最初监测数组的方法来对新增数据进行观测
//那个新增的方法在Observe实例的observeArray函数,所以我们要拿到Observe实例
//怎么拿? 在Observe类中,我们已经绑定了this到__ob__属性上,并且在这里:根据这里被调用的位置可知,这里的this就是class Observe的实例对象
//所以,我们直接拿一个变量来承接Observe实例 -> let ob = this.__ob__;(在上面已经声明过了)
//下面就是对新增的数据再次进行观测
ob.observeArray(inserted);
}
ob.dep.notify() //!!! 数组变化,通知对应的watcher进行更新
//ob就是我们的Observer实例,它身上就有dep属性,那么就通知更新
return result
}
})
测试代码:
注意:目前我们实现了对已存在的属性值进行修改,视图会更新
const vm = new Vue({
data: {
//代理数据
firstname: '珠',
lastname: '峰',
arr: [1,2,3,{
a:1}],
a: {
a:1}
},
el: '#app', //将数据解析到el元素上
})
vm.a.a = 100
vm.arr.push(100,100,100)
现在我们实现对数组的劫持:
因为可能是数组里面套数组,那么就需要进行递归。
还是在数据劫持的函数中:
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)
}
}
}
// 数据劫持
// 执行get时会进行dep和watcher的收集,在执行set时,让dep更新
export function defineReactive(target, key, value){
let childOb = observe(value); //递归进行:如果属性是对象那就继续进行代理
//childOb中就有一个dep属性 用来收集依赖
let dep = new Dep(); //每一个属性都增加一个dep
Object.defineProperty(target, key, {
get(){
//取值的时候 执行get
if(Dep.target){
dep.depend(); //让这个属性的收集器记住当前的watcher
if(childOb){
childOb.dep.depend();//让数组和对象本身也实现依赖收集
if(Array.isArray(value)){
//对数组进行依赖收集
dependArray(value)
}
}
}
return value
},
set(newValue){
//修改的时候,会执行set
if(newValue){
if(newValue === value) return
observe(value); //递归进行:如果设置的值还是对象那就继续进行代理
value = newValue
dep.notify(); //更新dep,通知watcher进行更新
}
}
})
}
测试代码:
arr: [1,2,3,{a:1},['z','q']],
vm.arr[4].push('a')