大家在利用Vue进行前端开发过程中,是否遇到过这样的问题:
{{scope.row.name}}
request(url).then(({ data }) => {
this.tableData = data && data.items;
this.tableData.forEach((itemData) => {
itemData.disable = true; //发现这句竟然不起作用,页面上没有显示item.name;
});
});
又或者遇到这种情况:
那么我们接下来就来扒一扒Vue的响应式是如何做的吧?
要了解Vue响应式原理,我们要从两个方向去了解,一个是Vue如何将数据渲染到页面上的;第二个就是Vue如何在数据变化的时候来更新视图的。
举一个例子:
{{name}}
那么我们接下来就了解下模版与数据是如何最终渲染成视图/DOM的?
/src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
可以看到,要使用Vue实例,必须通过new 关键字来生成,new之后,就调了vue内部方法_init(options)函数,那么我们跟进看下_init函数。
/src/core/instance/index.js 部分关键代码
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a flag to avoid this being observed
vm._isVue = true
// 合并配置
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// 主要将 Vue默认的option 与我们自定义的选项对象合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
// 一些初始化操作
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')
if (vm.$options.el) { // 进行挂载
vm.$mount(vm.$options.el)
}
}
由于渲染与构建方式跟平台有关,因此,vm.$mount分布在了很多地方,比如:/src/platforms/web/entry-runtime-with-compiler.js和/src/platforms/web/runtime/index.js;/src/platforms/weex/runtime/index.js
在Vue 2.0中,引入了服务端渲染(SSR),需讲编译器和运行时分开来,因此出现了两种版本: 独立构建和运行时构建。独立构建就是包括编译和运行时,比如
new Vue({
el: '#app',
template: ''
});
使用template时,需将template渲染为render函数,运行时再执行render函数;
运行时构建不包括编译阶段,比如
new Vue({
el: '#app',
render: h => h(App)
});
由于独立构建的$mount
过程实质与运行时构建的$mount
过程具有相似性,这里我们只对运行时的$mount
过程做一个梳理
/src/platforms/web/runtime/index.js 代码
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
可以看出,$mount
方法主要调了mountComponent方法。
/src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 运行时构建需传入render属性
if (!vm.$options.render) {
// 如果没有render属性,提示warning;
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 生命周期相关,暂时先跳过
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
这里主要是通过new Watcher来运行回调函数updateComponent,而updateComponent主要进行了两个操作:vm._render(), vm._update(),分别是生成vdom和将vdom转换成DOM元素,这里不做展开,下次在vDOM分析的时候再细展开。最终,在vm._isMounted = true时,代表整个mount过程已经完成。
看过Vue官方文档的同学,应该对数据驱动有所了解,数据驱动主要利用了Object.defineProperty()来产生一个访问器属性,即定义get和set函数,从而在获取和改变数据时,能插入自定义的一些操作。
那么我们通过VUE源码来了解下其中具体的机制:
首先,new Vue之后还是执行了this._init,_init函数里面,执行了一些初始化的函数:
// 一些初始化操作
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')
其中,initState就是处理一些相关数据的操作。
/src/core/instance/state.js
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
/* 这里主要是处理data对象里数据的逻辑 */
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
处理data: {}里面的数据主要是在initData中来做:
/src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
// observe data
observe(data, true /* asRootData */)
}
proxy(vm, _data
, key)主要是将this._data代理到vm实例上面来,使得我们可以直接通过this来访问data里面的数据;
最核心的部分就在于observe()函数了,observe()函数主要就做了下面一件事:
/src/core/observer/index.js
ob = new Observer(value);
return ob;
/src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 主要是walk函数
this.walk(value)
}
/src/core/observer/index.js
walk (obj: Object) {
// 对data里面每一个值执行了一次defineReactive()函数
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
那么我们主要来看看defineReactive函数干了什么?
在这之前,我们了解下Dep Class和Watcher Class,以及二者之间的关系。
watcher和dep是很典型的发布订阅模式,每个数据都有一个dep对象,这个dep对象记录这个数据所订阅的watcher;当该数据发生变化时,会触发每个watcher的更新,执行watcher上面的回调函数。具体通过代码来看看如何实现的:
dep.js
class Dep {
static target: ?Watcher;
id: number;
subs: Array;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
可以看到,dep对象只有两个属性,一个id,另一个是subs,这个主要是用于记录某个数据依赖的watcher有哪些的;
addSubs => 将某个数据依赖的watcher记录到dep.subs数组里面;
notify => 主要是当数据变化的时候,执行watcher的update方法;
depend => 每一个watcher都对应有很多个订阅者,所以该方法用于记录订阅某个watcher的dep有哪些。
这里值得一提的是:static target 用于记录全局唯一的watcher,也就是说 任何时刻,只能有一个watcher执行更新。
再来看watcher:
watcher.js
// watcher对象有很多属性
vm: Component;
expression: string; // 由外部传入,构造函数会调用该计算表达式或方法
cb: Function; // 回调函数
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array; // 记录依赖的数据的dep
newDeps: Array;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
// watcher构造函数
// 去掉了一些判断和特殊处理,只留了最重要的一部分代码
// watcher的构造函数其实主要是执行get()方法
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
vm._watchers.push(this)
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
// get方法
// 主要执行new Watcher(vm, ex, cb)时传入的计算表达式或函数ex; 可以看到在执行的时候,先把当前的watcher压栈,中间执行一些函数,后面再进行弹栈,保证每一次全局只有一个激活的Watch对象。
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
// 记录订阅该Watcher的deps;
addDep (dep: 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)
}
}
}
大概了解dep和watcher的概念之后,我们接着来看defineReactive函数:
defineReactive():
{
const dep = new Dep() // 每个数据都有一个dep对象
let childOb = !shallow && observe(val)
// 重点来了!!
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { // 定义访问器属性,在设置某个数据的时候,对其进行依赖收集,将当前的激活Watcher push到该数据对应的dep.subs中
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 当数据发生变化时,调用dep.notify(),将调用该数据的dep.subs中所有Watcher的update方法,也就是说,所有依赖于该数据的数据/视图都将发生变化。
dep.notify()
}
})
}
以上就是整个数据驱动的全过程,从数据到视图的绑定,到数据改变触发setter从而改变视图。这篇文章只是大概的理解和梳理,后面将对具体某块进行分析和理解,有不同的意见和建议希望大家可以提出。。