设计一个简单mvvm例子(vue2.x)

1. 引言

学习vue有段时间了,mvvm在vue中是个典型应用,最近参考了参考网上一些资料,整理了一下,也加入了自己的理解,实现一个简单版的demo,也方便有些面试的同学遇到设计一个mvvm的面试题。

2. 逻辑结构

mvvm的设计模式是“发布与订阅者”模式(observe/watcher),主要步骤有三步:

  • observe来劫持并监听所有的属性(也就是vue中的data)
  • 给每一个需要监听的属性,绑定一个订阅者(watcher)
  • 当observe监听到属性变化时,通知watcher去更新视图

ok,步骤讲完了,接下来就开始实现每一步

3. observe

observe的主要功能:

  • 劫持和监听数据
  • 当数据更新时,触发通知(后面的dep会讲,这里跳过)
    那么observe劫持和监听数据的呢?用Object.defineProperty来实现

先看一个例子:

function observe (obj) {
   var keys = Object.keys(obj)
   keys.forEach(function(key){
     var val = void 0;
     Object.defineProperty(obj, key, {
        enumerable : true, 
        configurable : true,
        get : function () {
           console.log('这个属性是', key)
           return val;
         },
        set : function ( newValue ) {
           val = newValue
           console.log('属性' + key + '已经被监听了,此时的值是:' + newValue)
         }
     })
   })
}
var book = {page : 300}
observe(book)
var b = book.page   // 后台会打印 这个属性是page
book.page = 400    // 后台会打印  属性page已经被监听,此时的值是400

在这里我们就实现了属性的监听,上述例子中,我们用Object.defineProperty重写了set和get函数,使得属性的值变化时可以被我们监听到(如果不了解Object.defineProperty的,可以查阅Object.defineProperty

ok,原理我们清楚了,接下来就开始写observe了

//data对象,key和val分别是data的键值对
function defineReactive(data, key, val){
    observe(val)  //递归调用data中的子对象 
    Object.defineProperty(data, key, {
        enumerable : true,  //可枚举,可在for in 和 Object.keys中得到
        configurable : true,
        get : function () {
            return val;
        },
        set : function ( newValue ) {
            if(val === newValue){
                return;
            }
            val = newValue
            console.log('属性' + key + '已经被监听了,此时的值是:' + newValue)
        }
    })
}
//观察者,用来监听数据
function observe (obj) {
    if(!obj || typeof obj !== 'object'){
        return;
    }
    Object.keys(obj).forEach(function(key){
        defineReactive(obj, key, obj[key])
    })
}
  • defineReactive函数的三个参数,分别是要注册的对象,对象的key,以及对象的值
  • defineReactive中调用observe,目的是递归调用所有的属性

3. watcher

observe写完了,接下来我们就要看watcher了,因为每个属性都绑定一个watcher,所以可能会有很多的watcher,因此我们需要一个调度中心(暂时定义为Dep),来统一指挥watcher

Dep的主要功能:

  • 将每个watcher都push进去
  • 当接收到observe的属性更新通知时,通知对应的watcher来更新视图

接下来上代码

//订阅器,用来收集订阅者,并且通知订阅者更新函数
function Dep(){
    this.subs = []
}
Dep.prototype = {
    addSub : function (sub){
        this.subs.push(sub)
    },
    notify : function (){
        this.subs.forEach(function(sub){
            sub.update()
        })
    }
}

Dep已经定义好了,接下来我们需要改一下observe,将dep加进去,这样我们就实现了在get函数中将属性注册一个watcher再push进dep中,并且set函数中数据更新时通dep,dep会再通知watcher去更新视图

function defineReactive(data, key, val){
    observe(val)  //递归调用data中的子对象 
    var dep = new Dep();
    Object.defineProperty(data, key, {
        enumerable : true,  //可枚举,可在for in 和 Object.keys中得到
        configurable : true,
        get : function () {
            // 这里目的是定义一个flag,用来判断什么时候需要push一个sub
            //因为不能每次调用属性都push一个sub,只有在第一次时才需要push
            if(Dep.target){   
                dep.addSub(Dep.target)
            }
            return val;
        },
        set : function ( newValue ) {
            if(val === newValue){
                return;
            }
            val = newValue
            console.log('属性' + key + '已经被监听了,此时的值是:' + newValue)
            dep.notify()
        }
    })
}

到这里Dep调度中心就完成了,接下来我们实现watcher

watcher的主要功能:

  • observe中get函数只是定义了一个watcher,但是触发这个get函数需要在这里,这样就完成了注册
  • 接到dep的更新通知后,调用更新函数

ok,先实现代码:

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 // 这里设置一下target,下一行代码会直接调用属性的get函数,会用到target
        var value = this.vm.data[this.exp]
        Dep.target = null 
        return value
    }
}
  • Watcher的三个参数,分别是vue,要订阅的属性,以及回调函数(触发更新时调用的函数)
  • this.value = this.get()这行代码就是初始化就去获取这个属性值,这样就会调用observe中的get函数,然后将watcher加入到Dep中去。
  • Dep.target = this 就是上文中提到的只有target有值时才会将watcher加入到Dep中

ok,到这里最简易版本的mvvm已经完成了
然后我们定义一个vue:

// data 是所有的属性,el是绑定的元素节点(#app),exp是绑定的属性
function dVue (data, el, exp) {
    this.data = data;
    observe(data);
    el.innerHTML = this.data[exp];  // 初始化模板数据的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}

在html中调用

<body>
    <h1 id="app">{{math}}</h1>
</body>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/index.js"></script>
<script type="text/javascript">
    var ele = document.querySelector('#app');
    var dVue = new dVue({
        math : '1'
    }, ele, 'math');
 
    setInterval(function () {
        dVue.data.math = Math.random() * 100
    }, 1000);
 
</script>

OK,接下来我们完善一下,实现vue中的{{ }}绑定


4. compile

compile主要功能:

  • 获取模板,并且解析模板,将数据替换模板,完成初始化视图
  • 给模板中绑定的属性,new初始化一个watcher(之前是在dVue函数中完成的,现在移到这里)
 function Compile(el, vm){
    this.vm = vm
    this.el = document.querySelector(el)
    this.fragment = null
    this.init()
 }
 Compile.prototype = {
    init : function(){
        if(this.el){
            this.fragment = this.nodeToFragment(this.el)
            this.compileElement(this.fragment)
            this.el.appendChild(this.fragment)
        }else{
            console.log('节点不存在')
        }
    },
    nodeToFragment : function(el){
        //创建一个虚拟的文档片段,用来操作dom节点,因为这个片段是存在于内存中
        //所以相对于直接操作dom,性能会更好一点
        var fragment = document.createDocumentFragment()
        var child = el.firstChild
        while(child){
            fragment.appendChild(child)
            child = el.firstChild
        }

        return fragment
    },
    compileElement :function(el){
        var childNodes = el.childNodes
        var self = this;
        Array.prototype.slice.call(childNodes).forEach(function(node){
            var reg = /\{\{\s*(.*?)\s*\}\}/
            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)
            }
        })
    },
    compileText :function(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)
        })
    },
    updateText : function(node, value){
        node.textContent = typeof value == 'undefined' ? '' : value
    },
    isTextNode : function(node){
        return node.nodeType == 3
    }
 }
  • nodeToFragment是在内存建立一个虚拟的节点,然后将模板赋值给它,再继续操作模板,这样可以提升性能,参考文档nodeToFragment
  • compileElement这个函数,解析模板,找到{{ }}指令的文本节点,然后运行核心函数compileText,解析文本节点
  • compileText这个函数中,做了两件事,第一件事是初始化视图,也就是调用updateText函数,第二件事就是给这个文本节点绑定一个watcher,用于订阅该属性,当属性值改变时,会调用里面的回调函数

到这里compile就完成了,这样我们需要把dVue重新修改一下

function dVue (options) {
    var self = this
    this.data = options.data
    this.vm = this
    // 将data上的属性挂载到vue上,this.data.a === this.a
    Object.keys(this.data).forEach((key)=>{
        this.proxyKeys(key)
    })
    //重写所有的data属性的set和get方法,用于劫持监听数据
    observe(this.data)
    //编译模板,得到绑定的节点,初始化视图,并且给该节点所绑定的属性注册一个watcher
    new Compile(options.el, this)
    return this
}
//代理一下属性,这样的话 dVue.name = dVue.data.name ,不用每次都带着data了
//相当于把data的所有属性都注册到了dVue上
dVue.prototype = {
    proxyKeys : function(key){
        var self = this
        Object.defineProperty(this, key, {
            enumerable : false,
            configurable : true,
            get : function(){
                return self.data[key]
            },
            set :function(val){
                self.data[key] = val
            }
        })
    }
}

到这里就基本结束了,我们在html中调用一下

<html>
<head>
    <title>实现一个简单的mvvm</title>
</head>
<body>
<div id = "app">
    <div>{{name}}</div>
    <div>{{age}}</div>
    <div>{{like}}</div>
</div>

<script type="text/javascript" src="./observe.js"></script>
<script type="text/javascript" src="./watcher.js"></script>
<script type="text/javascript" src="./compile.js"></script>
<script type="text/javascript" src="./dVue.js"></script>
<script>
    let dVueInit = new dVue({
        el : '#app',
        data:{
            name : 'ding',
            age : 14,
            like : '读书'
        }
    })
</script>
</body>
</html>

到此结束!

参考文章:
https://www.cnblogs.com/libin-1/p/6893712.html
https://github.com/canfoo/self-vue/tree/master/v2

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容