-如果你需要动态编译模版(比如:将字符串模版传递给 template
选项,或者通过提供一个挂载元素的方式编写 html
模版),你将需要编译器,因此需要一个完整的构建包。
当你使用 vue-loader
或者 vueify
时,*.vue
文件中的模版在构建时会被编译为 JavaScript 的渲染函数。因此你不需要包含编译器的全量包,只需使用只包含运行时的包即可。
只包含运行时的包体积要比全量包的体积小 30%
。因此尽量使用只包含运行时的包,如果你需要使用全量包,那么你需要进行如下配置:
module.exports = {// ...resolve: {alias: {'vue$': 'vue/dist/vue.esm.js'}}
}
const alias = require('rollup-plugin-alias')rollup({// ...plugins: [alias({'vue': 'vue/dist/vue.esm.js'})]
})
{// ..."browser": {"vue": "vue/dist/vue.common.js"}
}
从入口代码开始分析,我们先来分析 new Vue
背后发生了哪些事情。我们都知道,new
关键字在 Javascript 语言中代表实例化是一个对象,而 Vue 实际上是一个类,类在 Javascript 中是用 Function
来实现的,来看一下源码,在src/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
只能通过 new 关键字初始化,然后会调用 this._init
方法, 该方法在 src/core/instance/init.js
中定义。
Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._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 observedvm._isVue = true// merge 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 {vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {initProxy(vm)} else {vm._renderProxy = vm}// expose real selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {vm._name = formatComponentName(vm, false)mark(endTag)measure(`vue ${vm._name} init`, startTag, endTag)}if (vm.$options.el) {vm.$mount(vm.$options.el)}
}
Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。
响应式的核心是通过 Object.defineProperty
拦截对数据的访问和设置
响应式的数据分为两类:
methods、computed、watch 有什么区别
{{ returnMsg() }}{{ returnMsg() }}{{ getMsg }}{{ getMsg }}
methods
一般用于封装一些较为复杂的处理逻辑(同步、异步)computed
一般用于封装一些简单的同步逻辑,将经过处理的数据返回,然后显示在模版中,以减轻模版的重量watch
一般用于当需要在数据变化时执行异步或开销较大的操作methods VS computed
如果在一次渲染中,有多个地方使用了同一个 methods 或 computed 属性,methods 会被执行多次,而 computed 的回调函数则只会被执行一次。
在一次渲染中,多次访问
computedProperty
,只会在第一次执行computed
属性的回调函数,后续的其它访问,则直接使用第一次的执行结果(watcher.value
),而这一切的实现原理则是通过对watcher.dirty
属性的控制实现的。而methods
,每一次的访问则是简单的方法调用(this.xxMethods)。
computed VS watch
computed 和 watch 的本质是一样的,内部都是通过
Watcher
来实现的,其实没什么区别,非要说区别的话就两点:1、使用场景上的区别,2、computed 默认是懒执行的,切不可更改。
methods VS watch
methods 和 watch 之间其实没什么可比的,完全是两个东西,不过在使用上可以把 watch 中一些逻辑抽到 methods 中,提高代码的可读性。
Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。
当响应式数据更新后,会调用 dep.notify
方法,通知 dep 中收集的 watcher
去执行 update
方法,watcher.update 将 watcher 自己放入一个 watcher 队列
(全局的 queue 数组
)。
然后通过 nextTick
方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks
数组中。
如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks
的函数,则执行 timerFunc
函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。
flushCallbacks 函数负责执行 callbacks 数组中的所有
flushSchedulerQueue
函数。
flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。
Vue.nextTick 或者 vm.$nextTick 的原理其实很简单,就做了两件事:
将传递的回调函数用 try catch
包裹然后放入 callbacks
数组
执行 timerFunc
函数,在浏览器的异步任务队列放入一个刷新 callbacks 数组的函数
负责安装 plugin 插件,其实就是执行插件提供的 install 方法。
- 首先判断该插件是否已经安装过
- 如果没有,则执行插件提供的 install 方法安装插件,具体做什么有插件自己决定
负责在 Vue 的全局配置上合并 options 配置。然后在每个组件生成 vnode 时会将全局配置合并到组件自身的配置上来。
- 标准化 options 对象上的 props、inject、directive 选项的格式
- 处理 options 上的 extends 和 mixins,分别将他们合并到全局配置上
- 然后将 options 配置和全局配置进行合并,选项冲突时 options 配置会覆盖全局配置
负责注册全局组件。其实就是将组件配置注册到全局配置的 components 选项上(options.components),然后各个子组件在生成 vnode 时会将全局的 components 选项合并到局部的 components 配置项上。
- 如果第二个参数为空,则表示获取 compName 的组件构造函数
- 如果 Comp 是组件配置对象,则使用 Vue.extend 方法得到组件构造函数,否则直接进行下一步
- 在全局配置上设置组件信息,this.options.components.compName = CompConstructor
在全局注册 my-directive 指令,然后每个子组件在生成 vnode 时会将全局的 directives 选项合并到局部的 directives 选项中。原理同 Vue.component 方法:
- 如果第二个参数为空,则获取指定指令的配置对象
- 如果不为空,如果第二个参数是一个函数的话,则生成配置对象
- 然后将指令配置对象设置到全局配置上,
this.options.directives['my-directive'] = {xx}
负责在全局注册过滤器 my-filter,然后每个子组件在生成 vnode 时会将全局的 filters 选项合并到局部的 filters 选项中。原理是:
- 如果没有提供第二个参数,则获取 my-filter 过滤器的回调函数
- 如果提供了第二个参数,则是设置
this.options.filters['my-filter'] = function(val) {xx}
Vue.extend 基于 Vue 创建一个子类,参数 options 会作为该子类的默认全局配置,就像 Vue 的默认全局配置一样。所以通过 Vue.extend 扩展一个子类,一大用处就是内置一些公共配置,供子类的子类使用。
- 定义子类构造函数,这里和 Vue 一样,也是调用 _init(options)
- 合并 Vue 的配置和 options,如果选项冲突,则 options 的选项会覆盖 Vue 的配置项
- 给子类定义全局 API,值为 Vue 的全局 API,比如
Sub.extend = Super.extend
,这样子类同样可以扩展出其它子类- 返回子类 Sub
由于 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = ‘hi’),所以通过 Vue.set 为向响应式对象中添加一个 property,可以确保这个新 property 同样是响应式的,且触发视图更新。
- 更新数组指定下标的元素:Vue.set(array, idx, val),内部通过 splice 方法实现响应式更新
- 更新对象已有属性:Vue.set(obj, key ,val),直接更新即可 =>
obj[key] = val
- 不能向 Vue 实例或者 $data 动态添加根级别的响应式数据
- Vue.set(obj, key, val),如果 obj 不是响应式对象,会执行
obj[key] = val
,但是不会做响应式处理- Vue.set(obj, key, val),为响应式对象 obj 增加一个新的 key,则通过 defineReactive 方法设置响应式,并触发依赖更新
删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制,但是你应该很少会使用它。当然同样不能删除根级别的响应式属性。
- Vue.delete(array, idx),删除指定下标的元素,内部是通过 splice 方法实现的
- 删除响应式对象上的某个属性:
Vue.delete(obj, key)
,内部是执行 delete obj.key,然后执行依赖更新即可
Vue.nextTick(cb) 方法的作用是延迟回调函数 cb 的执行,一般用于 this.key = newVal 更改数据后,想立即获取更改过后的 DOM 数据:
this.key = 'new val'Vue.nextTick(function() {// DOM 更新了
})
其内部的执行过程是:
this.key = 'new val
,触发依赖通知更新,将负责更新的 watcher 放入 watcher 队列- 将刷新 watcher 队列的函数放到 callbacks 数组中
- 在浏览器的异步任务队列中放入一个刷新 callbacks 数组的函数
- Vue.nextTick(cb) 来插队,将 cb 函数放入 callbacks 数组
- 待将来的某个时刻执行刷新 callbacks 数组的函数
- 然后执行 callbacks 数组中的众多函数,触发 watcher.run 的执行,更新 DOM
- 由于 cb 函数是在后面放到 callbacks 数组,所以这就保证了先完成的 DOM 更新,再执行 cb 函数
vm.$set
用于向响应式对象添加一个新的property
,并确保这个新的 property 同样是响应式的,并触发视图更新。由于 Vue 无法探测对象新增属性或者通过索引为数组新增一个元素,比如:this.obj.newProperty = 'val'
、this.arr[3] = 'val'
。所以这才有了 vm.$set,它是 Vue.set 的别名。
- 为对象添加一个新的响应式数据:调用 defineReactive 方法为对象增加响应式数据,然后执行 dep.notify 进行依赖通知,更新视图
- 为数组添加一个新的响应式数据:通过 splice 方法实现