在讲 Vue 响应式原理之前,我们需要熟悉数组当中的 reduce 方法,并且可以巧妙的运用 reduce() 方法来解决问题的需要,为下面理解 Vue 响应式原理先提前热热脑子。下面我们先来介绍一下 reduce() 方法。
reduce() 方法
reduce()方法:会循环当前的数组,下一次操作依赖上一次的返回值,就相当于进行“滚雪球”的操作。
reduce((上次计算的结果,当前循环的item项) => {
return 上次结果 + 当前循环item项
},初始值)
应用场景:下次操作的初始值,依赖于上次操作的返回值。
实现数值的累加操作
当我们实现一个数值的累加的时候,一般会想到的都会是循环遍历使用 forEach() 方法进行求值。
const array = [1, 2, 3, 4, 5]
let sum = 0
array.forEach(item => {
sum += item
})
console.log(sum) // 15
上面的累加求和中,编写的代码行数太多了,还需要声明一个外部变量来存储数值。看起来比较啰嗦和麻烦。但是我们可以使用 reduce()方法快速的实现数值的累加操作。
const array = [1, 2, 3, 4, 5]
const sum = array.reduce((pre, next) => pre + next,0)
console.log(sum) // 15
上面两种方法中均实现可以累加求和,但是 reduce()方法比 forEach()方法看起来更加的简洁。
实现对象链式取值操作
reduce()方法的强大之处不仅能实现数值的累加操作,还可以实现对象链式取值的操作等等。
const obj = {
name: 'alex',
info: {
address: {
location: 'gz'
}
}
}
const array = ['info', 'address', 'location']
const value = array.reduce((pre, next) => pre[next], obj)
console.log(value) // gz
链式获取对象属性值升级操作
const obj = {
name: 'alex',
info: {
address: {
location: 'gz'
}
}
}
const objInfo = 'info.address.loaction'
const value =objInfo.split('.').reduce((pre, next) => pre[next], obj)
console.log(value) // gz
好了,通过上面 reduce()方法的使用,相信你已经进入状态了,现在正式进入主题。
发布订阅模式
举个简单的例子:现有一家商店供应百事可乐汽水,此时来了A、B、C三个人,都是来买百事可乐的,但是这家商店的百事可乐卖完了,此时店长就拿出一本子分别来记录他们的联系方式,等到百事可乐到货了,再拿出本子一一对应的通知它们三个人来取货,然后A、B、C就分别拿着百事可乐去做另外的事情、。这个例子中,就可以将A、B、C三个人理解为 Watcher 订阅者,商店理解为一个 Dep,负责进行依赖的收集,并且通知 Watcher,Watcher 收到通知之后就做相关的事情(比如重新渲染页面,数据驱动视图的更新)。
通过上面简单的例子,我们也知道了Dep类里面应该具备以下功能:
负责进行依赖收集。首先,有个数组,专门用来存放所有的订阅信息。其次,还要提供一个向数组中追加订阅的addSub()方法。最后,还要提供一个循环,循环触发(通知)数组中每个订阅信息。Watcher类:负责订阅一些事件,主要是一个回调函数
Vue 响应式原理
通过上面的例子,我们就很好的理解了什么是发布订阅模式了,也了解了Dep类和Watcher类都有什么作用。下面我们进行深入研究一下 Vue 响应式的原理。
Vue 响应式原理最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,这个方法是本文中最重要、最基础的内容之一。
要实现Vue的双向数据绑定,就必须要实现以下几点:
- 实现一个
Dep,主要用来收集依赖(订阅者),通知对应的订阅者更新数据。 - 实现一个数据监听器
Observer,能够对数据对象的所有属性都进行监听,都加上setter、和getter方法,如有变动可拿到最新值通知依赖收集对象(Dep)并通知订阅者(Watcher)来更新视图变化。 - 实现一个解析器
Compile,对每一个元素节点的指令进行扫描和解析,根据指令替换数据,以及绑定相应的更新函数。 - 实现一个
Watcher,作为连接Observer和Compile的桥梁,能够订阅并接收到每个属性变动的通知,执行相应的回调函数,从而更新视图。
实现解析器 Compile
实现一个解析器 Compile,对每一个元素节点的指令进行扫描和解析,根据指令替换数据,以及绑定相应的更新函数。
初始化
class MVue {
constructor (options) {
this.$el = options.el,
this.$data = options.data,
this.$options = options
// 如果存在template模板则开始编译
if (this.$el) {
// 创建解析器 Compile
new Compile(this.$el, this)
}
}
}
class Compile {
constructor (el, vm) {
this.vm = vm
// 判断是否是一个元素节点 如果是直接赋值 不是则获取值
this.el = this.isElementNode(el) ? el : document.querySelector(el)
}
isElementNode (node) {
// node.nodeType 等于1 是元素节点 等于3是文本节点
return node.nodeType === 1
}
}
创建文档碎片
因为每次匹配到进行替换时,会导致页面的回流和重绘,影响页面的的性能,所以需要创建文档碎片来进行缓存,减少页面的回流和重绘。
class MVue {
constructor (options) {
this.$el = options.el,
this.$data = options.data,
this.$options = options
// 如果存在template模板则开始编译
if (this.$el) {
// 创建解析器 Compile
new Compile(this.$el, this)
}
}
}
class Compile {
constructor (el, vm) {
this.vm = vm
// 判断是否是一个元素节点 如果是直接赋值 不是则获取值
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// 因为每次匹配到进行替换时,会导致页面的回流和重绘,影响页面的的性能
// 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
console.log(this.el);
const framgent = this.createFramgent(this.el)
// 再将文档碎片添加到根元素中然后渲染到页面
this.el.appendChild(framgent)
}
// 创建文档碎片
createFramgent (node) {
const framgent = document.createDocumentFragment(node)
// 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
// console.log(node.firstChild);
let children
while (children = node.firstChild) {
// 依次追加当文档碎片中
framgent.appendChild(children)
}
return framgent
}
isElementNode (node) {
// node.nodeType 等于1 是元素节点 等于3是文本节点
return node.nodeType === 1
}
}
递归编译模板
class MVue {
constructor (options) {
this.$el = options.el,
this.$data = options.data,
this.$options = options
// 如果存在template模板则开始编译
if (this.$el) {
// 创建解析器 Compile
new Compile(this.$el, this)
}
}
}
class Compile {
constructor (el, vm) {
this.vm = vm
// 判断是否是一个元素节点 如果是直接赋值 不是则获取值
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// 因为每次匹配到进行替换时,会导致页面的回流和重绘,影响页面的的性能
// 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
console.log(this.el);
const framgent = this.createFramgent(this.el)
// 开始进行模板的编译
this.compile(framgent)
// 再将文档碎片添加到根元素中然后渲染到页面
this.el.appendChild(framgent)
}
compile (framgent) {
const childNodes = framgent.childNodes
console.log(childNodes)
// 遍历全部的节点并判断是元素节点还是文本节点
// 将伪数组转为真数组
const childNodesArray = Array.from(childNodes)
childNodesArray.forEach(node => {
if(this.isElementNode(node)){
// 是元素节点
console.log(node);
} else {
//是文本节点
console.log(node);
}
// 多层嵌套需要递归 子元素
if(node.childNodes && node.childNodes.length){
this.compile(node)
}
})
}
// 创建文档碎片
createFramgent (node) {
const framgent = document.createDocumentFragment(node)
// 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
// console.log(node.firstChild);
let children
while (children = node.firstChild) {
// 依次追加当文档碎片中
framgent.appendChild(children)
}
return framgent
}
isElementNode (node) {
// node.nodeType 等于1 是元素节点 等于3是文本节点
return node.nodeType === 1
}
}
解析编译元素
class MVue {
constructor(options) {
this.$el = options.el,
this.$data = options.data,
this.$options = options
// 如果存在template模板则开始编译
if (this.$el) {
// 创建解析器 Compile
new Compile(this.$el, this)
}
}
}
class Compile {
constructor (el, vm) {
this.vm = vm
// 判断是否是一个元素节点 如果是直接赋值 不是则获取值
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// 因为每次匹配到进行替换是,会导致页面的回流和重绘,影响页面的的性能
// 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
console.log(this.el);
const framgent = this.createFramgent(this.el)
// 开始进行模板的编译
this.compile(framgent)
// 再将文档碎片添加到根元素中然后渲染到页面
this.el.appendChild(framgent)
}
compile (framgent) {
const childNodes = framgent.childNodes
// console.log(childNodes)
// 遍历全部的节点并判断是元素节点还是文本节点
// 将伪数组转为真数组
const childNodesArray = Array.from(childNodes)
childNodesArray.forEach(node => {
if(this.isElementNode(node)){
// 是元素节点
// console.log(node);
this.compileElement(node)
} else {
//是文本节点
// console.log(node);
this.compileText(node)
}
// 多层嵌套需要递归 子元素
if(node.childNodes && node.childNodes.length){
this.compile(node)
}
})
}
// 解析编译元素节点
compileElement (elementNode) {
// 编译元素 通过attributes获取元素节点的属性 里面包含name 和 value name为属性名字 value为属性值
const attributes = elementNode.attributes;
[...attributes].forEach(attr => {
// name 属性名 v-text v-html value 属性值 obj.name obj.age
const {name, value} = attr
if (this.isDirective(name)) {
// 是指令
// 解构 v-text v-html
const [,directive] = name.split('-')
const [dirName, eventName] = directive.split(':')
// 在compileUtils对象黎曼是否存这个指令 根据不同的指令处理不同的数据 text html model
compileUtils[dirName] && compileUtils[dirName](elementNode, value, this.vm, eventName)
// 依次标签中的属性
elementNode.removeAttribute('v-' + directive)
} else if (this.isEventName(name)) {
// 是事件
const [,eventName] = name.split('@')
// 根据不同的指令处理不同的数据 text html model
compileUtils['on'](elementNode, value, this.vm, eventName)
}
});
}
// 是否是指令
isDirective (name) {
// 以v-开头
return name.startsWith('v-')
}
// 是否是事件
isEventName (name) {
// 以@开头
return name.startsWith('@')
}
// 解析编译文本节点
compileText (textNode) {
// 编译文本
}
// 创建文档碎片
createFramgent (node) {
const framgent = document.createDocumentFragment(node)
// 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
// console.log(node.firstChild);
let children
while (children = node.firstChild) {
// 依次追加当文档碎片中
framgent.appendChild(children)
}
return framgent
}
isElementNode (node) {
// node.nodeType 等于1 是元素节点 等于3是文本节点
return node.nodeType === 1
}
}
解析编译文本
// 解析编译文本节点
compileText (textNode) {
// 编译文本
// 获取文本内容
const content = textNode.textContent
// 正则匹配
const reg = /\{\{(.+?)\}\}/
if(reg.test(content)) {
// 根据不同的指令处理不同的数据 text html model
compileUtils['text'](textNode, content, this.vm)
}
}
看到这里你可能会有点疑惑,compileUtils 是一个什么东西,用来处理什么的,compileUtils 是一个对象,主要是对不同的指令来做不同的处理,比如 v-text 是处理文本,v-html 是处理 html元素,v-model 是处理表单数据的....
compileUtils 对象里面有一个 updater 对象里面有对应的方法主要来更新视图的。
compileUtils 对象
const compileUtils = {
// 获取data中属性值值
getValue (value, vm) {
// 先以.分割为一个数组,然后使用reduce获取data中的属性值
return value.split('.').reduce((pre, next) => {
return pre[next]
},vm.$data)
},
text (node , value, vm) { // value可能是{{obj.name}} 可能是 obj.age
let val
if (value.indexOf('{{') !== -1) {
// 有{{ 说名是 {{obj.name}}
// 进行全局匹配
val = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm)
})
} else {
// obj.age
val = this.getValue(value, vm)
}
// 更新/替换数据
this.updater.textUpdata(node, val)
},
html (node, value, vm) {
const val = this.getValue(value, vm)
// 更新/替换数据
this.updater.htmlUpdata(node,val)
},
model (node, value ,vm) {
const val = this.getValue(value, vm)
this.updater.modleUpdata(node, val)
},
on (node, value, vm, eventName) {
// 获取回调函数
let fn = vm.$options.methods && vm.$options.methods[value]
node.addEventListener(eventName, fn.bind(vm), false)
},
// 里面存在对应指令的方法 用来更新视图
updater:{
textUpdata (node, value) {
node.textContent = value
},
htmlUpdata (node, value) {
node.innerHTML = value
},
modleUpdata (node, value) {
node.value = value
},
}
}
根据上面的流程图,我们已经完成了 new MVVM() -> Compile -> Updater 的步骤,实现了初始化页面视图数据的展示。下面我们继续完成从 Observer -> Dep -> Watcher -> Updater。
实现一个监听器 Observer
Observer 的作用就是用来对对象中的每个属性进行数据的劫持监听,设置 setter、getter 方法,如有变动,可以在setter 方法中拿到最新的值调用 依赖收集对象(Dep) 中的 notify() 通知订阅者。
我们可以利用 Object.defineProperty()来监听属性变动,通过 observe方法 将对象的数据进行递归遍历,包括子属性对象的属性,都加上 setter、getter,当我们给某个对象赋值或者是获取,就会触发 setter、getter方法,就能够监听到数据的变化了。
class Observer{
constructor (data) {
this.observe(data)
}
observe (data) {
// 不考虑数组 存在data 并且data为一个对象
if (data && typeof data === 'object'){
// 遍历对象的key
Object.keys(data).forEach(key => {
// 将对象 键传进入 值传进去
this.defineReactive(data, key, data[key])
})
}
}
defineReactive (obj, key, value) {
// 递归遍历
this.observe(value)
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get () {
return value
},
set: (newValue) => {
if ( newValue != value) {
// 如果直接赋值对象 也需要对这个对象进行数据监听
this.observe(newValue)
value = newValue
// dep.notify 通知watcher变化 更新视图
dep.notify()
}
}
})
}
}
依赖收集对象 Dep
通过上面的 Observer 我们可以对每个对象属性进行了数据的监听,并且当数据变化的时候通过 dep 的 notify方法 通知订阅者(Watcher)执行回调函数变更视图。
Dep作用:
创建一个数组存放订阅者,声明添加订阅者的方法(addSub)声明一个通知订阅之的方法(notify)
class Dep {
constructor () {
// 存放watcher
this.sub = []
}
// 添加订阅者
addSub (watcher) {
// 将订阅者存放到数组中
this.sub.push(watcher)
}
// 通知订阅者
notify () {
// 循环遍历 依次触发watcher绑定的回调函数
this.sub.forEach(watcher => watcher.update())
}
}
Watcher 订阅者
Watcher 作为连接 Observer 和 Compile 之间的桥梁,能够订阅并且收到每个属性变动的通知,执行绑定的相应的回调函数,从而更新视图。
Watcher 必须具备以下三点:
- 在自身
实例化往Dep里面添加自己。 - 自身必须要有一个
update()方法。 - 待属性值变动
dep.notify()通知时,能够调用自身的update()方法,并触发Compile中绑定的回调函数。
class Watcher{
constructor (vm, value, callback) {
// vm 里面存放着最新值
// value 数据变化的属性
// 绑定的回调函数
this.vm = vm
this.value = value
this.callback = callback
// 存放旧值
this.oldValue = this.getOldValue()
}
// 获取值 这里会间接触发 getter
getOldValue () {
Dep.target = this
const olaValue = compileUtils.getValue(this.value, this.vm)
Dep.target = null
return olaValue
}
update () {
// 更新操作 数据变化后 Dep会通过notify方法通知订阅者 然后订阅者更新视图
const newValue = compileUtils.getValue(this.value, this.vm)
if (newValue !== this.oldValue) {
this.callback(newValue)
this.oldValue = newValue
}
}
}
到此为止,我们已经编写好了所需要的 Observer、Watcher、Compiler、Dep。那么,剩下的就是应该如何把它们关联起来,形成一个回路,这才是我们实现响应式的目的。
当我们一打开页面时,首先执行的是 Observer 先将对象的每个属性进行数据的监听和劫持,然后再使用 Compile解析器 进行解析,第一大步:可以确定的是先 new Observer 再 new Compile
class MVue {
constructor(options) {
this.$el = options.el,
this.$data = options.data,
this.$options = options
// 如果存在template模板则开始编译
if (this.$el) {
// 1、对数据进行劫持
new Observer(this.$data)
// 2、创建解析器 Compile
new Compile(this.$el, this)
}
}
}
一开始就进行数据的监听,这里面会进行递归监听子属性的对象属性,在数据修改的时候触发 dep.notify方法,所以我们可以确定的是在递归结束之后 Object.defineProperty 之前 new Dep 来收集依赖和触发notify,并且我们可以在 getter 方法中触发 dep.addSub 方法 将 Watcher 实例 添加到数组中,这这样我们就收集了依赖对象。
class Observer{
constructor (data) {
this.observe(data)
}
observe (data) {
// 不考虑数组 存在data 并且data为一个对象
if (data && typeof data === 'object'){
// 遍历对象的key
Object.keys(data).forEach(key => {
// 将对象 键传进入 值传进去
this.defineReactive(data, key, data[key])
})
}
}
defineReactive (obj, key, value) {
// 递归遍历
this.observe(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get () {
// 如果存在订阅者 往收集依赖对象数组中添加订阅者
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newValue) => {
if ( newValue != value) {
// 如果直接赋值对象 也需要对这个对象进行数据监听
this.observe(newValue)
value = newValue
// dep.notify 通知watcher变化 更新视图
dep.notify()
}
}
})
}
}
最后,就只剩下 Complie 和 Watcher 应该如何进行关联了,在 Compile 中,我们是通过声明一个 compileUtils对象 来对不同的指令进行不同的处理,在这个对象里面,我们声明了一个 updater对象,里面包含着对各种指令的数据更新,比如textUpdate、htmlUpdate、modelUpdate 等等操作,所以,我们找到了更新数据的函数,可以确定的是在更新函数之前 new Watcher,当数据变化的时候触发订阅者实例(Watcher)来触发绑定的回调函数,从而更新视图。
const compileUtils = {
// 获取data中属性值值
getValue (value, vm) {
// 先以.分割为一个数组,然后使用reduce获取data中的属性值
return value.split('.').reduce((pre, next) => {
return pre[next]
},vm.$data)
},
// {{obj.name}}---{{obj.age}} 重新获取值,避免修改一个同时两个都变 重复渲染
getContentValue (value, vm) {
return value.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
})
},
text (node , value, vm) { // value可能是{{obj.name}} 可能是 obj.age
let val
if (value.indexOf('{{') !== -1) {
// 有{{ 说名是 {{obj.name}}
// 进行全局匹配
val = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
// ...args 打印出的三个分别是 当前匹配的值,匹配项在字符串中最小的为止,原始字符串
new Watcher(vm, args[1], () => {
this.updater.textUpdata(node, this.getContentValue(value, vm))
})
return this.getValue(args[1], vm)
})
} else {
// obj.age
val = this.getValue(value, vm)
}
// 更新/替换数据
this.updater.textUpdata(node, val)
},
html (node, value, vm) {
const val = this.getValue(value, vm)
new Watcher(vm, value, (newValue) => {
this.updater.htmlUpdata(node, newValue)
})
// 更新/替换数据
this.updater.htmlUpdata(node,val)
},
model (node, value ,vm) {
const val = this.getValue(value, vm)
new Watcher(vm, value, (newValue) => {
this.updater.modleUpdata(node, newValue)
})
this.updater.modleUpdata(node, val)
},
on (node, value, vm, eventName) {
// 获取回调函数
let fn = vm.$options.methods && vm.$options.methods[value]
node.addEventListener(eventName, fn.bind(vm), false)
},
updater:{
textUpdata (node, value) {
node.textContent = value
},
htmlUpdata (node, value) {
node.innerHTML = value
},
modleUpdata (node, value) {
node.value = value
},
}
}
经过上面分析,我们已经完成了数据变化到视图的更新,但是在表单中,还没实现视图变化到数据的更新,所以,我们还需要对表单数据进行分析。
/ 根据不同的指令处理不同的数据 text html model
const compileUtils = {
// 获取data中属性值值
getValue (value, vm) {
// 先以.分割为一个数组,然后使用reduce获取data中的属性值
return value.split('.').reduce((pre, next) => {
return pre[next]
},vm.$data)
},
getContentValue (value, vm) {
return value.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
})
},
// 视图 -> 数据
setValue (vm,value,inputValue) {
return value.split('.').reduce((pre, next) => {
if (typeof pre[next] !== 'object') {
// 如果不是是一个对象直接赋值
pre[next] = inputValue
}
// 是对象 直接取值
return pre[next]
},vm.$data)
},
text (node , value, vm) { // value可能是{{obj.name}} 可能是 obj.age
let val
if (value.indexOf('{{') !== -1) {
// 有{{ 说名是 {{obj.name}}
// 进行全局匹配
val = value.replace(/\{\{(.+?)\}\}/g, (...args) => {
// ...args 打印出的三个分别是 当前匹配的值,匹配项在字符串中最小的为止,原始字符串
new Watcher(vm, args[1], () => {
this.updater.textUpdata(node, this.getContentValue(value, vm))
})
return this.getValue(args[1], vm)
})
} else {
// obj.age
val = this.getValue(value, vm)
}
// 更新/替换数据
this.updater.textUpdata(node, val)
},
html (node, value, vm) {
const val = this.getValue(value, vm)
new Watcher(vm, value, (newValue) => {
this.updater.htmlUpdata(node, newValue)
})
// 更新/替换数据
this.updater.htmlUpdata(node,val)
},
model (node, value ,vm) {
const val = this.getValue(value, vm)
// 数据更新 -> 视图变化
new Watcher(vm, value, (newValue) => {
this.updater.modleUpdata(node, newValue)
})
// 视图变化 -> 数据更新
node.addEventListener('input',(e) => {
this.setValue(vm, value, e.target.value)
} ,false)
this.updater.modleUpdata(node, val)
},
on (node, value, vm, eventName) {
// 获取回调函数
let fn = vm.$options.methods && vm.$options.methods[value]
node.addEventListener(eventName, fn.bind(vm), false)
},
updater:{
textUpdata (node, value) {
node.textContent = value
},
htmlUpdata (node, value) {
node.innerHTML = value
},
modleUpdata (node, value) {
node.value = value
},
}
}
class Compile {
constructor (el, vm) {
this.vm = vm
// 判断是否是一个元素节点 如果是直接赋值 不是则获取值
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// 因为每次匹配到进行替换是,会导致页面的回流和重绘,影响页面的的性能
// 所以需要创建文档碎片来进行缓存,减少页面的回流和重绘
console.log(this.el);
let framgent = this.createFramgent(this.el)
// 开始进行模板的编译
this.compile(framgent)
// 再将文档碎片添加到根元素中然后渲染到页面
this.el.appendChild(framgent)
}
compile (framgent) {
const nodes = framgent.childNodes;
// console.log(childNodes)
// 遍历全部的节点并判断是元素节点还是文本节点
// 将伪数组转为真数组
// const childNodesArray = Array.from(childNodes)
[...nodes].forEach(node => {
if(this.isElementNode(node)){
// 是元素节点
// console.log(node);
this.compileElement(node)
} else {
//是文本节点
// console.log(node);
this.compileText(node)
}
// 多层嵌套需要递归 子元素
if(node.childNodes && node.childNodes.length){
this.compile(node)
}
})
}
// 解析编译元素节点
compileElement (elementNode) {
// 编译元素 通过attributes获取元素节点的属性 里面包含name 和 value name为属性名字 value为属性值
const attributes = elementNode.attributes;
[...attributes].forEach(attr => {
// name 属性名 v-text v-html value 属性值 obj.name obj.age
const {name, value} = attr
if (this.isDirective(name)) {
// 是指令
// 解构 v-text v-html
const [,directive] = name.split('-')
const [dirName, eventName] = directive.split(':')
// 是否存在这个指令对应的函数
compileUtils[dirName] && compileUtils[dirName](elementNode, value, this.vm, eventName)
// 依次标签中的属性
elementNode.removeAttribute('v-' + directive)
} else if (this.isEventName(name)) {
// 是事件
const [,eventName] = name.split('@')
compileUtils['on'](elementNode, value, this.vm, eventName)
}
});
}
// 是否是指令
isDirective (name) {
// 以v-开头
return name.startsWith('v-')
}
// 是否是事件
isEventName (name) {
// 以@开头
return name.startsWith('@')
}
// 解析编译文本节点
compileText (textNode) {
// 编译文本
// 获取文本内容
const content = textNode.textContent
// 正则匹配
const reg = /\{\{(.+?)\}\}/
if(reg.test(content)) {
compileUtils['text'](textNode, content, this.vm)
}
}
// 创建文档碎片
createFramgent (node) {
const framgent = document.createDocumentFragment(node)
// 循环依次将节点添加到文档碎片中 firstChild 包含空格换行符
// console.log(node.firstChild);
let children
while (children = node.firstChild) {
// 依次追加当文档碎片中
framgent.appendChild(children)
}
return framgent
}
isElementNode (node) {
// node.nodeType 等于1 是元素节点 等于3是文本节点
return node.nodeType === 1
}
}
class MVue {
constructor(options) {
this.$el = options.el,
this.$data = options.data,
this.$options = options
// 如果存在template模板则开始编译
if (this.$el) {
// 1、对数据进行劫持
new Observer(this.$data)
// 2、创建解析器 Compile
new Compile(this.$el, this)
}
}
}
数据代理 proxy
在vue中我们可以直接使用vm.msg获取到数据,其实是内部帮我们进行了数据的代理,相当于vm.$data.msg,所以我们也需要进行数据的代理。
class MVue {
constructor(options) {
this.$el = options.el,
this.$data = options.data,
this.$options = options
// 如果存在template模板则开始编译
if (this.$el) {
// 1、对数据进行劫持
new Observer(this.$data)
// 数据代理
this.proxy(this.$data)
// 2、创建解析器 Compile
new Compile(this.$el, this)
}
}
// 数据代理
proxy(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get () {
return data[key]
},
set (newValue) {
data[key] = newValue
}
})
}
}
}
梳理Vue响应式全过程
- 初始化 Vue实例 时,
Observer会遍历 data 中的所有属性,使用Object.defineProperty()方法将这些属性都转为getter/setter。并且创建依赖收集对象dep(一个属性一个Dep实例,用来管理该属性下的所有 Watcher,如果同一个属性在 DOM 节点中多次使用会创建多个 Watcher)。 - 在解析指令的时候,创建
Watcher实例,然后将更新的函数放到 Watcher实例 的回调上。 - 在初始化视图的时候,会读取属性值,触发
getter,将创建Watcher实例 添加到 dep 数组中。 - 当修改数据的时候,触发
setter,调用dep.notify方法,通知该 dep 内部的所有 Wacther 执行回调函数,重新render当前组件,生成新的虚拟 DOM树。 - Vue 框架会使用
diff算法遍历并对比新虚拟DOM树和旧虚拟DOM树中每个节点的差别,并记录下来,最后,加载操作,将所记录的不同点,局部修改到真DOM树上。
面试回答术语
谈谈你对 vue 的 MVVM 响应式原理的理解。
Vue 是采用数据劫持结合发布订阅模式的方式,通过 Object.defineProperty()来劫持各个属性的 getter,setter,在数据变动时发布消息给订阅者,然后触发相应的监听回调函数来更新视图。
需要 Observer 对数据进行递归遍历,包括子属对象的属性,都添加上 getter、setter,当读取值或者修改数据的时候,就会 触发getter 或 setter,就能够监听到数据变化。
Compile 解析指令,初始化页面将模板中的变量替换成数据,并将每个指令对应的节点绑定更新的回调函数,添加订阅者,一旦数据变动,订阅者收到通知,触发回调更新视图。
Watcher 是 Observer 和 Compile 之间的桥梁,首先,需要在自身实例时往 dep 中添加自己,其次,要有一个 update方法 更新,最后,数据变动时触发 dep.notify方法,调用自身的 update方法,触发 Compile 中绑定的回调函数。
MVVM 作为数据绑定的入口,整合Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析指令,最终利用Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model更新的双向绑定效果。
以上是Vue响应式的全部内容。需要获取源码可以点下面链接。