手写MVVM

let vm = new MVVM({
            el:'#app',
            data:{
                message:{
                    a: 'hello zfpx',
                },
                a:1
            }
        })
复制代码

vue中初始化一个vue实例是这样用的,那么MVVM内部原理究竟是怎么实现的呢?

MVVM实现图示如下

如图主要分为5部分:MVVM类,Compile, Observer,Dep,Watcher

1. MVVM类实现

MVVM类主要功能:

  1. 存挂载的dom元素以及data
  2. 代理$data上的属性到this实例上
  3. 调用Observer类数据劫持
  4. 将挂载的东西传入Compile类,编译模板
class MVVM{
    constructor(options){
        // 先把可用的东西挂载在实例上
        this.$el = options.el;
        this.$data = options.data;

        // 如果有要编译的模板,就开始编译
        if(this.$el){
            // 数据劫持 就是把对想的所有属性 改成get和set方法
            new Observer(this.$data); 
            //将$data代理到this上
            this.proxyData(this.$data);
            // 用数据和元素进行编译
            new Compile(this.$el, this);
        }
    }
    proxyData(data){
        Object.keys(data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return data[key]
                },
                set(newValue){
                    data[key] = newValue
                }
            })
        })
    }
}

复制代码

2. Observer类实现 Observer类主要功能:

  1. 将vue中data的每个属性都改成劫持形式(set和get的形式),深度递归data对象的所有属性
  2. 每个属性都对应一个数组(Dep类实现的方法),这个数组存放所有更新的操作
class Observer{
    constructor(data){
       this.observe(data);
    }
    observe(data){
        // 要对这个data数据将原有的属性改成set和get的形式
        if(!data || typeof data !== 'object'){
            return;
        }
        // 要将数据 一一劫持 先获取取到data的key和value
        Object.keys(data).forEach(key=>{
            // 劫持
            this.defineReactive(data,key,data[key]);
            this.observe(data[key]);// 深度递归劫持
        });
    }
    // 定义响应式
    defineReactive(obj,key,value){
        let that = this;
        // 每个属性 都会对应一个数组,这个数组是存放所有更新的操作
        let dep = new Dep(); 
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){ 
    // 当取值时,将Dep.target(watcher即set属性时,要执行的回调函数)推入数组
                Dep.target && dep.addSub(Dep.target); 
                return value;
            },
            set(newValue){ 
               // 当给data属性中设置值的时候 更改获取的属性的值
                if(newValue!=value){
                    // 这里的this不是实例
                    // 如果是对象继续劫持 
                    // 比如你this.$data = {a: '2'}换成了一个新对象
                    that.observe(newValue);
                    value = newValue;
                    dep.notify(); // 通知所有watcher数据更新了
                }
            }
        });
    }
}
复制代码

3. Dep类实现 Dep类的主要功能

  1. 订阅,发布更新通知
class Dep{
    constructor(){
        // 订阅的数组
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher);
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}
复制代码

4.watcher类实现

watcher功能

  1. 给每一个属性添加一个观察者,当属性变化时,执行对应的更新操作
class Watcher{
// vm: vm实例 expr: {{message.a}}中的message.a cb:属性更新后的回调函数
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先获取一下老的值,进入get方法,将watcher实例赋给Dep.target
        //然后 执行this.getval()方法时,会读取data对象上的属性
        // 一旦读取属性(数据劫持)
        // 就会执行Observer类中的defineReactive方法
        // 执行这里:Dep.target && dep.addSub(Dep.target); 
        //将watcher实例推入订阅的数组
        this.value = this.get(); // 读取老值 
    }
     // 获取data对象上对应的expr属性值
    getVal(vm, expr) {
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    }
    get(){
        Dep.target = this;
        let value = this.getVal(this.vm,this.expr);
        Dep.target = null;
        return value;
    }
    // 对外暴露的方法
    //一旦给data对象的属性设置值,就会执行Observer类defineReactive方法中的
    // dep.notify()--》执行订阅的数组中的watcher队列的update方法
    update(){
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if(newValue != oldValue){
            this.cb(newValue); // 对应watch的callback
        }
    }
}
// 用新值和老值进行比对 如果放生变化 就调用更新方法
复制代码

5. Compile类实现 Compile类功能

  1. 把真实dom移入内存 fragment
  2. 编译:结合模板和数据进行编译,主要处理v-*, {{}},等其他vue特有的东西
  3. 把编译好的fragment塞回到页面里去
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        if (this.el) {
            // 如果这个元素能获取到 我们才开始编译
            // 1.先把这些真实的DOM移入到内存中 fragment
            let fragment = this.node2fragment(this.el);
            // 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
            this.compile(fragment);
            // 3.把编译好的fragment在塞回到页面里去
            this.el.appendChild(fragment);
        }
    }
    /* 专门写一些辅助的方法 */
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // 是不是指令
    isDirective(name) {
        return name.includes('v-');
    }
    /* 核心的方法 */
    compileElement(node) {
        // 带v-model v-text
        let attrs = node.attributes; // 取出当前节点的属性
        Array.from(attrs).forEach(attr => {
            // 判断属性名字是不是包含v-model
            let attrName = attr.name;
            if (this.isDirective(attrName)) {
                // 取到对应的值放到节点中
                let expr = attr.value;
                let [, type] = attrName.split('-');
                // node this.vm.$data expr
                CompileUtil[type](node, this.vm, expr);
            }
        })
    }
    compileText(node) {
        // {{}}的处理
        let expr = node.textContent; // 取文本中的内容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
        if (reg.test(expr)) {
            // node this.vm.$data text
            CompileUtil['text'](node, this.vm, expr);
        }
    }
    compile(fragment) {
        // 递归
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                // 是元素节点,还需要继续深入的检查
                // 这里需要编译元素
                this.compileElement(node);
                this.compile(node)
            } else {
                // 文本节点
                // 这里需要编译文本
                this.compileText(node);
            }
        });
    }
    node2fragment(el) { 
        // 需要将el中的内容全部放到内存中
        // 文档碎片 内存中的dom节点
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment; // 内存中的节点
    }
}

CompileUtil = {
    getVal(vm, expr) { // 获取实例上对应的数据
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    },
    getTextVal(vm, expr) { 
    // 获取编译文本后的结果 {{message.a}}--> message.a-->data.message.a
        return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
            return this.getVal(vm, arguments[1]);
        })
    },
    text(node, vm, expr) { // 文本处理
        let updateFn = this.updater['textUpdater'];
        // {{message.a}} => hello,zfpx;
        let value = this.getTextVal(vm, expr);
        // {{a}} {{b}}
        expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
        //每个属性都对应一个watcher
            new Watcher(vm, arguments[1],(newValue)=>{
                // 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
                updateFn && updateFn(node,this.getTextVal(vm,expr));
            });
        })
        updateFn && updateFn(node, value)
    },
    setVal(vm,expr,value){ // [message,a]
        expr = expr.split('.');
        // 收敛
        return expr.reduce((prev,next,currentIndex)=>{
            if(currentIndex === expr.length-1){
                return prev[next] = value;
            }
            return prev[next];
        },vm.$data);
    },
    model(node, vm, expr) { // 输入框处理
        let updateFn = this.updater['modelUpdater'];
        // 这里应该加一个监控 数据变化了 应该调用这个watch的callback
        new Watcher(vm,expr,(newValue)=>{
            // 当值变化后会调用cb 将新的值传递过来 ()
            updateFn && updateFn(node, this.getVal(vm, expr));
        });
        node.addEventListener('input',(e)=>{
            let newValue = e.target.value;
            this.setVal(vm,expr,newValue)
        })
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value
        },
        // 输入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}
复制代码

转载于:https://juejin.im/post/5c3754125188252410606c79

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
课程介绍本套课程,大喵将带着大家手把手,编辑每一行代码,使用原生Javascipt及ES6的一些新特性,仿照目前国内非常流行的Vue框架的源码,开发构建一个属于你们自己的MVVM框架。让大家能够很清晰的学习及掌握,模板编译,数据绑定,响应式,文档碎片,观察者模式,发布订阅模式等等,这些Vue的核心知识点,都是怎么回事,它们之间有哪些关联,是如何进行双向数据绑定的等等。在使用vue2.x的核心Object.defineProperty构建完成框架后,大喵也会使用Vue3.0引入的数据劫持的Proxy新特性,来改造我们的MVVM框架,提升我们的框架性能。最终,大喵会手把手带着大家,从0开始配置webpack.config.js配置文件以及引入babel-loader,配置.babelrc文件,转换我们JS文件中的的ES6语法,最终压缩输出我们的MVVM框架库文件。课程目录01 手把手搭建MVVM框架 课程介绍、02 文件结构及基础类创建、03 入口类DamiaoMvvm的实现、04 模板编译、05 元素节点解析编译、06 v-model 数据绑定逻辑梳理、07 v-model 数据绑定实现、08 模板文本编译逻辑梳理、09 模板文本渲染绑定、10 观察者Wather逻辑梳理、11 Wather 逻辑实现、12 Object.defineProperty()、13 defineProperty 小案例、14 observer数据劫持梳理、15 defineProperty数据劫持实现、16 发布订阅类实现、17 Proxy 介绍与概述、18 使用Proxy改造MVVM框架、19 使用Proxy实现响应式、20 webpack 打包配置、21 babel 配置及文件输出、22 Mvvm Proxy 框架打包、23 Mvvm 框架搭建课程总结MVVM框架介绍MVVM是 Model-View-ViewModel 的缩写。mvvm是一种设计思想。Model 层代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来,ViewModel 是一个同步View 和 Model的对象。在MVVM架构模式下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值