vue 源码解析(实例化前) - 初始化全局 API(最终章)
栏目: JavaScript · 发布时间: 5年前
内容简介:上一章的最后,总结了这篇文章,会涉及到组件更新的实现:
上一章的最后,总结了 Watcher
的实现,对于 vue
实例化前要做的事情,在这一章,就要终结了,所以这一篇,也就是 vue
实例化前的最终章。
这篇文章,会涉及到 vue
一些事件的实现:
$on
、
$once
、
$off
、
$emit
;
组件更新的实现:
updated
、
$forceUpdate
、
$destroy
;
渲染 dom
的实现:
$nextTick
、
render
。
实例方法 / 事件
eventsMixin(Vue); 复制代码
在该函数里面,,就是 $on
、 $once
、 $off
、 $emit
的实现,只是在这几个方法实现的前面,有一个正则:
var hookRE = /^hook:/; 复制代码
用来判断是否是以 hook:
开头的事件。
$on
对于
$on
的实现,其实就是一个发布订阅关系中,一个充当订阅的角色,和 $emit
是配合使用:
Vue.prototype.$on = function (event, fn) { var vm = this; if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn); } } else { (vm._events[event] || (vm._events[event] = [])).push(fn); if (hookRE.test(event)) { vm._hasHookEvent = true; } } return vm }; 复制代码
在 $on
当中,一开始的时候会保存当前的 this
指针,然后检查在调用 $on
方法时候,接收到的 event
参数是否是数组,如果是数组,就循环调用 $on
方法,一直到发现 event
不是数组为止;
然后检查 vue
的构造函数下的 _events
对象是否存在当前的事件,不存在就创建一个数组,存在的话,把订阅的回调 fn
添加到 _events
的当前事件属性的数组当中;
检查当前的事件是否是以 hook:
开头的事件,如果是的话,就设置当前 vue
的 _hasHookEvent
的状态为 true
。
$once
$once
就是用来监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。
Vue.prototype.$once = function (event, fn) { var vm = this; function on() { vm.$off(event, on); fn.apply(vm, arguments); } on.fn = fn; vm.$on(event, on); return vm }; 复制代码
这里就比较简单了, $once
接收到的参数,和 $on
一样,其实在 $once
当中,直接绑定的也是 $on
方法;
在发布订阅的时候,直接执行的是在 $once
内部的 on
方法;
在 on
方法中,调用 $off
移除了事件监听器;
最后把 $once
接收到的回调函数 fn
的 this
指向 vue
构造函数,把在 $on
接收到的参数,传给 $once
的回调函数。
$off
$off
用来移除自定义事件监听器。
Vue.prototype.$off = function (event, fn) { var vm = this; // all if (!arguments.length) { vm._events = Object.create(null); return vm } // array of events if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn); } return vm } // specific event var cbs = vm._events[event]; if (!cbs) { return vm } if (!fn) { vm._events[event] = null; return vm } if (fn) { var cb; var i = cbs.length; while (i--) { cb = cbs[i]; if (cb === fn || cb.fn === fn) { cbs.splice(i, 1); break } } } return vm }; 复制代码
当 $off
不接收任何参数的时候,代表要把 vue
构造函数内的所有事件监听器全部卸载;
如果接收到的 event
是数组,那就循环调用 $off
去分别卸载每一个数组内的事件监听器;
如果当前的事件不存在,就直接返回;
如果不存在回调函数的话,直接把当前事件给移除;
如果存在回调的话,检查当前的订阅数组,删除当前回调函数,并退出。
$emit
Vue.prototype.$emit = function (event) { var vm = this; { var lowerCaseEvent = event.toLowerCase(); if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { tip( "Event \"" + lowerCaseEvent + "\" is emitted in component " + (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " + "Note that HTML attributes are case-insensitive and you cannot use " + "v-on to listen to camelCase events when using in-DOM templates. " + "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"." ); } } var cbs = vm._events[event]; if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs; var args = toArray(arguments, 1); for (var 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 }; 复制代码
在发布订阅的时候,要检查当前发布事件的命名问题;
如果当前的要发布的事件,存在回调,就依次发布事件到订阅的事件里面。
知识点:可以通过源码发现,上面的所有事件,都支持链式调用
组件更新的实现
updated
_update
用来更新组件信息
Vue.prototype._update = function (vnode, hydrating) { var vm = this; var prevEl = vm.$el; var prevVnode = vm._vnode; var restoreActiveInstance = setActiveInstance(vm); vm._vnode = vnode; if (!prevVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false); } else { vm.$el = vm.__patch__(prevVnode, vnode); } restoreActiveInstance(); if (prevEl) { prevEl.__vue__ = null; } if (vm.$el) { vm.$el.__vue__ = vm; } if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el; } }; 复制代码
把当前的 $el
和 _vnode
保存起来;
调用 restoreActiveInstance
返回一个结果,保存到 restoreActiveInstance
变量当中;
var activeInstance = null; function setActiveInstance(vm) { var prevActiveInstance = activeInstance; activeInstance = vm; return function () { activeInstance = prevActiveInstance; } } 复制代码
实现的就是在更新的时候,使用接收到的 vue
实例,使用完毕后调用 return
回去的函数,替换回原来的实例对象;
检查当前的 vNode
是否被渲染,如果没渲染过,就初始化渲染,否则就做更新;
执行 restoreActiveInstance
把 activeInstance
换成原来的值;
其实就是更新完 node
后,把 activeInstance
置空。
如果存在渲染节点,那么就给当前的 vm.$el
添加一个 __vue__
属性,默认值为 null
;
__vue__
指向更新时接收到的 vue
实例;
如果当前实例的父级 $parent
是 HOC,那么也更新其 $el
$forceUpdate
$forceUpdate
迫使 Vue 实例重新渲染。
注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
Vue.prototype.$forceUpdate = function () { var vm = this; if (vm._watcher) { vm._watcher.update(); } }; 复制代码
这里比较简单了,就是一个更新当前实例的监听, watcher
的实现,在上一章写过,入口: Vue 源码解析(实例化前) - 初始化全局API(三)
,这里介绍了 watcher
所有的实现。
$destroy
$destroy
完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
触发 beforeDestroy
和 destroyed
的钩子。
Vue.prototype.$destroy = function () { var vm = this; if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy'); vm._isBeingDestroyed = true; var parent = vm.$parent; if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm); } if (vm._watcher) { vm._watcher.teardown(); } var i = vm._watchers.length; while (i--) { vm._watchers[i].teardown(); } if (vm._data.__ob__) { vm._data.__ob__.vmCount--; } vm._isDestroyed = true; vm.__patch__(vm._vnode, null); callHook(vm, 'destroyed'); vm.$off(); if (vm.$el) { vm.$el.__vue__ = null; } if (vm.$vnode) { vm.$vnode.parent = null; } }; 复制代码
检查当前的 vue
实例是否正在卸载;
注册一个 beforeDestroy
钩子:
function callHook(vm, hook) { pushTarget(); var handlers = vm.$options[hook]; if (handlers) { for (var i = 0, j = handlers.length; i < j; i++) { try { handlers[i].call(vm); } catch (e) { handleError(e, vm, (hook + " hook")); } } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook); } popTarget(); } 复制代码
检查当前的实例化对象中,有没有当前的 hook
钩子,如果在实例化 Vue
构造函数的时候,配置属性里面没有当前钩子,就跳过;如果有的话,执行。
执行完 beforeDestroy
后,开始从当前实例化对象的父级去移除当前对象;
卸载当前实例上的 watcher
对象;
从数据对象中移除引用冻结对象可能没有观察者;
在当前渲染树上调用销毁钩子;
执行 destroyed
钩子;
卸载所有的事件监听器;
把和当前有关系的一些属性,全设为 null
。
组件渲染
这里最主要做的就是有关组件渲染的方法, $nextTick
和 组件的 render
钩子。
$nextTick
将回调延迟到下次 DOM
更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM
更新。它跟全局方法 Vue.nextTick
一样,不同的是回调的 this
自动绑定到调用它的实例上。
2.1.0 起新增:如果没有提供回调且在支持 Promise
的环境中,则返回一个 Promise
。请注意 Vue
不自带 Promise
的 polyfill
,所以如果你的目标浏览器不是原生支持 Promise
(IE:你们都看我干嘛),你得自行 polyfill
。
Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this) }; 复制代码
nextTick
在之前 Vue 源码解析(实例化前) - 初始化全局API(二)
章节中,做过讲解,不了解的大家可以过去看一下。
render
Vue.prototype._render = function () { var vm = this; var ref = vm.$options; var render = ref.render; var _parentVnode = ref._parentVnode; if (_parentVnode) { vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject; } vm.$vnode = _parentVnode; var vnode; try { vnode = render.call(vm._renderProxy, vm.$createElement); } catch (e) { handleError(e, vm, "render"); if (vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e); } catch (e) { handleError(e, vm, "renderError"); vnode = vm._vnode; } } else { vnode = vm._vnode; } } if (!(vnode instanceof VNode)) { if (Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ); } vnode = createEmptyVNode(); } // set parent vnode.parent = _parentVnode; return vnode }; 复制代码
检查配置的属性当中,是否存在 _parentVnode
属性,如果存在就把他的 data.scopedSlots
指向实例化对象的 $scopedSlots
;
点击查看
$scopedSlots
的使用
设置父 parent Vnode
,这允许渲染函数访问占位符节点上的数据;
把当前的 render
的 this
指向 vm._renderProxy
并把 vm.$createElement
当做参数传给 render
;
当然,在渲染的过程当中,如果报错,那么就返回错误呈现结果或以前的Vnode,以防止呈现错误导致空白组件。
结束语
这一篇,基本上会讲解的就是这部分内容,在这一篇文章写完后,会总结一篇 vue 生命周期方法的实现
的文章和一篇 vue 实例化前的源码汇总
,由于涉及的知识点太多,分开了很多章去写,理解和学习起 vue
的实现理念来,不是很方便,但是如果大家想了解作者到底是怎么实现的 vue
还是建议大家挨个文章就看一下。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Vue 源码解析(实例化前) - 初始化全局API(二)
- Vue 源码解析(实例化前) - 初始化全局API(三)
- C++ 的一大误区——深入解释直接初始化与复制初始化的区别
- 初始化监听端口
- 类初始化导致死锁
- nodejs源码—初始化
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。