1.数组的reduce方法
应用场景: 下次操作的初始值,依赖于上次操作的返回值
- 数组的累加计算
const arr = [3, 8, 9 ,12, 89, 56, 43]
// 普通程序员的实现逻辑
let total = 0;
arr.forEach(item => {
total += item;
})
console.log(total)
// reduce方法实现
// arr.reduce(函数, 初始值)
// arr.reduce((上次计算的结果, 当前循环的item) => {}, 0)
const total = arr.reduce((oldValue, item) => {
return oldValue + item
}, 0)
console.log(total)
- 链式获取对象属性的值
const obj = {
name: 'zs',
info: {
address: {
location: '北京顺义'
}
}
}
const attrs = ['info', 'address', 'location']
// 第一次reduce
初始值是 obj 这个对象
当前的 item 项是 info
第一次 reduce 的结果是 obj.info 属性对应的对象
// 第二次reduce
初始值是 obj.info 这个对象
当前的 item 项是 address
第二次reduce的结果是 obj.info.address 属性对应的对象
// 第三次reduce
初始值是 obj.info.address 这个对象
当前的 item 项是 location
第三次reduce的结果是 obj.info.address.location 属性的值
const val = attrs.reduce((newObj, k) => {
return newObj[k]
}, obj)
console.log(val)
2.发布订阅模式
1. Dep类
- 负责进行依赖收集
- 首先有个数组专门来存放所有的订阅信息
- 其次,还要提供一个向数组中追加订阅信息的方法
- 然后,还要提供一个循环,循环触发数组中的每个订阅信息
2. Watcher类
- 负责订阅一些事件
// 收集依赖/收集订阅者
class Dep {
constructor() {
// 这个 subs 数组,用来存放所有订阅者的信息
this.subs = []
}
// 向 subs 数组中,添加订阅者信息
addSub(watcher) {
this.subs.push(watcher)
}
// 发布通知(订阅)的方法
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 订阅者的类
class Watcher {
constructor(cb) {
// 这里的作用就是cb回调函数,根据得到的最新数据来更新自己的DOM结构的
this.cb = cb
}
update() {
this.cb()
}
}
const w1 = new Watcher(() => {
console.log('我是第一个订阅者')
})
const w2 = new Watcher(() => {
console.log('我是第二个订阅者')
})
// 将w1 和 w2这两个观察者放入 Dep 的 subs 数组中
const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
// 只要我们为 Vue 中 data 数据重新赋值了,这个赋值操作,会被 Vue 监听到
// 然后 Vue 要把数据的变化,通知到每个订阅者
// 接下来,订阅者(DOM元素)要根据最新的数据,更新自己的内容
dep.notify()
这里 Vue 要做的事情就是要把 data 的变化通知到每一个订阅者,在这里每一个订阅者就是DOM元素,当 Vue 发现数据变化的时候会通知到每个订阅者拿到最新的数据,这里通过 dep.notify
方法来执行watcher中的 update
方法,update
方法中的回调函数来实现 DOM 元素数据的更新
3.使用 Object.defineProperty() 进行数据劫持
- 通过
get()
劫持取值操作 - 通过
set()
劫持赋值操作
Object.defineProperty
语法,在 MDN 上是这么定义的:
Object.defineProperty(obj, prop, descriptor)
(1)参数
-
obj
要在其上定义属性的对象。
-
prop
要定义或修改的属性的名称。
-
descriptor
将被定义或修改的属性描述符。
(2)返回值
被传递给函数的对象。
(3)属性描述符
Object.defineProperty()
为对象定义属性,分 数据描述符 和 存取描述符 ,两种形式不能混用。
数据描述符和存取描述符均具有以下可选键值:
configurable
当且仅当该属性的 configurable
为 true
时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
enumerable
当且仅当该属性的 enumerable
为 true
时,该属性允许被循环。默认为 false。
Object.defineProperty(obj, 'name', {
enumerable: true, // 当前属性,允许被循环
configurable: true // 当前属性允许被配置 delete
})
存取描述符具有以下可选键值:
get
一个给属性提供 getter
的方法,如果没有 getter
则为 undefined
。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。默认为 undefined
。
set
一个给属性提供 setter
的方法,如果没有 setter
则为 undefined
。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined
。
const obj = {
name: 'zs',
age: '23',
}
Object.defineProperty(obj, 'name', {
get() {
return '我不是zs'
}
set(newVal) {
console.log('我不要你给的值', newVal)
dep.notify()
}
})
console.log(obj.name) // 我不是张三
// 这里如果没有`defineProperty`对属性进行get操作,那么打印结果应该是zs,但是通过get操作,这里的结果应该是:我不是zs,说明get方法可以拦截这个属性取值操作(getter)
obj.name = ls // 执行后结果为:我不要你给的值 ls
//说明set方法可以拦截这个属性的赋值操作(setter)
4.模拟Vue实现简单的双向数据绑定
- 原理图:
- 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>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'zs',
age: 20,
info: {
a: 'a1',
b: 'b1'
}
}
})
</script>
- vue.js内容:
class Vue {
// options指向的就是传进来的对象
constructor(options) {
this.$data = options.data
// 调用数据劫持的方法
Observe(this.$data)
// 属性代理
// 我们希望只通过vm就能获取到data中第一层属性的值
// 这里就比如我们在生命周期中获取data中属性 name 的值可以直接使用 this.name 就是因为我们做了属性代理
// 即:获取 vm.name -> 自动去找 vm.$data.name vm在这里只是做了一个代理
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.$data[key]
// 这里只要有访问vm获取data值的时候,它本身并没有,直接去找 $data 获取对应的值
},
set(newVal) {
this.$data[key] = newVal
}
})
})
// 调用模板编译的函数
Compile(options.el, this)
}
}
// 定义一个数据劫持的方法
function Observe(obj) {
// 这是递归的终止条件
if(!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通过 Object.keys 获取到 obj 上的每一个属性
Object.keys(obj).forEach(key => {
// 当前被循环的 key 所对应的属性值
let value = obj[key]
// 判断 value 是否是一个对象,如果是对象那么继续递归,如果不是,那么在开头就会被递归终止条件终止了
// 把 value 这个子节点进行递归
Observe(value)
// 需要为当前的 key 所对应的属性添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// getter拦截取值后我们应该返回拦截属性所对应的值
get() {
// Dep.target 此时还没有为null,还是指向 Watcher 实例
//只要执行了下面这一行,那么刚才 new 的 Watcher 实例
// 就被放入了 dep.subs 这个数组中
// target 所指向的 Watcher 实例,加到数组中
Dep.target && dep.addSub(Dep.target)
return value
},
// setter拦截赋值,应该把拦截属性当前值修改为新的值
set(newVal) {
value = newVal
// 为新赋值的对象添加 getter 和 setter
Observe(value)
// 通知每一个订阅者更新自己的文本
dep.notify()
}
})
})
}
// 对HTML结构进行模板编译的方法
function Compile(el, vm) {
// 获取到的 dom 元素直接挂载到 vm 的 $el 上
vm.$el = document.querySelector(el)
// 创建文档碎片,提高 DOM 操作性能
// 如果我们页面中有很多的插值表达式,那么我们要频繁的去更新 dom 元素的内容,这个时候会触发页面的重绘和重排。浪费我们的内存
// 内容发生变化会触发重绘,定位和位置发生变化会触发重排
// 这时候我们就要创建一个文档碎片,所谓文档碎片就是一块内存,把页面的每个 dom 节点都存进去
// 这时候页面中就没有这个 dom 节点了,我们这时候直接在内存中操作 dom 元素
// 由于文档碎片不在页面上,所以我们这时候随意修改也不会触发重绘和重排
const fragment = document.createDocumentFragment() // 创建文档碎片
while(childNode = vm.$el.firstChild) {
fragment.appendChild(childNode) // 把所有节点都放入文档碎片中,这时候页面中就没有 dom 节点了
}
// 再把文档碎片中的节点放回到页面中
// 在这里进行模板编译
// 因为在这一行之前页面中还没有dom节点,我可以在这个节点的时候dom元素还在文档碎片中放着呢
// 此时我们可以操作文档碎片中的每个子节点进行编译,编译完成后在append回去就不会触发重绘和重排)
Replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 dom 节点进行编译的方法
function Replace(node) {
// 对插值表达式进行正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/
// 证明当前的node节点是一个文本子节点,需要进行正则的替换
if(node.nodeType === 3) {
// 注意:文本子节点也是一个 dom 对象
// 如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
const text = node.textContent
// 进行字符串的正则匹配与提取
const execResult = regMustache.exec(text)
if(execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value) // 这里的replace方法是字符串本身的方法
// 在这个时候创建 watcher 类的实例
// 为什么要在这里调用Watcher类?
// 当执行到上面这行代码的时候,你是第一次知道怎么来更新自己
// 这个时候你应该立即把怎么更新自己的代码存到cb这个回调函数中
// 因为cb回调函数就是来记录怎么更新自己的
// 怎么存到cb中?这时候需要new一个实例才能存到cb中
new Watcher(vm, execResult[1], (newVal) =>{
// 根据最新的value值来更新自己的文本内容
node.textContent = text.replace(regMustache, newVal)
})
}
// 终止递归的条件
return
}
// 实现文本框数据绑定
// 如果是一个 dom 节点,就要判断你身上有没有 v-model 这个属性
// 如果存在我就认为你是一个文本框,并且要给你提供一个值
// 判断当前的 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
})
}
}
// 走到这一步证明不是文本子节点,需要进行递归处理
node.childNodes.forEach(child => Replace(child))
}
}
// 我们只用 Object.defineProperty 我们只能实现在页面打开的一瞬间实现数据编译
// 但是后面页面数据发生变化的时候是没有办法重现渲染页面的
// 这时候就需要用到发布订阅模式来实现数据的实时更新
// 因为加了发布订阅就相当于每个dom订阅了数据更新的一个行为,只要数据更新就会自动进行发布
// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
constructor() {
// 今后,所有的 watcher 都要存在这个数组中
this.subs = []
}
// 向 subs 数组中添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 负责同志每一个 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
// 订阅者的类
class Watcher {
// cb 回调函数中,记录着当前 watcher 如何更新自己的文本内容
// 但是,只知道如何更新自己还不行,还必须拿到最新的数据
// 因此,还需要在 new Watcher 期间,把vm也传递进来(因为vm中存着最新的数据)
// 除此之外,还需要知道在 vm 身上众多的数据中,哪个数据才是当前自己所需要的数据
// 因此必须在 new Watcher 期间,指定watcher对应的数据的名字
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
// 当我们执行这一步的操作的时候可以拿到对应key的值,但是我们的目的不是为了拿到key的值
// 因为这一步触发了 getter 方法,到这一步会暂缓下面的代码执行,跳到 getter 函数中,这就是我们的目的(具体看上面getter中操作)
// 我们这里的真正目的是为了将 new Watcher 每次调用的观察者存入 Dep 数组中,要不然下次无法通知到它
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)
}
}