Vue源码解读

一、Vue源码解析–响应式原理

1、课程目标

  • Vue.js的静态成员和实例成员初始化过程
  • ​ 首次渲染的过程
  • 数据响应式原理

2、准备工作

Vue源码的获取

项目地址:https://github.com/vuejs/vue

为什么分析Vue2.6? 新的版本发布后,现有项目不会升级到3.0,2.x还有很长的一段过渡期。

3.0项目地址https://github.com/vuejs/vue-next

源码目录结构(在src目录下面定义的就是源码内容):

compiler: 编译相关(主要作用:就是把模板转换成render函数,在render函数中创建虚拟DOM)
core:Vue核心库
platforms:平台相关代码,web:基于web的开发,weex是基于移动端的开发
server:SSR,服务端渲染
sfc:将.vue文件编译为js对象
shared:公共的代码

​ 在core目录是Vue的核心库,在core目录下面,也定义了很多的文件夹,下面我们先简单来看一下。

components目录下面定义的是keep-alive.js组件。

global-api:定义的是Vue中的静态方法。vue.filter,vue.extend,vue.mixin,vue.use等。

Instance:创建vue的实例,定义了Vue的构造函数,初始化,以及生命周期的钩子函数等。

observer:定义响应式机制的位置,

util:定义公共成员。

vodom:定义虚拟DOM

3、打包

这里我们来介绍一下,关于Vue源码中使用的打包方式。

打包工具Rollup

Vue.js 所使用的打包工具为Rollup,RollupWebpack更加轻量,Webpack是把所有的文件(例如:图片文件,样式等)当作模块进行打包,Rollup只处理js文件,所以Rollup更适合在Vue.js这样的库中进行使用。

Rollup打包不会生成冗余的代码,如果是Webpack打包,那么会生成一些浏览器支持模块化的代码。

以上就是WebpackRollup之间的区别。根据以上的讲解,其实我们可以总结出,Rollup更适合在库的开发中使用,Webpack更适合在项目开发中使用,所以它们各自有自己的应用场景。

下面看一下打包的步骤:

第一步:安装依赖

npm i

第二步设置:sourcemap

sourcemap是代码地图,在sourcemap中记录了打包后的代码与源码之间的对应关系。如果出错了,也会告诉我们源码中的第几行出错了。怎样设置sourcemap呢?在package.json 文件中的dev脚本中添加--sourcemap.

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

第三步:执行dev,运行npm run dev执行打包,用的是rollup,-w 参数是监听源码文件的变化,源码文件变化后自动的重新进行打包。-c是设置配置文件,scripts/config.js就是配置文件,environment环境变量,通过后面设置的值,来打包生成不同版本的Vue.web-full-dev:web:指的是打包web平台下的,full:表示完整版,包含了编译器与运行时,dev:表示的是开发版本,不会对代码进行压缩。

web-runtime-cjs-dev: runtime:表示运行时,cjs:表示CommonJS模块。

在执行npm run dev进行打包之前,可以先来看一下dist目录,该目录下面已经有很多的js文件,这些文件针对的是不同版本的Vue.那么为了更好的看到,执行npm run dev命令后的打包效果,在这里可以将这些文件先删除掉。

4、Vue不同版本说明

https://cn.vuejs.org/v2/guide/installation.html#对不同构建版本的解释

完整版:同时包含编译器运行时版本。

​ 什么是编译器?用来将模板字符串编译成为javascript渲染函数(render函数,render函数用来生成虚拟DOM)的代码,体积大,效率低。

​ 什么是运行时?用来创建Vue实例,渲染并处理虚拟DOM等的代码,体积小,效率高,基本上就是除去编译器的代码。

还有一点需要说明的是:Vue包含了不同的模块化方式。

UMD:指的是通用的模块版本,支持多种模块方式,UMD 版本可以通过 <script> 标签直接用在浏览器中

CommonJSCommonJS 版本用来配合老的打包工具比如 [Browserify](http://browserify.org/) 或 [webpack 1](https://webpack.github.io/)

ES Module:从 2.6 开始 Vue 会提供两个 ES Modules (ESM,也是ES6的模块化方式,这时标准的模块化方式,后期会使用该方式替换其它的模块化方式) 构建文件:

  • 为打包工具提供的 ESM:为诸如 [webpack 2](https://webpack.js.org/)[Rollup](https://rollupjs.org/) 提供的现代打包工具。ESM 格式被设计为可以被静态分析(在编译的时候进行代码的处理也就是解析模块之间的依赖,而不是运行时),所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件 (pkg.module) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js)。
  • 为浏览器提供的 ESM (2.6+):用于在现代浏览器中通过 <script type="module"> 直接导入。

如果使用vue-cli创建的项目,默认的就是运行时版本,并且使用的是ES6的模块化方式。

同时使用vue-cli创建的项目中,有很多的.vue文件,而这些文件浏览器是不支持的,所以在打包的时候,会将这些单文件转换成js对象,在转换js对象的过程中,会将.vue文件中的template转换成render函数。

所以单文件组件在运行的时候也是不需要编译器的。

5、寻找入口文件

查看vue的源码,就需要找到对应的入口文件。

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

可以从srcipts/config.js这个配置文件中进行查找。

该配置文件中的内容是比较多的。所以可以看一下文件的底部,底部导出了相应的内容、

如下所示:

//判断环境变量是否有`TARGET`
//如果有的话,使用`genConfig()`生成`rollup`配置文件。
if (process.env.TARGET) {module.exports = genConfig(process.env.TARGET)
} else {exports.getBuild = genConfigexports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

下面,我们看一下genConfig方法的代码实现

getConfig方法中有一行代码如下所示:

 const opts = builds[name]

我们看到在builds这个对象中,该对象中的属性就是:环境变量的值。

由于在package.json文件中,关于dev的配置中的环境变量的值为web-full-dev.

所以下面,我们在builds对象中查找该属性对应的内容。

具体内容如下:

 // Runtime+compiler development build (Browser)'web-full-dev': {//表示入口文件,我们查找的就是该文件。entry: resolve('web/entry-runtime-with-compiler.js'),//出口,打包后的目标文件dest: resolve('dist/vue.js'),//模块化的方式,这里是umdformat: 'umd',//打包方式,env的取值可以是开发模式或者是生产模式env: 'development',//别名,这里先不用关系alias: { he: './entity-decoder' },//表示的就是文件的头,打包好的文件的头部信息。banner},

web-full-dev中定义的就是在打包的时候,需要的一些配置的基本信息。

通过以上代码的注意,我们知道,web-full-dev打包的是完整版,包含了运行时与编译器。

下面我们来看一下,入口文件,入口文件的地址为web/entry-runtime-with-compiler.js, 但是问题是在scripts目录中,我们没有发现web目录,我们进入reslove方法看一下,

const aliases = require('./alias')//导入alias模块
const resolve = p => {//根据传递过来的参数,安装`/`进行分隔,然后获取第一项内容。//很明显这里获取的是 webconst base = p.split('/')[0]//根据获取到的`web`,从aliases中获取一个值,下面看一下aliases中的内容。if (aliases[base]) {//aliases[base]的值:src/platforms/web// p的值为:web/entry-runtime-with-compiler.js//p.slice(base.length + 1):获取到的就是entry-runtime-with-compiler.js//整个返回的内容是:src/platforms/web/entry-runtime-with-compiler.js 的绝对路径并返回return path.resolve(aliases[base], p.slice(base.length + 1))} else {return path.resolve(__dirname, '../', p)}
}

aliases中的内容定义在scripts/alias.js文件,具体的代码如下:

const path = require('path')const resolve = p => path.resolve(__dirname, '../', p)module.exports = {vue: resolve('src/platforms/web/entry-runtime-with-compiler'),compiler: resolve('src/compiler'),core: resolve('src/core'),shared: resolve('src/shared'),web: resolve('src/platforms/web'),weex: resolve('src/platforms/weex'),server: resolve('src/server'),sfc: resolve('src/sfc')
}

通过上面的代码,我们可以看到这里是通过path.resolve获取到了当前的绝对路径,并且是在scripts目录的上一级src下面去查找platforms/web目录中的内容。

下面我们继续来看一下genConfig方法。

function genConfig (name) {//获取到了关于配置的基础信息const opts = builds[name]//config对象就是所有的配置信息const config = {input: opts.entry,//入口external: opts.external,plugins: [flow(),alias(Object.assign({}, aliases, opts.alias))].concat(opts.plugins || []),output: {file: opts.dest,//出口format: opts.format,banner: opts.banner,name: opts.moduleName || 'Vue'},onwarn: (msg, warn) => {if (!/Circular/.test(msg)) {warn(msg)}}}// built-in varsconst vars = {__WEEX__: !!opts.weex,__WEEX_VERSION__: weexVersion,__VERSION__: version}// feature flagsObject.keys(featureFlags).forEach(key => {vars[`process.env.${key}`] = featureFlags[key]})// build-specific envif (opts.env) {vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)}config.plugins.push(replace(vars))if (opts.transpile !== false) {config.plugins.push(buble())}Object.defineProperty(config, '_name', {enumerable: false,value: name})
//将配置信息返回return config
}

6、从入口开始

通过上一小节的内容,我们已经找到了对应的入口文件src/platform/web/entry-runtime-with-compiler.js

下面我们要对入口文件进行分析,在分析的过程中,我们要解决一个问题,如下代码所示:

const vm=new Vue({el:'#app',
template:'<h3>hello template</h3>'
render(h){return h('h4','hello render')
}
})

在上面的代码中,我们在创建Vue的实例的时候,同时指定了templaterender,那么会渲染执行哪个内容?

一会我们通过查看源码来解决这个问题。

下面我们打开入口文件。

先来看一下$mount

//保留Vue实例的$mount方法
const mount = Vue.prototype.$mount;
//$mout:挂载,作用就是把生成的DOM挂载到页面中。
Vue.prototype.$mount = function (el?: string | Element,//非ssr情况下为false,ssr的时候为truehydrating?: boolean
): Component {//获取el选项,创建vue实例的时候传递过来的选项。//el就是DOM对象。el = el && query(el);/* istanbul ignore if *///如果el为body或者是html,并且是开发环境,那么会在浏览器的控制台//中输出不能将Vue的实例挂载到<html>或者是<body>标签上if (el === document.body || el === document.documentElement) {process.env.NODE_ENV !== "production" &&warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`);//直接返回vue的实例return this;}//获取options选项const options = this.$options;// resolve template/el and convert to render function//判断options中是否有render(在创建vue实例的时候,也就new Vue的时候是否传递了render函数)if (!options.render) {//没有传递render函数。获取template模板,然后将其转换成render函数//关于将`template`转换成render的代码比较多,目录先知道其主要作用就可以了let template = options.template;if (template) {if (typeof template === "string") {//如果是id选择器if (template.charAt(0) === "#") {//获取对应的DOM对象的innerHTML,作为模板template = idToTemplate(template);/* istanbul ignore if */if (process.env.NODE_ENV !== "production" && !template) {warn(`Template element not found or is empty: ${options.template}`,this);}}} else if (template.nodeType) {//如果模板是元素,返回元素的innerHTMLtemplate = template.innerHTML;} else {//如果不是字符串,也不是元素,在开发环境中会给出警告信息,模板不合法if (process.env.NODE_ENV !== "production") {warn("invalid template option:" + template, this);}//返回Vue实例。return this;}} else if (el) {//如果选项中没有设置template模板,那么获取el的outerHTML 作为模板。template = getOuterHTML(el);}if (template) {/* istanbul ignore if */if (process.env.NODE_ENV !== "production" && config.performance && mark) {mark("compile");}
//把template模板编译成render函数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;/* istanbul ignore if */if (process.env.NODE_ENV !== "production" && config.performance && mark) {mark("compile end");measure(`vue ${this._name} compile`, "compile", "compile end");}}}//如果创建 Vue实例的时候,传递了render函数,这时会直接调用mount方法。// mount方法的作用就是渲染DOM,这块内容在下一小节会讲解到。return mount.call(this, el, hydrating);
};

下面代码是query方法实现的代码。

/*** Query an element selector if it's not an element already.*/
export function query(el: string | Element): Element {// 如果el等于字符串,表明是选择器。//否则是DOM对象,直接返回if (typeof el === "string") {//获取对应的DOM元素const selected = document.querySelector(el);if (!selected) {//如果没有找到,判断是否为开发模式,如果是开发模式//在控制台打印“找不到元素”process.env.NODE_ENV !== "production" &&warn("Cannot find element: " + el);//这时会创建一个`div`元素返回。return document.createElement("div");}//返回找到的dom元素return selected;} else {return el;}
}

看完上面的代码后,我们就可以回答最开始的时候,提出的问题,如果传递了render函数,是不会处理template这个模板的,直接调用mount方法渲染dom

现在面临的一个问题就是$mount这个方法是在哪儿被调用的呢?

core/instance/init.js文件中,查找到如下代码:

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

通过以上代码,可以看到调用了$mount方法。

也就是在Vue._init方法中调用的。

那么_init方法是在哪被调用的呢?
core/instance/index.js文件中、

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这个方法中调用了_init方法,而Vue方法,是在创建Vue实例的时候被调用的。所以上面的Vue方法就是一个构造函数。

现在我们将以上的内容做一个总结,重点是以下三点内容。

  • el不能是body 或者是html标签
  • 如果没有render,把template转换成render函数。
  • 如果有render方法,直接调用mount挂载DOM

7、Vue的初始化过程

在这一小节中,我们需要考虑如下的一个问题。

Vue实例成员和Vue的静态成员是从哪里来的?

src/platforms/web目录下面定义的文件都是与平台有关的文件。

下面我们还是看一下开始文件:entry-runtime-with-compiler.js文件。

/* @flow */import config from "core/config";
import { warn, cached } from "core/util/index";
import { mark, measure } from "core/util/perf";
//导入Vue的构造函数
import Vue from "./runtime/index";
import { query } from "./util/index";
import { compileToFunctions } from "./compiler/index";
import {shouldDecodeNewlines,shouldDecodeNewlinesForHref,
} from "./util/compat";const idToTemplate = cached((id) => {const el = query(id);return el && el.innerHTML;
});
//保留Vue实例的$mount方法,方便下面重写$mount的功能
const mount = Vue.prototype.$mount;
//$mout:挂载,作用就是把生成的DOM挂载到页面中。
Vue.prototype.$mount = function (el?: string | Element,//非ssr情况下为false,ssr的时候为truehydrating?: boolean
): Component {//获取el选项,创建vue实例的时候传递过来的选项。//el就是DOM对象。el = el && query(el);/* istanbul ignore if *///如果el为body或者是html,并且是开发环境,那么会在浏览器的控制台//中输出不能将Vue的实例挂载到<html>或者是<body>标签上if (el === document.body || el === document.documentElement) {process.env.NODE_ENV !== "production" &&warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`);//直接返回vue的实例return this;}//获取options选项const options = this.$options;// resolve template/el and convert to render function//判断options中是否有render(在创建vue实例的时候,也就new Vue的时候是否传递了render函数)if (!options.render) {//没有传递render函数。获取template模板,然后将其转换成render函数//关于将`template`转换成render的代码比较多,目录先知道其主要作用就可以了let template = options.template;// 如果模板存在if (template) {//判断对应的类型如果是字符串if (typeof template === "string") {//如果模板是id选择器if (template.charAt(0) === "#") {//获取对应的DOM对象的innerHTMLtemplate = idToTemplate(template);/* istanbul ignore if */if (process.env.NODE_ENV !== "production" && !template) {warn(`Template element not found or is empty: ${options.template}`,this);}}} else if (template.nodeType) {//如果模板是元素,返回元素的innerHTMLtemplate = template.innerHTML;} else {if (process.env.NODE_ENV !== "production") {warn("invalid template option:" + template, this);}return this;}} else if (el) {//如果模板不存在template = getOuterHTML(el);}if (template) {/* istanbul ignore if */if (process.env.NODE_ENV !== "production" && config.performance && mark) {mark("compile");}
//把template模板编译成render函数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;/* istanbul ignore if */if (process.env.NODE_ENV !== "production" && config.performance && mark) {mark("compile end");measure(`vue ${this._name} compile`, "compile", "compile end");}}}//如果创建 Vue实例的时候,传递了render函数,这时会直接调用mount方法。// mount方法的作用就是渲染DOM,这里的mount就是下面我们要看的./runtime/index文件中的$mount,只不过// 在当前的文件中重写了。return mount.call(this, el, hydrating);
};/*** Get outerHTML of elements, taking care* of SVG elements in IE as well.*/
function getOuterHTML(el: Element): string {if (el.outerHTML) {//如果有outerHTML属性,返回内容的HTML形式return el.outerHTML;} else {//创建divconst container = document.createElement("div");//把el的内容克隆,然后追加到div中container.appendChild(el.cloneNode(true));//返回div的innerHTMLreturn container.innerHTML;}
}
//注册Vue.compile方法,根据HTML字符串返回render函数
Vue.compile = compileToFunctions;export default Vue;

通过查看该文件中的代码,可以发现,在该文件中并没有创建Vue的实例,关于实例的创建在如下导入的文件中。

//导入Vue的构造函数
import Vue from "./runtime/index";

通过前面的讲解,我们知道在入口文件中,最主要的方法是mount.

//保留Vue实例的$mount方法,方便下面重写$mount的功能
const mount = Vue.prototype.$mount;

在该方法中,很重要的一个操作就是将template模板,转换成render函数。

下面,我们先来看一下./runtime/index文件中的内容。

// install platform specific utils
//给Vue.config注册了方法,这些方法都是与平台相关的方法。这些方法是在Vue内部使用的。Vue.config.mustUseProp = mustUseProp;
//是否为保留的标签,也就是说,传递过来的内容是否为HTML中特有的标签
Vue.config.isReservedTag = isReservedTag;
//是否是保留的属性,也就是说,传递过来的内容是否为HTML中特有的属性
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;

以上内容简短了解一下就可以。

下面我们继续查看如下内容:

// install platform runtime directives & components
//通过extend方法注册了与平台相关的全局的指令与组件。
//extend的作用就是将第二个参数的成员全部拷贝到第一个参数中
//那么问题是注册了哪些指令与组件呢?
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

那么问题是注册了哪些指令与组件呢?

我们可以查看,extend的第二个参数,以上extend参数的内容,分别来自如下两个文件。

import platformDirectives from "./directives/index";
import platformComponents from "./components/index";

我们首先看一下./directives/index文件中的内容,如下所示:

import model from './model'
import show from './show'export default {model,show
}

通过以上代码,我们可以看到这个文件中导出了v-showv-model这两个指令。

下面再看一下./components/index文件中的内容。

import Transition from './transition'
import TransitionGroup from './transition-group'export default {Transition,TransitionGroup
}

以上文件导出的就是v-Transitionv-TransitionGroup这两个组件的内容。

通过下面两行代码,我们还可以发现一个相应的问题。

extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

就是全局的指令与组件分别存储到了Vue.options.directivesVue.options.components中。

例如,我们在项目中使用Vue.component注册的组件,都存储到了Vue.options.components中,也就说存储了Vue.options.components中的组件都是全局可以访问的。

我们继续向下看,如下代码

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop;

以上代码就是在Vue的原型中注册了__patch__函数,该函数对我们来说比较熟悉了,在学习虚拟DOM的时候,我们知道patch函数的作用就是将虚拟DOM转换成真实的DOM.在给patch函数赋值的时候,首先判断是否为浏览器的环境,如果是则返回patch,否则返回noop,noop是一个空函数。

这里有一个问题就是:inBrowser是怎样判断是否为浏览器环境的呢?

inBrowser定义在如下文件中import { devtools, inBrowser } from "core/util/index";

该文件的代码如下:

/* @flow */export * from 'shared/util'
export * from './lang'
export * from './env'
export * from './options'
export * from './debug'
export * from './props'
export * from './error'
export * from './next-tick'
export { defineReactive } from '../observer/index'

通过以上的代码,我们并没有发现isBrowser的定义,那么很明显都被封装到具体的文件中了。

isBrowser是与环境有关的内容,所以很明显来自env这个文件,在该文件中,可以看到如下代码:

export const inBrowser = typeof window !== 'undefined'

如果window对象不等于undefined,表明当前的环境就是浏览器的环境

//给Vue原型注册了$mount方法,也就是给Vue的实例注册了$mount方法,在entry-runtime-with-compiler.js文件中对该方法进行了重写。
//在该方法中调用了mountComponent方法,用来渲染DOM
Vue.prototype.$mount = function (el?: string | Element,hydrating?: boolean
): Component {//这里重新获取el,并且判断是否为浏览器环境.//这里有一个问题,就是为什么要重新获取el,在entry-runtime-with-compiler.js文件中重写$mount方法的时候也获取了el,这里为什么要重新获取呢?//原因是:entry-runtime-with-compiler.js是带编译器版本的Vue,而当前文件是运行时版本执行的,那么就不会执行entry-runtime-with-compiler.js文件来获取el,所以这里必须要重新获取el。el = el && inBrowser ? query(el) : undefined;return mountComponent(this, el, hydrating);
};

下面看一下mountComponent方法的内部实现。

import { mountComponent } from "core/instance/lifecycle";
export function mountComponent (vm: Component,el: ?Element,hydrating?: boolean
): Component {vm.$el = el//把el赋值给Vue实例中的$el属性//判断$options中是否有render函数。if (!vm.$options.render) {vm.$options.render = createEmptyVNodeif (process.env.NODE_ENV !== 'production') {/* istanbul ignore if *///如果在运行时环境中,如果使用了template模板,会出现如下的警告信息。//警告信息:使用的是运行时版本,编译器无效,应该使用完整版,或者是编写render函数。if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||vm.$options.el || el) {warn('You are using the runtime-only build of Vue where the template ' +'compiler is not available. Either pre-compile the templates into ' +'render functions, or use the compiler-included build.',vm)} else {warn('Failed to mount component: template or render function not defined.',vm)}}}//触发beforeMount钩子函数,表示的是挂载之前。callHook(vm, 'beforeMount')let updateComponent //完成组件的更新/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {updateComponent = () => {const name = vm._nameconst id = vm._uidconst startTag = `vue-perf-start:${id}`const endTag = `vue-perf-end:${id}`mark(startTag)const vnode = vm._render()mark(endTag)measure(`vue ${name} render`, startTag, endTag)mark(startTag)vm._update(vnode, hydrating)mark(endTag)measure(`vue ${name} patch`, startTag, endTag)}} else {//重点看这一段代码:// vm._render():表示执行的是用户传入的render函数,或者是执行编译器生成的render函数。// render( )函数最终会返回虚拟DOM,把返回的虚拟DOM传递给_update函数。// _update函数,会将虚拟DOM转换成真实的DOM。//也就说该方法执行完毕后,页面会呈现出具体的内容。updateComponent = () => {vm._update(vm._render(), hydrating)}}// we set this to vm._watcher inside the watcher's constructor// since the watcher's initial patch may call $forceUpdate (e.g. inside child// component's mounted hook), which relies on vm._watcher being already defined//在 Watcher中调用了updateComponent方法。// 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 hookif (vm.$vnode == null) {vm._isMounted = true//触发了mounted钩子函数,表明页面已经挂载完毕了。callHook(vm, 'mounted')}return vm
}

mountComponent后面的代码就是一些调试的代码,这里也不要在看。

下面,把我们看到的代码总结一下。以上代码定义的是与平台有关的代码,主要是注册了patch方法,以及$mount方法。还有就是注册了全局的指令与组件。

但在当前的这个文件中,我们还是没有看到Vue的构造函数。

在该文件的顶部,可以看到如下导入的语句。

import Vue from "core/index";

该文件中的代码如下:

import Vue from "./instance/index";
import { initGlobalAPI } from "./global-api/index";
import { isServerRendering } from "core/util/env";
import { FunctionalRenderContext } from "core/vdom/create-functional-component";
//给Vue构造函数注册一些静态的方法。
initGlobalAPI(Vue);
//以下通过defineProperty定义的内容都是与服务端渲染有关的内容。
Object.defineProperty(Vue.prototype, "$isServer", {get: isServerRendering,
});Object.defineProperty(Vue.prototype, "$ssrContext", {get() {/* istanbul ignore next */return this.$vnode && this.$vnode.ssrContext;},
});// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, "FunctionalRenderContext", {value: FunctionalRenderContext,
});
//指定Vue版本。
Vue.version = "__VERSION__";export default Vue;

下面我们来看一下initGlobalAPI方法中的代码。

该方法的代码定义在core/global-api/index.js文件中。

具体的代码如下:

/* @flow */import config from "../config";
import { initUse } from "./use";
import { initMixin } from "./mixin";
import { initExtend } from "./extend";
import { initAssetRegisters } from "./assets";
import { set, del } from "../observer/index";
import { ASSET_TYPES } from "shared/constants";
import builtInComponents from "../components/index";
import { observe } from "core/observer/index";import {warn,extend,nextTick,mergeOptions,defineReactive,
} from "../util/index";export function initGlobalAPI(Vue: GlobalAPI) {// configconst configDef = {};configDef.get = () => config;if (process.env.NODE_ENV !== "production") {configDef.set = () => {warn("Do not replace the Vue.config object, set individual fields instead.");};}// 初始化了`Vue.config`对象,该对象是Vue的静态成员Object.defineProperty(Vue, "config", configDef);// exposed util methods.// NOTE: these are not considered part of the public API - avoid relying on// them unless you are aware of the risk.//这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们,也就是说,使用这些API会出现一些问题。Vue.util = {warn,extend,mergeOptions,defineReactive,};//静态方法set/delete/nextTick//挂载到了Vue的构造函数中。后期会继续看内部源码的实现Vue.set = set;Vue.delete = del;Vue.nextTick = nextTick;// 2.6 explicit observable API//让一个对象变成可响应式的,内部调用了observe方法。Vue.observable = <T>(obj: T): T => {observe(obj);return obj;};//初始化了Vue.options对象,并给其扩展了//components/directives/filters内容,后面还会看这块内容Vue.options = Object.create(null);ASSET_TYPES.forEach((type) => {Vue.options[type + "s"] = Object.create(null);});// this is used to identify the "base" constructor to extend all plain-object// components with in Weex's multi-instance scenarios.Vue.options._base = Vue;//设置keep-alive组件extend(Vue.options.components, builtInComponents);
//注册Vue.use(),用来注册插件initUse(Vue);//注册Vue.mixin( )实现混入initMixin(Vue);//注册Vue.extend( )基于传入的options返回一个组件的构造函数initExtend(Vue);// 注册Vue.directive(),Vue.component( ),Vue.filter( )initAssetRegisters(Vue);
}

目前我们只需要知道在initGlobalAPI方法,初始化了Vue的静态方法。

现在,我们回到import Vue from "core/index"; 文件,发现在该文件中也没有创建Vue的构造函数,

但是在其顶部,导入了:import Vue from "./instance/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";
//Vue构造函数
function Vue(options) {//判断是否为生产环境,如果不等于生产环境并且如果this不是Vue的实例//那么说明用户将其作为普通函数调用,而不是通过new来创建其实例,所以会出现如下错误提示if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {warn("Vue is a constructor and should be called with the `new` keyword");}//调用_init( )方法this._init(options);
}
//注册vm的_init( )方法,初始化vm
initMixin(Vue);
//注册vm(Vue实例)的$data/$props/$set/$delete/$watch
stateMixin(Vue);
//初始化事件相关的方法
//$on/$once/$off/$emit
eventsMixin(Vue);
//初始化生命周期相关的混入方法
// $forceUpdate/$destroy
lifecycleMixin(Vue);
//混入render
// $nextTick
renderMixin(Vue);export default Vue;

下面看一下stateMixin方法,在该方法中,我们可以看到如下的代码

 //在Vue的实例总初始化了一些属性和者方法。Object.defineProperty(Vue.prototype, "$data", dataDef);Object.defineProperty(Vue.prototype, "$props", propsDef);Vue.prototype.$set = set;Vue.prototype.$delete = del;

通过上面的代码,我们可以看到在stateMixin方法中为Vue的原型上注册了对应的属性和方法,也就说在这个位置实现了为Vue的实例初始化属性和方法。

到此位置,我们最开始提出的问题:

Vue实例成员和Vue的静态成员是从哪里来的?

现在已经全部找到。

关于import Vue from "./instance/index";文件中的其他方法,后续课程内容中还会查看。

目前我们只需要知道,在在该文件中,创建了Vue的构造函数,并且设置了Vue实例的成员。

这里还有一个小的问题就是,在创建Vue的实例的时候,这里使用了构造函数,而没有使用类(class)的形式,

原因是:因为使用类来实现构造函数的,下面的方法就不容易实现。在这些方法中为Vue的原型上挂在了很多的成员,而使用类的构造函数不容易实现。

现在我们将这块内容做一总结:

通过前面的讲解,我们看了四个文件

src/platforms/web/entry-runtime-with-compiler.js

  • web平台相关的入口
  • 重写了平台相关的$mount( )方法,把template模板转换成render函数
  • 注册了Vue.compile( )方法,可以根据传递的HTML字符串返回render函数

src/platforms/web/runtime/index.js

  • web平台相关

  • 注册和平台相关的全局指令:v-model,v-show

  • 注册和平台相关的全局组件:v-transition,v-transition-group

  • 全局的指令与组件分别存储到了Vue.options.directivesVue.options.components中。

  • 全局方法

    __patch__:把虚拟DOM转换成真实DOM

    $mount:挂载方法,把DOM渲染到页面中,在src/platforms/web/entry-runtime-with-compiler.js文件中重写了

    $mount,使其具有了编译的能力。

  • src/core/index.js

    与平台无关

    设置了Vue的静态方法,initGlobalAPI(Vue)

  • src/core/instance/index.js

    与平台无关

    定义了Vue的构造方法,调用了this._init(options)方法(该方法是整个程序的入口)

    Vue中混入了常用的实例成员。

8、静态成员初始化

​ 这一小节我们来看一下Vue中静态成员的初始化,通过前面的讲解,我们知道静态成员都是在文件src/core/index.js中完成初始化,下面再看一下该文件。

在该文件有一个initGlobalAPI方法,在该方法中注册了一些静态成员。

//给Vue构造函数注册一些静态的方法,属性。
initGlobalAPI(Vue);

initGlobalAPI方法的具体实现如下:

export function initGlobalAPI(Vue: GlobalAPI) {// configconst configDef = {};//为configDef对象添加一个get方法,返回一个config对象configDef.get = () => config;if (process.env.NODE_ENV !== "production") {//如果不是生产环境,则为开发环境,这时会为configDef添加一个set方法,如果为config进行赋值操作,会出现不能给`Vue.config`重新赋值的错误。configDef.set = () => {warn("Do not replace the Vue.config object, set individual fields instead.");};}// 初始化了`Vue.config`对象,该对象是Vue的静态成员//这里不是定义响应式数据,而是为Vue定义了一个config属性//并且为其设置了configDef约束。Object.defineProperty(Vue, "config", configDef);// exposed util methods.// NOTE: these are not considered part of the public API - avoid relying on// them unless you are aware of the risk.//这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们,也就是说,使用这些API会出现一些问题。Vue.util = {warn,extend,mergeOptions,defineReactive,};//静态方法set/delete/nextTick//挂载到了Vue的构造函数中。后期会继续看内部源码的实现Vue.set = set;Vue.delete = del;Vue.nextTick = nextTick;// 2.6 explicit observable API//让一个对象变成可响应式的,内部调用了observe方法。Vue.observable = <T>(obj: T): T => {observe(obj);return obj;};//初始化了Vue.options对象,并给其扩展了//components/directives/filters内容Vue.options = Object.create(null);ASSET_TYPES.forEach((type) => {Vue.options[type + "s"] = Object.create(null);});// this is used to identify the "base" constructor to extend all plain-object// components with in Weex's multi-instance scenarios.Vue.options._base = Vue;extend(Vue.options.components, builtInComponents);initUse(Vue);initMixin(Vue);initExtend(Vue);initAssetRegisters(Vue);
}

在上面的代码中,首先初始化了Vue.config对象,并且添加了相应的约束。

 // 初始化了`Vue.config`对象,该对象是Vue的静态成员//这里不是定义响应式数据,而是为Vue定义了一个config属性//并且为其设置了configDef约束。Object.defineProperty(Vue, "config", configDef);

初始化Vue.config对象后,在什么位置为其挂载了成员。

platforms/web/runtime/index.js文件中

// install platform specific utils
//给Vue.config注册了方法,这些方法都是与平台相关的方法。这些方法是在Vue内部使用的。Vue.config.mustUseProp = mustUseProp;
//是否为保留的标签,也就是说,传递过来的内容是否为HTML中特有的标签
Vue.config.isReservedTag = isReservedTag;
//是否是保留的属性,也就是说,传递过来的内容是否为HTML中特有的属性
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;

下面,我们来看如下的代码,前面的代码我们后面在讲解(set,delete,observable等内容)。

  //初始化了Vue.options对象,并给其扩展了//components/directives/filters内容//创建了Vue.options对象,并且没有指定原型。这样性能更高。Vue.options = Object.create(null);ASSET_TYPES.forEach((type) => {//从`ASSET_TYPES`数组中取出每一项,并为其添加了`s`,来作为Vue.options对象的属性。//也就是说给Vue.options中挂载了三个成员,分别是:components/directives/filters,并且都初始化成了空对象。这三个成员的作用是用来存储全局的组件,指令和过滤器,我们通过Vue.component,Vue.directive,Vue.filter创建的组件,指令,过滤器最终都会存储到Vue.options中的这三个成员中。Vue.options[type + "s"] = Object.create(null);});

ASSET_TYPES数组进行遍历,那么该数组中存储的是什么内容呢?该数组具体定义的位置:

import { ASSET_TYPES } from "shared/constants";

src/shared目录下面找到constants.js文件,该文件中的代码如下:

export const SSR_ATTR = 'data-server-rendered'
//该数组中定义了我们比较常见的Vue.component,Vue.directive,Vue.filter的方法名称。
export const ASSET_TYPES = ['component','directive','filter'
]
//生命周期的钩子函数名称,后面在讲解这块内容
export const LIFECYCLE_HOOKS = ['beforeCreate','created','beforeMount','mounted','beforeUpdate','updated','beforeDestroy','destroyed','activated','deactivated','errorCaptured','serverPrefetch'
]

现在,我们知道了ASSET_TYPES数组中的内容,然后在来看一下对应的循环内容。

看完循环内容后,我们再来看一下如下代码:

//将Vue的构造函数存储到了_base属性中,后期会用到。  
Vue.options._base = Vue;
//设置keep-alive组件
//extend方法的作用是将第二个参数中的属性,拷贝到第一个参数中。下面可以看一下extend方法的具体实现。
extend(Vue.options.components, builtInComponents);

extend方法定义在

import {warn,extend,nextTick,mergeOptions,defineReactive,
} from "../util/index";

查看until/index中的内容

export * from 'shared/util'
export * from './lang'
export * from './env'
export * from './options'
export * from './debug'
export * from './props'
export * from './error'
export * from './next-tick'
export { defineReactive } from '../observer/index'

src/shared/util文件中,打开该文件搜索extend

export function extend (to: Object, _from: ?Object): Object {for (const key in _from) {to[key] = _from[key]}return to
}

实现了一个浅拷贝。把一个对象的成员拷贝给另外一个对象

下面的代码是将builtInComponents拷贝给Vue.options.components,这里完成的就是全局组件的注册

//设置keep-alive组件
extend(Vue.options.components, builtInComponents);

下面再来看一下builtInComponents中的内容。

import builtInComponents from "../components/index";
import KeepAlive from './keep-alive'
export default {KeepAlive
}

通过上面的代码可以看到,导出了KeepAlive这个组件

下面看一下 initUse(Vue); 该方法的作用:注册Vue.use(),用来注册插件

initUser方法定义在global-api/use.js文件。

import { toArray } from '../util/index'
//参数为Vue的构造函数
export function initUse (Vue: GlobalAPI) {//为Vue添加use函数//plugin:是一个函数或者是对象,表示的就是插件Vue.use = function (plugin: Function | Object) {//installedPlugins:表示已经安装的插件//注意:this表示的是Vue的构造函数const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))//判断传递过来的plugin这个插件是否在installedPlugins中存在,如果存在表示已经注册安装了,直接返回if (installedPlugins.indexOf(plugin) > -1) {return this}// additional parametersconst args = toArray(arguments, 1)args.unshift(this)//下面就是实现插件的注册,如果plugin中有install这个属性,表示传递过来的plugin表示的是对象//这时候直接调用plugin中的install完成插件的注册。也就是说如果在注册插件的时候,传递的是一个对象,这个对象中一定要有install这个方法,关于这块内容我们在前面的课程中也已经讲解过来。if (typeof plugin.install === 'function') {plugin.install.apply(plugin, args)} else if (typeof plugin === 'function') {//如果传递的是函数,直接调用函数plugin.apply(null, args)}//将插件添加的数组中installedPlugins.push(plugin)return this}
}

下面我们再来查看一下initMixin方法,该方法的作用就是用来注册Vue.mixin( )用来实现混入。

具体代码的位置:global-api/mixin.js文件

import { mergeOptions } from '../util/index'export function initMixin (Vue: GlobalAPI) {Vue.mixin = function (mixin: Object) {//将mixin这个对象中的所有成员拷贝到options中,this指的就是Vuethis.options = mergeOptions(this.options, mixin)return this}
}

下面,我们再来看一下initExtend方法,该方法的作用是注册Vue.extend( ),基于传入的options返回一个组件的构造函数。

代码位置:global-api/entend.js

  Vue.extend = function (extendOptions: Object): Function {extendOptions = extendOptions || {}//Vue的构造函数const Super = thisconst SuperId = Super.cid//从缓存中加载组件的构造函数const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})if (cachedCtors[SuperId]) {return cachedCtors[SuperId]}const name = extendOptions.name || Super.options.nameif (process.env.NODE_ENV !== 'production' && name) {//如果是开发环境验证组件的名称validateComponentName(name)}
//VueComponent表示组件的构造函数const Sub = function VueComponent (options) {//调用_init()初始化this._init(options)}//改变了Sub这个构造函数的原型,让其继承了Vue, Super.prototype表示的是Vue的原型。//所以说所有的Vue组件都是继承自Vue.Sub.prototype = Object.create(Super.prototype)Sub.prototype.constructor = SubSub.cid = cid++Sub.options = mergeOptions(Super.options,extendOptions)Sub['super'] = Super// For props and computed properties, we define the proxy getters on// the Vue instances at extension time, on the extended prototype. This// avoids Object.defineProperty calls for each instance created.if (Sub.options.props) {initProps(Sub)}if (Sub.options.computed) {initComputed(Sub)}// allow further extension/mixin/plugin usage//把Super(Vue)的成员拷贝到Sub这个构造函数中,这样就表明我们创建的组件具有了这些成员。Sub.extend = Super.extendSub.mixin = Super.mixinSub.use = Super.use// create asset registers, so extended classes// can have their private assets too.ASSET_TYPES.forEach(function (type) {Sub[type] = Super[type]})// enable recursive self-lookupif (name) {Sub.options.components[name] = Sub}// keep a reference to the super options at extension time.// later at instantiation we can check if Super's options have// been updated.Sub.superOptions = Super.optionsSub.extendOptions = extendOptionsSub.sealedOptions = extend({}, Sub.options)// cache constructor//把组件的构造函数缓存到options._CtorcachedCtors[SuperId] = Sub//返回组件的构造函数VueComponentreturn Sub}

以上内容简单了解一下就可以。

下面看一下:initAssetRegisters方法,在该方法中注册了Vue.directive(),Vue.component( ),Vue.filter( )

在这里需要注意的一点就是,以上三个方法并不是一个一个的注册的,而是一起注册的。为什么会一起注册呢?因为这三个方法的参数是一样的。这块可以参考文档。

下面看一下具体的源码实现。

/* @flow */import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'export function initAssetRegisters (Vue: GlobalAPI) {/*** Create asset registration methods.*///变量ASSET_TYPES数组,为`Vue`定义相应的方法//ASSET_TYPES数组包括了`directive`,`component`,`filter`ASSET_TYPES.forEach(type => {//分别给Vue中的`directive`,`component`,`filter`注册方法Vue[type] = function (id: string,//是名字(组件,指令,过滤器的名字)definition: Function | Object //定义,可以是对象或者是函数,这两个参数可以通过查看手册:https://vuejs.bootcss.com/api/#Vue-directive): Function | Object | void {if (!definition) {// 如果没有传递第二个参数,通过this.options找到之前存储的directive,component,filter,并返回//通过前面的学习,我们知道Vue.directive,Vue.component,Vue.filter都注册到了this.options['directives'],this.options['components'],this.options['filters']中return this.options[type + 's'][id]} else {/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && type === 'component') {validateComponentName(id)}//判断从ASSET_TYPES数组中取出的是否为`component`(也就是是否为组件)//同时判断definition参数是否为对象if (type === 'component' && isPlainObject(definition)) {//为definition设置名字,如果在Vue.component中的第二个参数设置了name属性,那么就使用该属性的值.//如果没有设置,则使用id的值作为definition的名字definition.name = definition.name || id//this.options._base表示的是Vue的构造函数。//Vue.extend():我们看过,作用就是将一个普通的对象转换成了VueComponent的构造函数、//看到这里,我们回到官方手册:https://vuejs.bootcss.com/api/#Vue-component// 注册组件,传入一个扩展过的构造器//Vue.component('my-component', Vue.extend({ /* ... */ }))// 注册组件,传入一个选项对象 (自动调用 Vue.extend)//Vue.component('my-component', { /* ... */ })//以上是官方手册中的内容,如果在使用Vue.component方法的时候,传递的第二个参数为Vue.extend,//那么会直接执行this.options[type + 's'][id] = definition这样代码,因为如果传递的是Vue.extend,那么以上if判断条件不成立。// 表示将definition对象的内容存储到this.options中,形式this.options[components]['my-component']//如果传递的是一个对象,那么会执行 this.options._base.extend(definition)这行代码。//那么现在我们就明白了文档中的这句话的含义:注册组件,传入一个选项对象 (自动调用 Vue.extend)definition = this.options._base.extend(definition)}//如果是指令,那么第二个参数可以是可以是对象,也可以是函数。//如果是对象,直接执行 this.options[type + 's'][id] = definition这行代码//如果是函数,会将definition设置给bind与update这两个方法,//在官方手册中,有如下内容// 注册 (指令函数)//Vue.directive('my-directive', function () {// 这里将会被 `bind` 和 `update` 调用//})//现在我们能够理解为什么会写`这里将会被 `bind` 和 `update` 调用`这句话了if (type === 'directive' && typeof definition === 'function') {definition = { bind: definition, update: definition }}//最终注册的Vue.component,Vue.filter,Vue.directive都会存储到this.options['components']//this.options['filters'],this.options['directives']中。是一个全局的注册。//Vue.component,Vue.filter,Vue.directive 是全局注册的组件,过滤器,指令this.options[type + 's'][id] = definitionreturn definition}}})
}

9、Vue实例成员初始化

通过前面的学习我们知道,实例成员在``src/core/instance/index.js`文件中,下面我们来看一下该文件中的如下方法内容:

//注册vm的_init( )方法,初始化vm
initMixin(Vue);
//注册vm(Vue实例)的$data/$props/$set/$delete/$watch 属性或方法
stateMixin(Vue);
//初始化事件相关的方法
//$on/$once/$off/$emit
eventsMixin(Vue);
//初始化生命周期相关的混入方法
// $forceUpdate/$destroy
lifecycleMixin(Vue);
//混入render
// $nextTick
renderMixin(Vue);

这些方法都是以Mixin进行结尾,表示混入的意思,并且传递的参数都是Vue的构造函数,也就是说这些方法都是为Vue混入一些成员。

initMixin方法的主要作用就是为Vue实例增加了_init方法,该方法在Vue的构造函数中被调用,该方法也是整个应用的入口。关于_initMixin方法中的代码,后面我们还会详细的讲解。

stateMixin方法,具体的代码如下:

export function stateMixin(Vue: Class<Component>) {// flow somehow has problems with directly declared definition object// when using Object.defineProperty, so we have to procedurally build up// the object here.const dataDef = {};dataDef.get = function () {return this._data;};const propsDef = {};propsDef.get = function () {return this._props;};if (process.env.NODE_ENV !== "production") {dataDef.set = function () {warn("Avoid replacing instance root $data. " +"Use nested data properties instead.",this);};propsDef.set = function () {warn(`$props is readonly.`, this);};}
//为Vue的原型上添加`$data`与`$props`属性。也就是完成了`$data`与`$props`的初始化//在这里为什么使用Object.defineProperty添加属性呢?//原因是:第三个参数,第三个参数就是一个约束。//dataDef和propsDef都是对象,并且为其添加了get,在开发环境中添加了set.//在访问`$data`或者是`$props`的时候会执行get,如果在开发环境中向`$data`或者是`$props`赋值会执行set,从而给出相应的错误提示信息Object.defineProperty(Vue.prototype, "$data", dataDef);Object.defineProperty(Vue.prototype, "$props", propsDef);//为Vue的原型上挂在了$set与$delete方法,与Vue.set和Vue.delete是一样的。Vue.prototype.$set = set;Vue.prototype.$delete = del;
//监视数据的变化,该方法后面还会在进行查看。Vue.prototype.$watch = function (expOrFn: string | Function,cb: any,options?: Object): Function {const vm: Component = this;if (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options);}options = options || {};options.user = true;const watcher = new Watcher(vm, expOrFn, cb, options);if (options.immediate) {try {cb.call(vm, watcher.value);} catch (error) {handleError(error,vm,`callback for immediate watcher "${watcher.expression}"`);}}return function unwatchFn() {watcher.teardown();};};
}

下面,我们看一下eventsMixin方法。

//初始化事件相关的方法
//$on/$once/$off/$emit
eventsMixin(Vue);
$on:注册事件
$once:注册事件,只能触发一次
$off:取消事件
$emit:是触发事件

下面我们只看一下$on中的代码,其它内容的代码,可以自己查看。

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {const vm: Component = this//如果event是一个数组,遍历该数组,给每一个事件,添加对应的处理函数。if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$on(event[i], fn)}} else {//如果是字符串,则根据event(事件名称)从_events对象中查找对应内容,如果没有则指定一个空数组,//然后向数组中添加了对应的处理函数。这样每个事件都有了对应的处理函数。(vm._events[event] || (vm._events[event] = [])).push(fn)// optimize hook:event cost by using a boolean flag marked at registration// instead of a hash lookupif (hookRE.test(event)) {vm._hasHookEvent = true}}return vm}

下面看一下lifecycleMixin方法:

//初始化生命周期相关的混入方法
//  _update/$forceUpdate/$destroy
lifecycleMixin(Vue);
 // _update方法的作用就是把虚拟DOM转换成真实的DOM
//首次渲染的时候会调用,数据更新会调用
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$elconst prevVnode = vm._vnodeconst restoreActiveInstance = setActiveInstance(vm)vm._vnode = vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.//首次渲染if (!prevVnode) {// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)} else {// updates(数据更新)vm.$el = vm.__patch__(prevVnode, vnode)}restoreActiveInstance()// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as wellif (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.}

其它方法,后续再看。

下面看一下renderMixin函数

//混入render
// $nextTick
renderMixin(Vue);
 installRenderHelpers(Vue.prototype)//安装了渲染有关的帮助方法

installRenderHelpers内部实现:

export function installRenderHelpers (target: any) {target._o = markOncetarget._n = toNumbertarget._s = toStringtarget._l = renderListtarget._t = renderSlottarget._q = looseEqualtarget._i = looseIndexOftarget._m = renderStatictarget._f = resolveFiltertarget._k = checkKeyCodestarget._b = bindObjectPropstarget._v = createTextVNode //创建一个虚拟文本节点target._e = createEmptyVNodetarget._u = resolveScopedSlotstarget._g = bindObjectListenerstarget._d = bindDynamicKeystarget._p = prependModifier
}

以上这些函数都是在编译的时候会用到,也就是将template模板编译成render函数的时候。

installRenderHelpers 创建了$nextTick,关于这块内容,我们后期再来查看

Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)}

下面有一个_render方法,在该方法中有一个非常重要的代码。

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

在上面的代码中首先看一下render的定义:

const { render, _parentVnode } = vm.$options

从上面这行代码中,我们知道了render来自于vm.$options,那么就表明这个render是在用户创建Vue的实例的时候执行的的render.

render.call范围render方法的调用,第一个参数改变this的指向,第二个参数: vm.$createElement其实就是我们在创建Vue的实例的时候,指定的h函数。h函数的作用就是创建虚拟DOM

以上就是我们这一小节讲解的内容,当然其内部还有一些其它方法,这些我们会在后面在继续查看。

10、init方法

在``src/core/instance/index.js文件中,创建了Vue的构造函数,并且在其内部调用了_init( )方法,而该方法的初始化是在initMixin(Vue);方法中完成的,下面我们再来看一下该方法的实现。_init( )`方法完成了初始化的工作,所以我们重点看一下该方法中初始化了哪些内容?

export function initMixin (Vue: Class<Component>) {Vue.prototype._init = function (options?: Object) {//Vue的实例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//如果是Vue实例,不需要被observe处理。vm._isVue = true// merge options// 合并optionsif (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 {//将用户掺入的options与Vue构造函数中的options进行合并、//在初始化静态成员的时候已经为Vue构造函数初始化了v-show,v-model,keep-alive等指令和组件vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}/* istanbul ignore else *///设置渲染的代理对象if (process.env.NODE_ENV !== 'production') {//开发环境调用initProxy方法initProxy(vm)} else {//渲染的时候设置的代理对象就是Vue的实例//在渲染的时候会用到该属性。vm._renderProxy = vm}// expose real selfvm._self = vm//下面的函数是完成Vue实例的一些初始化操作。//初始与生命周期相关的属性($children,$parent,$root,$refs)initLifecycle(vm)//初始化当前组件的事件      initEvents(vm)//初始化了render中所使用的h函数//同时还初始化了$slots/$attrs 等等//在initRender方法中,注意如下两行代码//   vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)//vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)//当我们在创建一个Vue实例的时候,是可以直接传递一个render函数,render函数需要一个参数就是h函数,//$createElement就是传递过来的h函数。其作用就是将虚拟DOM转换成真实DOM//当我们把template编译成render函数的时候,在内部调用的是_c这个函数,在模板编译的过程中,会看到这个方法// 而$createElement函数,是在new Vue实例的时候传递的render函数所调用的。initRender(vm)//触发生命周期中的beforeCreate钩子函数callHook(vm, 'beforeCreate')//初始化inject,把inject的成员注入到Vue的实例上initInjections(vm) // resolve injections before data/props//初始化Vue实例中的methods/computed/watch等,关于该函数会在下一小节中进行讲解initState(vm)// 初始化provideinitProvide(vm) // resolve provide after data/props//触发生命周期中的created钩子函数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)}
//调用$mount挂载整个页面,并且进行页面的渲染if (vm.$options.el) {vm.$mount(vm.$options.el)}}
}

initProxy方法的实现

onst hasProxy =typeof Proxy !== 'undefined' && isNative(Proxy)
//如果有Proxy,通过new Proxy来创建一个代理。if (hasProxy) {const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')config.keyCodes = new Proxy(config.keyCodes, {set (target, key, value) {if (isBuiltInModifier(key)) {warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)return false} else {target[key] = valuereturn true}}})}

11、initState方法

initState方法的作用就是:初始化Vue实例中的methods/computed/watch等.

export function initState(vm: Component) {vm._watchers = [];//获取optionsconst opts = vm.$options;//初始化props,并且注入到Vue实例中if (opts.props) initProps(vm, opts.props);//初始化methods,把methods中的方法注册到Vue实例中,下面看一下initMethods方法的内部实现if (opts.methods) initMethods(vm, opts.methods);//如果options中有data属性会调用initData方法,下面查看一下initData方法内部实现if (opts.data) {initData(vm);//初始化data} else {//如果没有data属性,会为Vue的实例创建一个空对象,并且将其修改成响应式的。关于响应式的内容我们后期还会查看。observe((vm._data = {}), true /* asRootData */);}//初始化computed,会把computed注册到Vue的实例中,可以自己查看源码if (opts.computed) initComputed(vm, opts.computed);//初始化watch,会把watch注册到Vue的实例中,可以自己查看源码if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch);}
}

initMethods方法实现

function initMethods(vm: Component, methods: Object) {const props = vm.$options.props;//获取propsfor (const key in methods) {//对所有的methods进行遍历if (process.env.NODE_ENV !== "production") {//在开发环境中if (typeof methods[key] !== "function") {//获取对应的method如果不是函数,会给出相应的警告warn(`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +`Did you reference the function correctly?`,vm);}// 如果method的名字与props中的属性名字重名也会给出相应的警告if (props && hasOwn(props, key)) {warn(`Method "${key}" has already been defined as a prop.`, vm);}//判断方法的名称是否为Vue的实例,同时判断方法的名称是否以_或者是$开头。//如果以 _ 开头表示一个私有属性,所以不建议方法名称以 _ 开头。// 以$开头的都是成员都是Vue的成员,所以也不建议方法名称使用$开头if (key in vm && isReserved(key)) {warn(`Method "${key}" conflicts with an existing Vue instance method. ` +`Avoid defining component methods that start with _ or $.`);}}//把method注册到Vue的实例中//首先判断从methods中取出来的方法如果不是"function",返回noop,也就是一个空函数。//如果是"funciton",返回该函数,同时通过bind修改函数内部this的指向,然后指向到Vue的实例。vm[key] =typeof methods[key] !== "function" ? noop : bind(methods[key], vm);}
}

initData方法实现

function initData(vm: Component) {let data = vm.$options.data;//获取props中的data内容//初始化_data,判断data的类型是不是一个函数如果不是直接返回data,如果是调用getData方法,getData中就是通过call来调用data函数。//其实就是初始化组件中的data,组件中的data就是一个函数,如果是Vue实例中的data,那么就是一个对象。data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};if (!isPlainObject(data)) {data = {};process.env.NODE_ENV !== "production" &&warn("data functions should return an object:\n" +"https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",vm);}// proxy data on instance//获取data中的所有属性const keys = Object.keys(data);// 获取propsconst props = vm.$options.props;// 获取methodsconst methods = vm.$options.methods;let i = keys.length;//判断data中的成员是否和`props/methods`重名。在开发环境中有重名会出现相应的警告信息。while (i--) {const key = keys[i];if (process.env.NODE_ENV !== "production") {if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm);}}if (props && hasOwn(props, key)) {process.env.NODE_ENV !== "production" &&warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm);} else if (!isReserved(key)) {//isReserved方法就是判断当前的属性是否以`_`和`$`开头,如果是以`_`和`$`开头就不会把当前的属性注入到Vue实例中。否则会将当前属性(key的值)注册到Vue的实例中。proxy(vm, `_data`, key);}}// observe data//对data做响应式的处理。observe(data, true /* asRootData */);
}

下面可以看一下proxy方法中的代码。

export function proxy(target: Object, sourceKey: string, key: string) {// 如果访问get,那么返回的就是this._data中当前属性的值sharedPropertyDefinition.get = function proxyGetter() {return this[sourceKey][key];};sharedPropertyDefinition.set = function proxySetter(val) {this[sourceKey][key] = val;};//把当前属性注入到Vue实例中Object.defineProperty(target, key, sharedPropertyDefinition);
}

12、总结

下面我们把上面讲解的内容,做一个总结:

在首次渲染的时候,首先对Vue进行初始化,同时完成实例成员与静态成员的初始化。初始化完成后会执行构造函数,在构造函数中调用了_init方法,该方法是整个Vue的入口,在该方法中调用了vm.$mount方法,通过前面的学习,我们知道有两个$mount, 首先第一个$mount来自于src/platforms/web/entry-runtime-with-compiler.js文件,这个$mount方法的作用就是把模板编译成render函数,当然,在把模板编译成render函数之前,先判断一下是否传入了render这个选项,如果没有传入,这时就会去获取template选项,如果也没有template这个选项,那么会把el中的内容作为模板,然后把模板编译成render函数。这里是通过compileToFunctions这个函数,把模板编译成render函数。把编译好的render函数存储到options.render中。

下面会调用src/platforms/web/runtime/index.js文件中的$mount方法。在这个方法中会重新获取el,因为运行时版本,是不会执行src/platforms/web/entry-runtime-with-compiler.js文件中的代码,所以这里需要重新获取el,下面调用mountComponent( )方法,该方法定义的文件为src/core/instance/lifecycle.js,在mountComponent( )方法中,首先判断是否有render选项,如果没有但是传入了模板,并且当前是开发环境,那么会打印一个警告信息。警告信息为运行时版本不支持编译器。下面触发了beforeMount这个钩子函数,然后定义updateComponent,在updateComponent中,调用了vm._render( )函数和vm._update函数。

vm._render()函数的作用就是生成虚拟DOM(在 vm._render()这个方法中,调用了在创建Vue实例的时候传入的render函数,或者是将template编译成的render函数),vm._update( )函数的作用就是将虚拟DOM转换成真实的DOM(在vm._udpate这个方法中调用了vm.__patch__方法将虚拟DOM转换成了真实的DOM然后挂在到页面中).接下来创建了Watcher的实例,在Watcher实例中调用了updateComponent, 接下来会执行mounted这个钩子函数,完成挂在,最后返回Vue的实例。

13、响应式处理入口

通过查看 源码解决如下的问题

  • vm.msg={count:0} 重新给属性赋值,是否是响应式的?
  • vm.arr[0]=4 给数组元素赋值,视图是否会更新
  • vm.arr.length=0 修改数组的length,视图是否会更新
  • vm.arr.push(5) 视图是否会更新

响应式处理的入口

整个响应式处理的过程是比较复杂的,下面我们先查看src/core/instance/init.js文件,在该文件中有initState(vm)方法,该方法完成了vm状态的初始化,初始化了_data,_props,methods等。

然后我们再来看一下,src/core/instance/state.js

//数据的初始化
if(opts.data){initData(vm)//把data中的成员注入到Vue实例,并且转换成响应式的对象
}else{//如果options选项中没有data,这里会将data初始化一个空对象。传入到observe这个方法中转换成响应式的对象observe(vm._data={},true)//observe就是响应式的入口。
}

下面看一下src/core/instance/init.js文件,在该文件中找到initState(vm)方法,该方法就是注册vmVue实例)的$data/$props/$set/$delete/$watch 属性或方法.当我们单击进入该方法后,可以看到该方法所在的文件为src/core/instance/state.js.

在该文件中找到如下代码

 if (opts.data) {initData(vm)} else {observe(vm._data = {}, true /* asRootData */)} 

下面在进入initData方法,代码如下:

function initData(vm: Component) {let data = vm.$options.data;//获取props中的data内容//初始化_data,判断data的类型是不是一个函数如果不是直接返回data对象,如果是调用getData方法,getData中就是通过call来调用data函数。//其实就是初始化组件中的data,组件中的data就是一个函数,如果是Vue实例中的data,那么就是一个对象。data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};if (!isPlainObject(data)) {data = {};process.env.NODE_ENV !== "production" &&warn("data functions should return an object:\n" +"https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",vm);}// proxy data on instance//获取data中的所有属性const keys = Object.keys(data);// 获取propsconst props = vm.$options.props;// 获取methodsconst methods = vm.$options.methods;let i = keys.length;//判断data中的成员是否和`props/methods`重名。在开发环境中有重名会出现相应的警告信息。while (i--) {const key = keys[i];if (process.env.NODE_ENV !== "production") {if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm);}}if (props && hasOwn(props, key)) {process.env.NODE_ENV !== "production" &&warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm);} else if (!isReserved(key)) {//isReserved方法就是判断当前的属性是否以`_`和`$`开头,如果是以`_`和`$`开头就不会把当前的属性注入到Vue实例中。否则会将当前属性(key的值)注册到Vue的实例中。proxy(vm, `_data`, key);}}// observe data//对data做响应式的处理。observe(data, true /* asRootData */);
}

在上面的代码中,我们可以看到最后调用了observe方法。data就是传递过来的options选项中的data,第二个参数为true,表示的就是根数据,根数据会做相应的处理。


/*** Attempt to create an observer instance for a value,* returns the new observer if successfully observed,* or the existing observer if the value already has one.*/
//试图为value创建一个Observer对象,如果创建成功,将创建的Observer返回,或者返回一个已经存在的Observer对象。也就是说valu这个参数如果已经有Observer对象,直接返回。
export function observe (value: any, asRootData: ?boolean): Observer | void {// 判断 value 是否是对象,如果不是对象,或者是VNode,直接返回,不做响应式的处理if (!isObject(value) || value instanceof VNode) {return}let ob: Observer | void //声明一个Observer类型的变量// 判断value中是否有'__ob__'这个属性,如果有,那么就需要判断value中的ob这个属性是否为Observer的实例//如果value中的ob属性是Observer的实例,在这就赋值给ob这个变量。最后直接返回ob.//这一点就和最开始我们说的是一样的,也是如果已经存在Observer对象,直接返回,相当于做了一个缓存的效果。if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__} else if (//如果value中没有ob这个属性,那么就需要创建一个Observer对象。//在创建Observer对象之前,需要做一些判断的处理。//这里我们重点看一下,如下的判断      //(Array.isArray(value) || isPlainObject(value)) &&//Object.isExtensible(value) &&//!value._isVue//(Array.isArray(value):判断传递过来的value是否为一个数组//isPlainObject(value)):判断value是否为一个对象。//!value._isVue:判断value是否为一个Vue的实例,在core/instance/index.js文件中,调用了initMixin方法,//在该方法中,设置了_isVue这个属性,如果传递过来的的value是Vue的实例就不要通过Observer设置响应式。//如果value可以进行响应式的处理,就需要创建一个Observer对象。shouldObserve &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {// 创建一个 Observer 对象//在Observer中把value中的所有属性转换成get与set的形式。ob = new Observer(value)}if (asRootData && ob) {ob.vmCount++}return ob
}

这里最重要的就是Observer对象的创建,下一小节我们来看一下Observer中是怎样处理的。

14、Observer

在上一小节中,我们已经找到了响应式的入口,Observer.(src/core/observer/index.js

下面我们看一下Observer对应的代码。Observer是一个类

/*** Observer class that is attached to each observed* object. Once attached, the observer converts the target* object's property keys into getter/setters that* collect dependencies and dispatch updates.*/
//Observer类附加到每个被观察的对象,一旦附加,observer就会转换目标对象的所有属性,将其转换成`getter/setter`.
//其目的就是收集依赖和派发更新。其实就是我们前面所讲的发送通知。
export class Observer {// 观察对象value: any;// 依赖对象dep: Dep;// 实例计数器vmCount: number; // number of vms that have this object as root $dataconstructor (value: any) {this.value = valuethis.dep = new Dep()// 初始化实例的 vmCount 为0this.vmCount = 0// 将实例挂载到观察对象的 __ob__ 属性上。def(value, '__ob__', this)// 数组的响应式处理,后面再看具体的实现if (Array.isArray(value)) {if (hasProto) {protoAugment(value, arrayMethods)} else {copyAugment(value, arrayMethods, arrayKeys)}// 为数组中的每一个对象创建一个 observer 实例this.observeArray(value)} else {// 遍历对象中的每一个属性,转换成 setter/getterthis.walk(value)}}

下面 看一下对应的walk方法的实现。

  walk (obj: Object) {// 获取观察对象的每一个属性const keys = Object.keys(obj)// 遍历每一个属性,设置为响应式数据for (let i = 0; i < keys.length; i++) {//该方法就是将对象中的属性转换成getter和setter.//当然在将属性转换成getter/setter前,也做了其它的一些处理,例如收集依赖,当数据发生变化后,发送通知等。//下一小节,查看该方法的实现。defineReactive(obj, keys[i])}}

最好还调用了observeArray方法,该方法的作用就是将数组转换成响应式的。

 observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}}

Observer类核心的作用就是对数组和对象做响应式的处理。

15、defineReactive

这一小节我们来看一下Observer类中的defineReactive方法,在上一小节中我们说过该方法的作用就是:就是将对象中的属性转换成gettersetter.

下面看一下defineReactive方法中的源码实现。

// 为一个对象定义一个响应式的属性
/*** Define a reactive property on an Object.*/
//shallow的值为true,表示只监听对象中的第一层属性,如果是false那就是深度监听,也就是说当key这个属性的值是一个对象,那么还需要监听这个对象中的每个值的变化。
export function defineReactive (obj: Object,//目标对象key: string, //转换的属性val: any,customSetter?: ?Function,//用户自定义的函数,很少会用到。shallow?: boolean
) {// 创建依赖对象实例,其作用就是为key收集依赖,也就是收集所有观察当前key这个属性的所有的watcher.const dep = new Dep()// 获取 obj 的属性描述符对象,在属性描述符中可以定义getter/setter. 还可以定义configurable,也就是该属性是否为可配置的。const property = Object.getOwnPropertyDescriptor(obj, key)//判断是否存在属性描述符并且configurable的值为false。如果configurable的值为false,表明是不可配置的,那么就不能通过delete将这个属性进行删除。、//也不能通过 Object.defineProperty进行重新的定义。//而在接下的操作中我们需要通过 Object.defineProperty对属性重新定义描述符,所以这里判断了configurable属性如果为false,则直接返回。if (property && property.configurable === false) {return}// 获取属性中的get和set.因为obj这个对象有可能是用户传入的,如果是用户传入的那么就有可能给obj这个对象中的属性设置了get/set.//所以这里先将用户设置的get和set存储起来,后面需要对get/set进行重写,为其增加依赖收集与派发更新的功能。// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.set//如果传入了两个参数(obj和key),这里需要获取对应的key这个属性的值。if ((!getter || setter) && arguments.length === 2) {val = obj[key]}// 判断shallow是否为false.如果当前的shallow是false,那么就不是浅层的监听。那么需要调用observe,也就是val是一个对象,那么需要将该对象中的所有的属性转换成getter/setter,observe方法返回的就是一个Observer对象let childOb = !shallow && observe(val)//下面就是通过Object.defineProperty将对象的属性转换成了get和set  Object.defineProperty(obj, key, {enumerable: true,//可枚举configurable: true,//可以配置get: function reactiveGetter () {// 首先调用了用户传入的getter,如果用户设置了getter,那么首先会通过用户设置的getter获取对象中的属性值。//如果没有设置getter,直接返回我们前面获取到的值。const value = getter ? getter.call(obj) : val//下面就是收集依赖(这块内容我们在一下小节中再来讲解,下面再来看一下set)// 如果存在当前依赖目标,即 watcher 对象,则建立依赖if (Dep.target) {dep.depend()// 如果子观察目标存在,建立子对象的依赖关系if (childOb) {childOb.dep.depend()// 如果属性是数组,则特殊处理收集数组对象依赖if (Array.isArray(value)) {dependArray(value)}}}// 返回属性值return value},set: function reactiveSetter (newVal) {//如果用户设置了getter,通过用户设置的getter获取对象中的属性值,否则直接返回前面获取到的值。const value = getter ? getter.call(obj) : val// 如果新值等于旧值或者新值旧值为NaN则不执行/* eslint-disable no-self-compare */if (newVal === value || (newVal !== newVal && value !== value)) {return}/* eslint-enable no-self-compare */if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()}// 如果没有 setter 直接返回// #7981: for accessor properties without setterif (getter && !setter) return// 如果setter存在则调用,为对象中的属性赋值if (setter) {setter.call(obj, newVal)} else {//当getter和setter都不存在,将新值赋值给旧值。val = newVal}// 如果新值是对象,那么把这个对象的属性再次转换成getter/setter//childOb就是一个Observe对象。childOb = !shallow && observe(newVal)// 派发更新(发布更改通知)dep.notify()}})
}

16、依赖收集

defineReactive方法中定义了getter/setter.在getter中做了收集依赖。依赖收集就是把依赖该属性的watcher对象,添加到dep中的subs数组中。

当数据发生变化后,通知数组中的watch,在前面的课程中,我们模拟过这个过程,下面看一下在Vue中是怎样实现的。

//下面就是收集依赖// 判断Dep中是否有target属性,该属性中存储的就是watcher对象if (Dep.target) {//depend方法就是进行依赖收集,就是把watch对象添加到Dep中的subs数组中。dep.depend()// 如果子对象存在,建立子对象的依赖关系if (childOb) {//每一个Observer对象,都有一个dep对象,然后调用depend方法建立子对象的依赖关系。childOb.dep.depend()// 如果属性是数组,则收集数组对象依赖if (Array.isArray(value)) {dependArray(value)}}}

现在我们对以上代码有了一个基本的了解,下面我们思考一个问题是,在什么时候给Deptarget属性赋值的?要回答这个问题,我们首先要考虑的是,什么时候创建Watcher对象的。

src/core/instance/lifecycle.js文件中创建了Watcher对象。

找到该文件中的mountComponent方法,在该方法中创建了Watcher对象。

  // we set this to vm._watcher inside the watcher's constructor// since the watcher's initial patch may call $forceUpdate (e.g. inside child// component's mounted hook), which relies on vm._watcher being already definednew Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)

下面进入Watcher的内部,看一下在其内部是怎样给Deptarget属性赋值的。在其内部有个get方法,在该方法的内部的内部调用了 pushTarget(this),在这里this就是Watcher对象,下面进入pushTarget方法的内部。

// Dep.target 用来存放目前正在使用的watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 入栈并将当前 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,
// 再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
export function pushTarget (target: ?Watcher) {//将Watcher对象存储到栈中。//因为在V2.0以后每一个组件对应一个Watcher对象,如果组件之间有嵌套,先处理子组件,所以这时应该先将父组件的	`Watcher`存储起来,这里是存储到栈中了,//子组件处理完毕后,把父组件中的Watcher从栈中弹出,继续处理父组件。targetStack.push(target)Dep.target = target//给Dep中的target赋值。
}export function popTarget () {// 出栈操作targetStack.pop()Dep.target = targetStack[targetStack.length - 1]
}

下面,我们查看一下 dep.depend()这个方法内部的代码。我们知道depend方法的作用就是收集依赖。

 // 将观察对象和 watcher 建立依赖depend () {if (Dep.target) {// 如果 target 存在,把 dep 对象添加到 watcher 的依赖中Dep.target.addDep(this)}}

Dep.target指的就是Watcher ,所以接下来我们查看observer/watcher.js这个文件中的代码。

  addDep (dep: Dep) {const id = dep.id//唯一表示,每次创建一个Dep对象的时候,会让该编号加1,这里可以进入Dep中查看。例如,在页面中有两个{{msg}},针对这个属性,只会收集一次依赖,即使使用两次msg,这样就避免了重复收集依赖。if (!this.newDepIds.has(id)) {//如果在newDepIds集合中没有id,将其添加到该集合中。this.newDepIds.add(id)//将dep对象添加到newDeps集合中。this.newDeps.push(dep)if (!this.depIds.has(id)) {// 调用dep对象中的addSub方法,将Watcher对象添加到subs数组中。this为Watcher对象。dep.addSub(this)}}}

下面我们来看一下Dep中的addSub方法

  // 将Watcher对象添加到subs数组中。addSub (sub: Watcher) {this.subs.push(sub)}

17、数组的响应式处理

数组的响应式处理的核心代码在Observer类的构造函数中(/core/observer/index.js)

 constructor (value: any) {this.value = valuethis.dep = new Dep()// 初始化实例的 vmCount 为0this.vmCount = 0// 将实例挂载到观察对象的 __ob__ 属性def(value, '__ob__', this)// 数组的响应式处理if (Array.isArray(value)) {//  export const hasProto = '__proto__' in {}//判断当前浏览器是否支持对象的原型这个属性,目的完成浏览器兼容的处理if (hasProto) {//支持对象的原型,则调用如下的函数,//value是数组,//arrayMethods:数组相关的方法。//该方法重新设置数组的原型属性,对应的值为arrayMthods.protoAugment(value, arrayMethods)} else {copyAugment(value, arrayMethods, arrayKeys)}// 为数组中的每一个对象创建一个 observer 实例this.observeArray(value)} else {// 遍历对象中的每一个属性,转换成 setter/getterthis.walk(value)}}

arrayMethods的实现如下:

//数组构造函数的原型
const arrayProto = Array.prototype
//使用Object.create创建一个对象,让对象的原型指向arrayProto,也就是数组的prototype
export const arrayMethods = Object.create(arrayProto)
//我们可以看到,如下内容都是数组中的方法,而且这些方法会对数组进行修改,例如push向数组增加内容,造成了原有数组的更新。
//而当数组中的内容发生了变化后,我们要调用Dep中的notity方法发送通知。通知watcher,数据发生了变化,要重新更新视图。
//但是数组的原生方法不知道Dep,也就不会调用Dep中的notity方法。所以说要做一些处理。下面看一下怎样进行处理的。
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'
]
//对methodsToPatch数组进行遍历。
//method表示的是从methodsToPatch数组中取出来的方法的名字
methodsToPatch.forEach(function (method) {// cache original method// arrayProto:数组的原型,这里就是获取数组的原始方法,例如push,pop等const original = arrayProto[method]// 调用 Object.defineProperty() ,将method中存储的方法的名字,重新定义到arrayMthods,也就是给arrayMthods对象,重新定义push,pop等这些方法。// 方法的值,就是defineProperty()方法的第三个参数mutator,该方法需要参数args:该参数中存储的就是我们在调用push或者是pop时传递的内容。def(arrayMethods, method, function mutator (...args) {// 执行数组的原始方法const result = original.apply(this, args)// 获取数组关联的Observer对象const ob = this.__ob__//存储数组中新增的内容,例如如果是push,unshift,则将args赋值给inserted,因为这时args存储的就是新增的内容。let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice'://如果是splice方法,那么把第三个值存储到inserted中。inserted = args.slice(2)break}// 如果有新增的元素,将会重新遍历数组中的元素,并且将其设置为响应式数据。也就是说,调用push,unshift,splice方法,向数组中添加的内容都是响应式的。if (inserted) ob.observeArray(inserted)// notify change// 找到Observer中的dep对象,调用其中的notify方法来发送通知ob.dep.notify()//返回方法执行的结果。return result})
})

以上就是对数组中的会修改数组原有内容的方法的处理。具体的过程就是先调用数组中的原始方法,例如push,pop等这些方法。

下面就是找到对数组进行新增元素的这些方法,例如:push,unshift,splice,,如果新增了元素,就调用observer中的observerArray这个方法,去遍历数组中的这些新增的元素,然后转换成响应式。当调用了数组中的新增元素的这些方法后,会发送通知。最后返回方法执行的结果。

下面我们再回到Observer类的构造函数中(/core/observer/index.js)。

如果浏览器不支持原型属性会调用copyAugment方法,该方法有三个参数,前两个参数与protoAugment方法参数的含义是一样的,第三个参数是arrayKeys.

arrayKeys具体的含义是:

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

获取数组中的push,pop等方法的名字,arrayKeys是一个数组。

下面看一下copyAugment方法中的代码

function copyAugment (target: Object, src: Object, keys: Array<string>) {//变量传递过来的数组中方法的名字for (let i = 0, l = keys.length; i < l; i++) {const key = keys[i]//给数组对象重新定义这些数组的方法,例如pop,push. 当然这些方法都是经过处理的。该方法的作用与protoAugment方法的一样def(target, key, src[key])}
}

下面我们再来看一下observeArray方法。

遍历数组中的成员,为数组中的每个对象设置为响应式的对象。

  observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}}
}

18、数组练习88888

<div id="app">{{arr}}
</div>
<script src="vue.js"></script>
<script>const vm=new Vue({el:'#app',data:{arr:[2,3,5]}})//看一下如下操作是否为响应式// vm.arr.push(8)// vm.arr[0]=100// vm.arr.length=0
</script>

通过前面的代码的阅读,我们知道push方法,在Vue中做了一定的处理,当通过push方法向数组中添加了一个新的数据后,对应的视图页面也会进行更新。

vm.arr[0]=100,会修改数组中的内容,但是当数组中的内容修改了以后,视图并没有发生任何的变化,所以这种操作并不是响应式的。也就是说通过数组的索引来

修改数组的时候,并没有调用Dep中的notify,也就没有通知watcher去重新渲染视图。通过源码,我们并没有发现对这种情况的处理,也就是没有监听数组中的每个属性(index,length都是数组的属性)将其转换成响应式,同理vm.arr.length=0也不是响应式的。

在源码中,我们看的是对数组进行遍历,对数组中的每个元素中是对象的元素转换成了响应式。

如果现在我们想让 vm.arr[0]=100 vm.arr.length=0这种操作变成响应式的效果,应该怎样实现呢?

首先,我们先来看 vm.arr[0]=100,就是修改数组中的第一个元素,这里我们可以使用splice方法来达到相同的目的,而且通过前面阅读源码我们知道,splice方法是响应式的,也就是通过该发你规范修改完了数组后,对应的视图也会进行。所以对应的代码为:

//第一个参数0,表示删除arr数组中的第一个元素。
//第二个参数1.表示的是删除的个数
//第三个参数100,表示的是把删除的元素用100来替换。
vm.arr.splice(0,1,100)

下面我们再来看一下关于vm.arr.length=0,表示的是清空数组中的内容。

为了达到响应式的效果,这里也可以使用splice方法来完成,清空数组的操作。

vm.arr.splice(0)//清空数组中的所有元素

通过查看源码我们对上面的问题有了更加深入的理解。

19、Watcher

关于Watcher我们首先会查看一下在首次渲染的时候的执行过程,然后再来看一下当数据发生变化后,Watcher的执行过程。

Watcher分为三种,Computed Watcher(计算属性,本质也是通过Watcher来实现的),用户Watcher(侦听器),渲染Watcher

前面两种Watcher是在initState的时候初始化的。

下面我们来复习一下首次渲染的时候Watcher的执行过程。

/src/core/instance/lifecycle.js中的mountComponent组件中创建的。

如下代码所示:

 //vm:Vue的实例
// updateComponent
//noop空函数,new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {//触发beforeUpate方法。callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)//ture表示渲染的watcher.

下面进入Watcher类中,看一下做了哪些事情。以下的代码是Watcher类的构造函数。

  constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {//将Vue的实例记录到了vm这个属性中。this.vm = vmif (isRenderWatcher) {//如果是渲染`Watcher`,则将`Watcher`的实例保存到`Vue`实例中的`_watcher`属性中。vm._watcher = this}//将所有的`Watcher`实例都保存到`Vue`实例中的`_watchers`这个数组中。// _watchers数组中不仅存储了渲染`Watcher`,还存储了计算属性对应的watcher,还有就是侦听器。vm._watchers.push(this)// optionsif (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.syncthis.before = options.before} else {this.deep = this.user = this.lazy = this.sync = false}//创建渲染watcher的实例的时候,传递过来的cb函数就是一个noop空函数。//如果是用户创建的`Watcher`的时候,传递过来的就是一个回调函数。this.cb = cbthis.id = ++uid //为了区分每个watcher,创建一个编号 uid for batchingthis.active = true// active表示这个watcher是否为活动的watcher,如果为true,则为活动的watcher.this.dirty = this.lazy // for lazy watchers//下面的集合记录的都是Depthis.deps = []this.newDeps = []this.depIds = new Set()this.newDepIds = new Set()this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''// parse expression for getter//判断expOrFn是否为函数,我们在前面创建渲染watcher的时候,传递过来的updateComponent函数给了expOrFn这个参数。if (typeof expOrFn === 'function') {//将函数保存到了getter中。this.getter = expOrFn} else {// expOrFn 是字符串的时候,也就是创建侦听器的时候传递的内容,例如 watch: { 'person.name': function... }// 这时候侦听器侦听的内容是字符串,也就是person.name// parsePath('person.name') 返回一个函数获取 person.name 的值//parsePath的作用就是生成一个函数,来获取`person.name`的值。将返回的函数记录到了getter中。记录geeter中的目的,就是当获取属性值的时候会触发对应的getter,当触发getter的时候会触发依赖。this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = noopprocess.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}//如果是计算属性,lazy的值为true,表示延迟执行。如果是渲染watcher,会立即调用get方法。this.value = this.lazy? undefined: this.get()}

下面就是get方法的源码。

  get () {//把当前的watcher对象入栈,并且把当前的watcher赋值给Dep的target属性。//当有父子组件嵌套的时候,先将父组件的watcher入栈,然后对子组件进行处理,处理完毕后,在从栈中获取父组件的watcher进行处理。pushTarget(this)let valueconst vm = this.vmtry {//对getter函数进行调用,如果是渲染函数,这里调用的是updateComponent//当updateComponent函数执行完毕后会将虚拟DOM转换成真实的DOM,然后渲染到页面中。value = this.getter.call(vm, vm)} catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)} else {throw e}} finally {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {//表示进行深度监听,深度监听表示的就是如果监听的是一个对象的话,会监听这个对象下的子属性、traverse(value)}//处理完毕后,做一些清理的工作,例如将watcher从栈中弹出popTarget()//将Watcher从subs数组中移除this.cleanupDeps()}return value}

以上就是首次渲染的时候,Watcher的执行过程。

下面,我们来看一下当数据发生更新的时候,Watcher是怎样工作的?

下面我们来查看一下observer/dep.js

我们知道当数据发生了变化后,会调用Dep中的notify这个方法,下面我们来看一下该方法的代码,如下所示:

// 发布通知notify () {// stabilize the subscriber list first//subs数组中存储的就是`watcher`对象,调用slice方法实现了克隆,因为下面会对subs数组中的内容进行排序。const subs = this.subs.slice()if (process.env.NODE_ENV !== 'production' && !config.async) {// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// order//按照Watcher对象中的id值进行从小到大的排序,也就是按照`watcher`的创建顺序进行排序,从而保证了在执行`watcher`的时候顺序是正确的。subs.sort((a, b) => a.id - b.id)}// 调用每个watcher对象的update方法实现更新for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}

在上面的代码中,对subs数组进行遍历,然后获取对应的Watcher,然后调用Watcher对象的update方法,下面我们来看一下update这个方法中的内容

update () {/* istanbul ignore else *///在渲染watcher的时候,把lazy属性与sync属性设置为了falseif (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {//渲染watcher会执行queueWatcher,//该方法的作用就是将watcher的实例放到一个队列中。queueWatcher(this)}}

下面,我们来查看一下queueWatcher方法内部的代码:

export function queueWatcher (watcher: Watcher) {const id = watcher.id//获取watcher的id属性//has是一个对象,下面获取has中的值,如果为null,表示当前这个watcher对象还没有被处理。//下面加这个判断的目的,就是为了防止watcher被重复性的处理。if (has[id] == null) {has[id] = true//把has[id]设置为true,表明当前的watcher对象已经被处理了。//下面就是开始正式的处理watcher//flushing为true,表明queue这个队列正在被处理。队列中存储的是watcher对象,也就是watcher对象正在被处理。//如果下面的判断条件成立,表明没有处理队列,那么就将watcher放到队列中。if (!flushing) {queue.push(watcher)} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.//如果执行else表明队列正在被处理,那么这里需要找到队列中一个合适位置,然后把watcher插入到队列中。//那么这里是怎样获取位置的呢?//首先获取队列的长度。//index表示现在处理到了队列中的第几个元素,如果i大于index,则表明当前这个队列并没有处理完。//下面需要从后往前,取到队列中的每个watcher对象,然后判断id是否大于watcher.id,如果大于正在处理的这个watcher的id,那么这个位置就是插入watcher的位置let i = queue.length - 1while (i > index && queue[i].id > watcher.id) {i--}//下面就是把待处理的watcher放到队列的合适位置。queue.splice(i + 1, 0, watcher)//上面的代码其实就是把当前将要处理的watcher对象放到队列中。//下面就开始执行队列中的watcher对象。}// queue the flush//下面判断的含义就是判断一下当前的队列是否正在被执行。//如果watiing为false,表明当前队列没有被执行,下面需要将waiting设置为true.if (!waiting) {waiting = trueif (process.env.NODE_ENV !== 'production' && !config.async) {//开发环境直接调用下面的flushSchedulerQueue方法//flushSchedulerQueue方法的作用会遍历队列,然后调用队列中每个watcher的run方法。flushSchedulerQueue()return}//生产环境会将flushSchedulerQueue函数传递到nextTick函数中,后面再来讲解nextTick的应用。nextTick(flushSchedulerQueue)}}
}

下面我们来看一下flushSchedulerQueue方法内部的代码。

function flushSchedulerQueue () {currentFlushTimestamp = getNow()flushing = true//将flushing设置为true,表明正在处理队列let watcher, id// Sort queue before flush.// This ensures that://组件的更新顺序是从父组件到子组件,因为先创建了父组件后创建了子组件// 1. Components are updated from parent to child. (because parent is always//    created before the child)//组件的用户watcher,要在渲染watcher之前运行,因为用户watcher是在渲染watcehr之前创建的。// 2. A component's user watchers are run before its render watcher (because//    user watchers are created before the render watcher)// 如果一个组件,在父组件执行前被销毁了,那么对应的watcher应该跳过。// 3. If a component is destroyed during a parent component's watcher run,//    its watchers can be skipped.//对队列中的watcher进行排序,排序的方式是根据对应id,从小到大的顺序 进行排序。也就是按照watcher的创建顺序进行排列。//为什么要进行排序呢?上面的注释已经给出了三点的说明queue.sort((a, b) => a.id - b.id)// do not cache length because more watchers might be pushed// as we run existing watchers//以上注释的含义:不要缓存length,因为watcher在执行的过程中,还会向队列中放入新的watcher.for (index = 0; index < queue.length; index++) {//对队列进行遍历,然后取出当前要处理的watcher.watcher = queue[index]if (watcher.before) {//判断是否有before这个函数,该函数是在渲染watcher中具有的一个函数。其作用就是触发beforeupdate这个钩子函数。//也就是说走到这个位置beforeupate这个钩子函数被触发了。watcher.before()}id = watcher.id//获取watcher的idhas[id] = null//将has[id]的值设为null,表明当前的watcher已经被处理过了。watcher.run()//执行watcher中的run方法。下面看一下run方法中的源码。// in dev build, check and stop circular updates.if (process.env.NODE_ENV !== 'production' && has[id] != null) {circular[id] = (circular[id] || 0) + 1if (circular[id] > MAX_UPDATE_COUNT) {warn('You may have an infinite update loop ' + (watcher.user? `in watcher with expression "${watcher.expression}"`: `in a component render function.`),watcher.vm)break}}}

下面是run方法的实现代码:

run () {//标记当前的watcher对象是否为存活的状态。active默认值为true,表明可以对watcher进行处理。if (this.active) {const value = this.get()//调用watcher对象中的get方法,在get方法中会进行判断,如果是渲染watcher会调用updatecomponent方法,来渲染组件,更新视图//对于渲染watcher来说,对应的updateComponent方法是没有返回值,所以常量value的值为undefined.所以下面的代码不在执行,但是是用户 watcher,那么会调用其对应的回调函数,我们创建侦听器的时候,指定了回调函数。if (value !== this.value ||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value) ||this.deep) {// set new valueconst oldValue = this.valuethis.value = valueif (this.user) {try {this.cb.call(this.vm, value, oldValue)} catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)}} else {this.cb.call(this.vm, value, oldValue)}}}}

以上就是数据更新后,watcher的执行过程。

我们总结一下:当数据发生了变化后,会调用Dep中的notify方法去通知watcher,首先会将watcher放入到一个队列中,然后遍历队列,调用Watcher对象的run方法,在run方法中调用了渲染watcherupdatecomponet这个函数来渲染组件,更新视图,以上就是整个的处理过程。

20、总结响应式处理的过程

响应式是从Vue实例的initState方法开始的,在initState中完成了Vue实例状态的初始化,在initState方法的内部调用了initData方法,该方法的作用就是把data属性注入到了Vue的实例中,并且在其内部调用了observe方法,在observe方法中把data对象转换成响应式对象。所以说observe就是响应式的入口,下面我们来看一下在observe方法中做了哪些事情:

observe方法所在的位置src/core/observer/index.js.

在调用observe方法的时候,会传递一个参数value,所以在observe方法中首先会判断一下传递过来的参数value是否为对象,如果不是对象直接返回。

然后判断value对象是否有__ob__属性,如果有说明之前已经对其做过响应式的处理,所以直接返回。

如果没有__ob__属性,在创建observer对象,最后将observer对象返回。

那在创建observer对象的时候又做了哪些事情呢?

observer对象是有Observer类创建的(位置:src/core/observer/index.js),在Observer类的构造函数中,给value对象定义不可枚举的__ob__属性,并且通过该数据记录当前的observer对象。接下来进行了数组的响应式处理与对象的响应式处理。

在对数组进行响应式处理的时候,主要是设置了数组常用的方法,例如push,pop等。这些方法会改变原数组,所以当这些方法调用的时候,会发送通知。

在发送通知的时候,找到数组对应的__ob__属性,也就是observer对象,再找到observer对象的dep,然后调用dep中的notify方法。

更改了这些数组的方法后,下面就开始遍历数组中的每个成员,对每一个成员再去调用observer,如果这个成员是对象,也会将这个对象转换成响应式。

这就是关于数组的响应式处理。

下面我们再来看一下关于对象的响应式处理。关于对象的响应式处理,调用的是walk方法。

walk方法内部会遍历对象中的所有属性,对每一个属性调用defineReactive方法(位置:src/core/observer/index.js),在defineReactive方法的内部会为每一个属性创建dep对象。让dep收集依赖。如果当前对象的属性值是对象,则会调用observe,也就是说如果当前对象的属性是对象,调用observe方法的目录就是把这个对象也转换成响应式对象。

defineReactive方法的内部定义了gettersetter.

getter中收集依赖,当然在收集依赖的时候,会为每一个属性收集依赖。如果属性的值为对象,也要收集依赖。getter方法最终会返回属性的值。

下面看一下setter,在setter方法中会保存新值,如果新值是对象也会调用observe,把这个新设置的对象也转换为响应式的对象。在setter方法椎间盘每个。数据发生变化,所以会派发通知,调用dep.notify方法。

下面我们再来看一下关于收集依赖的过程。在收集依赖的过程中,首先会调用watcher对象的get方法,在get方法中调用了pushTarget,在该方法中会将当前的watcher对象记录到Dep.target属性中。

在访问data中的成员的时候收集依赖,当访问属性的值时候会触发defineReactive中的getter方法来收集依赖。这时候会把属性对应的watcher对象添加到depsubs数组中。如果属性的值也是对象,这时会创建一个childOb对象,为子对象收集依赖,目的就是在子对添加或者是删除成员的时候发送通知。

下面我们再来看一下Watcher,当数据发生变化的时候会调用dep.notify方法发送通知,同时在内容调用了update方法,在该方法中又调用了queueWatcher方法,在queueWatcher方法中会判断当前的watcher是否被处理了。如果没有处理,在添加到queue队列中,并调用了flushSchedulerQueue()方法,在该方法中触发了beforeUpdate这个钩子函数,然后调用了watcher.run方法,在该方法中最终调用了updateComponent方法(当前是渲染watcher).这时已经将数据更新到了视图中,那么我们在页面中看到了最新的数据。最后触发了actived钩子函数和updated钩子函数。

21、动态添加一个响应式属性

在这里我们考虑一个问题,就是给一个响应式对象,动态增加一个属性,那么这个属性是否为响应式的呢?

下面我们通过如下代码来演示。

<body><div id="app">{{obj.title}}<hr>{{obj.name}}<hr>{{arr}}
</div>
<script src="./vue.js"></script>
<srcipt>const vm=new Vue({el:'#app',data:{obj:{title:'Hello World'},arr:[2,2,3]}	})</srcipt></body>

在上面的代码中,data中定义了一个对象obj,并且obj中有一个属性title,并且展示在了视图中,

同时在视图中还展示了obj对象中的name属性,但是我们并没有在obj对象中创建name属性。下面我们会动态的向

obj对象中动态的增加一个属性name,看一下是否会渲染视图。

下面我们在浏览器中打开上面的页面,会展示titlearr数组中的内容。

但是由于没有name,所以不会展示。

打开浏览器的控制台,在Console中输入如下代码:

vm.obj.name="abc"

执行完上面的代码后,并没有看到对应的视图的变化,所以动态添加的name属性并不是响应式的。

如果我们这里有这个需求,可以通过vm.$set或者是Vue.set来解决(这两个方法是一样的)。

如下代码:

vm.$set(vm.obj,'name','zhangsan')

上面的代码,通过Vue的实例调用了$set方法,给obj对象添加了name属性,值为zhangsan.

通过以上方式添加属性为响应式的。

下面我们修改一下arr数组中的第一项内容,看一下是否为响应式的。

如果是arr[0]=100这种修改方式,并不是响应式的,关于这一点我们在前面也讲解过。

我么可以使用slice函数来修改,或者可以使用vm.$set方法也是可以的。

vm.$set(vm.arr,0,100)

arr数组中的下标为0的这一项的值修改为100/

以上代码实现的操作为响应式的。

关于vm.$set的使用也可以参考官方文档。

https://cn.vuejs.org/v2/api/#Vue-set

向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')

注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象

以下代码是错误的。

vm.$set(vm,'abc','a')

以上代码是想vue的实例添加属性abc,以上写法是错误的。

vm.$set(vm.$data,'abc','12')

以上代码是向data中添加属性,以上代码也是错误的。表明不能向Vue实例的根数据对象中动态添加属性。

以上就是关于set方法的使用的方式。下一小节,我们来查看set的源代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/670322.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

杨中科 配置系统

1、配置系统入门 说明 1、传统Web.config配置的缺点&#xff0c;之前DI讲到过 2、为了兼容&#xff0c;仍然可以使用Web.config和ConfigurationManager类&#xff0c;但不推荐。 3、.NET 中的配置系统支持丰富的配置源&#xff0c;包括文件(json、xml、ini等)、注册表、环境变…

序列化和反序列化、pytest-DDT数据驱动

序列化 序列化就是将对象转化成文件 python转成json import jsondata {"数字": [1, 1.1, -1],"字符串": ["aaaa", bbbb],"布尔值": [True, False],"空值": None,"列表": [[1, 2, 3], [4, 5, 6], [7, 8, 9]],&…

OpenCV/C++:点线面相关计算(二)

接续&#xff0c;继续更新 OpenCV/C:点线面相关计算_线面相交的点 代码计算-CSDN博客文章浏览阅读1.6k次&#xff0c;点赞2次&#xff0c;收藏12次。OpenCV处理点线面的常用操作_线面相交的点 代码计算https://blog.csdn.net/cd_yourheart/article/details/125626239 目录 1、…

git的奇特知识点

展示帮助信息 git help -gThe common Git guides are:attributes Defining attributes per pathcli Git command-line interface and conventionscore-tutorial A Git core tutorial for developerscvs-migration Git for CVS usersdiff…

Unity_修改天空球

Unity_修改天空球 Unity循序渐进的深入会发现可以改变的其实很多&#xff0c;剖开代码逻辑&#xff0c;可视化的表现对于吸引客户的眼球是很重要的。尤其对于知之甚少的客户&#xff0c;代码一般很难说服客户&#xff0c;然表现确很容易。 非代码色彩通才&#xff0c;持续学习…

《Docker极简教程》--Docker基础--Docker的基本概念

在这篇文章中我们先大致的了解以下Docker的基本概念&#xff0c;在后续的文章中我们会详细的讲解这些概念以及使用。 一、容器(Container) 1.1 容器的定义和特点 容器的定义 容器是一种轻量级、可移植的软件打包技术&#xff0c;用于打包应用及其依赖项和运行环境&#xff0c…

[英语学习][27][Word Power Made Easy]的精读与翻译优化

[序言] 译者的这次翻译非常好. 对what与从句的嵌套用法&#xff0c; 进行了精准的翻译. 这次的记录, 也是对我自己的一次翻译经验的提升. 但是唯一遗憾的是"derivation"没有翻译好. [英文学习的目标] 提升自身的英语水平, 对日后编程技能的提升有很大帮助. 希望大家…

STM32F1 引脚重映射功能

STM32 端口引脚重映射 文章目录 STM32 端口引脚重映射前言1、查阅芯片数据手册1.1 串口引脚重映射描述 2、代码部分2.1 核心代码部分 3、实验现象4、总结 前言 在写程序时遇到想要的端口功能&#xff0c;而这个引脚又被其它的功能占用了无法删除掉或直接使用&#xff0c;这种情…

蓝桥杯----凑算式

这个算式中A~I代表1~9的数字,不同的字母代表不同的数字。 比如: 68/3952/714 就是一种解法, 53/1972/486 是另一种解法. 这个算式一共有多少种解法? 注意:你提交应该是个整数,不要填写任何多余的内容或说明性文字。

Leetcode—42. 接雨水【困难】

2024每日刷题&#xff08;112&#xff09; Leetcode—42. 接雨水 空间复杂度为O(n)的算法思想 实现代码 class Solution { public:int trap(vector<int>& height) {int ans 0;int n height.size();vector<int> l(n);vector<int> r(n);for(int i 0; …

javaEE - 24( 20000 字 Servlet 入门 -2 )

一&#xff1a; Servlet API 详解 1.1 HttpServletResponse Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到HttpServletResponse 对象中. 然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过S…

2024数据分析管理、数字经济与教育国际学术会议(ICDAMDEE2024)

会议简介 2024年数据分析管理、数字经济和教育国际学术会议&#xff08;ICDAMDEE 2024&#xff09;将在武汉举行。会议不仅展示了来自世界各地的研究专家围绕数据分析管理、数字经济和教育的最新科研成果&#xff0c;还为来自不同地区的代表们提供了面对面的交流意见和实验经验…

[C++] opencv + qt 创建带滚动条的图像显示窗口代替imshow

在OpenCV中&#xff0c;imshow函数默认情况下是不支持滚动条的。如果想要显示滚动条&#xff0c;可以考虑使用其他库或方法来进行实现。 一种方法是使用Qt库&#xff0c;使用该库可以创建一个带有滚动条的窗口&#xff0c;并在其中显示图像。具体步骤如下&#xff1a; 1&…

ES6扩展运算符——三个点(...)用法详解

目录 1 含义 2 替代数组的 apply 方法 3 扩展运算符的应用 &#xff08; 1 &#xff09;合并数组 &#xff08; 2 &#xff09;与解构赋值结合 &#xff08; 3 &#xff09;函数的返回值 &#xff08; 4 &#xff09;字符串 &#xff08; 5 &#xff09;实现了 Iter…

耳机空间音频头部相关传递函数(HRTF)设计方法与空间音频渲染

加我微信hezkz17可以申请加入数字音频系统研究开发交流答疑群,加群附加赠送车载DSP音频项目核心开发资料(包含声场,最佳听音位),TWS降噪蓝牙耳机项目资料,空间音频源码 在空间音频的应用里最常见的一种就是“听音辨位”。比如在很多射击游戏中,我们能够通过耳机中目标的…

Java实现教学过程管理系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 教师端2.2 学生端2.3 微信小程序端2.3.1 教师功能如下2.3.2 学生功能如下 三、系统展示 四、核心代码4.1 查询签到4.2 签到4.3 查询任务4.4 查询课程4.5 生成课程成绩 六、免责说明 一、摘要 1.1 项目介绍 基于JAVAVu…

Python学习路线 - Python高阶技巧 - PySpark案例实战

Python学习路线 - Python高阶技巧 - PySpark案例实战 前言介绍Spark是什么Python On SparkPySparkWhy PySpark 基础准备PySpark库的安装构建PySpark执行环境入口对象PySpark的编程模型 数据输入RDD对象Python数据容器转RDD对象读取文件转RDD对象 数据计算map方法flatMap方法red…

数据结构高级算法

目录 最小生成树 Kruskal(克鲁斯卡尔)(以边为核心) 9) 不相交集合(并查集合) 基础 Union By Size 图-相关题目 4.2 Greedy Algorithm 1) 贪心例子 Dijkstra Prim Kruskal 最优解(零钱兑换)- 穷举法 Leetcode 322 最优解(零钱兑换)- 贪心法 Leetcode 322 3)…

opensuse安装百度Linux输入法

前言 Linux下有输入法&#xff0c;拼音&#xff0c;百度的都有&#xff0c;但是用起来总感觉不如在windows下与安卓中顺手。 目前搜狗与百度都出了Linux的输入法&#xff0c;但是没有针对OpenSUSE的&#xff0c;只有ubuntu/deepin/UOS的安装包。 本文主要讲的如何把百度Linux输…

Java 获取 Linux服务器主机名称、内网ip、cpu使用率、内存使用率、磁盘使用率、JVM使用率

下面的代码直接打包带走使用 1、pom文件依赖 <dependency><groupId>com.jcraft</groupId><artifactId>jsch</artifactId><version>0.1.55</version> <!-- 请根据实际情况检查最新版本 --></dependency>2、代码 package…