Vue源码阅读


数据驱动

图片描述

new Vue

new Vue 发生了什么

new关键字代表实例化一个对象, 而Vue实际上是一个类, 源码位置是/src/core/instance/index.js

clipboard.png

new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期事件propsmethodsdatacomputedwatch
clipboard.png

源码 -> _init

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

在初始化时,会调用以上_init中代码,生命周期就是通过 callHook 调用的
它的定义在 src/core/instance/lifecycle 中:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

callHook 函数的逻辑很简单,根据传入的字符串 hook,去拿到 vm.$options[hook] 对应的回调函数数组,然后遍历执行,执行的时候把 vm 作为函数执行的上下文。

callhook 函数的功能就是调用某个生命周期钩子注册的所有回调函数。

beforeCreate & created

初始化最核心的逻辑是这段:

clipboard.png

beforeCreatecreated 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的,它的定义在 src/core/instance/init.js 中:

beforeCreate 调用的时候,是获取不到 props 或者 data 中的数据的,因为这些数据的初始化都在 initState 中。

可以看到 beforeCreate 和 created 的钩子调用是在 initState 的前后,initState 的作用是初始化 props、data、methods、watch、computed 等属性,之后我们会详细分析。那么显然 beforeCreate 的钩子函数中就不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数。

在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM,一般来说,如果组件在加载的时候需要和后端有交互,放在这俩个钩子函数执行都可以,如果是需要访问 props、data 等数据的话,就需要使用 created 钩子函数。
--> 此段来自Vue.js 技术揭秘

clipboard.png

clipboard.png
这里query el

clipboard.png

接下来会执行这里的挂载函数mountComponent

clipboard.png

beforeMount 就是在挂载前执行的,然后开始创建 VDOM 并替换成真实 DOM,最后执行 mounted 钩子。

这里会有个判断逻辑,如果是外部 new Vue({}) 的话,不会存在 $vnode ,所以直接执行 mounted 钩子了。如果有子组件的话,会递归挂载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。

clipboard.png

对照生命周期图,我们看看在beforeMount钩子和mounted钩子之间的 Create vm.$el and replace "el" width it 具体都有做了什么:

clipboard.png

clipboard.png

挂载

初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在
template 的情况,需要进行「编译」步骤。

Vue 实例挂载如何实现
Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义,如 src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js。因为 $mount 这个方法的实现是和平台、构建方式都相关的。

clipboard.png

clipboard.png

clipboard.png

render

src/core/instance/render.jsVue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。

NextTick

双向绑定

SFC文件解析为SFCDescriptor

图片描述

Virtual DOM

Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用 VNode 这么一个 Class 去描述,它是定义在 src/core/vdom/vnode.js 中的。

Virtual DOM 其实就是一棵以 JavaScript 对象(VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台WeexNode 等。

VNode

实现 Virtual DOM 下的一个 VNode 节点
// VNode 就是一个 JavaScript 对象,用 JavaScript 对象的属性来描述当前节点的一些状态,
// 用 VNode 节点的形式来模拟一棵 Virtual DOM 树。
class VNode {
  constructor(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
      // 当前节点的标签名
      this.tag = tag // String
      // 当前节点的一些数据信息, 比如props,attrs等数据  
      this.data = data // VNodeData
      // 当前节点的子节点,是一个数组
      this.children = children // Array<VNode>
      // 当前节点的文本
      this.text = text // String
      // 当前虚拟节点对应的真实dom节点
      this.elm = elm // Node

      this.ns = undefined // String | Void
      // rendered in this component's scope
      this.context = context // Component | Void
    
    // real context vm for functional nodes
      this.fnContext = undefined // Component | void
      // for SSR caching
      this.fnOptions = undefined
      // functional scope id support
      this.fnScopeId = undefined

      this.key = data && data.key // String | Number | Void
    this.componentOptions = componentOptions // VNodeComponentOptions | Void
    this.componentInstance = undefined // component instance
    // Component placeholder node
    this.parent = undefined // VNode | Void

    // strictly internal 
    // contains raw HTML? (server only)
    this.raw = false // boolean 
    // hoisted static node
    this.isStatic = false // boolean
    // necessary for enter transition check
    this.isRootInsert = true // boolean
    // empty comment placeholder
    this.isComment = false // boolean
    // is a cloned node ?
    this.isCloned = false // boolean
    // is a v-once node ?
    this.isOnce = false // boolean
    // aysync component factory function
    this.asyncFactory = asyncFactory // Function
    this.asyncMeta = undefined // Oject | void
  }

  get child() {
      return this.componentInstance
  }
}

// 对VNode进一步封装,实现一些产生常用VNode方法
// 创建空节点
const createEmptyVNode = (text) => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
// 创建文本节点
function createTextVNode (val) {
    return new VNode(undefined, undefined, undefined, String(val))
}

// 克隆一个VNode节点
// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference 
function cloneVNode(vnode) {
  const cloned = new VNode(
      vnode.tag,
      vnode.data,
      // clone children array to avoid mutating original in case of cloning a child
      vnode.children && vnode.children.slice()
      vnode.text,
      vnode.elm,
      vnode.context,
      vnode.componentOptions,
      vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned    
}

如果我们有这样一个vue组件

<template>
  <span class="demo" v-show="isShow">
    This is a span.
  </span>
</template>

转化成render函数描述的js代码形式就是这样的:

vue-router

相关基础看这篇文章 -> Vue 路由知识点归纳总结

Vue.use

Vue 通用的插件注册原理

Vue 从它的设计上就是一个渐进式 JavaScript 框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。

如何注册插件 --> Vue.use: Vue 提供了 Vue.use 的全局 API 来注册这些插件
vue/src/core/global-api/use.js

/* @flow */

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  // Vue.use 接受一个 plugin 参数
  Vue.use = function (plugin: Function | Object) {
    // 并且维护了一个 _installedPlugins 数组,它存储所有注册过的 plugin
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    // 判断 plugin 有没有定义 install 方法
    if (typeof plugin.install === 'function') {
      // 如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 最后把 plugin 存储到 installedPlugins 中
    installedPlugins.push(plugin)
    return this
  }
}


// 可以看到 Vue 提供的插件注册机制很简单,
// 每个插件都需要实现一个静态的 install 方法,
// 当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,
// 并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,
// 这样的好处就是作为插件的编写方不需要再额外去import Vue 了

实例 component,props,slot

我们知道, 当vue库文件加载完后,vue的初始化中已有这个东西:

Vue.options={
    components:{
        KeepAlive:Object,
        Transition:Object,
        TransitionGroup:Object
    },
    directives:{
        show:Object,
        model:Object
    },
    filter:{},
    _base:function Vue$3(options){...}
}

这些都是vue库内置的组件和指令,当执行Vue.component、Vue.directive、Vue.filter时就是在对这些内置组件、指令、过滤器进行扩充,所以:

  var child=Vue.component('child',{
        template:'<div>child</div>',
        props:['name']
    })

执行完后

    Vue.options.components={
        KeepAlive:Object,
        Transition:Object,
        TransitionGroup:Object,
        child:function VueComponent(options)
    }

参考

https://yuchengkai.cn/docs/fr...
https://ustbhuangyi.github.io...
vue中SFC文件解析为SFCDescriptor的流程
Vue源码分析(11)--实例分析component,props,slot
vue变化侦测原理

阅读 648

推荐阅读
镜心的小树屋
用户专栏

方寸湛然GitHub组织地址:[链接]

47 人关注
123 篇文章
专栏主页