参考:霍春阳 https://juejin.cn/post/6927473239228841998

1.从打包命令开始

从打包入口开始 package.json

  1. "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",

-w = --watch
-c = --config 就是指定配置文件为 build/config.js

build/config.js

  1. const builds = {
  2. //....
  3. // Runtime+compiler development build (Browser)
  4. 'web-full-dev': {
  5. entry: resolve('web/entry-runtime-with-compiler.js'),
  6. dest: resolve('dist/vue.js'),
  7. format: 'umd',
  8. env: 'development',
  9. alias: { he: './entity-decoder' },
  10. banner
  11. },
  12. //....
  13. }
  14. function genConfig (name) {
  15. const opts = builds[name]
  16. const config = {
  17. input: opts.entry,
  18. external: opts.external,
  19. plugins: [
  20. flow(),
  21. alias(Object.assign({}, aliases, opts.alias))
  22. ].concat(opts.plugins || []),
  23. output: {
  24. file: opts.dest,
  25. format: opts.format,
  26. banner: opts.banner,
  27. name: opts.moduleName || 'Vue'
  28. },
  29. onwarn: (msg, warn) => {
  30. if (!/Circular/.test(msg)) {
  31. warn(msg)
  32. }
  33. }
  34. }
  35. }
  36. if (process.env.TARGET) {
  37. module.exports = genConfig(process.env.TARGET) //传入参数 web-full-dev
  38. } else {
  39. exports.getBuild = genConfig
  40. exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
  41. }
  • 这里其实就是一个打包配置文件,为了多平台打包的配置。genConfig 函数返回一个Rollup 的配置对象。

2. 寻找 Vue 的构造函数

入口文件为 web/entry-runtime-with-compiler.js:

  1. import Vue from './runtime/index'

src/core/instance/index.js

一路下来找到 src/core/instance/index.js

  1. import { initMixin } from './init'
  2. import { stateMixin } from './state'
  3. import { renderMixin } from './render'
  4. import { eventsMixin } from './events'
  5. import { lifecycleMixin } from './lifecycle'
  6. import { warn } from '../util/index'
  7. function Vue (options) {
  8. if (process.env.NODE_ENV !== 'production' &&!(this instanceof Vue)) {
  9. warn('Vue is a constructor and should be called with the `new` keyword')
  10. }
  11. // 初始化
  12. this._init(options)
  13. }
  14. initMixin(Vue) // 实现了_init()
  15. stateMixin(Vue) // $data,$props,$set,$delete,$watch
  16. eventsMixin(Vue)
  17. lifecycleMixin(Vue) // _update()
  18. renderMixin(Vue) // _render
  19. export default Vue

这个文件就是定义构造函数,然后调用了 5 个 Mixin 方法给 Vue 构造函数增加原型方法

处理后 Vue 的 prototype 上会挂载很多方法

src/core/index.js

回到上一个文件 src/core/index.js

  1. initGlobalAPI(Vue)

initGlobalAPI的作用是在 Vue 构造函数上挂载静态属性和方法,Vue在经过initGlobalAPI之后,会变成这样:

  1. Vue.config
  2. Vue.util = util
  3. Vue.set = set
  4. Vue.delete = del
  5. Vue.nextTick = util.nextTick
  6. Vue.options = {
  7. components: {
  8. KeepAlive
  9. },
  10. directives: {},
  11. filters: {},
  12. _base: Vue
  13. }
  14. Vue.use
  15. Vue.mixin
  16. Vue.cid = 0
  17. Vue.extend
  18. Vue.component = function(){}
  19. Vue.directive = function(){}
  20. Vue.filter = function(){}
  21. Vue.prototype.$isServer
  22. Vue.version = '__VERSION__'

runtime/index.js

在往上就是 runtime/index.js
主要做了三件事:
1、覆盖Vue.config的属性,将其设置为平台特有的一些方法
2、Vue.options.directivesVue.options.components安装平台特有的指令和组件
3、在Vue.prototype上定义__patch__$mount

Vue会变成这样:

  1. Vue.config.isUnknownElement = isUnknownElement
  2. Vue.config.isReservedTag = isReservedTag
  3. Vue.config.getTagNamespace = getTagNamespace
  4. Vue.config.mustUseProp = mustUseProp
  5. Vue.options = {
  6. components: {
  7. KeepAlive,
  8. Transition,
  9. TransitionGroup
  10. },
  11. directives: {
  12. model,
  13. show
  14. },
  15. filters: {},
  16. _base: Vue
  17. }
  18. // install platform patch function
  19. Vue.prototype.__patch__ = inBrowser ? patch : noop
  20. // public mount method
  21. Vue.prototype.$mount = function (el,hydrating) {
  22. el = el && inBrowser ? query(el) : undefined
  23. return mountComponent(this, el, hydrating)
  24. }

注意的是Vue.options的变化。

$mount方法:

  1. Vue.prototype.$mount = function (
  2. el?: string | Element,
  3. hydrating?: boolean
  4. ): Component {
  5. el = el && inBrowser ? query(el) : undefined
  6. return mountComponent(this, el, hydrating)
  7. }
  8. // el=document.querySelector(el)

web-runtime-with-compiler.js

最后回到入口文件web-runtime-with-compiler.js

  1. // 缓存 $mount 函数
  2. const mount = Vue.prototype.$mount
  3. // 覆盖 $mount 函数
  4. Vue.prototype.$mount = function(){...}
  5. //在 Vue 上挂载 compile
  6. Vue.compile = compileToFunctions
  7. //compileToFunctions 函数的作用,就是将模板 template 编译为 render 函数

runtime/index.js主要是添加 web 平台特有的配置、组件和指令,web-runtime-with-compiler.js给 Vue 的$mount方法添加compiler编译器,支持template。

3. Vue 初始化流程

  1. let v = new Vue({
  2. el: '#app',
  3. data: {
  4. a: 1,
  5. b: [1, 2, 3]
  6. }
  7. })

Vue 调用的第一个方法_init()

  1. function Vue (options) {
  2. if (process.env.NODE_ENV !== 'production' &&
  3. !(this instanceof Vue)
  4. ) {
  5. warn('Vue is a constructor and should be called with the `new` keyword')
  6. }
  7. // 初始化
  8. this._init(options)
  9. }

在调用_init()之前,还做了一个安全模式的处理,告诉开发者必须使用new操作符调用 Vue。

  1. Vue.prototype._init = function (options?: Object) {
  2. const vm: Component = this
  3. // a uid
  4. vm._uid = uid++
  5. let startTag, endTag
  6. /* istanbul ignore if */
  7. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  8. startTag = `vue-perf-start:${vm._uid}`
  9. endTag = `vue-perf-end:${vm._uid}`
  10. mark(startTag)
  11. }
  12. // a flag to avoid this being observed
  13. vm._isVue = true
  14. // merge options
  15. if (options && options._isComponent) {
  16. // optimize internal component instantiation
  17. // since dynamic options merging is pretty slow, and none of the
  18. // internal component options needs special treatment.
  19. initInternalComponent(vm, options)
  20. } else {
  21. vm.$options = mergeOptions(
  22. resolveConstructorOptions(vm.constructor),
  23. options || {},
  24. vm
  25. )
  26. }
  27. /* istanbul ignore else */
  28. if (process.env.NODE_ENV !== 'production') {
  29. initProxy(vm)
  30. } else {
  31. vm._renderProxy = vm
  32. }
  33. // expose real self
  34. vm._self = vm
  35. initLifecycle(vm) // 初始化$parent,$root,$children,$refs
  36. initEvents(vm) // 处理父组件传递的监听器
  37. initRender(vm) // $slots, $scopedSlots,_c(),$createElement()
  38. callHook(vm, 'beforeCreate')
  39. initInjections(vm) // 获取注入数据
  40. initState(vm) // 初始化组件中props、methods、data、computed、watch
  41. initProvide(vm) // 提供数据
  42. callHook(vm, 'created')
  43. /* istanbul ignore if */
  44. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  45. vm._name = formatComponentName(vm, false)
  46. mark(endTag)
  47. measure(`vue ${vm._name} init`, startTag, endTag)
  48. }
  49. if (vm.$options.el) {
  50. vm.$mount(vm.$options.el)
  51. }
  52. }

_init()方法在一开始的时候,在this对象上定义了两个属性:_uid_isVue,然后判断有没有定义options._isComponent,在使用 Vue 开发项目的时候,我们是不会使用_isComponent选项的,这个选项是 Vue 内部使用的,按照本节开头的例子,这里会走else分支:

  1. vm.$options = mergeOptions(
  2. resolveConstructorOptions(vm.constructor),
  3. options || {},
  4. vm
  5. )

这样Vue第一步所做的事情就来了:使用策略对象合并参数选项
可以发现,Vue 使用mergeOptions来处理我们调用 Vue 时传入的参数选项 (options),然后将返回值赋值给this.$options(vm === this),传给mergeOptions方法三个参数,我们分别来看一看,首先是:resolveConstructorOptions(vm.constructor),我们查看一下这个方法:

  1. export function resolveConstructorOptions (Ctor: Class<Component>) {
  2. let options = Ctor.options // 相当于let options = Vue.options
  3. if (Ctor.super) { //处理继承
  4. //...
  5. }
  6. return options
  7. }

这个方法接收一个参数Ctor,通过传入的vm.constructor我们可以知道,其实就是Vue构造函数本身。

  1. //Vue.options的样子
  2. Vue.options = {
  3. components: {
  4. KeepAlive,
  5. Transition,
  6. TransitionGroup
  7. },
  8. directives: {
  9. model,
  10. show
  11. },
  12. filters: {},
  13. _base: Vue
  14. }

传给mergeOptions方法的第二个参数是我们调用 Vue 构造函数时的参数选项,
第三个参数是vm也就是this对象,
最终运行的代码应该如下:

  1. vm.$options = mergeOptions(
  2. {
  3. components: {
  4. KeepAlive,
  5. Transition,
  6. TransitionGroup
  7. },
  8. directives: {
  9. model,
  10. show
  11. },
  12. filters: {},
  13. _base: Vue
  14. },
  15. {
  16. el: '#app',
  17. data: {
  18. a: 1,
  19. b: [1, 2, 3]
  20. }
  21. },
  22. vm
  23. )

并将最终的值赋值给实例下的$options属性即:this.$options

下面的代码是_init()方法合并完选项之后的代码:

  1. if (process.env.NODE_ENV !== 'production') {
  2. initProxy(vm)
  3. } else {
  4. vm._renderProxy = vm
  5. }
  6. vm._self = vm
  7. initLifecycle(vm)
  8. initEvents(vm)
  9. callHook(vm, 'beforeCreate')
  10. initState(vm)
  11. callHook(vm, 'created')
  12. initRender(vm)

根据上面的代码,在生产环境下会为实例添加两个属性,并且属性值都为实例本身:

  1. vm._renderProxy = vm
  2. vm._self = vm

然后,调用了4个init*方法分别为:initLifecycleinitEventsinitStateinitRender,且在initState前后分别回调了生命周期钩子beforeCreatecreated,而initRender是在created钩子执行之后执行的,看到这里,也就明白了为什么 created的时候不能操作 DOM 了。因为这个时候还没有渲染真正的 DOM 元素到文档中。created仅仅代表数据状态的初始化完成。

最后在initRender中如果有vm.$options.el还要调用vm.$mount(vm.$options.el),如下:

  1. if (vm.$options.el) {
  2. vm.$mount(vm.$options.el)
  3. }

这就是为什么如果不传递el选项就需要手动 mount的原因了。

4. 数据响应式

Vue 的数据响应系统包含三个部分:ObserverDepWatcher

  1. function initData (vm: Component) {
  2. let data = vm.$options.data
  3. data = vm._data = typeof data === 'function'
  4. ? data.call(vm)
  5. : data || {}
  6. if (!isPlainObject(data)) {
  7. data = {}
  8. process.env.NODE_ENV !== 'production' && warn(
  9. 'data functions should return an object:\n' +
  10. 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
  11. vm
  12. )
  13. }
  14. const keys = Object.keys(data)
  15. const props = vm.$options.props
  16. let i = keys.length
  17. while (i--) {
  18. if (props && hasOwn(props, keys[i])) {
  19. process.env.NODE_ENV !== 'production' && warn(
  20. `The data property "${keys[i]}" is already declared as a prop. ` +
  21. `Use prop default value instead.`,
  22. vm
  23. )
  24. } else {
  25. proxy(vm, keys[i])
  26. }
  27. }
  28. observe(data)
  29. data.__ob__ && data.__ob__.vmCount++
  30. }

这里会判断是不是fucntion,得到最终我们传入的data

  1. data: {
  2. a: 1,
  3. b: [1, 2, 3]
  4. }

然后是一个while循环,循环的目的是在实例对象上对数据进行代理,这样我们就能通过this.a来访问data.a了,代码的处理是在proxy函数中,该函数非常简单,仅仅是在实例对象上设置与data属性同名的访问器属性,然后使用_data数据劫持,如下:

  1. function proxy (vm: Component, key: string) {
  2. if (!isReserved(key)) {
  3. Object.defineProperty(vm, key, {
  4. configurable: true,
  5. enumerable: true,
  6. get: function proxyGetter () {
  7. return vm._data[key]
  8. },
  9. set: function proxySetter (val) {
  10. vm._data[key] = val
  11. }
  12. })
  13. }

做完数据的代理,就正式进入响应式系统

  1. observe(data)

我们说过,数据响应系统主要包含三部分:ObserverDepWatcher,代码分别存放在:observer/index.jsobserver/dep.js以及observer/watcher.js文件中

假如,我们有如下代码:

  1. var data = {
  2. a: 1,
  3. b: {
  4. c: 2
  5. }
  6. }
  7. observer(data)
  8. new Watch('a', () => {
  9. alert(9)
  10. })
  11. new Watch('a', () => {
  12. alert(90)
  13. })
  14. new Watch('b.c', () => {
  15. alert(80)
  16. })

这段代码目的是,首先定义一个数据对象data,然后通过 observer对其进行观测,之后定义了三个观察者,当数据有变化时,执行相应的方法,这个功能使用 Vue 的实现原来要如何去实现?其实就是在问observer怎么写?Watch构造函数又怎么写?接下来我们逐一实现。
首先,observer的作用是:将数据对象 data 的属性转换为访问器属性:

  1. class Observer {
  2. constructor (data) {
  3. this.walk(data)
  4. }
  5. walk (data) {
  6. let keys = Object.keys(data)
  7. for(let i = 0; i < keys.length; i++){
  8. defineReactive(data, keys[i], data[keys[i]])
  9. }
  10. }
  11. }
  12. function defineReactive (data, key, val) {
  13. observer(val)
  14. Object.defineProperty(data, key, {
  15. enumerable: true,
  16. configurable: true,
  17. get: function () {
  18. return val
  19. },
  20. set: function (newVal) {
  21. if(val === newVal){
  22. return
  23. }
  24. observer(newVal)
  25. }
  26. })
  27. }
  28. function observer (data) {
  29. if(Object.prototype.toString.call(data) !== '[object Object]') {
  30. return
  31. }
  32. new Observer(data)
  33. }

上面的代码中,我们定义了 observer方法,该方法检测了数据 data是不是纯 JavaScript对象,如果是就调用Observer类,并将data作为参数透传。

Observer类中,我们使用walk方法对数据 data的属性循环调用defineReactive方法,defineReactive方法很简单,仅仅是将数据 data的属性转为访问器属性,并对数据进行递归观测,否则只能观测数据 data 的直属子属性。这样我们的第一步工作就完成了,当我们修改或者获取 data 属性值的时候,通过get 和 set 即能获取到通知。

我们继续往下看,来看一下Watch:

  1. new Watch('a', () => {
  2. alert(9)
  3. })

现在的问题是,Watch要怎么和observer关联?

我们看看Watch它知道些什么,通过上面调用Watch的方式,传递给Watch两个参数,一个是 ‘a’ 我们可以称其为表达式,另外一个是回调函数。所以我们目前只能写出这样的代码:

  1. class Watch {
  2. constructor (exp, fn) {
  3. this.exp = exp
  4. this.fn = fn
  5. }
  6. }

那么要怎么关联呢,大家看下面的代码会发生什么:

  1. class Watch {
  2. constructor (exp, fn) {
  3. this.exp = exp
  4. this.fn = fn
  5. data[exp]
  6. }
  7. }

多了一句data[exp],这句话是在干什么?是不是在获取data下某个属性的值,比如 exp为 ‘a’ 的话,那么data[exp]就相当于在获取data.a的值,那这会发生?

大家不要忘了,此时数据data下的属性已经是访问器属性了,所以这么做的结果会直接触发对应属性的get函数,这样我们就成功的和observer产生了关联,但这样还不够,我们还是没有达到目的,不过我们已经无限接近了,我们继续思考看一下可不可以这样:

既然在Watch中对表达式求值,能够触发observer的get,那么可不可以在get中收集Watch中函数呢?

答案是可以的,不过这个时候我们就需要Dep出场了,它是一个依赖收集器。

我们的思路是:data下的每一个属性都有一个唯一的Dep对象,在get中收集仅针对该属性的依赖,然后在set方法中触发所有收集的依赖,这样就搞定了,看如下代码:

  1. class Dep {
  2. constructor () {
  3. this.subs = []
  4. }
  5. addSub () {
  6. this.subs.push(Dep.target)
  7. }
  8. notify () {
  9. for(let i = 0; i < this.subs.length; i++){
  10. this.subs[i].fn()
  11. }
  12. }
  13. }
  14. Dep.target = null
  15. function pushTarget(watch){
  16. Dep.target = watch
  17. }
  18. class Watch {
  19. constructor (exp, fn) {
  20. this.exp = exp
  21. this.fn = fn
  22. pushTarget(this)
  23. data[exp]
  24. }
  25. }

上面的代码中,我们在Watch中增加了pushTarget(this),可以发现,这句代码的作用是将Dep.target的值设置为该 Watch对象。在pushTarget之后我们才对表达式进行求值,接着,我们修改defineReactive代码如下

  1. function defineReactive (data, key, val) {
  2. observer(val)
  3. let dep = new Dep()
  4. Object.defineProperty(data, key, {
  5. enumerable: true,
  6. configurable: true,
  7. get: function () {
  8. dep.addSub()
  9. return val
  10. },
  11. set: function (newVal) {
  12. if(val === newVal){
  13. return
  14. }
  15. observer(newVal)
  16. dep.notify()
  17. }
  18. })
  19. }

如标注,新增了三句代码,我们知道,Watch中对表达式求值会触发 get 方法,我们在 get 方法中调用了dep.addSub,也就执行了这句代码:this.subs.push(Dep.target),由于在这句代码执行之前,Dep.target的值已经被设置为一个Watch对象了,所以最终结果就是收集了一个Watch对象

然后在set方法中我们调用了dep.notify,所以当 data 属性值变化的时候,就会通过dep.notify循环调用所有收集的 Watch 对象中的回调函数:

  1. notify () {
  2. for(let i = 0; i < this.subs.length; i++){
  3. this.subs[i].fn()
  4. }
  5. }

这样observerDepWatch三者就联系成为一个有机的整体,实现了我们最初的目标,完整的代码可以戳这里:observer-dep-watch

这里还给大家挖了个坑,因为我们没有处理对数组的观测,由于比较复杂并且这又不是我们讨论的重点,如果大家想了解可以戳我的这篇文章:JavaScript 实现 MVVM 之我就是想监测一个普通对象的变化,另外,在 Watch 中对表达式求值的时候也只做了直接子属性的求值,所以如果 exp 的值为 ‘a.b’ 的时候,就不可以用了,Vue 的做法是使用.分割表达式字符串为数组,然后遍历一下对其进行求值,大家可以查看其源码。如下:

  1. const bailRE = /[^\w.$]/
  2. export function parsePath (path: string): any {
  3. if (bailRE.test(path)) {
  4. return
  5. } else {
  6. const segments = path.split('.')
  7. return function (obj) {
  8. for (let i = 0; i < segments.length; i++) {
  9. if (!obj) return
  10. obj = obj[segments[i]]
  11. }
  12. return obj
  13. }
  14. }
  15. }

Vue 的求值代码是在src/core/util/lang.js文件中parsePath函数中实现的。总结一下 Vue 的依赖收集过程应该是这样的:

实际上,Vue 并没有直接在get中调用addSub,而是调用的dep.depend,目的是将当前的 dep对象收集到 watch对象中,(大家注意数据的每一个字段都拥有自己的dep对象和get方法。)

这样 Vue 就建立了一套数据响应系统,之前我们说过,按照我们的例子那样写,初始化工作只包含两个主要内容即:initDatainitRender。现在initData我们分析完了,接下来看一看initRender

6. 渲染

initRender方法中,因为我们的例子中传递了el选项,所以下面的代码会执行:

  1. if (vm.$options.el) {
  2. vm.$mount(vm.$options.el)
  3. }

这里,调用了$mount方法,在还原 Vue 构造函数的时候,我们整理过所有的方法,其中$mount方法在两个地方出现过:

1、在web-runtime.js文件中:

  1. Vue.prototype.$mount = function (
  2. el
  3. hydrating?: boolean
  4. ): Component {
  5. el = el && inBrowser ? query(el) : undefined
  6. return this._mount(el, hydrating)
  7. }

它的作用是通过el获取相应的 DOM 元素,然后调用lifecycle.js文件中的_mount方法。

2、在web-runtime-with-compiler.js文件中:
分析一下可知web-runtime-with-compiler.js的逻辑如下:
1、缓存来自web-runtime.js文件的$mount方法
2、判断有没有传递render选项,如果有直接调用来自web-runtime.js文件的 $mount 方法
3、如果没有传递render选项,那么查看有没有template选项,如果有就使用compileToFunctions函数根据其内容编译成render函数
4、如果没有template选项,那么查看有没有el选项,如果有就使用compileToFunctions函数将其内容 (template = getOuterHTML(el)) 编译成render函数
5、将编译成的render函数挂载到this.$options属性下,并调用缓存下来的web-runtime.js文件中的 $mount方法

不过不管怎样,我们发现这些步骤的最终目的是生成render函数,然后再调用lifecycle.js文件中的_mount方法,我们看看这个方法做了什么事情,查看_mount方法的代码,这是简化过得:

  1. Vue.prototype._mount = function (
  2. el?: Element | void,
  3. hydrating?: boolean
  4. ): Component {
  5. const vm: Component = this
  6. vm.$el = el
  7. callHook(vm, 'beforeMount')
  8. vm._watcher = new Watcher(vm, () => {
  9. vm._update(vm._render(), hydrating)
  10. }, noop)
  11. if (vm.$vnode == null) {
  12. vm._isMounted = true
  13. callHook(vm, 'mounted')
  14. }
  15. return vm
  16. }

上面的代码很简单,该注释的都注释了,唯一需要看的就是这段代码:

  1. vm._watcher = new Watcher(vm, () => {
  2. vm._update(vm._render(), hydrating)
  3. }, noop)

看上去很眼熟有没有?我们平时使用 Vue 都是这样使用 watch的:

  1. this.$watch('a', (newVal, oldVal) => {
  2. })
  3. // 或者
  4. this.$watch(function(){
  5. return this.a + this.b
  6. }, (newVal, oldVal) => {
  7. })

第一个参数是 表达式或者函数,第二个参数是回调函数,第三个参数是可选的选项。

原理是 Watch 内部对表达式求值或者对函数求值从而触发数据的 get 方法收集依赖。

可是_mount方法中使用Watcher的时候第一个参数vm是什么鬼。我们不妨去看看源码中$watch函数是如何实现的,根据之前还原 Vue 构造函数中所整理的内容可知:

$watch方法是在src/core/instance/state.js文件中的stateMixin方法中定义的,源码如下:

  1. Vue.prototype.$watch = function (
  2. expOrFn: string | Function,
  3. cb: Function,
  4. options?: Object
  5. ): Function {
  6. const vm: Component = this
  7. options = options || {}
  8. options.user = true
  9. const watcher = new Watcher(vm, expOrFn, cb, options)
  10. if (options.immediate) {
  11. cb.call(vm, watcher.value)
  12. }
  13. return function unwatchFn () {
  14. watcher.teardown()
  15. }
  16. }

我们可以发现,$warch其实是对 Watche r的一个封装,内部的 Watcher 的第一个参数实际上也是vm即:Vue 实例对象,这一点我们可以在 Watcher 的源码中得到验证,observer/watcher.js文件查看:

  1. export default class Watcher {
  2. constructor (
  3. vm: Component,
  4. expOrFn: string | Function,
  5. cb: Function,
  6. options?: Object = {}
  7. ) {
  8. }
  9. }

可以发现真正的 Watcher 第一个参数实际上就是vm。第二个参数是表达式或者函数,然后以此类推,所以现在再来看_mount中的这段代码:

  1. vm._watcher = new Watcher(vm, () => {
  2. vm._update(vm._render(), hydrating)
  3. }, noop)

忽略第一个参数vm,也就说,Watcher内部应该对第二个参数求值,也就是运行这个函数:

  1. () => {
  2. vm._update(vm._render(), hydrating)
  3. }

所以vm._render()函数被第一个执行,该函数在src/core/instance/render.js中,该方法中的代码很多,下面是简化过的:

  1. Vue.prototype._render = function (): VNode {
  2. const vm: Component = this
  3. const {
  4. render,
  5. staticRenderFns,
  6. _parentVnode
  7. } = vm.$options
  8. ...
  9. let vnode
  10. try {
  11. vnode = render.call(vm._renderProxy, vm.$createElement)
  12. } catch (e) {
  13. ...
  14. }
  15. vnode.parent = _parentVnode
  16. return vnode
  17. }

_render方法首先从vm.$options中解构出render函数,大家应该记得:vm.$options.render方法是在web-runtime-with-compiler.js文件中通过compileToFunctions方法将templateel编译而来的。解构出render函数后,接下来便执行了该方法:

  1. vnode = render.call(vm._renderProxy, vm.$createElement)

其中使用call指定了render函数的作用域环境为vm._renderProxy,这个属性在我们整理实例对象的时候知道,他是在Vue.prototype._init方法中被添加的,即:vm._renderProxy = vm,其实就是 Vue 实例对象本身,然后传递了一个参数:vm.$createElement。那么render函数到底是干什么的呢?

让我们根据上面那句代码猜一猜,我们已经知道render函数是从templateel编译而来的,如果没错的话应该是返回一个虚拟 DOM 对象。我们不妨使用console.log打印一下render函数,当我们的模板这样编写时:

  1. <ul>
  2. <li>{{a}}</li>
  3. </ul>

其实了解 Vue2.x 版本的同学都知道,Vue 提供了render选项,作为template的代替方案,同时为 JavaScript 提供了完全编程的能力,下面两种编写模板的方式实际是等价的:

  1. new Vue({
  2. el: '#app',
  3. data: {
  4. a: 1
  5. },
  6. template: '<ul><li>{{a}}</li><li>{{a}}</li></ul>'
  7. })
  8. new Vue({
  9. el: '#app',
  10. render: function (createElement) {
  11. createElement('ul', [
  12. createElement('li', this.a),
  13. createElement('li', this.a)
  14. ])
  15. }
  16. })

现在我们再来看我们打印的render函数:

  1. function anonymous() {
  2. with(this){
  3. return _c('ul', {
  4. attrs: {"id": "app"}
  5. },[
  6. _c('li', [_v(_s(a))])
  7. ])
  8. }
  9. }

是不是与我们自己写render函数很像?因为 render函数的作用域被绑定到了 Vue 实例,即:render.call(vm._renderProxy, vm.$createElement),所以上面代码中_c_v_s以及变量a相当于 Vue 实例下的方法和变量。

大家还记得诸如_c_v_s这样的方法在哪里定义的吗?我们在整理 Vue 构造函数的时候知道,他们在src/core/instance/render.js文件中的renderMixin方法中定义,除了这些之外还有诸如:_l_m_o等等。其中_l就在我们使用v-for指令的时候出现了。所以现在大家知道为什么这些方法都被定义在render.js文件中了吧,因为他们就是为了构造出render函数而存在的。

现在我们已经知道了render函数的长相,也知道了render函数的作用域是 Vue 实例本身即:this(或vm)。那么当我们执行render函数时,其中的变量如:a,就相当于:this.a,我们知道这是在求值,所以_mount中的这段代码:

  1. vm._watcher = new Watcher(vm, () => {
  2. vm._update(vm._render(), hydrating)
  3. }, noop)

vm._render执行的时候,所依赖的变量就会被求值,并被收集依赖

按照 Vue 中watcher.js的逻辑,当依赖的变量有变化时不仅仅回调函数被执行,实际上还要重新求值,即还要执行一遍:

  1. () => {
  2. vm._update(vm._render(), hydrating)
  3. }

这实际上就做到了re-render,因为vm._update就是文章开头所说的虚拟 DOM 中的最后一步:patch
vm_render方法最终返回一个vnode对象,即虚拟 DOM,然后作为vm_update的第一个参数传递了过去,我们看一下vm_update的逻辑,在src/core/instance/lifecycle.js文件中有这么一段代码:

  1. if (!prevVnode) {
  2. vm.$el = vm.__patch__(
  3. vm.$el, vnode, hydrating, false ,
  4. vm.$options._parentElm,
  5. vm.$options._refElm
  6. )
  7. } else {
  8. vm.$el = vm.__patch__(prevVnode, vnode)
  9. }

如果还没有prevVnode说明是首次渲染,直接创建真实 DOM。如果已经有了prevVnode说明不是首次渲染,

那么就采用patch算法进行必要的 DOM 操作。这就是 Vue 更新 DOM 的逻辑。只不过我们没有将 virtual DOM 内部的实现。

现在我们理理思路,当我们写如下代码时:

  1. new Vue({
  2. el: '#app',
  3. data: {
  4. a: 1,
  5. b: [1, 2, 3]
  6. }
  7. })

Vue 所做的事:
1、构建数据响应系统,使用Observer将数据 data转换为访问器属性;将el编译为render函数,render函数返回值为虚拟 DOM

2、在_mount中对_update求值,而_update又会对render求值,render内部又会对依赖的变量求值,收集为被求值的变量的依赖,当变量改变时,_update又会重新执行一遍,从而做到re-render

到此,我们从大体流程,挑着重点的走了一遍 Vue,但是还有很多细节我们没有提及,比如:

1、将模板转为render函数的时候,实际是先生成的抽象语法树(AST),再将抽象语法树转成的render函数,而且这一整套的代码我们也没有提及,因为他在复杂了,其实这部分内容就是在完正则。

2、我们也没有详细的讲 Virtual DOM 的实现原理,网上已经有文章讲了,大家可以搜一搜

3、我们的例子中仅仅传递了eldata选项,大家知道 Vue 支持的选项很多,比如我们都没有讲到,但都是触类旁通的,比如你搞清楚了data选项再去看computed选项或者props选项就会很容易,比如你知道了Watcher的工作机制再去看watch选项就会很容易。