Vue.js 源码 (1)——如何开始学习

前言

最近在看 vue.js 2.x 的源码,后面会陆续的写一些笔记和大家分享和学习。

前置条件

Flow

Flow 是 facebook 出品的一个静态类型检查工具,它的语法和 Typescript 类似。vue 2.x 用了 Flow,所以没接触过的同学可能需要先了解一下。官网地址: flow.org/

Virtual Dom

在学习 vue.js 之前,我们需要先了解一下 Virtual Dom —— 虚拟DOM。

Virtual DOM(虚拟DOM),在形态上表现为一个能够描述DOM结构及其属性信息的普通的JS对象,因为不是真实的DOM对象,所以叫虚拟DOM。

目前,社区里有两个 virtual dom 库,一个是 virtual-dom , 另一个是 snabbdom 。前者已经有5年没更新了,后者还在更新,特别指出,vue 2.x 引入的 virtual dom 和 diff 算法就是基于 snabbdom 改造的。所以,建议没了解过 snabbdom 可以看看相关的文章。

响应式(双向绑定)

关于 vue 的响应式原理,我们在看到相关代码时,再具体分析。我们现在只需要知道它是通过Object.defineProperty() 定义存取器 gettersetter 来实现的。

准备工作

从github上获取源码

项目地址:github.com/vuejs/vue 。fork 一份到自己仓库,克隆到本地,从 dev 创建一个 study 的学习分支,方便写注释。

源码结构

我们先看一下vue源码的目录结构,vue 源码包含以下几个目录:

src
├── compiler // 编译器,将 template 编译成 render 函数
├── core // Vue构造函数,一些静态方法,vdom和响应式原理
├── platforms // web/weex
├── server // 服务端渲染
├── sfc // *.vue 单文件组件的编译方法
└── shared // 公用的工具函数
复制代码

后续我们主要需要研究的是 前三个 目录里的代码,即编译器、核心代码和 web 平台相关的代码。

如何调试

vue 2.x 是使用 rollup 编译的,添加 --sourcemap 编译选项,可以生成 sourcemap

{
    "scripts": {
        "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
    }
}
复制代码

我们可以新建一个示例,打开 chrome 开发者工具中的 source 标签,设置断点来调试。下图的 src 就是开启了sourcemap 后出现的

image.png

简单示例:

<div id="app>{{count}}</div>
<script scr="xxx/vue.js"></script> // 为了方便看源码,引用没有压缩混淆过的 vue
<script>
    var vm = new Vue({
        data: {
            count: 1
        }
    })
    
   vm.$mount("#app")

</script>
复制代码

入口文件

我们直接从完整版的 vue 的入口文件开始看,即 src/platform/web/entry-runtime-with-compiler.js。从命名上可以看出来,这是带有模版编译器的运行时版本。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el) // document.querySelector 获取 dom 元素

  const options = this.$options
  // 如果没有 render 函数,则使用 template
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
            // 如果 template 是选择器符号,则获取对应 dom 的 innerHTML。
            // <script type="text/x-template>...</script>">
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        return this
      }
    } else if (el) {
      // 如果没有 template,则把 el 的 outerHTML 作为 template
      template = getOuterHTML(el)
    }
    if (template) {
      // 将 template 转换成渲染函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 调用原先定义的 mount 方法挂载
  return mount.call(this, el, hydrating)
}

复制代码

带有编译器的版本,扩展了 mount 方法。如果用户没有定义 render 函数,会尝试寻找 template 模版,如果 template 是选择器描述符,则获取内部的 html ,最终都是编译成 render 函数来初始化。

mount 挂载

下面,我们转到 src/platform/web/runtime/index.js 文件中,看不带编译器的 $mount 是如何运行的。

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
复制代码

获取需要挂载的 dom节点,然后调用 mountComponent 方法。我们继续转到src/core/instance/lifecycle.jsmountComponent

function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el // 需要挂载的 dom 元素赋值给 $el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount') // 触发 beforeMount 钩子函数

  let updateComponent
  updateComponent = () => {
      // _update 中会调用 patch, _render 会生成 vdom 树
      vm._update(vm._render(), hydrating)
    }
  // updateComponent 会在 Watcher 实例化过程中被调用
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  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
}

复制代码

如果没有提供 el 和 render 函数,则创建一个空的 vnode, 并触发 beforeMount 钩子。

updateComponent 函数用来比对 virtual dom,并更新dom节点。

接下来新建一个渲染 watcher,每个组件对应一个渲染 watcher,一个渲染 watcher 对应多个 dep。watcher 在实例化的过程中,会执行 updateComponent 函数,每个状态的 dep 都会把 watcher 添加到订阅列表中。当用户修改 vm.$data 上的属性时,对应的 dep 会调用 notify 方法,通知订阅的 watchers 依次 update,也就是更新组件。

从Vue.js 2.0开始,它引入了虚拟DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。这样状态变化后,会通知到组件,组件内部再使用虚拟DOM进行比对。这可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。—— 《深入浅出Vue.js》

总结

vm.$mount(el) 做了什么?

首先 el 必填。

  • 如果 vm 提供了 render 函数,优先使用。
  • 如果没有 render,再看是否提供了 template, 如果 template 是个选择器符,则获取对应 dom 的 innerHTML,
  • 如果 template 不是选择器符,则把它编译成 render 函数。
  • 如果 render 和 template 都没有,把 el 选择器对应的 dom 的 outerHTML 作为模版编译成 render 函数。
  • 最终,运行 render 函数生成 vnode,交给 vm._update 执行 patch(vnode 的 diff 运算,并挂载到真实 Dom 上)
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享