前言
今天看了一篇关于vue响应式原理的文章,知识比较深入,虽然花了比较长的时间,但还是稍微的理解了一些其中的原理,赶紧写一篇文章分享一下我的理解,可能比较白话一点,比较适合新手观看,
什么是响应式原理
数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这是官方文档的解释,那官方文档确实太官方了,咱们就简单的理解一下,就是当一个数据发生改变时,与他相关的数据也会即刻做出变化,那这章就来讲讲响应式原理的底层逻辑,也就是具体是怎么实现的,
响应式的设计模式
响应式的设计模式其实就是基于观察者模式,观察者模式简单来说就是,你想去一个商店买一个东西,但是你不知道商店什么时候会有,正常情况你需要一遍遍去问,但是你想到一个好主意,你把电话给了老板,等到有货的时候,打电话给你,来通知你,这样你就不需要一遍遍问了,节省了你很多的时间,这里节省了你的时间就相当于节省了性能,
如果想具体了解的话可以转站
设计模式--观察者模式
vue包含的文件
一个基础vue需要包含这些文件,接下来来一一介绍他们的作用
1. vue.js
很明显vue.js一看就知道是vue的入口,每个程序都需要有一个入口,而vue的入口当然是vue.js,vue.js中有个_proxyData()方法,作用是劫持data中的所有数据,
2. compiler.js
一个处理文件,专门用来处理解析指令,差值表达式,等等的集合
3.dep.js
前面说vue响应式结合了观察者模式,这里就开始体现了,dep中存在一个存储容器,添加观察者的方法,和通知观察者的方法,也就是观察者模式中的被观察者,我前面举例中的商店
作用: 收集依赖,也就是watcher (dep中讲解)
4.observer.js
不要被他的名字误导了,认为他是观察者,其实他不是,他内部有个walk方法,遍历data给data中的每个数据添加get和set现在说感觉有点听不懂,没有关系,先大概有个认识,然后继续看
5.watcher.js
这个才是观察者模式中的观察者,包含接收更新通知的方法,调用了data数据的地方
具体代码
建议全部看一遍之后再来逐个文件理解,跟着文章中的指引观看
vue.js
class Vue {
constructor (options) {
this.$options = options || {} // save options
this.$el = typeof options.el === 'string' ? document.querySelector(options.el)
:options.el // get dom
this.$data = options.data // get data
this.$methods = options.methods
// 1.data 所有数据进行劫持代理
this._proxyData(this.$data)
// 2.调用observe对象,监听数据变化
new Observer(this.$data)
// 3.调用compiler对象,解析指令和差值表达式
new Compiler(this)
}
_proxyData (data) {
// 遍历所有data
Object.keys(data).forEach(key => {
// 将每一个data通过defineProperty进行劫持
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newValue) {
if (data[key] === newValue) {
return
}
data[key] = newValue
}
})
})
}
}
进入第一个文件vue.js,我当时就遇到了问题,我废了好大劲,查了mdn把里面的方法看懂,比较陌生的在_proxyData方法中,接下来就来解释一下这两个函数Object.keys和Object.defineProperty,我们先看看observer文件再来讲解
observer.js
class Observer {
constructor(data) {
this.walk(data)
}
walk (data) { // 循环执行data
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive (obj, key, val) {
let that = this
this.walk(val) // 如果val是对象,则给他绑定get和set时触发的方法
let dep = new Dep() // 负责收集依赖,并发送通知
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
Dep.target && dep.addSub(Dep.target) // 收集依赖
return val // 如果使用obj[key],会变成死循环
},
set(newValue) {
if (newValue === val) {
return
}
val = newValue
that.walk(newValue) // 修改后可能是对象,set函数内部调用,修改了this指向
dep.notify() // 发送通知
}
})
}
}
!!!必看
observer中有一个walk方法使用递归的方式,将data中的每个数据添加get和set,具体等看完就能豁然开朗,walk中调用了Object.keys()方法,先来看看这个方法的作用:看右边目录中的插曲下面有讲解两个方法是作用和用法,如果不懂的话先看完插曲再继续看代码
分析代码
- observer.js 首先使用递归将data中所有的存在对象的key全部拆解开,
- 使用Object.keys方法强制转换为对象,并存储key值,forEach遍历
- defineReactive中调用Object.defineProperty给每个数据添加get和set,用于监听数据变化,当调用了该数据就会执行get方法,赋值则执行set方法,
- let dep = new Dep() 为每个数据创建dep实例(dep就是收集watcer的) (watcher就是调用数据的,可以理解为视图调用了数据显示)当watcher调用数据,就会执行get方法Dep.target && dep.addSub(Dep.target)就会执行这个数据的dep实例中的addSub方法,将调用了该数据的watcher存起来,
- 当某个数据发生变化时,就会调用set方法,去调用这个数据的dep实例中的notify方法(也就是通知watcher更新),因为与这个数据有关的watcher已经再调用的时候被存放到dep中sub数组中了
为什么每次遍历都要生成一个dep实例?
这个我当时没仔细看不理解,理解的就不用看了
首先要知道dep中存放的是什么,他存放的是依赖对象,不是data这点要清楚,
每个data都会有自己各自的依赖对象,也就是watcher,所以每个data都需要一个dep实例来存放他们各自的依赖对象,所以要每个数据创建一个实例
dep.js
// 订阅者Dep,存放观察者对象
class Dep {
constructor() {
this.subs = [] //存放观察者
}
/*添加观察者对象*/
addSub (sub) {
this.subs.push(sub)
}
// 通知所有watcher对象更新视图
notify () {
this.subs.forEach((sub) => {
sub.update() // watcher的更行方法
})
}
}
代码分析
- 这就是观察者模式中的被观察者
- 包含存放观察者的数组,添加观察者的方法,和通知观察者的方法
- dep在observer中被创建实例,并且是每个数据一个dep实例,为什么每个实例都需要一个dep实例呢? 每个数据都会有依赖对象,也就是需要使用这个数据的watcher,这就是依赖对象,dep中存放的也就是这些依赖对象
什么是收集依赖?
假如我是data,那我怎么收集依赖,谁需要使用我,那就是对我有依赖,那我就把他收集起来,那谁会依赖我呢,那就是下面的watcher,这么解释应该很清晰了,所以dep中数组存放的是watcher,而不是data
watcher.js
class Watcher {
constructor (vm, key, cb) {
this.vm = vm
// data中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据发生变化的时候通知视图更新
update () {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
代码分析
- watcher对象在构造函数,构造时,将this也就是自己本身赋值给dep.target,
- 然后在observer中判断dep.target是否存在,存在就添加到响应的数据dep实例中
- watcher拥有updata方法,用来更新视图
compiler.js
// 解析 v-model
modelUpdater (node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => { // 创建watcher对象,当数据改变更新视图
node.value = newValue
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 编译模板
compile (el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) { // 处理文本节点
this.compileText(node)
} else if(this.isElementNode(node)) { // 处理元素节点
this.compileElement(node)
}
// 如果还有子节点,递归调用
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
// console.log(node.attributes)
if (node.attributes.length) {
Array.from(node.attributes).forEach(attr => { // 遍历所有元素节点
let attrName = attr.name
if (this.isDirective(attrName)) { // 判断是否是指令
attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) // 获取 v- 后面的值
let key = attr.value // 获取data名称
this.update(node, key, attrName)
}
})
}
}
// 编译文本节点,处理差值表达式
compileText (node) {
// 获取 {{ }} 中的值
// console.dir(node) // console.dir => 转成对象形式
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim() // 返回匹配到的第一个字符串,去掉空格
node.textContent = value.replace(reg, this.vm[key])
new Watcher(this.vm, key, (newValue) => { // 创建watcher对象,当数据改变更新视图
node.textContent = newValue
})
}
}
小结:
compile 把元素转换为数据模型,他是普通的 JavaScript 对象,我们这叫做 vnode 对象, >然后遍历 vnode 对象,根据标识分为元素节点,文本节点,数据三个分类,分别进入不同的处理函数,并且创建一个 Watcher 对象,然后在 Watcher 对象中触发 get 实现响应式,同步会进行 updata 更新数据,转换成真实 dom ,完成页面渲染,更新就是如此反复。
插曲
Object.keys
作用:将参数强制转化为对象,并将对象中的key抽出放入一个数组
举个栗子:
let obj = {name : "orange" , age : 20} //一个对象 取出key值存放起来
Object.keys(obj) // ["name","age"]
let arr = [ "1", "2"] //一个数组 ---> {0: "1", 1: "2"} //强制转化为对象取出key值
Object.keys(obj) // ["0","1"]
这就是Object.keys的作用,相关文档
将data中的key存放到数组然后forEach遍历,所以为什么forEach中写的是key而不是item就是这个原因了 遍历的使用调用了defineReactive()函数,接下来,我们来看看这个函数,
又一次调用walk()方法,这就是递归,为什么要调用呢,因为如果data的key还有对象,就需要再次拆分,(这个我不确定我理解的对不对)直到if (!data || typeof data !== 'object') { return }这个时候结束递归,接着往下看,let dep = new Dep()这里new了一个dep的实例,然后调用了一个陌生的函数Object.defineProperty,我们来了解一下这个函数的作用
Object.defineProperty
作用:在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty的参数
Object.defineProperty(obj,prop,descriptor)
obj ==> 一个对象
prop ==> 需要添加或者修改的名称
descriptor ==> 要定义或修改的属性描述符 来看看代码,这是我自己写的一个例子,去验证一下Object.defineProperty中属性描述符的含义
function Person(){
let name = 'a'
Object.defineProperty(this,"name",{
get(){
return name + 1 //为了区别这里加个1
},
set(value){
name = value
}
})
}
const person = new Person()
console.log(person.name); //a1
person.name = "orange"
console.log(person.name); //orange1
这里来讲一讲我对get和set的理解:
这里的this是指向Person这个对象,也就是function本身,因为第一个参数传入的要求也是个对象,所以满足,
第二个对象是"name",Person中有name属性,所以是修改name,如果没有name属性,则是添加,这个一会再说,
第三个参数是个对象,里面有get和set方法,但是这是Person实例调用的方法吗,其实不是,在Object.defineProperty中的get和set分别代码取值和赋值,在你取值的时候就会调用get方法,赋值的时候会调用set方法,前提是取值和赋值的是第二个参数也就是"name"属性,,
当我第一次console.log也就是第一次取值,调用了get方法所以打印的是a1而不是a,在我赋值的时候调用了set方法,所以第二次取值的时候name变成了orange,所以打印的是orange1,这样就明白Object.defineProperty中set和get的具体作用了,这里就不展开讲数据描述符和存取描述符了,想了解更多可以看我另一篇方法介绍 或者 去官网查看相关用法
总结: 循环遍历data是为了给data中每个数据添加get和set,以至于监听每个数据的变化