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 ;

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me7 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架