我是靠谱客的博主 小巧烤鸡,最近开发中收集的这篇文章主要介绍MVVM 原理解析介绍1、小试一下2、运行机制3、源码,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 介绍
  • 1、小试一下
  • 2、运行机制
  • 3、源码
    • 关键点
    • 3.1 html.js
    • 3.2 vue.js
    • 3.3 compile.js

介绍

MVVM可以分解成(Model-View-VIewModel)。ViewModel可以理解为在
presenter基础上的进阶版。

在这里插入图片描述

  • ViewModel通过实现一套数据响应式机制自动响应Model中数据变化。
  • 同时Viewmodel会实现一套更新策略自动将数据变化转换为视图更新。
  • 通过事件监听响应View中用户交互修改Model中数据,这样在ViewModel中就减少了大量DOM操作代码。
  • MVVM在保持View和Model松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性。

  • Vue的双向数据绑定的设计思想为观察者模式。
  • Dep对象:Dependency依赖的简写,包含有三个主要属性id, subs, target和四个主要函数addSub,removeSub, depend, notify,是观察者的依赖集合,负责在数据发生改变时,使用notify()触发保存在subs下的订阅列表,依次更新数据和DOM。
  • Observer对象:即观察者,包含两个主要属性value, dep。做法是使用getter/setter方法覆盖默认的取值和赋值操作,将对象封装为响应式对象,每一次调用时更新依赖列表,更新值时触发订阅者。绑定在对象的_ ob _原型链属性上。

1、小试一下

<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>你好,<span id="name"></span></p>
    </div>
</body>
<script>
    var obj = {}
    // Object.defineProperty() 方法会直接在一个对象上定义一个新属性,
    // 或者修改一个对象的现有属性, 并返回这个对象。
    Object.defineProperty(obj, 'name', {
        get: function () {
            return document.getElementById('name').innerHTML
        },
        set: function (inner) {
            document.getElementById('name').innerHTML = inner
        }
    })

    console.log(obj.name) // null
    obj.name = '张三' // 这里相当于调用了name属性的set方法
    console.log(obj.name) // 张三
</script>
</html>

2、运行机制

在这里插入图片描述

3、源码

关键点

1、实现一个数据监听Observer,对数据对象的所有属性进行监听,数据发生变化可以获取到最新值通知订阅者。

2、实现一个解析器Compile解析页面节点指令,初始化视图。

3、实现一个观察者Watcher,订阅数据变化同时绑定相关更新函数。并且将自己放入观察者集合Dep中。Dep是Observer和Watcher的桥梁,数据改变通知到Dep,然后Dep通知相应的Watcher去更新视图。

3.1 html.js

<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <div v-html="html">html tag</div>
        {{text}}
        <div v-text="text2"></div>
        <input type="text" v-model="text" />
    </div>
</body>

<script src="vue.js"></script>
<script src="compile.js"></script>
<script>
    const obj = new Vue({
        el: '#app',
        data: {
            text: 'this is a text',
            text2: 'this is a v-text content',
            html: '<button  @click="onClick">click</button>'
        },
        methods: {
            onClick() {
                alert('blabla')
            }
        }
    })
</script>
</html>

3.2 vue.js

class Vue {
    // 传入配置选项
    constructor(options) {
        this.$data = options.data // 保存data选项
        this.observe(this.$data) // 执行响应式
        this.$options = options

        this.$compile= new Compile(options.el, this)
    }

    // 监听属性
    observe(value) {
        if (!value || typeof value !== 'object') {
            return
        }
        // 遍历data选项
        Object.keys(value).forEach(key => {
            // 为每一个key定义响应式
            this.defineReactive(value, key, value[key])
            // 为vue的data做属性代理,方便complie中vm[exp]的使用
            this.proxyData(key)
        })
    }

    proxyData(key) {
        Object.defineProperty(this, key, {
            get(){
                return this.$data[key];
            },
            set(newVal){
                this.$data[key] = newVal;
            },
        });
    }

    // 定义响应式
    defineReactive(obj, key, val) {
        // 递归查找嵌套
        this.observe(val)

        // 创建Dep
        const dep = new Dep()

        // 为data对象定义属性
        Object.defineProperty(obj, key, {
            enumerable: true, // 可枚举(可遍历)
            configurable: true, // 可修改或删除
            get() {
                Dep.target && dep.addDep(Dep.target)
                return val
            },
            set(newVal) {
                if (newVal === val) {
                    return; // 新老数据无变化,不处理
                }
                val = newVal
                dep.notify()
            }
        })
    }
}

// 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知
class Dep {
    constructor() {
        this.deps = [] // deps 里面存放的是watcher的实例
    }
    addDep(dep) {
        this.deps.push(dep)
    }
    // 通知所有watcher执行更新
    notify() {
        this.deps.forEach(dep => {
            dep.update()
        })
    }

}

// 具体的更新执行者
class Watcher {
    constructor(vm, key, callback) {
        this.vm = vm
        this.key = key
        this.callback = callback

        // 将来new一个监听器时,将当前watcher实例附加到Dep.target中
        Dep.target = this // 因为是弱语言,所以可以直接对Dep增加属性
        this.vm[this.key] // 这里会执行get, 就执行了Dep.target && dep.addDep(Dep.target)
        Dep.target = null // 避免不必要的重复添加
    }

    // 更新
    update() {
        this.callback.call(this.vm, this.vm[this.key]) // .call: 为了上下文(this)正确
        //通过 .call(),您能够使用属于另一个对象的方法。
        //调用this.vm中的callback方法,并将 this.vm[this.key] 作为参数传入。
    }
}

3.3 compile.js

// 扫描模板中所有依赖,创建更新函数和watcher
class Compile {
    // el是宿主元素或其选择器
    // vm当前Vue实例
    constructor(el, vm) {
        this.$vm = vm
        this.$el = document.querySelector(el) // 默认选择器

        if (this.$el) {
            // 将dom节点转换为Fragment片段,以提高执行效率
            // DocumentFragments: 一次将所有的更改内容插入到DOM树中
            this.$fragment = this.node2Fragment(this.$el)
            // 执行编译
            this.compile(this.$fragment)
            // 将生成的结果追加至宿主元素
            this.$el.appendChild(this.$fragment)
        }
    }

    node2Fragment(el) {
        // 创建一个新的Fragment
        const fragment = document.createDocumentFragment()
        let child
        // 将原生节点拷贝至fragment
        while ((child = el.firstChild)) {
            // 因为child引用的是el.firstChild的内存地址,
            // 将child插入到fragment,就相当于el.firstChild移动到了fragment,
            // 因此下次调用el.firstChild 其实是调用的上次元素的下一个兄弟元素
            fragment.appendChild(child)
        }
        return fragment
    }

    // 编译指定片段
    compile(el) {
        let childNodes = el.childNodes
        // 将一个类数组对象或者可遍历对象转换成一个真正的数组
        Array.from(childNodes).forEach(node => {
            // 判断node类型,做响应处理
            if (this.isElementNode(node)) {
                // 元素节点要识别v-xx或@xx
                this.compileElement(node)
            } else if (this.isTextNode(node) && /{{(.*)}}/.test(node.textContent)) {
                // 文本类型,只关心{{xx}}格式
                this.compileText(node, RegExp.$1) // RegExp.$1: 指的是(.*)里面的内容
            }
            // 遍历可能存在的子节点
            if (node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }

    // 编译元素节点
    compileElement(node) {
        //例:<div v-text="lalala" @click="onclick"></div>
        const attrs = node.attributes
        Array.from(attrs).forEach(attr => {
            const attrName = attr.name // 属性名
            const exp = attr.value // 属性值
            if (this.isDirective(attrName)) { // 指令:v-text
                const dir = attrName.substr(2)
                this[dir] && this[dir](node, this.$vm, exp) // this[dir],这时指的是this.text
            } else if (this.isEventDirective(attrName)) { // 事件:@click
                const dir = attrName.substr(1)
                this.eventHandler(node, this.$vm, exp, dir)
            }
        })
    }

    // 编译文本节点
    compileText(node, exp) {
        this.text(node, this.$vm, exp)
    }

    // 判断是否是元素节点
    isElementNode(node) {
        return node.nodeType === 1 // 元素节点
    }

    // 判断是否是文本节点
    isTextNode(node) {
        return node.nodeType === 3 // 文本节点
    }

    isDirective(attr) {
        return attr.indexOf("v-") === 0 // 判断是否由'v-'开头,是则就是指令
    }

    isEventDirective(attr) {
        return attr.indexOf("@") === 0
    }

    // 文本更新
    text(node, vm, exp) {
        this.update(node, vm, exp, 'text')
    }

    // 处理html
    html(node, vm, exp) {
        this.update(node, vm, exp, 'html')
    }

    // 处理双向绑定
    model(node, vm, exp) {
        this.update(node, vm, exp, 'model')

        const val = vm.exp
        // 双向绑定还要处理视图对模型的更新
        node.addEventListener('input', e => {
            vm[exp] = e.target.value
            // val = e.target.value
        })
    }

    //更新
    update(node, vm, exp, dir) {
        let updatorFn = this[dir+'Updator']
        updatorFn && updatorFn(node, vm[exp]) // 立刻更新一次,相当于初始化更新
        new Watcher(vm, exp, function(value) {
            updatorFn && updatorFn(node, value);
        })
    }

    textUpdator(node, value) {
        node.textContent = value
    }

    htmlUpdator(node, value) {
        node.innerHTML = value
    }

    modelUpdator(node, value) {
        node.value = value
    }

    eventHandler(node, vm, exp, dir) {
        // 拿到 vue中methods对应的函数
        let fn = vm.$options.methods && vm.$options.methods[exp] // exp: 为@οnclick="xx"中的xx
        if (dir && fn) {
            node.addEventListener(dir, fn.bind(vm)) // fn.bind(vm):绑定实例(上下文),类似:fn.bind(this)
        }
    }
}

最后

以上就是小巧烤鸡为你收集整理的MVVM 原理解析介绍1、小试一下2、运行机制3、源码的全部内容,希望文章能够帮你解决MVVM 原理解析介绍1、小试一下2、运行机制3、源码所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部