Vue中的数据双向绑定(Vue2)
前置条件
- 数组的reduce()方法
- 发布订阅模式
- 使用Object.defineProperty()进行数据劫持
数组的reduce()方法
reduce方法基本定义
Array.prototype.reduce()
reduce() 方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
第一次执行回调函数时,不存在“上一次的计算结果”。如果需要回调函数从数组索引为 0 的元素开始执行,则需要传递初始值。否则,数组索引为 0 的元素将被作为初始值 initialValue,迭代器将从第二个元素开始执行(索引为 1 而不是 0)。
概括说:reduce()就是一个滚雪球的方法,方便用于实现累加之类的操作
下面是一个例子:
let arr = [1, 2, 3, 4, 5]
let sum = arr.reduce((pre, item) => { return pre + item }, 0)
console.log(sum) //15
reduce函数有两个参数,第一个是每轮的回调函数,每次遍历都会执行这个函数,第二个参数为初始值,第一次遍历的时候会把初始值作为参数传入回调函数中,回调函数有多个参数,一般用前两个分别为上一次的值和当前遍历的值,之后回调函数会返回一个值,作为下一次遍历的pre值
reduce((previousValue, currentValue, currentIndex, array) => { /* ... */ }, initialValue)
reduce方法获取对象属性
通过一个数组,可以访问到对象最底层的属性
let arr = ['info', 'address', 'location']
let obj = {
name: 'zs',
info: {
address: {
location: 'china'
}
}
}
let addr = arr.reduce((pre, k) => { return pre[k] }, obj)
console.log(addr) //china
通过字符串获得所需要的数组,得到所需要的属性
let obj = {
name: 'zs',
info: {
address: {
location: 'china'
}
}
}
let addr = 'info.address.location'
let arr = addr.split('.').reduce((pre, k) => pre[k], obj)
console.log(arr) //china
用这种方法就可以处理Vue中的插值语句,例如{{info.address.location}}
发布订阅模式
发布订阅模式可以简单理解为,有一个对象负责收集所有的订阅信息,一旦收到更新消息后,就对订阅者进行通知,订阅者就进行更新
class Dep { //订阅信息收集类
constructor() {
this.sub = [] //订阅者数组
}
addSub(watcher) { //添加订阅者的方法
this.sub.push(watcher)
}
notify() { //通知订阅者更新的方法
this.sub.forEach(watcher => {
watcher.update()
})
}
}
class Watcher {
constructor(cb) { //传一个回调函数
this.cb = cb
}
update() { //触发回调的方法
this.cb()
}
}
let w1 = new Watcher(() => {
console.log('我是第一个订阅者')
})
let w2 = new Watcher(() => {
console.log('我是第二个订阅者')
})
let dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
dep.notify()
//输出
//我是第一个订阅者
//我是第二个订阅者
这种方法就实现了,当给一个data更新的时候,vue监听到了,就调用Dep.notify()通知每个订阅者,每一个订阅者就更新自己的dom元素,每一个Watcher在创建的时候都有一个回调函数作为参数,这个回调的作用就是更新自己的dom元素
使用Object.defineProperty()进行数据劫持
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77;
// throws an error in strict mode
console.log(object1.property1);
// expected output: 42
Object.defineProperty()有三个参数,分别为,对象、属性名和属性描述信息
在其中可以进行多种配置,如上面的value和writable,其中有两个方法是实现数据劫持的关键方法,即get()和set()
get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined。
set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined。
const obj = {}
Object.defineProperty(obj, 'name', {
enumerable: true, //该属性可以被循环,比如forIn
configurable: true, //该属性可以被配置
get() {
console.log('有人正在取obj.name')
return '不给你取值' //取值操作会被拦截,取到的值是返回的一个结果
},
set(newValue) {
console.log('有人正在设置obj.name', newValue)
//赋值操作会被拦截,可以获取到新赋的值,但是不会赋值给属性
}
})
obj.name = 'zs' //赋值
console.log(obj.name) //取值
实现一个数据劫持功能的Vue
首先写一个基本的html页面,类似Vue
index.html
<body>
<div id="app">
<h1>姓名是{{name}}</h1>
<h1>年龄是{{age}}</h1>
</div>
<script src="./vue.js"></script>
<script>
let vue = new Vue({
el: '#app',
data: {
name: 'zs',
age: 18,
attr: {
a1: 'a1',
a2: 'a2'
}
}
})
</script>
</body>
vue.js
class Vue {
constructor(option) {
this.$data = option.data
//调用数据劫持的方法,对$data上的属性都进行数据劫持
observe(this.$data)
}
}
function observe(obj) {
//如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
//这里没有考虑到数组的情况
if (!obj || typeof obj !== 'object') {
return
} else { //如果是对象,就对对象的每个属性添加getter和setter
//拿到当前对象上的每一个属性,形成一个数组
console.log(Object.keys(obj)) //['name','age','attr']
Object.keys(obj).forEach(key => {
let value = obj[key] //将该属性的值存起来
observe(value) //对子属性递归,添加getter和setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value //将改属性的值返回出去
},
set(newValue) {
value = newValue
observe(value) //重新赋值属性,也需要递归添加gettet和setter
}
})
})
}
}
通过上面的代码就可以实现基本的数据劫持
现在访问data需要用到vue.$data.来访问,而不能通过vue.来访问,所以可以进行一个数据代理,更新后的代码如下
class Vue {
constructor(option) {
this.$data = option.data
//调用数据劫持的方法,对$data上的属性都进行数据劫持
observe(this.$data)
//数据代理,可以通过vue.直接访问到vue.$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
}
})
})
}
}
实现一个单向绑定的Vue
new一个Vue实例的时候会有一个el属性,拿到el属性后对其中的区域进行插值替换需要进行模板编译,因此写一个编译函数
vue.js
class Vue {
constructor(option) {
this.$data = option.data
//调用数据劫持的方法,对$data上的属性都进行数据劫持
observe(this.$data)
//数据代理,可以通过vue.直接访问到vue.$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
}
})
})
//进行模板编译
compile(option.el, this)
}
}
compile()函数
function compile(el, vm) {
//获取el对应的dom元素
vm.$el = document.querySelector(el)
//创建文档碎片,在内存中操作dom,绘制好之后再返回给页面
//避免页面检测到变化不断重绘重排浪费性能
const fragment = document.createDocumentFragment()
//当存在子节点时,每循环一次将子节点移入文档碎片中(此时元素会从页面中消失)
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
//进行模板编译
replace(fragment)
//处理完所有的区域后再把子节点还回去
vm.$el.appendChild(fragment)
//定义模板编译函数
function replace(node) {
//定义插值表达式的正则,匹配{{name}}这种
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//只替换文本子节点,否则递归,找到文本子节点,对其替换
//根据nodeType判断是文本子节点
if (node.nodeType === 3) {
const text = node.textContent //拿到文本内容
const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本
if (execResult) {
//首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//对当前节点的文本内容进行替换,替换为value
node.textContent = text.replace(regMustache, value)
}
return
}
//不是纯文本子节点,则需要去递归其子节点
node.childNodes.forEach((node) => {
replace(node)
})
}
}
添加发布订阅模式
添加发布订阅模式实现数据更新,否则只会在页面第一次加载时填充
实现
数据更新 => 页面更新
这一句记住了如何更新自己,即watcher的更新方法
node.textContent = text.replace(regMustache, value)
首先需要定义Dep类和Watcher类,和上面代码有些许变化
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
class Watcher {
//cb中记录着如何更新自己
//vm中保存这最新数据
//vm身上的众多数据中哪一个是这个watcher的属性,用key
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
}
update() {
this.cb()
}
}
之后需要创建Watcher,在第一次更新插值替换后,watcher已经知道了如何更新自己,所以在第一次更新后创建watcher实例,传入回调,在compile函数中
更改之后的replace:
function replace(node) {
//定义插值表达式的正则,匹配{{name}}这种
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//只替换文本子节点,否则递归,找到文本子节点,对其替换
//根据nodeType判断是文本子节点
if (node.nodeType === 3) {
const text = node.textContent //拿到文本内容
const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本
if (execResult) {
//首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//对当前节点的文本内容进行替换,替换为value
node.textContent = text.replace(regMustache, value)
//创建Watcher实例,并且回调中添加一个newVaule为新的值
new Watcher(vm, execResult[1], (newValue) => {
node.textContent = text.replace(regMustache, newValue)
})
}
return
}
//不是纯文本子节点,则需要去递归其子节点
node.childNodes.forEach((node) => {
replace(node)
})
}
巧妙点
然后是把Watcher实例存到Dep中,这一步的操作时通过Watcher的构造函数来实现的,更新后的Watcher:
class Watcher {
//cb中记录着如何更新自己
//vm中保存这最新数据
//vm身上的众多数据中哪一个是这个watcher的属性,用key
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
//关键点
// 首先给Dep添加了Target这个属性,其指向了当前的watcher实例,临时挂载属性
Dep.target = this
//这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
//目的也不是取值,所以不需要拿到返回值
key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//执行完毕将target置空,避免内存占用
Dep.target = null
}
update() {
this.cb()
}
}
function observe(obj) {
//如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
//这里没有考虑到数组的情况
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
//如果是对象,就对对象的每个属性添加getter和setter
//拿到当前对象上的每一个属性,形成一个数组
// console.log(Object.keys(obj)) //['name','age','attr']
Object.keys(obj).forEach(key => {
let value = obj[key] //将该属性的值存起来
observe(value) //对子属性递归,添加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
observe(value) //重新赋值属性,也需要递归添加gettet和setter
}
})
})
}
在更新数据的时候也需要通知订阅者,即在set中notify()
function observe(obj) {
//如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
//这里没有考虑到数组的情况
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
//如果是对象,就对对象的每个属性添加getter和setter
//拿到当前对象上的每一个属性,形成一个数组
// console.log(Object.keys(obj)) //['name','age','attr']
Object.keys(obj).forEach(key => {
let value = obj[key] //将该属性的值存起来
observe(value) //对子属性递归,添加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
observe(value) //重新赋值属性,也需要递归添加gettet和setter
//通知更新
dep.notify()
}
})
})
}
之后写新值的更新update函数
class Watcher {
//cb中记录着如何更新自己
//vm中保存这最新数据
//vm身上的众多数据中哪一个是这个watcher的属性,用key
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
//关键点
// 首先给Dep添加了Target这个属性,其指向了当前的watcher实例,临时挂载属性
Dep.target = this
//这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
//目的也不是取值,所以不需要拿到返回值
key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//执行完毕将target置空,避免内存占用
Dep.target = null
}
update() {
//取到最新的值,拿给value
const value = this.key.split('.').reduce((preObj, k) => preObj[k], this.vm)
//将value传给cb,更新自己
this.cb(value)
}
}
文本框的单向数据绑定
html结构:
<body>
<div id="app">
<h1>姓名是{{name}}</h1>
<h1>年龄是{{age}}</h1>
<h1>attr的a1值是{{attr.a1}}</h1>
<div>name的值:<input type="text" v-model="name" /></div>
<div>age的值:<input type="text" v-model="age" /></div>
</div>
<script src="./vue.js"></script>
<script>
let vue = new Vue({
el: '#app',
data: {
name: 'zs',
age: 18,
attr: {
a1: 'a1',
a2: 'a2'
}
}
})
console.log(vue)
</script>
</body>
在replace中,需要对v-model的文本框进行插值,原理和上面一样,通过reduce等方法取到值,然后返回
//定义模板编译函数
function replace(node) {
//定义插值表达式的正则,匹配{{name}}这种
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//只替换文本子节点,否则递归,找到文本子节点,对其替换
//根据nodeType判断是文本子节点
if (node.nodeType === 3) {
const text = node.textContent //拿到文本内容
const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本
if (execResult) {
//首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//对当前节点的文本内容进行替换,替换为value
node.textContent = text.replace(regMustache, value)
//创建Watcher实例,并且回调中添加一个newVaule为新的值
new Watcher(vm, execResult[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)
//寻找v-model属性
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
//取到找到结果的值
const expStr = findResult.value
const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
node.value = value
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
}
}
//不是纯文本子节点,则需要去递归其子节点
node.childNodes.forEach((node) => {
replace(node)
})
}
单向数据绑定完整代码
class Vue {
constructor(option) {
this.$data = option.data
//调用数据劫持的方法,对$data上的属性都进行数据劫持
observe(this.$data)
//数据代理,可以通过vue.直接访问到vue.$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
}
})
})
//进行模板编译
compile(option.el, this)
}
}
function observe(obj) {
//如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
//这里没有考虑到数组的情况
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
//如果是对象,就对对象的每个属性添加getter和setter
//拿到当前对象上的每一个属性,形成一个数组
// console.log(Object.keys(obj)) //['name','age','attr']
Object.keys(obj).forEach(key => {
let value = obj[key] //将该属性的值存起来
observe(value) //对子属性递归,添加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
observe(value) //重新赋值属性,也需要递归添加gettet和setter
dep.notify()
}
})
})
}
function compile(el, vm) {
//获取el对应的dom元素
vm.$el = document.querySelector(el)
//创建文档碎片,在内存中操作dom,绘制好之后再返回给页面
//避免页面检测到变化不断重绘重排浪费性能
const fragment = document.createDocumentFragment()
//当存在子节点时,每循环一次将子节点移入文档碎片中(此时元素会从页面中消失)
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
//进行模板编译
replace(fragment)
//处理完所有的区域后再把子节点还回去
vm.$el.appendChild(fragment)
//定义模板编译函数
function replace(node) {
//定义插值表达式的正则,匹配{{name}}这种
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//只替换文本子节点,否则递归,找到文本子节点,对其替换
//根据nodeType判断是文本子节点
if (node.nodeType === 3) {
const text = node.textContent //拿到文本内容
const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本
if (execResult) {
//首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//对当前节点的文本内容进行替换,替换为value
node.textContent = text.replace(regMustache, value)
//创建Watcher实例,并且回调中添加一个newVaule为新的值
new Watcher(vm, execResult[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)
//寻找v-model属性
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
//取到找到结果的值
const expStr = findResult.value
const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
node.value = value
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
}
}
//不是纯文本子节点,则需要去递归其子节点
node.childNodes.forEach((node) => {
replace(node)
})
}
}
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
class Watcher {
//cb中记录着如何更新自己
//vm中保存这最新数据
//vm身上的众多数据中哪一个是这个watcher的属性,用key
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
//关键点
// 首先给Dep添加了Target这个属性,其指向了当前的watcher实例
Dep.target = this
//这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//执行完毕将target置空,避免内存占用
Dep.target = null
}
update() {
//取到最新的值,拿给value
const value = this.key.split('.').reduce((preObj, k) => preObj[k], this.vm)
//将value传给cb,更新自己
this.cb(value)
}
}
实现数据的双向绑定
通过监听文本框的输入事件,来拿到文本框最新的值,然后把值传给Vue实例,更新$data值,即可以实现双向绑定
,在拿到文本框之后,添加一个监听事件,完整代码,这时候就很好理解了
replace函数
function replace(node) {
//定义插值表达式的正则,匹配{{name}}这种
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//只替换文本子节点,否则递归,找到文本子节点,对其替换
//根据nodeType判断是文本子节点
if (node.nodeType === 3) {
const text = node.textContent //拿到文本内容
const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本
if (execResult) {
//首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//对当前节点的文本内容进行替换,替换为value
node.textContent = text.replace(regMustache, value)
//创建Watcher实例,并且回调中添加一个newVaule为新的值
new Watcher(vm, execResult[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)
//寻找v-model属性
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
//取到找到结果的值
const expStr = findResult.value
const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
node.value = value
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
//拿到最后一项的值,对其更新
const obj = keyArr.slice(0, keyArr.length - 1).reduce((preObj, k) => preObj[k], vm)
obj[keyArr[keyArr.length - 1]] = e.target.value
})
}
}
完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>姓名是{{name}}</h1>
<h1>年龄是{{age}}</h1>
<h1>attr的a1值是{{attr.a1}}</h1>
<div>name的值:<input type="text" v-model="name" /></div>
<div>age的值:<input type="text" v-model="age" /></div>
</div>
<script src="./vue.js"></script>
<script>
let vue = new Vue({
el: '#app',
data: {
name: 'zs',
age: 18,
attr: {
a1: 'a1',
a2: 'a2'
}
}
})
console.log(vue)
</script>
</body>
</html>
vue.js
class Vue {
constructor(option) {
this.$data = option.data
//调用数据劫持的方法,对$data上的属性都进行数据劫持
observe(this.$data)
//数据代理,可以通过vue.直接访问到vue.$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
}
})
})
//进行模板编译
compile(option.el, this)
}
}
function observe(obj) {
//如果传过来的不是一个对象,而是一些具体的值比如name,age,则不需要递归
//这里没有考虑到数组的情况
if (!obj || typeof obj !== 'object') return
const dep = new Dep()
//如果是对象,就对对象的每个属性添加getter和setter
//拿到当前对象上的每一个属性,形成一个数组
// console.log(Object.keys(obj)) //['name','age','attr']
Object.keys(obj).forEach(key => {
let value = obj[key] //将该属性的值存起来
observe(value) //对子属性递归,添加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
observe(value) //重新赋值属性,也需要递归添加gettet和setter
dep.notify()
}
})
})
}
function compile(el, vm) {
//获取el对应的dom元素
vm.$el = document.querySelector(el)
//创建文档碎片,在内存中操作dom,绘制好之后再返回给页面
//避免页面检测到变化不断重绘重排浪费性能
const fragment = document.createDocumentFragment()
//当存在子节点时,每循环一次将子节点移入文档碎片中(此时元素会从页面中消失)
while ((childNode = vm.$el.firstChild)) {
fragment.appendChild(childNode)
}
//进行模板编译
replace(fragment)
//处理完所有的区域后再把子节点还回去
vm.$el.appendChild(fragment)
//定义模板编译函数
function replace(node) {
//定义插值表达式的正则,匹配{{name}}这种
const regMustache = /\{\{\s*(\S+)\s*\}\}/
//只替换文本子节点,否则递归,找到文本子节点,对其替换
//根据nodeType判断是文本子节点
if (node.nodeType === 3) {
const text = node.textContent //拿到文本内容
const execResult = regMustache.exec(text) //匹配正则文本,1号元素则是需要的属性文本
if (execResult) {
//首先分割,然后再用reduce拿到其属性的值,将其返回,值是vm中的$data
const value = execResult[1].split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//对当前节点的文本内容进行替换,替换为value
node.textContent = text.replace(regMustache, value)
//创建Watcher实例,并且回调中添加一个newVaule为新的值
new Watcher(vm, execResult[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)
//寻找v-model属性
const findResult = attrs.find((x) => x.name === 'v-model')
if (findResult) {
//取到找到结果的值
const expStr = findResult.value
const value = expStr.split('.').reduce((preObj, k) => preObj[k], vm)
node.value = value
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
//拿到最后一项的值,对其更新
const obj = keyArr.slice(0, keyArr.length - 1).reduce((preObj, k) => preObj[k], vm)
obj[keyArr[keyArr.length - 1]] = e.target.value
})
}
}
//不是纯文本子节点,则需要去递归其子节点
node.childNodes.forEach((node) => {
replace(node)
})
}
}
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
class Watcher {
//cb中记录着如何更新自己
//vm中保存这最新数据
//vm身上的众多数据中哪一个是这个watcher的属性,用key
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
//关键点
// 首先给Dep添加了Target这个属性,其指向了当前的watcher实例
Dep.target = this
//这时候通过reduce取vm中的值,就会被数据劫持,到get()中执行
key.split('.').reduce((preObj, k) => { return preObj[k] }, vm)
//执行完毕将target置空,避免内存占用
Dep.target = null
}
update() {
//取到最新的值,拿给value
const value = this.key.split('.').reduce((preObj, k) => preObj[k], this.vm)
//将value传给cb,更新自己
this.cb(value)
}
}