MVVM原理初探索

在这里插入图片描述
不要在翻译插件里跑demo, 会出不来的

html代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="school.name">
        <div>{{school.name}}</div>
        <div>{{school.age}}</div>
        <ul>
            <li>1</li>
            <li>1</li>
        </ul>
    </div>
    <script src="./mvvm.js"></script>
    <script>

        var vm = new Vue({
            el: '#app',
            data: {
                school: {
                    name: '珠峰',
                    age: 10
                }
            }
        })

    </script>
</body>
</html>

mvvm.js

// 观察者模式  发布订阅
class Dep {
    constructor () {
        this.subs = []; // 存放所有watcher
    }
    // 订阅  添加
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 发布
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}
class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm;  // 观察者绑定vm实例
        this.expr = expr;    // 观察者绑定对应表达式 school.name
        this.cb = cb;    // 观察者绑定cb函数,被观察者通知更新时 执行
        this.oldValue = this.get();   // 获取 表达式 school.name旧值
    }
    get() {
        Dep.target = this;  // 挂载(当前表达式school.name)观察者实例到全局Dep.target
        let value = compileUnit.getVal(this.vm, this.expr); // 获取初始化旧值
        Dep.target = null;   // 清空缓存
        return value
    }
    update() { // 数据更新,被观察者通知观察者update
        let newVal = compileUnit.getVal(this.vm, this.expr);   // 重新获取表达式值
        if (newVal !== this.oldValue) {  // 如果数据改变,观察者执行回调
            this.cb(newVal)  
        }
    }
}

/* vm.$watch(vm, 'school.name', (newVal => {
})) */
// 数据劫持
class Observer {
    constructor (data) {
        this.observer(data)
    }
    observer(data) {
        // 判断传入的vm.$data是否是一个对象
        if (data && typeof data == 'object') {
            for (var key in data) {  // 对vm.$data所有属性进行遍历
                this.defineRective(data, key, data[key])  // 进行属性劫持
            }
        }
    }
    defineRective(data, key, value) {
        this.observer(value);  // 子递归 {school: {name: '珠峰'}}, 对所有属性值添加get,set方法
        let dep = new Dep(); // 给每一个属性,添加发布订阅的功能, 声明一个被观察者(被观察数据),一旦属性数据改变,通知观察者
        Object.defineProperty(data, key, { // 重写对象的属性   Object.defineProperty(对象,属性,属性特性)
            get () { // 获取属性值  vm.data.school   vm.data.school.name都要挂载
                // 创建watcher时 会取到对应的实例,并且把watcher放到全局上
                Dep.target && dep.addSub(Dep.target)
                return value  
            },  
            set: (newVal) => { // 设置属性值
                if (newVal != value) { // vm.school.age=18,newVal为设置新值,与原先不同时才进入
                    this.observer(newVal) // 递归赋值的元素,vm.school.age = {a:123}可能赋值的是个对象,里面属性也要劫持
                    value = newVal; // 设置新值
                    dep.notify(); // 数据被重设,通知观察者
                }
            }
        })
    }
}
class Complier {
    constructor (el, vm) {
        // 判断el是不是元素节点
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
        this.vm = vm; 
        // 获取view层el节点内所有元素,放到内存中进行编译
        let fragment = this.node2fragment(this.el);
        // 编译模板,fragment文档节点的数据绑定
        this.compile(fragment);
        // 把内容塞到页面中
        this.el.appendChild(fragment)
    }
    isDirective(attrName) { 
        return attrName.startsWith('v-')
    }
    compileElement (node) {
     //  {type="text", v-model="school.name"}
        let attributes = node.attributes;  
     //   console.log(attributes);
        [...attributes].forEach(attr => { // 遍历节点内所有属性
            let {name, value: expr} = attr; // v-model="school.name", 解构属性名,属性值
            if (this.isDirective(name)) { // 判断当前属性名是否是以'v-'开头的指令
                let [ ,directive] = name.split('-');  // directive 是自定义的指令
                compileUnit[directive](node, expr, this.vm)  //定义compileUnit对象对不同指令数据进行编译, (传入节点,传入表达式, vm实例)对节点进行数据绑定

            }
        })
    }
    compileText(node) {
        let content = node.textContent; 
        if (/\{\{(.+?)\}\}/g.test(content)) { // 如果文本值是插值表达式
            compileUnit['text'](node, content, this.vm)   //content => {{a}} {{b}} 可能多个插值表达式
        }
    }
    compile (node) {
        let childNodes = node.childNodes; //获取fragment第一级子节点,相当于el内的第一级,包括元素节点,与文本节点
        [...childNodes].forEach(child => { // 节点转数组遍历
            if (this.isElementNode(child)) { // 判断当前节点是否是元素节点
                this.compileElement(child)  // 对元素节点进行编译,数据绑定
                this.compile(child)
            } else {  // 当前节点是文本节点
                this.compileText(child)
            }
        })
    }
    isElementNode(node) {
        return node.nodeType === 1 // 元素节点nodeType类型是1
    }
    node2fragment (node) { 
        let fragment = document.createDocumentFragment(); //创建一个文档碎片
        let firstChild;  
        // node.firstChild为el绑定区域的第一个孩子节点,不断获取,赋值给firstChild变量
        while (firstChild = node.firstChild) {
            //appendChild具有移动性,追加到文档碎片中,el绑定区元素就不存在了,相当于不断移动el内第一个孩子节点到fragment
            fragment.appendChild(firstChild)
        }
        // 此时,el区域内没有任何节点
        return fragment 
    }
   
}
// 定义的编译对象, 对不同指令执行不同方法 , model,html
compileUnit = {
    getVal(vm,expr) { // vm.$data,  expr=> school.name
        return expr.split('.').reduce((prev,next) => {
            return prev[next]
        }, vm.$data)
    },
    setVal(vm, expr, value) {
         expr.split('.').reduce((prev,next, index, arr) => { // $data.school.name  = 18
            if (index == arr.length - 1) { // 当循环到最后一项,直接赋值
                prev[next] = value
            }
            return prev[next]
        }, vm.$data)
    },
    model (node,expr,vm) { // node是节点,expr是表达式(school.name),vm是实例
        let fn = this.updater['modelUpdater'];  // 更新函数
        // 声明观察者实例,
        new Watcher(vm, expr, (newVal) => { // 给输入框添加观察者,如果数据更新了,此方法会给输入框重新赋值
            fn(node, newVal)  // 给input节点赋值
        })
        node.addEventListener('input', (e) => { // 给input节点添加input事件,输入时不断获取input值,在vm.$data中重新赋值 
            let value = e.target.value;  
            this.setVal(vm, expr, value)
        })
        let value = this.getVal(vm,expr);
        fn(node, value)  // input节点赋值
    },
    html () {

    },
    getContentValue(vm, expr) {  // 一旦一个插值表达式改变,获取文本节点所有值
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getVal(vm, args[1]);
        })
    },
    text(node,expr,vm) { // expr => {{a}} {{b}}
         let fn = this.updater['textUpdater'];  // 更新函数
         let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { // 检测每个{{}}
            // 给表达式每个{{}} 都加上观察者, 一旦一个改变,渲染所有
             new Watcher(vm, args[1], () => { //args[1],表达式,获取()元组中内容
                fn(node, this.getContentValue(vm, expr)) // 返回一个全的字符串
             })
             // 每一次返回 将插值表达式替换为实际值,{{school.name}} => ‘珠峰‘
            return this.getVal(vm, args[1])  //args[1], 获取()元组中内容
         })
         fn(node,content) // 文本节点赋值
    },
    // 定义更新方法,可实现部分复用
    updater: {
        modelUpdater(node,value) {
            node.value = value
        },
        htmlUpdater() {

        },
        textUpdater(node,value) {
            node.textContent = value
        }
    }
}
class Vue {
    constructor (options) {
        this.$el = options.el;  // 获取view层绑定元素
        this.$data = options.data;  // 获取model数据
        if (this.$el) {  // 如果view层存在,开始数据劫持,和模板编译
            new Observer(this.$data);  // 数据劫持
            new Complier(this.$el, this);  // 模板编译
            // 添加数据代理
            this.proxyVm(this.$data);
        }
    }
    proxyVm(data) {
        for (var key in data) {
            // 获取vm中key值,会指向vm.$data中key值
            Object.defineProperty(this, key, {
                get () {
                    return this.$data[key]
                }
            })
        }
    }
}

主要参考: https://www.bilibili.com/video/BV1o4411T7ib?p=2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值