由new Vue的Vue都经历了什么,我们new Vue()的构造函数中会执行this.__init(options)
函数,该函数上一章中也有查看代码,方便大家观看,再次贴一下代码:
Vue.prototype._init = function (options?: Object) {
vm._isVue = true
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
// beforeCreate 中获取不到 数据信息,包括data,props,inject等数据信息
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
// created 钩子里才能获取到 data,props,inject等数据信息
callHook(vm, 'created')
// 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
在 initState(vm)
中,对data() { return {} }
数据进行了数据劫持。下面将对Vue中数据劫持的过程进行个mini
写法,以便大家进行理解。
数据劫持流程
observe
首先执行observe函数,observe函数会判断该对象是否已经被劫持过,如果已经劫持过,那么直接返回被劫持的对象,如果没有被劫持过则返回一个Observe对象。
function observe (value) {
if(value.__ob__) {
return value.__ob__
}
const ob = new Observer(value);
return ob;
}
Observe
Observe构造函数中会给数据增加__ob__
属性,以便observe方法中能够判断数据是否已经被劫持过,然后调用defineReactive
函数,进行数据劫持。
class Observer {
constructor(value) {
value.__ob__ = this;
// 简化写法,默认都是对象形式,先省略数组模式
this.walk(value);
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
defineReactive
defineReactive函数中通过Object.defineProperty
函数进行数据的set/get
方法劫持,当数据被使用时进行依赖收集,数据被修改时进行视图的更新。
function defineReactive (obj, key) {
const dep = new Dep ();
let value = obj[key];
Object.defineProperty(obj, key , {
enumerable: true,
configurable: true,
get: function () {
if(Dep.target) {
dep.depend();
}
return value;
},
set: function (newVal) {
value = newVal;
dep.notify();
}
})
}
Dep
Dep用来收集Watcher对象和通知进行视图更新。
class Dep {
constructor() {
this.subs = [];
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
console.log('更新视图啦~~~');
}
}
addSub (sub) {
this.subs.push(sub)
}
}
Watcher
addDep
方法用于将当前watcher对象收集到Dep对象中。
class Watcher {
addDep (dep) {
dep.addSub(this)
}
}
存在的问题是如何解决的
我们知道Object.defineProperty
是存在缺陷的,例如不能监听到对象里新增属性的情况;不能直接监听到对象嵌套对象的情况;针对数组push、unshift
等修改数组数据的方法监听不到等问题。Vue是如何解决这些问题的?下面会一一解答:
对象中新增数据监听不到
Vue中提供了$set
方法,当对象中新增属性时,可以调用该方法保证数据的更新。
嵌套对象问题
Vue中在defineReactive
函数中的set/get
方法中都调用的observe
方法,确保对象中属性为对象这种情况能覆盖监听到。
Vue中
数组中方法问题
Vue中劫持了数组类型的__proto__
,重写了[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
这些方法,以便在调用这些方法时视图能正常更新。
视图重复更新
在Watcher.addDep
方法中,记录已经收集过的dep对象,通过dep.id对比,防止同一个watcher对象重复收集dep对象,在通知watcher对象update视图的方法中同样通过判断watcher对象是否已经存在来防止重复更新视图。
Watcher
示例代码
addDep (dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update() {
queueWatcher(this)
}
queueWatcher
方法 示例代码
const has = {};
function queueWatcher (watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
setTimeout(() => {
console.log('视图更新了~~');
}, 0)
}
}
除了上述优化点外,Vue还做了其他优化点,大家可以去Vue源码中查看。
上述mini-vue-data
代码已上传到github mock-vue2-data-proxy中,大家有需要可以自行查看。