Vue 2.0源码进修

Vue2.0引见

从客岁9月份相识到Vue后,就被他简约的API所吸收。1.0版本正式宣布后,就在营业中最先运用,将本来jQuery的功用逐渐的举行迁徙。
本年的10月1日,Vue的2.0版本正式宣布了,个中中心代码都举行了重写,于是就特地花时间,对Vue 2.0的源码举行了进修。本篇文章就是2.0源码进修的总结。

先对Vue 2.0的新特性做一个简朴的引见:

  • 大小 & 机能。Vue 2.0的线上包gzip后只要12Kb,而1.0须要22Kb,react须要44Kb。而且,Vue 2.0的机能在react等几个框架中,机能是最快的。

  • VDOM。完成了Virtual DOM, 而且将静态子树举行了提取,削减界面重绘时的对照。与1.0对照机能有显著提拔。

  • template & JSX。尽人皆知,Vue 1.0运用的是template来完成模板,而React运用了JSX完成模板。关于template和JSX的争辩也许多,许多人不运用React就是由于没有支撑template写法。Vue 2.0对template和JSX写法都做了支撑。运用时,能够依据详细营业细节举行挑选,能够很好的发挥二者的上风。就这一点,Vue已凌驾React了。

  • Server Render。2.0还对了Server Render做了支撑。这一点并没有在营业中运用,不做评价。

Vue的最新源码能够去 https://github.com/vuejs/vue 取得。本文讲的是 2.0.3版本,2.0.3能够去 https://github.com/vuejs/vue/… 这里取得。

下面最先进入正题。起首从生命周期最先。

生命周期

《Vue 2.0源码进修》

上图就是官方给出的Vue 2.0的生命周期图,个中包含了Vue对象生命周期历程当中的几个中心步骤。相识了这几个历程,能够很好的协助我们明白Vue的建立与烧毁历程。
从图中我们能够看出,生命周期重要分为4个历程:

  • createnew Vue时,会先举行create,建立出Vue对象。

  • mount。依据el, template, render要领等属性,会天生DOM,并增加到对应位置。

  • update。当数据发生变化后,会从新衬着DOM,并举行替代。

  • destory。烧毁时运转。

那末这4个历程在源码中是怎样完成的呢?我们从new Vue最先。

new Vue

为了更好的明白new的历程,我整顿了一个序列图:

《Vue 2.0源码进修》

new Vue的历程重要涉及到三个对象:vm、compiler、watcher。个中,vm示意Vue的详细对象;compiler担任将template剖析为AST render要领;watcher用于视察数据变化,以完成数据变化后举行re-render。

下面来剖析下详细的历程和代码:
起首,运转new Vue()的时刻,会进入代码src/core/instance/index.js的Vue组织要领中,并实行this._init()要领。在_init中,会对各个功用举行初始化,并实行beforeCreatecreated两个生命周期要领。中心代码以下:

initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)

这个历程有一点须要注重:
beforeCreate和created之间只要initState,和官方给出的生命周期图并不完整一样。这里的initState是用于初始化data,props等的监听的。

_init的末了,会运转initRender要领。在该要领中,会运转vm.$mount要领,代码以下:

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

这里的vm.$mount能够在营业代码中挪用,如许,new 历程和 mount历程就能够依据营业状况举行星散。

这里的$mountsrc/entries/web-runtime-with-compiler.js中,重要逻辑是依据el, template, render三个属性来取得AST render要领。代码以下:

if (!options.render) {   // 假如有render要领,直接运转mount
  let template = options.template
  if (template) {  // 假如有template, 猎取template参数关于的HTML作为模板
    if (typeof template === 'string') {
      if (template.charAt(0) === '#') {
        template = idToTemplate(template)
      }
    } 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, 且存在el,则猎取el的outerHTML作为模板
    template = getOuterHTML(el)
  }
  if (template) { // 假如猎取到了模板,则将模板转化为render要领
    const { render, staticRenderFns } = compileToFunctions(template, {
      warn,
      shouldDecodeNewlines,
      delimiters: options.delimiters
    }, this)
    options.render = render
    options.staticRenderFns = staticRenderFns
  }
}
return mount.call(this, el, hydrating)

这个历程有三点须要注重:
compile时,将最大静态子树提取出来作为零丁的AST衬着要领,以提拔背面vNode对照时的机能。所以,当存在多个一连的静态标签时,能够在外边增加一个静态父节点,如许,staticRenderFns数量能够削减,从而提拔机能。
Vue 2.0中的模板有三种援用写法:el, template, render(JSX)。个中的优先级是render > template > el。
el, template两种写法,末了都邑经由过程compiler转化为render(JSX)来运转,也就是说,直接写成render(JSX)是机能最优的。固然,假如运用了构建东西,终究天生的包就是运用的render(JSX)。如许子,在源码上就能够不必过量斟酌这一块的机能了,直接用可维护性最好的体式格局就行。

将模板转化为render,用到了compileToFunctions要领,该要领末了会经由过程src/compiler/index.js文件中的compile要领,将模板转化为AST语法构造的render要领,并对静态子树举行星散。

完成render要领的天生后,会进入_mount(src/core/instance.lifecycle.js)中举行DOM更新。该要领的中心逻辑以下:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

起首会new一个watcher对象,在watcher对象建立后,会运转传入的要领vm._update(vm._render(), hydrating)(watcher的逻辑鄙人面的watcher小节中细讲)。个中的vm._render()重要作用就是运转前面compiler天生的render要领,并返回一个vNode对象。这里的vNode就是一个假造的DOM节点。

拿到vNode后,传入vm._update()要领,举行DOM更新。

VDOM

上面已讲完了new Vue历程当中的重要步骤,个中涉及到template怎样转化为DOM的历程,这里零丁拿出来说下。先上序列图:

《Vue 2.0源码进修》

从图中能够看出,从template到DOM,有三个历程:

  • template -> AST render (compiler剖析template)

  • AST render -> vNode (render要领运转)

  • vNode -> DOM (vdom.patch)

起首是template在compiler中剖析为AST render要领的历程。上一节中有说到,initRender后,会挪用到src/entries/web-runtime-with-compiler.js中的Vue.prototype.$mount要领。在$mount中,会猎取template,然后挪用src/platforms/web/compiler/index.jscompileToFunctions要领。在该要领中,会运转compile将template剖析为多个render要领,也就是AST render。这里的compile在文件src/compiler/index.js中,代码以下:

const ast = parse(template.trim(), options)   // 剖析template为AST
optimize(ast, options)  // 提取static tree
const code = generate(ast, options)  // 天生render 要领
return {
  ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
}

能够看出,compile要领就是将template以AST的体式格局举行剖析,并转化为render要领举行返回。

再看第二个历程:AST render -> vNode。这个历程很简朴,就是将AST render要领举行运转,取得返回的vNode对象。

末了一步,vNode -> DOM。该历程当中,存在vNode的对照以及DOM的增加修正操纵。
在上一节中,有讲到vm._update()要领中对DOM举行更新。_update的重要代码以下:

// src/core/instance/lifecycle.js
if (!prevVnode) {
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating)  // 初次增加
} else {
  vm.$el = vm.__patch__(prevVnode, vnode)  // 数据变化后触发的DOM更新
}

能够看出,无论是初次增加照样后期的update,都是经由过程__patch__来更新的。这里的__patch__中心步骤是在src/core/vdom/patch.js中的patch要领举行完成,源码以下:

function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (!oldVnode) {
      ...
    } else {
      ...
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)  // diff并更新DOM。
      } else {
        elm = oldVnode.elm
        parent = nodeOps.parentNode(elm)
        ...
        if (parent !== null) {
          nodeOps.insertBefore(parent, vnode.elm, nodeOps.nextSibling(elm))  // 增加element到DOM。
          removeVnodes(parent, [oldVnode], 0, 0)
        }
        ...
      }
    }
    ...
  }

初次增加很简朴,就是经由过程insertBefore将转化好的element增加到DOM中。假如是update,则会变更patchVnode()。末了来看下patchVnode的代码:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  ...
  const elm = vnode.elm = oldVnode.elm
  const oldCh = oldVnode.children
  const ch = vnode.children
  ...
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {  // 当都存在时,更新Children
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {  // 只存在新节点时,即增加节点
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {  // 只存在老节点时,即删除节点
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {  // 删除了textContent
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) { // 修正了textContent
    nodeOps.setTextContent(elm, vnode.text)
  }
}

个中有挪用了updateChildren来更新子节点,代码以下:

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  ...
  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)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      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)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      ...
    }
  }
  ...
}

能够看到updateChildren中,又经由过程patchVnode来更新当前节点。梳理一下,patch经由过程patchVnode来更新根节点,然后经由过程updateChildren来更新子节点,详细子节点,又经由过程patchVnode来更新,经由过程一个类似于递归的体式格局逐一节点的完成对照和更新。

Vue 2.0中对怎样去完成VDOM的思绪是不是清晰,经由过程4层构造,很好的完成了可维护性,也为完成server render, weex等功用供应了能够。拿server render举例,只须要将末了的vNode -> DOM 改成 vNode -> String 或许 vNode -> Stream, 就能够完成server render。剩下的compiler和Vue的中心逻辑都不须要改。

Watcher

我们都晓得MVVM框架的特性就是当数据发生变化后,会自动更新对应的DOM节点。运用MVVM以后,营业代码中就能够完整不写DOM操纵代码,不仅能够将营业代码聚焦在营业逻辑上,还能够进步营业代码的可维护性和可测试性。那末Vue 2.0中是怎样完成对数据变化的监听的呢?按例,先看序列图:
《Vue 2.0源码进修》

能够看出,全部Watcher的历程能够分为三个历程。

  • 对state设置setter/getter

  • 对vm设置好Watcher,增加好state 触发 setter时的实行要领

  • state变化触发实行

前面有说过,在生命周期函数beforeCreatecreated直接,会运转要领initState()。在initState中,会对Props, Data, Computed等属性增加Setter/Getter。拿Data举例,设置setter/getter的代码以下:

function initData (vm: Component) {
  let data = vm.$options.data
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    ...
    proxy(vm, keys[i])   // 设置vm._data为代办
  }
  // observe data
  observe(data)
}

经由过程挪用observe要领,会对data增加好视察者,中心代码为:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()  // 处理好依靠watcher
      ...
    }
    return value
  },
  set: function reactiveSetter (newVal) {
    ...
    childOb = observe(newVal)  // 对新数据从新observe
    dep.notify()  // 关照到dep举行数据更新
  }
})

这个时刻,对data的监听已完成。能够看到,当data发生变化的时刻,会运转dep.notify()。在notify要领中,会去运转watcher的update要领,内容以下:

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
run () {
  if (this.active) {
    const value = this.get()
  }
  ...
}

update要领中,queueWatcher要领的目标是经由过程nextTicker来实行run要领,属于干线逻辑,就不剖析了,这里直接看run的完成。run要领实在很简朴,就是挪用get要领,而get要领会经由过程实行this.getter()来更新DOM。

那末this.getter是什么呢?本文最最先剖析new Vue历程时,有讲到运转_mount要领时,会运转以下代码:

vm._watcher = new Watcher(vm, () => {
  vm._update(vm._render(), hydrating)
}, noop)

那末this.getter就是这里Watcher要领的第二个参数。来看下new Watcher的代码:

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object = {}
  ) {
    ...
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    ...
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

能够看出,在new Vue历程当中,Watcher会在组织完成后主动挪用this.get()来触发this.getter()要领的运转,以到达更新DOM节点。

总结一下这个历程:起首_init时,会对Data设置好setter要领,setter要领中会挪用dep.notify(),以便数据变化时关照DOM举行更新。然后new Watcher时,会将更新DOM的要领举行设置,也就是Watcher.getter要领。末了,当Data发生变化的时刻,dep.notify()运转,运转到watcher.getter()时,就会去运转render和update逻辑,终究到达DOM更新的目标。

总结与收成

刚最先以为看源码,是由于希望能相识下Vue 2.0的完成,看看能不能获得一些从文档中没法晓得的细节,用于提拔运转效力。把重要流程理清晰后,确实相识到一些,这里做个整顿:

  • el属性传入的假如不是element,末了会经由过程document.querySelector来猎取的,这个接口机能较差,所以,el传入一个element机能会更好。

  • $mount要领中对htmlbody标签做了过滤,这两个不能用来作为衬着的根节点。

  • 每个组件都邑从_init最先从新运转,所以,当存在一个长列表时,将子节点作为一个组件,机能会较差。

  • *.vue文件会在构建时转化为render要领,而render要领的机能比指定template更好。所以,源码运用*.vue的体式格局,机能更好。

  • 假如须要自定义delimiters,每个组件都须要零丁指定。

  • 假如是*.vue文件,指定delimiters是失效的,由于vue-loader*.vue文件举行剖析时,并没有将delimiters通报到compiler.compile()中。(这一点不确定是bug照样有意如许设想的)。

    原文作者:chenhao_ch
    原文地址: https://segmentfault.com/a/1190000007484936
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞