vuejs 的响应式就是通过数据劫持对每个 data 属性添加一个 Dep 对象,该 Dep 对象维护一个 Watcher 数组,data 发生改变时,通知所有的 Watcher 回调,每个组件都有一个默认的渲染 Watcher,它的回调就是 vm._update(vm._render())
方法,_render 内部调用 render 方法生成的当前组件的 VNode 对象,vm._update 内部调用__patch__进行 VNode 对比,并返回新的 el,vue 响应式的整体逻辑就是如此,下面我们详细了解一下:
1. 数据劫持 依赖搜集
先看 new Vue 执行的核心逻辑_init 方法里的核心代码:
代码语言:javascript复制Vue.prototype._init = function (options?: Object) {
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, "beforeCreate");
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
// 如果有el参数直接主动挂载,没有的话需要开发者手动挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
复制代码
其中,跟响应式相关的就是 initState 方法,他初始化了 props、data、watch、computed 等属性,查看 initState 方法,在初始化 data 时对整个 data 调用了 observe 方法,observe 方法主要通过递归调用为每个属性调用 defineReactive 方法,响应式的核心也在这个方法中:
代码语言:javascript复制export function defineReactive (
obj: Object,
key: string,
val: any
) {
const dep = new Dep()
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
复制代码
可以看到对每个属性使用 get 方法收集依赖,set 方法派发更新,其中 dep.depend()就是将当前的渲染 Watcher 加入到 dep 中,dep.notify()通知所有 Watcher 对象进行更新回调。
2. Watcher 是什么?
vuejs 中 Watcher 共分为 3 中:
- 渲染 Watcher,每个组件都有一个,在挂载组件 mountComponent 时 new 出来的 Watcher 对象; $mount 方法其实最终会调用 mountComponent,看下 mountComponent 方法:
export function mountComponent(
vm: Component,
el: ?Element,
): Component {
vm.$el = el;
callHook(vm, "beforeMount");
let updateComponent = () => {
vm._update(vm._render(), hydrating);
};
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
// 第一次调用,call mounted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, "mounted");
}
return vm;
}
复制代码
可以看到 mountComponent 的核心就是 new 了一个 Watcher 对象并将vm._update(vm._render(), hydrating);
作为他的回调执行,也就是说当当前组件的 data 发生改变,将会触发 vm._update 方法。
- userWatcher:通过 options.watcher 或者$watcher 方法定义的,这个比较简单,new Watcher 构造函数里会执行被监听的 data 的 getter 方法,可以将当前的 userWatcher 加入到该 data 的 dep 中,哪怕没有模版用到该 data,该 watcher 也会生效,因为已经收集过 watcher 对象了。
- computedWatcher:每个 computed 都会新建一个 Watcher(dirty = lazy = true)对象,第一次调用 computed 时在 computed 的 get 方法中通过调用该 Watcher 的 evalute 方法(dirty = false)然后执行 watcher.depend 方法,使得 computed 中依赖的响应式 data 的 Dep 既发布到该 computedWatcher,也发布到 renderWatcher 中,这样 computed 中依赖的响应式 data 改变就可以触发 renderWatcher 的回调更新 dom,不改变的话因为 watcher.dirty = false,不再去重新计算,从而实现数据缓存。
3. Render Watcher 的回调
上面说了当 data 发生改变时,会触发当前 render watcher 的回调,从上面代码可以看出 render watcher 的回调是执行了vm._update(vm._render())
语句,下面分析一下该语句:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
复制代码
_update 方法主要就是调用的patch方法,patch方法就是对比两个 VNode 并同时更新 dom,这个放到 vdom 的 diff 算法中详说。_update 方法需要传入当前组件的 VNode 对象,该对象就是通过_render 方法生成,看下_render 方法:
代码语言:javascript复制Vue.prototype._render = function (): VNode {
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
}
return vnode
}
}
复制代码
其实核心调用了 render 方法和一些其他的处理。这里还有一点需要注意的是,render watcher 的回调并不是立即执行的, 会加入一个异步队列,Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替,如下代码:。
代码语言:javascript复制update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
复制代码
data 发生改变时,dep 通知 watcher 进行 update,此时并不是立即执行,而是调用 queueWatcher 将该 watcher 放到一个异步队列中,nextTick 方法其实就是将一个回调方法放入这一轮回调的最后。
代码语言:javascript复制export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
}
复制代码
以上就是 vue 实现响应式的整个逻辑了。