vue源码学习
Posted colorful-coco
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue源码学习相关的知识,希望对你有一定的参考价值。
一、直接用 <script>
引入vue.js
直接下载并用 <script>
标签引入,Vue
会被注册为一个全局变量。
<div id="app"></div> <script src="vue.js"></script> <script> var vm = new Vue({ el: ‘#app‘, data: {}, components: {}, created: function() {}, methods: { xxx: function() { } } }) </script>
// vue.js (function (global, factory) { typeof exports === ‘object‘ && typeof module !== ‘undefined‘ ? module.exports = factory() : typeof define === ‘function‘ && define.amd ? define(factory) : (global = global || self, global.Vue = factory()); }(this, function () { ‘use strict‘; function Vue (options) { this._init(options); } return Vue; }));
在此处浏览器上下文中this即为window对象,此处是将Vue方法(构造函数)暴露为一个全局方法。
el:提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用 vm.$mount()
手动开启编译。如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount()
手动地挂载一个未挂载的实例。
Vue.prototype._init = function (options) { var vm = this; initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, ‘beforeCreate‘); initInjections(vm); // resolve injections before data/props initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, ‘created‘); if (vm.$options.el) { vm.$mount(vm.$options.el); } }; Vue.prototype.$mount = function (el, hydrating) {}
部分生命周期截图,参考:https://cn.vuejs.org/v2/guide/instance.html#生命周期图示
// public mount method // 2)mount.call调用该方法 Vue.prototype.$mount = function (el, hydrating) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) }; var mount = Vue.prototype.$mount; // 1)初始化先执行以下$mount挂载 Vue.prototype.$mount = function (el, hydrating) { el = el && query(el); return mount.call(this, el, hydrating) };
mountComponent
function mountComponent (vm, el, hydrating) { vm.$el = el; callHook(vm, ‘beforeMount‘); var updateComponent; if (config.performance && mark) { } else { updateComponent = function () { vm._update(vm._render(), hydrating); }; } new Watcher(vm, updateComponent, noop, { before: function before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, ‘beforeUpdate‘); } } }, true /* isRenderWatcher */); }
vm._render()
Vue 选项中的 render
函数若存在,则 Vue 构造函数不会从 template
选项或通过 el
选项指定的挂载元素中提取出的 html 模板编译渲染函数。
<div id="app"> <p>message: {{message}}</p> </div> <script src="vue.js"></script> <script> var vm = new Vue({ el: ‘#app‘, data: { message: ‘Hello Vue!‘, blogTitle: ‘render‘ }, render: function(createElement) { return createElement(‘h1‘, this.blogTitle) } }) </script>
vue.js设置了默认的render函数,如果Vue 选项中的 render
函数存在,则执行用户自定义的render函数。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。return createElement(‘h1‘, this.blogTitle)。createElement
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription
,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
参考:https://cn.vuejs.org/v2/guide/render-function.html#虚拟-DOM
Vue.prototype._render = function () { var vm = this; var ref = vm.$options; var render = ref.render; var vnode; vnode = render.call(vm._renderProxy, vm.$createElement); return vnode };
Vue.prototype._update = function (vnode, hydrating) { var vm = this; var prevVnode = vm._vnode; // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } else { // updates vm.$el = vm.__patch__(prevVnode, vnode); } };
patch操作dom渲染页面
Vue.prototype.__patch__ = inBrowser ? patch : noop; function patch (oldVnode, vnode, hydrating, removeOnly) { var isInitialPatch = false; var insertedVnodeQueue = []; // replacing existing element var oldElm = oldVnode.elm; var parentElm = nodeOps.parentNode(oldElm); // create new node createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ); // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0); } return vnode.elm } function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { var data = vnode.data; var children = vnode.children; var tag = vnode.tag; // 示例中tag为‘h1‘ if (isDef(tag)) { vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode); createChildren(vnode, children, insertedVnodeQueue); // 将h1的子节点元素插入h1中 insert(parentElm, vnode.elm, refElm); // 将h1插入父元素body中 } }
此例执行insert后页面渲染如下(底层是使用insertBefore这些原生js方法操作dom)
当使用新数组替换旧数组时(如下),vue处理流程分析
<div id="app"> <p v-for="item in arr">{{item.name}}</p> <button type="button" @click="testConcat">测试concat</button> </div> <script src="vue.js"></script> <script> var vm = new Vue({ el: ‘#app‘, data: { arr: [{name: ‘cl‘}] }, methods: { testConcat: function() { var newArr = [{name: ‘lalala‘}] this.arr = this.arr.concat(newArr) } } }) </script>
参考:https://cn.vuejs.org/v2/guide/list.html#数组更新检测
变异方法,顾名思义,会改变调用了这些方法的原始数组。相比之下,也有非变异 (non-mutating method) 方法,例如 filter()
、concat()
和 slice()
。它们不会改变原始数组,而总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组。你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
function defineReactive$$1 (obj, key, val, customSetter, shallow) { var dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, set: function reactiveSetter (newVal) { childOb = !shallow && observe(newVal); dep.notify(); } }); }
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
需要对this._data.arr操作才会触发set。为了偷懒,我们需要一种方便的方法通过this.arr直接设置就能触发set对视图进行重绘。那么就需要用到代理。这样我们就把data上面的属性代理到了vm实例上。
初始化时调用observe这个函数将Vue的数据设置成observable的
new Vue -> this._init(options); -> initState -> ... -> observe -> new Observer -> defineReactive$$1
function initState (vm) { if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } } function initData (vm) { var data = vm.$options.data; var keys = Object.keys(data); var i = keys.length; while (i--) { var key = keys[i]; proxy(vm, "_data", key); } observe(data, true /* asRootData */); } // 代理:把data上面的属性代理到了vm实例上 var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key]; // sourceKey即为_data }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; // sourceKey即为_data }; Object.defineProperty(target, key, sharedPropertyDefinition); } // 将Vue的数据设置成observable的 function observe (value, asRootData) { var ob; if (hasOwn(value, ‘__ob__‘) && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if (...) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob }
初始化渲染1:new Vue -> this._init(options); -> vm.$mount -> mountComponent
// 2、再执行该mount方法 Vue.prototype.$mount = function (el, hydrating) { return mountComponent(this, el, hydrating) }; var mount = Vue.prototype.$mount; // 1、先执行该mount方法 Vue.prototype.$mount = function (el, hydrating) { return mount.call(this, el, hydrating) };
初始化渲染2:mountComponent -> new Watcher -> Watcher.prototype.get -> vm._update(vm._render(), hydrating)
function mountComponent (vm, el, hydrating) { vm.$el = el; callHook(vm, ‘beforeMount‘); var updateComponent; updateComponent = function () { vm._update(vm._render(), hydrating); }; new Watcher(vm, updateComponent, noop, { before: function before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, ‘beforeUpdate‘); } } }, true /* isRenderWatcher */); if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, ‘mounted‘); } return vm } var Watcher = function Watcher (vm, expOrFn, cb, options, isRenderWatcher) { if (typeof expOrFn === ‘function‘) { this.getter = expOrFn; } this.get(); }; Watcher.prototype.get = function get () { var value; var vm = this.vm; value = this.getter.call(vm, vm); return value };
this.arr = this.arr.concat(newArr) -> dep.notify() -> Dep.prototype.notify -> Watcher.prototype.update -> Watcher.prototype.run -> Watcher.prototype.get -> vm._update(vm._render(), hydrating)
【Dep就是一个发布者,可以订阅多个观察者,依赖收集之后Deps中会存在一个或多个Watcher对象,在数据变更的时候通知所有的Watcher。】
Vue.prototype._update = function (vnode, hydrating) { var vm = this; var prevVnode = vm._vnode; if (!prevVnode) { // 初始化 // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */); } else { // 更新 // updates vm.$el = vm.__patch__(prevVnode, vnode); } };
patch渲染页面
return function patch (oldVnode, vnode, hydrating, removeOnly) { var insertedVnodeQueue = []; if (isUndef(oldVnode)) { } else { var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) { // 更新 // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); } else { // 初始化 if (isRealElement) { oldVnode = emptyNodeAt(oldVnode); } // replacing existing element var oldElm = oldVnode.elm; var parentElm = nodeOps.parentNode(oldElm); // create new node createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ); // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0); } } } return vnode.elm }
patchVnode更新
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) { var elm = vnode.elm = oldVnode.elm; var data = vnode.data; var oldCh = oldVnode.children; var ch = vnode.children; if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); } } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); } } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text); } } function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { var oldStartIdx = 0; var newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx, idxInOld, vnodeToMove, refElm; // 比较结点 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx); canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); } else { vnodeToMove = oldCh[idxInOld]; if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); oldCh[idxInOld] = undefined; canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm); } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); } } newStartVnode = newCh[++newStartIdx]; } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
以上是关于vue源码学习的主要内容,如果未能解决你的问题,请参考以下文章