深入理解vue2.x双向数据绑定原理

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 深入理解vue2.x双向数据绑定原理

目录


前言


在面试中会经常问到vue2.x双向数据绑定是怎么实现的,大多数面试者都会回答Object.defineProperty()方法对属性进行拦截,把data中的每个数据的读写都转化成getter/setter,当数据发生变化时候通知试图进行更新。本文将详细论述双向数据绑定的原理是怎么实现。


MVVM模式


简单来说就是:


  • 页面数据变化——>data中同步变化
  • data中数据变化——>文本节点内容同步变化


页面中数据改变,我们可以用过事件监听得到,比如input 框中数据改变,我们可以通过oninput监听,但是data中数据的改变怎么更新到视图层呢?data中数据改变可以通过Object.defineProperty()对属性设置set属性,所以我们需要在set中添加一些更新数据的方法,就能够实现data变化更新视图。

实现双向数据绑定,首先需要对数据劫持监听,因此设置一个听器Observer来监听数据。属性数据发生变化需要告诉订阅者Watcher是否需要更新数据。订阅者可能是多个(多个属性发生变化),所以就需要一个消息订阅器Dep收集这些订阅者,Observer和Watcher通过Dep进行统一管理。我们能监听数据是怎么改变,但是如何更新呢。我们就需要一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此我们需要四个操作:


  1. 实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
  2. 实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理
  3. 实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
  4. 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。


监听器Observer


以下我们对数据可以实现了监听。如果多个属性就需要遍历属性监听。


function defineReactive(data, key, val) {
    //递归遍历所有子属性
    observe(val); 
    //具体监听操作
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return val;
        },
        set: function(newVal) {
            val = newVal;
        }
    });
}
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    //批量监听
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

我们需要一个容器能够放置这些订阅者,若有属性变化则对应的订阅者执行更新的函数。将上面代码更改,植入一个消息订阅者。

function Dep () {
    //订阅者集合
    this.subs = [];
}
Dep.prototype = {
    //添加订阅者
    addSub: function(sub) {
        this.subs.push(sub);
    },
    //提醒订阅者更新视图
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;
function defineReactive(data, key, val) {
// 递归遍历所有子属性
    observe(val); 
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
             //同一时间只能有一个Watcher执行且只有在watcher初始化的时候加入到消息订阅器,所以在消息订阅器中加入target属性
            if (Dep.target) {
                // 在这里添加一个订阅者
                dep.addSub(watcher); 
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal)  return
            val = newVal;
            // 如果数据变化,通知所有订阅者
            dep.notify(); 
           }
    });
}

从代码上看,我们设计了一个订阅器 Dep 类,该类里面定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 Dep.target,这是一个全局唯一 的Watcher,因为在同一时间只能有一个全局的 Watcher 被计算

我们还将订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者,至于具体设计方案,下文会详细说明的。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整Observer已经实现了,接下来我们开始设计Watcher。


订阅者Watcher


订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep 中,那该如何添加呢?我们已经知道监听器Observer 是在 get 函数执行了添加订阅者 Wather 的操作的,所以我们只要在订阅者 Watcher 初始化的时候触发对应的 get 函数去执行添加订阅者操作即可,那要如何触发 get 的函数,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了 Object.defineProperty( ) 进行数据监听。

这里还有一个细节点需要处理,我们只要在订阅者 Watcher 初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在 Dep.target 上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者 Watcher 的实现如下:

function Watcher(vm, exp, cb) {
    this.cb = cb;//更新函数
    this.vm = vm;//vue实例对象
    this.exp = exp;//指令属性值
    // 将自己添加到消息订阅器的操作
    this.value = this.get();  
}
Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        // 缓存自己
        Dep.target = this;  
        // 强制执行监听器里的get函数
        var value = this.vm.data[this.exp]
        Dep.target = null;  // 释放自己
        return value;
    }
};

订阅者Watcher中的参数:

  • cb:更新函数
  • vm:vue实例
  • exp:node节点指令的属性值。比如v-mode="data",exp就是data。

当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入get 函数,首先会执行:

Dep.target = this;

将自己赋值为全局的订阅者 复制代码实际上就是把Dep.target 赋值为当前的渲染 watcher ,接着又执行

let value =this.vm.data[this.exp]

强制执行监听器里的get函数 复制代码在这个过程中会对 vm上的数据访问,其实就是为了触发数据对象的 getter。 每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的dep 的 watchers 中,这个目的是为后续数据变化时候能通知到哪些 watchers 做准备。

这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target恢复成上一个状态,即:

Dep.target = null;

释放自己 复制代码而 update() 函数是用来当数据发生变化时调Watcher 自身的更新函数进行更新的操作。先通过 let value = this.vm.data[this.exp],获取到最新的数据,然后将其与之前 get() 获得的旧数据进行比较,如果不一样,则调用更新函数 cb 进行更新。 至此,简单的订阅者Watcher 设计完毕。


解析器Compile


通过监听器 Observer 订阅器 Dep 和订阅者 Watcher 的实现,其实就已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom 节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器 Compile 来做解析和绑定工作。解析器 Compile 实现步骤:


  • 解析模板指令,并替换模板数据,初始化视图;
  • 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器。

我们下面对 ‘{{变量}}’ 这种形式的指令处理的关键代码进行分析,感受解析器 Compile 的处理逻辑,关键代码如下:

//解析元素
function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/;
        var text = node.textContent;
        // 判断是否是符合这种形式{{}}的指令
        if (self.isTextNode(node) && reg.test(text)) {  
            self.compileText(node, reg.exec(text)[1]);
        }
        // 继续递归遍历子节点
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    // 将初始化的数据初始化到视图中
    this.updateText(node, initText);  
    // 生成订阅器并绑定更新函数
    new Watcher(this.vm, exp, function (value) {  
        self.updateText(node, value);
    });
},
function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

总结


从头撸到尾,还是很有收获的,参考了几位大佬的文章做了总结,收获满满。

目录
相关文章
|
5月前
|
JavaScript 前端开发 Serverless
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
Vue.js的介绍、原理、用法、经典案例代码以及注意事项
153 2
|
3月前
|
JavaScript 算法 编译器
vue3 原理 实现方案
【8月更文挑战第15天】vue3 原理 实现方案
43 1
|
11天前
|
缓存 JavaScript 搜索推荐
Vue SSR(服务端渲染)预渲染的工作原理
【10月更文挑战第23天】Vue SSR 预渲染通过一系列复杂的步骤和机制,实现了在服务器端生成静态 HTML 页面的目标。它为提升 Vue 应用的性能、SEO 效果以及用户体验提供了有力的支持。随着技术的不断发展,Vue SSR 预渲染技术也将不断完善和创新,以适应不断变化的互联网环境和用户需求。
30 9
|
2月前
|
缓存 JavaScript 前端开发
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
该文章全面覆盖了Vue.js从基础知识到进阶原理的48个核心知识点,包括Vue CLI项目结构、组件生命周期、响应式原理、Composition API的使用等内容,并针对Vue 2与Vue 3的不同特性进行了详细对比与讲解。
「offer来了」从基础到进阶原理,从vue2到vue3,48个知识点保姆级带你巩固vuejs知识体系
|
27天前
|
JavaScript UED
Vue双向数据绑定的原理
【10月更文挑战第7天】
|
13天前
|
JavaScript 前端开发 API
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
vue3知识点:Vue3.0中的响应式原理和 vue2.x的响应式
22 0
|
2月前
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
该文章对比了Vue2与Vue3在响应式原理上的不同,重点介绍了Vue3如何利用Proxy替代Object.defineProperty来实现更高效的数据响应机制,并探讨了这种方式带来的优势与挑战。
vue2的响应式原理学“废”了吗?继续观摩vue3响应式原理Proxy
|
2月前
|
开发框架 JavaScript 前端开发
手把手教你剖析vue响应式原理,监听数据不再迷茫
该文章深入剖析了Vue.js的响应式原理,特别是如何利用`Object.defineProperty()`来实现数据变化的监听,并探讨了其在异步接口数据处理中的应用。
|
2月前
|
缓存 JavaScript 容器
vue动态组件化原理
【9月更文挑战第2天】vue动态组件化原理
43 2
|
3月前
|
缓存 JavaScript 前端开发
[译] Vue.js 内部原理浅析
[译] Vue.js 内部原理浅析