做为一个vue爱好学习者,也加入了源码解读的学习阵营,对一个vue和react框架都用过的前端妹子来讲,仍是更喜欢写vue的语法,如今也很主流,一直想研究一下vue框架背后的实现机制,对api掌握、数据驱动、数据更新、以及组件等有个更全面的认识、而不只仅局限于会用它,如今就当作记录一下本身的理解,会持续更新~html
其实就是一个用Function实现的Class,经过它的原型prototype以及它自己扩展的一系列的方法和属性,因此通常咱们会在main.js中会先new Vue一个实例对象出来,不然会报错warn('Vue is a constructor and should be called with the new
keyword')前端
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
所谓的数据驱动,是指视图是由数据驱动生成的,对视图的修改,再也不直接操做DOM,而是经过修改数据。咱们所关心的只是数据的修改,DOM变成了数据的映射。vue
合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watchernode
作了一层initState()的方法,给设置了data属性,会执行getData()方法,这里会先对data进行判断,是否是一个function,代码以下react
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } function initData (vm: Component) { let data = vm.$options.data // 这里判断data是否是一个function data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} // 会报错给咱们咱们data未初始换成一个对象的错误 process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) }
注意:在循环遍历对象属性时,会对props和data进行一层判断,两者不能重名,由于最后都会挂载到vm对象上,而后对vm对象进行一层proxy代理,下面的代码很重要api
// proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { //会报props和data重名同样的警告 process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { // 进行数据代理操做 proxy(vm, `_data`, key) } } // 将vm对象用_data进行代理,收集和触发更新依赖 proxy(vm, `_data`, key) export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) }
这里的proxy,经过Object.defineProtery能够作到给原型去作代理,get()方法收集依赖、set()方法去触发更新,因此好比在mounted()时,例如打印一个console.log(this.messags)和console.log(this._data.message)是同样的结果,实际上访问的就是vm._data.messageapp
接着el设置了以后,进行mount函数处理,即mount钩子函数框架
if (vm.$options.el) { vm.$mount(vm.$options.el) }
Vue不能挂载到body或html这样的根节点上,通常都用div嵌套包括起来,会被覆盖,Vue2.0版本中,全部的vue组件渲染最终都须要rendr方法,不论写的是el或者template属性,最终都会转换陈render方法,即"在线编译的过程"dom
// 原型上添加$mount方法 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) /* istanbul ignore if */ // 若el挂载到body或者html上会报以下警告 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function // 若是是已经render()的话,没必要再compile() if (!options.render) { let template = options.template if (template) { ..... } } // 若是是template模板,须要进行compile解析 if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } } // 最后会建立DOM元素,在这里内容进行覆盖,这也是为何外层通常要有一个父级div包裹它,而不是写在body或html上,实际上template会走一个compileToFunctions的过程 function getOuterHTML (el: Element): string { if (el.outerHTML) { return el.outerHTML } else { const container = document.createElement('div') container.appendChild(el.cloneNode(true)) return container.innerHTML } } Vue.compile = compileToFunctions
_render():Vue实例的一个私有方法,它用来把实例渲染成一个虚拟Node,用一个原生的JS对象去描述一个DOM节点,会比建立一个DOM的代价要小不少,这里和react的思想是同样的async
onstructor ( tag?: string, // vNode的标签,例如div、p等标签 data?: VNodeData, // vNode上的的data值,包括其全部的class、attribute属性、style属性已经绑定的时间 children?: ?Array<VNode>, // vNode上的子节点 text?: string, // 文本 elm?: Node, // vNode上对应的真实dom元素 context?: Component, //vdom的上下文 componentOptions?: VNodeComponentOptions ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.functionalContext = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }
上面是VNode的初始化,而后Vue它是经过createElement方法建立的VNode
export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { // 注意:这里会先进行一层判断,进行属性值前移,该方法能够借鉴在实际项目中 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } // _createElement()是它的私有方法,建立成一个VNode,每一个 VNode 有 children,children 每一个元素也是一个 VNode,这样就造成了一个 VNode Tree return _createElement(context, tag, data, children, normalizationType) }
目的是为了把vNode转换为真实的DOM,_update会再首次渲染和数据更新的时候去调用,核心方法实际上是其中的_patch()方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = vm // 建立一个新的vNode vm._vnode = 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 // 和以前的vNode,进行diff,将须要更新的dom操做和已经patch的vNode大道须要更新的vNode,完成真实的dom操做 vm.$el = vm.__patch__(prevVnode, vnode) } activeInstance = prevActiveInstance // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }
看一下_patch里面作了什么
// 定义了生命周期,这些钩子函数 const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } // ... // oldVnode:旧的VNode节点or DOM对象 // vnode: 执行了_render()以后范湖的VNode的节点 // hydrating:是不是服务端渲染,由于patch是和平台相关的,在Web和Weex环境下,把VNode映射到平台DOM的方法也是不一样(有它本身的nodeOps和modules) // removeOnly: 给transition-group用的 return function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true // 建立新的节点 createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node // oldVNode和vnode进行diff,并对oldVnode打patch patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node // createElm的做用:经过传入的VNode去建立真是的DOM元素,并插图到它的父节点中, createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { ... } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } //执行全部created的钩子并把vnodepush到insertedVnodeQueue 中 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }
其中对oldVNode和vnode类型判断中有一个sameVnode方法,这个方法很重要,是oldVNode和vnode须要进行diff和patch的前提
function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }
注意:insert()方法把DOM插入到父节点中,进行了递归调用,子元素会优先调用 insert,因此整个 vnode 树节点的插入顺序是先子后父
insert(parentElm, vnode.elm, refElm) function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (ref.parentNode === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } } export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { parentNode.insertBefore(newNode, referenceNode) } export function appendChild (node: Node, child: Node) { node.appendChild(child) }
因此在patch的过程当中,会有这个问题抛出来
if (isDef(tag)) { if (process.env.NODE_ENV !== 'production') { if (data && data.pre) { creatingElmInVPre++ } // 忘记注册组件的时候,会常常遇到以下报错,这个刚开始的时候遇到的状况不少 if (isUnknownElement(vnode, creatingElmInVPre)) { warn( 'Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.', vnode.context ) } } ..... }
能够看到最终返回的是一个patch()方法,赋值给vm.__patch__()方法
在createElm过程当中,能够看到若是vnode节点不包含tag的话,它有多是一个注释或者纯文本节点,能够直接插入到父元素中,递归建立一个完整的DOM并插入到body中。
对数据渲染的过程有了更深的一层理解,从new Vue()开始,建立了一个vue是对象,会先进行init初始化——>$mount()——>compile(若已是render则该过程不须要)——>render——>建立VNode——>patch过程——>生成真实的DOM