本篇文章借鉴自《深入浅出 vue.js》,根据该书中变化侦测的内容,对 vue 变化侦测的原理进行了文本梳理和理解

# 变化侦测

什么是变化侦测?

页面在渲染的过程中,状态发生不同的变化,变化侦测就是这样的一个存在,确定在渲染过程中状态发生了什么

对于 Angulars 和 React,他们变化侦测的方式更像是__“拉”__。

什么叫拉呢? 我觉得吧,就像是一棵苹果树,摘苹果的时候,你从树顶上去 " 拉 “他,从最上面开始摘,顺着树枝的节点,依次向下摘。

Angular 和 React 的状态发生变化的时候,他们都不知道是谁突然变化 ,没有办法只好 通过” 拉 “的方式,通过暴力比对的方式来查找。 在 Angular 里,他选择了脏检查的办法,而在 React 他选择虚拟 DOM 的方法,二者都是在 **“拉”** 的基础上进行。

但我今天看到书是《深入浅出 Vue.js》,说明 Vue 和以上二者都不太一样,他选择的是一种 **“推”** 的方法。

那么什么叫 “推”?同样和苹果数一样,“推” 就是把从下往上,一个苹果被摘了,我们就顺着这枝桠向上进行摘取,知道把这个枝桠的苹果摘完。

在 Vue 里,当状态发生改变的时候,vue 就马上知道了,并且在一定程度上,知道了,是哪些状态发生了变化。这样的状态变更信息他知道的更多之后,他就可以进行细粒度的更新了

__什么叫细粒度的更新?__就是:假如说有一个状态绑定了好多个依赖,每一个依赖表示一个具体的 DOM 节点,那么当这个状态发生改变的时候,对这个状态的所有依赖发出通知,让他们进行 DOM 的更新操作

# 如何追踪变化?

作者说,学过 js 的都知道有两种方法都知道,侦测变化的方法有两种:

  • Object.definProperty
  • ES6 的 Proxy

我好惭愧,我不知道。。。(i am so vegetable), 但还好,我会百度和 google。

废话不多说,直接讲原理:

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,// 可枚举嘛?
        configurable: true,// 可配置嘛?
        get: function () {
            return val;
        },
        set: function (newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
        }
    })
}

defineProperty(data,key,descriptor) 干什么的?侦测对象变化!(扣题欸!!)

defineReactive(data,key,val) 干嘛的?一看就是定义一个响应式数据嘛~

把函数的存取 val 的方式封装是为什么?我觉得吧,是因为,在你取一个值的时候,存一个值的时候会有耦合,通过上述代码,会更清晰明确一点(反正我是这样想的)。

# 如何收集依赖?

如果只是讲封装的 defineReactive(data,key,val) ,那明显不清楚我们侦测的原理是什么嘛,我们更在意的是 ------- 这么做到的?怎么收集到依赖的?

因为我们观察数据,目的是为了,当数据发生变化的时候,可以通知哪些用过他的地方,进行修改。

__所以收集依赖的核心是什么?__就是把使用过数据的地方都收集起来,他们怎么使用的?-- 通过 getter 对咩? 那就好说啦~。简单来说就是:通过 getter 收集依赖,然后再通过 setter 依次循环依赖,再触发一遍

# 依赖收集在哪?

首先我们想到,每个 key(属性)都有自己的值,这个值,会别很多人(依赖)取来用,那么这些个依赖的集合,是专属于当前 key(属性)的,那么我们可以大胆猜想,是不是存在一个数组 dep[] 专门来收集这个属性的依赖,且这些个依赖的集合 dep 被存在 window.target 上面呢?

function definedReactive(data, key, val) {
    let dep = [];//******** * 新增数组 dep 用来存储被收集的依赖 /
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.push(window.target);//******* */
            return val;
        },
        set: function (newVal) {
            if (val === newVal) {
                return;
            }
            for (let i = 0; i < dep.length; i++) {
                dep[i](newVal, val);
                //set 被触发的时,循环 dep,触发之前收集到的依赖,进行数据修改
            }//******* */
            val = newVal;
        }
    })
}

但这样写,还是耦合了,我们试试看,写一个专门的类,叫做 Dep , 专门为我们来管理依赖们

export default class Dep{
    constructor(){
        this.sub = [];
    }
    addSub(subb){
        this.sub.push(subb);
    }
    removeSub(subbb){
        remove(this.sub,subbb);
    }
    depend(){
        if(window.target){
            this.addSub(window.target);
        }
    }
    // 通知:大家伙们~,数据改啦!!
    notify(){
        const subs = this.sub.slice();
        for(let i = 0,l = subs.length;i<l;i++){
            subs[i].update();
        }
    }
}
function remove(arr,item){
    if(arr.length){
        const index = arr.indexOf(item);
        if(index =-1){
            return arr.splice(index,1);
        }
    }
}

于是之前的 definReactive 变成

function definedReactive(data, key, val) {
    let dep = Dep();///***** */
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val;
            dep.depend();//**** */
        },
        set: function (newVal) {
            if (val === newVal) {
                return;
            }
            dep.notify();//***** */
            val = newVal;
        }
    })
}

这样就回答了 :依赖在哪?------Dep 里(扣题~)

# 依赖是谁?

在上面的代码里,我们收集的依赖是 window.target ,那么他到底长什么样?我们到底要收集谁呢?

收集谁?归根结底,就是,你状态发生改变,你要通知谁?

通知的东西就很多啦啊?有可能是模板,或者说,用户自己写的一个 watch,这些个东西太多了,所以我们就想,是不是要写一个类来管理,这个类用来通知这些对象。这样的话,我们收集依赖的时候,只需要把这个类引进来,然后他来通知其他地方

我们姑且叫这个类为 Watcher

所以依赖是谁?----Watcher

# 什么是 Watcher?

watcher 是一个中介角色,数据发生变化的时候,通知他,然后他在通知其他地方。

// 关于 watcher 的例子
vm.$watch('a.b.c',function(newVal,oldVal){
    // 做点什么
})
// 这段代码表示当某个 data 的 'a.b.c' 属性发生改变的时候,触发第二个函数

我们思考一下是怎么实现这个功能的,可以猜测,是不是,把 watcher 的实例添加到 a.b.c 属性里,然后属性里值变化时通知 watcher , 然后他再通知给别人。

export default class Watcher{
    constructor (vm,expOrFn,cb){
        this.vm = vm ;
        // 执行 this.getter (), 就可以读取 “data.a.b.c” 里面的内容
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get();
    }
    get(){
        window.target = this;
        let value = this.getter.call(this.vm,this.vm);
        window.target = undefined;
        return value;
    }
    update(){
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm,this.value,oldValue)
    } 
}

这段代码可以把自己加进 data.a.b.c 的 Dep 中去

为什么?

因为,在 get 方法里,我把 window.target = this, ,也就是说:this 所指的当前 watcher 实例(也就是依赖)被我变成了 window.target

然后在读取 data.a.b.c 属性的值的时候,我们肯定会触发 getter(),触发 getter,说明触发了收集依赖的逻辑,关于收集依赖的逻辑,我们上述说过,会在 window.target 里面读取一个依赖并添加到 dep 里。

这就导致,只要 window.target = this 然后在读取一下值,就会主动把这个 this(watcher 实例 ===》依赖)添加到 dep 收集依赖的数组里。

依赖注入到 dep 里面后,每当 data.a.b.c 的值发生了变化时,就会让依赖列表的所有依赖循环触发,自己所含有的 update()方法。

# 递归所有的 key(属性)

上述讲的只是怎么实现一个属性的变化侦测。

但我们希望把数据里面的所有属性(包括子属性)都侦测到,所以需要封装一个 Observe 类,他的任务时,把一个数据里面的所有属性都转化成 getter/setter 的形式。

/**
 * Observer 会附加在每一个被侦测的 Object 对象上
 * 一旦被附加,Observe 会把 Object 的所有属性转换成 getter/setter
 * 来收集属性的依赖,并且当属性发生变化时,会通知这些依赖
 */
export class Observer {
    constructor(value) {
        this.value = value;
        if (!Array.isArray(value)) {
            this.walk(value);
        }
    }
    /** 
     * walk () 会将每一个属性转换成 setter/getter 的行式来侦测变化
     * 这个方法只有在数据类型为 Object 时调用
    */
    walk(obj) {
        const keys = Object.keys(obj);// 一个表示给定对象的所有可枚举属性的字符串数组。
        for (let i = 0; i < keys.length; i++) {
            definedReactive(obj, keys[i], obj[keys[i]]);
        }
    }
}
function definedReactive(data, key, val) {
    if (typeof val === 'object') {
        new Observer(val);
    }
    let dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend();
            return val;
        },
        set: function () {
            if (val === newVal) {
                return
            }
            val = newVal;
            dep.notify();
        }
    })
}

# 关于 Object 的问题

前面介绍了 Object 类型的变化侦测原理,了解了数据的变化是通过 getter/setter 来追踪的,

但也正是由于这种方式,有些语法里即使是数据发生了变化,vue 也追踪不到

var vm = new Vue({
    el:'#el',
    tempalte:'#demo-template',
    methods:{
        action:function(){
            this.obj.name = "awsl";
        }
    },
  data:{
      obj:{}
  }
})//vue 无法侦察

或者说

var vm = new Vue({
    el:'#el',
    tempalte:'#demo-template',
    methods:{
        action:function(){
            delete this.obj.name;
        }
    },
  data:{
      obj:{name:'awsl'}
  }
})//vue 无法侦察

vue 都无法察觉数据变化

# 总结

  • 变化侦测就是侦测数据的变化
  • Object 可以通过 Object.definProperty 将属性转换成 settet/getter 的行式,读取数据的时候触发 getter,存储数据 的时候触发 setter;
  • Dep 收集依赖,Watcher 就是依赖,数据变化,通知他。然后遍历 Dep 里的依赖,实现其他依赖的数据更新。
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

神烦大眼怪 微信支付

微信支付

神烦大眼怪 支付宝

支付宝

神烦大眼怪 贝宝

贝宝