vue 源码分析
核心
Vue 响应式的核心是利用 Object.defineProperty()这个方法进行数据劫持和观察者模式进行数据响应式的。Object.defineProperty()这个方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。具体用法如下:
它有三个参数,第一个是 object,它代表要定义属性的对象。;第二个是 prop,它代表目标对象的属性值;第三个参数是 descriptor,它代表要定义或修改的属性描述符。
响应式用法:
1const person = {};
2Object.defineProperty(person, 'name', {
3 get: function () {
4 console.log('获取到name了');
5 },
6 set: function (val) {
7 console.log('设置了name为' + val);
8 },
9});
10person.name; //获取到name了
11person.name = 'h7ml'; //设置了name为h7ml
观察者模式
什么是观察者模式?它分为注册环节跟发布环节。
比如我去买芝士蛋糕,但是店家还没有做出来。这时候我又不想在店外面傻傻等,我就需要隔一段时间来回来问问蛋糕做好没,对于我来说是很麻烦的事情,说不定我就懒得买了。
店家肯定想要做生意,不想流失我这个吃货客户。于是,在蛋糕没有做好的这段时间,有客户来,他们就让客户把自己的电话留下,这就是观察者模式中的注册环节。然后蛋糕做好之后,一次性通知所有记录了的客户,这就是观察者的发布环节。
一个简单的观察者模式类:
1function Observer() {
2 this.dep = [];
3
4 register(fn) {
5 this.dep.push(fn)
6 }
7
8 notify() {
9 this.dep.forEach(item => item())
10 }
11}
12
13const wantCake = new Oberver();
14// 每来一个顾客就注册一个想执行的函数
15wantCake.register(() => {'console.log("call daisy")'})
16wantCake.register(() => {'console.log("call anny")'})
17wantCake.register(() => {'console.log("call sunny")'})
18
19// 最后蛋糕做好之后,通知所有的客户
20wantCake.notify()
初始化
vue 在初始化执行 initState 的时候,会对props
、methods
、data
、computed
和 wathcer
等属性做初始化操作。
1export function initState(vm: Component) {
2 vm._watchers = [];
3 const opts = vm.$options;
4 if (opts.props) initProps(vm, opts.props);
5 if (opts.methods) initMethods(vm, opts.methods);
6 if (opts.data) {
7 initData(vm);
8 } else {
9 observe((vm._data = {}), true /* asRootData */);
10 }
11 if (opts.computed) initComputed(vm, opts.computed);
12 if (opts.watch && opts.watch !== nativeWatch) {
13 initWatch(vm, opts.watch);
14 }
15}
在初始化 data 的时候,一共做了两件事,一件是利用 proxy 将数据代理到整个 vue 实例上,另一件就是将 data 中的所有对象属性 reactive 化,变成响应式对象,为对象添加 getter 和 setter。
1/**
2 * Observer class that is attached to each observed
3 * object. Once attached, the observer converts the target
4 * object's property keys into getter/setters that
5 * collect dependencies and dispatch updates.
6 */
7export class Observer {
8 value: any;
9 dep: Dep;
10 vmCount: number; // number of vms that has this object as root $data
11
12 constructor(value: any) {
13 this.value = value;
14 this.dep = new Dep();
15 this.vmCount = 0;
16 def(value, '__ob__', this);
17 if (Array.isArray(value)) {
18 const augment = hasProto ? protoAugment : copyAugment;
19 augment(value, arrayMethods, arrayKeys);
20 this.observeArray(value);
21 } else {
22 this.walk(value);
23 }
24 }
25
26 /**
27 * Walk through each property and convert them into
28 * getter/setters. This method should only be called when
29 * value type is Object.
30 */
31
32 walk(obj: Object) {
33 const keys = Object.keys(obj);
34 for (let i = 0; i < keys.length; i++) {
35 defineReactive(obj, keys[i]);
36 }
37 }
38
39 /**
40 * Observe a list of Array items.
41 */
42 observeArray(items: Array<any>) {
43 for (let i = 0, l = items.length; i < l; i++) {
44 observe(items[i]);
45 }
46 }
47}
上面的这段代码,它将对象的所有属性进行遍历执行 defineReactive()方法进行响应式化
1walk (obj: Object) {
2 const keys = Object.keys(obj)
3 for (let i = 0; i < keys.length; i++) {
4 defineReactive(obj, keys[i])
5 }
6 }
在执行这个方法的过程中,会 new 一个 dep 实例,记得注意,后面会用到
1function defineReactive(obj: Object, key: string, ...) {
2 const dep = new Dep()
3
4 Object.defineProperty(obj, key, {
5 enumerable: true,
6 configurable: true,
7 get: function reactiveGetter () {
8 ....
9 dep.depend()
10 return value
11 ....
12 },
13 set: function reactiveSetter (newVal) {
14 ...
15 val = newVal
16 dep.notify()
17 ...
18 }
19 })
20 }
21
22 class Dep {
23 static target: ?Watcher;
24 subs: Array<Watcher>;
25
26 depend () {
27 if (Dep.target) {
28 Dep.target.addDep(this)
29 }
30 }
31
32 notify () {
33 const subs = this.subs.slice()
34 for (let i = 0, l = subs.length; i < l; i++) {
35 subs[i].update()
36 }
37 }
上述代码中的 Dep 类就是一个观察者类,每个对象属性都一个 dep 实例对象,在执行 get 的时候进行触发 depend 方法,触发 sit 的时候执行 notify 方法。
mount 阶段
在 vue 实例挂载阶段,会创建一个 Watcher 类的实例对象,这个 Watcher 实际上是连接 Vue 组件与 Dep(也就是视图更新环节)的桥梁。
1mountComponent(vm: Component, el: ?Element, ...) {
2 vm.$el = el
3
4 ...
5
6 updateComponent = () => {
7 vm._update(vm._render(), ...)
8 }
9
10 new Watcher(vm, updateComponent, ...)
11 ...
12}
13
14class Watcher {
15 getter: Function;
16
17 // 代码经过简化
18 constructor(vm: Component, expOrFn: string | Function, ...) {
19 ...
20 this.getter = expOrFn
21 Dep.target = this // 注意这里将当前的Watcher赋值给了Dep.target
22 this.value = this.getter.call(vm, vm) // 调用组件的更新函数
23 ...
24 }
25}
在 rende()方法将模板渲染成虚拟 Vnode 的过程中会访问 data,从而触发属性的 getter,然后每个对象属性又有一个 dep 实例对象(上面提到的),然后再 getter 的逻辑中又会调用该 dep 的 depend 方法,将 watcher 实例 add 到 sub(存在 Dep 类中的存储 watcher 的数组)里面。在 depend 方法里面,Dep.target 就是 watcher 本身,在 Wacher 类中的构造函数会执行(上面代码有)。以上过程就叫依赖收集。
派发更新
在对象属性的数据改变之后,会触发 sitter,从而执行 sitter 函数的逻辑,从而调用 dep 实例的 notify 方法,从而进行遍历调用 sub 中所有 watcher 的 upadte 方法进行视图更新。
总结
**第一步:**组件初始化的时候,先给每一个 Data 属性都注册 getter,setter,也就是 reactive 化。然后再 new 一个自己的 Watcher 对象,此时 watcher 会立即调用组件的 render 函数去生成虚拟 DOM。在调用 render 的时候,就会需要用到 data 的属性值,此时会触发 getter 函数,将当前的 Watcher 函数注册进 sub 里。
**第二步:**当 data 属性发生改变之后,会触发 sitter,遍历 sub 里所有的 watcher 对象,通知它们去重新渲染组件。
注意:vue 对数组响应式是通过变异方法完成的,而且如果要对响应式对象添加新的属性,必须要用$set 方法才能完成
参考文章:
vue 技术揭秘
最简化 VUE 的响应式原理