内容为自学的笔记如有疏漏及错误的地方欢迎指出相互学习,谢谢
Vue响应式模拟
1. 前置知识
- 数据驱动
- 响应式核心原理
- 发布订阅模式和观察者模式
1.1 数据驱动
-
数据响应式
- 数据模型仅仅式普通的javaScript对象,而我们修改数据式,试图会进行更新,避免繁琐的DOM操作,提高开发效率
-
双向绑定
- 数据改变,试图改变;试图改变,数据也随之改变(直观表单)
- 我们可以使用v-model在表单元素上创建双向数据绑定
-
数据驱动:Vue最独特的特性之一
- 开发过程中仅需要关注数据本身,不需要关心数据式如何渲染视图,开发过程中我们仅需关注数据的操作,不需要关心渲染过程
1.2 数据响应原理
- vue 2.0就是Object.defineProperty(),通过get()set()实现,因为式新的属性所有有兼容性的问题
- vue 3.0是es6的proxy,好处是可以直接监听对象,而非属性,而且更多拦截操作,这部分内容也可以网上查阅。
1.3 发布订阅模式和观察者模式
-
定义 :
-
发布订阅模式是一种订阅模式,消息的放送者(发送者)不会直接发送消息特定的订阅者,而是将发布的消息消息发送给调度中心,由调度中心做信息过滤之后,根据之前依赖关系(订阅者和发布者的关系)当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
-
演示代码:
<script>
class Sub {
constructor() {
// list用来收集依赖(调度中心)
this.list = {}
}
//订阅
on(name,user,fn) {
// 当用户调用订阅法昂发的时候 step1 需要再调度中心添加一跳记录 =》建立依赖
if(!(this.list[name] instanceof Array)){
this.list[name] = [] //判断是否为数组,因为一个数据存在多一个依赖
}
this.list[name].push({user,fn})
}
//发布
emit(name,content) {
// 先找到这个发布者有多少个订阅者 并且把content的内容发布到每个订阅者中
this.list[name].forEach(ele =>{
// ele.就是每个订阅者和它的fn()
ele.fn(content)
})
}
//取消订阅
cancel(name,user) {
this.list[name].forEach((ele,index) => {
if(ele.user === user){
this.list[name].splice(index,1)
}
})
}
}
let exSub = new Sub()
// 用户a关注了bluej,调用on的方法
// on,应该又三参数 1,发布者 2,用户名(watcher) 3.回调函数(收到数据改变后得方法)
exSub.on('bluej', 'A', function (content) {
console.log('A用户接收到了bluej发送过来的推文' + content);
})
exSub.on('bluej', 'B', function (content) {
console.log('B用户接收到了bluej发送过来的推文' + content);
})
exSub.on('bluej', 'C', function (content) {
console.log('C用户接收到了bluej发送过来的推文' + content);
})
exSub.on('haha', 'B', function (content) {
console.log('B用户接收' + content);
})
exSub.on('haha', 'C', function (content) {
console.log('C用户接' + content);
})
console.log(exSub);
//触发emit之后会执行订阅了该发布者(bluej)的waicher('A')的回调函数
exSub.emit('bluej','前端的坑点')
exSub.emit('haha','这里haha的发布的推文')
exSub.cancel('bluej','A')
exSub.emit('bluej','今天下大雨')
</script>
1.4 vue基本实现原理
2. Vue响应式原理模拟
2.1 基本流程
首先要对数据(data)进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器(调度中新)Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
2.2 Vue类
-
基本功能
- 负责接收初始化的参数(选项)
- 负责把data中的属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler 解析指令/差值表达式
-
结构
- $options:用来记录传入的数据
- $el: 记录一个dom对象
- $data: 记录传入数据
- _proxtData():是私有成员,该方法是私有方法,把data中的属性注入到Vue实例,转换成getter/setter
class Vue {
constructor(options) {
// 1 负责接收初始化的参数(选项)
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2 负责把data中的属性注入到Vue实例,转换成getter/setter
this._proxyData(this.$data)
// 3 负责调用observer监听data中所有属性的变化
// 4 负责调用compiler 解析指令/差值表达式
}
_proxyData(data) {
//遍历 data中的说有数据,把他注入到vue实例中
// Object.keys()方法会返回一个由一个给定对象的自身可枚举属性组成的数组
Object.keys(data).forEach(key => { //注意使用箭头函数,指向vue实例。否则指向window
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get(){
return data[key]
},
set(newVal){
if(newVal === data[key]) return;
data[key] = newVal
}
})
})
}
}
- 在index页面中引入并调用,在控制台中打印vm可以看到对应的结果
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data:{
msg: 'hello Vue',
count: 100
}
})
</script>
2.3 Observer
-
功能
- 负责把 data 选项中的属性转换成响应式数据
- data 中的某个属性也是对象, 把该属性转换成响应式数据
- 数据变化发送通知
-
结构
+ walk(data)------遍历data中所有属性+ defineReactive(data, key, value)------- 定义响应式数据
-
基本代码
- 在Vue类constructor中调
new Observer(this.$data)使用
- 在Vue类constructor中调
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
if (!data || typeof data !== 'object') return
// 遍历data对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
//如果这里式return一个data[key]的话,会触发一个死递归
// 这里当我们创建一个新的vue实例的时候,会创建一个Observer类
// 外部$data就会引用到这个get方法形成闭包
return val
},
set(newValue){
if (newValue === val ) return
val = newValue
}
//发送通知
})
}
}
- 调用
<script src="./js/observer.js"></script>
<script src="./js/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data:{
msg: 'hello Vue',
count: 100,
person: { name:"小明"}
}
})
console.log(vm.msg);
vm.msg = {test:"this is test"}
</script>
- 问题
- 实例中data无法深度监听
- 当data的键值改为对象时无法监听
- 解决后代码
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
// 优化1:实例中data无法深度监听.调用walk经行处理
this.walk(val)
const that = this
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
return val
},
set(newValue){
if (newValue === val ) return
val = newValue
// 优化2:实例中data改变成对象无法监听.调用walk经行处理
that.walk(newValue)
}
})
}
}
2.4 Compiler
-
功能
- 负责编译模板,解析指令中的表达式
- 负责页面的首次渲染图
- 当数据变化后重新渲染试图
- 一句话概括dom操作
-
结构
名称 作用 el vue中传入的$el vm Vue实例 compile(el) 遍历对象的所有节点,并判断节点内容 compileElement(node) 解析元素节点,处理指令 compileText(node) 处理文件节点,解析插值表达式 isDirectivr(attrName) 解析指令 isTextNode(node) 判断是否文本节点 IsElementNode(node) 判断是否元素节点
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compiler(this.el)
}
// 编译模板,处理文本节点和元素节点
compiler(el) {
let childNode = el.childNodes // 获取节点中子节点结合
Array.from(childNode).forEach(node => { // 判断节点类型并给相关函数处理
if (this.isTextNode(node)) {
this.compilerText(node)
} else if (this.isElementNode(node)) {
this.compilerElement(node)
}
// 解决无法处理内层元素节点问题
if (node.childNodes && node.childNodes.length) {
this.compiler(node)
}
})
}
// 编译元素节点,处理指令
compilerElement(node) {
// console.log(node.attributes);
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
attrName = attrName.slice(2)
let key = attr.value // 获取属性值 => 对应的data
this.updata(node, key, attrName)
}
})
}
// 执行对应的updata指令 ,这样不用通过判断直接执行对应的函数
updata(node, key, attrName) { // attrName--指令的后缀 =》的应的updata方法
let updataFn = this[`${attrName}Updata`]
updataFn && updataFn(node, this.vm[key])
}
// 处理v-text指令
textUpdata(node, value) {
node.textContent = value
}
// 处理v-modle指令
modleUpdata(node, value) {
node.value = value // 更新表单的时候用的
}
// 编译文件节点,处理插值表达式
compilerText(node) {
// {{ value }} 使用正则去匹配该内容
// 将括号内的内容取出
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim() // 匹配第一个原子组,获取属性名
node.textContent = value.replace(reg, this.vm[key])
}
}
// 判断元素属性是否指令
isDirective(attrName) {
return attrName.startsWith('v-') // 判断是否式v-开头
}
// 判断节点是都文件节点
isTextNode(node) {
return node.nodeType === 3
}
// 判断节点是否元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
2.5 Dep(dependency)
-
功能
- 收集依赖,添加观察者模式
- 通知所有观察者
-
结构
功能 作用 subs 存储所有的观察者 addSub(sub) 添加观察者 notify() 派发消息 -
调用
- 我们需要对每一个响应式数据创建一个dep对象,收集依赖
- 当数据发生变法的时候通知观察者
- 调用观察这者中的updata方法去更新试图
- 在Observer中条用
- defineReactive去创建对象
let dep = new Dep() - set的去执行更新的方法
dep.nocify() - 在get中收集依赖
Dep.target && dep.addSub(Dep.target)(watcher时候回来看)
- defineReactive去创建对象
2.6 Watcher
-
功能
- 当数据变化触发依赖,dep同时所有watcher实例更新视图
- 自身实例化的时候往dep对象添加自己
-
结构
| 功能 | 作用 |
|---|---|
| vm | vue实例 |
| key | 数据名 |
| cb | 回调函数,当更新不同类型时候所触发的会低调 |
| oldValue | 旧值 |
| update() | 更新数据 |
2.7 流程分析
我们回到最后最开始流程图,通过浏览器断点模拟整个过程
-
首次渲染 (断点设置在new Vue)
=》浏览器渲染基本html =》创建一个 Vue实例 =》记录参数,生成app节点将,所有数据注入到vue实例中 =》调用Obsever,为每个data数据创建一个Dep(调度中心),同时劫持数据(设置getter和setter)。 - 因为dep实例子啊get中有引用所以会被保留下来 =》调用compiler编译模板 - 当到对应的模板语法的时候,触发对应的updata指令,将数据渲染到页面上(首次) - 同时渲染后,创建有一个Watcher对象(订阅者) - wathcher对象会记录对应data的数据,会触发getter方法,将这个wather对象push到Dep的队列中 -
数据变化(断点设置在set)
=》当数据变化的触发setter,向调用中心发送消息(调用该数据的dep对象中的nocify方法) =》调度中心会遍历队列中的watcher,并向他们发送消息,触发他们的update的方法。