不到100行的MVVM实现,会js就能看懂!

我认为,每个人都有一个觉醒期,但觉醒的早晚决定个人的命运。。      ---路遥 《平凡的世界》

人都说面试造火箭,工作拧螺丝,其实也能够理解,如果不这样又怎么能证明你的学习能力呢?和相亲是一个道理,第一次相亲,你穿的衣衫褴褛的进去了,应该也就没有下文了吧?哈哈

我最大的特点就是懒,总是不喜欢特别长的文章,这不100行的代码实现了vue的MVVM的双向绑定的原理,基本上要点全部都有注释,并且基本上全是大白话,没有官话和套话,学过js你就能看懂。。。没有学过js的就先把js学了再过来

这版代码简化了一些没必要的操作,比如很多是吧数据代理和数据劫持进行分离的,我直接是同时进行的,跟读小说一样,只要你花10分钟,你就能完全理解了,要是能再写一遍就更好了,不要觉得代码长,其实不到100行,,,不要怕,敢不敢赌一把?

html是这样的。。。没啥好说的

 <div id="app">
        <input type="text" v-model="text">
        {{ text }}
        <button @click="reset" >重置</button>
    </div>
    <script>
        var vm = new saoVm({
            el: 'app',
            data: {
                text: ''
            },
            methods: {
                reset() {
                    this.text = '';
                },
            },
        });
    </script>
复制代码

关键的部分来了,屏息凝神10分钟。。。先大致浏览一下架子结构,再看。。。

class saoVm {
    constructor(options) {
        const {
            el,
            data,
            methods
        } = options;
        this.methods = methods;
        this.target = null;
        // 发布初始化
        this.observe(this, data);
        // 订阅初始化
        this.compile(document.getElementById(el));
    }
    /*
    为啥是Object.defineProperty?
    双向绑定不就是设置值的时候,改变视图,视图改变,值也变化吗?
    那我总得知道什么时候他设置了值,什么时候改变了视图啊?
    这就需要用到这个API的get和set,这点不再做详细介绍
	通过循环data中的属性为data中的每一个属性都通过Object.defineProperty进行定义
	从而重写data的set和get函数来实现的
	*/
    observe(root, data) {
        for (const key in data) {
            this.defineReactive(root, key, data[key]);
        }
    }

    defineReactive(root, key, value) {
        /*
		比如data数据是 data:{foo:{bar:ggg}}
		那我们给foo进行数据劫持的同时,也要对bar做数据劫持啊
		不然bar的改变就不会触发视图改变了
		所以,这里使用递归对value(也就相当于data.foo)的值进行了判断并在此进行数据劫持
		*/
        if (typeof value == 'object') {
            return this.observe(value, value);
        }
        const dep = new Dispatcher();
        /* 事件代理的同时进行事件劫持,这就是为什么第一个参数直接把this传进来
        ,本来是this.data.xxx,后续就可以使用this.xxx来访问了,同时又重写了get和set,一举两得
	*/
        Object.defineProperty(root, key, {
            set(newValue) {
                // 如果值和原来一样就不做处理 直接返回
                if (value == newValue) return;
                value = newValue;
                // 数据改变通知触发视图替换操作,这叫发布
                dep.notify(newValue);
            },
            get() {
                /* add的过程就叫做订阅,这里的this.target是什么?
		在compile阶段中你就会得到答案,往下看
		*/
                dep.add(this.target);
                return value;
            }
        });
    }
    //编译模板-也就是将 {{ msg }} 替换为msg真正的值的过程
    compile(dom) {
        //拿到$el这个dom节点下的所有子节点(注意node节点和元素节点的区别
        const nodes = dom.childNodes;
        //开始遍历所有的节点...(注意:for..in 拿到的是key,for..of拿到的是value)
        for (const node of nodes) {
            //nodeType为1是元素节点,2是属性节点,3是文本节点。
            // 此处属性节点不需要关心,,,可以剔除
            // 元素节点
            if (node.nodeType == 1) {
                // 拿到所有的元素属性,比如v-model,style,@click这些属性
                const attrs = node.attributes;
                for (const attr of attrs) {
                    // 如果这个属性名称是v-model
                    if (attr.name == 'v-model') {
                        const name = attr.value;
                        // 绑定input事件
                        node.addEventListener('input', e = >{
                        /*
        			比如v-model="xxx"
        			下边的目的是this.xxx = 输入的值,改变了data中的数据
        			设置值就会触发发布订阅的发布,继而更新视图
    			*/
                            this[name] = e.target.value;
                        });
                        /*
			还记得上边留的那个坑吗?
			this.target是什么?
			是一个watcher的实例,每一个watcher都有一个
			update方法来执行视图的更新操作,这里把这个watcher赋值给了this之后
			。。。this.target就有值了,这够大白话了吧,没有一点专业术语了,哈哈
			*/
                        this.target = new Watcher(node, 'input');
                        // 这一步纯粹是为了触发该属性的get方法
                        // 从而执行get中的addS方法,来订阅事件
                        this[name];
                    }
                    // 如果是@click就监听为这个node监听事件
                    if (attr.name == '@click') {
                        const name = attr.value;
                        node.addEventListener('click', this.methods[name].bind(this));
                    }
                }
            }
            // text节点--
            if (node.nodeType == 3) {
                // -匹配的就是{{ xxx }} 这段文本
                const reg = /\{\{(.*)\}\}/;
                // node.nodeValue此处就相当于 innerText
                const match = node.nodeValue.match(reg);
                if (match) {
                    // match[1],看上边正则是不是有个括号,match[1]
                    //就是匹配括号中的内容并去两侧空格
                    const name = match[1].trim();
                    // 这不用解释了吧。上边解释过了,在强调下,这里的
                    // 第二个参数是因为,input更新视图改变的是它的value值
                    // 而别的文本节点改变的就是它的nodeValue,
                    //因此这里当时候会通过这个入参进行区分
                    this.target = new Watcher(node, 'text');
                    this[name];
                    // 这一步是直接将 {{ xxx }} 替换为空
                    node.nodeValue = '';
                }
            }
        }
    }
}

class Dispatcher {
    constructor() {
        this.watchers = [];
    }
    add(watcher) {
        this.watchers.push(watcher);
    }
    notify(value) {
        this.watchers.forEach(watcher = >watcher.update(value));
    }
}
// 一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
class Watcher {
    constructor(node, type) {
        this.node = node;
        this.type = type;
    }
    update(value) {
        // 如果是input需要改变value值
        if (this.type == 'input') {
            this.node.value = value;
        }
        // 如果是text需要改变value值
        if (this.type == 'text') {
            this.node.nodeValue = value;
        }
    }
}
复制代码

总结

  1. 一个observe循环data数据绑定getter和setter
  2. 一个compile来解析模板,循环childNodes,解析模板语法以及指令
  3. 一个发布订阅模式,在get中订阅,在set中发布,和订报纸,发报纸一样的道理
  4. 在compile阶段设置this.target,并在此触发get方法,来进行发布监听的操作

都看到这了?小老弟您可真牛x,关注和赞走一个鼓励一下。。。❥(^_-)

觉得对你有帮助,不妨点个

,后续持续输出这种简短有效的文章,帮助你用最短的时间内掌握最多的内容,毕竟谁不喜欢一劳永逸不是? ❥(^_-) thank you ~

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值