# Vue源码解读(三)

# 一、Vue中computed和watch

两者区别:

  • 计算属性computed是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值。
  • 侦听器watch,当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

计算属性computed和侦听器watch的初始化都在 src\core\instance\state.js initState方法里面。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)  // 初始化props
  if (opts.methods) initMethods(vm, opts.methods) // 初始化methods
  if (opts.data) {
    initData(vm)  // 初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) // 初始化计算属性
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)  // 初始化侦听器
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

initState会调用initComputed来初始化计算属性和initWatch来初始化侦听器。先看computed的实现

# computed

computed的基本使用如下

data: {
  message: 'Hello'
},
computed: {
  // 计算属性的 getter
  reversedMessage: function () {
    // `this` 指向 vm 实例
    return this.message.split('').reverse().join('')
  }
}
1
2
3
4
5
6
7
8
9
10

现在我们从源码角度来看computed的实现,computed的初始化是调用initComputed方法完成的。

  • initComputed 初始化计算属性

具体做了两件事,为每个计算属性创建watcher和定义计算属性的getter和setter,核心代码如下

const computedWatcherOptions = { lazy: true }//是否延迟执行watcher的get方法,计算属性默认都延迟

function initComputed (vm: Component, computed: Object) {
  // 创建computedWatchers数组来保存每个计算属性的watcher
  const watchers = vm._computedWatchers = Object.create(null)
  // 在SSR服务端渲染的时候,计算属性仅仅是getter
  const isSSR = isServerRendering()
  // 遍历这些计算属性,key就是计算属性的名字
  for (const key in computed) {
    // userDef是计算属性的value,有两种:一种是function,一种是包含get和set的Object
    const userDef = computed[key]
    // 得到getter
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // 如果不是SSR,则为每个key创建watcher
      watchers[key] = new Watcher(
        vm, // vue实例
        getter || noop,  // getter或者空函数
        noop, // 空函数
        computedWatcherOptions // 延迟执行watcher的get方法
      )
    }

    // 如果这些key不在vm实例上,则用defineComputed定义这些计算属性
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  • defineComputed 定义计算属性的getter和setter。
// 通用的属性定义描述符
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop, // 空函数
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()  // 如果不是服务端渲染,即客户端渲染,则需要使用缓存

  // 如果计算属性的value是一个function
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)   // 如果是客户端渲染,则使用createComputedGetter创建getter
      : createGetterInvoker(userDef) // 否则使用createGetterInvoker创建getter
    sharedPropertyDefinition.set = noop
  } else {
    // 如果计算属性的value是带getter和setter的对象,默认只有getter,不过在需要时也可以提供一个 setter
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key) // 如果是客户端渲染,则使用createComputedGetter创建getter
        : createGetterInvoker(userDef.get) // 否则使用createGetterInvoker创建getter
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 为每个计算属性定义getter和setter存取描述符
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  • createComputedGetter和createGetterInvoker

创建计算属性的getter。如果是客户端渲染,则使用createComputedGetter创建getter,否则使用createGetterInvoker创建getter。

// 客户端
function createComputedGetter (key) {
  // 返回getter函数
  return function computedGetter () {
    // 拿到这个计算属性的watcher
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
     // 当第一次访问computed中的值时,会因为watcher的初始化工作,watcher.dirty = watcher.lazy的原因,从而调用evalute()方法   
      if (watcher.dirty) { 
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

// 服务端渲染
function createGetterInvoker(fn) {
  // 因为服务端渲染只有首屏才会用到,所以不需要做依赖收集,直接返回fn的执行结果即可,即计算属性的value
  return function computedGetter () {
    return fn.call(this, this)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

当第一次访问computed中的值时,会因为初始化 watcher.dirty = watcher.lazy的原因,从而调用evalute()方法,evalute()方法很简单,就是调用了watcher实例中的get方法以及设置dirty = false,我们将这两个方法放在一起,evalute()定义在 src/core/observer/watcher.js

evaluate () {
  this.value = this.get()
  this.dirty = false
}
  
get () {  
  // 将当前watcher放入Dep.target对象,这个时候,Dep.target就是当前计算属性的watcher了
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 当调用用户传入的方法时,会触发watcher的getter操作,即用户自己声明的方法
    value = this.getter.call(vm, vm)
  } catch (e) {
  } finally {
    popTarget()
    // 去除不相关代码
  }
  return value
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在get方法中中,第一行就调用了pushTarget方法,其作用就是将Dep.target设置为所传入的watcher,即所访问的computed中属性的watcher,然后调用了value = this.getter.call(vm, vm)方法,想一想,调用这个方法会发生什么?

this.getter 在Watcher构建函数中提到,本质就是用户传入的方法(也是watcher的更新函数),也就是说,this.getter.call(vm, vm)就会调用用户自己声明的方法,那么如果方法里面用到了 this.data中的值或者其他被用defineReactive包装过的对象,那么,访问this.data 或者其他被defineReactive包装过的属性,是不是就会访问被代理的该属性的get方法。defineReactive中get方法如下:

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 这个时候,Dep.target有值了
      if (Dep.target) {
        // computed的watcher依赖了this.data的dep
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这个时候我们走完了一个依赖收集流程,知道了computed是如何知道依赖了谁。最后根据this.data所代理的set方法中调用的notify,就可以改变this.data的值,去更新所有依赖this.data值的computed属性value了。

computed整体流程

1、给每个计算属性创建 watcher和getter setter,watcher的更新函数是用户传入的方法。

2、首次渲染,自动触发计算属性的getter,getter会自动调用 watcher的evaluate()方法,该方法会将当前watcher放入Dep.target对象,并且执行用户传入的方法。

3、由于用户传入的方法里面依赖了this.data中的属性(即依赖属性)或者其他被用defineReactive包装过的对象,那么,访问依赖属性或者其他被defineReactive包装过的属性时,就会访问被代理的该属性的get方法。该属性的get方法会把当前computed watcher添加到该属性的Dep对象里面,这样就完成了该属性的依赖收集了。

4、this.data里面的属性值改变时,所代理的set方法中会调用dep的notify方法,通知该Dep中的所有watcher,依赖this.data值的computed属性就会自动更新了。

# watch

watch的基本使用如下:

1、最简单的用法,直接写一个监听处理函数

watch: {
  // 如果 `question` 发生改变,这个函数就会运行
  question: function (newQuestion, oldQuestion) {
    this.answer = 'Waiting for you to stop typing...'
    this.debouncedGetAnswer()  // 异步请求数据
  }
}
1
2
3
4
5
6
7

2、在所监听的数据后面直接加字符串形式的监听方法名

watch: {
  question: 'handleChange'
}
1
2
3

3、复杂用法,传递一个监听对象,这个对象包含handler、immediate、deep属性。

使用watch时有一个特点,就是当值第一次绑定的时候,不会执行监听函数,只有值发生改变才会执行。如果我们需要在最初绑定值的时候也执行函数,则就需要用到immediate属性。

当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,只有data中的数据才能够监听到变化,此时就需要deep属性对对象进行深度监听。

watch: {
  question: {
      // 监听处理函数
    handler(newName, oldName) {
      // ...
    },
    immediate: true, // 立即执行
      deep: true  // 深度watch
  }
} 
1
2
3
4
5
6
7
8
9
10

现在我们从源码角度来看watch的实现,watch的初始化是调用initWatch方法完成的。

  • initWatch,初始化Watch,调用createWatcher方法创建watcher。
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    // handler可能是多个监听方法名或者多个监听对象
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  • createWatcher,会调用内部的vm.$watch方法创建Watcher。
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 如果是纯对象
  if (isPlainObject(handler)) {
    options = handler // 拿到options
    handler = handler.handler  // 拿到监听处理函数handler
  }
  // 如果是字符串,则说明是个方法名,则从vm里面取出这个方法
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

vm.$watch方法在src\core\instance\state.js stateMixin方法中定义。

export function stateMixin (Vue: Class<Component>) {
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      // 如果cb是对象,即上面传进来的handler,则递归调用createWatcher直到cb是一个函数
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true  // 标记为user watcher
    const watcher = new Watcher(vm, expOrFn, cb, options) // 实例化一个watcher,cb是更新函数
    if (options.immediate) {
      try {
        // 如果需要立即执行,则直接调用cb方法
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回移除这个watcher的方法
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

通过以上分析,我们知道了侦听器watch方法的实现原理,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb ,如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher。

这里需要注意一点,这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。

# 二、vue-router

vue-router源码解析参考

# 实现简易版vue-router

import Vue from "vue";

class VueRouter {
  constructor(options) {
    this.$options = options;
    this.routeMap = {};  // 路由map,key是path,value是导入的组件

    // 借助Vue构造函数实现路由响应式
    this.app = new Vue({
      data: {
        current: "/"
      }
    });
  }

  init() {
    this.bindEvents(); //初始化事件,监听url变化
    this.createRouteMap(this.$options); //解析路由配置
    this.initComponent(); // 实现两个组件
  }

  bindEvents() {
    window.addEventListener("load", this.onHashChange.bind(this));
    window.addEventListener("hashchange", this.onHashChange.bind(this));
  }
  onHashChange() {
    // 拿到当前的hash值
    this.app.current = window.location.hash.slice(1) || "/";
  }
  createRouteMap(options) {
    options.routes.forEach(item => {
      this.routeMap[item.path] = item.component;
    });
  }
  initComponent() {
    // 实现router-link组件
    Vue.component("router-link", {
      props: { to: String },
      render(h) {
        // h(tag, data, children)
        return h("a", { attrs: { href: "#" + this.to } }, [
          this.$slots.default
        ]);
      }
    });

    // 实现router-view组件
    Vue.component("router-view", {
      render: h => {
        const comp = this.routeMap[this.app.current];
        return h(comp);
      }
    });
  }
}
VueRouter.install = function(Vue) {
  // 混入
  Vue.mixin({
    beforeCreate() {
      // this是Vue实例
      if (this.$options.router) {
        // 仅在根组件执行一次
        Vue.prototype.$router = this.$options.router;
        this.$options.router.init();
      }
    }
  });
};

Vue.use(VueRouter);

export default new VueRouter({
  routes: [{ path: "/", component: Home }, { path: "/about", component: About }]
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

# 三、vuex

vuex源码解析参考

# 实现简易版vuex

import Vue from 'vue'
let Vue;

class Store {
  constructor(options) {
    this.state = new Vue({
      data: options.state
    });

    this.mutations = options.mutations;
    this.actions = options.actions;

    options.getters && this.handleGetters(options.getters)
  }

  // 声明为箭头函数,确保this执向正确
  commit = (type, arg) => {
    this.mutations[type](this.state, arg);
  };

  dispatch(type, arg) {
    this.actions[type]({
      commit: this.commit,
      state: this.state
    }, arg);
  }

  handleGetters(getters) {
    this.getters = {};
    // 遍历getters所有key
    Object.keys(getters).forEach(key => {
        // 为this.getters定义若干属性,这些属性是只读的
        // $store.getters.score
        Object.defineProperty(this.getters, key, {
            get: () => {
                return getters[key](this.state);
            }
        })
    })
  }
}

function install(_Vue) {
  Vue = _Vue;

  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) {
        Vue.prototype.$store = this.$options.store;
      }
    }
  });
}

const Vuex ={ Store, install }

export default Vuex;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Last Updated: 3/26/2020, 4:20:55 PM