Vue 响应式原理
作为写业务的码农,几乎不必知道原理。但是当你去找工作的时候,可是需要造原子弹的,什么都得知道一些才行。所以找工作之前可以先复习下,只要是关于 vue 的,必定会问响应式原理。
核心:
//es5
Object.defineProperty(obj,key,{get() {// 获取 obj[key] 的时候触发
},
set(val) {// obj[key] = 'xxx' 时触发
}
})
其实,只需要在修改 data 值的时候,需要触发一个回调方法,来更新与此值有关的数据,就完了。但是你面试的时候,那些大佬可不会觉得是这样的,需要把整个流程说明白才行。
简单的 Vue 响应式代码如下:
Vue.js:
class Vue {constructor(opts) {
this.opts = opts
if (opts.data) this.initData(opts.data);
if (opts.watch) this.initWatch(opts.watch);
if (opts.computed) this.initComputed(opts.computed);
if (opts.el) this.$mount(opts.el)
}
initData(data) {
// 让 data 上的数据被 get 的时候能够搜集 watcher,data 变为观察者
new Observable(data);
this.data = data
// 数据可在 this 上直接读取
Object.keys(data).forEach(key => {this.proxy(key)
})
}
initWatch(watch) {Object.keys(watch).forEach(key => {
//
new Watcher(this, key, watch[key])
})
}
initComputed(data) {Object.keys(data).forEach(key => {new Watcher(this, key)
this.proxy(key, {get: data[key],
})
})
}
$mount(el) {el = document.querySelector(el)
this.template = el.innerHTML
this.el = el
const fn = _ => {const nwTemp = this.parseHTML(this.template)
this.el.innerHTML = nwTemp
if(this.opts.mounted) {this.opts.mounted.call(this)
}
}
new Watcher(this, fn)
}
parseHTML(template) {return template.replace(/\{\{(.*?)\}\}/g, (str, str1) => {return this[str1.trim()]
})
}
// 将数据直接挂到 this 上,使用 getterSetter 代理,获取 vm.data 上的值
proxy(key, getterSetter) {
const vm = this
getterSetter = getterSetter || {set(value) {vm.data[key] = value
},
get() {return vm.data[key]
}
}
Object.defineProperty(vm, key, getterSetter)
}
}
// 给 data 作为观察者用的
class Observable {constructor(obj) {Object.keys(obj).forEach(key => {this.defineReact(obj, key, obj[key])
});
}
defineReact(obj, key, value) {const dep = new Dep()
Object.defineProperty(obj, key, {get() {
// 获取 data 里面信息的时候,能够搜集依赖,这些依赖都是 watcher 实例
if (Dep.target) {dep.append(Dep.target)
}
return value
},
set(val) {
value = val
// 修改 data 里面数据的时候,去通知已搜集的依赖更新
dep.notify()}
})
}
}
// 搜集与触发 wacher
class Dep {constructor() {this.subs = []
}
append(watcher) {
// 避免重复添加 watcher
// watcher 在 update 的时候,会重新获取值,此时不必再添加
if(this.subs.includes(watcher)) return
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {watcher.update()
})
}
}
Dep.target = null
class Watcher {
// keyOrFn 为字符串或者 function,opts.watch 为字符串,computed,$mount 中为 function
constructor(vm, keyOrFn, cb) {
this.cb = cb
if (typeof keyOrFn === 'string') {this.getter = function () {return vm[keyOrFn] // 例:initWatch 时,watch:{a(){}} ,a 为 data 里的数据,此处获取 vm.a 会触发 a 的收集
}
} else {this.getter = keyOrFn // 如果为 fn(computed 中)值为此函数的返回值}
this.value = this.get()}
get() {
Dep.target = this // 此 watcher 记录下来
const value = this.getter(this.vm)
// 运行 getter, 如果是 watch,获取一次 data 里的值,完成收集。如果是 computed,运行其方法,其方法中含有 data 的值时,会触发收集
// 当更新时,也会触发 getter
Dep.target = null
return value
}
update() {
// 更新
const oldValue = this.value
// 重新获取值
const value = this.get()
if (this.cb) {
// 如果是 watch, 会触发 watch 的函数
this.cb.call(this.vm, value, oldValue)
}
}
}
index.html
<div id="app">
<h2>Vue 响应式原理 </h2>
<br />
msg: {{msg}}
<br />
<p>num: {{num}}</p>
<p>num+ 1 计算属性值:{{add1}}</p>
<button>add</button>
</div>
<script src="./Vue.js"></script>
<script>
const app = new Vue({
el: "#app",
data: {
msg: "这是 msg",
num: 1
},
watch: {msg(newVal, oldVal) {console.log(newVal, oldVal);
}
},
computed: {add1() {return this.num + 1;}
},
mounted() {const btn = document.querySelector("button");
btn.onclick = _ => {this.num += 1;};
}
});
</script>