一、观察者模式
先上一个图来描述一下
vue的响应式就是基于观察者模式进行的。其实不难理解:比如你是一个作者(发布者),很多粉丝(观察者)关注了你(订阅事件),你一旦写好了一本书发布了(触发了事件),然后粉丝就会阅读书籍(观察者根据触发的事件做一些事情)。那么在vue里面,数据改变了视图就更新,其实道理很简单,数据改变了执行一些函数(dom操作)让页面更新。用一个图来表示数据和这些函数之间的关系。那么这里,每个数据就是发布者,函数就是观察者。
在上面可以看到,依赖(dom操作的函数)收集发生在数据被访问的时候,其实准确来说应该是第一次被访问的时候。一个数据(发布者)可以对应多个函数(观察者),当这个数据被修改的时候,就会触发依赖(dom操作的函数),执行对应的函数,然后达到视图更新的效果。
二、对Vue的整体分析
整个流程按顺序大概就是:
1.初始化Vue,完成对应的参数配置(Vue类)
2.把每个数据都进行数据劫持(Observer类)
3.解析指令(v-model等),渲染页面(Compiler类)
4.在初次渲染页面中,初始化观察者(Watcher类)
5.把数据对应的每一个watcher添加到Dep中(Dep类)
那么整个过程下来以后,我们就能实现响应式。
下面就按顺序实现每一个类。
三.Vue类
在Vue类中,有一下事情需要做:
1.初始化一些参数
2.把data中的成员转换成getter和setter,注入到vue实例中(简单理解为用户可以用过this.xx直接拿到数据)
3.调用Observer类监听数据的变化
4.解析模板,渲染页面
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中的成员转换成getter和setter,注入到vue实例中
this._proxyData(this.$data)
// 3.调用Observer类监听数据的变化
new Observer(this.$data)
// 4.解析模板,渲染页面
new Compiler(this)
}
_proxyData (data) {
Object.keys(data).forEach(key => {
// 注意这里劫持的是this,也就是Vue的实例。
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newValue) {
if (newValue === data[key]) return
data[key] = newValue
}
})
})
}
}
四.Observer类
这个类的作用就是把传入的data的每一个属性都要劫持,包括嵌套对象的属性。
class Observer {
constructor(data) {
this.walk(data)
}
// 如果当前的data为对象才可进行遍历每个属性,监听每个属性。
walk (data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineProperty(data, key, data[key])
})
}
defineProperty (data, key, value) {
const that = this
// 为每一个属性定义一个发布者
let dep = new Dep()
// 若当前的值还是对象,则继续使用walk方法,递归执行。
this.walk(value)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
// 依赖收集,在数据仅在第一次被访问的时候才会执行下面这一行代码
Dep.target && dep.addWatch(Dep.target)
return value
},
set (newValue) {
if (value === newValue) {
return
}
value = newValue
// 如果newValue也是个对象,则使用walk方法,劫持它每一个属性
that.walk(newValue)
// 数据当前已经发生了改变,触发依赖,告诉对应的函数(观察者)该执行了
dep.notify()
}
})
}
}
五.Compiler类
这里只是简单的实现了插值表达式的渲染,以及v-text,v-model指令的渲染。
明确一点就是:每一个指令对应的渲染函数只会执行一次(比如compilerText),在后面修改数据的时候,修改页面是使用watcher去修改的,其实watcher中的函数,和原本的渲染函数是一样的。在Vue源码中,一般正在执行的渲染函数用一个全局变量记载,被称为被激活的渲染函数,那么添加依赖的时候,直接把这个全局变量添加即可。
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
// 立刻执行compiler进行解析指令,渲染页面
this.compiler(this.el)
}
compiler (el) {
// 先把当前元素的子节点拿出来,然后逐个分析,解析其中的指令。
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if (this.isTextNode(node)) {
this.compilerText(node)
} else if (this.isElementNode(node)) {
this.compilerElement(node)
}
// <h3>123123</h3> 元素节点中如果有内容,它的childNodes中就会有一个文本节点
if (node.childNodes && node.childNodes.length) {
this.compiler(node)
}
})
}
// 文本节点渲染方法
compilerText (node) {
// 匹配{{}}的形式,这种形式就是使用插值表达式
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) => {
node.textContent = newValue
})
}
}
// 元素节点渲染方法
compilerElement (node) {
let attributes = node.attributes
Array.from(attributes).forEach(attr => {
if (attr.name.startsWith('v-')) {
// 因为vue的指令一般以v-开头,所以这里截取了v-之后的字符串,对应指令的渲染函数以 v-之后的字符串+'Update'组成
let command = attr.name.substr(2)
let key = attr.value.trim()
let fn = this[command + 'Update']
fn && fn.call(this, node, key)
}
})
}
// v-text的渲染函数
textUpdate (node, key) {
node.textContent = this.vm[key]
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// v-model的渲染函数
modelUpdate (node, key) {
console.log('modelUpdate');
node.value = this.vm[key]
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// v-model的形式还要监听文本框的变化,如果文本框的内容一旦变化了,把值重新赋给对应的数据,就会触发依赖,其它用到该数据的视图就会发生变化
node.addEventListener('input', (value) => {
console.log(value);
this.vm[key] = node.value
})
}
// 判断是否为元素节点
isElementNode (node) {
return node.nodeType === 1
}
// 判断是否为文本节点
isTextNode (node) {
return node.nodeType === 3
}
}
六.Watcher类
watcher只有在数据第一次执行对应的渲染函数时候创建,我们在watcher内部进行依赖添加。我们可以先看到Observer类中,添加依赖的条件就是
Dep.target && dep.addWatch(Dep.target)
也就是说,Dep这个类下,有个target的属性才会去添加依赖。这个target其实指的是准备要添加的watcher。
所以第一步我们先把Dep下的target指向当前的watcher。
Dep.target = this
第二步,访问这个数据,触发这个数据的拦截器,来进行依赖收集,其实就是收集这个watcher。
this.oldValue = vm[key]
第三步,把Dep.target置为null,防止下一个下个数据被访问的时候,添加了当前的watcher。
Dep.target = null
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 触发依赖收集
Dep.target = this
this.oldValue = vm[key] //主要这里访问了vm.[key]触发了对应的get方法进行依赖收集
Dep.target = null
}
update (newValue) {
let newValue = this.vm[this.key]
if (this.oldValue == newValue) return
this.oldValue = newValue
this.cb(newValue)
}
}
七.Dep类
class Dep {
constructor() {
this.subs = []
}
addWatch (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 数据修改的时候,告诉收集回来的所有依赖该执行对应的方法了
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}