手写MVVM框架 之vue双向数据绑定原理剖析
<!DOCTYPE html>
<html> <head>
<meta charset="UTF-8">
<title>自定义MVVM框架,这是比较牛逼的v-text,v-model和数据绑定原理介绍</title>
</head> <body>
<div id="app">
<p v-text="message"></p>
<input type="text" v-model="message"/>
<p>{{message}}</p>
</div>
<script src="observer.js"></script>
<script src="TemepCompiler.js"></script>
<script src="watcher.js"></script>
<script src="mvvm.js"></script>
<script>
var vm = new MVVM({
el: "#app", //挂载视图
data: {
msg:'这是自己写的mvvm框架',
message:'mvvm出来吧'
} //定义数据
})
</script>
</body> </html>
//创建一个MVVM框架类
class MVVM { //类构造器(创造实例模板代码)
constructor(options) { //相当于函数的参数 {el:...,data:...}
//缓存重要属性
this.$vm = this;
this.$el = options.el;
this.$data = options.data; //判断视图是否存在
if(this.$el) {
//添加属性观察对象(实现属性挟持)
new Observer(this.$data);
//创建模板编译器,来解析视图
this.$compiler = new TemepCompiler(this.$el, this.$vm);
}
}
}
class TemepCompiler { //类构造器(创造实例模板代码)
    constructor(el, vm) { //相当于函数的参数 (this.$el,this.$vm)
        //缓存重要属性
        this.vm = vm;
        this.el = this.isElemetNode(el) ? el : document.querySelector(el);
        //判断视图是否存在
        if(this.el) {
            //1.把模板内容放入内存(片段)
            var fragment = this.node2fragment(this.el);
            //2.解析模板
            this.compile(fragment);
            //3.把内存的结果返回页面
            this.el.appendChild(fragment);
        }
    }
    //工具方法
    isElemetNode(node) {
        return node.nodeType === 1 //1.元素节点 2.属性节点 3.文本节点
    }
    isTextNode(node) {
        return node.nodeType === 3 //1.元素节点 2.属性节点 3.文本节点
    }
    toArray(arr) {
        return [].slice.call(arr); //假数组转为数组;
    }
    isDerective(attrName) {
        return attrName.indexOf("v-") >= 0; //判断属性名中是否有v-开头的属性
    }
    //核心方法(节省内存)把模板放入内存,等待解析
    node2fragment(node) {
        //创建内存片段
        var fragment = document.createDocumentFragment(),
            child;
        //模板内容丢到内存
        while(child = node.firstChild) {
            fragment.appendChild(child);
        }
        //返回
        return fragment;
    }
    compile(parentNode) {
        //获取子节点
        var childNodes = parentNode.childNodes, //类数组
            compiler = this;
        //遍历每一个节点
        this.toArray(childNodes).forEach((node) => {
            //判断节点类型
            if(compiler.isElemetNode(node)) {
                //1.属性节点(解析指令)
                compiler.compileElement(node);
            } else {
                //2.文本节点(解析指令)
                var textReg = /\{\{(.+)\}\}/; //(\转义)文本表达式验证规则
                var expr = node.textContent;
                //var expr = node.innerText; //谷歌支持
                //按照规则验证内容
                if(textReg.test(expr)) {
                    expr = RegExp.$1 //缓存最近一次的正则里面的值;
                    //调用方法编译
                    compiler.compileText(node, expr); //如果还有子节点,继续解析;
                }
            }
        });
    }
    //解析元素节点的指令的方法
    compileElement(node) {
        //获取当前元素节点的所有属性
        var arrs = node.attributes;
        self = this;
        //遍历当前元素所有属性
        this.toArray(arrs).forEach(attr => {
            var attrName = attr.name;
            //判断属性是否是指令
            if(self.isDerective(attrName)) {
                var type = attrName.substr(2); //v-text,v-model...
                var expr = attr.value; //指令的值就是表达式
                //找帮手
                CompilerUntils[type](node, self.vm, expr);
            }
        })
    }
    //解析表达式的方法
    compileText(node, expr) {
        CompilerUntils.text(node, this.vm, expr);
    }
}
//解析指令帮手
CompilerUntils = {
    //解析text指令
    text(node, vm, expr) {
        //第一次观察
        //1.找到更新规则对象的更新方法
        var updaterFn = this.updater["textUpdater"];
        //2.执行方法
        updaterFn && updaterFn(node, vm.$data[expr]) //等价if(updaterFn){updaterFn(node,vm.$data[expr])}
        //第n+1次观察
        new Watcher(vm, expr, (newVaule) => {
            //触发订阅时按之前的规则对节点进行更新;v-model的也一样;
            updaterFn && updaterFn(node, newVaule);
        })
    },
    //解析model指令
    model(node, vm, expr) {
        //1.找到更新规则对象的更新方法
        var updaterFn = this.updater["modelUpdater"];
        //2.执行方法
        updaterFn && updaterFn(node, vm.$data[expr]) //等价if(updaterFn){updaterFn(node,vm.$data[expr])}
        //第n+1次观察
        new Watcher(vm, expr, (newVaule) => {
            //触发订阅时按之前的规则对节点进行更新;v-model的也一样;
            updaterFn && updaterFn(node, newVaule);
        })
        //视图到模型变化
        node.addEventListener("input", (e) => {
            var newValue = e.target.value;
            //把值放进数据
            console.log(newValue+'新值');
            vm.$data[expr] = newValue;
        })
    },
    updater: {
        //文本更新方法
        textUpdater(node, value) {
            node.textContent;
            //node.innerText;  //谷歌支持
        },
        //输入框值更新方法
        modelUpdater(node, value) {
            node.value = value;
        }
    }
}
class Observer {
    //构造函数
    constructor(data) {
        //提供一个解析方法,完成属性的分析,和挟持
        this.observe(data);
    }
    observe(data) {
        //判断数据的有效性 必须是对象
        if(!data || typeof data !== "object") {
            return;
        }
        var keys = Object.keys(data); //拿到所有的属性(key)转为数组
        keys.forEach((key) => {
            //重新定义key
            this.defineReactive(data, key, data[key]); //动态属性需要中括号不能.
        })
    }
    //针对当前对象属性的重新定义(挟持)
    defineReactive(obj, key, val) {
        var dep = new Dep();
        //重新定义
        Object.defineProperty(obj, key, {
            enumerable: true, //是否可以遍历
            configurable: false, //是否可以重新配置
            get() { //getter取值
                Dep.target && dep.addSub(Dep.target); //拿到订阅者
                //返回属性
                return val;
            },
            set(newValue) { //setter修改值
                val = newValue; //新值覆盖旧值
                dep.notify(); //通知,触发update操作;
            }
        })
    }
}
//创建发布者
//1.管理订阅者
//2.通知
class Dep {
    constructor() {
        this.subs = [];
    }
    //添加订阅
    addSub(sub) { //其实就是Watcher实例
        this.subs.push(sub);
    }
    //集体通知
    notify() {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}
//定义一个订阅者
class Watcher {
//构造函数
//1.需要使用订阅功能的节点
//2.全局vm对象,用于获取数据
//3.需要使用订阅功能的节点
constructor(vm, expr, cb) {
//缓存重要属性
this.vm = vm;
this.expr = expr;
this.cb = cb; //缓存当前值
this.value = this.get();
} //获取当前值
get() {
//把当前订阅者添加到全局
Dep.target = this; //watcher实例
//获取当前值
var value = this.vm.$data[this.expr];
//清空全局
Dep.target = null;
//返回
return value;
}
//提供一个更新
update() {
//获取新值
var newValue = this.vm.$data[this.expr];
//获取老值
var oldValue = this.value;
//执行回调
if(newValue !== oldValue) {
this.cb(newValue); //效果一样关键需不需要改变this指向this.cb.call(this.vm,newValue)
}
} }
手写MVVM框架 之vue双向数据绑定原理剖析的更多相关文章
- Vue双向数据绑定原理分析(转)
		
add by zhj: 目前组里使用的是前端技术是jQuery + Bootstrap,后端使用的Django,Flask等,模板是在后端渲染的.前后端没有分离,这种做法有几个缺点 1. 模板一般是由 ...
 - vue双向数据绑定原理探究(附demo)
		
昨天被导师叫去研究了一下vue的双向数据绑定原理...本来以为原理的东西都非常高深,没想到vue的双向绑定真的很好理解啊...自己动手写了一个. 传送门 双向绑定的思想 双向数据绑定的思想就是数据层与 ...
 - Vue双向数据绑定原理深度解析
		
首先,什么是双向数据绑定?Vue是三大MVVM框架之一,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化. 在分析其原理和代码的时候,大家首先了解如下几个j ...
 - 前端MVVM框架avalon揭秘 - 双向绑定原理
		
avalon大家可能不熟悉,但是Knockout估计或多或少听过用过,那么说说KO的几个概念 监控属性(Observables)和依赖跟踪(Dependency tracking) 声明式绑定(Dec ...
 - Vue双向数据绑定原理解析
		
基本原理 Vue.采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,数据变动时发布消息给订阅者,触发相应函数的回调 ...
 - Vue 双向数据绑定原理分析 以及 Object.defineproperty语法
		
第三方精简版实现 https://github.com/luobotang/simply-vue Object.defineProperty 学习,打开控制台分别输入以下内容调试结果 userInfo ...
 - vue 双向数据绑定原理
		
博客地址: https://ainyi.com/8 采用defineProperty的两个方法get.set 示例 <!-- 表单 --> <input type="tex ...
 - Vue双向数据绑定原理
		
https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension
 - angular和vue双向数据绑定
		
angular和vue双向数据绑定的原理(重点是vue的双向绑定) 我在整理javascript高级程序设计的笔记的时候看到面向对象设计那章,讲到对象属性分为数据属性和访问器属性,我们平时用的js对象 ...
 
随机推荐
- JSON详解+  C# String.Format格式说明+  C# ListView用法详解 很完整
			
JSON详解 C# String.Format格式说明 C# ListView用法详解 很完整
 - GDUT 积木积水   2*n 时间复杂度
			
题意 Description 现有一堆边长为1的已经放置好的积木,小明(对的,你没看错,的确是陪伴我们成长的那个小明)想知道当下雨天来时会有多少积水.小明又是如此地喜欢二次元,于是他把这个三维的现实问 ...
 - iOS bounds、frame之间的关系
			
这几个都是在ios程序中,经常会注意到的一些小细节,能否真正了解这些,对写ios程序也有很大的好处. frame 是UIView中表示此view的一个矩形面积,包括了view在它的superview中 ...
 - 「NOI2004」「LuoguP1486」郁闷的出纳员
			
Descrption OIER公司是一家大型专业化软件公司,有着数以万计的员工.作为一名出纳员,我的任务之一便是统计每位员工的工资.这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调 ...
 - USACO 回文的路径
			
传送门 这道题和传纸条在某些方面上非常的相似.不过这道题因为我们要求回文的路径,所以我们可以从中间一条大对角线出发去向两边同时进行DP. 这里就有了些小小的问题.在传纸条中,两个路径一定是同时处在同一 ...
 - 内部锁之一:锁介绍(偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景)
			
一.内部锁介绍 上篇文章<Synchronized之二:synchronized的实现原理>中向大家介绍了Synchronized原理及优化锁.现在我们应该知道,Synchronized是 ...
 - UVa11077
			
dp+置换 可以把排列分成几个循环,然后dp统计 dp[i][j]=dp[i-1][j-1]*(i-1)+dp[i-1][j],表示当前有i个元素,至少换j次,然后如果不在自己应该在的位置有i-1种情 ...
 - 2.27 MapReduce Shuffle过程如何在Job中进行设置
			
一.shuffle过程 总的来说: *分区 partitioner *排序 sort *copy (用户无法干涉) 拷贝 *分组 group 可设置 *压缩 compress *combiner ma ...
 - html中target的用法
 - winform 屏蔽 空格键
			
private void call_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)Keys.Space) ...