资讯专栏INFORMATION COLUMN

vue的数据驱动原理及简单实现

caoym / 2046人阅读

摘要:监听器构造函数被监听数据属性遍历监听函数属性被监听了,现在值为监听器被监听对象构造函数所有入参监听数据更新视图实现在流程介绍中,我们需要创建一个可以订阅者的订阅器,主要负责手机订阅者,属性变化的时候执行相应的订阅者,更新函数。

1、目标实现

理解双向数据绑定原理;

实现{{}}、v-model和基本事件指令v-bind(:)、v-on(@);

新增属性的双向绑定处理;

PS:实例源码https://github.com/wuwhs/imit...,欢迎随手给个start,就此谢过!

2、双向数据绑定原理

vue实现对数据的双向绑定,通过对数据劫持结合发布者-订阅者模式实现的。

2.1 Object.defineProperty

vue通过Object.defineProperty来实现数据劫持,会对数据对象每个属性添加对应的get和set方法,对数据进行读取和赋值操作就分别调用get和set方法。

Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {

        // do something

        return val;
    },
    set: function(newVal) {

        // do something
    }
});

我们可以将一些方法放到里面,从而完成对数据的监听(劫持)和视图的同步更新。

2.2 过程说明

实现双向数据绑定,首先要对数据进行数据监听,需要一个监听器Observer,监听所有属性。如果属性发生变化,会调用setter和getter,再去告诉订阅者Watcher是否需要更新。由于订阅者有很多个,我们需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。还有,我们需要一个指令解析器Complie,对每个元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或绑定相应函数。当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

3、实现Observer

Observer是一个数据监听器,核心方法是我们提到过的Object.defineProperty。如果要监听所有属性的话,则需要通过递归遍历,对每个子属性都defineProperty。

/**
 * 监听器构造函数
 * @param {Object} data 被监听数据
 */
function Observer(data) {

    if (!data || typeof data !== "object") {
        return;
    }

    this.data = data;
    this.walk(data);

}

Observer.prototype = {
    /**
     * 属性遍历
     */
    walk: function(data) {
        var self = this;
        Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
        });
    },

    /**
     * 监听函数
     */
    defineReactive: function(data, key, val) {

        observe(val);

        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }

                val = newVal;

                console.log("属性:" + key + "被监听了,现在值为:" + newVal);

                updateView(newVal);
            }
        });

        updateView(val);
    }
}

/**
 * 监听器
 * @param {Object} data 被监听对象
 */
function observe(data) {

    return new Observer(data);
}

/**
 * vue构造函数
 * @param {Object} options 所有入参
 */
function MyVue(options) {

    this.vm = this;

    this.data = options.data;

    // 监听数据
    observe(this.data);

    return this;
}

/**
 * 更新视图
 * @param {*} val
 */
function updateView(val) {
    var $name = document.querySelector("#name");
    $name.innerHTML = val;
}

var myvm = new MyVue({
    el: "#demo",
    data: {
        name: "hello word"
    }
});

4、实现Dep

在流程介绍中,我们需要创建一个可以订阅者的订阅器Dep,主要负责手机订阅者,属性变化的时候执行相应的订阅者,更新函数。下面稍加改造Observer,就可以插入我们的订阅器。

Observer.prototype = {
    // ...

    /**
     * 监听函数
     */
    defineReactive: function(data, key, val) {
        var dep = new Dep();

        observe(val);

        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function() {

                // 判断是否需要添加订阅者 什么时候添加订阅者呢? 与实际页面DOM有关联的data属性才添加相应的订阅者
                if (Dep.target) {
                    // 添加一个订阅者
                    dep.addSub(Dep.target);
                }

                return val;
            },
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }

                val = newVal;

                console.log("属性:" + key + "被监听了,现在值为:" + newVal);

                // 通知所有订阅者
                dep.notify(newVal);
            }
        });

        updateView(val);

        // 订阅器标识本身实例
        Dep.target = dep;
        // 强行执行getter,往订阅器中添加订阅者
        var v = data[key];
        // 释放自己
        Dep.target = null;
    }
}

/**
 * 监听器
 * @param {Object} data 被监听对象
 */
function observe(data) {

    return new Observer(data);
}

/**
 * 订阅器
 */
function Dep() {
    this.subs = [];
    this.target = null;
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
        console.log("this.subs:", this.subs);
    },
    notify: function(data) {
        this.subs.forEach(function(sub) {
            sub.update(data);
        });
    },
    update: function(val) {
        updateView(val)
    }
};

// ...

PS:将订阅器Dep添加到一个订阅者设计到getter里面,是为了让Watcher初始化进行触发。

5、实现Watcher

订阅者Watcher在初始化的时候需要将自己添加到订阅器Dep中,那该如何添加呢?我们已经知道监听器Observer是在get函数执行添加了订阅者Watcher的操作,所以我们只要在订阅者Watcher初始化的时候触发对应的get函数去执行添加订阅者操作。那么,怎样去触发get函数?很简单,只需获取对应的属性值就可以触发了,因为我们已经用Object.defineProperty监听了所有属性。vue在这里做了个技巧处理,就是咋我们添加订阅者的时候,做一个判断,判断是否是事先缓存好的Dep.target,在订阅者添加成功后,把target重置null即可。

// ...

/**
 * 订阅者
 * @param {Object} vm vue对象
 * @param {String} exp 属性值
 * @param {Function} cb 回调函数
 */
function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    // 将自己添加到订阅器
    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函数
        // this.vm.data[this.exp] 调用getter,添加一个订阅者sub,存入到全局变量subs
        var value = this.vm.data[this.exp];

        // 释放自己
        Dep.target = null;

        return value;
    }
};

/**
 * vue构造函数
 * @param {Object} options 所有入参
 */
function MyVue(options) {

    this.vm = this;

    this.data = options.data;

    observe(this.data);

    var $name = document.querySelector("#name");

    // 给name属性添加一个订阅者到订阅器中,当属性发生变化后,触发回调
    var w = new Watcher(this, "name", function(val) {
        $name.innerHTML = val;
    });

    return this;
}

到这里,其实已经实现了我们的双向数据绑定:能够根据初始数据初始化页面特定元素,同时当数据改变也能更新视图。

5、实现Compile

步骤4整个过程都能有去解析DOM节点,而是直接固定节点进行替换。接下来我们就来实现一个解析器,完成一些解析和绑定工作。

获取页面的DOM节点,遍历存入到文档碎片对象中;

解析出文本节点,匹配{{}}(暂时只做"{{}}"的解析),用初始化数据替换,并添加相应订阅者;

分离出节点的指令v-on、v-bind和v-model,绑定相应的事件和函数;

// ...

/**
 * 编译器构造函数
 * @param {String} el 根元素
 * @param {Object} vm vue对象
 */
function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}

Compile.prototype = {
    /**
     * 初始
     */
    init: function() {
        if (this.el) {
            console.log("this.el:", this.el);
            // 移除页面元素生成文档碎片
            this.fragment = this.nodeToFragment(this.el);
            // 编译文档碎片
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log("DOM Selector is not exist");
        }
    },

    /**
     * 页面DOM节点转化成文档碎片
     */
    nodeToFragment: function(el) {
        var fragment = document.createDocumentFragment();
        var child = el.firstChild;

        // 此处添加打印,出来的不是页面中原始的DOM,而是编译后的?
        // NodeList是引用关系,在编译后相应的值被替换了,这里打印出来的NodeList是后来被引用更新了的
        console.log("el:", el);
        // console.log("el.firstChild:", el.firstChild.nodeValue);
        while (child) {
            // append后,原el上的子节点被删除了,挂载在文档碎片上
            fragment.appendChild(child);
            child = el.firstChild;
        }

        return fragment;
    },
    /**
     * 编译文档碎片,遍历到当前是文本节点则去编译文本节点,如果当前是元素节点,并且存在子节点,则继续递归遍历
     */
    compileElement: function(fragment) {
        var childNodes = fragment.childNodes;
        var self = this;
        [].slice.call(childNodes).forEach(function(node) {
            // var reg = /{{s*(.+)s*}}/g;
            var reg = /{{s*((?:.|
)+?)s*}}/g;
            var text = node.textContent;

            if (self.isElementNode(node)) {
                self.compileAttr(node);
            } else if (self.isTextNode(node) && reg.test(text)) {
                reg.lastIndex = 0

                /* var match;
                while(match = reg.exec(text)) {
                    self.compileText(node, match[1]);
                } */

                self.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                self.compileElement(node);
            }
        });
    },

    /**
     * 编译属性
     */
    compileAttr: function(node) {
        var nodeAttrs = node.attributes;
        var self = this;

        Array.prototype.forEach.call(nodeAttrs, function(attr) {
            var attrName = attr.name;

            // 只对vue本身指令进行操作
            if (self.isDirective(attrName)) {
                var exp = attr.value;

                // v-on指令
                if (self.isOnDirective(attrName)) {
                    self.compileOn(node, self.vm, exp, attrName);
                }
                // v-bind指令
                if(self.isBindDirective(attrName)) {
                    self.compileBind(node, self.vm, exp, attrName);
                }
                // v-model
                else if (self.isModelDirective(attrName)) {
                    self.compileModel(node, self.vm, exp, attrName);
                }

                node.removeAttribute(attrName);
            }
        })
    },

    /**
     * 编译文档碎片节点文本,即对标记替换
     */
    compileText: function(node, exp) {
        var self = this;
        var exps = exp.split(".");
        var initText = this.vm.data[exp];

        // 初始化视图
        this.updateText(node, initText);

        // 添加一个订阅者到订阅器
        var w = new Watcher(this.vm, exp, function(val) {
            self.updateText(node, val);
        });
    },

    /**
     * 编译v-on指令
     */
    compileOn: function(node, vm, exp, attrName) {
        // @xxx v-on:xxx
        var onRE = /^@|^v-on:/;
        var eventType = attrName.replace(onRE, "");

        var cb = vm.methods[exp];

        if (eventType && cb) {
            node.addEventListener(eventType, cb.bind(vm), false);
        }
    },

    /**
     * 编译v-bind指令
     */
    compileBind: function (node, vm, exp, attrName) {
        // :xxx v-bind:xxx
        var bindRE = /^:|^v-bind:/;
        var attr = attrName.replace(bindRE, "");

        var val = vm.data[exp];

        node.setAttribute(attr, val);
    },

    /**
     * 编译v-model指令
     */
    compileModel: function(node, vm, exp, attrName) {
        var self = this;
        var val = this.vm.data[exp];

        // 初始化视图
        this.modelUpdater(node, val);

        // 添加一个订阅者到订阅器
        new Watcher(this.vm, exp, function(value) {
            self.modelUpdater(node, value);
        });

        // 绑定input事件
        node.addEventListener("input", function(e) {
            var newVal = e.target.value;
            if (val === newVal) {
                return;
            }
            self.vm.data[exp] = newVal;
            // val = newVal;
        });
    },

    /**
     * 更新文档碎片相应的文本节点
     */
    updateText: function(node, val) {
        node.textContent = typeof val === "undefined" ? "" : val;
    },

    /**
     * model更新节点
     */
    modelUpdater: function(node, val, oldVal) {
        node.value = typeof val == "undefined" ? "" : val;
    },

    /**
     * 属性是否是vue指令,包括v-xxx:,:xxx,@xxx
     */
    isDirective: function(attrName) {
        var dirRE = /^v-|^@|^:/;
        return dirRE.test(attrName);
    },

    /**
     * 属性是否是v-on指令
     */
    isOnDirective: function(attrName) {
        var onRE = /^v-on:|^@/;
        return onRE.test(attrName);
    },

    /**
     * 属性是否是v-bind指令
     */
    isBindDirective: function (attrName) {
        var bindRE = /^v-bind:|^:/;
        return bindRE.test(attrName);
    },

    /**
     * 属性是否是v-model指令
     */
    isModelDirective: function(attrName) {
        var mdRE = /^v-model/;
        return mdRE.test(attrName);
    },

    /**
     * 判断元素节点
     */
    isElementNode: function(node) {
        return node.nodeType == 1;
    },

    /**
     * 判断文本节点
     */
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
};

/**
 * vue构造函数
 * @param {Object} options 所有入参
 */
function MyVue(options) {

    this.vm = this;

    this.data = options.data;

    this.methods = options.methods;

    observe(this.data);

    new Compile(options.el, this.vm);

    return this;
}

这样我们就可以调用指令v-bind、v-on和v-model。


    
    



    

{{ name }}

5、其他 5.1 proxy代理data

可能注意到了,我们不管是在赋值还是取值,都是在myvm.data.someAttr上操作的,而在vue上我们习惯直接myvm.someAttr这种形式。怎样实现呢?同样,我们可以用Object.defineProperty对data所有属性做一个代理,即访问vue实例属性时,代理到data上。很简单,实现如下:

/**
 * 将数据拓展到vue的根,方便读取和设置
 */
MyVue.prototype.proxy = function(key) {
    var self = this;

    Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get: function proxyGetter() {
            return self.data[key];
        },
        set: function proxySetter(newVal) {
            self.data[key] = newVal;
        }
    });
}

5.2 parsePath

上面对于data的操作只是到对于简单的基本类型属性,对于对象属性的改变该怎么更新到位呢?其实,只要深度遍历对象属性路径,就可以找到要访问属性值。

/**
 * 根据对象属性路径,最终获取值
 * @param {Object} obj 对象
 * @param {String} path 路径
 * return 值
 */
function parsePath(obj, path) {
    var bailRE = /[^w.$]/;
    if (bailRE.test(path)) {
        return
    }
    var segments = path.split(".");

    for (var i = 0; i < segments.length; i++) {
        if (!obj) { return }
        obj = obj[segments[i]];
    }
    return obj;
}

用这个方法替换我们的所有取值操作
vm[exp] => parsePath(vm, exp)

6、新增属性的双向数据绑定 6.1 给对象添加属性

Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上。
也就是我们需要在Vue原型上添加一个set方法去设置新添加的属性,新属性同样要进行监听和添加订阅者。

/**
 * vue的set方法,用于外部新增属性 Vue.$set(target, key, val)
 * @param {Object} target 数据
 * @param {String} key 属性
 * @param {*} val 值
 */
function set(target, key, val) {
    if (Array.isArray(target)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val);
        return val;
    }

    if (target.hasOwnProperty(key)) {
        target[key] = val;
        return val
    }
    var ob = (target).$Observer;

    if (!ob) {
        target[key] = val;
        return val
    }

    // 对新增属性定义监听
    ob.defineReactive(target, key, val);

    ob.dep.notify();

    return val;
}

MyVue.prototype.$set = set;
6.1 给数组对象添加属性

把数组看成一个特殊的对象,就很容易理解了,对于unshift、push和splice变异方法是添加了对象的属性的,需要对新加的属性进行监听和添加订阅者。

var arrKeys = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
var extendArr = [];

arrKeys.forEach(function(key) {
    def(extendArr, key, function() {
        var result,
            arrProto = Array.prototype,
            ob = this.$Observer,
            arr = arrProto.slice.call(arguments),
            inserted,
            index;

        switch (key) {
            case "push":
                inserted = arr;
                index = this.length;
                break;
            case "unshift":
                inserted = arr;
                index = 0;
                break;
            case "splice":
                inserted = arr.slice(2);
                index = arr[0];
                break;
        }

        result = arrProto[key].apply(this, arguments);

        // 监听新增数组对象属性
        if (inserted) {
            ob.observeArray(inserted);
        }

        ob.dep.notify();

        return result;
    });
});

var arrayKeys = Object.getOwnPropertyNames(extendArr);

/**
 * 监听器构造函数
 * @param {Object} data 被监听数据
 */
function Observer(data) {

    this.dep = new Dep();

    if (!data || typeof data !== "object") {
        return;
    }

    // 在每个object上添加一个observer
    def(data, "$Observer", this);

    // 继承变异方法
    if (Array.isArray(data)) {

        // 把数组变异方法的处理,添加到原型链上
        data.__proto__ = extendArr;

        // 监听数组对象属性
        this.observeArray(data);
    } else {
        this.data = data;
        this.walk(data);
    }
}

Observer.prototype = {
    // ...

    /**
     * 监听数组
     */
    observeArray: function(items) {
        console.log("items:", items);
        for (var i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    }
};

本文是在查看vue源码及大神相关博客而成,只为加深自己的学习印象,拿出来和大家一起学习,有什么不对的地方欢迎提出,参考文章:
http://www.cnblogs.com/giggle...
http://www.cnblogs.com/canfoo...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/107453.html

相关文章

  • 理解数据驱动视图原理

    摘要:源代码响应式原理数据观察数据递归无反应无反应无反应包括无反应讲解只所以能实现双向绑定,是利用里面的这就是为什么只支持及以上从以上代码可以看出,对象属性的删除和添加新属性,不会触发对应的方法对于初始化没有定义的属性,设置值不能触发视图层渲染在 源代码1 // 响应式原理 defineProperty //数据 const data = { obj: { a: 4, ...

    Lyux 评论0 收藏0
  • vue-threeJS数据驱动三维图形可视化

    摘要:数据驱动的三维图形可视化在信息暴涨的年间,冷暴力的扁平化确实有效降低用户的信息焦虑感,使有限的精力更高效处理过多的信息流。 数据驱动的三维图形可视化 在信息暴涨的2010-2016年间,冷暴力的扁平化确实有效降低用户的信息焦虑感,使有限的精力更高效处理过多的信息流。二维平面化扁平化在苹果等大头引领下,成为大众用户机器交流默认的语言。然后,随着PC、平板、手机、智能家居等用户持有终端的性...

    SegmentFault 评论0 收藏0
  • 前端开发之走进Vue.js

    摘要:作为目前最热门最具前景的前端框架之一,其提供了一种帮助我们快速构建并开发前端项目的新的思维模式。的新版本,的简称。的包管理工具,用于同一管理我们前端项目中需要用到的包插件工具命令等,便于开发和维护。 Vue.js作为目前最热门最具前景的前端框架之一,其提供了一种帮助我们快速构建并开发前端项目的新的思维模式。本文旨在帮助大家认识Vue.js,了解Vue.js的开发流程,并进一步理解如何通...

    zxhaaa 评论0 收藏0
  • vue总结系列--数据驱动和响应式

    摘要:由于是需要兼容的后台系统,该项目并不能使用到等技术,因此我在上的经验大都是使用原生的编写的,可以看见一个组件分为两部分视图部分,和数据部分。 在公司里帮项目组里开发后台系统的前端项目也有一段时间了。 vue这种数据驱动,组件化的框架和react很像,从一开始的快速上手基本的开发,到后来开始自定义组件,对element UI的组件二次封装以满足项目需求,期间也是踩了不少坑。由于将来很长一...

    AbnerMing 评论0 收藏0
  • vue培训记录

    在公司做了一次vue相关的培训,自己整理了一些大纲。供大家参考学习!当然 优先要先看官方文档 1. 项目构成及原理 Vue 主流框架见解及差别 react ALL IN JS 灵活 angular 架构清晰 层级多 重 vue 类似react并吸收了angular的一些优点 Node运行在服务端的JS 谷歌V8引擎 使JS语言能在服务器端运行 Webpack—一个前端的打包工具 ...

    curlyCheng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<