博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vue的数据驱动原理及简单实现
阅读量:6612 次
发布时间:2019-06-24

本文共 15165 字,大约阅读时间需要 50 分钟。

1、目标实现

  1. 理解双向数据绑定原理;
  2. 实现{
    {}}、v-model和基本事件指令v-bind(:)、v-on(@);
  3. 新增属性的双向绑定处理;

PS:实例源码,欢迎随手给个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    }});

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

definePropertypng

2.2 过程说明

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

intruduce

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"    }});

Observer

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节点,而是直接固定节点进行替换。接下来我们就来实现一个解析器,完成一些解析和绑定工作。

  1. 获取页面的DOM节点,遍历存入到文档碎片对象中;
  2. 解析出文本节点,匹配{
    {}}(暂时只做"{
    {}}"的解析),用初始化数据替换,并添加相应订阅者;
  3. 分离出节点的指令v-on、v-bind和v-model,绑定相应的事件和函数;

progress

// .../** * 编译器构造函数 * @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*((?:.|\n)+?)\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 }}

imitate5

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;        }    });}

imitate6

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)

imitate6_1

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://hgaso.baihongyu.com/

你可能感兴趣的文章
转] C#中out和ref之间的区别
查看>>
android关键知识
查看>>
selenium切换窗口后定位元素出现问题的解决方案
查看>>
java模拟实现生产者---消费者问题
查看>>
QTP的那些事--有关的一些重要可用的函数(发送邮件)
查看>>
排列与组合的一些定理
查看>>
模板引擎Nvelocity实例
查看>>
[原创]Gerrit中文乱码问题解决方案分享
查看>>
灵活运用Zend框架
查看>>
HTML元素的默认CSS设置介绍
查看>>
CSS-图片不变形设置
查看>>
Autodesk云计算系列视频 --- 开篇介绍 Up to the cloud 直上云端
查看>>
自旋锁spin_lock和raw_spin_lock【转】
查看>>
Git异常:fatal: could not create work tree dir 'XXX': No such file or directory
查看>>
JavaScript:综合案例-表单验证
查看>>
GNU make manual 翻译(八十二)
查看>>
js命名空间
查看>>
python批量下载图片的三种方法
查看>>
C语言:返回两个数组中第一个元素的指针,并输出这个值
查看>>
[项目过程中所遇到的各种问题记录]ORM篇——使用NHibernate配置对象实体的一些小问题...
查看>>