vue数据驱动你还不了解吗?看这里全知道 您所在的位置:网站首页 数据驱动的实现途径有 vue数据驱动你还不了解吗?看这里全知道

vue数据驱动你还不了解吗?看这里全知道

#vue数据驱动你还不了解吗?看这里全知道| 来源: 网络整理| 查看: 265

hi,all 在说数据驱动之前先很大家讲一下vue的两种编译模式,不看源码是真的不晓得🤔这两种编译模式到底是有怎样的区别噻,想知道就往下⬇️:

Runtime Only与 Runtime+compuler编译方式

Runtime Only的优点:压缩体积小,运行速度快.

只能识别render函数,不能识别template。.vue文件中的也是被 vue-template-compiler 翻译成了 render函数,所以只能在.vue里写 template。

vue对template的解析方式:template->ast->render->虚拟dom->真实dom。需要5步才能将内容展示给用户,使用runtime-only则会省略前面两步。

在runtimeOnly中,render会调用一个函数创建元素:

new Vue({ el:'#app', router, render:createElement=>{ return createElement( 'div', {class:'div'}, ['wo', createElement('h2',["ni"])] ) } })

Runtime-only:

new Vue({ el:"#app", router, render:h=>h(App) })

Runtime-compiler:

new Vue({ el:'#app', template:'', components:{App} }) 数据驱动

数据驱动:是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作DOM,而是通过修改数据。

弄懂模版和数据如何渲染成最终的DOM

vue初始化及挂载 vue初始化 :new Vue讲解

从下面的源码可以看出来,Vue只能通过new进行初始化。【路径:src/core/instance/index.js】

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) }

初始化的时候用_init方法进行初始化:_init中的源码【路径:src/core/instance/init.js】,我们主要关注initMixin中的_init方法:

initLifecycle(vm) //初始化生命周期 initEvents(vm) //初始化事件 initRender(vm) //初始化渲染 callHook(vm, 'beforeCreate') //调用生命周期钩子函数 initInjections(vm) // 初始化Injections initState(vm) //初始化data,props,methods,watch、computed initProvide(vm) // 初始化provide callHook(vm, 'created') //调用生命周期钩子函数

初始化的事情做完之后,就调用$mount方法挂载vm,即就是要把模板渲染成最终的DOM结构:【路径:src/core/instance/init.js】

if (vm.$options.el) { vm.$mount(vm.$options.el) } vue实例挂载:$mount

(1)分析compiler版本$mount的实现:【路径:src/platforms/web/entry-runtime-with-compiler.js】

// 缓存原型上的$mount方法,再重新定义该方法 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) // 限制Vue不能挂在到body或者html节点上 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to or - mount to normal elements instead.` ) return this } const options = this.$options // 解析模板并将其渲染成render // 没有render方法,就将el/template转换成render方法 if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } // vue所有组件的渲染丢需要render方法,无论是.vue方式开发组件还是写了el/template属性,都会被转换成render // compileToFunctions这个方法是一个在线编译的过程,来生成render函数 const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } //调用vue本身的mount函数,返回一个Component return mount.call(this, el, hydrating) }

(2)分析runtime only版本$mount的实现:【路径:src/platforms/web/runtime/index.js】

由mountComponent去挂载组件

Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }

再看看mountComponent:核心是实例化一个渲染watcher。

watcher在这里的作用:

(1)初始化时会执行回调函数

(2)当vm实例中检测的数据发生变化的时候执行回调函数

export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 判断实例上是否存在渲染函数,如果不存在; // 则设置一个默认的渲染函数createEmptyVNode,该渲染函数会创建一个注释类型的VNode节点 if (!vm.$options.render) { vm.$options.render = createEmptyVNode if (process.env.NODE_ENV !== 'production') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { warn( 'Failed to mount component: template or render function not defined.', vm ) } } } //调用callHook函数来触发beforeMount生命周期钩子函数,可以为理解此时只是render 函数并没有形成虚拟dom,也没有将页面内容真正渲染上 callHook(vm, 'beforeMount') // 定义updateComponent,这个函数中参数'vm._render()'将会为我们得到一份最新的VNode节点树,'如果调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中' let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) //创建VNode const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) //通过_update方法渲染DOM vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined // updateComponent函数作为第二个参数传给Watcher类从而创建了watcher实例,那么 // updateComponent函数中读取的所有数据都将被watcher所监控 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // if (vm.$vnode == null) { //null表示当前根Vue的实例 vm._isMounted = true //为true表示实例已经挂在,同时执行mounted的 callHook(vm, 'mounted') } return vm }

接下来我们分析一下render

render和 Virtual DOM

组件初始化的时候就会调用Vue.prototype.__init,vm.__init中调用了initRender

在实例初始化时,给实例绑定__c方法,所以vm可以直接调用到__c,__c内部调用了createElement.

export function initRender (vm: Component) { // ... // bind the createElement fn to this instance // so that we get proper render context inside it. // args order: tag, data, children, normalizationType, alwaysNormalize // internal version is used by render functions compiled from templates vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) // normalization is always applied for the public version, used in // user-written render functions. vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) }

createElementt方法呢?其实是对_createElement方法的封装,这里我们直接来看__createElement,查看源码会发现主要是调用new VNode,生成vnode并返回。这里只展示了核心部分:

export function _createElement ( context: Component, tag?: string | Class | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array { ...... if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // 创建普通的VNode节点 vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 创建组件VNode vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // 创建组件VNode vnode = createComponent(tag, data, context, children) } ...... }

VNode其实就是对真实DOM的一种抽象描述。本质上其实就是一个普通的Javascript对象,对象的核心定义就是:标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

到这里我们就知道了在mountComponent中,vm._render是在哪里去创建VNode的,接下来就看怎样把VNode渲染成一个真实的DOM。这个过程我们需要在vm._update中看看:

update

_update被调用的时机有2个:首次渲染、数据更新;作用:就是把VNode渲染成真实的DOM

由于在数据更新的时候要进行新旧VNode的对比,然后进行差异化更新视图,_update的核心就是调用vm.__patch__方法(diff算法过程),这个方法在不同的平台定义不同:

._update函数 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { ...... if (!prevVnode) { // 首次初始化 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 页面数据更新 vm.$el = vm.__patch__(prevVnode, vnode) } ...... } .__patch__函数 Vue.prototype.__patch__ = inBrowser ? patch : noop //由于服务端渲染中不存在真实的浏览器环境,因此不需要渲染成真实的DOM,因此是一个空函数 patch

vm.__patch__方法其实调用的是createPatchFunction的返回值

const modules = platformModules.concat(baseModules) export const patch: Function = createPatchFunction({ nodeOps, modules }) //nodeOps封装了DOM操作的方法,modules定义了模块的钩子函数的实现

patch是与平台相关的,在web和weex平台将虚拟DOM映射到“平台DOM”的方法是不同的,并且对“DOM”包括的属性模块创建和更新也不尽相同。因此每个平台的nodeOps, modules各不相同。代码托管在src/platforms目录下。而不同平台patch主逻辑部分相同,所以公共部分托管在core下。

createPatchFunction

modules:定义的模块钩子函数的实现,向外暴露一些特有的方法 nodeOps:封装的一些列DOM操作方法

export function createPatchFunction (backend) { let i, j const cbs = {} const { modules, nodeOps } = backend // hooks =['create', 'activate', 'update', 'remove', 'destroy'] //遍历钩子hooks找到modules的各模块对应的方法 for (i = 0; i < hooks.length; ++i) { // 比如cbs.create=[] cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { // 遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组中 cbs[hooks[i]].push(modules[j][hooks[i]]) } } } ...... return function patch (oldVnode, vnode, hydrating, removeOnly) { //1.新节点不存在,旧节点存在,移除旧节点 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 2.新节点存在,旧节点不存在,添加新节点 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)) { //旧节点不是真实元素且与新旧节点是同一节点,然后就去对比修改 //patchVnode就是diff算法的过程 patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // 旧节点是真实的元素 if (isRealElement) { // 挂载到真实的DOM和处理服务器端渲染 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 ' + '

, or missing . Bailing hydration and performing ' + 'full client-side render.' ) } } // 不是服务器端渲染或者渲染失败,就把oldVnode转换为VNode对象 oldVnode = emptyNodeAt(oldVnode) } // 旧节点是真实的元素 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 通过虚拟节点创建真实的 DOM 并插入到它的父节点中parentElm createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // vnode.parent存在又不是同一节点,递归更新父占位符节点元素(异步组件) if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } const insert = ancestor.data.hook.insert if (inserrged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // 移除旧节点 if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode)//移除节点方法 } } } // invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }

createPatchFunction返回的patch方法,主要做了以下操作:

新节点不存在,旧节点存在,移除旧节点 新节点存在,旧节点不存在,添加新节点 新节点和旧节点都存在,旧节点不是真实元素且与新旧节点是同一节点,然后就去对比修改 新节点和旧节点都存在,旧节点是真实的元素。旧挂载到真实的DOM和处理服务器端渲染。旧节点是真实DOM也就是传进来的vm.$el对应的元素。例如:;这里还有一种情况就是vnode.parent存在,又不是同一节点,表示更新。比如异步组件 createElm

createElm的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。createElm的关键逻辑:

function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { //createComponent是用来创建子组件的,初始化时返回true/false。存在则走下面 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } //来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签; //然后再去调用平台 DOM 的操作去创建一个占位符元素。 //创建元素节点 vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) ...... //接下来递归调用createChildren创建子元素 createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { //调用invokeCreateHooks,执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 中 //调用各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等 invokeCreateHooks(vnode, insertedVnodeQueue) } //调用insert方法把DOM插入到父节点中 insert(parentElm, vnode.elm, refElm) ...... }

这里的整个patch的方法:首次渲染先调用createElm,传入的parentElm是oldVnode.elm的父元素.实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。

最后根据递归createElm生成的vnode插入顺序队列,执行相关的insert钩子函数

new Vue-->init-->$mount-->compiler-->render-->vnode-->patch-->DOM

下面再说一下patchVnode和updateChildren

patchVnode是patch函数的核心方法,也是在这个方法中调用了diff:updateChildren。下面是对patchVnode解读注释

patchVnode function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 新旧节点相同,直接返回 if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } // 执行组件的prepatch钩子 let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children // 全量更新新节点的属性 if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // patch过程 if (isUndef(vnode.text)) {//新节点不是文本节点 if (isDef(oldCh) && isDef(ch)) { //1.新旧节点都有子节点,且旧节点不等于新节点,执行updateChildren方法进行diff算法 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) {//2.新节点有子节点,旧节点没有,若有文本,清空旧节点的文本内容,插入子节点 if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) {//3.新节点没有子节点,旧节点有子节点,移除旧节点的子节点 removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) {//旧节点有文本,新节点没有,清空旧节点文本内容 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) {//4.新节点文本内容与旧节点文本内容不一致,更新旧节点文本内容 nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { // 执行组件的postpatch钩子函数 if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }

updateChildren是Vue diff算法中的核心方法:对比新的children和老的children节点数组的差别,然后进行更新。对比的核心就是尽可能的复用可以复用的节点。

updateChildren // diff新旧节点双端比较的重点: function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0//旧节点的开始索引 let newStartIdx = 0//新节点的开始索引 let oldEndIdx = oldCh.length - 1//旧节点的结束索引 let oldStartVnode = oldCh[0]//第一个旧节点 let oldEndVnode = oldCh[oldEndIdx]//最后一个旧节点 let newEndIdx = newCh.length - 1//新节点的开始索引 let newStartVnode = newCh[0]//第一个新节点 let newEndVnode = newCh[newEndIdx]//最后一个新节点 let oldKeyToIdx, idxInOld, vnodeToMove, refElm//旧节点的开始索引 // removeOnly是一个特殊的标志,仅由 使用,以确保被移除的元素在离开转换期间保持在正确的相对位置 const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { // 检查新节点是否重复 checkDuplicateKeys(newCh) } while (oldStartIdx newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }

看到了这里是不是就很心累啊!其实我也心累真的是看源码看的心塞哦!但是还是得看啊,必须得提升啊!所以看到这篇文章的同学,不点赞都没有关系,只要你看了有帮助就可以了!

有什么看源码更好的意见和建议,方便的话,各位大神们评论区给我指点指点,菜鸟求指教哦!



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有