笔记来源:拉勾教育 – 大前端就业集训营
文章内容:学习过程中的笔记、感悟、和经验
Vue.js 高阶特性及实现原理 – 响应式原理
响应式原理介绍
只有在data中声明的数据才是响应式的,不同操作方法可能导致响应效果不同
数据响应式
- 单向绑定:视图绑定数据,数据变化视图也跟着变化
- 双向绑定:视图和数据双向绑定,一方变化另一方也随之变化
准备工作
数据驱动
在学习vue中,常见的三个概念:
- 数据驱动:vue核心特性,数据驱动视图,只需要关注数据本身,不需要考虑如何渲染到视图上,只需要修改数据就可以实现数据驱动视图
- 数据响应式:数据驱动的具体实现方式
- 双向数据绑定:数据驱动的体现
核心原理分析
Vue2.xx与3.xx版本响应式实现方式有所不同:
- Vue2.xx响应式基于ES5的Object.defineProperty实现
- Vue3.xx响应式基于ES6的Proxy实现
Object.defineProperty
构造函数的方法,语言层面上对对象属性进行定义,例如值、是否只读、是否可遍历、是否可以在此设置等属性
语法:Object.defineProperty( 目标对象,属性名,{ 配置参数 })
// 初始化对象
var obj = {
name: 'zs',
age: 19
}
// 使用defineProperty设置对象 sex 属性
Object.defineProperty(obj, 'sex', {
// 属性值
value: '男',
// 是否可修改(默认false)
writable: true,
// 是否可遍历(默认false)
enumerable: true,
// 是否可再次设置(默认false)
configurable: true
})
// 再次尝试设置相同的属性
Object.defineProperty(obj, 'sex', {
// writable: true 就算上面 configurable 为 false也可以设置
// 如果上面 configurable 为 false 这里就会报错
enumerable: false
})
// 打印看一下
console.log(obj)
复制代码
get、set(存取描述符)和value、writable(数据描述符)是不能共存的
// 初始化对象
var obj = {
name: 'zs',
age: 19
}
// 设置一个中间变量用于属性的 get 和 set 方法
let sexValue = '男'
// 使用definproperty设置 sex 属性
Object.defineProperty(obj, 'sex', {
// get 方法
get() {
console.log('get里面可以书写任意js代码,只需要最终return数据就可以')
// 返回数据
return sexValue
},
// set 方法,接受一个新的值
set(newValue) {
// 不可以这么写,这么写会导致循环调用修改 sex 的操作,形成无限递归
// this.sex = newValue
console.log('set里面可以书写任意js代码')
// 设置新的值
sexValue = newValue
}
})
复制代码
注意:以为get不属于赋值,所以直接打印obj不会看到sex
Vue2.xx响应式原理
文档地址,现在看还比较复杂,当作参考
核心原理:设置data后,遍历data中所有属性(数据),转换为Getter-获取和Setter-变化,从而在数据变化时进行属兔更新等操作
<body>
<!-- 模拟视图 -->
<div id="app">原始文本</div>
<script>
// 数据
let data = {
num: '初始文本'
}
// 模拟vue实例
let vm = {}
// 设置 num 属性
Object.defineProperty(vm, 'num', {
// 可遍历、可再次设置
enumerable: true,
configurable: true,
// 读取数据
get() {
console.log('获取num')
// 返回data中的数据
return data.num
},
set(newValue) {
// 设置data中的数据
data.num = newValue
// 修改视图中的数据
document.getElementById('app').innerText = data.num
}
})
// 初始化时直接使用数据
document.getElementById('app').innerText = vm.num
</script>
</body>
复制代码
原理是利用了数据劫持,vm劫持了data中的数据,当读取或者更改vm中数据的时候实际访问的是data中的数据
存在问题
- 存在多个属性,无法处理多个属性
- 无法监听数组的变化(在vue中同样存在)
- 无法处理属性也是对象的情况
改进:处理多个属性
遍历全部属性,再进行设置
<body>
<!-- 模拟视图 -->
<div id="num">原始文本</div>
<div id="num2">原始文本</div>
<script>
// 数据
let data = {
// 多条数据
num: '初始文本',
num2: '初始文本2'
}
// 模拟vue实例
let vm = {}
// 遍历data全部属性
// Object.keys可以把对象的key组成一个数组输出
Object.keys(data).forEach(key => {
// 设置全部属性
Object.defineProperty(vm, key, {
// 可遍历、可再次设置
enumerable: true,
configurable: true,
// 读取数据
get() {
console.log('获取num')
// 返回data中的数据
return data[key]
},
set(newValue) {
// 设置data中的数据
data[key] = newValue
// 修改视图中的数据
document.getElementById(key).innerText = data[key]
}
})
})
// 初始化时直接使用数据
document.getElementById('num').innerText = vm.num
document.getElementById('num2').innerText = vm.num2
</script>
</body>
复制代码
改进:监测数组方法
当数据为数组时候,一些数组操作(push、索引操作)等无法更新视图(但可以触发get)
Vue中做了一些特殊操作使部分数组方法可以生效
我们创建一个新的原型对象替换掉原来数组的原型,并且在原型方法里修改视图
- 使用需要更新视图的方法组成一个新的原型对象
- 当发现我们遍历的属性值为数组的时候把这个属性的原型指向我们创造的新原型对象
- 新原型对象的每个方法都来自Array的原型,只不过增加了一步刷新视图的操作
- 最后为了避免有些没有列举的方法不能使用,要把新原型对象的原型指向Array,这样利用原型链就不会导致方法不可用了
<body>
<!-- 模拟视图 -->
<div id="#app">原始文本</div>
<script>
// 数据
let data = {
// 多条数据
num: '初始文本',
num2: '初始文本2',
// 添加数组
arr: [1, 2, 3]
}
// 模拟vue实例
let vm = {}
// 创建数组包含所有需要支持的数组方法名
const arrMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 创建一个对象来模拟新的原型对象
const arrprototype = {}
// 为了避免未列举的方法不能使用,我们把新原型的原型指向Array
arrprototype.__proto__ = Array.prototype
// 遍历全部需要支持的方法名
arrMethods.forEach(method => {
// 设置新原型上面的方法,注意这里使用function,不要使用箭头函数
arrprototype[method] = function() {
// 直接获取Array原型上面的同名方法,修改this指向即可
const x = Array.prototype[method].apply(this, arguments)
// 更新视图
document.getElementById('#app').innerText = this
// 返回新的方法
return x
}
})
// 遍历data全部属性
// Object.keys可以把对象的key组成一个数组输出
Object.keys(data).forEach(key => {
// 在设置之前先判断当前要设置的属性是不是数组
if (Array.isArray(data[key])) {
// 如果是数组修改原型指向为我们新创建的原型对象
data[key].__proto__ = arrprototype
}
// 设置全部属性
Object.defineProperty(vm, key, {
// 可遍历、可再次设置
enumerable: true,
configurable: true,
// 读取数据
get() {
console.log('获取num')
// 返回data中的数据
return data[key]
},
set(newValue) {
// 设置data中的数据
data[key] = newValue
// 修改视图中的数据
document.getElementById('#app').innerText = data[key]
}
})
})
// 注意:arrprototype[method]里面的this,就是调用方法的数组实例本身,所以这个this就是arr
</script>
</body>
复制代码
改进:封装与递归
当对象里面的属性也是一个对象,那么这个对象里面的所有属性也需要设置响应式,依此类推,所以我们需要使用递归的方式只要发现属性是个对象就在此深入
把遍历对象单独进行封装为一个函数(私有函数),一旦发现属性值为对象里吗进入重新进入新的遍历
<body>
<!-- 模拟视图 -->
<div id="#app">原始文本</div>
<script>
// 数据
let data = {
// 多条数据
num: '初始文本',
num2: '初始文本2',
// 添加数组
arr: [1, 2, 3],
// 添加对象
obj: {
name: 'zs',
age: 18
}
}
// 模拟vue实例
let vm = {}
// 把整个封装起来,使用一个变量接受返回值
const z = (function() {
// 数组处理不需要动
// 创建数组包含所有需要支持的数组方法名
const arrMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 创建一个对象来模拟新的原型对象
const arrprototype = {}
// 为了避免未列举的方法不能使用,我们把新原型的原型指向Array
arrprototype.__proto__ = Array.prototype
// 遍历全部需要支持的方法名
arrMethods.forEach(method => {
// 设置新原型上面的方法,注意这里使用function,不要使用箭头函数
arrprototype[method] = function() {
// 直接获取Array原型上面的同名方法,修改this指向即可
const x = Array.prototype[method].apply(this, arguments)
// 更新视图
document.getElementById('#app').innerText = this
// 返回数据,例如push返回数组长度
return x
}
})
// 真正需要递归的地方封装为单个函数作为返回值返回
return function(data, vm) {
// 遍历data全部属性
// Object.keys可以把对象的key组成一个数组输出
Object.keys(data).forEach(key => {
// 在设置之前先判断当前要设置的属性是不是数组
if (Array.isArray(data[key])) {
// 如果是数组修改原型指向为我们新创建的原型对象
data[key].__proto__ = arrprototype
} else if (typeof data[key] === 'object' && data[key] !== null) {
// 判断是都是对象(类型为obj并且不为空),把vm的属性设置为空对象
vm[key] = {}
// 重新带入开始递归,参数1 - 要代入的data对象数据,参数2 - 要设置的vm对应属性
z(data[key], vm[key])
// 返回,避免下面的执行
return
}
// 设置全部属性,如果上面两个都不是,那么直接执行
Object.defineProperty(vm, key, {
// 可遍历、可再次设置
enumerable: true,
configurable: true,
// 读取数据
get() {
console.log('获取num')
// 返回data中的数据
return data[key]
},
set(newValue) {
// 设置data中的数据
data[key] = newValue
// 修改视图中的数据
document.getElementById('#app').innerText = data[key]
}
})
})
}
})()
// 最后调用执行变量
z(data, vm)
</script>
</body>
复制代码
proxy – ES6提供
ES6提供了Proxy类,Proxy可以代理某个对象,在操作Proxy实例的时候实际上操作的就是代理的对象,方便很多,性能更好
缺点:兼容性较差,IE中不支持
// 创建data对象,内部属性使用字符串、数组、对象三种
const data = {
str: 'cwn',
arr: [1, 2, 3],
obj: {
name: 'zs',
age: 19
}
}
// 创建一个Proxy实例
// 参数1:要代理的对象
// 参数2:配置对象
const p = new Proxy(data, {
// get方法
get (obj, key, proxy) {
console.log(obj, key, proxy)
return obj[key]
},
// set方法,get和set参数意义相同
// 参数1:被代理的对象
// 参数2:获取/修改的属性名
// 参数3:要修改属性值
// 参数4:proxy实例 本身
set (obj, key, value, proxy) {
console.log(obj, key, value, proxy)
obj[key] = value
}
})
// 当我们修改、读取p属性的时候实际上是修改读取data的属性
// proxy修改参数可以深入对象内部的对象修改
复制代码
Vue3.xx响应式原理
利用proxy代理要代理的对象,通过修改proxy实例中的参数实现修改代理的对象中的数据
<body>
<!-- 视图 -->
<div id="app">初始文本</div>
<script>
// 创建data对象,内部属性使用字符串、数组、对象三种
const data = {
str: 'cwn',
arr: [1, 2, 3],
obj: {
name: 'zs',
age: 19
}
}
const vm = new Proxy(data, {
get (obj, key) {
return obj[key]
},
set (obj, key, newvalue) {
// 更新数据
obj[key] = newvalue
// 修改视图
document.querySelector('#app').textContent = obj[key]
}
})
</script>
</body>
复制代码
**问题:**修改数组和对象的内容还依旧无法更新视图,目前只有修改字符串属性值才能更新视图
相关设计模式
针对软件设计中普遍存在的各种问题所提出的解决方案
观察者模式
表示在对象之间定义一个一对多(被观察者和多个观察者)的关联,一旦被观察者(对象)状态发生改变,所有其他观察者会被通知并且自动刷新(所有观察者可能行为不同,但都源于被观察者状态的变化)
核心概念
- 观察者Observer:等待被观察者状态变化,一旦状态变化立即作出反应
- 被观察者Subiect:保存所有观察者,发生状态变化立即通知观察者
// 创建被观察者类class Subiect { constructor() { // 全部观察者组成的数组 this.observes = [] } // 添加观察者方法 addObserve (observe) { // 判断传入的是不是一个有效的观察者(观察者必须有updata方法) if (observe && observe.updata) this.observes.push(observe) } // 通知观察者方法 notify () { // 遍历所有已知观察者,触发他们的upadta方法 this.observes.forEach(item => item.updata()) }}// 创建观察者类class Observe { // 观察者具备的updata方法 updata () { console.log('我收到通知了,要干活了') }}// 创建一个被观察者,两个观察者const subiect = new Subiect()const ob1 = new Observe()const ob2 = new Observe()// 把观察者添加到被观察者的观察者数组中subiect.addObserve(ob1)subiect.addObserve(ob2)// 被观察者发出通知subiect.notify()
复制代码
发布、订阅模式
观察者模式的解耦进阶版本,特点如下:
在发布者和订阅者之间添加一个消息中心,所有消息均通过这个消息中心管理,而发布者和订阅者不再存在直接联系,从而实现两者解耦
核心概念:
- 消息中心Dep:发布者和订阅者的中间人
- 发布者Subscriber:通知消息中心触发功能
- 订阅者Publisher:在消息中心订阅消息,消息中心中某些事件被触发,会接到通知进行操作
<body> <!-- 使用vue --> <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.runtime.min.js"></script> <script> // 创建vue实例作为消息中心 const bus = new Vue() // 注册事件(添加订阅) bus.$on('go', () => { console.log('订阅者1事件') }) bus.$on('go', () => { console.log('订阅者2事件') }) // 触发事件(发布者发布) bus.$emit('go') // 上面的例子中 // bus是一个消息中心 // 订阅者在消息中心中通过 $on 订阅消息(注册事件) // 发布者可以通过 $emit 触发消息 </script></body>
复制代码
小结
- 观察者模式由观察者河北观察目标组成,适合在组件内部操作
- 在事件触发后,观察目标统一通知观察者
- 订阅、发布模式由消息中心处理,适合消息类型比较复杂的情况
- 特殊事件发生,消息中心接到发布者发布指令,会根据时间类型给对应的订阅者发送消息
vue响应式原理模拟
整体分析
要miniVue实现响应式数据,需要先观察vue实例的结构,分析要实现那些属性和功能
- Vue类:将data数据注入Vue实例,便于方法内操作
- Observe发布者:数据劫持,监听数据变化,并在变化时通知Dep
- Dep消息中心:存储订阅者及管理消息发送
- Watcher订阅者:订阅数据变化,进行视图更新
- Compiler:解析模版中的指令和差值表达式,并替换为相应数据,利用Dom
Vue类
功能:
- 接收配置信息
- 将data中的属性转换为getter、setter,并注入到vue实例中
- (暂时不能实现)监听data中的属性变化,设置成响应式数据
- (暂时不能实现)调用解析功能(解析模版内的差值表达式、指令等)
每一个类都单独一个js文件
创建index.html文件,和之前使用vue一样创建vue实例
在vue.js文件中创建vue类,接收参数书写option、data、el(两种传入方式)
通过数据劫持把data中的数据注入到vm中,使用Vue2.xx的方式
<!-- 新建index.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> <!-- Vue容器 --> <div id="app"></div> <!-- 引入Vue --> <script src="./js/Vue.js"></script> <script> // 创建实例 const vm = new Vue({ // el可以使用dom节点或者字符串设置 // el: document.querySelector('#app'), el: '#app', // data数据 data: { str1: '内容1', str2: '内容2' } }) </script></body></html>
复制代码
// 新建Vue.js文件// 创建Vue类class Vue { // 构造函数带入传入的数据 constructor (options) { // 设置$options并设置默认值 this.$options = options || {} // $data从$options中获取 this.$data = this.$options.data // $el分两种情况,当传入的值是字符串那么就要自己寻找元素节点,否则直接使用元素节点即可 this.$el = typeof this.$options.el === 'string' ? document.querySelector(this.$options.el) : this.$options.el // 注入数据,传入 Vue 类和 $data proxyData(this, this.$data) } }// 使用数据劫持劫持data中的数据function proxyData (Vue, data) { // 将data全部 key 组成数组然后遍历 Object.keys(data).forEach(key => { // 使用 defineProperty 设置属性 Object.defineProperty(Vue, key, { // 是否可遍历(默认false) enumerable: true, // 是否可再次设置(默认false) configurable: true, // get方法:直接返回数据 get () { return data[key] }, // set方法:直接修改 set (newValue) { data[key] = newValue } } ) })}
复制代码
Observer类
- 通过数据劫持的方式监视data中的属性变化,变化时通知消息中心Dep
- 要考虑到data数据可能也是一个对象,也要做响应式数据的处理
index.html 引入onserver.js,给data新增对象功能<body> <!-- Vue容器 --> <div id="app"></div> <!-- 引入 onserver ,因为 vue 使用了 onserver 所以要在 vue 前面引入!!!!!!! --> <script src="./js/onserver.js"></script> <!-- 引入 Vue --> <script src="./js/Vue.js"></script> <script> // 创建实例 const vm = new Vue({ // el可以使用dom节点或者字符串设置 // el: document.querySelector('#app'), el: '#app', // data数据!!!!!!!!!!!!!!!!!! data: { str1: '内容1', str2: '内容2', obj: { name: 'zs', age: 18, s: { a: 'a', b: 'b' } } } }) </script></body>
复制代码
// js/Vue.js 在构造函数哪创建Observer实例,并把this.data传递进去进行数据劫持// 创建Vue类class Vue { // 构造函数带入传入的数据 constructor (options) { // 1、接收配置信息 // 设置$options并设置默认值 this.$options = options || {} // $data从$options中获取 this.$data = this.$options.data // $el分两种情况,当传入的值是字符串那么就要自己寻找元素节点,否则直接使用元素节点即可 this.$el = typeof this.$options.el === 'string' ? document.querySelector(this.$options.el) : this.$options.el // 2、注入数据,传入 Vue 类和 $data proxyData(this, this.$data) // 3、创建Observer实例 监听data变化 new Observer(this.$data) } }// 使用数据劫持劫持data中的数据function proxyData (Vue, data) { // 将data全部 key 组成数组然后遍历 Object.keys(data).forEach(key => { // 使用 defineProperty 设置属性 Object.defineProperty(Vue, key, { // 是否可遍历(默认false) enumerable: true, // 是否可再次设置(默认false) configurable: true, // get方法:直接返回数据 get () { return data[key] }, // set方法:直接修改 set (newValue) { data[key] = newValue } } ) })}
复制代码
// js/onserver.js 新建Observer文件// 创建Observer类class Observer { // 从Vue实例中创建Observer实例传递过来的data数据 constructor (data) { // 存储 this.data = data // 劫持this.data this.ergodicData(this.data) } // 遍历所有 data 数据,交给 kidnap 劫持 ergodicData (data) { Object.keys(data).forEach(key => this.kidnap(key, data[key])) } // 调用 hijackingData 函数进行数据劫持 kidnap (key, value) { // 最终劫持给 this.data hijackingData(this.data, key, value) }}// 这里跟称好多函数是为了让功能更加单一,增加复用性// 数据劫持函数// 参数1: Observer 实例的 data// 参数2: 属性名// 参数3:属性值function hijackingData (data, key, value) { // 调用 foundObserver 进行对象检测 foundObserver(value) // 如果检测通过 value 不是对象,进行数据劫持 Object.defineProperty(data, key, { // 是否可遍历(默认false) enumerable: true, // 是否可再次设置(默认false) configurable: true, // get方法:直接返回数据 get () { // 做出动作 console.log('Observer读取数据了') // 直接返回value return value }, // set方法:修改数据 set (newValue) { // 做出动作 console.log('Observer修改数据了') // 如果新值和老值一样直接返回 if (newValue === value) return // 修改数据 value = newValue // 调用 foundObserver 进行对象检测 foundObserver(value) } })}function foundObserver (value) { // 判断传入的数据是不是对象形式,如果是对象再新建一个 Observer 实例劫持这个对象 if (typeof value === 'object' && value !== null) { return new Observer(value) }}// 实际上,一个Observer实例只能劫持一个对象,当对象内部还有对象的时候,就需要新建一个Observer进行再次劫持
复制代码
最终结果:直接修改vm.属性名既可以触发Vue劫持,又可以触发Observer劫持
Dep类
Dependency的简写,含义是依赖,Dep用于收集和管理订阅者与发布者之间的依赖关系
- 为每个数据收集对应的依赖,存储依赖(暂时不能实现)
- 添加并存储所有订阅者
- 数据变化时,通知所有订阅者
// js/dep.js 创建Dep文件// Dep 类class Dep { constructor () { // 所有订阅者 this.subs = [] } // 添加订阅者 addSub (sub) { if (sub && sub.updata) this.subs.push(sub) } // 发动通知执行 notify () { this.subs.forEach(sub => sub.updata()) }}
复制代码
// js/onserver.jsfunction hijackingData (data, key, value) { // 为每个data数据都要创建一个Dep实例 const dep = new Dep() .................. set (newValue) { ................... foundObserver(value) // 发生变化调用dep通知订阅者 dep.notify() } })}
复制代码
但是我们现在还没有订阅者,所以我们下面需要创建订阅者类才能实现
这里我们将每一个数据单独创建一个消息中心,而使用数据的地方创建一个订阅者
Watcher类
- 实例化watcher的时候,在dep中添加自己
- 当数据变化时触发dep,dep通知所有对应的watcher实例进行视图更新
// js/watcher.js 新建订阅者类class Watcher { // 参数1:vue实例 // 参数2:订阅者要订阅的数据 // 参数3:发生变化时候订阅者要执行的回调函数 constructor (vm, key, callback) { // 存储传入的三个数据 this.vm = vm this.key = key this.cb = callback // 在订阅者创建的时候把自己设置给 Dep 类的属性 Dep.watcher = this // 在初始化订阅者的时候获取一下当前的初始值 this.oldValue = vm[key] // 最后清除Dep中刚设置的订阅者属性 Dep.watcher = null } // 数据发生变化方法 updata () { // 如果新数据和旧数据完全一样,那么直接返回不管了 if (vm[this.key] === this.oldValue) return // 如果不一样,执行回调函数并把新值带进去 this.cb(this.vm[this.key]) }}
复制代码
// js/onserver.js 在数据劫持的get方法中判断并向消息中心添加订阅者// 创建Observer类class Observer { // 从Vue实例中创建Observer实例传递过来的data数据 constructor (data) { // 存储 this.data = data // 劫持this.data this.ergodicData(this.data) } // 遍历所有 data 数据,交给 kidnap 劫持 ergodicData (data) { Object.keys(data).forEach(key => this.kidnap(key, data[key])) } // 调用 hijackingData 函数进行数据劫持 kidnap (key, value) { // 最终劫持给 this.data hijackingData(this.data, key, value) }}// 这里跟称好多函数是为了让功能更加单一,增加复用性// 数据劫持函数// 参数1: Observer 实例的 data// 参数2: 属性名// 参数3:属性值function hijackingData (data, key, value) { // 为每个data数据都要创建一个Dep实例 const dep = new Dep() // 调用 foundObserver 进行对象检测 foundObserver(value) // 如果检测通过 value 不是对象,进行数据劫持 Object.defineProperty(data, key, { // 是否可遍历(默认false) enumerable: true, // 是否可再次设置(默认false) configurable: true, // get方法:直接返回数据 get () { // 做出动作 console.log('Observer读取数据了') // 在创建订阅者的时候必然会触发一次 get ,这时候订阅者会把自己设置给 Dep 类的静态属性中!!!!!!!!!!!!!!!!!!!!!!!!!!1 // 我们可以查看 Dep 类这个属性是否有值来的判断当前get是不是由订阅者触发!!!!!!!!!!!!!!!!!!!!!!!!!! // 如果有代表当前 get 是订阅者用来获取初始值触发的,那么就把订阅者添加到dep实例的数组中!!!!!!!!! if (Dep.watcher) dep.addSub(Dep.watcher) // 直接返回value return value }, // set方法:修改数据 set (newValue) { // 做出动作 console.log('Observer修改数据了') // 如果新值和老值一样直接返回 if (newValue === value) return // 修改数据 value = newValue // 调用 foundObserver 进行对象检测 foundObserver(value) // 发生变化调用dep通知订阅者 dep.notify() } })}function foundObserver (value) { // 判断传入的数据是不是对象形式,如果是对象再新建一个 Observer 实例劫持这个对象 if (typeof value === 'object' && value !== null) { return new Observer(value) }}// 实际上,一个Observer实例只能劫持一个对象,当对象内部还有对象的时候,就需要新建一个Observer进行再次劫持
复制代码
compiler类与文本节点处理
-
模版编译解析内部指令和差值表达式
-
进行页面首次渲染
-
数据变化后重新渲染视图
处理文本节点
// js/compiler.js 新建文件 创建订阅者类class Compiler { constructor(vm) { // 保存数据 this.vm = vm this.el = vm.$el // 调用方法处理容器内容 this.compile(this.el) } // vue 挂载容器内容 compile (el) { // 遍历所有挂载 容器下面的节点 Array.from(el.childNodes).forEach(node => { if (node.nodeType === 3) { // 如果是文本节点,调用文本节点编译方法 this.compileText(node) } else if (node.nodeType === 1) { // 如果是元素节点,调用元素节点编译方法 // console.log('我是元素节点')÷ } }) } // 文本节点编译方法 compileText (node) { // 创建一个正则,用来筛选模版字符串 const reg = /\{\{(.+?)\}\}/g // 过滤文本内容,去掉全部空白字符 const newtext = node.textContent.replace(/\s/g, '') // 创建一个数组接纳所有需要拼接的字符串 const strs = [] // 匹配到的模版字符串在整个字符串中结束位置:下面循环中需要 let lastIndex = 0 // 匹配到的模版字符串在整个字符串中开始位置:下面循环中需要 let nowIndex = 0 // 创建一个变量接纳每次匹配出来的结果 let result // 遍历字符串匹配结果 while (result = reg.exec(newtext)) { // 将匹配到的开始位置设置给 nowIndex = result.indexnowIndex // 如果当前位置大于 lastIndex 表示中间有字符串尚未保存,那么把两个位置中间的字符串保存进数组 if (nowIndex > lastIndex) strs.push(newtext.slice(lastIndex, nowIndex)) // 修改lastIndex以便下次再截取(加上模版字符串本身长度,因为这段长度会被替换掉) lastIndex = nowIndex + result[0].length // 将再vm中匹配到的属性值添加进数组 strs.push(this.vm[result[1]]) // 记住当前模版字符串再数组中的位置 const meIndex = strs.length - 1 // 创建订阅者(每个模版字符串都是一个订阅者) new Watcher(this.vm, result[1], newValue => { // 把修改后的新值设置到数组中 strs[meIndex] = newValue // 数组转字符串设置给文本节点内容 node.textContent = strs.join('') }) } // 最后判断一下如果最后一个模版字符串后面还有东西,则将后面的文本也添加到数组中 if (lastIndex < newtext.length) strs.push(newtext.slice(lastIndex)) // 初次渲染是也要做数组转字符串设置内容 node.textContent = strs.join('') } compileElement (node) { }}
复制代码
处理元素节点
class Compiler { constructor(vm) { // 保存数据 this.vm = vm this.el = vm.$el // 调用方法处理容器内容 this.compile(this.el) } // vue 挂载容器内容 compile (el) { // 遍历所有挂载 容器下面的节点 Array.from(el.childNodes).forEach(node => { if (node.nodeType === 3) { // 如果是文本节点,调用文本节点编译方法 this.compileText(node) } else if (node.nodeType === 1) { // 如果是元素节点,调用元素节点编译方法 this.compileElement(node) } // 判断该节点是否存在子节点,存在利用递归再次遍历 if (node.childNodes && node.childNodes.length) { this.compile(node) } }) } // 文本节点编译方法 compileText (node) { // 创建一个正则,用来筛选模版字符串 const reg = /\{\{(.+?)\}\}/g // 过滤文本内容,去掉全部空白字符 const newtext = node.textContent.replace(/\s/g, '') // 创建一个数组接纳所有需要拼接的字符串 const strs = [] // 匹配到的模版字符串在整个字符串中结束位置:下面循环中需要 let lastIndex = 0 // 匹配到的模版字符串在整个字符串中开始位置:下面循环中需要 let nowIndex = 0 // 创建一个变量接纳每次匹配出来的结果 let result // 遍历字符串匹配结果 while (result = reg.exec(newtext)) { // 将匹配到的开始位置设置给 nowIndex = result.index // 如果当前位置大于 lastIndex 表示中间有字符串尚未保存,那么把两个位置中间的字符串保存进数组 if (nowIndex > lastIndex) strs.push(newtext.slice(lastIndex, nowIndex)) // 修改lastIndex以便下次再截取(加上模版字符串本身长度,因为这段长度会被替换掉) lastIndex = nowIndex + result[0].length // 将再vm中匹配到的属性值添加进数组 strs.push(this.vm[result[1]]) // 记住当前模版字符串再数组中的位置 const meIndex = strs.length - 1 // 创建订阅者(每个模版字符串都是一个订阅者) new Watcher(this.vm, result[1], newValue => { // 把修改后的新值设置到数组中 strs[meIndex] = newValue // 数组转字符串设置给文本节点内容 node.textContent = strs.join('') }) } // 最后判断一下如果最后一个模版字符串后面还有东西,则将后面的文本也添加到数组中 if (lastIndex < newtext.length) strs.push(newtext.slice(lastIndex)) // 初次渲染是也要做数组转字符串设置内容 node.textContent = strs.join('') } // 元素节点编译方法 compileElement (node) { // 遍历节点全部属性,一旦碰到以 v- 开头的属性名。立马调用 updata 对属性进行处理 Array.from(node.attributes).forEach(attr => { if (attr.name.slice(0,2) !== 'v-') return this.updata(node, attr.name.slice(2), attr.value) }) } // v- 开头的属性处理 updata (node, name, key) { // 调用 this 里相应的方法 this[name](node, key, this.vm[key]) } // v-text 处理方法 text (node, key, value) { // 创建订阅 new Watcher(this.vm, key, newValue => { // 直接把新值设置给node节点 node.textContent = newValue }) // 初始化设置 node.textContent = value } // v-model 处理方法 model (node, key, value) { // 同样创建订阅 new Watcher(this.vm, key, newValue => { // data变化直接修改 node.value = newValue }) // 初始化设置 node.value = value // 由于v-mode是双向绑定,在这里给node节点绑定input方法 node.addEventListener('input', () => { // input 的时候修改 vm 里的对应属性值 this.vm[key] = node.value }) }}
复制代码
功能回顾与总结
- Vue类
- 把data的属性注入到vue实例中,这样可以直接使用在实例中使用属性名调用数据
- 调用Observer类实现数据响应式处理的操作
- 调用Compiler进行模版编译
- Observer类 – 发布者
- 将vue中的data属性更改为get和set
- 为Dep添加订阅者Wather
- 数据变化发送通知Dep
- Dep类 – 通知中心
- 收集依赖,添加订阅者
- 通知订阅者
- Watcher类 – 订阅者
- 编译模版时候创建订阅者,订阅数据变化
- 接到Dep通知时,调用Compiler中的模版功能更新视图
- Compiler类 – 模版编译
- 编译模版,解析指令和插值表达式
- 负责页面的首次渲染和数据更新后的重新渲染