响应式原理的核心就是观测数据的变化,数据发生变化以后能通知到对应的观察者来执行相关的逻辑。
核心就是Dep,它是连接数据和观察者的桥梁。
data/props: defineReactive
computed: computed watcher
(depend)->
(notify)-> Dep ➡️(depend) getter ➡(notify)setter️
⬇️(update) ⬇️(addDep)
watch: user watcher (run)-> user callback
⬇️(update) ⬇️(addDep)
mount: render watcher (run)-> updateComponent
数据劫持/数据代理
主要是通过Object.defineProperty的get和set属性
第一步:
定义一个函数取名为dVue,主要功能是创建一个对象并返回
第二步:
定义一个函数取名为defineReactive,主要功能是循环对象内的值,并给每个值绑定上对应的set和get
第三步:
定义一个函数defineProperty,主要功能就是绑定set和get
第四步:
返回该对象
依赖收集
核心思想是事件发布订阅模式
订阅者Dep和观察者Watcher
收集依赖需要为依赖找一个存储依赖的地方,Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
实现一个订阅者Dep类,用于解耦属性的依赖收集和派发更新操作,它的主要作用是用来存放Watcher观察者对象。我们可以把Watcher
理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
发布订阅模式
vue->observer数据劫持->dep发布者
|
compiler解析指令->watcher观察者
import Compiler from './Compiler'
import Observer from './Observer'
export default class dVue {
constructor(options) {
// 1、保存vue实例传递过来的数据
this.$options = options // options是vue实例传递过来的对象
this.$data = options.data // 传递过来的data数据
// el 是字符串还是对象
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2、把this.$data中的成员转换成getter 和setter ,并且注入到vue实例中,使vue实例中有data里面的属性
// 但是this.$data自身内部成员并没有实现在自身内部属性的gettter和setter,需要通过observer对象来实现
this._proxyData(this.$data)
// this._proxyData(this.$data)
// 3、调用observer对象,监视data数据的变化
new Observer(this.$data)
// 4、调用compiler对象,解析指令和差值表达式
// debugger
new Compiler(this) // this是vue实例对象
}
_proxyData (data) {
// 遍历传递过来的data对象的数据,key是data对象中的属性名
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
configurable: true, // 可修改
enumerable: true, // 可遍历
// get 是 Object.defineProperty()内置的方法
get () {
return data[key]
},
// set 是 Object.defineProperty()内置的方法
set (newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
// main.js 进行用例测试
import dVue from './index'
var dVues = new dVue({
el: '#app',
data: {
text: 1,
object: {cc: 'cc1', dd: '2'},
array: [{c1: '1', d1: '2'}, {c2: '11', d2: '22'}],
},
methods: {
changeText() {
dVues.text = '2'
},
changeObject(){
// dVues.object.cc = '3'
dVues.object = 222
},
changeObjectValue(){
dVues.object.cc = '3'
console.log(dVues.object.cc)
// dVues.object = 222
},
changeArray() {
dVues.array[0].c1 = '333';
}
}
});
接下来用Observer类来拆分循环判断
// 定义一个Observer.js
import Dep from './Dep'
/**
* Observer类:作用是把data对象里面的所有属性转换成getter和setter
* data 是创建vue实例的时候,传递过来的对象里面的data,data也是个对象
*/
export default class Observer {
// constructor 是创建实例的时候,立刻自动执行的
constructor(data) {
this.walk(data);
}
// 遍历data对象的所有属性
// data 是创建vue实例的时候,传递过来的对象里面的data,data也是个对象
walk (data) {
// 判断data是否是对象
if (!data || typeof data !== 'object') {
return
}
const keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
this.defineReactive(data, keys[i], data[keys[i]])
}
}
// 把data对象里面的所有属性转换成getter和setter
defineReactive (obj, key, val) {
// 解决this的指向问题
let that = this
// 为data中的每一个属性,创建dev对象,用来收集依赖和发送通知
// 收集依赖:就是保存观察者
let dep = new Dep()
// 如果val也是对象,就把val内部的属性也转换成响应式数据,
/// 也就是调用Object.defineProperty()的getter和setter
console.log(key)
// console.log(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// Dep.target就是观察者对象,调用dev对象的addSub方法,把观察者保存在dev对象内
// target是Dep类的静态属性,但是却是在Watcher类中声明的
if(Dep.target){
dep.addSub(Dep.target)
}
// Dep.target && dep.addSub(Dep.target)
return val
},
set (newValue) {
if (newValue === val) {
return
}
val = newValue
// 对vue实例初始化后,传入的data数据的值进行修改,由字符串变成对象
// 也要把新赋值的对象内部的属性,转成响应式
that.walk(newValue)
// debugger
// data里面的数据发生了变化,调用dev对象的notify方法,通知观察者去更新视图
dep.notify()
}
})
}
}
定义一个Dep Dep主要是干什么呢 主要用来进行依赖收集 也就是管理watch 需要哪些东西呢?
// Dep 的核心是 notify
// 通过自定义数组subs进行控制
// 主要实现 addSub removeSub 循环遍历subs 去通知watch 更新
export default class Dep {
constructor () {
this.subs = [];
}
addSub (sub) {
console.log(sub)
if(sub && sub.update) {
this.subs.push(sub);
}
}
removeSub(sub) {
remove(this.subs, sub)
}
// 这个方法等同于 this.subs.push(Watcher);
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
// 这个方法就是发布通知 告诉你 有改变了
notify() {
const subs = this.subs.slice()
subs.sort((a, b) => a.id - b.id);
for (let i = 0, l = subs.length; i < l;i++){
subs[i].update()
}
}
}
Dep.target = null;
然后再用一个Watcher类去进行依赖收集,用Dep进行管理
import Dep from './Dep'
/**
* 当data数据发生变化,dep对象中的notify方法内通知所有的watcher对象,去更新视图
* Watcher类自身实例化的时候,向dep对象中addSub方法中添加自己(1、2)
*/
export default class Watcher {
constructor(vm, key, cb) {
this.vm = vm // vue的实例对象
this.key = key // data中的属性名称
this.cb = cb // 回调函数,负责更新视图
// 1、把watcher对象记录到Dev这个类中的target属性中
Dep.target = this // this 就是通过Watcher类实例化后的对象,也就是watcher对象
// 2、触发observer对象中的get方法,在get方法内会调用dep对象中的addSub方法
this.oldValue = vm[key] //更新之前的页面数据
// console.log(Dep.target)
Dep.target = null
}
// 当data中的数据发生变化的时候,去更新视图
update () {
// console.log(this.key)
const newValue = this.vm[this.key]
if (newValue === this.oldValue) {
return
}
this.cb(newValue)
}
}
好了,简单的实现了响应式,但是如何把响应的数据动态的绑定到页面上去呢? 通过Compiler.js
import Watcher from './Watch'
/**
* 主要就是用来操作dom
* 负责编译模板,解析指令/插值表达式
* 负责页面的首次渲染
* 当数据变化后重新渲染视图
*/
export default class Compiler {
constructor(vm) {
this.el = vm.$el // vue实例下的模板
this.vm = vm // vm就是vue实例
this.compile(this.el) // compiler实例对象创建后,会立即调用这个方法
}
// 编译模板,处理文本节点和元素节点
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)
}
// 递归调用compile,把所有的子节点都处理掉,也就是嵌套的节点都处理掉
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令,这里只处理v-text和v-model
compileElement (node) {
// console.dir(node.attributes)
Array.from(node.attributes).forEach((attr) => {
// console.log(attr.name)
let attrName = attr.name // 指令属性名 v-modelv-texttypev-count
// 判断是否是vue指令
if (this.isDirective(attrName)) {
// v-text ==> text
attrName = attrName.substr(2) // textmodelon:clickhtml
let key = attr.value // 指令属性值 // msgcounttextclickBtn()
// 处理v-on指令
if (attrName.startsWith('on')) {
const event = attrName.replace('on:', ''); // 获取事件名
// 事件更新
this.onUpdater(node, key, event);
} else {
this.update(node, key, attrName);
}
}
})
}
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater'] // textUpdater(){} 或者 modelUpdater(){}
// this 是compiler对象
updateFn && updateFn.call(this, node, this.vm[key], key) // updateFn的名字存在才会执行后面的函数
}
// 处理v-text指令
textUpdater (node, value, key) {
// console.log(node)
node.textContent = value
// 创建watcher对象,当数据改变去更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理v-html指令
htmlUpdater (node, value, key) {
// console.log(node)
node.innerHTML = value
// 创建watcher对象,当数据改变去更新视图
// this.vm: vue的实例对象 key:data中的属性名称 ()=>{}: 回调函数,负责更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// 处理v-model指令
modelUpdater (node, value, key) {
// console.log(node, value)
node.value = value
// console.log(node.value)
// 创建watcher对象,当数据改变去更新视图
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向数据绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 处理v-on指令
onUpdater (node, key, event) {
// console.log(node ,key, event)
node.addEventListener(event, () => {
// 判断函数名称是否有()
if (key.indexOf('(') > 0 && key.indexOf(')') > 0) {
this.vm.$options.methods[key.slice(0,-2)]()
} else {
this.vm.$options.methods[key]()
}
})
}
// 编译文本节点,处理差值表达式{{ msg }}
compileText (node) {
// console.dir(node)
let reg = /{{(.+?)}}/
let value = node.textContent // 获取文本节点内容:{{ msg }}
if (reg.test(value)) {
let key = RegExp.$1.trim() // 把差值表达式{{ msg }}中的msg提取出来
// 把{{ msg }}替换成 msg对应的值,this.vm[key] 是vue实例对象内的msg
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据改变去更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是vue指令
isDirective (attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点(元素节点1属性节点2文本节点3)
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点(元素节点1属性节点2文本节点3)
isElementNode (node) {
return node.nodeType === 1
}
}
最后,想要跑起来本实例的话需要配置webpack.config
//webpack.config.js
module.exports = {
mode: 'development',
devServer: {
host: 'localhost',
port: 9987,
hot: true,
open: true
},
entry: ['./src/reactive/index.js', './src/reactive/main.js'],
plugins: [
new HtmlWebpackPlugin({
// 打包输出HTML
title: 'dVue',
filename: 'index.html',
template: 'src/reactive/index.html'
})
]
}