vue源码解析(二)

418 阅读6分钟

一、vue源码准备工作

1. 调试vue源码的方法

  • 调试vue项⽬的⽅式
    • 安装依赖:npm i
    • 安装打包⼯具:npm i rollup -g
      win10:添加这段代码
      //node_modules/rollup-plugin-alias/dist/rollup-plugin-alias.js
        if (!/js$/.test(updatedId)) {
          console.log(updatedId + '  ---->  ' + updatedId + '.js');
          updatedId += '.js';
        }
      
    • 修改\build\config.js 文件:把 genConfig 函数的 config 变量加一个属性 sourceMap: true
    • npm run dev
    • 修改samples⾥⾯的⽂件引⽤新⽣成的vue.js

2. Vue源码目录

src
├── compiler        # 编译相关 
├── core            # 核心代码 
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码
  • compiler
    编译的工作可以在构建时做(借助 webpack、vue-loader 等辅助插件);也可以在运行时做,使用包含构建功能的 Vue.js。显然,编译是一项耗性能的工作,所以更推荐前者——离线编译。
  • core
    core 目录包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。
  • platform
    platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。
  • server
    Vue.js 2.0 支持了服务端渲染,服务端渲染相关的逻辑,注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器.
  • sfc 这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。

3. Vue.js 源码构建

  • 构建脚本
    在package.json有配置script字段作为npm的执行脚本。
    {
      "script": {
        "build": "node scripts/build.js",
        "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
        "build:weex": "npm run build -- weex"
      }
    }
    

先打开构建的入口 JS 文件,在 scripts/build.js 中:

let builds = require('./config').getAllBuilds()
    // filter builds via command line arg
    if (process.argv[2]) {
      const filters = process.argv[2].split(',')
      builds = builds.filter(b => {
        return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
      })
    } else {
      // filter out weex builds by default
      builds = builds.filter(b => {
        return b.output.file.indexOf('weex') === -1
      })
    }
    build(builds)

先从配置文件读取配置,再通过命令行参数对构建配置做过滤,这样就可以构建出不同用途的 Vue.js 了。接下来我们看一下配置文件,在 scripts/config.js 中:

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.js'),
    format: 'cjs',
    banner
  }

列举了一些 Vue.js 构建的配置,关于还有一些服务端渲染 webpack 插件以及 weex 的打包配置。对于单个配置,它是遵循 Rollup 的构建规则的。其中 entry 属性表示构建的入口 JS 文件地址,dest 属性表示构建后的 JS 文件地址。format 属性表示构建的格式。

  • Runtime Only VS Runtime + Compiler
    • Runtime Only 在编译阶段做的,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript。
    • Runtime + Compiler
      如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板。

4. Vue源码入口

为了分析vue的编译过程,我们来分析 Runtime + Compiler 构建出来的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js。 从入口找到初始化vue的js:src/core/index.js

  • vue的入口

    import Vue from './instance/index'
    import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
    
    initGlobalAPI(Vue)
    
    • initGlobalAPI
      Vue.js 在整个初始化过程中,除了给它的原型 prototype 上扩展方法,还会给 Vue 这个对象本身扩展全局的静态方法。
  • vue的定义
    接着打开src/core/instance/index.js,也就是vue的定义。

    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)//实现 set,watch、$delete
    eventsMixin(Vue)// $on、$once、$emit等事件监听初始化
    lifecycleMixin(Vue)//实现update、destroy、forceUpdate
    renderMixin(Vue)//_render、$nextTick
    
    export default Vue
    

在这段代码中,执行了初始化_init方法。xxxMixin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vue 的 prototype 上扩展一些方法

  • 总结
    本质上就是一个用 Function 实现的 Class,然后它的原型 prototype 以及它本身都扩展了一系列的方法和属性。

二、数据驱动

1. new vue()之后发生了什么

new vue()之后会调用 this._init 方法, 该方法在 src/core/instance/init.js 中定义。

vm._self = vm
  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)
  }
 function initState (vm) {
    vm._watchers = [];
    var 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);
    }
  }

vue初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等.

  • 总结
    在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM,接下来分析vue实例的挂载过程。

2. vue实例挂载的实现

compiler 版本的 $mount 实现非常有意思,先来看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定义:

/* @flow */
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  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
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      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
    }
  }
  return mount.call(this, el, hydrating)
}
Vue.compile = compileToFunctions
export default Vue

  • 首先,它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。
  • 如果没有定义 render 方法,则判断template 传入的参数,获取对应的innerHtml,如果是el,则执行getOuterHTML获取innerHtml。并且都赋值给template,最后利用compileToFunctions转换成 render,将render赋给options,options.render = render。
  • 我们目前分析的是Runtime + Compiler,这种模式下的$mount实现了将template、el转换为render。如果没有compiler,会通过webpack 和 loader在编译阶段转换为render。
  • 注意两个mount方法的区别,接下来调用原先原型上的mount 方法挂载,定义在 src/platform/web/runtime/index.js 中。
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount 方法实际上会去调用 mountComponent 方法。这个方法定义在 src/core/instance/lifecycle.js 文件中。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
 new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }

mountComponent 核心就是先实例化一个渲染Watcher,可以看出一个组件对应一个watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成vnode传入update(),最终调用 vm._update 更新 DOM。
Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数。接下来分析vm._render 和 vm._update。

3. render

Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件中:

 vnode = render.call(vm._renderProxy, vm.$createElement)

render传入的参数为createElement,因此vm._render 最终是通过执行 createElement 方法并返回的是 vnode。

4. Virtual DOM

真正的 DOM 元素是非常庞大的,有很多属性,因为浏览器的标准就把 DOM 设计的非常复杂。当我们频繁的去做 DOM 更新,会产生一定的性能问题。
而 Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多,它是定义在 src/core/vdom/vnode.js 中的。

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  ...

其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,因为不需要包含操作DOM的方法,因此很轻便。
Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。

5. update

_update 是实例的一个私有方法,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候。_update 方法的作用是把 VNode 渲染成真实的 DOM,它的定义在 src/core/instance/lifecycle.js 中:

if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }

update 的核心就是调用 vm.patch 方法。这个方法在不同的平台,比如 web 和 weex 上的定义是不一样的,因此在 web 平台中它的定义在 src/platforms/web/runtime/index.js 中:

Vue.prototype.__patch__ = inBrowser ? patch : noop

因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数,而在浏览器端渲染中,它指向了 patch 方法,它的定义在 src/platforms/web/runtime/patch.js中:

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)、
export const patch: Function = createPatchFunction({ nodeOps, modules })

该方法的定义是调用 createPatchFunction 方法的返回值。接下来看createPatchFunction 的实现,它定义在 src/core/vdom/patch.js 中:

return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
    /*vnode不存在则删*/
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
      /*oldVnode不存在则创建新节点*/
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
        /*oldVnode有nodeType,说明传递进来⼀个DOM元素*/
      const isRealElement = isDef(oldVnode.nodeType)
      /*是组件且是同⼀个节点的时候打补丁*/
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
        /*传递进来oldVnode是dom元素*/
      } else {
        if (isRealElement) {
        // 将该dom节点清空转换为vnode
          oldVnode = emptyNodeAt(oldVnode)
        }
        /*取代现有元素:*/
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        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)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // 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(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.patch。 patch的核心diff算法是通过同层的树节点进行比较而不是对树进行逐层遍历的方法,所以时间复杂度只有O(n),同层级只做三件事:增删改,newnode不存在就删,oldnode不存在就增,都存在就进行类型对比,类型不同直接替换,类型相同执行更新,接下来分析最后一种情况。

if (!isRealElement && sameVnode(oldVnode, vnode)) {
     /*vnode类型相同*/
     patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
}

接下来分析patch的核心方法patchVnode。

5.1 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
        }
        /*
         如果新旧VNode都是静态的,同时它们的key相同(代表同⼀节点),
         并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染⼀次),
         那么只需要替换elm以及componentInstance即可。
        */
        if (isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key &&
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        }
        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)
        }
        <!--新vnode没有文本节点时-->
        if (isUndef(vnode.text)) {
            //新旧vnode的children存在
          if (isDef(oldCh) && isDef(ch)) {
            <!--新⽼节点均有children⼦节点,则对⼦节点进⾏diff操作,调⽤updateChildren-->
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
            <!--新vnode存在,老vnode不存在-->
          } else if (isDef(ch)) {
            if (process.env.NODE_ENV !== 'production') {
              checkDuplicateKeys(ch)
            }
            <!--则清空老节点的文本内容,然后添加新的子节点-->
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
             <!--老vnode存在,新vnode不存在-->
          } else if (isDef(oldCh)) {
            <!--移除老节点的children-->
            removeVnodes(oldCh, 0, oldCh.length - 1)
            <!--都没有children且新node没有文本,则清空-->
          } else if (isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '')
          }
          <!--新节点有文本,替换-->
        } else if (oldVnode.text !== vnode.text) {
          nodeOps.setTextContent(elm, vnode.text)
        }
        if (isDef(data)) {
          if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
        }
      }

因此,patchNode的具体规则如下:
1、如果新旧vnode是静态的同时key相同,并且新的vnode是clone或者标记了v-once,则替换componentInstance即可。
2、新vnode没有子节点而老vnode有节点时,则移除。
3、新vnode有节点而老vnode没有节点时,则增添。
4、新旧vnode都没有子节点时,就是文本的替换。
5、新旧vnode都有子节点时,则需要对子节点进行diff,调⽤updateChildren,是diff的核心点。

5.2 updateChildren方法解析

updateChildren主要作⽤是⽤⼀种较⾼效的⽅式⽐对新旧两个VNode的children得出最⼩操作补丁。执 ⾏⼀个双循环是传统⽅式,vue中针对web场景特点做了特别的算法优化。接下来先看图分析: 在新⽼两组VNode节点的左右头尾两侧都有⼀个变量标记,在遍历过程中这⼏个变量都会向中间靠拢。
当oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx时结束循环。 下⾯是遍历规则:

  • ⾸先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两交叉⽐较,共有4种⽐较⽅法。
    1. 开始和开始、结束和结束对比,满足samenode,则直接patchNode。
    2. 开始和结束,则说明newStartVnode移到了oldEndVnode后面,进⾏patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后⾯。

3. 结束和开始,则说明newEndVnode移到了oldStartVnode前面,进⾏patchVnode的同时还需要将真实DOM节点移动到oldStartVnode的前⾯。

  • 如果以上情况均不符合,有两种情况。
  1. 需在oldVnode中的其他vnode找与newStartVnode满足sameNode的节点。如果存在则执行patchVnode并将对应的DOM移到oldStartVnode的前面。
  2. 也有可能newStartVnode在old VNode节点中找不到⼀致的key,或者是即便key相同却不是 sameVnode,这个时候会调⽤createElm创建⼀个新的DOM节点。
  • ⾄此循环结束,但是我们还需要处理剩下的节点。
  1. 当结束时oldStartIdx > oldEndIdx,这个时候旧的VNode节点已经遍历完了,但是新的节点还没有。说 明了新的VNode节点实际上⽐⽼的VNode节点多,需要将剩下的VNode对应的DOM插⼊到真实DOM 中,此时调⽤addVnodes(批量调⽤createElm接⼝)。
  2. 当结束时newStartIdx > newEndIdx时,说明新的VNode节点已经遍历完了,但是⽼的节点还有 剩余,需要从⽂档中删 的节点删除。 代码解读如下:
   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)) {
    /*分别⽐较oldCh以及newCh的两头节点4种情况,判定为同⼀个VNode,则直接patchVnode
    即可*/
    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 {
    /*
     ⽣成⼀个哈希表,key是旧VNode的key,值是该VNode在旧VNode中索引
     */
    if (isUndef(oldKeyToIdx))
      oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
    /*如果newStartVnode存在key并且这个key在oldVnode中能找到则返回这个节点的索引*/
    idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null;
    if (isUndef(idxInOld)) {
      /*没有key或者是该key没有在⽼节点中找到则创建⼀个新的节点*/
      createElm(
        newStartVnode,
        insertedVnodeQueue,
        parentElm,
        oldStartVnode.elm
      );
      newStartVnode = newCh[++newStartIdx];
    } else {
      /*获取同key的⽼节点*/
      elmToMove = oldCh[idxInOld];
      if (sameVnode(elmToMove, newStartVnode)) {
        /*如果新VNode与得到的有相同key的节点是同⼀个VNode则进⾏patchVnode*/
        patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
        /*因为已经patchVnode进去了,所以将这个⽼节点赋值undefined,之后如果还有新节
    点与该节点key相同可以检测出来提示已有重复的key*/
        oldCh[idxInOld] = undefined;
        /*当有标识位canMove实可以直接插⼊oldStartVnode对应的真实DOM节点前⾯*/
        canMove &&
          nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm);
        newStartVnode = newCh[++newStartIdx];
      } else {
        /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(⽐如说tag不⼀样或
    者是有不⼀样type的input标签),创建⼀个新的节点*/
        createElm(
          newStartVnode,
          insertedVnodeQueue,
          parentElm,
          oldStartVnode.elm
        );
        newStartVnode = newCh[++newStartIdx];
      }
    }
  }
}
if (oldStartIdx > oldEndIdx) {
  /*全部⽐较完成以后,发现oldStartIdx > oldEndIdx的话,说明⽼节点已经遍历完了,新节
    点⽐⽼节点多,所以这时候多出来的新节点需要⼀个⼀个创建出来加⼊到真实DOM中*/
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
  addVnodes(
    parentElm,
    refElm,
    newCh,
    newStartIdx,
    newEndIdx,
    insertedVnodeQueue
  );
} else if (newStartIdx > newEndIdx) {
  /*如果全部⽐较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,⽼节
    点多余新节点,这个时候需要将多余的⽼节点从真实DOM中移除*/
  removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}

  • createElm方法
    接下来分析新旧vnode存在,oldNode为DOM节点的情况:
    一般发生在初始化挂载时期,假设this.$mount('#app'),
oldVnode = emptyNodeAt(oldVnode)
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
  vnode,
  insertedVnodeQueue,
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)
//移除旧的节点
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}

将传入的dom节点转换为空的vnode节点,获取它的父节点,执行createElm。 createElm的核心代码为:

     function createElm (
       vnode,
       insertedVnodeQueue,
       parentElm,
       refElm,
       nested,
       ownerArray,
       index
     ) {
       if (isDef(vnode.elm) && isDef(ownerArray)) {
         vnode = ownerArray[index] = cloneVNode(vnode)
       }
       vnode.isRootInsert = !nested // for transition enter check
       if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
         return
       }
       const data = vnode.data
       const children = vnode.children
       const tag = vnode.tag
       if (isDef(tag)) {
         vnode.elm = vnode.ns
           ? nodeOps.createElementNS(vnode.ns, tag)
           : nodeOps.createElement(tag, vnode)
         setScope(vnode)
         if (__WEEX__) {
           if (!appendAsTree) {
             if (isDef(data)) {
               invokeCreateHooks(vnode, insertedVnodeQueue)
             }
             insert(parentElm, vnode.elm, refElm)
           }
           createChildren(vnode, children, insertedVnodeQueue)
           if (appendAsTree) {
             if (isDef(data)) {
               invokeCreateHooks(vnode, insertedVnodeQueue)
             }
             insert(parentElm, vnode.elm, refElm)
           }
         } else {
           createChildren(vnode, children, insertedVnodeQueue)
           if (isDef(data)) {
             invokeCreateHooks(vnode, insertedVnodeQueue)
           }
           insert(parentElm, vnode.elm, refElm)
         }
       } else if (isTrue(vnode.isComment)) {
         vnode.elm = nodeOps.createComment(vnode.text)
         insert(parentElm, vnode.elm, refElm)
       } else {
         vnode.elm = nodeOps.createTextNode(vnode.text)
         insert(parentElm, vnode.elm, refElm)
       }
     }

这段代码的核心是遍历子元素调用createChildren方法,createChildren方法的核心是递归createElm,并对节点进行判断。接着再调用 invokeCreateHooks 方法执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 中。最后调用 insert 方法把 DOM 插入到父节点中。

三、深入响应式原理

1. 响应式对象

1.1 initState

在 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm) 方法,它的定义在 src/core/instance/state.js 中。

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

initState 方法主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作。
其中initProps:props 的初始化主要过程,就是遍历定义的 props 配置。遍历的过程主要做两件事情:一个是调用 defineReactive 方法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性。另一个是通过 proxy 把 vm._props.xxx 的访问代理到 vm.xxx 上。
initData:

proxy(vm, `_data`, key)
observe(data, true /* asRootData */)

data 的初始化主要过程也是做两件事,一个是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式。
无论是 props 或是 data 的初始化都是把它们变成响应式对象。

  • observe
    observe 的功能就是用来监测数据的变化,它的定义在 src/core/observer/index.js 中。
    export function observe (value: any, asRootData: ?boolean): Observer | void {
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob: Observer | void
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
      } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }

observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。

1.2 Observer

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    //将__ob__:observer实例添加到value上
    def(value, '__ob__', this)
    //如果是数组,执行observeArray
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // 如果是对象,执行walk
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。可以看到 observeArray 是遍历数组再次调用 observe 方法,而 walk 方法是遍历对象的 key 调用 defineReactive 方法。

1.3 defineReactive

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter,它的定义在 src/core/observer/index.js 中。

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    //获取对象子元素
    val = obj[key]
  }
  //子元素递归调用observe,进行深度监控
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象(对象和数组)递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter。

1.4 数组响应式

第一次传入的值为{arr:[item...]},执行defineReactive,判断子元素若是数组,new Observer(),执行dependArray收集依赖。

//对数组的每一项进行依赖收集,并且进行递归操作。
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

接着进入Observer中:

if (Array.isArray(value)) {
      if (hasProto) {
      //如果有原型,覆盖
        protoAugment(value, arrayMethods)
      } else {
      //如果没有,就definePeoperty定义拦截方法
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    //observe数组的每一项
    observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
    }

接下来看arrayMethods

const arrayProto = Array.prototype
// 创建数组:原型对应的
//现有的对象来提供新创建的对象的proto,也就是_proto_:arrayProto
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // 存储原始方法
  const original = arrayProto[method]
  // Define a property.obj, key, val, enumerable
  def(arrayMethods, method, function mutator (...args) {
    //调用数组原始方法
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    //对新添加的数组项进行观察
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

数组无法用arr[1]='1'这种方式去修改数组,因为没办法实现拦截。 变更数组有7个方法push、pop等,使用defineproperty重写arrayMethods对象(key为7个方法,value为mutator 方法),将arrayMethods覆盖在数组的__proto__上,当数组调用这七个方法时,相当于执行mutator(),例如arr.push(1),等同于执行mutator(1),在mutator中,执行原始数组的方法,通知dep更新,同时对新添加的选项执行observe。

  • 总结
    核心就是利用 Object.defineProperty 给数据添加了 getter 和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑:getter 做的事情是依赖收集,setter 做的事情是派发更新。

2. 依赖收集

2.1 Dep

defineReactive在 get 函数中通过 dep.depend 做依赖收集,Dep 是整个 getter 依赖收集的核心,它的定义在 src/core/observer/dep.js 中:

import type Watcher from './watcher'
import { remove } from '../util/index'

let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

Dep.target是全局唯一的watcher,通过这个全局属性连接Dep和watcher。因为同一时间只能有一个watcher被设计。dep的自身属性 subs 也是 Watcher 的数组。Dep 实际上就是对 Watcher 的一种管理。

2.2 Watcher

  get () {
  //把 Dep.target 赋值为当前的渲染 watcher 并压栈
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      //把 Dep.target 恢复成上一个状态
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

this.getter 对应就是 updateComponent 函数,这实际上就是在执行:vm._update(vm._render(), hydrating),它会先执行 vm._render() 方法,这个方法会生成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter。
那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。

2.3 派发更新

class Dep {
  // ...
  notify () {
  // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法,它的定义在 src/core/observer/watcher.js 中:调用了queueWatcher(this)。

2.4 总结

Vue中先遍历data选项中所有的属性(发布者)用Object.defineProperty劫持这些属性将其转为getter/setter。读取数据时候会触发getter。修改数据时会触发setter。 然后给每个属性对应new Dep(),Dep是专门收集依赖、删除依赖、向依赖发送消息的。先让每个依赖设置在Dep.target上,在Dep中创建一个依赖数组,先判断Dep.target是否已经在依赖中存在,不存在的话添加到依赖数组中完成依赖收集,随后将Dep.target置为上一个依赖。 组件在挂载过程中都会new一个Watcher实例。这个实例就是依赖(订阅者)。Watcher第二参数式一个函数,此函数作用是更新且渲染节点。在首次渲染过程,会自动调用Dep方法来收集依赖,收集完成后组件中每个数据都绑定上该依赖。当数据变化时就会在seter中通知对应的依赖进行更新。在更新过程中要先读取数据,就会触发Wacther的第二个函数参数。一触发就再次自动调用Dep方法收集依赖,同时在此函数中运行patch(diff运算)来更新对应的DOM节点,完成了双向绑定。

参考网址:
1.ustbhuangyi.github.io/vue-analysi… 2.juejin.im/post/684490…