Vue 2.0源码学习

jason_2004 4年前
   <h2><strong>Vue2.0介绍</strong></h2>    <p>从去年9月份了解到Vue后,就被他简洁的API所吸引。1.0版本正式发布后,就在业务中开始使用,将原先jQuery的功能逐步的进行迁移。</p>    <p>今年的10月1日,Vue的2.0版本正式发布了,其中核心代码都进行了重写,于是就专门花时间,对Vue 2.0的源码进行了学习。本篇文章就是2.0源码学习的总结。</p>    <p>先对Vue 2.0的新特性做一个简单的介绍:</p>    <ul>     <li> <p>大小 & 性能。Vue 2.0的线上包gzip后只有12Kb,而1.0需要22Kb,react需要44Kb。而且,Vue 2.0的性能在react等几个框架中,性能是最快的。</p> </li>     <li> <p>VDOM。实现了Virtual DOM, 并且将静态子树进行了提取,减少界面重绘时的对比。与1.0对比性能有明显提升。</p> </li>     <li> <p>template & JSX。众所周知,Vue 1.0使用的是template来实现模板,而React使用了JSX实现模板。关于template和JSX的争论也很多,很多人不使用React就是因为没有支持template写法。Vue 2.0对template和JSX写法都做了支持。使用时,可以根据具体业务细节进行选择,可以很好的发挥两者的优势。就这一点,Vue已经超过React了。</p> </li>     <li> <p>Server Render。2.0还对了Server Render做了支持。这一点并没有在业务中使用,不做评价。</p> </li>    </ul>    <p>Vue的最新源码可以去 <a href="/misc/goto?guid=4958977564508786080" rel="nofollow,noindex">https://github.com/vuejs/vue</a> 获得。本文讲的是 2.0.3版本,2.0.3可以去 <a href="/misc/goto?guid=4959725313375943625" rel="nofollow,noindex">https://github.com/vuejs/vue/...</a> 这里获得。</p>    <p>下面开始进入正题。首先从生命周期开始。</p>    <h2><strong>生命周期</strong></h2>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/aaa1b52d4c397d884d73775189440dcd.png"></p>    <p>上图就是官方给出的Vue 2.0的生命周期图,其中包含了Vue对象生命周期过程中的几个核心步骤。了解了这几个过程,可以很好的帮助我们理解Vue的创建与销毁过程。</p>    <p>从图中我们可以看出,生命周期主要分为4个过程:</p>    <ul>     <li> <p>create。 new Vue 时,会先进行create,创建出Vue对象。</p> </li>     <li> <p>mount。根据el, template, render方法等属性,会生成DOM,并添加到对应位置。</p> </li>     <li> <p>update。当数据发生变化后,会重新渲染DOM,并进行替换。</p> </li>     <li> <p>destory。销毁时运行。</p> </li>    </ul>    <p>那么这4个过程在源码中是怎么实现的呢?我们从 new Vue 开始。</p>    <h2><strong>new Vue</strong></h2>    <p>为了更好的理解new的过程,我整理了一个序列图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/266c386997380dfe90fb2dac1f42cb5b.jpg"></p>    <p>new Vue的过程主要涉及到三个对象:vm、compiler、watcher。其中,vm表示Vue的具体对象;compiler负责将template解析为AST render方法;watcher用于观察数据变化,以实现数据变化后进行re-render。</p>    <p>下面来分析下具体的过程和代码:</p>    <p>首先,运行 new Vue() 的时候,会进入代码 src/core/instance/index.js 的Vue构造方法中,并执行 this._init() 方法。在 _init 中,会对各个功能进行初始化,并执行 beforeCreate 和 created 两个生命周期方法。核心代码如下:</p>    <pre>  <code class="language-javascript">initLifecycle(vm)  initEvents(vm)  callHook(vm, 'beforeCreate')  initState(vm)  callHook(vm, 'created')  initRender(vm)</code></pre>    <p>这个过程有一点需要注意:</p>    <p>beforeCreate和created之间只有initState,和官方给出的生命周期图并不完全一样。这里的initState是用于初始化data,props等的监听的。</p>    <p>在 _init 的最后,会运行 initRender 方法。在该方法中,会运行 vm.$mount 方法,代码如下:</p>    <pre>  <code class="language-javascript">if (vm.$options.el) {    vm.$mount(vm.$options.el)  }</code></pre>    <p>这里的 vm.$mount 可以在业务代码中调用,这样,new 过程和 mount过程就可以根据业务情况进行分离。</p>    <p>这里的 $mount 在 src/entries/web-runtime-with-compiler.js 中,主要逻辑是根据el, template, render三个属性来获得AST render方法。代码如下:</p>    <pre>  <code class="language-javascript">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)</code></pre>    <p>这个过程有三点需要注意:</p>    <p>compile时,将最大静态子树提取出来作为单独的AST渲染方法,以提升后面vNode对比时的性能。所以,当存在多个连续的静态标签时,可以在外边添加一个静态父节点,这样,staticRenderFns数目可以减少,从而提升性能。</p>    <p>Vue 2.0中的模板有三种引用写法:el, template, render(JSX)。其中的优先级是render > template > el。</p>    <p>el, template两种写法,最后都会通过compiler转化为render(JSX)来运行,也就是说,直接写成render(JSX)是性能最优的。当然,如果使用了构建工具,最终生成的包就是使用的render(JSX)。这样子,在源码上就可以不用过多考虑这一块的性能了,直接用可维护性最好的方式就行。</p>    <p>将模板转化为render,用到了 compileToFunctions方法 ,该方法最后会通过 src/compiler/index.js 文件中的 compile 方法,将模板转化为AST语法结构的render方法,并对静态子树进行分离。</p>    <p>完成render方法的生成后,会进入 _mount (src/core/instance.lifecycle.js)中进行DOM更新。该方法的核心逻辑如下:</p>    <pre>  <code class="language-javascript">vm._watcher = new Watcher(vm, () => {    vm._update(vm._render(), hydrating)  }, noop)</code></pre>    <p>首先会new一个watcher对象,在watcher对象创建后,会运行传入的方法 vm._update(vm._render(), hydrating) (watcher的逻辑在下面的watcher小节中细讲)。其中的 vm._render() 主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。这里的vNode就是一个虚拟的DOM节点。</p>    <p>拿到vNode后,传入 vm._update() 方法,进行DOM更新。</p>    <h2><strong>VDOM</strong></h2>    <p>上面已经讲完了 new Vue 过程中的主要步骤,其中涉及到template如何转化为DOM的过程,这里单独拿出来讲下。先上序列图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/bc29acd04d7babe09e7085d1bafdd1cb.jpg"></p>    <p>从图中可以看出,从template到DOM,有三个过程:</p>    <ul>     <li> <p>template -> AST render(compiler解析template)</p> </li>     <li> <p>AST render -> vNode(render方法运行)</p> </li>     <li> <p>vNode -> DOM(vdom.patch)</p> </li>    </ul>    <p>首先是template在compiler中解析为AST render方法的过程。上一节中有说到, initRender 后,会调用到 src/entries/web-runtime-with-compiler.js 中的 Vue.prototype.$mount 方法。在 $mount 中,会获取template,然后调用 src/platforms/web/compiler/index.js 的 compileToFunctions 方法。在该方法中,会运行 compile 将template解析为多个render方法,也就是AST render。这里的 compile 在文件 src/compiler/index.js 中,代码如下:</p>    <pre>  <code class="language-javascript">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  }</code></pre>    <p>可以看出, compile 方法就是将template以AST的方式进行解析,并转化为render方法进行返回。</p>    <p>再看第二个过程:AST render -> vNode。这个过程很简单,就是将AST render方法进行运行,获得返回的vNode对象。</p>    <p>最后一步,vNode -> DOM。该过程中,存在vNode的对比以及DOM的添加修改操作。</p>    <p>在上一节中,有讲到 vm._update() 方法中对DOM进行更新。 _update 的主要代码如下:</p>    <pre>  <code class="language-javascript">// 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更新  }</code></pre>    <p>可以看出,无论是首次添加还是后期的update,都是通过 __patch__ 来更新的。这里的 __patch__ 核心步骤是在 src/core/vdom/patch.js 中的 patch 方法进行实现,源码如下:</p>    <pre>  <code class="language-javascript">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)          }          ...        }      }      ...    }</code></pre>    <p>首次添加很简单,就是通过insertBefore将转化好的element添加到DOM中。如果是update,则会调动 patchVnode() 。最后来看下 patchVnode 的代码:</p>    <pre>  <code class="language-javascript">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)    }  }</code></pre>    <p>其中有调用了 updateChildren 来更新子节点,代码如下:</p>    <pre>  <code class="language-javascript">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 {        ...      }    }    ...  }</code></pre>    <p>可以看到 updateChildren 中,又通过 patchVnode 来更新当前节点。梳理一下, patch 通过 patchVnode 来更新根节点,然后通过 updateChildren 来更新子节点,具体子节点,又通过 patchVnode 来更新,通过一个类似于递归的方式逐个节点的完成对比和更新。</p>    <p>Vue 2.0中对如何去实现VDOM的思路是否清晰,通过4层结构,很好的实现了可维护性,也为实现server render, weex等功能提供了可能。拿server render举例,只需要将最后的 vNode -> DOM 改成 vNode -> String 或者 vNode -> Stream , 就可以实现server render。剩下的compiler和Vue的核心逻辑都不需要改。</p>    <h2><strong>Watcher</strong></h2>    <p>我们都知道MVVM框架的特征就是当数据发生变化后,会自动更新对应的DOM节点。使用MVVM之后,业务代码中就可以完全不写DOM操作代码,不仅可以将业务代码聚焦在业务逻辑上,还可以提高业务代码的可维护性和可测试性。那么Vue 2.0中是怎么实现对数据变化的监听的呢?照例,先看序列图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b2f109f40ac63ad8cc6fe73a1739008c.jpg"></p>    <p>可以看出,整个Watcher的过程可以分为三个过程。</p>    <ul>     <li> <p>对state设置setter/getter</p> </li>     <li> <p>对vm设置好Watcher,添加好state 触发 setter时的执行方法</p> </li>     <li> <p>state变化触发执行</p> </li>    </ul>    <p>前面有说过,在生命周期函数 beforeCreate 和 created 直接,会运行方法 initState() 。在 initState 中,会对Props, Data, Computed等属性添加Setter/Getter。拿Data举例,设置setter/getter的代码如下:</p>    <pre>  <code class="language-javascript">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)  }</code></pre>    <p>通过调用 observe 方法,会对data添加好观察者,核心代码为:</p>    <pre>  <code class="language-javascript">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进行数据更新    }  })</code></pre>    <p>这个时候,对data的监听已经完成。可以看到,当data发生变化的时候,会运行 dep.notify() 。在 notify 方法中,会去运行watcher的 update 方法,内容如下:</p>    <pre>  <code class="language-javascript">update () {    if (this.lazy) {      this.dirty = true    } else if (this.sync) {      this.run()    } else {      queueWatcher(this)    }  }  run () {    if (this.active) {      const value = this.get()    }    ...  }</code></pre>    <p>update 方法中,queueWatcher方法的目的是通过 nextTicker 来执行 run 方法,属于支线逻辑,就不分析了,这里直接看 run 的实现。 run 方法其实很简单,就是调用 get 方法,而 get 方法会通过执行 this.getter() 来更新DOM。</p>    <p>那么 this.getter 是什么呢?本文最开始分析 new Vue 过程时,有讲到运行 _mount 方法时,会运行如下代码:</p>    <pre>  <code class="language-javascript">vm._watcher = new Watcher(vm, () => {    vm._update(vm._render(), hydrating)  }, noop)</code></pre>    <p>那么 this.getter 就是这里Watcher方法的第二个参数。来看下 new Watcher 的代码:</p>    <pre>  <code class="language-javascript">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()    }  }</code></pre>    <p>可以看出,在 new Vue 过程中,Watcher会在构造完成后主动调用 this.get() 来触发 this.getter() 方法的运行,以达到更新DOM节点。</p>    <p>总结一下这个过程:首先 _init 时,会对Data设置好setter方法,setter方法中会调用 dep.notify() ,以便数据变化时通知DOM进行更新。然后 new Watcher 时,会将更新DOM的方法进行设置,也就是 Watcher.getter 方法。最后,当Data发生变化的时候, dep.notify() 运行,运行到 watcher.getter() 时,就会去运行render和update逻辑,最终达到DOM更新的目的。</p>    <h2><strong>总结与收获</strong></h2>    <p>刚开始觉得看源码,是因为希望能了解下Vue 2.0的实现,看看能不能得到一些从文档中无法知道的细节,用于提升运行效率。把主要流程理清楚后,的确了解到一些,这里做个整理:</p>    <ul>     <li> <p>el属性传入的如果不是element,最后会通过 document.querySelector 来获取的,这个接口性能较差,所以,el传入一个element性能会更好。</p> </li>     <li> <p>$mount 方法中对 html , body 标签做了过滤,这两个不能用来作为渲染的根节点。</p> </li>     <li> <p>每一个组件都会从 _init 开始重新运行,所以,当存在一个长列表时,将子节点作为一个组件,性能会较差。</p> </li>     <li> <p>*.vue 文件会在构建时转化为 render 方法,而 render 方法的性能比指定 template 更好。所以,源码使用 *.vue 的方式,性能更好。</p> </li>     <li> <p>如果需要自定义 delimiters ,每一个组件都需要单独指定。</p> </li>     <li> <p>如果是 *.vue 文件,指定 delimiters 是失效的,因为 vue-loader 对 *.vue 文件进行解析时,并没有将 delimiters 传递到 compiler.compile() 中。(这一点不确定是bug还是故意这样设计的)。</p> </li>    </ul>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000007484936</p>    <p> </p>