1 序
为什么是不一样的Vue2
响应式原理?
事情源于前两天给同事review
代码,发现了一个超出自己认知的Vue2
响应式现象,一个很有意思的现象,示例大家可以参考这个 沸点
Vue2
对象的响应式,大家普遍的认知是这样的,来源于官网
:
- 对于对象
简单来说就是:对象的响应式只能针对已存在的属性,
obj.newPro
和delete obj.oldPro
时Vue2
的响应式是拦截不到的,需要使用this.$set
或者Vue.set
方法才可以 - 对于数组
简单来说就是:数组的响应式一般是通过 7 个数组操作方法(被
Object.defineProperty
教育过的七个葫芦娃)来实现响应式,像是arr[0] = 1
、arr.length = 0
这种是没办法实现响应式的
但是,不一样的Vue2响应式原理
就会让你重新认识,发现上面的说法不是绝对的正确
接下来我们就通过一个简单的示例来从源码
层次去找答案,去解析Vue
从初始化到更新这两个过程都做了什么,当然,这篇文章主要是为了说明问题,源码做了一部分精简,全部拿出来的话太多了。
2 以下示例的现象和内部原理(执行过程)是什么?
大家可以先看代码,再看答案,当然也可以自己写个示例试一试,验证以下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id = "app">
<p>{{ form }}</p>
<ul>
<li v-for = "item in arr" :key = "item">{{ item }}</li>
</ul>
</div>
<script src = "../../dist/vue.js"></script>
<script>
const ins = new Vue({
data () {
return {
form: {
name: 'lyn'
},
arr: [1]
}
},
mounted () {
setTimeout(() => {
this.form.name = 'test'
this.arr[0] = 11
}, 2000)
}
})
ins.$mount('#app')
</script>
</body>
</html>
2.1 先上结论
相信很多同学的回答都是,页面初始完成,显示内容为:
然后两种以后执行定时函数,页面更新为入下:
如果你的回答是这个,那这篇文章你是值得一看的
当然你的回答所用的理论基础是没错的,但是答案为什么会错呢?理由很简单,对响应式整个执行过程的理解认知有那么一些瑕疵,至少前两天在看到这个现象时的我是这样的
2.1.1 现象
初始渲染结果为:
2s 钟后执行定时函数,页面更新为:
2.1.2 内部原理
示例代码被加载到浏览器以后,Vue
开始初始化,执行各种init
操作,其中最重要的就是实例化组件Watcher
、初始化数据、收集依赖(dep)、关联dep
和watcher
,当然中间穿插着生命周期方法的执行,比如beforeCreate
、created
、beforeMount
、mounted
,如果有子组件的话,beforemount
执行完了会去初始化子组件,直到自组件的mounted
执行完成,然后回来执行mounted
,唉,扯远了,不过不影响,问题点不在初始化,而是后面的更新。
页面渲染完 2s 后执行定时函数
首先执行 this.form.name = 'test'
,这里分了两步执行
-
首先
this.form
触发getter
得到value = { name: 'lyn' }
-
然后执行
value.name = 'test'
触发setter
,更新数据,现在this.form.name
的值为test
在setter
中触发dep.notify()
,通知watcher
执行自己的update
方法,update
方法会将watcher
自己 push 进一个队列(queue数组),然后调用nextTick
方法注册一个刷新队列(其实就是执行queue数组中的每个watcher的run方法)的函数,nextTick
方法将刷新队列的函数用一个箭头函数包裹起来然后保存到一个名叫callbacks
的数组中,接下来nextTick
会执行timerFunc
函数
timerFunc
函数利用浏览器的异步机制,将刷新callbacks
数组的函数注册为一个异步任务,当所有的同步任务执行完成以后就会去刷新刚才注册的队列,由于现在同步任务还没执行完,还差一个this.arr[0] = 11
,所以异步任务暂时先挂起
接下来执行this.arr[0] = 11
,这里也是分两步执行
-
首先
this.arr
会触发getter
得到value = [1]
-
然后,就没了,因为
this.arr[0] = 11
这样的写法,Vue2
的响应式核心Object.defineProperty
无法拦截,但是
但是很重要的一点,this.arr[0] = 11
这句代码确实是执行了,就意味着this.arr
现在的值真的是[11]
了,很重要,有疑问,带着疑问接着往下看
到这里所有的同步任务执行完成,开始执行刚才注册的异步任务
上面说的异步任务,就那堆回调函数,它最终做的事情很简单(纯粹),就是执行watcher.run
方法,watcher.run
方法执行watcher.get
方法,get
方法负责执行updateComponent
方法,这个方法是初始化组件时,实例化watcher
时传递给watcher
的,执行updateComponent
方法时会先执行vm._render
函数生成新的vdom
,注意,生成新的vdom
时需要去读取vue
实例也就是this
上的各个属性,当然只读取模版中用到的属性,我们的示例中就是this.form
和this.arr
,读到这里是不是已经有点明白了?
虽然this.arr[0] = 11
没办法触发Vue2
的响应式机制,但是它却可以更改this
的属性值,所以,就在页面上看到了之前不理解的一幕
生成新的vdom
,updateComponent
执行vm._update
方法,调用patch
方法,对比新旧vdom
,找出发生变化的dom
节点,然后更新
看到这里是不是已经有点明白了,是不是也有点不一样的想法了?比如:
我就想通过this.arr[idx] = xxx
更新数组元素,不想用this.splice
之类的方法,这时只需要再带一个可以触发setter
的有效操作即可,当然,在实际中还是不要这么写,就当是一个比较有意思的黑魔法
吧,毕竟万一你写了,给别人带来不好体验就不太好了。
看到这里不知道是直接就明白了?还是有点懵,可以接着往下看,从源码中找答案,代码经过精简,有详细的注释,看完以后,自己回想一下过程,再回来对照着这个结论看,一定会有很大的收获的。
2.2 从源码中找答案
这一节会分析整个示例代码的执行过程,当内容被加载到浏览器以后,
Vue
源码的执行过程是这样的:
-
src/core/instance/index.js
/** * Vue 构造函数,执行初始化操作 */ function Vue (options) { this._init(options) }
-
src/core/instance/init.js
/** * 执行各种初始化操作,比如: * 最重要的给数据设置响应式(这部分内容就不展开了,否则太多了)然后执行实例的 $mount 方法 */ 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') if (vm.$options.el) { vm.$mount(vm.$options.el) } }
-
src/platforms/web/runtime/index.js
/** * $mount, 负责执行 mountComponent 方法 */ Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
-
src/platforms/web/entry-runtime-with-compiler.js
/** * 不用管这个,和问题无关,这里其实重写了 $mount 执行了编译模版的动作, * 最后生成 render 函数,这部分内容被我删了 */ const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { return mount.call(this, el, hydrating) }
-
src/core/instance/lifecycle.js
/** * mountComponent 方法 * 很重要的几点 * 1、定义组件的 updateComponent 方法 * 2、实例化组件 watcher,并将 updateComponent 方法传递给 watcher * * watcher 后面会在自己的 run 方法中调用 get 方法,get 方法会负责执行这个 updateComponent 方法,重新生成新的 vdom,watcher 相关看下面的 watcher 部分 */ export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { callHook(vm, 'beforeMount') let updateComponent = () => { // vm._render 执行后会生成新的 vdom,vm._update 方法会调用 patch 方法,对比新旧 dom,更新视图 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 // 调用组件实例的 mounted 方法 if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm } /** *负责执行各种各样的生命周期方法,比如 mounted */ export function callHook (vm: Component, hook: string) { // handlers = vm.$options.mounted const handlers = vm.$options[hook] const info = `${hook} hook` if (handlers) { for (let i = 0, j = handlers.length; i < j; i++) { invokeWithErrorHandling(handlers[i], vm, null, vm, info) } } } /** * 负责执行 patch 方法,分为首次渲染和再次更新 */ Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } }
-
src/core/util/error.js
// 执行 声明周期方法 export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string ) { // 真正执行声明周期方法的地方 return args ? handler.apply(context, args) : handler.call(context) }
走到这里,mounted 方法已经执行完成,页面也已经渲染完成,接下来就是执行 2s 后的定时函数,更新数据,通过响应式拦截触发视图更新
```javascript
/**
* mounted 方法中的定时函数
*/
setTimeout(() => {
this.form.name = 'test'
this.arr[0] = 11
}, 2000)
```
接下来分析定时器注册的回调函数执行过程是什么
-
src/core/observer/index.js
执行定时函数时会触发下面的
getter
和setter
,比如:this.form.name = 'test'
会先执行this.form
触发getter
得到value = { name: 'lyn' }
,再执行value.name = 'test'
触发setter
更新name
属性,然后执行dep.notify()
/** * 这个其实就是数据响应式的核心了,拦截了示例对象上的各个属性,数据读取时执行 get,设置数据时执行 set */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { 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 /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // 通知 watcher 去执行 update 方法 dep.notify() } }) }
-
src/core/observer/dep.js
/** * A dep is an observable that can have multiple * directives subscribing to it. * * dep 负责收集依赖,通知 watcher 更新 * 这里只保留了 notify(通知watcher更新) 和 构造函数 */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } // 通知通知执行 update 方法 notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { // 这个其实就是 watcher 的 update 方法 subs[i].update() } } }
-
src/core/observer/watcher.js
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. * * 一个组件对应一个 watcher 实例(渲染watcher),实例化过程是在 mountComponent 方法中做的,也就是执行 vm.$mount 之后 * 在同步执行过程中最重要的就是将当前 watcher 实例 push 到一个 watcher 执行队列中, * 待将来执行,通过一个 Promise.resolve().then() 来执行 run 方法,从而执行 updateComponent 方法 */ export default class Watcher { constructor ( vm: Component, // updateComponent expOrFn: string | Function, // noop cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm // 很重要,就是租价更新方法,updateComponent this.getter = expOrFn } /** * Evaluate the getter, and re-collect dependencies. * * 由 this.run 执行,执行 updateComponent 方法,生成新的 vdom,然后执行 patch,更新视图 */ get () { // Dep.target = watcher实例,这里让 dep 和 watcher 关联 pushTarget(this) let value // 组件实例 const vm = this.vm try { // 这里其实执行的是这个 updateComponent 方法: // let updateComponent = () => { vm._update(vm._render(), hydrating) } value = this.getter.call(vm, vm) } catch (e) { } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() } return value } /** * 将 watcher 实例加入 watcher 队列 */ update () { queueWatcher(this) } /** * Scheduler job interface. * Will be called by the scheduler. * * * 这里其实通过 timerFunc 来调用,借用了浏览器的异步机制(Promise) * 执行 this.get 方法,让 get 执行 updateComponent */ run () { const value = this.get() } }
-
src/core/observer/scheduler.js
/** * 将 watcher push 进 queue 数组,然后注册一个回到函数,在将来[Promise.resolve().then()]来执行这些 watcher 的 run 方法 */ export function queueWatcher (watcher: Watcher) { queue.push(watcher) // 这里其实就是注册回调函数 flushSchedulerQueue nextTick(flushSchedulerQueue) } /** * 负责让队列中所有的 watcher 执行自己的 run 方法. */ function flushSchedulerQueue () { for (index = 0; index < queue.length; index++) { watcher = queue[index] watcher.run() } }
-
src/core/util/next-tick.js
如果宏任务、微任务不太理解,可以看 这篇文章
/** * 很重要的几个点 * * 定义 nextTick 方法,将回调函数全部放到一个 callbacks 数组,然后执行 timerFunc * 定义 timerFunc,其实就是就是利用了浏览器的异步任务机制,这里选了 Promise 微任务,Vue首选就是Promise * Promise.resolve().then() 注册的回调函数就是刷新刚才存储的 queue 队列(数组), * 执行 watcher.run(),触发 updateComponent,这里很关键的一点是理解宏任务、微任务, * 当宏任务都执行结束后,比如示例中的整个setTimeout 回调,就会执行这里注册的微任务,Promise.resolve().then() */ const callbacks =[] let pending = false // nextTick 就是用一个箭头函数将 flushSchedulerQueue 函数包裹然后放到 callbacks 数组 export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { cb.call(ctx) }) if (!pending) { pending = true // 就是执行一个 异步 方法,首选 Promise timerFunc() } } // 执行一个立即就绪 Promise,Promise 回调负责执行 flushCallbacks 函数 let timerFunc = () => { Promise.resolve().then(flushCallbacks) } /** * 执行 callbacks 数组中的 () => flushSchedulerQueue.call(ctx),而最终会放 watcher 去执行自己的 run 方法, * run 方法执行 get 方法,get 方法中最终会调用组件的 updateComponent 方法,然后执行 render 重新生成 vnode,然后执行 * patch 过程,最终更新 dom */ function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
-
vm._render 函数执行后生成的内容是什么?
/** * 可以看到,生成 vdom 的时候,会去组件实例对象上读取响应的属性值,比如我们这里的,this.form,this.arr * 理解这里很重要,为什么我们的视图会被有效更新?是因为 vm.arr 确实被更新成了 [11] */ function anonymous() { with(this) { return _c( 'div', {attrs:{"id":"app"}}, [ // this.form _c('p',[_v(_s(form))]),_v(" "), _c( 'ul', _l( // this.arr (arr), function(item) { return _c('li',{key:item},[_v(_s(item))]) } ), 0 ) ] ) } }
3 升华 ?
读到这里,自己在梳理一遍思路,再回头看一遍前面的结论,是不是就有种豁然开朗的感觉??