vue怎么实现双向绑定?本篇文章就手把手教你写一个vue的双向绑定,让大家可以更好的理解双向绑定的逻辑走向,希望对大家有所帮助!
|
vue怎么实现双向绑定?本篇文章就手把手教你写一个vue的双向绑定,让大家可以更好的理解双向绑定的逻辑走向,希望对大家有所帮助!
本文主要是一个写坑和填坑的过程,让观众更好的理解如何去实现双向绑定,双向绑定的逻辑走向,至此一步步来重头实现一个双向绑定。这是一个类教程文章,只要跟着文章走,仔细观察每个类其实也并不难实现。 开始开局法师?带一条狗!! 好像不对 重来,开局一张图
从图上可以看出
接下里就是分析如何去实现,并且都需要写什么,先看一段vue的基础代码,我们从头开始分析 <div id="app">
<input v-model="message" />
<p>{{message}}</p>
</div>let app = new Vue({
el:"#app",
data:{
message:"测试这是一个内容"
}
})从上面代码我们可以看到 实现 Vue 类如下代码所示,这就写完了 class Vue {
constructor(options) {
// 代理数据
new Observer(options.data)
// 绑定数据
this.data = options.data
// 解析模板
new Compile(options.el, this)
}
}接着往下我们先写一个
实现模板解析 Compile 类下面我们将逐步实现
class Compile {
constructor(el, vm) {
// 获取静态节点
this.el = document.querySelector(el);
// vue实例
this.vm = vm
// 虚拟dom
this.fragment = null
// 初始化方法
this.init()
}
}
class Compile {
//...省略其他代码
init() {
// 创建一个新的空白的文档片段(虚拟dom)
this.fragment = document.createDocumentFragment()
// 遍历所有子节点加入到虚拟dom中
Array.from(this.el.children).forEach(child => {
this.fragment.appendChild(child)
})
// 解析模板
this.parseTemplate(this.fragment)
// 解析完成添加到页面
this.el.appendChild(this.fragment);
}
}
class Compile {
//...省略其他代码
// 解析模板
parseTemplate(fragment) {
// 获取虚拟DOM的子节点
let childNodes = fragment.childNodes || []
// 遍历节点
childNodes.forEach((node) => {
// 匹配大括号正则表达式
var reg = /\{\{(.*)\}\}/;
// 获取节点文本
var text = node.textContent;
if (this.isElementNode(node)) { // 判断是否是html元素
// 解析html元素
this.parseHtml(node)
} else if (this.isTextNode(node) && reg.test(text)) { //判断是否文本节点并带有双花括号
// 解析文本
this.parseText(node, reg.exec(text)[1])
}
// 递归解析,如果还有子元素则继续解析
if (node.childNodes && node.childNodes.length != 0) {
this.parseTemplate(node)
}
});
}
}
class Compile {
//...省略其他代码
// 判断是否携带 v-
isVueTag(attrName) {
return attrName.indexOf("v-") == 0
}
// 判断是否是html元素
isElementNode(node) {
return node.nodeType == 1;
}
// 判断是否是文字元素
isTextNode(node) {
return node.nodeType == 3;
}
}
class Compile {
//...省略其他代码
// 解析html
parseHtml(node) {
// 获取元素属性集合
let nodeAttrs = node.attributes || []
// 元素属性集合不是数组,所以这里要转成数组之后再遍历
Array.from(nodeAttrs).forEach((attr) => {
// 获取属性名称
let arrtName = attr.name;
// 判断名称是否带有 v-
if (this.isVueTag(arrtName)) {
// 获取属性值
let exp = attr.value;
//切割 v- 之后的字符串
let tag = arrtName.substring(2);
if (tag == "model") {
// v-model 指令处理方法
this.modelCommand(node, exp, tag)
}
}
});
}
}
class Compile {
//...省略其他代码
// 处理model指令
modelCommand(node, exp) {
// 获取数据
let val = this.vm.data[exp]
// 解析时绑定数据
node.value = val || ""
// 监听input事件
node.addEventListener("input", (event) => {
let newVlaue = event.target.value;
if (val != newVlaue) {
// 更新data数据
this.vm.data[exp] = newVlaue
// 更新闭包数据,避免双向绑定失效
val = newVlaue
}
})
}
}
class Compile {
//...省略其他代码
//解析文本
parseText(node, exp) {
let val = this.vm.data[exp]
// 解析更新文本
node.textContent = val || ""
}
}至此已经完成了
下面就是我们目前所实现的流程图部分
坑点一:
坑点二:
实现数据代理 Observer 类这里主要是用于代理data中的所有数据,这里会用到一个
// 监听者
class Observer {
constructor(data) {
this.observe(data)
}
// 递归方法
observe(data) {
//判断数据如果为空并且不是object类型则返回空字符串
if (!data || typeof data != "object") {
return ""
} else {
//遍历data进行数据代理
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
// 代理方法
defineReactive(data, key, val) {
// 递归子属性
this.observe(data[key])
Object.defineProperty(data, key, {
configurable: true, //可配置的属性
enumerable: true, //可遍历的属性
get() {
return val
},
set(newValue) {
val = newValue
}
})
}
}下面我们来测试一下是否成功实现了数据代理,在Vue的构造函数输出一下数据 class Vue {
constructor(options) {
// 代理数据
new Observer(options.data)
console.log(options.data)
// 绑定数据
this.data = options.data
// 解析模板
new Compile(options.el, this)
}
}结果如下,我们可以看出已经实现了数据代理。
对应的流程图如下所示
坑点三:
实现管理器 Dep 类上面我们已经实现了模板解析到初始化视图,还有数据代理。而下面要实现的 class Dep {
constructor() {
// 记录订阅者
this.subList = []
}
// 添加订阅者
addSub(sub) {
// 先判断是否存在,防止重复添加订阅者
if (this.subList.indexOf(sub) == -1) {
this.subList.push(sub)
}
}
// 通知订阅者
notify() {
this.subList.forEach(item => {
item.update() //订阅者执行更新,这里的item就是一个订阅者,update就是订阅者提供的方法
})
}
}
// Dep全局属性,用来临时存储订阅者
Dep.target = null管理器实现完成之后我们也就实现了流程图中的以下部分。要注意下面几点
实现订阅者 Watcher 类订阅者代码相对少,但是理解起来还是有点难度的,在
// 订阅者
class Watcher {
// vm:vue实例本身
// exp:代理数据的属性名称
// cb:更新时需要做的事情
constructor(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.putIn()
}
update() {
// 调用cb方法体,改变this指向并传入最新的数据作为参数
this.cb.call(this.vm, this.vm.data[this.exp])
}
putIn() {
// 把订阅者本身绑定到Dep的target全局属性上
Dep.target = this
// 调用获取数据的方法将订阅者加入到管理器中
let val = this.vm.data[this.exp]
// 清空全局属性
Dep.target = null
}
}坑点四:
埋坑通过上面的代码我们已经完成了每一个类的构建,如下图所示,但是还是有几个流程是有问题的,也就是上面的坑点。所以下面要填坑
埋坑 1 和 2 完成坑点一和坑点二,在 modelCommand(node, exp) {
// ...省略其他代码
// 实例化订阅者,更新时直接更新node的值
new Watcher(this.vm, exp, (value) => {
node.value = value
})
}
parseText(node, exp) {
// ...省略其他代码
// 实例化订阅者,更新时直接更新文本内容
new Watcher(this.vm, exp, (value) => {
node.textContent = value
})
}埋坑 3 完成坑点三,主要是为了引入管理器,通知管理器发生改变,主要是在 // 监听方法
defineReactive(data, key, val) {
// 实例化管理器--------------增加这一行
let dep = new Dep()
// ...省略其他代码
set(newValue) {
val = newValue
// 通知管理器改变--------------增加这一行
dep.notify()
}
}埋坑 4 完成坑点四,主要四将订阅者加入到管理器中 defineReactive(data, key, val) {
// ...省略其他代码
get() {
// 将订阅者加入到管理器中--------------增加这一段
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
// ...省略其他代码
}完成了坑点四可能就会有靓仔疑惑了,这里是怎么加入的呢
至此我们已经实现了一个简单的双向绑定,下面测试一下
完结撒花 总结本文解释的并不多,所以才是类教程文章,如果读者有不懂的地方可以在评论去留言讨论
(学习视频分享:vuejs教程、web前端) 以上就是手把手教你怎么实现一个vue双向绑定的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |
