[关闭]
@frank-shaw 2019-11-30T10:19:37.000000Z 字数 4153 阅读 1467

vue源码阅读(一):Vue构造函数与初始化过程

vue 源码 构造函数 初始化


接着上面的准备。我们知道:在Vue2.6.10的源码结构中,入口文件是在src/platforms/web/entry-runtime-with-compiler.js。那么我们就具体来看看里面的代码吧。

在初次看源码的时候,有两个值得注意的点:

  1. 不要抠细节,把握整体的方向,以囫囵吞枣的方式看即可。
  2. 制定阅读的目标,有重点地去看

我们先来看看我们此次的目标:Vue的构造函数到底在哪里?具体的初始化过程又是怎样的?

追踪Vue构造函数

我们先来看entry-runtime-with-compiler.js文件的核心部分:

  1. //将$mount函数进行拓展,对用户输入的$options的template/el/render进行解析、处理
  2. const mount = Vue.prototype.$mount
  3. Vue.prototype.$mount = function (
  4. el?: string | Element,
  5. hydrating?: boolean
  6. ): Component {
  7. el = el && query(el)
  8. //处理用户自定义选项,可知执行顺序是 render > template > el
  9. const options = this.$options
  10. // resolve template/el and convert to render function
  11. if (!options.render) {
  12. let template = options.template
  13. if (template) {
  14. //字符串模板
  15. if (typeof template === 'string') {
  16. //选择器
  17. if (template.charAt(0) === '#') {
  18. template = idToTemplate(template)
  19. /* istanbul ignore if */
  20. if (process.env.NODE_ENV !== 'production' && !template) {
  21. warn(
  22. `Template element not found or is empty: ${options.template}`,
  23. this
  24. )
  25. }
  26. }
  27. } else if (template.nodeType) {
  28. //传进来的是dom
  29. template = template.innerHTML
  30. } else {
  31. if (process.env.NODE_ENV !== 'production') {
  32. warn('invalid template option:' + template, this)
  33. }
  34. return this
  35. }
  36. } else if (el) {
  37. //如果只设置el,那么就会直接获取其中的元素
  38. //值得注意的是,会将元素本身覆盖
  39. template = getOuterHTML(el)
  40. }
  41. if (template) {
  42. //获取render函数
  43. const { render, staticRenderFns } = compileToFunctions(template, {
  44. outputSourceRange: process.env.NODE_ENV !== 'production',
  45. shouldDecodeNewlines,
  46. shouldDecodeNewlinesForHref,
  47. delimiters: options.delimiters,
  48. comments: options.comments
  49. }, this)
  50. options.render = render
  51. options.staticRenderFns = staticRenderFns
  52. }
  53. }
  54. //执行原先的挂载函数
  55. return mount.call(this, el, hydrating)
  56. }

看代码知道,这个文件仅仅是对已有的mount函数做了增强:将用户输入的自定义模板选项,进行处理并发挥render函数。

从这里可以得出的结论是:
1.如果参数中同时存在el、render、template变量,其优先级顺序为:render > template > el。
2.编译发生的时间段(compileToFunctions,将模板转化为render函数)在整个初始化过程中非常早的,在执行具体mount函数之前。

这里并不是真正的Vue构造函数所在。我们接着看./runtime/index.js文件:

  1. // install platform patch function
  2. //定义补丁函数,这是将虚拟DOM转换为真实DOM的操作,非常重要
  3. Vue.prototype.__patch__ = inBrowser ? patch : noop
  4. // public mount method
  5. //定义挂载函数
  6. Vue.prototype.$mount = function (
  7. el?: string | Element,
  8. hydrating?: boolean
  9. ): Component {
  10. el = el && inBrowser ? query(el) : undefined
  11. //这才是真正的挂载函数
  12. return mountComponent(this, el, hydrating)
  13. }

结果发现,该文件仅仅是定义了原型链上的__patch__函数以及$mount函数。而详细的挂载过程还不在这里。具体细节我们先不看。

继续找:core/index.js

  1. //初始化全局API
  2. initGlobalAPI(Vue)

结果依然还是失望的。不禁感慨:这个俄罗斯套娃也太多层了吧。在core/index.js内部,也仅仅只是初始化了全局API而已。这个并不是今天我们关注的重点,还是继续找:src/core/instance/index.js :

  1. function Vue (options) {
  2. this._init(options)
  3. }
  4. initMixin(Vue)
  5. stateMixin(Vue)
  6. eventsMixin(Vue)
  7. lifecycleMixin(Vue)
  8. renderMixin(Vue)

在这里,终于找到了Vue的构造函数!!!。与此同时,我们也发现:该文件执行了许多Mixin操作。这些详细的部分我们先不理会,我们先来看看构造函数中的this._init()到底做了什么事情呢?

可以通过浏览器调试的方式,也可以通过全局搜索prototype._init的方式,找到_init()的所在文件:src/core/instance/init.js。

  1. //初始化顺序:生命周期->事件监听->渲染->beforeCreate->注入->state初始化->provide->created
  2. vm._self = vm
  3. initLifecycle(vm)
  4. initEvents(vm)
  5. initRender(vm)
  6. callHook(vm, 'beforeCreate')
  7. initInjections(vm) // resolve injections before data/props
  8. initState(vm) // 初始化 props/data/watch/methods, 此处会是研究数据响应化的重点
  9. initProvide(vm) // resolve provide after data/props
  10. callHook(vm, 'created')
  11. //如果存在el元素,则会自动执行$mount,这也是必须要理解的
  12. //也就是说,在写法上如果有el元素,可以省略$mount
  13. if (vm.$options.el) {
  14. vm.$mount(vm.$options.el)
  15. }

从这里可以得出的结论是:
1.在_init()函数中,我们看到vue实例初始化时候的执行顺序:生命周期->事件监听->渲染->beforeCreate->注入数据inject->组件状态初始化->提供数据provide->created。
2.如果存在el元素,则会自动执行挂载。

如果我们想要了解数据响应化的细节,那就应该详细去看initState()函数。它会是我们下节课的重点。

在这个时候,我们找到了主要的文件脉络。至于具体的初始化过程,我们还需要深入去看$mount过程中做了什么。于是我们回到真正的挂载函数mountComponent()函数里面去看看,其中发生的挂载细节:

  1. //这才是真正的mount函数
  2. export function mountComponent (
  3. vm: Component,
  4. el: ?Element,
  5. hydrating?: boolean
  6. ): Component {
  7. ...
  8. callHook(vm, 'beforeMount')
  9. //核心代码逻辑
  10. let updateComponent
  11. /* istanbul ignore if */
  12. if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  13. ...
  14. } else {
  15. updateComponent = () => {
  16. //更新Component,主要做了两个事情:render(生成vdom)、update(转换vdom为dom)
  17. vm._update(vm._render(), hydrating)
  18. }
  19. }
  20. // 在此处定义Watcher(一个Vue实例对应的是一个Watcher),并且与updateComponent关联起来
  21. new Watcher(vm, updateComponent, noop, {
  22. before () {
  23. if (vm._isMounted && !vm._isDestroyed) {
  24. callHook(vm, 'beforeUpdate')
  25. }
  26. }
  27. }, true /* isRenderWatcher */)
  28. hydrating = false
  29. if (vm.$vnode == null) {
  30. vm._isMounted = true
  31. callHook(vm, 'mounted')
  32. }
  33. return vm
  34. }

在上面的代码中,我们看到,在vue实例挂载的过程中,会新建一个Watcher。这个Watcher的作用是类似于一个观察者,它如果收到数据发生了变化的消息,那么就会执行updateComponent函数。而这个updateComponent函数,主要做了两个事情:render(生成vdom)、update(转换vdom为dom)。

综上,梳理为以下的流程图:

屏幕快照 2019-11-22 下午10.29.06.png-58.9kB

那么,Watcher的工作原理是怎样的,它是如何在整个数据响应化的过程中发挥作用的?下节课将会给你答案!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注