MVVM的实现原理

8 篇文章 0 订阅
7 篇文章 0 订阅

1.MVVM是什么?
响应式,双向数据绑定,即MVVM。是指数据层(Model)-视图层(View)-数据视图(ViewModel)的响应式框架。它包括:

1.修改View层,Model对应数据发生变化。

2.Model数据变化,不需要查找DOM,直接更新View。

2.MVVM的实现方式
(1)发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value)。

(2)脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,在指定的事件触发时进入脏值检测。

(3)数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

3.思路
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

4、mvvm入口函数,整合以上三者

如下图:

 

4.实现Observer

写个简单的observer,监听对象的每一个属性的变化

	const data = {
        a: 1,
        b: 2,
        c: {
            d: 4
        }
    }
    observer(data)
    data.c.d = 5
    // 观察者,遍历监听所有数据
    function observer(data) {
        if(!data || typeof data!=='object')return
        for (let key in data) {
            observeProperty(data, key, data[key])
        }
    }
    // 观察属性变化
    function observeProperty(data, key, val) {
        observer(val) // 递归监听子属性
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            get() {
                console.log('you get it')
                return val
            },
            set(newval) {
                console.log('new:' + key, newval)
                console.log('old:' + key, 1)
                val = newval
            }
        })
    }

监听到变化之后,需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ...
// 观察属性变化
    function observeProperty(data, key, val) {
        const dep = new Dep()
        observer(val) // 递归监听子属性
        Object.defineProperty(data, key, {
            // ...
            set(newval) {
                if(val === newval)return
                console.log('new:' + key, newval)
                console.log('old:' + key, 1)
                val = newval
                dep.notify() // 通知所有订阅者
            }
        })
    }
// 订阅器类
    class Dep{
        constructor(){
            this.subs = [] // 存储所有订阅者,也就是Watcher
        }
        addSub(sub){ // 新增订阅者
            this.subs.push(sub)
        }
        notify(){ // 通知所有订阅者,更新数据
            this.subs.forEach(sub=>{
                sub.update()
            })
        }
    }

通过思路图可知,这里的订阅者是指Watcher。

// Observer.js
Object.defineProperty(data, key, {
    // ...
    get() {
        console.log('you get it')
        // 当Dep.target不为空时,添加当前watcher, 添加完移除
        Dep.target && dep.addSub(Dep.target);
        return val
    }
})

// Watcher.js
Watcher.prototype = {
    get(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。

5.实现Compile
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:

 因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中。

class Compile {
        constructor(el, vm) {
            this.$vm = vm
            this.$el = this.isElementNode(el) ? el : document.querySelector(el)
            if (this.$el) {
                // 把模板内容放入内存(片段)
                this.$fragment = this.node2Fragment(this.$el)
                // 解析模板
                this.init()
                // 把内存的结果返回页面
                this.$el.appendChild(this.$fragment)
            }
        }
        init() {
            this.compile(this.$fragment)
        }
        node2Fragment(el) { // 将节点解析成片段
            const fragment = document.createDocumentFragment()
            while (child = el.firstChild) {
                // fragment有个特点,它每appendChild一个节点,该节点在dom上被删除
                fragment.appendChild(child)
            }
            return fragment
        }
        compile(el) {
            const childNodes = el.childNodes;
            [].slice.call(childNodes).forEach((node) => {
                const text = node.textContent
                const reg = /\{\{(.*)\}\}/
                if (this.isElementNode(node)) { //属性节点
                    this.compileElement(node)
                } else if (this.isTextNode(node) && reg.test(text)) { //文本节点
                    this.compileText(node, RegExp.$1)
                }
                // 递归遍历编译子节点
                if (node.childNodes && node.childNodes.length) {
                    this.compile(node)
                }
            })
        }
        compileElement(node) {
            const nodeAttrs = node.attributes;
            [].slice.call(nodeAttrs).forEach(attr => {
                // 规定:指令以 v-xxx 命名
                // 如 <span v-text="content1"></span> 中指令为 v-text
                const attrName = attr.name // v-text
                if (this.isDerective(attrName)) {
                    const exp = attr.value // content1
                    const dir = attrName.substring(2) // text
                    if (this.isEventDirective(dir)) {
                        // 事件指令, 如 v-on:click
                        compileUtil.eventHandler(node, this.$vm, exp, dir)
                    } else {
                        // 普通指令
                        compileUtil[dir] && compileUtil[dir](node, this.$vm, exp)
                    }
                }
            })
        }
        compileText(node, exp) {
            compileUtil.text(node, this.vm, exp)
        }
        isElementNode(node) {
            return node.nodeType === 1
        }
        isTextNode(node) {
            return node.nodeType === 3
        }
        isDerective(attrName) {
            return attrName.indexOf('v-') >= 0
        }
        isEventDirective(attrName) {
            return attrName.indexOf('on') >= 0
        }
    }

至此,视图和数据已经绑定成功,页面的初始化渲染由vm实例的属性初始化完成,并对节点上的事件进行了注册,接下了就是Watcher的事了。它将接受数据的变化,并重新更新视图。

其中,双向绑定由v-model实现,初始化时调用modelUpdater对input中的value初始化赋值,然后添加input事件监听

6.实现Watcher
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

1、在自身实例化时往属性订阅器(dep)里面添加自己

2、自身必须有一个update()方法3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

class Watcher {
        constructor(vm, exp, cb) {
            this.vm = vm
            this.exp = exp
            this.cb = cb
            this.value = this.get() // 缓存当前值(旧值)
        }
        get() {
            Dep.target = this // 将当前订阅者指向自己
            const value = this.vm[this.exp] // 触发getter,添加自己到属性订阅器中
            Dep.target = null // 添加完毕,重置
            return value
        }
        update() {
            this.run()
        }
        run() {
            const value = this.get() // 获取新值
            const oldValue = this.value
            if (value !== oldValue) {
                this.value = value
                this.cb.call(this.vm, value, oldValue) // 执行Compile中绑定的回调,更新视图
            }
        }
    }

// 这里再次列出Observer和Dep,方便理解
    Object.defineProperty(data, key, {
        get() {
            Dep.target && dep.addSub(Dep.target)
            return val
        }
    })
    Dep.prototype = {
        notify() {
            this.sbus.forEach(sub => sub.update())
        }
    }

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

7.实现MVVM
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果
 

class MVVM {
        constructor(options) {
            this.$options = options
            this.$el = options.el
            this.$data = options.data
            // 属性代理,实现 vm.xxx -> vm._data.xxx
            for (let key in this.$data) {
                this.$proxy(key)
            }
            observer(this.$data)
            this.$compile = new Compile(this.$el || document.body, this)
        }
        $proxy(key) {
            Object.defineProperty(this, key, {
                configurable: false,
                enumerable: true,
                get() {
                    return this.$data[key]
                },
                set(newVal) {
                    this.$data[key] = newVal
                }
            })
        }
    }

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MVVM框架的实现原理可以简单概括为以下几个步骤。首先,将视图(View)与数据模型(Model)进行分离,通过ViewModel将两者连接起来。ViewModel层负责将模型的数据绑定到视图上,并监听数据的变化。其次,通过双向数据绑定机制,当模型的数据发生变化时,ViewModel会自动更新视图。当用户对视图进行操作时,ViewModel会将变化传递给模型,从而实现数据的双向同步。 具体实现上,MVVM框架通常会提供数据绑定的机制,用于将视图与ViewModel中的数据进行绑定。在数据绑定过程中,当模型的数据发生变化时,会触发相应的事件通知ViewModel,ViewModel再将变化传递给视图进行更新。同时,MVVM框架还提供了命令绑定机制,用于处理用户在视图上的交互操作。 总结来说,MVVM框架的实现原理是通过将视图与数据模型分离,并通过ViewModel进行双向数据绑定,实现数据的低耦合和高重用性。这种框架能够简化开发过程,提高代码的可维护性和可测试性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [MVVM框架原理浅谈](https://blog.csdn.net/qq_33339479/article/details/103875718)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值