MVC
js
// ./index.js
// 创建应用对象
var myapp = {}
// model
myapp.Model = function () {
var val = 0
this.add = function (v) {
if (val < 100) val += v
}
this.sub = function (v) {
if (val > 0) val -= v
}
this.getVal = function (v) {
return val
}
// 观察者模式
var self = this, views = []
this.register = function (view) {
views.push(view)
}
this.notify = function () {
for (var i = 0; i < views.length; i++) {
views[i].render(self)
}
}
}
// view
myapp.View = function (controller) {
var $num = $('#num'), $incBtn = $('#increase'), $decBtn = $('#decrease');
this.render = function (model) {
$num.text(model.getVal() + 'rmb');
};
/* 绑定事件 */
$incBtn.click(controller.increase);
$decBtn.click(controller.decrease);
};
// controller
myapp.Controller = function () {
var model = null, view = null;
this.init = function () {
/* 初始化Model和View */
model = new myapp.Model();
view = new myapp.View(this);
/* View向Model注册,当Model更新就会去通知View啦 */
model.register(view);
model.notify();
};
/* 让Model更新数值并通知View更新视图 */
this.increase = function () {
model.add(1);
model.notify();
};
this.decrease = function () {
model.sub(1);
model.notify();
};
};
// init
(function () {
var controller = new myapp.Controller();
controller.init();
})();
html
// ./index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.staticfile.net/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<div id="num">x</div>
<button id="increase">+++</button>
<button id="decrease">---</button>
<script src="./index.js"></script>
</body>
</html>
MVVM
-
数据会绑定在viewModel层并⾃动将数据渲染到⻚⾯中
-
视图变化时,会通知viewModel层更新数据
vue
// view
<template>
<div>
<div>{{ val }}rmb</div>
<div>
<button v-on:click="add(1)">+++</button>
<button v-on:click="sub(1)">---</button>
</div>
</div>
</template>
<script>
// controller
export default {
name: "App",
data() {
return {
val: 0,
};
},
methods: {
add(v) {
this.val += v;
},
sub(v) {
this.val -= v;
},
},
};
</script>
arduino
// Vue是不是MVVM?React呢?
// 严格来讲都不是
// React:ui = render (data) 单向数据流
// Vue: 比如其中的ref,就没有通过view model层,直接获取了真实dom了,而并非是VDOM
1.Vue简介
1.1 Vue.js实例
只有当实例被创建时就已经存在于 data 中的 property 才是响应式的
js
const Vue = require('vue')
const data = { a: 1 }
const vm = new Vue({
data
})
console.log(vm.a === data.a); // true
vm.a = 2
console.log(data.a); // 2
// 反之亦然
// 新增的property没有响应式
vm.b = 123
console.log(vm); // 里面的b没有[Getter/Setter]
console.log(data); // { a: [Getter/Setter] }
想要有响应式,需在实例前设置一些初始值
这⾥唯⼀的例外是使⽤ Object.freeze()
,这会阻⽌修改现有的 property,也意味着响应系统⽆法再追踪变化。
修改被冰冻的数据,视图不会更新。
除了数据 property,Vue 实例还暴露了⼀些有⽤的实例 property 与⽅法。它们都有前缀 $,以便与⽤户定义的 property 区分开来。例如:
js
var data = { a: 1 }
var vm = new Vue({
el: '#example',
data: data
})
vm.$data === data // => true
vm.$el === document.getElementById('example') // => true
// $watch 是⼀个实例⽅法
vm.$watch('a', function (newValue, oldValue) {
// 这个回调将在 `vm.a` 改变后调⽤
})
1.2 动态参数
从 2.6.0 版本开始,可以⽤⽅括号括起来的 JavaScript 表达式作为⼀个指令的参数:
js
// 注意,参数表达式的写法存在⼀些约束
<a v-bind:[attributeName] = "url" > ... </a >
<a v-on:[attributeName] = "url" > ... </a >
<!-- 这会触发⼀个编译警告 -->
<a v-bind:['foo' + bar]="value"> ... </a>
1.3 自定义指令
js
// 注册⼀个全局⾃定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插⼊到 DOM 中时......
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
// 注册⼀个局部⾃定义指令 `v-focus`
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
当我们需要批量注册⾃定义指令时,可以利用Vue.use()的特性
js
// directives/directive.js
// 导⼊指令定义⽂件
import debounce from './debounce'
import throttle from './throttle'
// 集成⼀起
const directives = {
debounce,
throttle,
}
//批量注册
export default {
install(Vue) {
Object.keys(directives).forEach((key) => {
Vue.directive(key, directives[key])
})
},
}
在main.js中引入,并Vue.use()调用注册
js
import Vue from 'vue'
import Directives from './directives/directive.js'
Vue.use(Directives)
其中指令定义对象提供的钩子函数
及钩子函数的参数
详情看官网。
以下为3个自定义指令的示例:
1.v-longpress
js
// directive
const longpress = {
bind: function (el, { value: { fn, time } }) {
//没绑定函数直接返回
if (typeof fn !== 'function') return
// 定义定时器变量
el._timer = null
// 创建计时器( n秒后执⾏函数 )
el._start = (e) => {
//e.type表示触发的事件类型如mousedown,touchstart等
//pc端: e.button表示是哪个键按下0为⿏标左键,1为中键,2为右键
//移动端: e.touches表示同时按下的键为个数
if ((e.type === 'mousedown' && e.button && e.button !== 0) ||
(e.type === 'touchstart' && e.touches && e.touches.length > 1)
) return;
//定时⻓按n秒后执⾏事件
if (el._timer === null) {
el._timer = setTimeout(() => {
fn()
}, time)
//取消浏览器默认事件,如右键弹窗
el.addEventListener('contextmenu', function (e) {
e.preventDefault();
})
}
}
// 如果两秒内松⼿,则取消计时器
el._cancel = (e) => {
if (el._timer !== null) {
clearTimeout(el._timer)
el._timer = null
}
}
// 添加计时监听
el.addEventListener('mousedown', el._start)
el.addEventListener('touchstart', el._start)
// 添加取消监听
el.addEventListener('click', el._cancel)
el.addEventListener('mouseout', el._cancel)
el.addEventListener('touchend', el._cancel)
el.addEventListener('touchcancel', el._cancel)
},
// 指令与元素解绑时,移除事件绑定
unbind(el) {
// 移除计时监听
el.removeEventListener('mousedown', el._start)
el.removeEventListener('touchstart', el._start)
// 移除取消监听
el.removeEventListener('click', el._cancel)
el.removeEventListener('mouseout', el._cancel)
el.removeEventListener('touchend', el._cancel)
el.removeEventListener('touchcancel', el._cancel)
},
}
export default longpress
2.v-debounce
js
const debounce = {
inserted: function (el, { value: { fn, event, time } }) {
//没绑定函数直接返回
if (typeof fn !== 'function') return
el._timer = null
//监听点击事件,限定事件内如果再次点击则清空定时器并重新定时
el.addEventListener(event, () => {
if (el._timer !== null) {
clearTimeout(el._timer)
el._timer = null
}
el._timer = setTimeout(() => {
fn()
}, time)
})
},
}
export default debounce
3.v-throttle
js
const throttle = {
bind: function (el, { value: { fn, time } }) {
if (typeof fn !== 'function') return
el._flag = true;//开关默认为开
el._timer = null
el.handler = function () {
if (!el._flag) return;
//执⾏之后开关关闭
el._flag && fn()
el._flag = false
if (el._timer !== null) {
clearTimeout(el._timer)
el._timer = null
}
el._timer = setTimeout(() => {
el._flag = true;//三秒后开关开启
}, time);
}
el.addEventListener('click', el.handler)
},
unbind: function (el) {
el.removeEventListener('click', el.handler)
}
}
export default throttle
测试
vue
<template>
<div>
<div>
<button v-longpress="{ fn: long, time: 1500 }">长按</button>
<div>
<input
v-debounce="{ fn: debounce, event: 'input', time: 2500 }"
type="text"
/>
</div>
<button v-throttle="{ fn: throttle, time: 2500 }">节流</button>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {};
},
methods: {
long() {
console.log("被长按了");
},
debounce() {
console.log("防抖");
},
throttle() {
console.log("节流");
},
},
};
</script>
1.4 Vue mixin
mixin中的数据和方法都是独立的,组件之间使用后是不互相影响的。
当mixin中定义的属性或方法的名称与组件中定义的名称有冲突,会怎么办?
这里我的简要理解是这样的:在生命周期的执行顺序角度来想,mixin的会先执行,后执行组件的,那么组件的数据就会覆盖mixin的。
下图是mixin和组件的生命周期打印顺序:
mixin的实现
打开 core/global-api/mixin.js
实际上就是将两个选项配置进行合并
js
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
2. Vue 插件
2.1 介绍
官网中提到,插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制,一般有下面几种:
-
添加全局方法或者 property。如:vue-custom-element;
-
添加全局资源:指令/过滤器/过渡等。如 vue-touch;
-
通过全局混入来添加一些组件选项。如 vue-router;
-
添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现;
-
一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router;
通过全局方法 Vue.use()
使用插件。它需要在你调用 new Vue()
启动应用之前完成:
js
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
})
也可以传入一个可选的选项对象:
js
Vue.use(MyPlugin, { someOption: true })
Vue.use 会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
Vue.js 官方提供的一些插件 (例如 vue-router) 在检测到 Vue 是可访问的全局变量时会自动调用 Vue.use() 。然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用 Vue.use() :
js
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
开发插件
Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:
js
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind(el, binding, vnode, oldVnode) {
// 逻辑...
}
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
2.2 原理解析
Vue插件概括出来就是:
-
通过 Vue.use(MyPlugin) 使用,本质上是调用 MyPlugin.install(Vue) ;
-
使用插件必须在new Vue()启动应用之前完成,实例化之前就要配置好;
-
如果使用Vue.use多次注册相同插件,那只会注册成功一次;
Vue.use 定义在 src/core/global-api 中,源码如下:
js
Vue.use = function (plugin) {
// 忽略已注册插件
if (plugin.installed) {
return
}
// 集合转数组,并去除第一个参数
var args = toArray(arguments, 1);
// 把this(即Vue)添加到数组的第一个参数中
args.unshift(this);
// 调用install方法
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
plugin.apply(null, args);
}
// 注册成功
plugin.installed = true;
return this;
};
Vue.use接受一个对象参数plugin,首先判断是否已注册,如果多次注册相同插件那么只会注册成功一次,在注册成功后设置 plugin.installed = true
。
然后执行 toArray(arguments, 1)
方法,arguments是一个表示所有参数的类数组对象,需要转换成数组之后才能使用数组的方法。
js
function toArray(list, start) {
start = start || 0;
var i = list.length - start;
var ret = new Array(i);
// 循环去除 前start元素
while (i--) {
ret[i] = list[i + start];
}
return ret
}
上面进行了一次转换,假设list是[1, 2, 3, 4],start是1,首先创建一个包含3个元素的数组,依次执行 ret[2] = list[ 2 + 1] 、 ret[1] = list[ 1 + 1] 、 ret[0] = list[ 0 + 1]
,实际上就是去除arguments的第一个参数然后把剩余的类数组赋值给新的数组,其实就是去除plugin参数,因为调用plugin.install的时候不需要这个参数。
转换成数组之后调用 args.unshift(this) ,把Vue对象添加到args的第一个参数中,这样就可以在调用 plugin.install 方法的时候把Vue对象传递过去。
2.3 实现一个插件
要求创建一个告诉Vue组件处理自定义rules规则选项的插件,这个rules需要一个对象,该对象指定组件中的数据的验证规则。
js
const vm = new Vue({
data: { foo: 10 },
rules: {
foo: {
validate: value => value > 1,
message: 'foo must be greater than one'
}
}
})
vm.foo = 0 // 输出 foo must be greater than one
- 先不考虑插件,在已有的VueAPI中是没有rules这个公共方法的,如果要简单实现的话可以通过钩子函数来,即在created 里面验证逻辑:
js
const vm = new Vue({
data: { foo: 10 },
rules: {
foo: {
validate: value => value > 1,
message: 'foo must be greater than one'
}
},
created: function () {
// 验证逻辑
const rules = this.$options.rules
if (rules) {
Object.keys(rules).forEach(key => {
// 取得所有规则
const { validate, message } = rules[key]
// 监听,键是变量,值是函数
this.$watch(key, newValue => {
// 验证规则
const valid = validate(newValue)
if (!valid) {
console.log(message)
}
})
})
}
}
})
可以通过 this.$options.rules
获取到自定义的rules对象,然后对所有规则遍历,使用自定义的 validate(newValue) 验证规则。
-
实现这个rules插件,为了在Vue中直接使用,可以通过
Vue.mixin
注入到Vue组件中,这样所有的Vue实例都可以使用。按照插件的开发流程,应该有一个公开方法
install
,在 install 里面使用全局的mixin方法添加一些组件选项, mixin 方法包含一个 created 钩子函数,在钩子函数中验证this.$options.rules
。
js
import Vue from 'vue'
// 定义插件
const RulesPlugin = {
// 插件应该有一个公开方法install
// 第一个参数是Vue 构造器
// 第二个参数是一个可选的选项对象
install(Vue) {
// 注入组件
Vue.mixin({
// 钩子函数
created: function () {
// 验证逻辑
const rules = this.$options.rules
if (rules) {
Object.keys(rules).forEach(key => {
// 取得所有规则
const { validate, message } = rules[key]
// 监听,键是变量,值是函数
this.$watch(key, newValue => {
// 验证规则
const valid = validate(newValue)
if (!valid) {
console.log(message)
}
})
})
}
}
})
}
}
// 调用插件,实际上就是调用插件的install方法
// 即RulesPlugin.install(Vue)
Vue.use(RulesPlugin)
3. Vue的设计思路(这里Vue的版本为2.6.14,主要研究web端)
在Vue项目中,所有核心的代码都是在src目录下完成,首先来了解一下src目录下代码的组织情况
javascript
├── compiler // 编译模块:将 template 编译成为可以生成 vnode 的 render 函数
│ ├── codeframe.js
│ ├── codegen // 代码生成文件:根据 ast 树可生成 vnode 的 render代码
│ ├── create - compiler.js // 创建编译器的工厂函数
│ ├── directives // 指令解析:v-on, v-bind, v-model
│ ├── error - detector.js
│ ├── helpers.js // 编译相关方法,如属性获取等方法
│ ├── index.js // 入口文件
│ ├── optimizer.js // 编译优化:将 ast 树进行优化
│ ├── parser // html 解析文件:将 template 解析成 ast 树
│ └── to - function.js // 创建编译器的工厂函数
├── core // 构造函数核心模块:构建Vue构造函数,添加原型方法,实现完成渲染流程的_init方法
│ ├── components // 自带的全局组件,如 keep-alive
│ ├── config.js // 配置相关
│ ├── global - api // 全局api,如 Vue.use, extend, mixin, component等方法
│ ├── index.js // 入口文件,在 Vue 上挂载全局方法并导出 Vue
│ ├── instance // 构造函数起始位置
│ ├── observer // 响应式原理
│ ├── util // 一些工具方法,包含 mergeOptions, nextTick 等方法的实现
│ └── vdom // 虚拟 dom
├── platforms // 平台相关,包含不同平台的不同构建入口,这里主要研究web端
│ ├── weex
│ └── web
│ ├── compiler // 与平台相关的编译
│ ├── entry - compiler.js // vue-template-compiler 包的入口文件
│ ├── entry - runtime -with-compiler.js // 构建入口,包含编译器
│ ├── entry - runtime.js // 构建入口,不包含编译器,不支持 template 转换 render
│ ├── entry - server - basic - renderer.js
│ ├── entry - server - renderer.js
│ ├── runtime // 与平台相关的构建
│ ├── server
│ └── util
│
├── server // 服务端渲染相关
├── sfc // 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
└── shared // 代码库通用代码
├── constants.js
└── util.js
3.1 Vue的真实面目
第一步想要真到Vue的真正入口,先找到package.json文件下的scripts配置。
实际就是运行 scripts/config.js 文件
通过运行命令参数我们可以知道 process.env.TARGET 的值为 web-full-dev ,因此可以在builds里找到对应的配置文件,如下:
看到entry入口,再依次找到src/platforms/web/entry-runtime-with-compiler.js
再一步步通过import Vue的文件找到定义Vue构造函数的地方在 src/core/instance/index.js
js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
可以看出Vue其实就是一个构造函数:
-
原型方法属性:通过 5 个 init 方法,向Vue的原型上添加方法;
-
静态方法属性:在导入Vue构造函数的过程中,向Vue构造函数上添加静态方法,也有向原型上添加方法;
-
实例化:在实例化的过程中,执行_init方法,完成整个Vue初始化到渲染的逻辑;
Vue的原型方法(通过5个init方法添加)
3.1.1 initMixin
从上面Vue构造函数我们可以知道,这个方法在实例化时有被调用,它主要的作用是实现:选项的合并,数据初始化(如响应式处理),以及触发编译和渲染的流程,所以十分重要。这里也只是先做一个进阶时的了解,详细部分在主解源码时再解析。
3.1.2 stateMixin
stateMixin主要实现了 data , props 的代理功能,即当我们访问 $data
时,实际访问的是 _data 。另外在非生产环境下,会对 $data
, $props
进行 set处理,每次设置新的值时都会打印提示,所以实际上 $data
, $props
都是只读属性。
3.1.3 eventsMixin
和node里EventEmitter类似,eventsMixin实现了四个方法: $on , $off , $once , $emit
,用于监听,触发,销毁事件:
3.1.4 lifecycleMixin
lifecycleMixin实现了三个方法: _update 方法非常重要,它主要负责将 vnode 生成真实节点。
3.1.5 renderMixin
-
installRenderHelpers 函数用于添加render相关方法,在编译环节最后生成的代码,都是由这些方法拼接而成的代码,相当于AST中最后生成代码的阶段;
-
$nextTick 方法,在下一次事件循环触发,涉及到事件循环机制;
-
_render 方法,用于生成 vnode ;