接上篇,我们以具体demo 去分析响应式原理
demo git地址:
[email protected]:Alan-W/shopping-website-used-nodejs.git
复制代码
main.js:
import Vue from '../src/platforms/web/entry-runtime-with-compiler.js' // 引入vue源码入口
import App from './App.vue'
var app = new Vue({
el: '#app',
render: h => h(App)
});
复制代码
App.vue:
<template>
<section>
<div>rawType: {{ rawType }}</div>
<div>object: {{ object.key }}</div>
<div>array:
<p v-for="(item, index) in array" :key="index">{{ index }} :{{ item.key }}</p>
</div>
<div>操作:
<button @click="handlehangeRawBtn" name="button">make rawType ++</button>
<button @click="handleChangeObject">add key to object</button>
</div>
<section>
<p>children: </p>
<child1 :raw="rawType" />
</section>
</section>
</template>
<script>
import child1 from './components/child1.vue'
export default {
name: 'demo',
components: { child1 },
data() {
return {
rawType: 111,
object: {
key: 1
},
array: []
}
},
mounted() {
console.log('App mounted this', this)
},
methods: {
handlehangeRawBtn() {
console.log('this in handlehangeRawBtn', this)
this.rawType++
},
handleChangeObject() {
this.$set(this.object, 'key2', 2)
},
}
}
</script>
复制代码
运行yarn serve, 在生成的main.js 中我们可以看到,rollup 插件已经将我们的template 组件转化为了变量:
var App = {
render: function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('section',[_c('div',[_vm._v("rawType: "+_vm._s(_vm.rawType))]),_vm._v(" "),_c('div',[_vm._v("object: "+_vm._s(_vm.object.key))]),_vm._v(" "),_c('div',[_vm._v("array: \n "),_vm._l((_vm.array),function(item,index){return _c('p',{key:index},[_vm._v(_vm._s(index)+" :"+_vm._s(item.key))])})],2),_vm._v(" "),_c('div',[_vm._v("操作:\n "),_c('button',{attrs:{"name":"button"},on:{"click":_vm.handlehangeRawBtn}},[_vm._v("make rawType ++")]),_vm._v(" "),_c('button',{on:{"click":_vm.handleChangeObject}},[_vm._v("add key to object")])]),_vm._v(" "),_c('section',[_c('p',[_vm._v("children: ")]),_vm._v(" "),_c('child1',{attrs:{"raw":_vm.rawType}})],1)])},
staticRenderFns: [],
name: 'demo',
components: { child1: child1 },
data: function data() {
return {
rawType: 111,
object: {
key: 1
},
array: []
}
},
mounted: function mounted() {
console.log('App mounted this', this);
},
methods: {
handlehangeRawBtn: function handlehangeRawBtn() {
console.log('this in handlehangeRawBtn', this);
this.rawType++;
},
handleChangeObject: function handleChangeObject() {
this.$set(this.object, 'key2', 2);
},
}
};
复制代码
child1组件也是一样的,可以在demo 中查看具体代码
当我们在main.js中 调用new Vue(...options)的时候,代码首先走到Vue的构造函数,去创建第一个vm实例,执行this._init
因为初始化我们并未传入任何data, props 等数据,这里我们只需要关注 vm.$mount(vm.$options.el) 这句,最终会调用到mountComponent
mountComponent:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
...
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) // 这里比较重要, 会最终触发到render函数的调用
}
}
vm._watcher = new Watcher(vm, updateComponent, noop) // 给 render watcher 赋值
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
}
复制代码
在mountComponent中最终要的是对render watcher进行了赋值
vm._watcher = new Watcher(vm, updateComponent, noop)
复制代码
这里传入的参数需要我们关注,尤其是第二个参数,看下render watcher的赋值 会产生的功效:
vm._watcher = new Watcher(vm, updateComponent, noop)触发Wather.get 的调用
这里大家可以关注下右侧的调用栈:(以firstVm代表入口如第一个被创建的Vue实例)
调用栈模拟
调用栈模拟(一):
1| firstVm._watcher.get() ->
2| pushTarget() // Dep.target === firstVm._watcher ->
3| updateComponent() // new Watcher的第二个参数 -> (这里需要注意的是firstVm._watcher.get并未执行完成
4| vm._update(vm._render(), false) -> // 先调用vm._render() 去创建firstVm 的 VNode
5| // 以下都在vm._update“现场”下:
6| vm._render() ->
7| options.render() // h(App) -> vm.$ceraetElement() ->
8| createElement$1() ->
9| _createElement() ->
10| createComponent() // 里面有句 Ctor = baseCtor.extend(Ctor); ->
11| // 以下都在createComponent的“现场”下:
12| /* extendOptions 其实就是我们的App.vue
13| extendOptions = {
14| name: "demo",
15| components: { child1: { ... } },
16| data(),
17| ...
18| }
19| */
20| Vue.extend(extendOptions)
21| /*
22| const Sub = function VueComponent(options) {
23| this._init(options);
24| };
25| Sub.prototype = Object.create(Super.prototype); // 子类的原型属于父类原型函数的实例
26| Sub.prototype.constructor = Sub; // 定义子类的构造函数
27| 定义了继承类的构造函数Sub,继承于Vue, 在之后子组件创建实例时都会用到这个构造函数,
28| 继承类Sub Vue, 对props 和 computed 建立了代理
29| */
30| // 回到 createComponent 的场景
31| // return 了App组件的VNode
31| // 一路返return _update 接下来的代码
33| patch() ->
34| createElm() ->
35| createComponent() ->
36| init() ->
37| createComponentInstanceForVnode() ->
38| // 该函数最后一句return new vnodeComponentOptions.Ctor(options);
39| // vnodeComponentOptions.Ctor 其实就是Vue.extend 的返回值,继承类Vue
40| VueComponent() ->
41| Vue._init() -> // 这个时候就真正去初始化App.vue 组件内部的数据了
复制代码
创建App.vue 的过程是一样的,但这里我们需要关注一些列数据的初始化,App.vue 未接收props, 我们从initData 开始看
对于initData 我们关注proxy 和 observe 两处调用
proxy 在vm 实例上对data中返回的每个key 建立了代理
这种建立代理的方式无论是之后的赋值,还是render函数的调用,都会直接用到
复制代码
看下observe的调用:
class Observer 就是创建(目标/被观察者/发布者)的一系列过程:
将所有数据转化为可reactive后,这个时候都没有涉及到任何收集依赖,也并未建立Observer, Wacther,Dep的关系,那我们继续我们函数的调用,回到 “调用栈模拟(一)第36行init函数的“现场””
调用栈模拟(二):
42| Vue.$mount()
43| mount()
44| mountComponent()
45| /* AppVm._watcher = new Watcher(vm, App.render, false) */
46| AppVm._watcher.get()
47| pushTarget() // Dep.target = AppVm._watcher
48| App.render() // 接下来就是收集依赖的一系列过程
复制代码
App.vue组件的render函数如下:
{
render: function () {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c('section', [
_c('div',
[_vm._v("rawType: " + _vm._s(_vm.rawType))]
),
_vm._v(" "),
_c('div',
[_vm._v("object: " + _vm._s(_vm.object.key))]
),
_vm._v(" "),
_c('div', [
_vm._v("array: \n "),
_vm._l((_vm.array),
function (item, index) {
return _c('p',
{ key: index },
[_vm._v(_vm._s(index) + " :" + _vm._s(item.key))]
)
})
], 2
),
_vm._v(" "),
_c('div', [_vm._v("操作:\n "),
_c('button', {
attrs: {
"name": "button"
},
on: {
"click": _vm.handlehangeRawBtn
}
},
[_vm._v("make rawType ++")]),
_vm._v(" "),
_c('button',
{
on: {
"click": _vm.handleChangeObject
}
},
[_vm._v("add key to object")]
)
]),
_vm._v(" "),
_c('section',
[_c('p',[_vm._v("children: ")]),
_vm._v(" "),
_c('child1', {
attrs: {
"raw": _vm.rawType
}
})
],
1)
])
}
}
复制代码
其实看到模板函数的时候,我们就能知道是怎么收集依赖了,render函数中获取vm上的各个变量,会触发我们在_init 函数中建立的Observer数据劫持,以_vm.rawType 为例:
走到了getter 中:rawType 的depId 刚好为6, 我们以 depId6 表示rawType对应的dep 实例
调用栈模拟(三):
49| dep.depend() // depId6.depend
50| /* depend() {
51| if (Dep.target) {
52| Dep.target.addDep(this); // Dep.target 就是App组件对应的render watcher(AppVm._watcher)
53| }
54| } */
55| addDep() // AppVm._watcher.addDep
56| /*
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); // AppVm._watcher 的依赖有depId6,建立watcher 和dep 的关系
}
}
}
*/
57| addSub() // depId6.addSub
/*
addSub (sub: Watcher) {
this.subs.push(sub) // depId6的订阅者中有当前的render watcher(AppVm._watcher),建立dep 和watcher 的关系
}
*/
58| pushTarget() // Dep.target = AppVm._watcher
59| App.render() // 接下来就是收集依赖的一系列过程
复制代码
其实到这里收集依赖的过程已经完成了,其余key 的数据也是一样的,以我们的例子来说,每个key对应的dep.subs中都是[Watcher], 这个Watcher 就是AppVm._watcher, 而AppVm._watcher的deps中包含了模板渲染中用到的所有变量的dep 但我们的调用栈并未结束,初始化完child1组件后,我们看下调用栈:
从调用栈也可以看出,组件是以深度优先去创建所有VNode,调用完所有代码后我们的页面也就渲染出来了
有了所有watcher 和 observer 的关系后触发更新就很容易理解:
depId6.notify 会遍历所有的订阅者,调用他们提供的update 方法
这里只有AppVm._watcher 一个订阅者,它的 update方法:
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
复制代码
以批处理的方式去调用watcher 的get 函数,其实最终触发的就是render 函数的调用, 重新调用render 函数,也就重新触发getter函数的调用,这个过程也就重新更新了watcher 中的deps 数据。(做到了观察者和订阅者数据的同步)
篇幅很长,之后会有视频,可能更方便看整个过程。
Vue2 源码分析(一): 入口platforms/web/runtime/index.js & initGlobalAPI