vue响应式数据的实现原理解析

1,796 阅读4分钟

今天讲下vue的响应式数据,也就是mvvm双向绑定模式,主要的目的是要让大家了解该模式在vue中是如何实现的,所以将以极简的代码进行示例。

我们先假设这样的一个使用情景:

<div id="app">
    <input type="text" v-model="text">
    {{text}}
</div>
let  vm=new Vue({
    el:'app',
    data:{
        text:'hello world'
    }
});

这里就涉及到了vue的双向绑定。



接下来我就用一些非常简单代码实现以上功能。

首先,我们得解析vue中的v-model指令,也就是html中的自定义属性,以及插入组件中的变量{{text}}。这两者都与响应式数据text有关。

vue2.x后就使用了虚拟dom和diff算法,但今天的目的是理解双休绑定的原理,为了方便大家的理解,这里使用vue1.x用过document Fragment来代替。document Fragment是一个节点片段,对它的进行dom操作不会导致页面的重排和重绘。它就是一个优化dom操作的对象。具体的大家可以参考下MDN教程

/**
 * 节点转换成节点片段,优化动态改变节点的性能
 * @param root - vue的根节点
 * @param vm - vue实例
 */
function nodeToFramge(root,vm) {
    let df=document.createDocumentFragment();
    let node;
    //不断的把vue要管理的dom元素放到document Fragment中。
    while(node=root.firstChild){
        //优化dom操作性能问题。
        df.appendChild(node);
        //进行dom的解析,也可以理解成编译吧
        compile(node,vm);
    }
    return df;
}

如何解析呢,一般就是遍历dom节点了,然后判断其中的节点类型,是元素节点就获取其中的属性节点,然后再进行遍历,最后获取到v-model属性,简单示例:

/**
 *编译模板
 * @param node - vue管理的节点
 * @param vm - vue实例
 */
function compile(node,vm) {
    //节点类型为元素
    if(node.nodeType===1){
        let attr=node.attributes;
        //遍历解析html的属性
        for(let i=0;i<attr.length;i++){
            //v-m的数据响应
            if(attr[i].nodeName==='v-model'){
                //获取html属性的值,也就是响应式数据的键名
                let name=attr[i].nodeValue;
                //初始化输入控件的数据
                node.value=vm[name];
                //监听数据的变化,实现v-m的数据响应
                node.addEventListener('input',function (e) {
                    vm[name]=e.target.value;
                });
                //删除v-model自定义属性
                node.removeAttribute('v-model');
            }
        }
    }
}

而当遍历到文本节点时:

//节点为文本类型
else if(node.nodeType===3){
    //识别响应式数据的正则表达式
    let reg=/\{\{(.*)\}\}/;
    //找出响应式数据
    if(reg.test(node.nodeValue)){
        //从正则表达式的子表达式中获取响应式数据的键名
        let name=RegExp.$1.trim();
        //创建观察者
        new Watcher(vm,node,name);
    }
}

然后我们开始为每个插入dom的中数据实现一个观察者:

/**
 * 观察者
 * @param vm
 * @param node
 * @param name
 * @constructor
 */
function Watcher(vm,node,name) {
    //标志变量。判断是否要进行观察者的注册。
    Moniter.target=this;
    //要改变的节点
    this.node=node;
    //响应式数据的键名
    this.name=name;
    //vue实例
    this.vm=vm;
    //初始化数据和注册观察者
    this.update();
    //注册完成,取消标志变量
    Moniter.target=null;
}
//更新数据
Watcher.prototype.update=function () {
    //第一次调用时就是触发数据的get方法去初始化数据和注册观察者。之后时更新数据
    this.node.nodeValue=this.vm[this.name];
};

然后数据劫持,注册观察者。数据劫持主要用到了Object.defineProperty方法,具体的同学可以看MDN的教程

/**
 * 数据劫持
 * @param vm
 */
function defineReactive(vm) {
    Object.keys(vm.data).forEach(function (name) {
        //保存未被访问器属性覆时,数据属性的值。
        let value=vm.data[name];
        //注册监听者
        let mo=new Moniter();
        //数据劫持
        Object.defineProperty(vm,name,{
            set:function (newValue) {
                if(value===newValue) return;
                //触发观察者实现数据更新
                value=newValue;
                mo.dispatch();
            },
            get:function () {
                //判断是否时初始化数据,然后注册观察者
                if(Moniter.target) mo.addWatcher(Moniter.target);
                return value;
            }
        })
    })
}

并为每个响应式的属性实现一个监听者:

/**
 * 监听者
 * @constructor
 */
function Moniter() {
    //保存观察者的数组
    this.watchers=[];
}
//触发观察者
Moniter.prototype.dispatch=function () {
    this.watchers.forEach(function (watcher) {
        watcher.update();
    })
};
//注册观察者
Moniter.prototype.addWatcher=function (target) {
    this.watchers.push(target);
}; 

vue的观察者并不是一个函数,而是一个对像,如watcher对象。每个属性都有一个监听者,就是保存观察者的数组。观察者和监听者之间又个全局标准,判断是否要实现数据监听。view到model方向的数据变化是js的事件监听实现的,也算是内置的观察者模式吧,在编译模板的时候就已经实现观察者的注册,mode到view方向的数据变化是自定义的观察者模式。在编译模板中创建观察者,在为数据创建访问器属性时创建坚监听者,在get方法中注册观察者,在set方法中触发监听器。

整体来说就是元素提取,模板编译,事件监听。

/**
 * vue类
 * @param options - 配置的数据
 * @constructor
 */
function Vue(options) {
    //将响应式数据与vue实例关联
    this.data=options.data;
    //获取vue的根节点
    let root=document.getElementById(options.el);
    //数据劫持
    defineReactive(this);
    //编译模板
    root.appendChild(nodeToFramge(root,this));
}

这篇文章感觉涉及的东西有点多,而且有点绕,一直想不好该怎么写才能让大家更好的理解,因此,我只好把几乎每句代码都写上了注释,希望大家能够理解并且有所收获吧。

可以直接运行得到示例代码

参考:Vue.js双向绑定的实现原理(这篇文章写得很好)