Vue源码学习(二):Object的变化侦测

11 篇文章 0 订阅

Vue源码系列


前言

Object 和Array的变化侦测采用不同的处理方式(后面会解释原因),这一篇将介绍Object的变化侦测。

一、什么是变化侦测?

  Vue.js会自动通过状态生成DOM,并将其输出到页面显示出来,这个过程叫渲染。
  通常,在运行时应用内部的状态会不断发生变化,此时需要不停地重新渲染。那如何知道状态发生变化了呢,变化侦测就是用来追踪状态变化的,一旦变化,就重新更新视图。
  变化侦测有两种类型,一种是“推”,一种是“拉”。Angular和React中是“拉”,即不知道哪个状态变了,只知道状态可能变了,然后发信号给框架,框架内部进行暴力比对找到节点进行渲染。Vue则是“推”,状态变了,立刻能侦测到并向改状态对应的依赖发送通知,让它们更新。
  Vue2粒度更新是组件级别的,而不再是具体的节点。(因为粒度越细,绑定的依赖就越多,依赖追踪在内存中的开销就越大)

二、如何追踪变化

如何侦测一个对象的变化?
easy,用Object.defineProperty和ES6的Proxy。Vue2用的Object.defineProperty,Vue3用Proxy重写了这部分。
那还是从Vue2学起吧…
写Vue2时,会new一个Vue,然后进行初始化,调用init方法,这个init方法绑定到了Vue构造函数的原型上:
在这里插入图片描述
在这里插入图片描述

进入init方法,数据的变化侦测就看initState方法就可以:
在这里插入图片描述

进入initState方法,发现里面是初始化的逻辑:(源码被简化过):
在这里插入图片描述
  我们看initData初始化数据的方法,通过observe方法对数据劫持,observe方法会去实例化一个Observe类,目的是为了循环对象,给每一个属性配成可侦测的。通过Object.defineproperty()就可以追踪变化
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
  通过以上调试,我们知道,Object.defineProperty可以侦测到对象的变化,函数defineReactive用来对Object.defineProperty进行封装。defineReactive函数作用是定义一个响应式数据。每当data的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。
  defineReactive函数会对value进行递归侦测,因为value有可能也是对象。

三、如何收集依赖

  我们已经知道如何侦测变化了,接下来要收集依赖…
  侦测数据的目的是,属性发生变化时需要通知用到数据的地方。Vue2中会发送到组件,然后组件内部通过虚拟DOM重新渲染。

<template>
	<h1>{{name}}</h1>
</template>
<!--该模板使用了数据那么,所以当它发生变化时,要向使用了它的地方发送通知-->

  使用这个name数据的地方可能很多,既有可能是模板,也可能是用户写的一个watch。这时需要抽象出一个能集中处理这些情况的类,这个类就是Watcher(Watcher的实现在下文)。
  依赖是Watcher,我们要收集的是Watcher。
  在依赖收集阶段只收集这个封装好的类的实例,通知也只通知它一个。

  如何收集依赖呢?把用到数据name的地方收集起来,然后等属性发生变化时,把之前收集的依赖循环触发一遍就好。即在getter中收集依赖,在setter中触发依赖

四、依赖收集在哪里?

  通过第三点,知道在getter中收集依赖,那把依赖放哪里呢?
  思考一下,首先想到的是每个key都有一个数组,用来存储当前key的依赖。假设依赖是一个函数,保存在window.target上,那defineReactive函数可以写成这样:

export function defineReactive(target, key ,value) { 
    let dep = []; // 每一个属性都有一个dep
    Object.defineProperty(target, key, {
        get() { // 取值调用
            dep.push(window.target); // 收集依赖
            return value
        },
        set(newValue) { // 修改的时候,会执行set
            if(newValue === value) return
			for(let i = 0; i < dep.length;i++) {
				dep[i](newVal,val); // 依次调用依赖方法,更新界面
			}
			val = newVal
        }
    })
}

  数组dep,用来存储被收集的依赖;set被触发时,循环dep以触发收集到的依赖。
  由于这样写有点耦合,所以封装成了Dep类,专门管理依赖,使用这个类,可以收集依赖、删除依赖或向依赖发送通知,代码如下:

let id = 0;
class Dep {
    constructor() {
        this.id = id++; // 属性dep手机watcher
        this.sub = []; // 存放当前属性对应的watcher有哪些
    }
    depend() {
        if(window.target){
        	this.addSub(window.target)
        }
    }
    addSub(watcher) {
        this.sub.push(watcher)
    }
    notify() {
        this.sub.forEach(watcher => {
            watcher.update(); // 告诉watcher更新
        });
    }
    // 还有其他方法略
}
Dep.target = null;
export default Dep;

因此,要封装一个管理依赖的类Dep,defineReactive函数可改造成:

export function defineReactive(target, key ,value) { 
    let dep = new Dep(); // 修改
    Object.defineProperty(target, key, {
        get() { 
            dep.depend(); // 修改
            return value
        },
        set(newValue) {
            if(newValue === value) return
			dep.notify() // 修改
			val = newVal
        }
    })
}

所以,依赖收集到了Dep类中。

五、Watcher是什么?

  通过上文,数据侦测时,getter把依赖收集到了Dep类中,setter触发时会通知依赖更新dom,而这个依赖就是Watcher。那究竟什么是Watcher呢?

  初始化之后,会调用Vue原型上的mount方法vm.$mount('#app'),mount方法里面有一个mountComponent方法,里面会创建一个渲染Watcher,数据变化时通知它,它再通知update更新方法重新渲染:
在这里插入图片描述
Watcher类实现代码:

export default class Watcher {
  constructor (vm,expOrFn,cb) { 
    this.vm = vm; // 实例
    this.cb = cb; // 用户定义的watch会调用这个cb,变化侦测可以不看这个
    this.getter = exprOrFn // 将实例化渲染watcher时传入的update更新方法放到getter中
    this.value = this.get() // 执行get方法
  }
  get () {
    window.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm) // 执行update方法,这里会去读取数据,会触发属性的getter方法,从而主动收集依赖
    window.target = undefined;
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

  Watcher可以自己主动添加到对应变化属性的Dep中,是不是很神奇?
  因为在get方法中先把window.target设置成了this,也就是当前的watcher实例,然后再读属性的值,就肯定会触发属性的getter。触发了getter,就会触发收集依赖的逻辑。
  依赖注入Dep后,属性变化时,就会让依赖列表中所有的依赖循环触发update方法,从而进行更新。
  所以,读取属性时,将Watcher放在Dep类的sub数组中;修改属性时,遍历sub数组,依次通知所有的Watcher实例去更新视图。
一个属性对应一个Dep,一个Dep对应多个Watcher(渲染Watcher、用户定义的Watcher等),一个Watcher也可以对应多个Dep。

六、不足之处

const vm = new Vue({
    data: {
        obj:{}
    },
    methods: {
        action() {
        	this.obj.name='lisa'
        }    
    },
    el:'#app', // 将数据解析到el元素上
})

在obj中增加了name属性,但是Vue.js无法侦测到这个变化,删除也不会被侦测到。这是因为Object.defineProperty的getter/setter只能追踪一个数据是否被修改,无法最终新增属性和删除属性。

总结

  侦测就是侦测数据的变化。当数据发生变化时,要能侦测到并发出通知。

  object可以通过object.defineProperty将属性转换成getter/setter的形式来追踪变化。

  我们需要在getter中收集有哪些依赖使用了数据。setter触发时,通知getter中收集的依赖数据发生了变化。

  收集依赖,我们创建了Dep类,管理收集、删除依赖等操作。

  依赖即Watcher类。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

  Observer类,作用是把Vue data中的object数据,通过object.defineProperty都转化为响应式的(包括子数据)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值