Vue响应式原理(2.6版本)

前言:此文章主要是分析Vue的响应式原理,从new Vue开始,看源码究竟发生了什么,其中会忽略掉大量边界处理以及“不重要”的代码,最后再验证一下,建议可以自己clone源码,然后跟着文中的步骤跟着看,文中对代码不会有太多讲解,源码本身的命名结构都是很清晰的(在去除边界处理代码之后),主要是记录过程,帮助以后自己回忆,如果你是对响应式原理完全不懂的,建议可以先看看Introduction - Advanced Components | Vue Mastery或者Vue 3 Reactivity - Vue 3 Reactivity | Vue Mastery

看源码!

  1. src/core/index.js中声明了Vue函数
function Vue(options) { this._init(options); }
// 设置Vue.prototype._init
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
  1. 接着来到src/core/instance/init.js,在initMixin中我们可以找到Vue中_init函数的声明,它主要是初始化了Vue组件的声明周期、事件、Render,然后触发了beforeCreate的钩子,再接着初始化Injections、Provide,再触发了created的钩子,最后挂载组件
export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function(options?: Object) {
        const vm: Component = this;
        // ...Normalizing options ...
        vm._self = vm;
        initLifecycle(vm);
        initEvents(vm);
        initRender(vm);
        callHook(vm, 'beforeCreate');
        initInjections(vm);
        initState(vm);
        initProvide(vm);
        callHook(vm, 'created');
        // ...
        vm.$mount(vm,$options.el);
    }
}
  1. 这其中我们需要关注的就是initState,来到src/core/instance/state.js
export function initState (vm: Component) {
    vm._watchers = [];
    const opts = vm.$options;
    if (opts.props) initProps(vm, opts.props)
    if (opts.methods) initMethods(vm, opts.methods);
    if (opts.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);
    }
}

可以看到,在initState中主要是初始化Props、Methods、Data、Computed(计算属性)和Watch(侦听器)

  1. 我们要关注的是initData,initData还是在state.js中
function initData (vm: Component) {
    let data = vm.$options.data;
    // ...
    observe(data, true /* asRootData */);
}
  1. 紧接着是observe(src/core/observer/index.js)
/**
* 尝试为value创建一个observer实例,并返回这个observer
*/
export function observe(value: any, asRootData: ?boolean): Observer | void {
    // ...
    ob = new Observer(value);
    return ob;
}
  1. 继续在src/core/observer/index.js找到Observer的Class声明
export class Observer {
    value: any;
    dep: Dep;

    constructor (value: any) {
        this.value = value;
        // ...
        if (Array.isArray(value)) {
            // ...
            this.observeArray(value);
        } else {
            this.walk(value);
        }
    }

    walk (obj: Object) {
        const keys = Object.keys(obj);
        for (let i = 0; i< keys.length; i++) {
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    }

    observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            observe(items[I]);
        }
    }
}

export function defineReactive (...) {
    const dep = new Dep();
    // ...
    let childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
        get: function reactiveGetter () { dep.depend(); },
        set: function reactiveSetter (newVal) { dep.notify(); }
    });
    //  ...
}
  1. 下面要看看Dep是如何定义的了,来到src/core/observer/dep.js
export default class Dep {
    static target: ?Watcher;
    subs: Array<Watcher>;
    constructor () {
        this.subs = [];
    }

    addSub (sub: Watcher) {
        this.subs.push(sub);
    }

    depend () {
        if (Dep.tartget) {
            Dep.target.addDep(this);
        }
    }

    notify () {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
  1. 最后就是Watcher,让我们把目光转移到src/core/observer/watcher.js
import Dep, { pushTarget, popTarget } from './dep'

export deafult class Watcher {
    vm: Component;
    newDepIds: ISet;
  getter: Function;
    value: any;

    constructor (vm: Component, expOrFn: string | Function ...) {
        // ...
        this.vm = vm;
        this.getter = expOrFn;
        // ...
        this.value = this.get();
    }

    get () {
        pushTarget(this);
        const vm = this.vm;
        let value = this.getter.call(vm, vm);
        // ...
        popTarget();
        return value;
    }
    
    update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

梳理一下

总的来理一遍:
首先,new Vue会调用initState,为data创建一个Observer类的实例,同时递归为data中的Object类型的property创建一个Observer类的实例(如果这些property中还有Object类型的属性,则继续递归创建Observer),并为每一个Observer都创建了一个Dep类的实例;
然后使用了Object.defineProperty来拦截并重写property的get和set,在defineProperty中为每个property都创建了一个Dep类的实例,再在get和set中分别调用了dep.depend()和dep.notify()用于将当前的Watcher添加到dep的订阅列表中和触发订阅列表中Wacther的updat方法;而Watcher才是真正记录了使用响应式数据的函数的类

验证一下

来看一个简单的例子:

<template>
  <div>
    <h1>{{ obj.name }}</h1>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      obj1: {
        name: "test",
        obj2: {
          test: 'test'
        },
      },
    };
  },
  mounted() {
    console.log(this.obj);
  },
};
</script>

来看看控制台的打印:

  1. 首先,data中的obj是有一个ob指向它的Observer的(data也有,但并没有直接有一个ob属性指向它的Observer)

  2. 然后我们也能看到obj中的obj也是有一个ob属性的,同样指向它的Observer,同时也能看到每一个Observer都对应的有一个Dep

  3. 接着就是确认get和set中的dep,下面是get name和set name中的dep,可以通过id确认这两个dep是同一个dep,均为defineProperty时闭包中的dep




    至此响应式原理分析和验证告一段落,最后放一张图帮助理解


    14F77938-0913-4378-99E6-04B75BE27E5B.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容