【Vue源码初探】四.渲染更新原理

四.渲染更新原理


我们已经实现了将数据渲染到页面上,但是当我们改变数据的时候,页面并不会自动更新,这里我们将实现页面自动更新。 -> 观察者模式:定义 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是不会被收集的

为什么要组件化?

扫描二维码关注公众号,回复: 14535490 查看本文章

复用,方便维护,局部更新


一.在渲染时: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')

猜你喜欢

转载自blog.csdn.net/weixin_52834435/article/details/127261099