直播课(1)如何通过数据劫持实现Vue(mvvm)框架
19.6.28更新:
这篇博客比较完善:将每一部分都分装在单独的js文件中:
半个月前看的直播课,现在才自己敲了一遍,罪过罪过
预览:

思路:
简单实现
Vuemvvm的双向数据绑定,需要以下几个步骤:
实现一个入口,把 指令渲染,数据劫持
实现指令渲染,包括层级嵌套的标签,文本
数据劫持
订阅发布
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等等,它们最大的优点就是可以实现数据绑定,再也不需 ...
随机推荐
- Git如何合并Commit
如果你在 push 你的修改之前想要将本地多次修改后的 commit 合并一下变得更好看,可以使用下面的方法. 指定你要合并的 commit 相关的命令有两种 你可以通过指定修改过去的几个 commi ...
- C# FTp 上传,下载
public class FtpHelper { string ftpServerIP; string ftpRemotePath; string ftpUserID; string ftpPassw ...
- k-近邻算法的优缺点及拓展思考
//2019.08.03晚#k-近邻算法的拓展思考与总结1.k-近邻算法是一种非常典型的分类监督学习算法,它可以解决多分类的问题:另外,它的整体思想简单,效果强大.它也可以用来解决回归问题,使用的库函 ...
- LINQ -- 匿名类型
匿名类型注意事项: 匿名类型只能和局部变量配合使用,不能用于成员. 由于匿名类型没有名字,我们必须使用var关键字作为变量类型. 不能设置匿名类型对象的属性.编译器为匿名类型穿件的属性是只读的. 除了 ...
- Jmeter - Linux 下面执行jmeter-server的时候出现:An error occurred: Cannot start. localhost is a loopback address.错误
Jmeter - Linux 下面执行jmeter-server的时候出现:An error occurred: Cannot start. localhost is a loopback addre ...
- S7-300过程映像区详解
一.概念 W过程镜像区输入字 PIW立即输入区字 PIW不用等系统刷新,立即读入 IW等待系统刷新后读入 二.PIW/IW,PQW/QW 引用西门子论坛一位大侠的比方加深理解: ...
- Oracle 修改 提交后 回退
1. -- 查询你执行update 语句之前的数据 精确到什么时间 select * from 表名 as of timestamp to_timestamp('2017-07-21 17:16:38 ...
- liunx mysql 5.7 二进制安装
liunx 5.6版本 本人安装次数不下20次,基本上按照正常的操作流程不会出现什么问题,一切顺利. 今天开发新项目需要按照mysql 5.7 版本.mysql 5.7版本和mysql 5.6版本变化 ...
- uboot源码分析2-启动第二阶段
一.背景知识 1.uboot第二阶段应该做什么? 概括来讲uboot第一阶段主要就是初始化了SoC内部的一些部件(譬如看门狗.时钟),然后初始化DDR并且完成重定位. 由宏观分析来讲,uboot的第二 ...
- 树莓派 Raspberry 软件源更改 看门狗启用
看门狗无法在pi1上执行,似乎后更高级的pi上面才可用 1.替换脚本 下面脚本请直接复制到终端执行!! 适用于raspbian-stretch(基于Debian9) sudo -s echo -e & ...