Vue响应式原理
一、简述
响应式原理即数据变化的同时改变界面,Vue2.x通过Object.defineProperty()的getter和setter函数结合观察者模式实现,Vue3.x通过ES6的proxy代理劫持对象实现。
二、Object.defineProperty(obj, key, {})
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。其中get()方法为获取属性值,set()方法为设置属性值。
// vue通过此方法对data的每个属性进行数据劫持
function convert (obj) {
Object.keys(obj).forEach(key) {
let internalValue = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log('getter log' + internalValue);
return internalValue
},
set(newValue) {
internalValue = newValue
console.log('setter log' + internalValue);
}
})
}
}
三、观察者模式
什么是观察者模式?它分为注册环节跟发布环节。
比如我去买芝士蛋糕,但是店家还没有做出来。这时候我又不想在店外面傻傻等,我就需要隔一段时间来回来问问蛋糕做好没,对于我来说是很麻烦的事情,说不定我就懒得买了。
店家肯定想要做生意,不想流失我这个吃货客户。于是,在蛋糕没有做好的这段时间,有客户来,他们就让客户把自己的电话留下,这就是观察者模式中的注册环节。然后蛋糕做好之后,一次性通知所有记录了的客户,这就是观察者的发布环节。
// 观察者类
class Dep {
constructor() {
this.subscribers = new Set()
}
depend(activeUpdate) {
if (activeUpdate) {
this.subscribers.add(activeUpdate)
}
}
notify() {
this.subscribers.forEach(subscriber => subscriber())
}
}
四、原理解析
Vue官网响应式原理图解:
1.组件初始化时,将data属性reactive化,即加上getter/setter函数,getter注册函数,setter通知函数,这个过程称为数据劫持
2.在每个组件compile时,都会创建一个Watcher对象,在Watcher对象里面会触发属性的getter函数收集依赖,即收集update函数
3.当数据发生变化时,触发setter函数,调用dep.notify()函数通知Watcher更新相关组件。
ps:一个组件对应一个Watcher
class Dep {
constructor() {
this.deps = new Set()
}
depend() {
this.deps.add(Dep.target)
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
class Watcher {
constructor(vm, key, cb) {
this.vm = vm // vue实例
this.key = key // data的key
this.cb = cb // 数据更新回调函数
Dep.target = this // 将Dep.target指向this,用于跟Dep类通信
this.vm[this.key] // 触发get函数收集依赖
Dep.target = null
}
update() {
this.cb.call(this.vm, this.vm[this.key]) // 执行更新界面方法
}
}
class Compile {
constructor(el, vm) {
this.$vm = vm
this.$el = el
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el)
this.compileElement(this.$fragment)
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el) {
let fragment = document.createDocumentFragment()
let child
while (child = el.firstChild) {
fragment.appendChild(child)
}
return fragment
}
compileElement() {
// 编译元素代码
let childNodes = el.childNodes
Array.from(childNodes).forEach((node)=> {
let text = node.textContent
// 表达式文本
// 识别{{}}中的数据
let reg = /\{\{(.*\}\})/
// 按元素节点类型编译
if (this.isElementNode(node)){
this.compile(node)
}else if(this.isTextNode(node) && reg.test(text)){
// 文本 并且有个{{}}
this.compileText(node, RegExp.$1)
}
// 遍历编译子节点
if (node.childNodes && node.childNodes.length){
this.compileElement(node)
}
})
}
compile(node) {
let nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach((attr) => {
// 指令以v-xxx 命名
// 如<span v-text = "content"></span>
let attrName = attr.name // v-text
let exp = attr.value // content
if (this.isDirective(attrName)) {
let dir = attrName.substring(2) // text
// 普通指令
this[dir] && this[dir](node, this.vm, exp)
}
if (this.isEventDirective(attrName)) {
let dir = attrName.substring(1) // text
this.eventHandle(node, this.vm, exp, dir)
}
})
}
compileText(node, exp){
this.text(node, this.vm, exp)
}
isDirective(attr) {
return attr.indexOf("v-") == 0
}
isEventDirective(dir){
return dir.indexOf('@') === 0
}
isElementNode(node) {
return node.nodeType == 1
}
isTextNode(node) {
return node.nodeType == 3
}
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
let val = vm.exp
node.addEventListener("input", (e)=>{
let newValue = e.target.value
vm[exp] = newValue
val = newValue
})
}
update(node, vm, key, dir) {
let updater = this[dir + 'Updater']
updater && updater(node, vm, vm[key]) // 执行更新界面方法
new Watcher(vm, key, function(value) {
updater && updater(node, value) // 保存更新界面方法
})
}
eventHandler(node, vm, exp, dir){
let fn = vm.$options.methods && vm.$options.methods[exp]
if (dir && fn) node.addEvent(dir, fn.bind(vm), false)
}
textUpdater(node, value){
node.textContent = value
}
htmlUpdater(node, value) {
node.innerHTML = value
}
modelUpdater(node, value){
node.value = value
}
}
function isObject(obj) {
return typeof obj === 'object' && !Array.isArray(obj) && obj !== null && obj!== undefined
}
function observe(obj) {
if (!isObject(obj)) {
throw new TypeError()
}
Object.keys(obj).forEach(key => {
const dep = new Dep()
let internalValue = obj[key]
if (isObject(internalValue)) observe(internalValue)
Object.defineProperty(obj, key, {
get () {
dep.depend(Dep.target)
return internalValue
},
set(newValue) {
const isChange = internalValue !== newValue
if (isChange) {
internalValue = newValue
dep.notify()
}
}
})
})
}