当前位置: 首页 > news >正文

安庆网站建设公司深圳建设局官网站

安庆网站建设公司,深圳建设局官网站,百达翡丽手表网站,商城 静态网站模板一、Vue源码解析–响应式原理 1、课程目标 Vue.js的静态成员和实例成员初始化过程​ 首次渲染的过程数据响应式原理 2、准备工作 Vue源码的获取 项目地址#xff1a;https://github.com/vuejs/vue 为什么分析Vue2.6? 新的版本发布后#xff0c;现有项目不会升级到3.0,…一、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,Rollup比Webpack更加轻量Webpack是把所有的文件例如图片文件样式等当作模块进行打包Rollup只处理js文件所以Rollup更适合在Vue.js这样的库中进行使用。 Rollup打包不会生成冗余的代码如果是Webpack打包那么会生成一些浏览器支持模块化的代码。 以上就是Webpack与Rollup之间的区别。根据以上的讲解其实我们可以总结出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 typemodule 直接导入。 如果使用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对象中查找该属性对应的内容。 具体内容如下 // Runtimecompiler 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 vmnew Vue({el:#app, template:h3hello template/h3 render(h){return h(h4,hello render) } })在上面的代码中我们在创建Vue的实例的时候同时指定了template与render那么会渲染执行哪个内容 一会我们通过查看源码来解决这个问题。 下面我们打开入口文件。 先来看一下$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 its 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 ./showexport default {model,show } 通过以上代码我们可以看到这个文件中导出了v-show与v-model这两个指令。 下面再看一下./components/index文件中的内容。 import Transition from ./transition import TransitionGroup from ./transition-groupexport default {Transition,TransitionGroup } 以上文件导出的就是v-Transition与v-TransitionGroup这两个组件的内容。 通过下面两行代码我们还可以发现一个相应的问题。 extend(Vue.options.directives, platformDirectives); extend(Vue.options.components, platformComponents);就是全局的指令与组件分别存储到了Vue.options.directives与Vue.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 watchers constructor// since the watchers initial patch may call $forceUpdate (e.g. inside child// components 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 Weexs 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); //注册vmVue实例的$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.directives与Vue.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 Weexs 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/indexexport 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 cidSub.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 Supers 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/indexexport function initAssetRegisters (Vue: GlobalAPI) {/*** Create asset registration methods.*///变量ASSET_TYPES数组为Vue定义相应的方法//ASSET_TYPES数组包括了directive,component,filterASSET_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); //注册vmVue实例的$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: ClassComponent) {// 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 | Arraystring, 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 parents 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: ClassComponent) {Vue.prototype._init function (options?: Object) {//Vue的实例const vm: Component this// a uid//唯一标识vm._uid uidlet 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,$refsinitLifecycle(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.length0 修改数组的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* objects 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: Arrayany) {for (let i 0, l items.length; i l; i) {observe(items[i])}}Observer类核心的作用就是对数组和对象做响应式的处理。 15、defineReactive 这一小节我们来看一下Observer类中的defineReactive方法在上一小节中我们说过该方法的作用就是就是将对象中的属性转换成getter和setter. 下面看一下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/setterobserve方法返回的就是一个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)}}}现在我们对以上代码有了一个基本的了解下面我们思考一个问题是,在什么时候给Dep的target属性赋值的要回答这个问题我们首先要考虑的是什么时候创建Watcher对象的。 在src/core/instance/lifecycle.js文件中创建了Watcher对象。 找到该文件中的mountComponent方法在该方法中创建了Watcher对象。 // we set this to vm._watcher inside the watchers constructor// since the watchers initial patch may call $forceUpdate (e.g. inside child// components 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的内部看一下在其内部是怎样给Dep的target属性赋值的。在其内部有个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: Arraystring) {//变量传递过来的数组中方法的名字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: Arrayany) {for (let i 0, l items.length; i l; i) {observe(items[i])}} }18、数组练习88888 div idapp{{arr}} /div script srcvue.js/script scriptconst vmnew Vue({el:#app,data:{arr:[2,3,5]}})//看一下如下操作是否为响应式// vm.arr.push(8)// vm.arr[0]100// vm.arr.length0 /script通过前面的代码的阅读我们知道push方法在Vue中做了一定的处理当通过push方法向数组中添加了一个新的数据后对应的视图页面也会进行更新。 vm.arr[0]100,会修改数组中的内容但是当数组中的内容修改了以后视图并没有发生任何的变化所以这种操作并不是响应式的。也就是说通过数组的索引来 修改数组的时候并没有调用Dep中的notify,也就没有通知watcher去重新渲染视图。通过源码我们并没有发现对这种情况的处理也就是没有监听数组中的每个属性index,length都是数组的属性将其转换成响应式同理vm.arr.length0也不是响应式的。 在源码中我们看的是对数组进行遍历对数组中的每个元素中是对象的元素转换成了响应式。 如果现在我们想让 vm.arr[0]100和 vm.arr.length0这种操作变成响应式的效果应该怎样实现呢 首先我们先来看 vm.arr[0]100,就是修改数组中的第一个元素这里我们可以使用splice方法来达到相同的目的而且通过前面阅读源码我们知道splice方法是响应式的也就是通过该发你规范修改完了数组后对应的视图也会进行。所以对应的代码为 //第一个参数0表示删除arr数组中的第一个元素。 //第二个参数1.表示的是删除的个数 //第三个参数100表示的是把删除的元素用100来替换。 vm.arr.splice(0,1,100) 下面我们再来看一下关于vm.arr.length0,表示的是清空数组中的内容。 为了达到响应式的效果这里也可以使用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 arent 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 components 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 components 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方法中调用了渲染watcher的updatecomponet这个函数来渲染组件更新视图以上就是整个的处理过程。 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方法的内部定义了getter和setter. 在getter中收集依赖当然在收集依赖的时候会为每一个属性收集依赖。如果属性的值为对象也要收集依赖。getter方法最终会返回属性的值。 下面看一下setter,在setter方法中会保存新值如果新值是对象也会调用observe,把这个新设置的对象也转换为响应式的对象。在setter方法椎间盘每个。数据发生变化所以会派发通知调用dep.notify方法。 下面我们再来看一下关于收集依赖的过程。在收集依赖的过程中首先会调用watcher对象的get方法在get方法中调用了pushTarget,在该方法中会将当前的watcher对象记录到Dep.target属性中。 在访问data中的成员的时候收集依赖当访问属性的值时候会触发defineReactive中的getter方法来收集依赖。这时候会把属性对应的watcher对象添加到dep的subs数组中。如果属性的值也是对象这时会创建一个childOb对象为子对象收集依赖目的就是在子对添加或者是删除成员的时候发送通知。 下面我们再来看一下Watcher,当数据发生变化的时候会调用dep.notify方法发送通知同时在内容调用了update方法在该方法中又调用了queueWatcher方法在queueWatcher方法中会判断当前的watcher是否被处理了。如果没有处理在添加到queue队列中并调用了flushSchedulerQueue()方法在该方法中触发了beforeUpdate这个钩子函数然后调用了watcher.run方法在该方法中最终调用了updateComponent方法当前是渲染watcher.这时已经将数据更新到了视图中那么我们在页面中看到了最新的数据。最后触发了actived钩子函数和updated钩子函数。 21、动态添加一个响应式属性 在这里我们考虑一个问题就是给一个响应式对象动态增加一个属性那么这个属性是否为响应式的呢 下面我们通过如下代码来演示。 bodydiv idapp{{obj.title}}hr{{obj.name}}hr{{arr}} /div script src./vue.js/script srciptconst vmnew Vue({el:#app,data:{obj:{title:Hello World},arr:[2,2,3]} })/srcipt/body在上面的代码中data中定义了一个对象obj,并且obj中有一个属性title,并且展示在了视图中 同时在视图中还展示了obj对象中的name属性但是我们并没有在obj对象中创建name属性。下面我们会动态的向 obj对象中动态的增加一个属性name,看一下是否会渲染视图。 下面我们在浏览器中打开上面的页面会展示title与arr数组中的内容。 但是由于没有name所以不会展示。 打开浏览器的控制台在Console中输入如下代码 vm.obj.nameabc执行完上面的代码后并没有看到对应的视图的变化所以动态添加的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.zqtcl.cn/news/998901/

相关文章:

  • 做电影网站配什么公众号网站新闻发布系统模板
  • 网站风格发展趋势wordpress悬浮音乐插件
  • 做网站前期费用新注册公司网站建设
  • 建站平台在线提交表格功能检测站点是否使用wordpress
  • 谁能做网站开发免费软件看电视剧
  • 深圳的网站建设网站建设网页设计做网站
  • 广州网站建设网页设计贵阳网站建设宏思锐达
  • 洪栾单页网站建设象山县城乡和住房建设局网站
  • 网站留言发送到邮箱潍坊商城网站建设
  • 四川省的住房和城乡建设厅网站首页产品设计是冷门专业吗
  • 北仑建设银行网站网站设计 导航条
  • 如何做网站宣传片单位做网站费用怎么记账
  • 西安网站建设现状购物app开发
  • 2019年做网站还有前景吗手机制作表格教程
  • 校园网站html模板南昌网站建设优化
  • 网站的建立目的来宾网站优化
  • 建设国家游戏网站网站建设规范方案
  • 做网站价位wordpress tag 列表
  • 网站建设 李奥贝纳百度软文推广公司
  • 网站建设流程平台企业微信开发者文档
  • 唐山建设网站的网站青海网站建设企业
  • 北京企业建站系统模板网站建设公司专业网站科技开发
  • 工商注册在哪个网站手机浏览器网站开发
  • 建设电影网站的目的各个国家的google网站
  • centos 网站搭建中国互联网协会调解中心
  • 手机端视频网站模板下载做单页网站需要做什么的
  • 太原网站建设外包中国做乱的小说网站
  • 青海做网站哪家好旅游网站的功能及建设
  • 百度网站优化工具汉川网页设计
  • 网站标签优化怎么做可以看图片的地图什么软件