双向数据绑定分步实现
1.数组的reduce()方法
应用场景:下次操作的初始值,依赖于上次操作的返回值
(1)数组的累加计算
普通实现
const arr = [1,2,3,4,5,6]
let total = 0;
arr.forEach(item=>{
total+=item;
})
console.log(total);
通过reduce实现
基础知识
- 使用reduce()方法实现
- 使用方法: 数组.reduce(函数,初始值)
- 循环当前数组,侧重于“滚雪球”
- 数组.reduce((上次计算的结果,当前循环的item项) => {},0)
- 数组.reduce((上次计算的结果,当前循环的item项) => {return上次计算的结果,当前循环的item项},0)
- const 累加的结果 = 数组.reduce((上次计算的结果,当前循环的item项) => {return上次计算的结果
代码
const total = arr.reduce((val,item) => {
return val + item;
},0)
console.log(total);
(2)链式获取对象属性的值
const obj = {
name:'xx',
info:{
address:{
location:'北京顺义',
}
}
}
普通实现
const location = obj.info.address.location
console.log(location)//北京顺义
通过reduce实现–初始给了一个数组
基础思路
//第一次 reduce
初始值是 obj这个对象,
当前的item项是 info
第一次reduce的结果是 obj.info的属性对应的对象
//第二次 reduce
初始值是 obj.info这个对象,
当前的item项是 address
educe的结果是 obj.info.address的属性对应的对象
//第三次 reduce
初始值是 obj.info.address这个对象,
当前的item项是 location
educe的结果是 obj.info.address.location的属性对应的值
代码
const attrs = ['info','address','location']
const location = attrs.reduce((newobj,key) => {
return newobj[key]
},obj)
console.log(location)
通过reduce实现升级操作–初始给了一个字符串,分割
代码
const attrs = 'info.address.location'
const location = attrs.split('.').reduce((newobj,key) =>
newobj[key]
,obj)
2.发布订阅模式
一.代码解析
(1)Dep类
- 负责进行依赖收集
- 第一,有个数组,专门来存放所有的订阅信息
- 第二,提供一个向数组中追加订阅信息的方法
- 第三,提供一个循环,循环触发数组中的每个订阅信息
代码
//收集依赖/订阅者
class Dep{
//constructor() 方法是一种特殊的方法,用于创建和初始化在类中创建的对象。
// 当初始化类时,constructor() 方法会被自动调用,并且它必须使用确切的名称 "constructor"
constructor() {
//这个subs数组,用来存放所有订阅者的信息
this.subs = []
}
//向subs数组中,添加订阅者信息,接受一个实例
addSub(watcher){
this.subs.push(watcher)
}
//发布通知的方法
notify(){
this.subs.forEach((watcher) => watcher.update())
}
}
(2)Watcher类
- 负责订阅一些事件
代码
//订阅者的类
class Watcher{
constructor(cb) {
this.cb =cb//将cb挂载到自己身上
}
//触发回调的方法
update(){
this.cb()
}
}
//创建类
const w1 = new Watcher(() => {
console.log('我是第一个订阅者')
})
// w1.update();调用这个函数才会打印我是第一个订阅者
const w2 = new Watcher(() => {
console.log('我是第二个订阅者')
})
const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
dep.notify()
二.运作
(1)介绍vue发布订阅模式如何运作
- 只要我们为vue中的data数据重新赋值了,这个赋值的动作,会被vue监听到,然后vue要把数据的变化,通知到每个订阅者,接下来,订阅者(DOM元素)要根据最新的数据,更新自己的类容
- 在数据更新完一瞬间,会触发update,里面写的代码就是根据新的数据进行的操作
3.使用Object.defineProperty()进行数据劫持
(1).通过get()劫持取值操作–叫做getter,函数名是get
(2).通过set()劫持赋值操作–叫做setter,函数名是set
const obj = {
name:'zs',
age:20,
}
Object.defineProperty(obj,'name',{
enumerable:true,//当前属性,允许被循环 for-in
configurable:true,//当前属性,允许被配置例如delete,允许对象中属性被删除
get(){
console.log('有人获取obj.name的值')
return '我不是zs'
},
set(newval){
console.log('不要值',newval)
dep.notify()
}
})
//取值操作
console.log(obj.name)
//赋值操作
obj.name = 'ls'
前期实现双向绑定铺垫代码学习
- html
- 实现getter,setter
- 属性代理
- 数据劫持
- 递归为每一个添加set,get
- 解决赋值后无set,get的情况
<body>
<div id="app">
<h1>姓名:{{name}}</h1>
</div>
<script src="05-vue.js"></script>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'zs',
age:20,
info:{
a:'a1',
c:'c1'
}
}
})
console.log(vm.name);//这里写vm.name为undefined
//这里访问的话需要访问vm.$data.name
//通过属性代理,可以使用vm.name访问
- js
- this指最终new出来那个实例
//实现vue中的getter
class Vue{
constructor(options) {
this.$data = options.data
//调用数据劫持的方法
observe(this.$data)
//属性代理
Object.keys(this.$data).forEach((key) => {
Object.defineProperty(this,key,{
enumerable:true,
configurable:'true',
get() {
return this.$data[key]
},
set(newvalue) {
this.$data[key] = newvalue
}
})
})
}
}
//定义一个数据劫持的方法
function observe(obj) {
//这是递归的终止条件
if(!obj || typeof obj !== 'object') return//是空的不是一个对象
//通过object.keys(obj)获取当前obj的每个属性
Object.keys(obj).forEach((key) => {
//当前被循环的key所对应的属性值
let value = obj[key]
//把value这个子节点,进行递归
observe(value)
//需要为当前的key所对应的属性,添加getter和setter
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get() {
console.log(`有人获取了${key}的值`);
return value
},
set(newvalue) {
value = newvalue;
//这里如果给对象里面的值a,c重新赋值,那么新的
//a和c就不会再有新的get和set,所以需要重新调用给新的这里修改的是info,还是一个对象vm.$data.info = {a:4,c:9},
observe(value)
}
})
})
}
- 文档碎片–提高性能,内容变了,页面需要重绘,定位位置变了,重排。为了性能,加文档碎片,也就是将节点存进去,在内存里进行修改,就不会进行重绘,编辑好以后,将结果重新渲染到页面上
- 将数据拿出来放到文本碎片里。进行操作,在还给vm
//调用模板编译的函数,编译需要模板结构,拿到数据传this,创建就调用
Compile(options.el,this)
//对HTML结构进行模板编译的方法
function Compile(el,vm) {
//获取el对应的Dom元素
vm.$el = document.querySelector(el)
//获取文档中 id="demo" 的元素:document.querySelector("#demo");
//创建文档碎片,提高Dom操作的性能
const fragment = document.createDocumentFragment()
//拿出来
while((childNode = vm.$el.firstChild)){
fragment.appendChild(childNode)
}
//进行模板编译
replace(fragment)
//放进去
vm.$el.appendChild(fragment)
function replace(node) {
//定义匹配插值表达式的正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//\S匹配任何非空白字符
//\s匹配任何空白字符,包括空格、制表符、换页符等等。
//()非空白字符提取出来,用一个小括号进行分组
//证明当前的node节点是一个文本子节点,需要进行正则的替换
if(node.nodeType == 3){
//注意:文本子节点,也是一个Dom对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
// console.log(node.textContent);
//终止递归的条件
return
}
//证明不是一个文本结点,可能是一个Dom元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
- 文本子节点进行正则的匹配和替换,到这里是将数据拿到渲染到页面上
if(node.nodeType == 3){
//注意:文本子节点,也是一个Dom对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
// console.log(node.textContent);
const text = node.textContent
//进行字符串的正则匹配与提取
const execRusult = regMustache.exec(text)//为一个数组,索引为0的为{{name}},为1的为name,exec() 方法用于检索字符串中的正则表达式的匹配。
if(execRusult){
const value = execRusult[1].split('.').reduce((newobj,k) => newobj[k],vm)
// console.log(value);
node.textContent = text.replace(regMustache,value)
}
- 发布订阅–当数据被更新时,页面会跟着渲染
- 创建两个类,创建watcher类的实例
//在这个时候,创建watcher类的实例,将这个方法存到watcher身上,调update就执行
new Watcher(vm,execRusult[1],(newValue) => {
node.textContent = text.replace(regMustache,newValue)
})
//依赖收集的类
class Dep{
constructor() {
//所有的watcher都要存到这个数组中
this.subs = []
}
//像数组中,添加watcher方法
addSub(watcher){
this.subs.push(watcher)
}
//负责通知每个watcher的方法
notify(){
this.subs.forEach((watcher) => watcher.update())
}
}
//订阅者的类
class Watcher{
//cb回调函数中,记录着当前watcher如和更新自己的文本内容
//同时,需要拿到最新的数据,因此,在new watcher 期间,需要传进来vm
//要知道在vm身上众多的数据中,那个数据,才是自己当前所需要的数据,在new watcher 期间,指定watcher对应数据的名字
constructor(vm,key,cb) {
this.vm = vm
this.key = key
this.cb = cb
}
//watcher实例,需要update函数,让发布者能够通知我们进行更新
update(){
this.cb()
}
}
- 将watcher实例存储到dep.subs数组中
get() {
//只要执行了下面这一行,那么刚才new的watcher实例,就加到了dep.subs这个数组中了
Dep.target&&dep.addSub(Dep.target)
}
//下面三行代码,负责把创建的watcher实例存到dep实例的subs数组中
Dep.target = this//自定义属性,watcher实例
key.split('.').reduce((newobj,k) => newobj[k],vm)//这里主要是想执行这一行代码,跳到get,返回name,age,
Dep.target = null
- 数据到view视图的单向绑定–修改之后显示在页面上
set(newvalue) {
dep.notify()
}
update(){
const value = this.key.split('.').reduce((newobj,k) => newobj[k],this.vm)
this.cb(value)
}
- 实现文本框的单向数据绑定
//判断当前的node节点是否为input输入框
if(node.nodeType === 1&& node.tagName.toUpperCase() === 'INPUT'){
//得到当前元素的所有属性节点
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x) => x.name === 'v-model')
if(findResult){
//获取到当前v-model属性的值 v-model=‘name’ v-model='info.a'
const expStr = findResult.value
const value = expStr.split('.').reduce((newobj,k) => newobj[k],vm)
node.value = value
//创建Watcher的实例
new Watcher(vm,expStr,(newValue) => {
node.value = newValue
})
}
}
- 实现文本框的双向数据绑定
//监听文本框的input事件,拿到文本框的最新的值,把最新的值,更新到vm上
node.addEventListener('input',(e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0,keyArr.length-1).reduce((newobj,k) => newobj[k],vm)
obj[keyArr[keyArr.length-1]] = e.target.value
})
完整代码
- HTML
<div id="app">
<h3 >姓名:{{name}}</h3>
<h3>年龄是:{{age}}</h3>
<h3>info.a的值是:{{info.a}}</h3>
<div>name的值:<input type="text" v-model="name"> </div>
<div>info.a的值:<input type="text" v-model="info.a"> </div>
</div>5
<script src="05-vue.js"></script>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'zs',
age:20,
info:{
a:'a1',
c:'c1'
}
}
})
console.log(vm.name);//这里写vm.name为undefined
//这里访问的话需要访问vm.$data.name
//通过属性代理,可以使用vm.name访问
</script>
</body>
- js
//实现vue中的getter
class Vue{
constructor(options) {
this.$data = options.data
//调用数据劫持的方法
observe(this.$data)
//属性代理
Object.keys(this.$data).forEach((key) => {
Object.defineProperty(this,key,{
enumerable:true,
configurable:'true',
get() {
return this.$data[key]
},
set(newvalue) {
this.$data[key] = newvalue
}
})
})
//调用模板编译的函数,编译需要模板结构,拿到数据传this,创建就调用
Compile(options.el,this)
}
}
//定义一个数据劫持的方法
function observe(obj) {
//这是递归的终止条件
if(!obj || typeof obj !== 'object') return//是空的不是一个对象
const dep = new Dep()
//通过object.keys(obj)获取当前obj的每个属性
Object.keys(obj).forEach((key) => {
//当前被循环的key所对应的属性值
let value = obj[key]
//把value这个子节点,进行递归
observe(value)
//需要为当前的key所对应的属性,添加getter和setter
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get() {
//只要执行了下面这一行,那么刚才new的watcher实例,就加到了dep.subs这个数组中了
Dep.target&&dep.addSub(Dep.target)
return value
},
set(newvalue) {
value = newvalue;
dep.notify()
//这里如果给对象里面的值a,c重新赋值,那么新的
//a和c就不会再有新的get和set,所以需要重新调用给新的这里修改的是info,还是一个对象vm.$data.info = {a:4,c:9},
observe(value)
}
})
})
}
//对HTML结构进行模板编译的方法
function Compile(el,vm) {
//获取el对应的Dom元素
vm.$el = document.querySelector(el)
//获取文档中 id="demo" 的元素:document.querySelector("#demo");
//创建文档碎片,提高Dom操作的性能
const fragment = document.createDocumentFragment()
//拿出来
while((childNode = vm.$el.firstChild)){
fragment.appendChild(childNode)
}
//进行模板编译
replace(fragment)
//放进去
vm.$el.appendChild(fragment)
function replace(node) {
//定义匹配插值表达式的正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//\S匹配任何非空白字符
//\s匹配任何空白字符,包括空格、制表符、换页符等等。
//()非空白字符提取出来,用一个小括号进行分组
//证明当前的node节点是一个文本子节点,需要进行正则的替换
if(node.nodeType == 3){
//注意:文本子节点,也是一个Dom对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
// console.log(node.textContent);
const text = node.textContent
//进行字符串的正则匹配与提取
const execRusult = regMustache.exec(text)//为一个数组,索引为0的为{{name}},为1的为name,exec() 方法用于检索字符串中的正则表达式的匹配。
if(execRusult){
const value = execRusult[1].split('.').reduce((newobj,k) => newobj[k],vm)
// console.log(value);
node.textContent = text.replace(regMustache,value)
//在这个时候,创建watcher类的实例,将这个方法存到watcher身上,调update就执行
new Watcher(vm,execRusult[1],(newValue) => {
node.textContent = text.replace(regMustache,newValue)
})
}
//终止递归的条件
return
}
//判断当前的node节点是否为input输入框
if(node.nodeType === 1&& node.tagName.toUpperCase() === 'INPUT'){
//得到当前元素的所有属性节点
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x) => x.name === 'v-model')
if(findResult){
//获取到当前v-model属性的值 v-model=‘name’ v-model='info.a'
const expStr = findResult.value
const value = expStr.split('.').reduce((newobj,k) => newobj[k],vm)
node.value = value
//创建Watcher的实例
new Watcher(vm,expStr,(newValue) => {
node.value = newValue
})
//监听文本框的input事件,拿到文本框的最新的值,把最新的值,更新到vm上
node.addEventListener('input',(e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0,keyArr.length-1).reduce((newobj,k) => newobj[k],vm)
obj[keyArr[keyArr.length-1]] = e.target.value
})
}
}
//证明不是一个文本结点,可能是一个Dom元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
}
}
//依赖收集的类
class Dep{
constructor() {
//所有的watcher都要存到这个数组中
this.subs = []
}
//像数组中,添加watcher方法
addSub(watcher){
this.subs.push(watcher)
}
//负责通知每个watcher的方法
notify(){
this.subs.forEach((watcher) => watcher.update())
}
}
//订阅者的类
class Watcher{
//cb回调函数中,记录着当前watcher如和更新自己的文本内容
//同时,需要拿到最新的数据,因此,在new watcher 期间,需要传进来vm
//要知道在vm身上众多的数据中,那个数据,才是自己当前所需要的数据,在new watcher 期间,指定watcher对应数据的名字
constructor(vm,key,cb) {
this.vm = vm
this.key = key
this.cb = cb
//下面三行代码,负责把创建的watcher实例存到dep实例的subs数组中
Dep.target = this//自定义属性
key.split('.').reduce((newobj,k) => newobj[k],vm)
Dep.target = null
}
//watcher实例,需要update函数,让发布者能够通知我们进行更新
update(){
const value = this.key.split('.').reduce((newobj,k) => newobj[k],this.vm)
this.cb(value)
}
}
ye