Vue(v2.0)全局API的实现原理
2020/4/8 11:01:40
本文主要是介绍Vue(v2.0)全局API的实现原理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
Vue.js最独特的特性之一是看起来并不显眼的响应式系统。数据模型仅仅是普通的javascript对象。而当你修改它时,视图会进行更新。这使得状态管理非常简单、直接。不过理解其工作原理同样重要,这样你可以回避一些常见的问题。 -----官方文档
vue.js无疑是前端目前最火的MVVM框架之一,就像曾经的jQuery,已经成为前端工程师必备的技能。而要在工作中用好它,如官方文档所言,理解其工作原理很重要。笔者于清明小长假期间认真阅读了一下刘博文老师的深入剖析Vue.js源码,在这里与大家一起学习分享一下vue(2.0版本)全局API的实现原理。
进入正文之前,我们先来看下vue.js内部的一段代码:
import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' 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) renderMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) export default Vue 复制代码
其中定义了Vue构造函数,然后分别调用了initMixin、stateMixin、eventsMixin、lifecycleMixin和renderMixin这5个函数,并将Vue构造函数当作参数传给了这5个函数。
这5个函数的作用就是向Vue的原型中挂载方法。以函数initMixin为例,它的实现方式是这样的:
export function initMixin (Vue) { Vue.prototype._init = function (options) { // 初始化操作--生命周期的流程、响应式系统流程的启动 } } 复制代码
其他4个函数也是如此,只是它们在Vue构造函数的prototype属性上挂载不同的方法而已
下面进入这次的正题,一起来学习Vue全局API的实现源码:
1. 数据相关的实例方法
与数据相关的实例方法有3个,分别是vm.set、vm.$delete,它们是在stateMixin中挂载到Vue的原型上的。
import { set, del } from '../observer/index' /** * target不能是Vue.js实例或Vue.js实例的根数据对象 * * @param {object | array} target * @param {String | Number} key * @param {*} value */ export function stateMixin (Vue) { Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function (expOrFn, cb, options) {} } 复制代码
(1)vm.$set
如果了解vue.js的变化侦测原理,我们就知道vue只能追踪到已经存在的属性的变化,新增的属性无法被追踪到。因为在ES6之前,Javascript并没有提供元编程的能力,无法侦测Object什么时候被添加了一个新属性。而用vm.$set可以解决这个问题,使用它可以为Object新增属性,然后vue可以将这个新增属性转换成响应式的。
set实现代码:
// oberver/index function set (target, key, val) { // target是数组且key是一个有效的索引值 先设置length属性 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 当我们使用splice方法把val设置到target中的时候 // 数组拦截器会侦测到target发生了变化 // 并且会自动把这个新增的val转换成响应式的 target.splice(key, 1, val) return val } // 如果key已经存在于target中,这种情况属于修改数据 // 修改数据的动作会被Vue.js侦测到 // 数据发生变化后 会自动向依赖发送通知 if (key in target && (!key in Object.prototype)) { target[key] = val return val } // 处理在target上新增的key const ob = target.__ob__ // target._isVue判断target是否为Vue实例 // ob.vmCount判断target是否为根数据(this.$data) if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } // 如果target没有__ob__属性 说明他不是响应式的 不需要做特殊处理 // 只需要通过key和val在target上设置 if (!ob) { target[key] = val return val } // target是响应式的 使用defineReactive将新增属性转换成getter/setter的形式 defineReactive(ob.value, key, val) // 向target的依赖触发变化通知 ob.dep.notify() return val } export.set = set 复制代码
(2) vm.$delete
从字面意思我们可以知道这个API的作用就是用来删除数据的某个属性的。我们知道vue的变化侦测是使用Object.defineProperty实现的,如果数据是使用delete关键字删除的,那么无法发现数据发生了变化。vm.$delete就是用来解决这个问题的。
del实现代码:
// observer/index function del (target, key) { // target为数组 if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = target.__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting reactive properties to a Vue instance or its root $data ' + '- just set it to null.' ) return } // 如果key不是target自身属性 直接return if (!hasOwn(target, key)) { return } // 从target中删除key属性 delete target[key] // 如果target不是响应式的 直接return if (!ob) { return } // 向依赖发送消息 ob.dep.notify() } 复制代码
(3)vm.$watch
vm.$watch其实是对Watcher的一种封装,Watcher的原理实现这里就不多多了,如果想了解有兴趣的话,可以去查找下相关资料。
(推荐:blog.csdn.net/wangweiange…)
实现代码:
Vue.prototype.$watch = function (expOrFn, cb, options) { const vm = this options = options || {} // watch依赖收集的Watcher const watcher = new Watcher(vm, expOrFn, cb, options) // immediate=true时 会调用一次 watcher.run 方法,因此会调用一次watch中相关key的函数 if (options.immediate) { cb.call(vm, watcher.value) } // 返回一个取消监听的函数 return function unwatchFn () { watcher.teardown() } } 复制代码
2. 与事件相关的实例方法
与事件相关的实例方法有4个,分别是:vm.once、vm.emit。这4个方法是在eventsMixin中挂载到vue构造函数的prototype属性中的。
export function eventsMixin (Vue) { // 监听当前实例上的自定义事件 事件由vm.$emit触发 Vue.prototype.$on = function (event, fn) { const vm = this // event是一个数组时 遍历数组 递归调用vm.$on if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { this.$on(event[i], fn) } } else { // vm._events是一个对象 用来存储事件 (vm._events[event] || vm._events[event] = []).push(fn) } return vm } // 移除自定义事件监听器 // 如果没有提供参数 则移除所有的事件监听器 // 如果只提供了事件 则移除该事件所有的监听器 // 如果同时提供了事件与回调 则只移除这个回调的监听器 Vue.prototype.$off = function (event, fn) { const vm = this // 移除所有事件的监听器 if (!arguments.length) { vm._events = Object.create(null) return vm } // event是数组 if (Array.isArray(event)) { for (let i = 0, l = event.length;i < l; i++) { this.$off(event[i], fn) } return vm } const cbs = vm._events[event] // 如果这个事件没有被监听 直接return if (!cbs) { return vm } // 移除该事件的所有监听器 if (arguments.length === 1) { vm_events[event] = null return vm } // 如果同时提供了事件与回调 只移除这个回调的监听器 if (fn) { const cbs = vm._events[event] let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) } } } return vm } // 监听一个自定义事件 但是只触发一次 第一次触发之后移除监听器 Vue.prototype.$once = function (event, fn) { const vm = this function on () { vm.$off(event, fn) fn.apply(vm, arguments) } on.fn = fn vm.$on(event, on) } // 触发当前实例上的事件 Vue.prototype.$emit = function (event) { const vm = this let cbs = vm._events[event] if (cbs) { const args = toArray(arguments, 1) for (let i = 0, l = cbs.length; i < l; i++) { try { cbs[i].apply(vm, args) } catch (e) { handleError(e, vm, `event handler for "${event}"`) } } } return vm } } 复制代码
3. 与生命周期相关的实例方法
与生命周期相关的实例方法有4个:vm.forceUpdate, vm.destory
vm.mount是在跨平台的代码中挂载到Vue构造函数的prototype属性上的
vm.destory是从lifecycleMixin中挂载的
vm.$nextTick是从renderMixin中挂载的
(1)vm.$forceUpdate
vm.$forceUpdate的作用是迫使vue.js实例重新渲染,它只影响实例本身以及插入插槽内容的子组件,而不是所有子组件。
代码实现:
Vue.prototype.$forceUpdate = function () { const vm = this if (vm._watcher) { vm._watcher.update() } } 复制代码
(2)vm.$destory
vm.$destory的作用是完全销毁一个实例,它会清理该实例与其他实例的连接,并解绑其全部指令及监听器,同时会触发 beforeDestory和destoryed的钩子函数
代码实现:
Vue.prototype.$destory = function () { const vm = this // 对属性_isBeingDestory进行判断 如果为 true vuejs实例正在被销毁 直接return 防止反复销毁 if (vm._isBeingDestory) { return } callHook(vm, beforeDestory) vm._isBeingDestoryed = true // 删除自己与父级之间的连接 const parent = vm.$parent // 如果当前实例有父级 同时父级没有被销毁且不是抽象组件 if (parent && !parent._isBeingDestoryed && vm.$options.abstract) { remove(parent.$children, vm) } // 销毁实例上的所有watcher // 从watcher监听的所有状态 if (vm._watcher) { vm._watcher.teardown() } // 每当创建watchers实例时 都会将watcher实例添加到 vm._watchers中 let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } vm._isDestoryed = true // 在 vnode树上触发destory钩子函数解绑指令 vm.__patch__(vm._vnode, null) // 触发destoryed钩子函数 callHook(vm, 'destoryed') // 移除所有的事件监听器 vm.$off() } 复制代码
(3) vm.$nextTick()
nextTick接收一个回调函数作为参数 它的作用是将回调延迟到下次DOM更新周期之后执行。
我们在开发项目时会遇到一种场景:当更新了状态(数据)后,需要对新DOM做一些操作,但是这是我们其实获取不到更新后的DOM,因为还没有重新渲染。这个时候我们需要使用nextTick方法
示例如下:
new Vue({ // ...... methods: { // ...... example: function () { // 修改数据 this.message = 'changed' // DOM还没有更新 this.$nextTick(function () { // DOM 现在更新了 // this绑定到当前实例 this.doSomethindElse() }) } } }) 复制代码
要理解nextTick,我们需要知道,在Vue.js中,当状态发生变化时,watcher会得到通知,然后触发虚拟DOM的渲染流程。而watcher触发渲染是异步的。Vue.js中有一个队列,每当需要渲染时,会将watcher推送到这个队列中,在下一次事件循环中再让watcher触发渲染的流程。
为什么vue.js使用异步更新队列
我们知道vue.js的变化侦测的通知只发送到组件,组件内用到的所有状态的变化都会通知到同一个watcher,所以为了避免多个状态变化,多次渲染,虚拟DOM会等所有状态都修改完毕之后,一次性将整个组件的DOM渲染到最新。
Vue.js实现的方式是将收到通知的watcher实例添加到队列中缓存起来,并且在添加到队列之前检查其中是否已经存在相同的watcher,只有不存在时,才将 watcher实例添加到队列中。然后在下一次事件循环中,Vue.js会让队列中的 watcher触发渲染流程并清空队列。
事件循环
我们都知道Javascript是一门单线程且非阻塞的脚本语言,这意味着Javascript代码在执行的任何时候只有一个主线程来处理所有任务。而非阻塞是指当代码需要处理异步任务时,主线程会挂起这个任务,当异步任务处理完毕后,主线程再根据一定规则去执行相应回调。
其实,当任务处理完毕后,Javascript会将这个事件加入一个队列中,即事件队列。被放入事件队列中的事件不会立刻执行其回调,而是等待当前执行栈中的所有任务执行完毕后,主线程会去查找事件队列中是否有任务。
异步任务有两种类型:微任务(microtask)和宏任务(macrotask)。不同类型的任务被分配到不同的任务队列中。
当执行栈中的所有任务都执行完毕后,会去检查微任务队列中是否有事件存在,如果存在,则会依次执行微任务队列中事件对应的回调,直到为空。然后去宏任务队列中取出一个事件,把对应的回调加入当前执行栈,当执行栈中的所有任务都执行完毕后,检查微任务队列中是否有事件存在。无限重复此过程,就形成了一个循环,即事件循环。
常见微任务的事件
Promise.then MutationObserver Object.observe process.nextTick 复制代码
常见宏任务的事件
setTimeout setInterval setImmediate MessageChannel requestAnimationFrame 复制代码
讲了这么多相关知识,我们回到nextTick的实现原理上来。vm.nextTick方法中,而是抽象成了nextTick方法供两个方法共用。
代码如下:
import { nextTick } from '../util/index' Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this) } 复制代码
由于vm.$nextTick会将回调添加到任务队列中延迟执行,所以在回调执行前,如果反复调用nextTick,Vue.js并不会反复将回调添加到任务队列中,只会向任务队列中添加一个任务,多次使用nextTick只会将回调添加到回调列表中缓存起来。当任务触发时,依次执行列表中的所有回调并清空列表。
const callbacks = [] let pending = false // 注册的任务 function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } let microTimerFunc const p = Promise.resolve() // 将flushCallbacks添加到微任务队列中 microTimerFunc = () => { p.then(flushCallbacks) } export function nextTick (cb, ctx) { callbacks.push(() => { if (cb) { cb.call(ctx) } }) // 通过pending判断是否需要向任务队列中添加任务 if (!pending) { pending = true microTimerFunc() } } 复制代码
下图给出nextTic的内部注册流程和执行流程:
在vue.js2.4版本之前,nextTick在任何地方都使用微任务,但是微任务的优先级太高,在某些场景下可能会出现问题,所以Vue.js提供了在特殊场合下可以强制使用宏任务的方法。具体实现如下:// 在上面代码基础上新增 // ...... let useMacroTask = false // ...... export function withMacroTask (fn) { return fn._withTask || (fn._withTask = function () { useMacroTask = true const res = fn.apply(null, arguments) useMacroTask = false return false }) } export function nextTick(cb, ctx) { // ...... if (!pending) { pending = true if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } } 复制代码
withMacroTask函数的作用是给回调函数做一层包装,保证在整个回调函数执行过程中,如果修改可状态(数据),那么更新DOM的操作会被推到宏任务队列中。
下面我们来看看macroTimerFunc是如何将回调添加到宏任务队列中的。
前面我们介绍过几种属于宏任务的事件。Vue.js优先使用setImmediate,但是它存在兼容问题,只能在IE中使用,所以使用MessageChannel作为备选方案,如果浏览器也不支持MessageChannel,那么最好会使用setTimeout将回调添加到宏任务队列中。
macroTimerFunc实现代码:
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MeaasgeChannel) || MessageChannel.toString() === '[object MessageChannelConstructor]' )) { const channel = new MessageChannel() const port = channel.port2 channle.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } } 复制代码
前面提到microTimerFunc的实现原理是使用Promise.then,但并不是所有浏览器都支持Poemise,当不支持时,会降级成macroTimerFunc,其实现方式如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() // 将flushCallbacks添加到微任务队列中 microTimerFunc = () => { p.then(flushCallbacks) } } else { microTimerFunc = macroTimerFunc } 复制代码
官方文档中有这样一句话:如果没有提供回调且在支持Promise的环境中,则返回一个Promise。也就是说可以这样使用vm.$nextTick:
this.$nextTick().then(function () { // DOM更新了 }) 复制代码
要实现这个功能,我们只需要在nextTick中进行判断,如果没有提供回调且当前环境支持Promise,那么返回Promise,并且在callbacks中添加一个函数,当这个函数执行时,执行Promise的resolve即可,代码如下:
export function nextTick (cb, ctx) { // ...... let _resolve callbacks.push(() => { if (cb) { cb.call(ctx) } else if (_resolve) { _resolve(ctx) } }) // ...... if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } 复制代码
(4) vm.$mount()
一个Vue.js实例在实例化时如果没有收到el选项,则它处于"未挂载"状态,没有关联的DOM元素。我们可以使用vm.$mount手动挂载一个未挂载的实例。如果没有提供参数,模板会被渲染为文档之外的元素,必须使用原生DOM的API把它插入到文档中。这个方法返回实例自身,因而可以链式调用其他实例方法。
4. 常见全局API的实现原理
全局API和实例方法不同,后者是在 Vue的原型上挂载方法,前者是直接在Vue上挂载方法
(1)Vue.extend
Vue.extend用来创建一个子类,让它继承Vue身上的一些功能,实现代码如下:
let cid = 1 Vue.extend = function (extendOptions) { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production') { // 校验name if (!/^[a-zA-Z][\w-]*$)/.test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumberic characters and the hyphen, ' + 'and must start with a letter.' ) } } const Sub = function VueComponent (options) { this._init(options) } // 将父类的原型继承到子类中 Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ // 将父类的options选项继承到子类中 Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super // 如果选项中存在props属性 则初始化 if (Sub.options.props) { initProps(Sub) } // 如果选项中存在computed 则对它进行初始化 if (Sub.options.computed) { initComputed(Sub) } // 将父类中存在的属性依次复制到子类中 Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // ASSET_TYPES = ['component', 'directive', 'filter'] ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) if (name) { Sub.options.components[name] = Sub } // 缓存构造函数 cachedCtors[SuperId] = Sub return Sub } 复制代码
初始化props是将key代理到_props中。例如vm.name实际上访问的是Sub.prototype._props.name。实现原理如下:
function initProps (Comp) { const props = Comp.options.props for (const key in props) { proxy(Comp.prototype, `_props`, key) } } const sharedPropertyDefinition = { enumberable: true, configurable: true, get: noop, set: noop } function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) } 复制代码
初始化computed只是将computed对象遍历一遍,并将里面的每一项都定义一遍
function initComputed (Comp) { const computed = Comp.options.computed for (const key in computed) { defineComputed(Comp.prototype, key, computed[key]) } } 复制代码
(2). Vue.nextTick
Vue.nextTick的实现原理与我们前面介绍的vm.$nextTick一样,代码如下:
import { nextTick } from '../util/index' Vue.nextTick = nextTick 复制代码
(3). Vue.set
Vue.set与vm.$set的实现原理相同,代码如下:
import { set } from '../observer/index' Vue.set = set 复制代码
(4). Vue.delete
Vue.delete与vm.$delete的实现原理相同,代码如下:
import { del } from '../observer/index' Vue.delete = del 复制代码
(5). Vue.directive
注册或获取全局指令,代码如下:
// 用于保存指令的位置 Vue.options = Object.create(null) Vue.options['directives'] = Object.create(null) Vue.directive = function (id, definition) { // 如果definition参数不存在 则使用id从this.options['direactives']中读出指令并返回 if (!definition) { return this.options['directives'][id] } else {// definition参数存在 // 如果definition是函数 则默认监听bind和update两个方法 // 将definition分别赋值给对象中的bind和uodate两个方法 if (typeof definition === 'function') { definition = { bind: definition, update: definition } } this.options['directives'][id] = definition return definition } } 复制代码
(6) Vue.filter
注册或获取全局过滤器,代码如下:
Vue.options['filter'] = Object.create(null) Vue.filter = function (id, definition) { // 如果definition参数不存在 则使用id从this.options['filter']中读出指令并返回 if (!definition) { return this.options['filter'][id] } else { this.options['filter'][id] = definition return definition } } 复制代码
(7) Vue.component
注册或获取全局组件。注册组件时,会自动使用给定的id设置组件的名称,使用方法如下:
// 注册组件 传入一个扩展过的构造器 Vue.component('my-component', Vue.extend({/* ... */})) // 注册组件 传入一个选项对象(自动调用Vue.extend) Vue.component('my-component', {/* ... */}) // 获取注册的组件(返回构造器) let MyComponent = Vue.component('my-component') 复制代码
Vue.component实现代码:
Vue.options['components'] = Object.create(null) Vue.component = function (id, definition) { if (!definition) { return this.options['components'][id] } else { if (isPlainObject(definition)) { definition.name = definition.name || id definition = Vue.extend(definition) } this.options['component'][id] = definition return definition } } 复制代码
(7) Vue.use
Vue.use是用来安装Vue.js插件的。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。
实现代码如下:
Vue.use = function (plugin) { const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) // 判断插件是否被注册过 if (installedPlugins.indexOf(plugin) > -1) { return this } // 其他参数 const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, plugin) } installedPlugins.push(plugin) return this } 复制代码
(8) Vue.mixin
全局注册一个混入(mixin),影响注册之后创建的每个Vue.js实例。可以使用混入向组件注入自定义行为(例如:监听声明周期钩子)。实现代码如下:
import { mergeOptions } from '../util/index' export function initMixin (Vue) { Vue.mixin = function (mixin) { // 将传入的对象与Vue.js自身的options属性合并在一起 this.options = mergeOptions(this.options, mixin) return this } } 复制代码
好了,清明小长假的学习分享就先到这了
这篇关于Vue(v2.0)全局API的实现原理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南