直播课(1)如何通过数据劫持实现Vue(mvvm)框架
19.6.28更新:
这篇博客比较完善:将每一部分都分装在单独的js文件中:
半个月前看的直播课,现在才自己敲了一遍,罪过罪过
预览:
简单实现
Vue
mvvm的双向数据绑定,需要以下几个步骤:
实现一个入口,把 指令渲染,数据劫持
实现指令渲染,包括层级嵌套的标签,文本
数据劫持
订阅发布
let vm = new Kvue({ el: "#app", data: { message: "测试数据", options: "123", name: "张三" } })
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) } }) } }
// 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 } }) }) }
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()
// 发布订阅模式 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) } }