Vue 简单MVVM实现思路

实现方式:数据劫持结合发布者-订阅者模式

MVVM的实现核心:

  • Observer监听器

劫持并监听data内的所有属性,如有变动,拿到最新值并且通知订阅者

  • Watcher订阅者

收到属性的变动通知,执行指令绑定的回调函数,从而更新视图
起到桥梁作用

  • Compiler解析器

对每个DOM节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

代码实现思路:

监听器Observer

  1. 遍历所有属性,进行响应式转化,做到监听所有属性的变化
function observer(data, vm) { // 遍历所有子属性
    if (!data || typeof data !== 'object') {
        return;
    }
    return new Observer(data); // run函数
}

Observer.prototype = {
   
    run: function (data) {
        var _this = this;
        Object.keys(data).forEach(function (key) { // 遍历data中的所有属性
            _this.convert(key, data[key]); // 将每个属性转化成可响应数据 即defineReactive
        });
    },
    convert: function(key, value) {
        this.defineReactive(this.data, key, value);
    },
     defineReactive: function(data, key, value) {
        var obj = observer(value); //如果data中的属性是一个对象,通过递归方法,监听子属性
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get: function() {
                return value;
            },
            set: function(newVal) {
                if (value === newVal) {
                    return;
                }
                value = newVal;
                obj = observer(value); // 新的值是object的话,进行监听
            }
        })
    },
    
}

问答:

为什么 Vue 不支持 IE8 以及更低版本浏览器?
因为:Object.defineProperty 是仅 ES5 支持,且无法 shim 的特性。

Object.keys()用于获取对象自身所有的可枚举的属性值,但不包括原型中的属性,然后返回一个由属性名组成的数组。注意它同for…in一样不能保证属性按对象原来的顺序输出。

  1. 创建消息订阅器Dep:负责收集订阅者Watcher,当属性变化时,触发notify,再调用订阅者的update方法
Object.defineProperty(data, key, {
  ...
    set: function(newVal) {
        ...
        dep.notify(); // 如果数据变化,通知所有订阅者
    }
})
function Dep() { // 属性订阅器
    this.id = uid++;
    this.subs = []; // 数组 存储属性 用来收集订阅者,数据变动触发notify,再调用订阅者的update方法
}
Dep.prototype = {
    addSub: function(sub) { // 负责向订阅器Dep中添加属性
        this.subs.push(sub);
    },
    depend: function() { 
        Dep.target.addDep(this); // 添加订阅器 addDep为watcher中的方法
    },
    notify: function() { // 如果数据有变化 通知所有订阅者
        this.subs.forEach(function(sub){
            sub.update(); //更新属性
        })
    }
}
Dep.target = null; // 定义全局变量暂存watcher
  1. 给Dep添加订阅者:就必须要在闭包内操作,所以在getter里面动手脚。
Object.defineProperty(data, key, {
  ...
    get: function() {
        if(Dep.target) { // 用来区分是普通get还是收集依赖时的get 判断是否需要添加订阅者
            dep.depend(); // 在这里添加一个订阅者
        }
        return value;
    },
  ...
})

将Dep添加设置在getter()中,是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者;
在setter()中,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数

订阅者Watcher

  1. 初始化的时候需要将自己添加进订阅器Dep中

监听器Observer在get()中执行了添加订阅者Watcher的操作,所以只要在订阅者Watcher初始化时,触发对应的get函数(获取对应的属性值)即可完成添加订阅者操作。

function Watcher(vm, expFn, cb) {
    this.vm = vm;
    this.expFn = expFn;
    this.cb = cb;
    this.depIds = {};

    // 此处为了触发属性的getter,从而在dep添加自己 即 将自己添加到订阅器的操作
    this.value = this.get();
}
Watcher.prototype = {
   ...
    get: function() {
        Dep.target = this; // 将当前订阅者指向自己
        var value = this.getter.call(this.vm, this.vm); // 这里会触发属性的getter,从而添加订阅者 将自己添加到属性订阅器中 
        Dep.target = null; // 添加完毕后,释放闭包中的变量
        return value; // 返回从订阅器中获取的属性最新值
    },
    addDep: function(dep) { // 添加订阅器的方法
        if(!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    }
}
  1. 初始化Dep添加

因为只在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了

Watcher.prototype = {
   ...
    get: function() {
        Dep.target = this; // 将当前订阅者指向自己
        var value = this.getter.call(this.vm, this.vm); // 这里会强制触发属性的getter,从而添加订阅者 将自己添加到属性订阅器中 
        Dep.target = null; // 添加完毕后,释放闭包中的变量
        return value; // 返回从订阅器中获取的属性最新值
    },
    ...
}
  1. 定义自身的update方法(因为在Observer中调用了watcher的update方法)
Dep.prototype = {
 ...
    notify: function() { // 如果数据有变化 通知所有订阅者
        this.subs.forEach(function(sub){
            sub.update(); //更新属性
        })
    }
}
Watcher.prototype = {
    update: function() {
        this.watchRun();
    },
    watchRun: function(){
        var originalValue = this.value; // 更新前属性的值
        var value = this.get(); // 更新后属性的值

        if (originalValue !== value) {
            this.value = value;
            this.cb.call(this.vm, value, originalValue); // 属性发生变化时,更新属性
        }
    },
}
  1. 待属性变动dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退

解析器Compile

  1. 将根节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中

// 遍历解析过程涉及多次dom节点操作,当要向document中添加大量数据时,如果逐个添加这些新节点,因为每添加一个节点都会调用父节点的appendChild()方法产生一次回流和重绘,过程非常缓慢,
// 为了提高性能,可以使用一个文档碎片,把所有的新节点附加其上,
// 然后把文档碎片一次性添加到document中,减少了重新渲染的次数提高了性能。

function Compile(el, vm) {
    this.$vm = vm;
    this.$el = this.isElementNode(el) ? el : document.querySelector(el); // 判断是否为元素节点

    if(this.$el) {
        this.$fragment = this.nodeToFragment(this.$el); // 复制原生节点
        this.init(); // 初始化
        this.$el.appendChild(this.$fragment); // 实现初始化的时候将数据渲染到视图上
    }
}
Compile.prototype = {
    nodeToFragment: function (el) {
        var fragment = document.createDocumentFragment(); // 创建元素
        var childElement;
        
        while(childElement = el.firstChild) { // 将原生节点拷贝到fragment 将el中的元素全部复制到fragment集中
            fragment.appendChild(childElement);
        }
        return fragment;
    },
    init: function () {
        this.compileElement(this.$fragment); // 解析fragment集的元素
    },
};
childElement = el.firstChild表示:appendChild是把一个节点给"移"到flag上,
也就是说移动之后node里面就没有这个child节点了,
所以这样可以遍历node的所有孩子并把它们都移到文档碎片fragment上
  1. 扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定
    本文只对带有 ‘{{变量}}’ 这种形式的指令进行处理
    compileElement: function (el) {
        var _this = this;
        var child = el.childNodes;

        [].slice.call(child).forEach(function (item) {
            var text = item.textContent; // 获得元素的文本属性
            var reg = /\{\{(.*)\}\}/; // 表达式文本 

            if(_this.isElementNode(item)) { // 判断是否为元素节点
                _this.compile(item);
            } else if (_this.isTextNode(item) && reg.test(text)){ // 判断是否是符合这种形式{{}}的指令
                _this.compileText(item, RegExp.$1); // 与正则表达式匹配的第一个子匹配的字符串
            }

            if(item.childNodes && item.childNodes.length) { // 继续遍历子元素
                _this.compileElement(item);
            }
        });
    },

[].slice.call():将arguments对象的数组提出来转化为数组,arguments本身并不是数组而是对象

  1. 判断是否是元素节点
isElementNode: function (node) { // 判断是否为元素节点
   return node.nodeType === 1;
},
   
// 解析指令
    compile: function (node) {
        var _this = this;
        var nodeAttrs = node.attributes;
        [].slice.call(nodeAttrs).forEach(function (item) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = item.name; // 本例中为 v-text
            if(_this.isDirective(attrName)) { // 判断是否为指令 v-xxx
                var value = item.value; // 本例中为 content
                var dir = attrName.substring(2); // 提取属性的v-后面的关键字 本例中为 text

                if(_this.isEventDirective(dir)) { // 事件指令 如 v-on:click
                    compileUtil.eventHandler(node, _this.$vm, value, dir)
                } else {
                    // 普通指令 此处为v-model v-text
                    compileUtil[dir] && compileUtil[dir](node, _this.$vm, value); // 这里只处理了v-model
                }
                node.removeAttribute(attrName); // 移除已读取处理过的dom节点上设置的属性
            }
        })
    },
    isDirective: function (attrName) { // 判断是否为指令
        return attrName.indexOf('v-') === 0;
    },
    isEventDirective: function (tmp) { // 判断是否为事件指令
        return tmp.indexOf('on') === 0;
    },
    eventHandler: function (node, vm, exp, tmp) { // 事件指令处理方法
        var eventType = tmp.split(':')[1]; // 事件指令绑定的方法名 v-on:click中的click
        var fun = vm.$options.methods && vm.$options.methods[exp]; // 绑定的方法

        if(eventType && fun) {
            node.addEventListener(eventType, fun.bind(vm), false); // 给元素添加监听事件,即绑定的事件
        }
    },
   var compileUtil = {
	text: function (node, vm, exp) { // 文本解析
        this.bind(node, vm, exp, 'text')
    },
    model: function (node, vm, value) { // model指令解析
        this.bind(node, vm, value, 'model');
        var _this = this;
        var val = this.nfuva // 读取属性的值

        node.addEventListener('input', function (e) { // 为input输入框添加监听事件,value值发生变化时触发
            var newValue = e.target.value; //

            if(val === newValue) {
                return ;
            }
            _this._setVMVal(vm, value, newValue);
            val = newValue;

        })
    },
}
  1. 判断是否是文本内容
 isTextNode: function (node) { // 判断是否为文本内容
    return node.nodeType === 3;
 }
compileText: function (node, exp) { // 解析文本
   compileUtil.text(node, this.$vm, exp);
},
var compileUtil = {
    text: function (node, vm, exp) { // 文本解析
        this.bind(node, vm, exp, 'text')
    },
    bind: function (node, vm, exp, tmp) {
        var updaterFn = updater[tmp + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function (value, originalValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, originalValue);
        })
    },
}
// 更新函数
var updater = {
    textUpdater: function (node, value) { // 文本内容更新
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    modelUpdater: function (node, value, originValue) { // model指令绑定的属性值更新
        node.value = typeof value == 'undefined' ? '' : value;
    }

};

双向数据绑定

data到view总结:data:msg—>{{msg}}
data属性变化–>触发set–>dep.notify()通知所有订阅者watcher–>Dep调用了所有Watcher的update–>data的值赋给了view中的节点

view到data总结:input输入框—>data:inputValue
compile.js
给view的节点添加事件—>如 input事件触发时,把e.target.value赋值给vue对象里面的data,以此来实现view的变化同步到data上

github完整可运行代码https://github.com/MingleJia/vue-data-binding
在这里插入图片描述

参考链接:
https://www.cnblogs.com/libin-1/p/6893712.html
https://www.jianshu.com/p/c2fa36835d77
https://github.com/DMQ/mvvm
https://segmentfault.com/a/1190000007741904

猜你喜欢

转载自blog.csdn.net/MingleHDU/article/details/84400984
今日推荐