19.6.28更新:

这篇博客比较完善:将每一部分都分装在单独的js文件中:

剖析Vue原理&实现双向绑定MVVM

半个月前看的直播课,现在才自己敲了一遍,罪过罪过

预览:

思路:

简单实现Vuemvvm的双向数据绑定,需要以下几个步骤:

  1. 实现一个入口,把 指令渲染,数据劫持

  2. 实现指令渲染,包括层级嵌套的标签,文本

  3. 数据劫持

  4. 订阅发布

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)框架的更多相关文章

  1. 对数据劫持 OR 数据代理 的研究------------引用

    数据劫持,也叫数据代理. 所谓数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果.比较典型的是 Object.defineProperty() 和 ...

  2. 《数据持久化与鸿蒙的分布式数据管理能力》直播课答疑和PPT分享

    问:hi3861开发板支持分布式数据库吗? 目前,分布式数据库仅支持Java接口,因此Hi3861没有现成的API用于操作分布式数据库. 问:分布式数据管理包括搜索吗? 分布式数据管理包括融合搜索能力 ...

  3. .4-Vue源码之数据劫持(2)

    开播了开播了! vue通过数据劫持来达到监听和操作DOM更新,上一节简述了数组变化是如何监听的,这一节先讲讲对象属性是如何劫持的. // Line-855 Observer.prototype.wal ...

  4. Vue之九数据劫持实现MVVM的数据双向绑定

    vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的. 如果不熟悉defineProperty,猛 ...

  5. Vue 核心之数据劫持

    前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Angular.Regular.Vue.React等等,它们最大的优点就是可以实现数据绑定,再也不需要手动进行DOM操作了,它们实现 ...

  6. vue双向绑定(数据劫持+发布者-订阅者模式)

    参考文献:https://www.cnblogs.com/libin-1/p/6893712.html 实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据. 关键点在于data如何更新v ...

  7. 免费在线直播课,送给所有IT项目经理

     [免费在线直播课,送给所有IT项目经理]项目管理培训领域的老资格——光环国际,精心策划了一门一个半小时的在线直播课,送给所有辛苦的IT项目经理们.[直播主题]变化时代IT项目经理的成长要求[直播内容 ...

  8. php特级课---5、网络数据转发原理

    php特级课---5.网络数据转发原理 一.总结 一句话总结: OSI七层模型 路由器 交换机 ARP 代理ARP 1.OSI7层模型? 电缆 MAC地址 ip 端口 应用 1层 通信电缆 2层 原M ...

  9. Vue框架核心之数据劫持

    本文来自网易云社区. 前瞻 当前前端界空前繁荣,各种框架横空出世,包括各类mvvm框架横行霸道,比如Angular.Regular.Vue.React等等,它们最大的优点就是可以实现数据绑定,再也不需 ...

随机推荐

  1. 六种方式实现hibernate查询,及IDE推荐

      这些天过的好乱,也许是因为考完试了,心里有些松懈吧.也许是最近发生的事对我有些触动吧.感觉自己都已经不懂自己了.面对一些人的教导,我很感激.因为很多话都对我有非常大的帮助和启发,也让我除了做技术, ...

  2. Eclipse创建一个动态maven项目详细步骤

    新建maven项目,new一个Dynamic Web Project 项目 输入完项目名直接finish 配maven,右键项目configure,选择Convert to Plug-in Proje ...

  3. 设备树DTS 学习:Linux DTS文件加载过程

    背景 了解机制有利于对内核有更深的认识. wget https://mirrors.aliyun.com/linux-kernel/v3.x/linux-3.2.61.tar.xz 内核 在drive ...

  4. Prometheus Operator 架构【转】

    本节讨论 Prometheus Operator 的架构.因为 Prometheus Operator 是基于 Prometheus 的,我们需要先了解一下 Prometheus. Prometheu ...

  5. Redis Cluster 强制kill某一个节点和shutdown某一个节点后修复过程

    redis cluster 命令行,执行以下命令需登录cluster,是集群所独有的集群(cluster)CLUSTER INFO 打印集群的信息CLUSTER NODES 列出集群当前已知的所有节点 ...

  6. 查看Python安装目录 -- 一个命令

    pip --version

  7. asp.net+sql数据库学生信息管理系统

    一款学生信息管理系统送给大家. 功能部分: 1.教师管理(添加,修改,删除,查看) 2.学生管理(添加,修改,删除,查看) 3.班级管理(添加,修改,删除,查看) 4.学生成绩管理(添加,修改,删除, ...

  8. maven package和install

    mvn clean package依次执行了clean.resources.compile.testResources.testCompile.test.jar(打包)等7个阶段.mvn clean ...

  9. 安装phpmyadmin数据可视化

    1.下载压缩包,并且解压 cd /usr/local/src wget https://files.phpmyadmin.net/phpMyAdmin/4.9.4/phpMyAdmin-4.9.4-a ...

  10. windows中的运行命令

    首先按“开始”-“运行”,或按WIN键+R,进入『运行』窗口. 下面是常用的运行命令 (按英文字符顺序排列) appwize.cpl----添加.删除程序 access.cpl-----辅助功能选项 ...