资讯专栏INFORMATION COLUMN

vue 源码学习(二) 实例初始化和挂载过程

时飞 / 2694人阅读

摘要:最后判断有无根节点,无则表示首次挂载,添加钩子函数,返回总结实例初始化挂载方法属性初始化挂载过程在版本,生成函数对作处理,执行中定义了通过实例化的回调执行执行,即调用了真实渲染成对象。

vue 入口

从vue的构建过程可以知道,web环境下,入口文件在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compiler模式构建,vue直接运行在浏览器进行编译工作)

import Vue from "./runtime/index"

下一步,找到./runtime/index,发现:

import Vue from "core/index"

下一步,找到core/index,发现:

import Vue from "./instance/index"

按照这个思路找,最后发现:Vue是在"core/index"下定义的

import { initMixin } from "./init"
import { stateMixin } from "./state"
import { renderMixin } from "./render"
import { eventsMixin } from "./events"
import { lifecycleMixin } from "./lifecycle"
import { warn } from "../util/index"

function Vue (options) {
  if (process.env.NODE_ENV !== "production" &&
    !(this instanceof Vue)
  ) {
    warn("Vue is a constructor and should be called with the `new` keyword")
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

引入方法,用function定义了Vue类,再以Vue为参数,调用了5个方法,最后导出了vue

可以进入这5个文件查看相关方法,主要就是在Vue原型上挂载方法,可以看到,Vue 是把这5个方法按功能放入不同的模块中,这很利于代码的维护和管理

initGlobalAPI

回到core/index.js, 看到除了引入已经在原型上挂载方法后的 Vue 外,还导入initGlobalAPI 、 isServerRendering、FunctionalRenderContext,执行initGlobalAPI(Vue),在vue.prototype上挂载$isServer、$ssrContext、FunctionalRenderContext,在vue 上挂载 version 属性,

看到initGlobalAPI的定义,主要是往vue.config、vue.util等上挂载全局静态属性和静态方法(可直接通过Vue调用,而不是实例调用),再把builtInComponents 内置组件扩展到Vue.options.components下。此处大致了解下它是做什么的即可,后面用到再做具体分析。

new Vue()

一般我们用vue都采用模板语法来声明:

{{ message }}
var app = new Vue({
  el: "#app",
  data: {
    message: "Hello Vue!"
  }
})

new Vue()时,vue做了哪些处理?

function Vue (options) {
  if (process.env.NODE_ENV !== "production" &&
    !(this instanceof Vue)
  ) {
    warn("Vue is a constructor and should be called with the `new` keyword")
  }
  this._init(options)
}

看到vue只能通过new实例化,否则报错。实例化vue后,执行了this._init(),该方法在通过initMixin(Vue)挂载在Vue原型上的,找到定义文件core/instance/init.js 查看该方法。

_init()

一开始在this对象上定义_uid、_isVue,判断options._isComponent,此次先不考虑options._isComponenttrue的情况,走else,合并options,接着安装proxy, 初始化生命周期,初始化事件、初始化渲染、初始化data、钩子函数等,最后判断有vm.$options.el则执行vm.$mount(),即是把el渲染成最终的DOM

初始化data 数据绑定

_init()中通过initState()来绑定数据到vm上,看下initState的定义:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

获取options,初始化propsmethodsdata、计算属性、watch绑定到vm上,先来看下initData()是如何把绑定data的:

先判断data是不是function类型,是则调用getData,返回data的自调用,不是则直接返回data,并将data赋值到vm._data

data、props、methods,作个校验,防止出现重复的key,因为它们最终都会挂载到vm上,都是通过vm.key来调用

通过proxy(vm, `_data`, key)把每个key都挂载在vm

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

proxy() 定义了一个get/set函数,再通过Object.defineProperty定义修改属性(不了解Object.defineProperty()的同学可以先看下文档,通过Object.defineProperty()定义的属性,通过描述符的设置可以进行更精准的控制对象属性),将对target的key访问加了一层get/set,即当访问vm.key时,实际上是调用了sharedPropertyDefinition.get,返回this._data.key,这样就实现了通过vm.key来调用vm._data上的属性

最后,observe(data, true /* asRootData */) 观察者,对数据作响应式处理,这也是vue的核心之一,此处先不分析

$mount() 实例挂载

Vue的核心思想之一是数据驱动,在vue下,我们不会直接操作DOM,而是通过js修改数据,所有逻辑只需要考虑对数据的修改,最后再把数据渲染成DOM。其中,$mount()就是负责把数据挂载到vm,再渲染成最终DOM

接下来将会分析下 vue 是如何把javaScript对象渲染成dom元素的,和之前一样,主要分析主线代码

预处理

还是从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)
  ···
}

首先将原先原型上的$mount方法缓存起来,再重新定义$mount

先判断 elel 不能是 body, html ,因为渲染出来的 DOM 最后是会替换掉el

判断render方法, 有的话直接调用mount.call(this, el, hydrating)

没有render方法时:

判断有没有template ,有则用compileToFunctions将其编译成render方法

没有template时,则查看有没有el,有转换成template,再用compileToFunctions将其编译成render方法

render挂载到options下

最后调用 mount.call(this, el, hydrating),即是调用原先原型上的mount方法

我们发现这一系列调用都是为了生成render函数,说明在vue中,所有的组件渲染最终都需要render方法(不管是单文件.vue还是el/template),vue 文档里也提到:

Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。
原先原型上的mount方法

找到原先原型上的mount方法,在src/platform/web/runtime/index.js中:

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

这个是公用的$mount方法,这么设计使得这个方法可以被 runtime onlyruntime+compiler 版本共同使用

$mount 第一个参数el, 表示挂载的元素,在浏览器环境会通过query(el)获取到dom对象,第二个参数和服务端渲染相关,不进行深入分析,此处不传。接着调用mountComponent()

看下query(),比较简单,当el string时,找到该选择器返回dom对象,否则新创建个div dom对象,eldom对象直接返回el.

mountComponent

mountComponent定义在src/core/instance/lifecycle.js中,传入vm,el,

el缓存在vm.$el

判断有没有render方法,没有则直接把createEmptyVNode作为render函数

开发环境警告(没有Render但有el/template不能使用runtime-only版本、rendertemplate必须要有一个)

挂载beforeMount钩子

定义 updateComponent , 渲染相关

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher() 实例化一个渲染watcher,简单看下定义,
this.getter = expOrFn
updateComponent挂载到this.getter
this.value = this.lazy ? undefined : this.get()

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {...}
  return value
}

执行this.get(),则执行了this.getter,即updateComponent,所以new Watcher()时会执行updateComponent,也就会执行到vm._update、vm._render方法。

因为之后不止初始化时需要渲染页面,数据发生变化时也是要更新到dom上的,实例watcher可以实现对数据进行监听以及随后的更新dom处理,watcher会在初始化执行回调,也会在数据变化时执行回调,此处先简单介绍为什么要使用watcher,不深入分析watcher实现原理。

最后判断有无根节点,无则表示首次挂载,添加mounted钩子函数 ,返回vm

总结

实例初始化:new Vue()->挂载方法属性->this._init->初始化data->$mount

挂载过程:(在complier版本,生成render函数)对el作处理,执行mountComponent,mountComponent中定义了updateComponent,通过实例化watcher的回调执行updateComponent,执行updateComponent,即调用了vm._update、vm._render真实渲染成dom对象。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100853.html

相关文章

  • Vue源码学习)——从宏观看Vue

    摘要:上一篇文章我们写到从入口文件一步步找到的构造函数,现在我们要去看看实例化经历的过程的构造函数我们知道的构造函数在中不明白的可以去看上一篇文章源码学习笔记一。 上一篇文章我们写到从入口文件一步步找到Vue的构造函数,现在我们要去看看Vue实例化经历的过程 Vue的构造函数 我们知道Vue的构造函数在src/core/instance/index.js中,不明白的可以去看上一篇文章 Vue...

    AndroidTraveler 评论0 收藏0
  • Vue原理】Component - 源码版 之 挂载组件DOM

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之挂载组件由这篇文章从模 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于...

    lbool 评论0 收藏0
  • Vue原理】从模板到DOM的简要流程

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理从模板到的简要流程今天的计划是, 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基...

    wenzi 评论0 收藏0
  • vue源码阅读之数据渲染过程

    摘要:图在中应用三数据渲染过程数据绑定实现逻辑本节正式分析从到数据渲染到页面的过程,在中定义了一个的构造函数。一、概述 vue已是目前国内前端web端三分天下之一,也是工作中主要技术栈之一。在日常使用中知其然也好奇着所以然,因此尝试阅读vue源码并进行总结。本文旨在梳理初始化页面时data中的数据是如何渲染到页面上的。本文将带着这个疑问一点点追究vue的思路。总体来说vue模版渲染大致流程如图1所...

    AlphaGooo 评论0 收藏0
  • Vue源码学习vue实例化到挂载到dom(上)

    摘要:作者王聪本篇目的是介绍实例化到挂载到的整体路线,一些细节会被省略。从源码中找到构造函数的声明,是一个很简洁的工厂模式声明的一个构造函数。内部做了逻辑判断构造函数调用必须有关键字。代表的是当前实例也就是构造函数被调用后的指向。 作者:王聪本篇目的是介绍vue实例化到挂载到dom的整体路线,一些细节会被省略。 从new Vue()开始 所有的一切都是从 new Vue()开始的,所以从这个...

    Hegel_Gu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<