Vue源码学习笔记3—数据绑定

分析准备

数据绑定

一旦data中某个属性更新,页面上对应使用了该属性的节点都会更新。

数据劫持

数据劫持的基本思想:给data中的所有属性提供set方法,set方法监视data属性中的变化,一旦发生变化就去更新界面。 在Vue中使用数据劫持来实现数据绑定。

数据绑定与数据代理

简单的的理解:

  • 通过vm对象来代理data对象中所有属性的操作来实现数据代理
  • 通过给data中的所有属性提供set方法,set方法监视data属性中的变化(数据劫持)来实现数据绑定

四个重要对象

在这里插入图片描述

Observer

  1. 用来对data所有属性数据进行劫持的构造函数
  2. 给data中所有属性重新定义属性描述(defineProperty),添加set/get方法。
  3. 为data中的每个属性创建对应的dep(dependency)对象

Dep

什么时候创建?

初始化时,给data属性进行数据劫持时创建。

Dep的个数?

与data中的属性一一对应,简单理解就是data中有多少个属性就有多少个Dep。

Dep的结构?

  • id:标识(从0开始),每个dep都有一个唯一的id
  • subs:包含n个对应watcher的数组

Compiler

  1. 用来解析模板页面的对象的构造函数(一个实例)
  2. 利用compile对象解析模板页面
  3. 每解析一个表达式(非事件指令)都会创建一个对应的watcher对象,并建立watcher与dep的关系

Watcher

什么时候创建?

初始化时,解析大括号表达式({{}})/一般指令(v-text)时创建。

Watcher的个数?

与模板中表达式(非事件指令)一一对应。

Watcher的结构?

function Watcher(vm, expOrFn, cb) {
    this.cb = cb; // 更新界面的回调函数
    this.vm = vm;
    this.expOrFn = expOrFn; // 对应的表达式
    this.depIds = {}; // 包含所有相关的dep的容器对象

    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = this.parseGetter(expOrFn.trim());
    }
    // 得到表达式对应的value
    this.value = this.get();
}

Dep与Watcher之间的关系

什么关系?

Dep与Watcher之间值多对多的关系(n:n)

  1. data属性–>Dep–>n个Watcher
        <p>{{name}}</p>
        <p v-text="name"></p>
  1. 表达式–>Watcher–>n个Dep
        <p v-text="friend.name"></p>

什么时候建立?

初始化时,解析模板中的表达式创建watcher对象时

源码分析

实例

    <div id="app">
        <p>{{name}}</p>
        <p v-text="name"></p>
        <p v-text="friend.name"></p>
        <button v-on:click="update">更新</button>
    </div>

在进行源码分析之前,首先要弄清楚Dep和Watcher分别是什么以及它们俩之间的关系。
Dep看作属性,如上述代码,实例中有name,friend以及friend.name三个Dep,为了好理解分别编号为d0,d1,d2。Watcher看作表达式,第2,3,4行代码,一共有三个表达式,分别编号为w1,w2,w3。

    <script type="text/javascript">
        new MVVM({
            el: "#app",
            data: {
                name: 'kk',
                friend: {
                    name: 'cc',
                    age: 20
                }
            },
            methods: {
                update() {
                    this.name = 'xx'
                }
            }
        })
    </script>

分析

以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现

  1. 创建observer对象,开始对data的监视(walk()),遍历data中所有属性,对指定的属性实现响应式的数据绑定。
function Observer(data) {
    this.data = data;
    this.walk(data);
}
    walk: function (data) {
        // 保存observer对象
        var me = this;
        Object.keys(data).forEach(function (key) {
            me.convert(key, data[key]);
        });
    },
    convert: function (key, val) {
        this.defineReactive(this.data, key, val);
    },
  1. 创建属性对应的dep对象,对data中所有层次属性的数据劫持,给data重新定义属性,添加set/get方法,建立关系。
    defineReactive: function (data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);
        // 给data重新定义属性,添加set/get方法
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            // 返回值,建立dep与watcher之间的关系
            get: function () {
                if (Dep.target) {
                    // 建立关系
                    dep.depend();
                }
                return val;
            },
            // 监视key属性的变化,更新界面
            set: function (newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是object的话,进行监听
                childObj = observe(newVal);
                // 通知所有相关的订阅者
                dep.notify();
            }
        });
    }
};
  1. 在解析模板的过程中,为表达式创建对应的Watcher,指定更新函数

compile.js

        // 为表达式创建一个对应的watcher,实现节点的更新显示
        new Watcher(vm, exp, function(value, oldValue) {
            //当表达式对应的一个属性值变化时回调
            // 更新界面中的指定节点
            updaterFn && updaterFn(node, value, oldValue);
        });
  1. 建立Watcher到Dep的关系(添加订阅者),Dep中的subs用于保存n个Watcher。

watcher.js

    addDep: function(dep) {
        // 判断dep与watcher关系是否已经建立
        if (!this.depIds.hasOwnProperty(dep.id)) {
            // 将watcher添加到dep 用于更新
            dep.addSub(this);
            // 将dep添加到watcher中 用于防止重复建立关系
            this.depIds[dep.id] = dep;
        }
    },
    // 得表达式的值,建立dep与watcher的关系
    get: function() {
        // 给dep指定当前的watcher
        Dep.target = this;
        // 获取表达式的值,内部调用get建立dep与watcher的关系
        var value = this.getter.call(this.vm, this.vm);
        // 去除dep中指定的当前watcher
        Dep.target = null;
        return value;
    },

以上均为初始化部分,综合分析前准备可以知道,此部分完成Dep、Watcher的创建以及两者关系的建立
当点击更新按钮时

  1. 负责劫持监听所有属性的Observer中的set方法会调用,把发生的变化遍历通知所有Dep(d0,d1,d2),先讲解d0对应w1和w2这一例子,后面的d1对应w3、d2对应w4操作相似。
  2. d0会依次遍历所有对应的watcher(w1,w2),通知watcher变化更新。
    notify: function () {
        this.subs.forEach(function (sub) {
            sub.update();
        });
    }
  1. watcher调用回调函数更新界面
    run: function() {
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },

总结

数据绑定中两个核心技术

  • defineProperty
    • 给data中所有属性重新定义属性描述,添加set/get方法。
  • 消息订阅与发布
    • 建立Watcher到Dep的关系,通知watcher变化更新。

v-model(双向数据绑定)

实例

    <div id="app">
        <input type="text" v-model="msg">
        <p>{{msg}}</p>
    </div>
new MVVM({
            el: "#app",
            data: {
                msg: 'kk'
            }
        })

分析

以下代码用的是:https://github.com/DMQ/mvvm.git,此版进行简化改造主要说明原理与实现

v-model为普通指令,进行v-model的解析。
compile.js

    model: function (node, vm, exp) {
        this.bind(node, vm, exp, 'model');
        var me = this,
            val = this._getVMVal(vm, exp);
        node.addEventListener('input', function (e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }

            me._setVMVal(vm, exp, newValue);
            val = newValue;
        });
    },

如上述代码,第二行中bind方法做了两件事情:1. 解析表达式显示value;2. 创建对应的watcher。第三行保存当前的的compile,取得表达式所对应的值(‘这里为kk’)。添加对应的input监听(输入时发生)。
当输入框发生变化时,上述代码中第6行先获取输入框的值,判断输入框的值与原来是否相等,如果不相等则将最新的值保存到data上面,由前面数据绑定的知识可以知道,此时会导致data上的set方法调用,如下代码。后续即跟数据绑定是一样的,set方法的调用会通知dep,dep会通知对应的watcher(该实例中有两个watcher),最终实现界面的更新。

 // 监视key属性的变化,更新界面
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新的值是object的话,进行监听
                childObj = observe(newVal);
                // 通知所有相关的订阅者
                dep.notify();
            }

小结

  • 双向数据绑定是建立在数据绑定的基础上
  • 双向数据绑定的实现流程
  1. 解析v-model指令,添加input监听
  2. input的value变化时,将最新的值赋值给当前表达式对应的data
  3. 进行数据绑定的操作(调用data上的set–>通知dep–>通知对应watcher–>updater)

一道面试题

vue双向数据绑定原理

答:Vue的双向数据绑定实现了View和Model的同步更新。它实际是建立在数据绑定的基础上。而数据绑定主要通过使用数据劫持和发布订阅者模式来实现的。
数据劫持就是通过Object.defineProperty()给data属性添加set方法,为data中的每个属性创建对应的dep对象。对v-model指令进行解析,添加input监听,为表达式创建对应的watcher, 建立Watcher到Dep的关系(添加订阅者)。当input的value发生变化时,将最新的值赋值给当前表达式所对应的data属性。由data更新,会导致data上的set方法调用,set调用会通知dep,dep会通知watcher,watcher收到通知后会更新视图(Updater)。

该篇为学习过程的学习笔记以及对面试题的简单理解。若有不足和错误,欢迎指正!

猜你喜欢

转载自blog.csdn.net/xicc1112/article/details/106160043