直播课(1)如何通过数据劫持实现Vue(mvvm)框架
19.6.28更新:
这篇博客比较完善:将每一部分都分装在单独的js文件中:
半个月前看的直播课,现在才自己敲了一遍,罪过罪过
预览:
思路:
简单实现
Vue
mvvm的双向数据绑定,需要以下几个步骤:
实现一个入口,把 指令渲染,数据劫持
实现指令渲染,包括层级嵌套的标签,文本
数据劫持
订阅发布
1.实现一个入口文件
let vm = new Kvue({
el: "#app",
data: {
message: "测试数据",
options: "123",
name: "张三"
}
})
2.替换{{}}
中的数据
class Kvue {
constructor(options) {
// 将传入的数据挂载到 Kvue 上
this.$options = options
this._data = options.data // 编译 {{}},此时需要把编译的范围当做入参
this.compile(options.el)
} // 模板替换
compile(el) {
// 获取挂载点
let element = document.querySelector(el)
this.compileNode(element)
} // 递归节点
compileNode(element) {
// 获取 childNodes
let childNodes = element.childNodes
// 将 childNodes 转换为 真正的数组
Array.from(childNodes).forEach(node => {
// 文本节点 nodeType = 3
if(node.nodeType == 3) {
// console.log(node)
// 获取节点内容
let nodeContent = node.textContent
// 使用正则匹配{{}},去除其中的空格
let reg = /\{\{\s*(\S*)\s*\}\}/
if(reg.test(nodeContent)) {
// console.log(RegExp.$1)
node.textContent = this._data[RegExp.$1]
}
} else if (node.nodeType == 1) {
// 标签节点
let attrs = node.attributes
// console.log(attrs)
// 遍历标签节点
Array.from(attrs).forEach(attr => {
// 获取标签的属性
let attrName = attr.name
// 获取标签的值
let attrValue = attr.value
// console.log(attrValue)
// 匹配是否是 k- 开头的指令
if(attrName.indexOf('k-') == 0) {
// 获取 k- 后面的部分,
attrName = attrName.substr(2)
// console.log(attrName)
// 目的是防止用户自定义 k-holle 的属性
if(attrName == "model") {
// 将 data 中的对应值赋给此节点
node.value = this._data[attrValue]
}
// 监听 input 变化
node.addEventListener('input', e => {
console.log(e.target.value)
this._data[attrValue] = e.target.value
})
}
})
}
// 递归判断是否有子节点
if(node.childNodes.length > 0) {
this.compileNode(node)
}
})
}
}
3.数据劫持
认识 defineProperty()
// let obj = {name: "张三"}
// console.log(obj);
// obj.name = "李四" // 数据劫持
let obj = Object.defineProperty({}, "name", {
configurable: true, // 可配置
enumerable: true, // 枚举
get() {
console.log("get");
return "张三" // 必须 return
},
set(newValue) {
console.log("set", newValue);
}
})
console.log(obj);
实现数据劫持
// 数据劫持
observer(data) {
Object.keys(data).forEach(key => {
let value = data[key]
Object.defineProperty(data, key, {
configurable: true,
enumrable: true,
get() {
return value
},
set(newValue) {
// console.log("set", newValue)
value = newValue
}
})
})
}
现在实现了数据劫持,那么数据变化,就需要通知 observer
去更新视图,这时就需要一个订阅发布模式
4.订阅发布,视图更新
订阅发布模式:
demo:
老王给孩子或者邻居通过电话讲故事,但是有时候电话没人接,老王需要重新打一次。这时就想到了发布订阅模式:老王将讲的故事录成视频,存到网上,然后孩子和邻居注册报备一下,老王知道谁订阅了他的故事,然后老王群发一个消息,让他们自己去看
// 发布订阅模式
// 老王,订阅收集器
class Dep {
constructor() {
// 把 孩子 邻居 放在一个容器中存起来
this.subs = []
} // 注册报备
addSub(sub) {
this.subs.push(sub)
} // 发布视频,通知 孩子 邻居 更新
notify() {
this.subs.forEach(v => {
v.update();
})
}
} // 订阅者 孩子,邻居
class Watcher {
constructor() { }
//
update() {
console.log('更新了');
}
} // 实力化 老王
let dep = new Dep() // 孩子 邻居
let watcher1 = new Watcher()
let watcher2 = new Watcher()
let watcher3 = new Watcher() // 孩子 邻居 注册报备
dep.addSub(watcher1)
dep.addSub(watcher2)
dep.addSub(watcher3) // 发布视频
dep.notify()
MVVM实现订阅发布
在数据劫持结合订阅发布模式实现视图更新(难点)
// 发布订阅模式
class Dep {
constructor() {
this.subs = []
} addSub(sub) {
this.subs.push(sub)
} notify(newValue) {
this.subs.forEach(v => {
// console.log(newValue)
v.update(newValue);
})
}
} class Watcher {
constructor(vm, exp, cb) {
// 在更新时,实例化,在什么位置加呢?在调取数据时添加Watcher,但是在加的时候先声明处订阅收集器——老王 —— get() {}
// 防止重复添加
Dep.target = this
// 触发 get 方法
vm._data[exp]
// 改变视图的回调
this.cb = cb
// 防止重复添加
Dep.target = null
}
update(newValue) {
console.log('更新了', newValue)
// 改变视图
this.cb(newValue)
}
}
总结
简单实现vue的双向绑定,没有涉及复杂的对象
代码冗余,没有抽离
Kvue 类太复杂,没有把 数据劫持,订阅发布,代码编译 抽离成单独的 js 文件
未完待续。。。
全部代码
index.html
<head>
<meta charset="UTF-8">
<title>如何通过数据劫持实现Vue(mvvm)框架</title>
<script src="./kvue.js"></script>
</head> <body>
<div id="app">
{{message}}
<p>{{message}}</p>
<hr>
<input type="text" k-model="name">
{{name}}
</div>
<script>
let vm = new Kvue({
el: '#app',
data: {
message: '测试数据',
name: '张三'
}
})
// 模拟数据改变,实现视图更新
setTimeout(() => {
vm._data.message = "修改的值"
}, 2000)
// vm._data.message = "修改的值"
// vm._data.name = "ls"
// vm.message
// vm.options
</script>
</body>
kvue.js
class Kvue {
constructor(options) {
// 将传入的数据挂载到 Kvue 上
this.$options = options
this._data = options.data // 劫持数据 defineProperty()
this.observer(this._data) // 编译 {{}},此时需要把编译的范围当做入参
this.compile(options.el)
} // 数据劫持
observer(data) {
Object.keys(data).forEach(key => {
let value = data[key]
// 订阅收集器
let dep = new Dep()
// 数据劫持
Object.defineProperty(data, key, {
configurable: true, // 可配置
enumrable: true, // 枚举
// get 需要触发
get() {
// 如果 Dep 中有 target,添加addSub()
if(Dep.target) {
dep.addSub(Dep.target)
}
return value // 必须 return
},
set(newValue) {
// console.log("set", newValue)
if(newValue !== value)
value = newValue
// 当改变时 通知 update(),更新UI视图
dep.notify(newValue)
}
})
})
} // 模板替换
compile(el) {
// 获取挂载点
let element = document.querySelector(el)
this.compileNode(element)
} // 递归节点
compileNode(element) {
// 获取 childNodes
let childNodes = element.childNodes
// 将 childNodes 转换为 真正的数组
Array.from(childNodes).forEach(node => {
// 文本节点 nodeType = 3
if(node.nodeType == 3) {
// console.log(node)
// 获取节点内容
let nodeContent = node.textContent
// 使用正则匹配{{}},去除其中的空格
let reg = /\{\{\s*(\S*)\s*\}\}/
if(reg.test(nodeContent)) {
// console.log(RegExp.$1)
node.textContent = this._data[RegExp.$1]
// 初次渲染 实例化 Watcher,并且防止递归过程中重复添加
// 将 this 传进来,目的是传 this 下的 data, 还有 下标 cb 是回调,作用是更新视图,不建议在 订阅发布中更新视图
new Watcher(this, RegExp.$1, newValue => {
// 更新视图
// console.log(newValue)
node.textContent = newValue
})
}
} else if (node.nodeType == 1) {
// 标签节点
let attrs = node.attributes
// console.log(attrs)
// 遍历标签节点
Array.from(attrs).forEach(attr => {
// 获取标签的属性
let attrName = attr.name
// 获取标签的值
let attrValue = attr.value
// console.log(attrValue)
// 匹配是否是 k- 开头的指令
if(attrName.indexOf('k-') == 0) {
// 获取 k- 后面的部分,
attrName = attrName.substr(2)
// console.log(attrName)
// 目的是防止用户自定义 k-holle 的属性
if(attrName == "model") {
// 将 data 中的对应值赋给此节点
node.value = this._data[attrValue]
}
// 监听 input 变化
node.addEventListener('input', e => {
this._data[attrValue] = e.target.value
})
// 注册
new Watcher(this, attrValue, newValue => {
node.value = newValue
})
}
})
}
// 递归判断是否有子节点
if(node.childNodes.length > 0) {
this.compileNode(node)
}
})
}
} // 发布订阅模式
class Dep {
constructor() {
this.subs = []
} addSub(sub) {
this.subs.push(sub)
} notify(newValue) {
this.subs.forEach(v => {
// console.log(newValue)
v.update(newValue);
})
}
} class Watcher {
constructor(vm, exp, cb) {
// 在更新时,实例化,在什么位置加呢?在调取数据时添加Watcher,但是在加的时候先声明处订阅收集器——老王 —— get() {}
// 防止重复添加
Dep.target = this
// 触发 get 方法
vm._data[exp]
// 改变视图的回调
this.cb = cb
// 防止重复添加
Dep.target = null
}
update(newValue) {
console.log('更新了', newValue)
// 改变视图
this.cb(newValue)
}
}
直播课(1)如何通过数据劫持实现Vue(mvvm)框架的更多相关文章
- 对数据劫持 OR 数据代理 的研究------------引用
数据劫持,也叫数据代理. 所谓数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果.比较典型的是 Object.defineProperty() 和 ...
- 《数据持久化与鸿蒙的分布式数据管理能力》直播课答疑和PPT分享
问:hi3861开发板支持分布式数据库吗? 目前,分布式数据库仅支持Java接口,因此Hi3861没有现成的API用于操作分布式数据库. 问:分布式数据管理包括搜索吗? 分布式数据管理包括融合搜索能力 ...
- .4-Vue源码之数据劫持(2)
开播了开播了! vue通过数据劫持来达到监听和操作DOM更新,上一节简述了数组变化是如何监听的,这一节先讲讲对象属性是如何劫持的. // Line-855 Observer.prototype.wal ...
- Vue之九数据劫持实现MVVM的数据双向绑定
vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的. 如果不熟悉defineProperty,猛 ...
- Vue 核心之数据劫持
前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Angular.Regular.Vue.React等等,它们最大的优点就是可以实现数据绑定,再也不需要手动进行DOM操作了,它们实现 ...
- vue双向绑定(数据劫持+发布者-订阅者模式)
参考文献:https://www.cnblogs.com/libin-1/p/6893712.html 实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据. 关键点在于data如何更新v ...
- 免费在线直播课,送给所有IT项目经理
[免费在线直播课,送给所有IT项目经理]项目管理培训领域的老资格——光环国际,精心策划了一门一个半小时的在线直播课,送给所有辛苦的IT项目经理们.[直播主题]变化时代IT项目经理的成长要求[直播内容 ...
- php特级课---5、网络数据转发原理
php特级课---5.网络数据转发原理 一.总结 一句话总结: OSI七层模型 路由器 交换机 ARP 代理ARP 1.OSI7层模型? 电缆 MAC地址 ip 端口 应用 1层 通信电缆 2层 原M ...
- Vue框架核心之数据劫持
本文来自网易云社区. 前瞻 当前前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Angular.Regular.Vue.React等等,它们最大的优点就是可以实现数据绑定,再也不需 ...
随机推荐
- 1-2SpringBoot项目属性配置
前面我们讲解了SpringBoot HelloWorld实现 今天具体来讲解上那个application.properties项目配置文件 打开是空白 里面可以配置项目,所以配置项目我们 alt+/ ...
- pgsql 查询jsonb中包含某个键值对的表记录
pgsql 查询jsonb中包含某个键值对的表记录 表名 table_name ,字段 combos 类型为 jsonb 可为空,示例内容如下, $arr_combos = [ ['id' => ...
- Lesson 44 Patterns of culture
What influences us from the moment of birth? Custom has not commonly been regarded as a subject of a ...
- android中的简单animation(二)push up,push left,cross fade,hyperspace
animation_2.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout x ...
- 118.django中表单的使用方式
表单 HTML中的表单: 从前端来说,表单就是用来将数据提交给服务器的,不管后台使用的是django还是php等其他的语言.只要把input标签放在form标签中,然后再添加一个提交的按钮,就可以将i ...
- Oracle--sqlplus--常用命令
登陆:win+R输入sqlplus即可 如果前期没有用户可以输入sqlplus /nolog 记得sqlplus后有一个空格 --格式化命令 进行数据查询时,默认的方式排版会很乱,如果我们要解决这个 ...
- Django(五)1 - 4章实战:从数据库读取图书列表并渲染出来、通过url传参urls.py path,re_path通过url传参设置、模板语法
一.从数据库读取图书数据并渲染出来 1)app1/views.py函数books编写 [1]从模型下导入bookinfo信息 [2]从数据库获取图书对象列表 [3]把获取到的图书对象赋值给books键 ...
- Java For 循环
章节 Java 基础 Java 简介 Java 环境搭建 Java 基本语法 Java 注释 Java 变量 Java 数据类型 Java 字符串 Java 类型转换 Java 运算符 Java 字符 ...
- 解决物理机U盘安装Kali Linux2018.1,光驱无法加载问题
1.无效的方法: (1)执行 df -m,然后查看U盘设备是否挂载到了/media,导致cd-rom不能被挂载,执行 umount /media. (2)在光驱加载安装界面,把U盘拔下换到电脑的另外 ...
- Ubuntu安装Python版本管理工具pyenv
gyf@gyf-VirtualBox:~$ git clone https://github.com/yyuu/pyenv.git ~/.pyenvCloning into '/home/gyf/.p ...