Vue响应式系统原理分析与简单实现(上)

对于VUE,最显著的特点之一就是其数据双向绑定而带来的奇妙开发体验。经由vue源码中的某些操作,使得工程师在项目开发过程中,无需操作Dom,逻辑层对数据的改变便会自动反馈在视图层;反过来,v-model的使用也会使得用户在视图层上的修改映射到真实数据上。

vue官方文档中有一目---”深入响应式原理“,专门阐述了这一特性的实现机制,然而篇幅有限,有些具体点的阐述对初学者来讲还是不是很友好。前一段时间自己专门去找了一些源码相关的内容去学习,详细了解了一下这一过程。此篇文章将会梳理总结一下自己的学习成果。完整代码见:https://github.com/cyanl77/mvvm

下面进入正题。(持续更新)

1 概述

1.1 数据变化监听

”深入响应式原理“第一小节叫做”如何追踪变化“,它想要探讨的问题和此部分一致,即javascript本身是如何监听到一个数据的变化的,了解这一点是理解”响应式“机制的第一步。

实现这一功能的是Object.defineProperty。该方法本身的目的在于定义或修改一个对象的现有属性,该方法第三个参数属性描述符可通过一对函数getter和setter来定义一个属性的存取特性,它们分别在该属性被读取或重新赋值的时候被调用。现在可明确,js即是通过定义待观测属性的getter和setter来达到监测其变化,进而响应变化的目的。

到此,可以写出如下实现响应式系统的雏形,假设我们要监测一个对象中属性,当其发生改变时,自动在控制台输出:

 Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get () {
            //do something
            return value;
        },
        set (newValue) {
            if(value !== newValue) {
                value = newValue;
                //do something
            }
        }
    })

1.2 vue中的数据依赖收集

vue构建的视图中,可能有多处依赖于data中的同一属性,当逻辑层的值发生变化,理应对视图层的每一个值进行相应的改变。上一小节的响应式雏形代码中只对在setter中承担了相应过程中的部分功能。而getter则完成了另一部分的功能,即收集视图层的数据依赖。这里的依赖准确来说应该叫做一个观察者,它负责监测一对相互关联的数据和引用该数据的视图,并维护着新数据渲染的方法。

vue响应式系统实现原理到此已经大致清晰:为data中所有属性绑定其存取属性getter和setter,其中,getter用来收集,setter用来更新。每当视图层对数据进行读取,则调用getter,将对应依赖收集起来;每当逻辑层改变该数据,则调用setter函数,依次更新收集到的所有依赖。

下面再重新审视官网文档上的这个原理图就要清楚的多,其中“touch”的过程就是渲染视图时读数据触发getter的过程,而“wathcer”就是上文说的观察者,它具体是怎样的实现将在之后的小节中进行具体说明。


Vue响应式系统原理分析与简单实现(上)_第1张图片
image

2 关键数据结构

2.1 订阅者Dep

订阅者Dep本质是一个类,其功能简单说就是一个收集管理处。我们知道,对于vue组件实例data中的某一数据,可能被视图层多处依赖,每一处依赖,就有一个对应的观察者watcher来负责执行视图的变化更新。所以为了在数据变化时更新到所有的视图层数据,对于每一个数据,我们都需要维护这样一个数据结构Dep来收集所有引用该数据的watcher,以使得数据变化时,它能一一通知收集到的watcher去执行对应的更新函数。dep与watcher的关系如下图所示:


Vue响应式系统原理分析与简单实现(上)_第2张图片
image.png

具体来说,订阅者对象实例承担了以下工作:

  • 收集watcher。
  • 存储watcher。
  • 数据更新时,循环通知所有watcher更新对应视图。

这里值得提及一下Dep实例收集观察者的过程,源码中采取了巧妙的方式使得一个watcher一旦被实例化,便自己将自己加入对应的dep中。其具体过程如下:
1). Dep类自身定义了静态变量target,指向新new出的watcher。
2).watcher在构造函数中会为了保存当前值(以便待观察数据被赋予新值时进行比较)而读取数据。
3).触发该数据的getter,而每个数据的getter中会调用对应dep的收集函数将target所指向的watcher实例存储起来。
4).解除target指向直到有新的watcher被实例化出来。

基于以上所述,可封装如下订阅者对象:

let depId = 0;
class Dep {
    constructor() {
        this.id = depId++;

        //存储watcher
        this.subs = [];
    }

    //添加watcher
    addSub(watcher) {
        this.subs.push(watcher);
    }

    depend(){
        Dep.target.addDep(this);
    }

    //数据变化,通知所有观察者更新对应视图
    notify() {
        this.subs.forEach(watcher =>{
            //依赖更新视图
            watcher.update();
        })
    }
}
Dep.target = null;

数据绑定存取属性的过程也进一步封装为一个函数,并补充完整其getter的内容,这里每个带观测数据和每个dep实例是一一对应的关系:

function defineReactive (obj,key,value) {
    const dep = new Dep();
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get () {
            if(Dep.target){
                dep.depend();
            }
            return value;
        },
        set (newValue) {
            if(value !== newValue) {
                value = newValue;
                dep.notify();
            }
        }
    })
}

function observer(obj) {
    Object.keys(obj).forEach((key) => {
        defineReactive(obj,key,obj[key]);
        if(typeof obj[key] === 'object') {
            observer(obj[key]);
        }
    })
}

2.2 观察者watcher

观察者watcher,其本质为一个对象,我们在组件实例中定义的watch的成员就是在为数据绑定一个个的watcher,视图部分有可能是dom中一个元素属性或文本节点,不同形式的视图层其更新方式有所不同。在响应式的环节中,每个观察者存有对应视图的更新方法。

由上节我们知道,当一个数据在逻辑层发生改变,会首先通知给watcher的收集管理处Dep,在由Dep一一传达收集的watcher,此时每个watcher调用对应的更新方法去更新视图。具体来说,watcher在这一过程中做了以下工作:

  • 首次实例化,将自己注册在订阅者Dep中。
  • 解析待观察的表达式,在data中获取对应的新值,存储旧值。
  • 比较新旧值,当新旧值不同,调用更新方法更新视图。

基于以上所述, watcher类定义的框架大致如下:

class Watcher {
    constructor (vm,expOrFunc,cb) {
        //vm vue实例
        this.vm = vm;

        // 被观察的属性变量名称
        this.exp = expOrFunc;
        this.getter = function(vm, exp){
            return vm.$data[exp];
        };
        this.id = watcherId++;

        //属性赋新值后调用回调
        this.cb = cb;
        this.deps = [];
        this.value = this.get(); //获取老值
    }

    get(){
        Dep.target = this;
        let value = this.getter(this.vm,this.exp);
        
        //配合getter中Dep.target非空判断防止相同watcher二次加入,读后需解绑
        Dep.target = null;
        return value;
    }

    //注册
    addDep(dep){
        if(this.deps.indexOf(dep.id) === -1){
            this.deps.push(dep.id);
            dep.addSub(this);
        }
    }

    //对外暴露的方法
    update(){
        let value = this.get(); //新值
        if(this.value !== value) {
            const oldValue = this.value;
            this.value = value;
            this.cb.call(this.vm,value,oldValue);
        }
    }
}

2.3 执行

不考虑各种各样的边界情况,到这里我们关键数据结构已经构建完全,可以进行实例化并简单的模拟响应式数据。由于代码中未加入html模板编译的过程,这里仅用js定义watch的形式来产生一个watcher观察数据, 回调函数在控制台打印更新后的数据。具体代码如下:

class Vue {
    constructor(options){
        this._data = options.data;
        observer(this._data);
        if(options.watch) {
            Object.keys(this._data).forEach((key)=>{
                const watcher = new Watcher(this,key,options.watch[key]);
            })
        }
    }
}

let o = new Vue({
    data: {
        a: 10,
        b: 'hhhh'
    },
    watch: {
        'a': function (newValue) {
            console.log("update a:"+newValue)
        },
        'b': function (newValue) {
            console.log("update b:"+newValue)
        }
    }
})

在vue实例构建的时候,会调用observer函数对data对象中的每个属性进行响应式化,即定义他们的getter和setter并初始化每个属性对应的dep实例。同时根据配置属性watch来生成一个针对属性a的watcher,每当这个数据发生变化时,将调用回调函数更新视图(这里只是控制台输出)。说明起见,每个响应式属性在setter中执行完属性收集,将打印一下对应的dep.subs。代码执行,控制台打印如下:


Vue响应式系统原理分析与简单实现(上)_第3张图片
image.png

控制台输出两个watcher的数组,由于数据a,b各自仅拥有一个观察者watcher,因此每个数组长度均为1,id分别为0和1。属性deps解释一下,该属性维护了其已注册了的订阅者实例dep的id,一旦watcher的注册函数addDep被调用,其首先会从属性deps中查看其在这个dep中是否已被注册过,如果是,则不重新注册。

当改变某个响应式属性,会在赋值时在控制台打印新值:


Vue响应式系统原理分析与简单实现(上)_第4张图片
image.png

在控制台改变一下数据a的值,触发了a所绑定的setter,从而让a的订阅者去
通知其subs中所有的watcher调用update方法去更新视图,最终调用了传给watcher的回调函数,在控制台打印“update a:70”。这里还会打印了dep.subs是因为在真正更新视图前,需要调用get函数去读取一下新值,所以又触发了一次setter。由于我们做了防止watcher重复注册的判断,故打印出的dep.subs中依然只有id为0的一个watcher。

(其实我也疑惑为什么不在setter中直接传值newValue不就无需触发getter了嘛,还有为什么watcher加入dep的行为不直接在dep中push了还兜那么大圈子... 也许是这样的写法解耦的比较彻底...)

然而,还有一个问题值得思考,vue中我们观察的很可能是个对象,比如a.name、a.name.first这样,当对象内部的值发生改变,视图依然可以发生改变。做到这样的深度观察,即需要为对象内部的值也定义好其setter及getter,实现方法不难,无非是递归,这里用Array的reduce方法来改变一下watcher中读取数据的方法getter:

this.getter = function(vm, exp){
      let exprArr = exp.split('.');
      let value = exprArr.reduce((prev,next) => {
          return prev[next];
      }, vm.$data)
      return value;
  };

将a的值改为一个数据再执行下上面的过程:

你可能感兴趣的:(Vue响应式系统原理分析与简单实现(上))