美文网首页
实现简单的MVVM

实现简单的MVVM

作者: YellowPoint | 来源:发表于2019-06-28 17:52 被阅读0次

MVVM框架的含义

  • M Model 模型层
  • V View 视图层
  • VM VIewModel 视图模型,V与M连接的桥梁
    • html标签是V、data是M、Vue的实例是vm
    • View 通过事件绑定来操作Model
    • Model通过数据绑定来操作View

MVVM框架的核心

  • 模板解析
  • 响应式 (Object.defineProperty)
  • 渲染 (fragment)

MVVM框架的实现

  1. new Observer的时候给每一个值都加上了一个 newDep来做发布者,搜集有用到这个值的订阅者
  2. 先走compiler的模板解析 如 text()方法 new一个watcher,监听数据变化
  3. new watcher的时候就在Dep中加入target=this
  4. 然后this.getVMValue(vm, expr) 就执行了 observer的get ,把Dep.target(也就是当前的监听者watcher)加到订阅者数组中
  5. 然后在watcher里 // 清空Dep.target Dep.target = null 方便后面在添加,(这里应该就相当于一个临时变量吧)
  6. 然后当用 vm.$data.msg=‘xx'’时 触发setter,就执行dep的notify,然后执行watcher的update,就触发了 每个指令或是文本节点解析的回调,就改变了值


    MVVM示意图

代码如下

/src/compiler.js 编译相关
//定义一个类,用于创建vue实例
class Compiler {
    constructor(el, vm) {
        this.el = typeof el === 'string' ? document.querySelector(el) : el
        this.vm = vm
        // 编译模板
        if (this.el) {
            // 1. 把el中所有的子节点都放到内存中,fragment
            let fragment = this.node2fragment(this.el)
            // 2. 在内存中编译
            this.compile(fragment)
            // 3. 把fragment一次性添加到页面
            this.el.appendChild(fragment)
        }
    }

    /* 核心方法 */
    node2fragment(node) {
        let fragment = document.createDocumentFragment()
        let childNodes = node.childNodes
        // console.log(childNodes instanceof Array)
        this.toArray(childNodes).forEach(node => {
            // console.log(node)
            // 把所有子节点都添加到fragment中
            fragment.appendChild(node)
        })
        return fragment
    }
    /*
        编译文档碎片  
    */
    compile(fragment) {
        let childNodes = fragment.childNodes
        this.toArray(childNodes).forEach(node => {
            // 编译节点
            // console.log( JSON.stringify(node)) ==>{} 为啥是空对象
            // console.log(node)
            // 如果是元素,则解析指令
            if (this.isElementNode(node)) {
                this.compileElement(node)
            }
            // 如果是文本节点,则解析插值表达式
            if (this.isTextNode(node)) {
                this.compileText(node)
            }
            // 如果当前节点还有子节点,则递归解析
            if (node.childNodes && node.childNodes.length > 0) {
                this.compile(node)
            }

        })


    }
    // 解析元素节点
    compileElement(node) {
        // console.log('解析元素节点')
        // 1. 获取当前节点下所有属性
        let attributes = node.attributes
        this.toArray(attributes).forEach(attr => {
            // 2. 解析vue指令(所有以v-开始的属性)
            // console.dir(attr)
            let attrName = attr.name

            if (this.isDirective(attrName)) {
                let expr = attr.value
                let type = attrName.slice(2)

                // 解析v-on指令
                if (this.isEventDirective(type)) {
                    CompileUtil['eventHandler'](node, this.vm, type, expr)
                } else {
                    // 解析其他指令
                    CompileUtil[type](node, this.vm, expr)
                }
            }
        })

    }
    // 解析文本节点
    compileText(node) {
        CompileUtil.mustache(node, this.vm)
    }

    /* 工具方法 */
    toArray(likeArray) {
        return [].slice.call(likeArray)
    }
    // nodeType:节点类型 1:元素节点,3:文本节点,很神奇没有2,哦被废弃了
    isElementNode(node) {
        return node.nodeType === 1
    }
    isTextNode(node) {
        return node.nodeType === 3
    }
    // 是否是vue的指令
    isDirective(attrName) {
        return attrName.startsWith('v-')
    }
    // 是否是v-on指令
    isEventDirective(type) {
        return type.split(':')[0] === 'on'
    }
}


let CompileUtil = {
    mustache(node, vm) {
        let txt = node.textContent
        let reg = /\{\{(.+)\}\}/
        if (reg.test(txt)) {
            let expr = RegExp.$1
            node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
            new watcher(vm, expr, newValue => {
                node.textContent = newValue
            })
        }
    },
    text(node, vm, expr) {
        node.textContent = this.getVMValue(vm, expr)

        // 通过watcher对象,监听expr的数据变化
        new watcher(vm, expr, newValue => {
            node.textContent = newValue
        })
    },
    html(node, vm, expr) {
        node.innerHtml = this.getVMValue(vm, expr)
        new watcher(vm, expr, newValue => {
            node.innerHtml = newValue
        })
    },
    model(node, vm, expr) {
        node.value = this.getVMValue(vm, expr)
        let that = this
        // 实现双向的数据绑定,给node注册input事件
        node.addEventListener('input', function () {
            that.setVMValue(vm, expr, this.value)
        })
        new watcher(vm, expr, newValue => {
            node.value = newValue
        })

    },
    eventHandler(node, vm, type, expr) {
        // 给当前元素注册事件
        let eventType = type.split(':')[1]
        // 错误处理
        let fn = vm.$methods && vm.$methods[expr]
        if (eventType && fn) {
            // 将方法的this指向当前实例vm
            node.addEventListener(eventType, fn.bind(vm))
        }

    },
    // 获取VM中的数据
    getVMValue(vm, expr) {
        let data = vm.$data
        expr.split('.').forEach(key => {
            data = data[key]
        })
        return data
    },
    setVMValue(vm, expr, value) {
        let data = vm.$data
        let arr = expr.split('.')
        arr.forEach((key, index) => {
            if (index < arr.length - 1) {
                data = data[key]
            } else {
                data[key] = value
            }

        })
    }
}

/src/observer.js 响应式
/*
 observer用于给data中所有的数据添加getter,setter
 */
class Observer {
  constructor(data) {
    this.data = data
    this.walk(data)
  }

  //核心方法
  // 遍历data中所有的数据,都添加getter和setter
  walk(data) {
    if (!data || typeof data != 'object') {
      return
    }
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
      this.walk(data[key])
    })
  }
  // 定义响应式的数据(数据劫持)
  defineReactive(obj, key, value) {
    let that = this
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      configurable: true,
      enumerable: true,
      get() {
        
        // 如果Dep.target中有watcher对象,则存储到订阅者数组中
        // 注意这里的 Dep和dep
        Dep.target && dep.addSub(Dep.target)
        console.log('获取了',key,Dep.target)
        return value
      },
      set(newValue) {
        if (value === newValue) {
          return
        }
        value = newValue
        dep.notify()
        // 如果newValue是一个对象,则继续劫持
        that.walk(newValue)
      }
    })
  }
}

/src/watcher.js

// watcher模块把compile模块与observe模块关联起来

class watcher {
  //vm:当前vue实例
  // expr:data中的数据
  // 一旦数据发送变化,就调用cb
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb

    // this表示新创建的watcher对象
    // 存储到Dep.target属性上
    Dep.target = this

    // 需要把expr的旧值存起来
    this.oldValue = this.getVMValue(vm, expr)
    // 清空Dep.target
    Dep.target = null
  }
  // 对外暴露的一个方法,用于更新页面
  update() {
    // 对比expr是否发生改变,如果改变则调用cb
    let oldValue = this.oldValue
    let newValue = this.getVMValue(this.vm, this.expr)
    if (oldValue != newValue) {
      this.cb(newValue, oldValue)
    }
  }


  // 获取VM中的数据
  getVMValue(vm, expr) {
    let data = vm.$data
    expr.split('.').forEach(key => {
      data = data[key]
    })
    return data
  }
}

// dep对象用于管理所有的订阅者和通知这些订阅者
class Dep {
  constructor() {
    // 用户管理订阅者
    this.subs = []
  }
  // 添加订阅者
  addSub(watcher) {
    console.log('this.addSub', watcher)
    this.subs.push(watcher)
  }
  // 通知
  notify() {
    console.log('this.subs', this.subs)
    // 遍历所有的订阅者,调用wathcer的uodate方法
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

/src/vue.js
//import Compiler from './compiler.js'

//定义一个类,用于创建vue实例
class Vue {
    constructor(options = {}) {
        this.$el = options.el
        this.$data = options.data
        this.$methods = options.methods


        // 监视data中的数据
        new Observer(this.$data)

        // 把data中所有的数据代理到了vm上
        this.proxy(this.$data)
        // 把methods中所有的方法代理到vm上
        this.proxy(this.$methods)

        if (this.$el) {
            //Compiler负责解析模板的内容
            //需要:模板和数据
            //(为啥不只传一个this,而是要分两个)
            new Compiler(this.$el, this)
        }

    }

    proxy(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return data[key]
                },
                set(newValue) {
                    if (data[key] == newValue) {
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
}

index.html
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
</head>

<body>

    <div id="app">
        <!-- <p>fsafas{{msg}}++{{msg}}</p> -->
        <p>fsafas{{msg}}</p>
        <div v-text="msg"></div>
        <div v-html="tag"></div>

        <p>{{car.brand}}</p>
        <div v-text="car.color"></div>
        <input type="text" v-model="car.color">
        <button v-on:click="clickFn">点我</button>
    </div>

    <script src="./src/watcher.js"></script>
    <script src="./src/observer.js"></script>
    <script src="./src/compiler.js"></script>
    <script src="./src/vue.js"></script>

    <script>
        const vm = new Vue({
            el: '#app',
            data: {
                msg: 'hahah',
                tag: '<h3>哎哟 不错</h3>',
                car: {
                    brand: '特斯拉',
                    color: 'red'
                },
                // $data:"我是data"

            },
            methods: {
                clickFn() {
                    this.$data.msg ="点击了"
                }
            }
        })
        // console.log(vm)
    </script>
</body>

</html>

相关文章

网友评论

      本文标题:实现简单的MVVM

      本文链接:https://www.haomeiwen.com/subject/vjbecctx.html