Vue2 源码分析(四): 深入响应式原理-demo篇

接上篇,我们以具体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

image.png

因为初始化我们并未传入任何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的赋值 会产生的功效:

image.png

vm._watcher = new Watcher(vm, updateComponent, noop)触发Wather.get 的调用

image.png

这里大家可以关注下右侧的调用栈:(以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 开始看

image.png

对于initData 我们关注proxy 和 observe 两处调用

image.png

proxy 在vm 实例上对data中返回的每个key 建立了代理
这种建立代理的方式无论是之后的赋值,还是render函数的调用,都会直接用到
复制代码

看下observe的调用:

image.png

class Observer 就是创建(目标/被观察者/发布者)的一系列过程:

image.png

image.png

将所有数据转化为可reactive后,这个时候都没有涉及到任何收集依赖,也并未建立Observer, Wacther,Dep的关系,那我们继续我们函数的调用,回到 “调用栈模拟(一)第36行init函数的“现场””

image.png

调用栈模拟(二):

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 为例:

image.png 走到了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组件后,我们看下调用栈:

image.png

从调用栈也可以看出,组件是以深度优先去创建所有VNode,调用完所有代码后我们的页面也就渲染出来了

有了所有watcher 和 observer 的关系后触发更新就很容易理解:

image.png

depId6.notify 会遍历所有的订阅者,调用他们提供的update 方法

image.png

这里只有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

Vue2 源码分析(二)# Vue构造函数 & this._init

Vue2 源码分析(三)# 观察者模式 & 深入响应式原理

猜你喜欢

转载自juejin.im/post/7034453370794409992