Vue的一些相对进阶点(14)

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

  1. 数据会绑定在viewModel层并⾃动将数据渲染到⻚⾯中

  2. 视图变化时,会通知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 添加全局功能。插件的功能范围没有严格的限制,一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element

  2. 添加全局资源:指令/过滤器/过渡等。如 vue-touch

  3. 通过全局混入来添加一些组件选项。如 vue-router

  4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现;

  5. 一个库,提供自己的 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插件概括出来就是:

  1. 通过 Vue.use(MyPlugin) 使用,本质上是调用 MyPlugin.install(Vue) ;

  2. 使用插件必须在new Vue()启动应用之前完成,实例化之前就要配置好;

  3. 如果使用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
  1. 先不考虑插件,在已有的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) 验证规则。

  1. 实现这个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其实就是一个构造函数:

  1. 原型方法属性:通过 5 个 init 方法,向Vue的原型上添加方法;

  2. 静态方法属性:在导入Vue构造函数的过程中,向Vue构造函数上添加静态方法,也有向原型上添加方法;

  3. 实例化:在实例化的过程中,执行_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

  1. installRenderHelpers 函数用于添加render相关方法,在编译环节最后生成的代码,都是由这些方法拼接而成的代码,相当于AST中最后生成代码的阶段;

  2. $nextTick 方法,在下一次事件循环触发,涉及到事件循环机制;

  3. _render 方法,用于生成 vnode ;

相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王1 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6414 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js