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. 1-2SpringBoot项目属性配置

    前面我们讲解了SpringBoot HelloWorld实现 今天具体来讲解上那个application.properties项目配置文件 打开是空白 里面可以配置项目,所以配置项目我们 alt+/ ...

  2. pgsql 查询jsonb中包含某个键值对的表记录

    pgsql 查询jsonb中包含某个键值对的表记录 表名 table_name ,字段 combos 类型为 jsonb 可为空,示例内容如下, $arr_combos = [ ['id' => ...

  3. Lesson 44 Patterns of culture

    What influences us from the moment of birth? Custom has not commonly been regarded as a subject of a ...

  4. android中的简单animation(二)push up,push left,cross fade,hyperspace

    animation_2.xml: <?xml version="1.0" encoding="utf-8"?> <LinearLayout x ...

  5. 118.django中表单的使用方式

    表单 HTML中的表单: 从前端来说,表单就是用来将数据提交给服务器的,不管后台使用的是django还是php等其他的语言.只要把input标签放在form标签中,然后再添加一个提交的按钮,就可以将i ...

  6. Oracle--sqlplus--常用命令

    登陆:win+R输入sqlplus即可 如果前期没有用户可以输入sqlplus /nolog  记得sqlplus后有一个空格 --格式化命令 进行数据查询时,默认的方式排版会很乱,如果我们要解决这个 ...

  7. Django(五)1 - 4章实战:从数据库读取图书列表并渲染出来、通过url传参urls.py path,re_path通过url传参设置、模板语法

    一.从数据库读取图书数据并渲染出来 1)app1/views.py函数books编写 [1]从模型下导入bookinfo信息 [2]从数据库获取图书对象列表 [3]把获取到的图书对象赋值给books键 ...

  8. Java For 循环

    章节 Java 基础 Java 简介 Java 环境搭建 Java 基本语法 Java 注释 Java 变量 Java 数据类型 Java 字符串 Java 类型转换 Java 运算符 Java 字符 ...

  9. 解决物理机U盘安装Kali Linux2018.1,光驱无法加载问题

    1.无效的方法: (1)执行 df -m,然后查看U盘设备是否挂载到了/media,导致cd-rom不能被挂载,执行 umount  /media. (2)在光驱加载安装界面,把U盘拔下换到电脑的另外 ...

  10. Ubuntu安装Python版本管理工具pyenv

    gyf@gyf-VirtualBox:~$ git clone https://github.com/yyuu/pyenv.git ~/.pyenvCloning into '/home/gyf/.p ...