[
  {
    "path": ".gitignore",
    "content": "node_modules/"
  },
  {
    "path": "README.md",
    "content": "# 深入剖析Vue源码\n\n![](https://user-gold-cdn.xitu.io/2019/10/14/16dc938b94904285?imageView2/1/w/1080/h/320/q/85/format/webp/interlace/1)\n\n## 网站地址\n\n[深入剖析Vue源码](https://book.penblog.cn)\n\n\n## 章节结构\n\n#### 丰富的选项合并策略\n```new Vue```是运行```Vue```框架的第一步，```Vue```作为构造器，实例化阶段的第一步是执行初始化过程，而选项合并是初始化的开始。我们会向构造器中传递各种类型的可配置选项，例如```data,props```,或者像```mounted```这类生命周期钩子。而除了这些用户自定义的选项，```Vue```还提供了很多内部的选项，这些选项遵循什么样的合并规则就是这一节分析的重点。\n\n\n#### 基础的数据代理\n使用```Vue```做开发的同学都知道，```Vue```的核心是它的响应式系统，而响应式系统的核心是利用了```Object.defineProperty```进行数据拦截，这一节内容会深入分析```Vue```中两种数据拦截的方式：```Object.defineProperty,Proxy```,尽管响应式系统用的是兼容性更好的```Object.defineProperty```，但是```proxy```也在源码中使用上了，其中的一个例子就是用作数据过滤筛选。\n\n\n#### 完整挂载流程和模板编译\n```Vue```版本提供了运行时版本和同时包含编译器和运行时的版本，他们都有各自的使用场景。除了介绍两者的区别外，文章的核心还介绍了实例在挂载阶段的完整流程，虽然不会对流程中的每个具体环节展开分析，但是可以知道大致完整的挂载思路。文章最后还介绍了编译器巧妙的设计思路。\n\n\n\n#### 完整渲染流程\n```Virtual DOM```是```js```操作和```DOM```渲染之间的桥梁，```JS```对```DOM```节点的操作，都会批量反应到```Virtual DOM```这个节点描述对象上，它的理念很大程度提高了渲染的性能。有了上一节的基础，这一节会分析两个挂载阶段的核心过程，```render,update```,```render```阶段会将模板编译渲染函数，解析成```Virtual DOM```树，```update```阶段会将```Virtual DOM```树映射为真实的```DOM```节点。\n\n\n\n#### 组件基础剖析\n组件是```Vue```另一个核心，组件化开发是衡量```Vue```开发能力的标准。文章会从组件的注册开始，介绍全局注册和局部注册在实现原理上的区别，另外组件的挂载流程也是分析的重点，这一切也都依赖于前面介绍过的渲染流程。\n\n\n\n#### 组件高级用法\n除了基础的组件用法，```Vue```还提供了高级的用法，例如异步组件和函数组件。异步组件是首屏性能优化的解决方案，深入它的实现原理更有助于我们在开发中首屏性能问题。而函数式组件也有其独特的使用场景。\n\n\n\n#### 深入响应式系统构建- 上，中，下\n响应式系统构建是```Vue```的核心，也是难点，这个系列会有三篇的内容去尝试分析内部的实现细节。从响应式数据的构建，再到每种数据类型依赖收集和派发更新的分析。文章也模拟了一个简易版的响应式系统方便深层次源码的分析。在响应式系统构建中，还有很多的特殊情况需要考虑，例如数组的响应式构建，对象的异常处理等。\n\n\n\n#### diff算法的实现\n```virtual dom```引入的另一个关键是在旧节点发生改变时，利用```diff```算法比较新旧节点的差异，以达到最小变化的改变真实节点。文章会从脱离框架的角度实现一个```diff```算法。\n\n\n\n#### 揭秘Vue的事件机制\n```Vue```提供了很多实用的功能给用户，其中一个就是使用模板去进行事件监听。```@click```作为事件指令会在模板编译阶段解析，并且会在真实节点的渲染阶段进行相关事件的绑定。而对于组件的事件而言，他提供了子父组件通信的方式，本质上是在同个子组件内部维护了一个事件总线。更多的内容可以参考文章的分析。\n\n\n\n#### 你想了解的```Vue```插槽\n```Vue```组件的另一个重要概念是插槽，它允许你以一种不同于严格的父子关系的方式组合组件。插槽为你提供了一个将内容放置到新位置或使组件更通用的出口。这一节将围绕官网对插槽内容的介绍思路，按照普通插槽，具名插槽，再到作用域插槽的思路，逐步深入内部的实现原理。\n\n\n#### v-model的语法糖\n我们都知道```v-model```是实现双向数据绑定的核心，但如果深入源码我们可以知道，```v-model```的核心只是通过事件触发去改变表单的值。除此之前```v-model```语法糖还在组合输入过程做了一系列的优化。另外组件上使用```v-model```本质上只是一个子父组件通信的语法糖。\n\n\n\n#### 动态组件的深入分析\n这一节，我们又回到了组件的分析。动态组件是我们平时开发中高频率使用的东西。核心是```is```属性的使用。文末还粗略介绍了另一个概念，动态组件。\n\n\n\n#### keep-alive的魔法\n内置组件中最重要，也是最经常使用的是```keep-alive```组件，我们将```keep-alive```配合动态组件```is```使用，达到在切换组件的同时，将旧组件进行缓存，以便保留初始状态的目的。```keep-alive```有不同于其他组件的生命周期，并且他在缓存上也做了优化。\n\n\n##### 码字不易，感谢支持\n![](https://static.sitestack.cn/projects/5865c0921b69e6006b3145a1/15cdfd3ae70566f9.png)\n![](https://static.sitestack.cn/projects/5865c0921b69e6006b3145a1/15cdfd3ed2af6660.png)\n\n\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"analysisofvue\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"dependencies\": {},\n  \"devDependencies\": {\n    \"gitbook-plugin-copy-code-button\": \"0.0.2\",\n    \"gitbook-theme-comscore\": \"0.0.3\"\n  },\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/Ocean1509/In-depth-analysis-of-Vue.git\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Ocean1509/In-depth-analysis-of-Vue/issues\"\n  },\n  \"homepage\": \"https://github.com/Ocean1509/In-depth-analysis-of-Vue#readme\"\n}\n"
  },
  {
    "path": "src/Vue-v2.6.8.js",
    "content": "/*!\n * Vue.js v2.6.8\n * (c) 2014-2019 Evan You\n * Released under the MIT License.\n */\n(function (global, factory) {\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n    typeof define === 'function' && define.amd ? define(factory) :\n    (global = global || self, global.Vue = factory());\n}(this, function () {\n  'use strict';\n\n  /*  */\n\n  var emptyObject = Object.freeze({});\n\n  // These helpers produce better VM code in JS engines due to their\n  // explicitness and function inlining.\n  function isUndef(v) {\n    return v === undefined || v === null\n  }\n\n  function isDef(v) {\n    return v !== undefined && v !== null\n  }\n\n  function isTrue(v) {\n    return v === true\n  }\n\n  function isFalse(v) {\n    return v === false\n  }\n\n  /**\n   * Check if value is primitive.\n   */\n  function isPrimitive(value) {\n    return (\n      typeof value === 'string' ||\n      typeof value === 'number' ||\n      // $flow-disable-line\n      typeof value === 'symbol' ||\n      typeof value === 'boolean'\n    )\n  }\n\n  /**\n   * Quick object check - this is primarily used to tell\n   * Objects from primitive values when we know the value\n   * is a JSON-compliant type.\n   */\n  function isObject(obj) {\n    return obj !== null && typeof obj === 'object'\n  }\n\n  /**\n   * Get the raw type string of a value, e.g., [object Object].\n   */\n  var _toString = Object.prototype.toString;\n\n  function toRawType(value) {\n    return _toString.call(value).slice(8, -1)\n  }\n\n  /**\n   * Strict object type check. Only returns true\n   * for plain JavaScript objects.\n   */\n  function isPlainObject(obj) {\n    return _toString.call(obj) === '[object Object]'\n  }\n\n  function isRegExp(v) {\n    return _toString.call(v) === '[object RegExp]'\n  }\n\n  /**\n   * Check if val is a valid array index.\n   */\n  function isValidArrayIndex(val) {\n    var n = parseFloat(String(val));\n    return n >= 0 && Math.floor(n) === n && isFinite(val)\n  }\n\n  // 判断promise对象的方法\n  function isPromise(val) {\n    return (\n      isDef(val) &&\n      typeof val.then === 'function' &&\n      typeof val.catch === 'function'\n    )\n  }\n\n  /**\n   * Convert a value to a string that is actually rendered.\n   */\n  function toString(val) {\n    return val == null ?\n      '' :\n      Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ?\n      JSON.stringify(val, null, 2) :\n      String(val)\n  }\n\n  /**\n   * Convert an input value to a number for persistence.\n   * If the conversion fails, return original string.\n   */\n  function toNumber(val) {\n    var n = parseFloat(val);\n    return isNaN(n) ? val : n\n  }\n\n  /**\n   * Make a map and return a function for checking if a key\n   * is in that map.\n   */\n  function makeMap(\n    str,\n    expectsLowerCase\n  ) {\n    var map = Object.create(null);\n    var list = str.split(',');\n    for (var i = 0; i < list.length; i++) {\n      map[list[i]] = true;\n    }\n    return expectsLowerCase ?\n      function (val) {\n        return map[val.toLowerCase()];\n      } :\n      function (val) {\n        return map[val];\n      }\n  }\n\n  /**\n   * Check if a tag is a built-in tag.\n   */\n  var isBuiltInTag = makeMap('slot,component', true);\n\n  /**\n   * Check if an attribute is a reserved attribute.\n   */\n  var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');\n\n  /**\n   * Remove an item from an array.\n   */\n  function remove(arr, item) {\n    if (arr.length) {\n      var index = arr.indexOf(item);\n      if (index > -1) {\n        return arr.splice(index, 1)\n      }\n    }\n  }\n\n  /**\n   * Check whether an object has the property.\n   */\n  var hasOwnProperty = Object.prototype.hasOwnProperty;\n\n  function hasOwn(obj, key) {\n    return hasOwnProperty.call(obj, key)\n  }\n\n  /**\n   * Create a cached version of a pure function.\n   */\n  function cached(fn) {\n    var cache = Object.create(null);\n    return (function cachedFn(str) {\n      var hit = cache[str];\n      return hit || (cache[str] = fn(str))\n    })\n  }\n\n  /**\n   * Camelize a hyphen-delimited string.\n   */\n  var camelizeRE = /-(\\w)/g;\n  var camelize = cached(function (str) {\n    return str.replace(camelizeRE, function (_, c) {\n      return c ? c.toUpperCase() : '';\n    })\n  });\n\n  /**\n   * Capitalize a string.\n   */\n\n  var capitalize = cached(function (str) {\n    return str.charAt(0).toUpperCase() + str.slice(1)\n  });\n\n  /**\n   * Hyphenate a camelCase string.\n   */\n  // aB 转 a-b\n  var hyphenateRE = /\\B([A-Z])/g;\n  var hyphenate = cached(function (str) {\n    return str.replace(hyphenateRE, '-$1').toLowerCase()\n  });\n\n  /**\n   * Simple bind polyfill for environments that do not support it,\n   * e.g., PhantomJS 1.x. Technically, we don't need this anymore\n   * since native bind is now performant enough in most browsers.\n   * But removing it would mean breaking code that was able to run in\n   * PhantomJS 1.x, so this must be kept for backward compatibility.\n   */\n\n  /* istanbul ignore next */\n  function polyfillBind(fn, ctx) {\n    function boundFn(a) {\n      var l = arguments.length;\n      return l ?\n        l > 1 ?\n        fn.apply(ctx, arguments) :\n        fn.call(ctx, a) :\n        fn.call(ctx)\n    }\n\n    boundFn._length = fn.length;\n    return boundFn\n  }\n\n  function nativeBind(fn, ctx) {\n    return fn.bind(ctx)\n  }\n\n  var bind = Function.prototype.bind ?\n    nativeBind :\n    polyfillBind;\n\n  /**\n   * Convert an Array-like object to a real Array.\n   */\n  function toArray(list, start) {\n    start = start || 0;\n    var i = list.length - start;\n    var ret = new Array(i);\n    while (i--) {\n      ret[i] = list[i + start];\n    }\n    return ret\n  }\n\n  /**\n   * Mix properties into target object.\n   */\n  // 将_from对象合并到to对象，属性相同时，则覆盖to对象的属性\n  function extend(to, _from) {\n    for (var key in _from) {\n      to[key] = _from[key];\n    }\n    return to\n  }\n\n  /**\n   * Merge an Array of Objects into a single Object.\n   */\n  function toObject(arr) {\n    var res = {};\n    for (var i = 0; i < arr.length; i++) {\n      if (arr[i]) {\n        extend(res, arr[i]);\n      }\n    }\n    return res\n  }\n\n  /* eslint-disable no-unused-vars */\n\n  /**\n   * Perform no operation.\n   * Stubbing args to make Flow happy without leaving useless transpiled code\n   * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).\n   */\n  function noop(a, b, c) {}\n\n  /**\n   * Always return false.\n   */\n  var no = function (a, b, c) {\n    return false;\n  };\n\n  /* eslint-enable no-unused-vars */\n\n  /**\n   * Return the same value.\n   */\n  var identity = function (_) {\n    return _;\n  };\n\n  /**\n   * Generate a string containing static keys from compiler modules.\n   */\n  function genStaticKeys(modules) {\n    return modules.reduce(function (keys, m) {\n      return keys.concat(m.staticKeys || [])\n    }, []).join(',')\n  }\n\n  /**\n   * Check if two values are loosely equal - that is,\n   * if they are plain objects, do they have the same shape?\n   */\n  function looseEqual(a, b) {\n    if (a === b) {\n      return true\n    }\n    var isObjectA = isObject(a);\n    var isObjectB = isObject(b);\n    if (isObjectA && isObjectB) {\n      try {\n        var isArrayA = Array.isArray(a);\n        var isArrayB = Array.isArray(b);\n        if (isArrayA && isArrayB) {\n          return a.length === b.length && a.every(function (e, i) {\n            return looseEqual(e, b[i])\n          })\n        } else if (a instanceof Date && b instanceof Date) {\n          return a.getTime() === b.getTime()\n        } else if (!isArrayA && !isArrayB) {\n          var keysA = Object.keys(a);\n          var keysB = Object.keys(b);\n          return keysA.length === keysB.length && keysA.every(function (key) {\n            return looseEqual(a[key], b[key])\n          })\n        } else {\n          /* istanbul ignore next */\n          return false\n        }\n      } catch (e) {\n        /* istanbul ignore next */\n        return false\n      }\n    } else if (!isObjectA && !isObjectB) {\n      return String(a) === String(b)\n    } else {\n      return false\n    }\n  }\n\n  /**\n   * Return the first index at which a loosely equal value can be\n   * found in the array (if value is a plain object, the array must\n   * contain an object of the same shape), or -1 if it is not present.\n   */\n  function looseIndexOf(arr, val) {\n    for (var i = 0; i < arr.length; i++) {\n      if (looseEqual(arr[i], val)) {\n        return i\n      }\n    }\n    return -1\n  }\n\n  /**\n   * Ensure a function is called only once.\n   */\n  // once函数保证了这个调用函数只在系统种调用一次\n  function once(fn) {\n    var called = false;\n    return function () {\n      if (!called) {\n        called = true;\n        fn.apply(this, arguments);\n      }\n    }\n  }\n\n  var SSR_ATTR = 'data-server-rendered';\n\n  // 资源选项\n  var ASSET_TYPES = [\n    'component',\n    'directive',\n    'filter'\n  ];\n\n  var LIFECYCLE_HOOKS = [\n    'beforeCreate',\n    'created',\n    'beforeMount',\n    'mounted',\n    'beforeUpdate',\n    'updated',\n    'beforeDestroy',\n    'destroyed',\n    'activated',\n    'deactivated',\n    'errorCaptured',\n    'serverPrefetch'\n  ];\n\n  /*  */\n\n\n\n  var config = ({\n    /**\n     * Option merge strategies (used in core/util/options)\n     */\n    // $flow-disable-line\n    optionMergeStrategies: Object.create(null),\n\n    /**\n     * Whether to suppress warnings.\n     */\n    silent: false,\n\n    /**\n     * Show production mode tip message on boot?\n     */\n    productionTip: \"development\" !== 'production',\n\n    /**\n     * Whether to enable devtools\n     */\n    devtools: \"development\" !== 'production',\n\n    /**\n     * Whether to record perf\n     */\n    performance: false,\n\n    /**\n     * Error handler for watcher errors\n     */\n    errorHandler: null,\n\n    /**\n     * Warn handler for watcher warns\n     */\n    warnHandler: null,\n\n    /**\n     * Ignore certain custom elements\n     */\n    ignoredElements: [],\n\n    /**\n     * Custom user key aliases for v-on\n     */\n    // $flow-disable-line\n    keyCodes: Object.create(null),\n\n    /**\n     * Check if a tag is reserved so that it cannot be registered as a\n     * component. This is platform-dependent and may be overwritten.\n     */\n    isReservedTag: no,\n\n    /**\n     * Check if an attribute is reserved so that it cannot be used as a component\n     * prop. This is platform-dependent and may be overwritten.\n     */\n    isReservedAttr: no,\n\n    /**\n     * Check if a tag is an unknown element.\n     * Platform-dependent.\n     */\n    isUnknownElement: no,\n\n    /**\n     * Get the namespace of an element\n     */\n    getTagNamespace: noop,\n\n    /**\n     * Parse the real tag name for the specific platform.\n     */\n    parsePlatformTagName: identity,\n\n    /**\n     * Check if an attribute must be bound using property, e.g. value\n     * Platform-dependent.\n     */\n    mustUseProp: no,\n\n    /**\n     * Perform updates asynchronously. Intended to be used by Vue Test Utils\n     * This will significantly reduce performance if set to false.\n     */\n    async: true,\n\n    /**\n     * Exposed for legacy reasons\n     */\n    _lifecycleHooks: LIFECYCLE_HOOKS\n  });\n\n  /*  */\n\n  /**\n   * unicode letters used for parsing html tags, component names and property paths.\n   * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname\n   * skipping \\u10000-\\uEFFFF due to it freezing up PhantomJS\n   */\n  var unicodeRegExp = /a-zA-Z\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD/;\n\n  /**\n   * Check if a string starts with $ or _\n   */\n  function isReserved(str) {\n    var c = (str + '').charCodeAt(0);\n    return c === 0x24 || c === 0x5F\n  }\n\n  /**\n   * Define a property.\n   */\n  function def(obj, key, val, enumerable) {\n    Object.defineProperty(obj, key, {\n      value: val,\n      enumerable: !!enumerable,\n      writable: true,\n      configurable: true\n    });\n  }\n\n  /**\n   * Parse simple path.\n   */\n  var bailRE = new RegExp((\"[^\" + (unicodeRegExp.source) + \".$_\\\\d]\"));\n\n  function parsePath(path) {\n    if (bailRE.test(path)) {\n      return\n    }\n    var segments = path.split('.');\n    return function (obj) {\n      for (var i = 0; i < segments.length; i++) {\n        if (!obj) {\n          return\n        }\n        obj = obj[segments[i]];\n      }\n      return obj\n    }\n  }\n\n  /*  */\n\n  // can we use __proto__?\n  var hasProto = '__proto__' in {};\n\n  // Browser environment sniffing\n  var inBrowser = typeof window !== 'undefined';\n  var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform;\n  var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();\n  var UA = inBrowser && window.navigator.userAgent.toLowerCase();\n  var isIE = UA && /msie|trident/.test(UA);\n  var isIE9 = UA && UA.indexOf('msie 9.0') > 0;\n  var isEdge = UA && UA.indexOf('edge/') > 0;\n  var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android');\n  var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios');\n  var isChrome = UA && /chrome\\/\\d+/.test(UA) && !isEdge;\n  var isPhantomJS = UA && /phantomjs/.test(UA);\n  var isFF = UA && UA.match(/firefox\\/(\\d+)/);\n\n  // Firefox has a \"watch\" function on Object.prototype...\n  var nativeWatch = ({}).watch;\n\n  var supportsPassive = false;\n  if (inBrowser) {\n    try {\n      var opts = {};\n      Object.defineProperty(opts, 'passive', ({\n        get: function get() {\n          /* istanbul ignore next */\n          supportsPassive = true;\n        }\n      })); // https://github.com/facebook/flow/issues/285\n      window.addEventListener('test-passive', null, opts);\n    } catch (e) {}\n  }\n\n  // this needs to be lazy-evaled because vue may be required before\n  // vue-server-renderer can set VUE_ENV\n  var _isServer;\n  var isServerRendering = function () {\n    if (_isServer === undefined) {\n      /* istanbul ignore if */\n      if (!inBrowser && !inWeex && typeof global !== 'undefined') {\n        // detect presence of vue-server-renderer and avoid\n        // Webpack shimming the process\n        _isServer = global['process'] && global['process'].env.VUE_ENV === 'server';\n      } else {\n        _isServer = false;\n      }\n    }\n    return _isServer\n  };\n\n  // detect devtools\n  var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;\n\n  /* istanbul ignore next */\n  function isNative(Ctor) {\n    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())\n  }\n\n  var hasSymbol =\n    typeof Symbol !== 'undefined' && isNative(Symbol) &&\n    typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);\n\n  var _Set;\n  /* istanbul ignore if */ // $flow-disable-line\n  if (typeof Set !== 'undefined' && isNative(Set)) {\n    // use native Set when available.\n    _Set = Set;\n  } else {\n    // a non-standard Set polyfill that only works with primitive keys.\n    _Set = /*@__PURE__*/ (function () {\n      function Set() {\n        this.set = Object.create(null);\n      }\n      Set.prototype.has = function has(key) {\n        return this.set[key] === true\n      };\n      Set.prototype.add = function add(key) {\n        this.set[key] = true;\n      };\n      Set.prototype.clear = function clear() {\n        this.set = Object.create(null);\n      };\n\n      return Set;\n    }());\n  }\n\n  /*  */\n\n  var warn = noop;\n  var tip = noop;\n  var generateComponentTrace = (noop); // work around flow check\n  var formatComponentName = (noop);\n\n  {\n    var hasConsole = typeof console !== 'undefined';\n    var classifyRE = /(?:^|[-_])(\\w)/g;\n    var classify = function (str) {\n      return str\n        .replace(classifyRE, function (c) {\n          return c.toUpperCase();\n        })\n        .replace(/[-_]/g, '');\n    };\n\n    warn = function (msg, vm) {\n      var trace = vm ? generateComponentTrace(vm) : '';\n\n      if (config.warnHandler) {\n        config.warnHandler.call(null, msg, vm, trace);\n      } else if (hasConsole && (!config.silent)) {\n        console.error((\"[Vue warn]: \" + msg + trace));\n      }\n    };\n\n    tip = function (msg, vm) {\n      if (hasConsole && (!config.silent)) {\n        console.warn(\"[Vue tip]: \" + msg + (\n          vm ? generateComponentTrace(vm) : ''\n        ));\n      }\n    };\n\n    formatComponentName = function (vm, includeFile) {\n      if (vm.$root === vm) {\n        return '<Root>'\n      }\n      var options = typeof vm === 'function' && vm.cid != null ?\n        vm.options :\n        vm._isVue ?\n        vm.$options || vm.constructor.options :\n        vm;\n      var name = options.name || options._componentTag;\n      var file = options.__file;\n      if (!name && file) {\n        var match = file.match(/([^/\\\\]+)\\.vue$/);\n        name = match && match[1];\n      }\n\n      return (\n        (name ? (\"<\" + (classify(name)) + \">\") : \"<Anonymous>\") +\n        (file && includeFile !== false ? (\" at \" + file) : '')\n      )\n    };\n\n    var repeat = function (str, n) {\n      var res = '';\n      while (n) {\n        if (n % 2 === 1) {\n          res += str;\n        }\n        if (n > 1) {\n          str += str;\n        }\n        n >>= 1;\n      }\n      return res\n    };\n\n    generateComponentTrace = function (vm) {\n      if (vm._isVue && vm.$parent) {\n        var tree = [];\n        var currentRecursiveSequence = 0;\n        while (vm) {\n          if (tree.length > 0) {\n            var last = tree[tree.length - 1];\n            if (last.constructor === vm.constructor) {\n              currentRecursiveSequence++;\n              vm = vm.$parent;\n              continue\n            } else if (currentRecursiveSequence > 0) {\n              tree[tree.length - 1] = [last, currentRecursiveSequence];\n              currentRecursiveSequence = 0;\n            }\n          }\n          tree.push(vm);\n          vm = vm.$parent;\n        }\n        return '\\n\\nfound in\\n\\n' + tree\n          .map(function (vm, i) {\n            return (\"\" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm) ?\n              ((formatComponentName(vm[0])) + \"... (\" + (vm[1]) + \" recursive calls)\") :\n              formatComponentName(vm)));\n          })\n          .join('\\n')\n      } else {\n        return (\"\\n\\n(found in \" + (formatComponentName(vm)) + \")\")\n      }\n    };\n  }\n\n  /*  */\n\n  var uid = 0;\n\n  /**\n   * A dep is an observable that can have multiple\n   * directives subscribing to it.\n   */\n  var Dep = function Dep() {\n    this.id = uid++;\n    this.subs = [];\n  };\n\n  Dep.prototype.addSub = function addSub(sub) {\n    this.subs.push(sub);\n  };\n\n  Dep.prototype.removeSub = function removeSub(sub) {\n    remove(this.subs, sub);\n  };\n\n  Dep.prototype.depend = function depend() {\n    if (Dep.target) {\n      Dep.target.addDep(this);\n    }\n  };\n\n  Dep.prototype.notify = function notify() {\n    // stabilize the subscriber list first\n    var subs = this.subs.slice();\n    if (!config.async) {\n      // subs aren't sorted in scheduler if not running async\n      // we need to sort them now to make sure they fire in correct\n      // order\n      subs.sort(function (a, b) {\n        return a.id - b.id;\n      });\n    }\n    for (var i = 0, l = subs.length; i < l; i++) {\n      subs[i].update();\n    }\n  };\n\n  // The current target watcher being evaluated.\n  // This is globally unique because only one watcher\n  // can be evaluated at a time.\n  Dep.target = null;\n  var targetStack = [];\n\n  function pushTarget(target) {\n    // 将当前的watcher推到targetStack数组中，目的是为了getter方法执行后，可以恢复之前的watcher\n    targetStack.push(target);\n    Dep.target = target;\n  }\n\n  function popTarget() {\n    // 恢复原始的wacher\n    targetStack.pop();\n    Dep.target = targetStack[targetStack.length - 1];\n  }\n\n  /*  */\n\n  var VNode = function VNode(\n    tag,\n    data,\n    children,\n    text,\n    elm,\n    context,\n    componentOptions,\n    asyncFactory\n  ) {\n    this.tag = tag; // 标签\n    this.data = data; // 数据\n    this.children = children; // 子节点\n    this.text = text; // 文本节点内容\n    this.elm = elm;\n    this.ns = undefined;\n    this.context = context;\n    this.fnContext = undefined;\n    this.fnOptions = undefined;\n    this.fnScopeId = undefined;\n    this.key = data && data.key;\n    this.componentOptions = componentOptions;\n    this.componentInstance = undefined;\n    this.parent = undefined;\n    this.raw = false;\n    this.isStatic = false;\n    this.isRootInsert = true;\n    this.isComment = false;\n    this.isCloned = false;\n    this.isOnce = false;\n    this.asyncFactory = asyncFactory;\n    this.asyncMeta = undefined;\n    this.isAsyncPlaceholder = false;\n  };\n\n  var prototypeAccessors = {\n    child: {\n      configurable: true\n    }\n  };\n\n  // DEPRECATED: alias for componentInstance for backwards compat.\n  /* istanbul ignore next */\n  prototypeAccessors.child.get = function () {\n    return this.componentInstance\n  };\n\n  Object.defineProperties(VNode.prototype, prototypeAccessors);\n\n  // 创建注释vnode节点\n  var createEmptyVNode = function (text) {\n    if (text === void 0) text = '';\n\n    var node = new VNode();\n    node.text = text;\n    node.isComment = true;\n    return node\n  };\n\n  // 创建文本vnode节点\n  function createTextVNode(val) {\n    return new VNode(undefined, undefined, undefined, String(val))\n  }\n\n  // optimized shallow clone\n  // used for static nodes and slot nodes because they may be reused across\n  // multiple renders, cloning them avoids errors when DOM manipulations rely\n  // on their elm reference.\n  function cloneVNode(vnode) {\n    var cloned = new VNode(\n      vnode.tag,\n      vnode.data,\n      // #7975\n      // clone children array to avoid mutating original in case of cloning\n      // a child.\n      vnode.children && vnode.children.slice(),\n      vnode.text,\n      vnode.elm,\n      vnode.context,\n      vnode.componentOptions,\n      vnode.asyncFactory\n    );\n    cloned.ns = vnode.ns;\n    cloned.isStatic = vnode.isStatic;\n    cloned.key = vnode.key;\n    cloned.isComment = vnode.isComment;\n    cloned.fnContext = vnode.fnContext;\n    cloned.fnOptions = vnode.fnOptions;\n    cloned.fnScopeId = vnode.fnScopeId;\n    cloned.asyncMeta = vnode.asyncMeta;\n    cloned.isCloned = true;\n    return cloned\n  }\n\n  /*\n   * not type checking this file because flow doesn't play well with\n   * dynamically accessing methods on Array prototype\n   */\n\n  var arrayProto = Array.prototype;\n  var arrayMethods = Object.create(arrayProto);\n\n  var methodsToPatch = [\n    'push',\n    'pop',\n    'shift',\n    'unshift',\n    'splice',\n    'sort',\n    'reverse'\n  ];\n\n  /**\n   * Intercept mutating methods and emit events\n   */\n  methodsToPatch.forEach(function (method) {\n    // cache original method\n    var original = arrayProto[method];\n    def(arrayMethods, method, function mutator() {\n      var args = [],\n        len = arguments.length;\n      while (len--) args[len] = arguments[len];\n\n      var result = original.apply(this, args);\n      var ob = this.__ob__;\n      var inserted;\n      switch (method) {\n        case 'push':\n        case 'unshift':\n          inserted = args;\n          break\n        case 'splice':\n          inserted = args.slice(2);\n          break\n      }\n      if (inserted) {\n        ob.observeArray(inserted);\n      }\n      // notify change\n      ob.dep.notify();\n      return result\n    });\n  });\n\n  /*  */\n\n  var arrayKeys = Object.getOwnPropertyNames(arrayMethods);\n\n  /**\n   * In some cases we may want to disable observation inside a component's\n   * update computation.\n   */\n  var shouldObserve = true;\n\n  function toggleObserving(value) {\n    shouldObserve = value;\n  }\n\n  /**\n   * Observer class that is attached to each observed\n   * object. Once attached, the observer converts the target\n   * object's property keys into getter/setters that\n   * collect dependencies and dispatch updates.\n   */\n  // 观察者类，对象只要设置成拥有观察属性，则对象下的所有属性都会重写getter和setter方法，而getter，setting方法会进行依赖的收集和派发更新\n  // 一个实例一个Observer\n  var Observer = function Observer(value) {\n    this.value = value;\n    this.dep = new Dep();\n    this.vmCount = 0;\n    // 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。\n    def(value, '__ob__', this);\n    // 数组处理\n    if (Array.isArray(value)) {\n      if (hasProto) {\n        protoAugment(value, arrayMethods);\n      } else {\n        copyAugment(value, arrayMethods, arrayKeys);\n      }\n      this.observeArray(value);\n    } else {\n      // 对象处理\n      this.walk(value);\n    }\n  };\n\n  /**\n   * Walk through all properties and convert them into\n   * getter/setters. This method should only be called when\n   * value type is Object.\n   */\n  Observer.prototype.walk = function walk(obj) {\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) {\n      defineReactive###1(obj, keys[i]);\n    }\n  };\n\n  /**\n   * Observe a list of Array items.\n   */\n  Observer.prototype.observeArray = function observeArray(items) {\n    for (var i = 0, l = items.length; i < l; i++) {\n      observe(items[i]);\n    }\n  };\n\n  // helpers\n\n  /**\n   * Augment a target Object or Array by intercepting\n   * the prototype chain using __proto__\n   */\n  //直接通过原型指向的方式\n  function protoAugment(target, src) {\n    /* eslint-disable no-proto */\n    target.__proto__ = src;\n    /* eslint-enable no-proto */\n  }\n\n  /**\n   * Augment a target Object or Array by defining\n   * hidden properties.\n   */\n  /* istanbul ignore next */\n  // 通过数据代理的方式\n  function copyAugment(target, src, keys) {\n    for (var i = 0, l = keys.length; i < l; i++) {\n      var key = keys[i];\n      def(target, key, src[key]);\n    }\n  }\n\n  /**\n   * Attempt to create an observer instance for a value,\n   * returns the new observer if successfully observed,\n   * or the existing observer if the value already has one.\n   */\n  function observe(value, asRootData) {\n    if (!isObject(value) || value instanceof VNode) {\n      return\n    }\n    var ob;\n    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {\n      ob = value.__ob__;\n    } else if (\n      shouldObserve &&\n      !isServerRendering() &&\n      (Array.isArray(value) || isPlainObject(value)) &&\n      Object.isExtensible(value) &&\n      !value._isVue\n    ) {\n      ob = new Observer(value);\n    }\n    if (asRootData && ob) {\n      ob.vmCount++;\n    }\n    return ob\n  }\n\n  /**\n   * Define a reactive property on an Object.\n   * 将data, props定义成响应式对象\n   */\n  function defineReactive###1(\n    obj,\n    key,\n    val,\n    customSetter,\n    shallow\n  ) {\n    var dep = new Dep();\n\n    var property = Object.getOwnPropertyDescriptor(obj, key);\n    if (property && property.configurable === false) {\n      return\n    }\n\n    // cater for pre-defined getter/setters\n    var getter = property && property.get;\n    var setter = property && property.set;\n    if ((!getter || setter) && arguments.length === 2) {\n      val = obj[key];\n    }\n\n    var childOb = !shallow && observe(val);\n    Object.defineProperty(obj, key, {\n      enumerable: true,\n      configurable: true,\n      get: function reactiveGetter() {\n        var value = getter ? getter.call(obj) : val;\n        if (Dep.target) {\n          // 为当前watcher添加dep数据\n          dep.depend();\n          if (childOb) {\n            childOb.dep.depend();\n            if (Array.isArray(value)) {\n              dependArray(value);\n            }\n          }\n        }\n        return value\n      },\n      set: function reactiveSetter(newVal) {\n        var value = getter ? getter.call(obj) : val;\n        /* eslint-disable no-self-compare */\n        if (newVal === value || (newVal !== newVal && value !== value)) {\n          return\n        }\n        /* eslint-enable no-self-compare */\n        if (customSetter) {\n          customSetter();\n        }\n        // #7981: for accessor properties without setter\n        if (getter && !setter) {\n          return\n        }\n        if (setter) {\n          setter.call(obj, newVal);\n        } else {\n          val = newVal;\n        }\n        childOb = !shallow && observe(newVal);\n        dep.notify();\n      }\n    });\n  }\n\n  /**\n   * Set a property on an object. Adds the new property and\n   * triggers change notification if the property doesn't\n   * already exist.\n   */\n  function set(target, key, val) {\n    //target必须为非空对象\n    if (isUndef(target) || isPrimitive(target)) {\n      warn((\"Cannot set reactive property on undefined, null, or primitive value: \" + ((target))));\n    }\n    // 数组场景，调用重写的splice方法，对新添加属性收集依赖。\n    if (Array.isArray(target) && isValidArrayIndex(key)) {\n      target.length = Math.max(target.length, key);\n      target.splice(key, 1, val);\n      return val\n    }\n    // 新增对象的属性存在时，直接返回新属性，触发依赖收集\n    if (key in target && !(key in Object.prototype)) {\n      target[key] = val;\n      return val\n    }\n    // 拿到目标源的Observer 实例\n    var ob = (target).__ob__;\n    if (target._isVue || (ob && ob.vmCount)) {\n      warn(\n        'Avoid adding reactive properties to a Vue instance or its root $data ' +\n        'at runtime - declare it upfront in the data option.'\n      );\n      return val\n    }\n    // 目标源对象本身不是一个响应式对象，则不需要处理\n    if (!ob) {\n      target[key] = val;\n      return val\n    }\n    defineReactive###1(ob.value, key, val);\n    ob.dep.notify();\n    return val\n  }\n\n  /**\n   * Delete a property and trigger change if necessary.\n   */\n  function del(target, key) {\n    if (isUndef(target) || isPrimitive(target)) {\n      warn((\"Cannot delete reactive property on undefined, null, or primitive value: \" + ((target))));\n    }\n    if (Array.isArray(target) && isValidArrayIndex(key)) {\n      target.splice(key, 1);\n      return\n    }\n    var ob = (target).__ob__;\n    if (target._isVue || (ob && ob.vmCount)) {\n      warn(\n        'Avoid deleting properties on a Vue instance or its root $data ' +\n        '- just set it to null.'\n      );\n      return\n    }\n    if (!hasOwn(target, key)) {\n      return\n    }\n    delete target[key];\n    if (!ob) {\n      return\n    }\n    ob.dep.notify();\n  }\n\n  /**\n   * Collect dependencies on array elements when the array is touched, since\n   * we cannot intercept array element access like property getters.\n   */\n  function dependArray(value) {\n    for (var e = (void 0), i = 0, l = value.length; i < l; i++) {\n      e = value[i];\n      e && e.__ob__ && e.__ob__.dep.depend();\n      if (Array.isArray(e)) {\n        dependArray(e);\n      }\n    }\n  }\n\n  /*  */\n\n  /**\n   * Option overwriting strategies are functions that handle\n   * how to merge a parent option value and a child option\n   * value into the final value.\n   */\n  // 默认策略\n  var strats = config.optionMergeStrategies;\n\n  /**\n   * Options with restrictions\n   */\n  {\n    // el选项，只有vue实例上才拥有el选项，其他子组件不应该拥有el选项\n    strats.el = strats.propsData = function (parent, child, vm, key) {\n      if (!vm) {\n        warn(\n          \"option \\\"\" + key + \"\\\" can only be used during instance \" +\n          'creation with the `new` keyword.'\n        );\n      }\n      return defaultStrat(parent, child)\n    };\n  }\n\n  /**\n   * Helper that recursively merges two data objects together.\n   */\n  function mergeData(to, from) {\n    if (!from) {\n      return to\n    }\n    var key, toVal, fromVal;\n    var keys = hasSymbol ?\n      Reflect.ownKeys(from) :\n      Object.keys(from);\n\n    for (var i = 0; i < keys.length; i++) {\n      key = keys[i];\n      // in case the object is already observed...\n      if (key === '__ob__') {\n        continue\n      }\n      toVal = to[key];\n      fromVal = from[key];\n      if (!hasOwn(to, key)) {\n        set(to, key, fromVal);\n      } else if (\n        toVal !== fromVal &&\n        isPlainObject(toVal) &&\n        isPlainObject(fromVal)\n      ) {\n        mergeData(toVal, fromVal);\n      }\n    }\n    return to\n  }\n\n  /**\n   * Data\n   */\n  function mergeDataOrFn(\n    parentVal,\n    childVal,\n    vm\n  ) {\n    if (!vm) {\n      // in a Vue.extend merge, both should be functions\n      if (!childVal) {\n        return parentVal\n      }\n      if (!parentVal) {\n        return childVal\n      }\n      // when parentVal & childVal are both present,\n      // we need to return a function that returns the\n      // merged result of both functions... no need to\n      // check if parentVal is a function here because\n      // it has to be a function to pass previous merges.\n      return function mergedDataFn() {\n        return mergeData(\n          typeof childVal === 'function' ? childVal.call(this, this) : childVal,\n          typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal\n        )\n      }\n    } else {\n      return function mergedInstanceDataFn() {\n        // instance merge\n        var instanceData = typeof childVal === 'function' ?\n          childVal.call(vm, vm) :\n          childVal;\n        var defaultData = typeof parentVal === 'function' ?\n          parentVal.call(vm, vm) :\n          parentVal;\n        if (instanceData) {\n          return mergeData(instanceData, defaultData)\n        } else {\n          return defaultData\n        }\n      }\n    }\n  }\n\n  strats.data = function (\n    parentVal,\n    childVal,\n    vm\n  ) {\n    if (!vm) { // vm代表是否为Vue创建的实例，否则是子父类的关系\n      if (childVal && typeof childVal !== 'function') { // 必须保证子类的data类型是一个函数而不是一个对象\n        warn(\n          'The \"data\" option should be a function ' +\n          'that returns a per-instance value in component ' +\n          'definitions.',\n          vm\n        );\n\n        return parentVal\n      }\n      return mergeDataOrFn(parentVal, childVal)\n    }\n\n    return mergeDataOrFn(parentVal, childVal, vm)\n  };\n\n  /**\n   * Hooks and props are merged as arrays.\n   */\n  function mergeHook(\n    parentVal,\n    childVal\n  ) {\n    var res = childVal ?\n      parentVal ?\n      parentVal.concat(childVal) :\n      Array.isArray(childVal) ?\n      childVal :\n      [childVal] :\n      parentVal; // 1.如果子类和父类都拥有钩子选项，则将子类选项和父类选项合并, 2如果父类不存在钩子选项，子类存在时，则以数组形式返回子类钩子选项， 3.当子类不存在钩子选项时，则以父类选项返回。\n    return res ?\n      dedupeHooks(res) :\n      res\n  }\n  // 防止多个组件实例钩子选项不受影响\n  function dedupeHooks(hooks) {\n    var res = [];\n    for (var i = 0; i < hooks.length; i++) {\n      if (res.indexOf(hooks[i]) === -1) {\n        res.push(hooks[i]);\n      }\n    }\n    return res\n  }\n\n  LIFECYCLE_HOOKS.forEach(function (hook) {\n    strats[hook] = mergeHook; // 对生命周期钩子选项的合并都执行mergeHook策略\n  });\n\n  /**\n   * Assets\n   *\n   * When a vm is present (instance creation), we need to do\n   * a three-way merge between constructor options, instance\n   * options and parent options.\n   */\n  // 资源选项自定义合并策略\n  function mergeAssets(\n    parentVal,\n    childVal,\n    vm,\n    key\n  ) {\n    var res = Object.create(parentVal || null); // 创建一个空对象，其原型指向父类的资源选项。\n    if (childVal) {\n      assertObjectType(key, childVal, vm); // components,filters,directives选项必须为对象\n      return extend(res, childVal) // 子类选项赋值给空对象\n    } else {\n      return res\n    }\n  }\n\n  // 定义资源合并的策略\n  ASSET_TYPES.forEach(function (type) {\n    strats[type + 's'] = mergeAssets; // 定义默认策略\n  });\n\n  /**\n   * Watchers.\n   *\n   * Watchers hashes should not overwrite one\n   * another, so we merge them as arrays.\n   */\n  // watch 选项合并策略\n  strats.watch = function (\n    parentVal,\n    childVal,\n    vm,\n    key\n  ) {\n    // work around Firefox's Object.prototype.watch...\n    if (parentVal === nativeWatch) {\n      parentVal = undefined;\n    }\n    if (childVal === nativeWatch) {\n      childVal = undefined;\n    }\n    /* istanbul ignore if */\n    if (!childVal) {\n      return Object.create(parentVal || null)\n    } {\n      assertObjectType(key, childVal, vm);\n    }\n    if (!parentVal) {\n      return childVal\n    }\n    var ret = {};\n    extend(ret, parentVal);\n    for (var key$1 in childVal) {\n      var parent = ret[key$1];\n      var child = childVal[key$1];\n      if (parent && !Array.isArray(parent)) {\n        parent = [parent];\n      }\n      ret[key$1] = parent ?\n        parent.concat(child) :\n        Array.isArray(child) ? child : [child];\n    }\n    return ret\n  };\n\n  /**\n   * Other object hashes.\n   */\n\n  // 其他选项合并策略\n  strats.props =\n    strats.methods =\n    strats.inject =\n    strats.computed = function (\n      parentVal,\n      childVal,\n      vm,\n      key\n    ) {\n      if (childVal && \"development\" !== 'production') {\n        assertObjectType(key, childVal, vm);\n      }\n      if (!parentVal) {\n        return childVal\n      }\n      var ret = Object.create(null);\n      extend(ret, parentVal);\n      if (childVal) {\n        extend(ret, childVal);\n      }\n      return ret\n    };\n  strats.provide = mergeDataOrFn;\n\n  /**\n   * Default strategy.\n   */\n  // 用户自定义选项策略\n  var defaultStrat = function (parentVal, childVal) {\n    return childVal === undefined ?\n      parentVal :\n      childVal\n  };\n\n  /**\n   * Validate component names\n   */\n  // components规范检查函数\n  function checkComponents(options) {\n    for (var key in options.components) {\n      validateComponentName(key);\n    }\n  }\n\n  function validateComponentName(name) {\n    if (!new RegExp((\"^[a-zA-Z][\\\\-\\\\.0-9_\" + (unicodeRegExp.source) + \"]*$\")).test(name)) {\n      // 正则判断检测是否为非法的标签\n      warn(\n        'Invalid component name: \"' + name + '\". Component names ' +\n        'should conform to valid custom element name in html5 specification.'\n      );\n    }\n    // 不能使用Vue自身自定义的组件名，如slot, component,不能使用html的保留标签，如 h1, svg等\n    if (isBuiltInTag(name) || config.isReservedTag(name)) {\n      warn(\n        'Do not use built-in or reserved HTML elements as component ' +\n        'id: ' + name\n      );\n    }\n  }\n\n  /**\n   * Ensure all props option syntax are normalized into the\n   * Object-based format.\n   */\n  // props规范校验\n  function normalizeProps(options, vm) {\n    var props = options.props;\n    if (!props) {\n      return\n    }\n    var res = {};\n    var i, val, name;\n    // props选项数据有两种形式，一种是['a', 'b', 'c'],一种是{ a: { type: 'String', default: 'hahah' }}\n    if (Array.isArray(props)) {\n      i = props.length;\n      while (i--) {\n        val = props[i];\n        if (typeof val === 'string') {\n          name = camelize(val);\n          res[name] = {\n            type: null\n          }; // 默认将数组形式的props转换为对象形式。\n        } else {\n          // 保证是字符串\n          warn('props must be strings when using array syntax.');\n        }\n      }\n    } else if (isPlainObject(props)) {\n      for (var key in props) {\n        val = props[key];\n        name = camelize(key);\n        res[name] = isPlainObject(val) ?\n          val :\n          {\n            type: val\n          };\n      }\n    } else {\n      // 非数组，非对象则判定props选项传递非法\n      warn(\n        \"Invalid value for option \\\"props\\\": expected an Array or an Object, \" +\n        \"but got \" + (toRawType(props)) + \".\",\n        vm\n      );\n    }\n    options.props = res;\n  }\n\n  /**\n   * Normalize all injections into Object-based format\n   */\n  function normalizeInject(options, vm) {\n    var inject = options.inject;\n    if (!inject) {\n      return\n    }\n    var normalized = options.inject = {};\n    if (Array.isArray(inject)) {\n      for (var i = 0; i < inject.length; i++) {\n        normalized[inject[i]] = {\n          from: inject[i]\n        };\n      }\n    } else if (isPlainObject(inject)) {\n      for (var key in inject) {\n        var val = inject[key];\n        normalized[key] = isPlainObject(val) ?\n          extend({\n            from: key\n          }, val) :\n          {\n            from: val\n          };\n      }\n    } else {\n      warn(\n        \"Invalid value for option \\\"inject\\\": expected an Array or an Object, \" +\n        \"but got \" + (toRawType(inject)) + \".\",\n        vm\n      );\n    }\n  }\n\n  /**\n   * Normalize raw function directives into object format.\n   */\n  function normalizeDirectives(options) {\n    var dirs = options.directives;\n    if (dirs) {\n      for (var key in dirs) {\n        var def###1 = dirs[key];\n        if (typeof def###1 === 'function') {\n          dirs[key] = {\n            bind: def###1,\n            update: def###1\n          };\n        }\n      }\n    }\n  }\n\n  function assertObjectType(name, value, vm) { // 判断是否为对象\n    if (!isPlainObject(value)) {\n      warn(\n        \"Invalid value for option \\\"\" + name + \"\\\": expected an Object, \" +\n        \"but got \" + (toRawType(value)) + \".\",\n        vm\n      );\n    }\n  }\n\n  /**\n   * Merge two option objects into a new one.\n   * Core utility used in both instantiation and inheritance.\n   */\n  function mergeOptions(\n    parent,\n    child,\n    vm\n  ) {\n    {\n      checkComponents(child);\n    }\n    if (typeof child === 'function') {\n      child = child.options;\n    }\n\n    normalizeProps(child, vm);\n    normalizeInject(child, vm);\n    normalizeDirectives(child);\n\n    // Apply extends and mixins on the child options,\n    // but only if it is a raw options object that isn't\n    // the result of another mergeOptions call.\n    // Only merged options has the _base property.\n    if (!child._base) {\n      if (child.extends) {\n        parent = mergeOptions(parent, child.extends, vm);\n      }\n      if (child.mixins) {\n        for (var i = 0, l = child.mixins.length; i < l; i++) {\n          parent = mergeOptions(parent, child.mixins[i], vm);\n        }\n      }\n    }\n\n    var options = {};\n    var key;\n    for (key in parent) {\n      mergeField(key);\n    }\n    for (key in child) {\n      if (!hasOwn(parent, key)) {\n        mergeField(key);\n      }\n    }\n\n    function mergeField(key) {\n      var strat = strats[key] || defaultStrat;\n      options[key] = strat(parent[key], child[key], vm, key);\n    }\n    // console.log(options)\n    return options\n  }\n\n  /**\n   * Resolve an asset.\n   * This function is used because child instances need access\n   * to assets defined in its ancestor chain.\n   */\n  // 需要明确组件是否存在于实例options中\n  function resolveAsset(\n    options,\n    type,\n    id,\n    warnMissing\n  ) {\n    /* istanbul ignore if */\n    if (typeof id !== 'string') {\n      return\n    }\n    var assets = options[type];\n    // check local registration variations first\n    if (hasOwn(assets, id)) {\n      return assets[id]\n    }\n    var camelizedId = camelize(id);\n    if (hasOwn(assets, camelizedId)) {\n      return assets[camelizedId]\n    }\n    var PascalCaseId = capitalize(camelizedId);\n    if (hasOwn(assets, PascalCaseId)) {\n      return assets[PascalCaseId]\n    }\n    // fallback to prototype chain\n    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];\n    if (warnMissing && !res) {\n      warn(\n        'Failed to resolve ' + type.slice(0, -1) + ': ' + id,\n        options\n      );\n    }\n    return res\n  }\n\n  /*  */\n\n\n\n  function validateProp(\n    key,\n    propOptions,\n    propsData,\n    vm\n  ) {\n    var prop = propOptions[key];\n    var absent = !hasOwn(propsData, key);\n    var value = propsData[key];\n    // boolean casting\n    var booleanIndex = getTypeIndex(Boolean, prop.type);\n    if (booleanIndex > -1) {\n      if (absent && !hasOwn(prop, 'default')) {\n        value = false;\n      } else if (value === '' || value === hyphenate(key)) {\n        // only cast empty string / same name to boolean if\n        // boolean has higher priority\n        var stringIndex = getTypeIndex(String, prop.type);\n        if (stringIndex < 0 || booleanIndex < stringIndex) {\n          value = true;\n        }\n      }\n    }\n    // check default value\n    if (value === undefined) {\n      value = getPropDefaultValue(vm, prop, key);\n      // since the default value is a fresh copy,\n      // make sure to observe it.\n      var prevShouldObserve = shouldObserve;\n      toggleObserving(true);\n      observe(value);\n      toggleObserving(prevShouldObserve);\n    } {\n      assertProp(prop, key, value, vm, absent);\n    }\n    return value\n  }\n\n  /**\n   * Get the default value of a prop.\n   */\n  function getPropDefaultValue(vm, prop, key) {\n    // no default, return undefined\n    if (!hasOwn(prop, 'default')) {\n      return undefined\n    }\n    var def = prop.default;\n    // warn against non-factory defaults for Object & Array\n    if (isObject(def)) {\n      warn(\n        'Invalid default value for prop \"' + key + '\": ' +\n        'Props with type Object/Array must use a factory function ' +\n        'to return the default value.',\n        vm\n      );\n    }\n    // the raw prop value was also undefined from previous render,\n    // return previous default value to avoid unnecessary watcher trigger\n    if (vm && vm.$options.propsData &&\n      vm.$options.propsData[key] === undefined &&\n      vm._props[key] !== undefined\n    ) {\n      return vm._props[key]\n    }\n    // call factory function for non-Function types\n    // a value is Function if its prototype is function even across different execution context\n    return typeof def === 'function' && getType(prop.type) !== 'Function' ?\n      def.call(vm) :\n      def\n  }\n\n  /**\n   * Assert whether a prop is valid.\n   */\n  function assertProp(\n    prop,\n    name,\n    value,\n    vm,\n    absent\n  ) {\n    if (prop.required && absent) {\n      warn(\n        'Missing required prop: \"' + name + '\"',\n        vm\n      );\n      return\n    }\n    if (value == null && !prop.required) {\n      return\n    }\n    var type = prop.type;\n    var valid = !type || type === true;\n    var expectedTypes = [];\n    if (type) {\n      if (!Array.isArray(type)) {\n        type = [type];\n      }\n      for (var i = 0; i < type.length && !valid; i++) {\n        var assertedType = assertType(value, type[i]);\n        expectedTypes.push(assertedType.expectedType || '');\n        valid = assertedType.valid;\n      }\n    }\n\n    if (!valid) {\n      warn(\n        getInvalidTypeMessage(name, value, expectedTypes),\n        vm\n      );\n      return\n    }\n    var validator = prop.validator;\n    if (validator) {\n      if (!validator(value)) {\n        warn(\n          'Invalid prop: custom validator check failed for prop \"' + name + '\".',\n          vm\n        );\n      }\n    }\n  }\n\n  var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/;\n\n  function assertType(value, type) {\n    var valid;\n    var expectedType = getType(type);\n    if (simpleCheckRE.test(expectedType)) {\n      var t = typeof value;\n      valid = t === expectedType.toLowerCase();\n      // for primitive wrapper objects\n      if (!valid && t === 'object') {\n        valid = value instanceof type;\n      }\n    } else if (expectedType === 'Object') {\n      valid = isPlainObject(value);\n    } else if (expectedType === 'Array') {\n      valid = Array.isArray(value);\n    } else {\n      valid = value instanceof type;\n    }\n    return {\n      valid: valid,\n      expectedType: expectedType\n    }\n  }\n\n  /**\n   * Use function string name to check built-in types,\n   * because a simple equality check will fail when running\n   * across different vms / iframes.\n   */\n  function getType(fn) {\n    var match = fn && fn.toString().match(/^\\s*function (\\w+)/);\n    return match ? match[1] : ''\n  }\n\n  function isSameType(a, b) {\n    return getType(a) === getType(b)\n  }\n\n  function getTypeIndex(type, expectedTypes) {\n    if (!Array.isArray(expectedTypes)) {\n      return isSameType(expectedTypes, type) ? 0 : -1\n    }\n    for (var i = 0, len = expectedTypes.length; i < len; i++) {\n      if (isSameType(expectedTypes[i], type)) {\n        return i\n      }\n    }\n    return -1\n  }\n\n  function getInvalidTypeMessage(name, value, expectedTypes) {\n    var message = \"Invalid prop: type check failed for prop \\\"\" + name + \"\\\".\" +\n      \" Expected \" + (expectedTypes.map(capitalize).join(', '));\n    var expectedType = expectedTypes[0];\n    var receivedType = toRawType(value);\n    var expectedValue = styleValue(value, expectedType);\n    var receivedValue = styleValue(value, receivedType);\n    // check if we need to specify expected value\n    if (expectedTypes.length === 1 &&\n      isExplicable(expectedType) &&\n      !isBoolean(expectedType, receivedType)) {\n      message += \" with value \" + expectedValue;\n    }\n    message += \", got \" + receivedType + \" \";\n    // check if we need to specify received value\n    if (isExplicable(receivedType)) {\n      message += \"with value \" + receivedValue + \".\";\n    }\n    return message\n  }\n\n  function styleValue(value, type) {\n    if (type === 'String') {\n      return (\"\\\"\" + value + \"\\\"\")\n    } else if (type === 'Number') {\n      return (\"\" + (Number(value)))\n    } else {\n      return (\"\" + value)\n    }\n  }\n\n  function isExplicable(value) {\n    var explicitTypes = ['string', 'number', 'boolean'];\n    return explicitTypes.some(function (elem) {\n      return value.toLowerCase() === elem;\n    })\n  }\n\n  function isBoolean() {\n    var args = [],\n      len = arguments.length;\n    while (len--) args[len] = arguments[len];\n\n    return args.some(function (elem) {\n      return elem.toLowerCase() === 'boolean';\n    })\n  }\n\n  /*  */\n\n  function handleError(err, vm, info) {\n    // Deactivate deps tracking while processing error handler to avoid possible infinite rendering.\n    // See: https://github.com/vuejs/vuex/issues/1505\n    pushTarget();\n    try {\n      if (vm) {\n        var cur = vm;\n        while ((cur = cur.$parent)) {\n          var hooks = cur.$options.errorCaptured;\n          if (hooks) {\n            for (var i = 0; i < hooks.length; i++) {\n              try {\n                var capture = hooks[i].call(cur, err, vm, info) === false;\n                if (capture) {\n                  return\n                }\n              } catch (e) {\n                globalHandleError(e, cur, 'errorCaptured hook');\n              }\n            }\n          }\n        }\n      }\n      globalHandleError(err, vm, info);\n    } finally {\n      popTarget();\n    }\n  }\n\n  function invokeWithErrorHandling(\n    handler,\n    context,\n    args,\n    vm,\n    info\n  ) {\n    var res;\n    try {\n      res = args ? handler.apply(context, args) : handler.call(context);\n      if (res && !res._isVue && isPromise(res)) {\n        // issue #9511\n        // reassign to res to avoid catch triggering multiple times when nested calls\n        // 当生命周期钩子函数内部执行返回promise对象是，如果捕获异常，则会对异常信息做一层包装返回\n        res = res.catch(function (e) {\n          return handleError(e, vm, info + \" (Promise/async)\");\n        });\n      }\n    } catch (e) {\n      handleError(e, vm, info);\n    }\n    return res\n  }\n\n  function globalHandleError(err, vm, info) {\n    if (config.errorHandler) {\n      try {\n        return config.errorHandler.call(null, err, vm, info)\n      } catch (e) {\n        // if the user intentionally throws the original error in the handler,\n        // do not log it twice\n        if (e !== err) {\n          logError(e, null, 'config.errorHandler');\n        }\n      }\n    }\n    logError(err, vm, info);\n  }\n\n  function logError(err, vm, info) {\n    {\n      warn((\"Error in \" + info + \": \\\"\" + (err.toString()) + \"\\\"\"), vm);\n    }\n    /* istanbul ignore else */\n    if ((inBrowser || inWeex) && typeof console !== 'undefined') {\n      console.error(err);\n    } else {\n      throw err\n    }\n  }\n\n  /*  */\n\n  var isUsingMicroTask = false;\n\n  var callbacks = [];\n  var pending = false;\n\n  function flushCallbacks() {\n    pending = false;\n    var copies = callbacks.slice(0);\n    // 取出callbacks数组的每一个任务，执行任务\n    callbacks.length = 0;\n    for (var i = 0; i < copies.length; i++) {\n      copies[i]();\n    }\n  }\n\n  // Here we have async deferring wrappers using microtasks.\n  // In 2.5 we used (macro) tasks (in combination with microtasks).\n  // However, it has subtle problems when state is changed right before repaint\n  // (e.g. #6813, out-in transitions).\n  // Also, using (macro) tasks in event handler would cause some weird behaviors\n  // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).\n  // So we now use microtasks everywhere, again.\n  // A major drawback of this tradeoff is that there are some scenarios\n  // where microtasks have too high a priority and fire in between supposedly\n  // sequential events (e.g. #4521, #6690, which have workarounds)\n  // or even between bubbling of the same event (#6566).\n  var timerFunc;\n\n  // The nextTick behavior leverages the microtask queue, which can be accessed\n  // via either native Promise.then or MutationObserver.\n  // MutationObserver has wider support, however it is seriously bugged in\n  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It\n  // completely stops working after triggering a few times... so, if native\n  // Promise is available, we will use it:\n  /* istanbul ignore next, $flow-disable-line */\n  if (typeof Promise !== 'undefined' && isNative(Promise)) {\n    var p = Promise.resolve();\n    timerFunc = function () {\n      p.then(flushCallbacks);\n      // In problematic UIWebViews, Promise.then doesn't completely break, but\n      // it can get stuck in a weird state where callbacks are pushed into the\n      // microtask queue but the queue isn't being flushed, until the browser\n      // needs to do some other work, e.g. handle a timer. Therefore we can\n      // \"force\" the microtask queue to be flushed by adding an empty timer.\n      if (isIOS) {\n        setTimeout(noop);\n      }\n    };\n    isUsingMicroTask = true;\n  } else if (!isIE && typeof MutationObserver !== 'undefined' && (\n      isNative(MutationObserver) ||\n      // PhantomJS and iOS 7.x\n      MutationObserver.toString() === '[object MutationObserverConstructor]'\n    )) {\n    // Use MutationObserver where native Promise is not available,\n    // e.g. PhantomJS, iOS7, Android 4.4\n    // (#6466 MutationObserver is unreliable in IE11)\n    var counter = 1;\n    var observer = new MutationObserver(flushCallbacks);\n    var textNode = document.createTextNode(String(counter));\n    observer.observe(textNode, {\n      characterData: true\n    });\n    timerFunc = function () {\n      counter = (counter + 1) % 2;\n      textNode.data = String(counter);\n    };\n    isUsingMicroTask = true;\n  } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {\n    // Fallback to setImmediate.\n    // Techinically it leverages the (macro) task queue,\n    // but it is still a better choice than setTimeout.\n    timerFunc = function () {\n      setImmediate(flushCallbacks);\n    };\n  } else {\n    // Fallback to setTimeout.\n    timerFunc = function () {\n      setTimeout(flushCallbacks, 0);\n    };\n  }\n\n  function nextTick(cb, ctx) {\n    var _resolve;\n    callbacks.push(function () {\n      if (cb) {\n        try {\n          cb.call(ctx);\n        } catch (e) {\n          handleError(e, ctx, 'nextTick');\n        }\n      } else if (_resolve) {\n        _resolve(ctx);\n      }\n    });\n    if (!pending) {\n      pending = true;\n      timerFunc();\n    }\n    // $flow-disable-line\n    if (!cb && typeof Promise !== 'undefined') {\n      return new Promise(function (resolve) {\n        _resolve = resolve;\n      })\n    }\n  }\n\n  /*  */\n\n  var mark;\n  var measure;\n\n  {\n    var perf = inBrowser && window.performance;\n    /* istanbul ignore if */\n    if (\n      perf &&\n      perf.mark &&\n      perf.measure &&\n      perf.clearMarks &&\n      perf.clearMeasures\n    ) {\n      mark = function (tag) {\n        return perf.mark(tag);\n      };\n      measure = function (name, startTag, endTag) {\n        perf.measure(name, startTag, endTag);\n        perf.clearMarks(startTag);\n        perf.clearMarks(endTag);\n        // perf.clearMeasures(name)\n      };\n    }\n  }\n\n  /* not type checking this file because flow doesn't play well with Proxy */\n\n  var initProxy;\n\n  {\n    var allowedGlobals = makeMap(\n      'Infinity,undefined,NaN,isFinite,isNaN,' +\n      'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +\n      'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +\n      'require' // for Webpack/Browserify\n    );\n\n    var warnNonPresent = function (target, key) {\n      warn(\n        \"Property or method \\\"\" + key + \"\\\" is not defined on the instance but \" +\n        'referenced during render. Make sure that this property is reactive, ' +\n        'either in the data option, or for class-based components, by ' +\n        'initializing the property. ' +\n        'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',\n        target\n      );\n    };\n\n    var warnReservedPrefix = function (target, key) {\n      warn(\n        \"Property \\\"\" + key + \"\\\" must be accessed with \\\"$data.\" + key + \"\\\" because \" +\n        'properties starting with \"$\" or \"_\" are not proxied in the Vue instance to ' +\n        'prevent conflicts with Vue internals' +\n        'See: https://vuejs.org/v2/api/#data',\n        target\n      );\n    };\n\n    var hasProxy =\n      typeof Proxy !== 'undefined' && isNative(Proxy);\n\n    if (hasProxy) {\n      var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact');\n      config.keyCodes = new Proxy(config.keyCodes, {\n        set: function set(target, key, value) {\n          if (isBuiltInModifier(key)) {\n\n            warn((\"Avoid overwriting built-in modifier in config.keyCodes: .\" + key));\n            return false\n          } else {\n            target[key] = value;\n            return true\n          }\n        }\n      });\n    }\n\n    var hasHandler = {\n      // key in obj或者with作用域时，会触发has的钩子\n      has: function has(target, key) {\n        var has = key in target;\n        var isAllowed = allowedGlobals(key) ||\n          (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));\n        if (!has && !isAllowed) {\n          if (key in target.$data) {\n            warnReservedPrefix(target, key);\n          } else {\n            warnNonPresent(target, key);\n          }\n        }\n        return has || !isAllowed\n      }\n    };\n\n    var getHandler = {\n      get: function get(target, key) {\n        if (typeof key === 'string' && !(key in target)) {\n          if (key in target.$data) {\n            warnReservedPrefix(target, key);\n          } else {\n            warnNonPresent(target, key);\n          }\n        }\n        return target[key]\n      }\n    };\n\n    initProxy = function initProxy(vm) {\n      if (hasProxy) {\n        // \n        var options = vm.$options;\n        var handlers = options.render && options.render._withStripped ?\n          getHandler :\n          hasHandler;\n        vm._renderProxy = new Proxy(vm, handlers);\n      } else {\n        vm._renderProxy = vm;\n      }\n    };\n  }\n\n  /*  */\n\n  var seenObjects = new _Set();\n\n  /**\n   * Recursively traverse an object to evoke all converted\n   * getters, so that every nested property inside the object\n   * is collected as a \"deep\" dependency.\n   */\n  function traverse(val) {\n    _traverse(val, seenObjects);\n    seenObjects.clear();\n  }\n\n  function _traverse(val, seen) {\n    var i, keys;\n    var isA = Array.isArray(val);\n    if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {\n      return\n    }\n    if (val.__ob__) {\n      var depId = val.__ob__.dep.id;\n      if (seen.has(depId)) {\n        return\n      }\n      seen.add(depId);\n    }\n    if (isA) {\n      i = val.length;\n      while (i--) {\n        _traverse(val[i], seen);\n      }\n    } else {\n      keys = Object.keys(val);\n      i = keys.length;\n      while (i--) {\n        _traverse(val[keys[i]], seen);\n      }\n    }\n  }\n\n  /*  */\n\n  var normalizeEvent = cached(function (name) {\n    var passive = name.charAt(0) === '&';\n    name = passive ? name.slice(1) : name;\n    var once###1 = name.charAt(0) === '~'; // Prefixed last, checked first\n    name = once###1 ? name.slice(1) : name;\n    var capture = name.charAt(0) === '!';\n    name = capture ? name.slice(1) : name;\n    return {\n      name: name,\n      once: once###1,\n      capture: capture,\n      passive: passive\n    }\n  });\n\n  function createFnInvoker(fns, vm) {\n    function invoker() {\n      var arguments$1 = arguments;\n\n      var fns = invoker.fns;\n      // fns是多个回调函数组成的数组\n      if (Array.isArray(fns)) {\n        var cloned = fns.slice();\n        for (var i = 0; i < cloned.length; i++) {\n          // 遍历执行真正的回调函数\n          invokeWithErrorHandling(cloned[i], null, arguments$1, vm, \"v-on handler\");\n        }\n      } else {\n        // return handler return value for single handlers\n        return invokeWithErrorHandling(fns, null, arguments, vm, \"v-on handler\")\n      }\n    }\n    invoker.fns = fns;\n    // 最终事件执行的回调函数\n    return invoker\n  }\n\n  function updateListeners(\n    on,\n    oldOn,\n    add,\n    remove###1,\n    createOnceHandler,\n    vm\n  ) {\n    var name, def###1, cur, old, event;\n    // 遍历事件\n    for (name in on) {\n      def###1 = cur = on[name];\n      old = oldOn[name];\n      event = normalizeEvent(name);\n      if (isUndef(cur)) {\n        // 事件名非法的报错处理\n        warn(\n          \"Invalid handler for event \\\"\" + (event.name) + \"\\\": got \" + String(cur),\n          vm\n        );\n      } else if (isUndef(old)) {\n        // 旧节点不存在\n        if (isUndef(cur.fns)) {\n          // createFunInvoker返回事件最终执行的回调函数\n          cur = on[name] = createFnInvoker(cur, vm);\n        }\n        // 只触发一次的事件\n        if (isTrue(event.once)) {\n          cur = on[name] = createOnceHandler(event.name, cur, event.capture);\n        }\n        // 执行真正注册事件的执行函数\n        add(event.name, cur, event.capture, event.passive, event.params);\n      } else if (cur !== old) {\n        old.fns = cur;\n        on[name] = old;\n      }\n    }\n    // 旧节点存在，接触旧节点上的绑定事件\n    for (name in oldOn) {\n      if (isUndef(on[name])) {\n        event = normalizeEvent(name);\n        remove###1(event.name, oldOn[name], event.capture);\n      }\n    }\n  }\n\n  /*  */\n\n  function mergeVNodeHook(def, hookKey, hook) {\n    if (def instanceof VNode) {\n      def = def.data.hook || (def.data.hook = {});\n    }\n    var invoker;\n    var oldHook = def[hookKey];\n\n    function wrappedHook() {\n      hook.apply(this, arguments);\n      // important: remove merged hook to ensure it's called only once\n      // and prevent memory leak\n      remove(invoker.fns, wrappedHook);\n    }\n\n    if (isUndef(oldHook)) {\n      // no existing hook\n      invoker = createFnInvoker([wrappedHook]);\n    } else {\n      /* istanbul ignore if */\n      if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {\n        // already a merged invoker\n        invoker = oldHook;\n        invoker.fns.push(wrappedHook);\n      } else {\n        // existing plain hook\n        invoker = createFnInvoker([oldHook, wrappedHook]);\n      }\n    }\n\n    invoker.merged = true;\n    def[hookKey] = invoker;\n  }\n\n  /*  */\n\n  function extractPropsFromVNodeData(\n    data,\n    Ctor,\n    tag\n  ) {\n    // we are only extracting raw values here.\n    // validation and default values are handled in the child\n    // component itself.\n    var propOptions = Ctor.options.props;\n    if (isUndef(propOptions)) {\n      return\n    }\n    var res = {};\n    // data.attrs针对编译生成的render函数，data.props针对用户自定义的render函数\n    var attrs = data.attrs;\n    var props = data.props;\n    if (isDef(attrs) || isDef(props)) {\n      for (var key in propOptions) {\n        var altKey = hyphenate(key); {\n          var keyInLowerCase = key.toLowerCase();\n          if (\n            key !== keyInLowerCase &&\n            attrs && hasOwn(attrs, keyInLowerCase)\n          ) {\n            // HTML 中的特性名是大小写不敏感的，所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时，camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命\n            tip(\n              \"Prop \\\"\" + keyInLowerCase + \"\\\" is passed to component \" +\n              (formatComponentName(tag || Ctor)) + \", but the declared prop name is\" +\n              \" \\\"\" + key + \"\\\". \" +\n              \"Note that HTML attributes are case-insensitive and camelCased \" +\n              \"props need to use their kebab-case equivalents when using in-DOM \" +\n              \"templates. You should probably use \\\"\" + altKey + \"\\\" instead of \\\"\" + key + \"\\\".\"\n            );\n          }\n        }\n        checkProp(res, props, key, altKey, true) ||\n          checkProp(res, attrs, key, altKey, false);\n      }\n    }\n    return res\n  }\n\n  function checkProp(\n    res,\n    hash,\n    key,\n    altKey,\n    preserve\n  ) {\n    if (isDef(hash)) {\n      if (hasOwn(hash, key)) {\n        res[key] = hash[key];\n        if (!preserve) {\n          delete hash[key];\n        }\n        return true\n      } else if (hasOwn(hash, altKey)) {\n        res[key] = hash[altKey];\n        if (!preserve) {\n          delete hash[altKey];\n        }\n        return true\n      }\n    }\n    return false\n  }\n\n  /*  */\n\n  // The template compiler attempts to minimize the need for normalization by\n  // statically analyzing the template at compile time.\n  //\n  // For plain HTML markup, normalization can be completely skipped because the\n  // generated render function is guaranteed to return Array<VNode>. There are\n  // two cases where extra normalization is needed:\n\n  // 1. When the children contains components - because a functional component\n  // may return an Array instead of a single root. In this case, just a simple\n  // normalization is needed - if any child is an Array, we flatten the whole\n  // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep\n  // because functional components already normalize their own children.\n  function simpleNormalizeChildren(children) {\n    for (var i = 0; i < children.length; i++) {\n      if (Array.isArray(children[i])) {\n        return Array.prototype.concat.apply([], children)\n      }\n    }\n    return children\n  }\n\n  // 2. When the children contains constructs that always generated nested Arrays,\n  // e.g. <template>, <slot>, v-for, or when the children is provided by user\n  // with hand-written render functions / JSX. In such cases a full normalization\n  // is needed to cater to all possible types of children values.\\\n  function normalizeChildren(children) {\n    return isPrimitive(children) ?\n      [createTextVNode(children)] :\n      Array.isArray(children) ?\n      normalizeArrayChildren(children) :\n      undefined\n  }\n\n  function isTextNode(node) {\n    return isDef(node) && isDef(node.text) && isFalse(node.isComment)\n  }\n\n  function normalizeArrayChildren(children, nestedIndex) {\n    var res = [];\n    var i, c, lastIndex, last;\n    for (i = 0; i < children.length; i++) {\n      c = children[i];\n      if (isUndef(c) || typeof c === 'boolean') {\n        continue\n      }\n      lastIndex = res.length - 1;\n      last = res[lastIndex];\n      //  nested\n      if (Array.isArray(c)) {\n        if (c.length > 0) {\n          c = normalizeArrayChildren(c, ((nestedIndex || '') + \"_\" + i));\n          // merge adjacent text nodes\n          if (isTextNode(c[0]) && isTextNode(last)) {\n            res[lastIndex] = createTextVNode(last.text + (c[0]).text);\n            c.shift();\n          }\n          res.push.apply(res, c);\n        }\n      } else if (isPrimitive(c)) {\n        if (isTextNode(last)) {\n          // merge adjacent text nodes\n          // this is necessary for SSR hydration because text nodes are\n          // essentially merged when rendered to HTML strings\n          res[lastIndex] = createTextVNode(last.text + c);\n        } else if (c !== '') {\n          // convert primitive to vnode\n          res.push(createTextVNode(c));\n        }\n      } else {\n        if (isTextNode(c) && isTextNode(last)) {\n          // merge adjacent text nodes\n          res[lastIndex] = createTextVNode(last.text + c.text);\n        } else {\n          // default key for nested array children (likely generated by v-for)\n          if (isTrue(children._isVList) &&\n            isDef(c.tag) &&\n            isUndef(c.key) &&\n            isDef(nestedIndex)) {\n            c.key = \"__vlist\" + nestedIndex + \"_\" + i + \"__\";\n          }\n          res.push(c);\n        }\n      }\n    }\n    return res\n  }\n\n  /*  */\n\n  function initProvide(vm) {\n    var provide = vm.$options.provide;\n    if (provide) {\n      vm._provided = typeof provide === 'function' ?\n        provide.call(vm) :\n        provide;\n    }\n  }\n\n  function initInjections(vm) {\n    var result = resolveInject(vm.$options.inject, vm);\n    if (result) {\n      toggleObserving(false);\n      Object.keys(result).forEach(function (key) {\n        /* istanbul ignore else */\n        {\n          defineReactive###1(vm, key, result[key], function () {\n            warn(\n              \"Avoid mutating an injected value directly since the changes will be \" +\n              \"overwritten whenever the provided component re-renders. \" +\n              \"injection being mutated: \\\"\" + key + \"\\\"\",\n              vm\n            );\n          });\n        }\n      });\n      toggleObserving(true);\n    }\n  }\n\n  function resolveInject(inject, vm) {\n    if (inject) {\n      // inject is :any because flow is not smart enough to figure out cached\n      var result = Object.create(null);\n      var keys = hasSymbol ?\n        Reflect.ownKeys(inject) :\n        Object.keys(inject);\n\n      for (var i = 0; i < keys.length; i++) {\n        var key = keys[i];\n        // #6574 in case the inject object is observed...\n        if (key === '__ob__') {\n          continue\n        }\n        var provideKey = inject[key].from;\n        var source = vm;\n        while (source) {\n          if (source._provided && hasOwn(source._provided, provideKey)) {\n            result[key] = source._provided[provideKey];\n            break\n          }\n          source = source.$parent;\n        }\n        if (!source) {\n          if ('default' in inject[key]) {\n            var provideDefault = inject[key].default;\n            result[key] = typeof provideDefault === 'function' ?\n              provideDefault.call(vm) :\n              provideDefault;\n          } else {\n            warn((\"Injection \\\"\" + key + \"\\\" not found\"), vm);\n          }\n        }\n      }\n      return result\n    }\n  }\n\n  /*  */\n\n\n\n  /**\n   * Runtime helper for resolving raw children VNodes into a slot object.\n   */\n  function resolveSlots(\n    children,\n    context\n  ) {\n    if (!children || !children.length) {\n      return {}\n    }\n    var slots = {};\n    for (var i = 0, l = children.length; i < l; i++) {\n      var child = children[i];\n      var data = child.data;\n      // remove slot attribute if the node is resolved as a Vue slot node\n      if (data && data.attrs && data.attrs.slot) {\n        delete data.attrs.slot;\n      }\n      // named slots should only be respected if the vnode was rendered in the\n      // same context.\n      if ((child.context === context || child.fnContext === context) &&\n        data && data.slot != null\n      ) {\n        var name = data.slot;\n        var slot = (slots[name] || (slots[name] = []));\n        if (child.tag === 'template') {\n          slot.push.apply(slot, child.children || []);\n        } else {\n          slot.push(child);\n        }\n      } else {\n        (slots.default || (slots.default = [])).push(child);\n      }\n    }\n    // ignore slots that contains only whitespace\n    for (var name$1 in slots) {\n      if (slots[name$1].every(isWhitespace)) {\n        delete slots[name$1];\n      }\n    }\n    return slots\n  }\n\n  function isWhitespace(node) {\n    return (node.isComment && !node.asyncFactory) || node.text === ' '\n  }\n\n  /*  */\n\n  function normalizeScopedSlots(\n    slots,\n    normalSlots,\n    prevSlots\n  ) {\n    var res;\n    var isStable = slots ? !!slots.$stable : true;\n    var key = slots && slots.$key;\n    if (!slots) {\n      res = {};\n    } else if (slots._normalized) {\n      // fast path 1: child component re-render only, parent did not change\n      return slots._normalized\n    } else if (\n      isStable &&\n      prevSlots &&\n      prevSlots !== emptyObject &&\n      key === prevSlots.$key &&\n      Object.keys(normalSlots).length === 0\n    ) {\n      // fast path 2: stable scoped slots w/ no normal slots to proxy,\n      // only need to normalize once\n      return prevSlots\n    } else {\n      res = {};\n      for (var key$1 in slots) {\n        if (slots[key$1] && key$1[0] !== '$') {\n          res[key$1] = normalizeScopedSlot(normalSlots, key$1, slots[key$1]);\n        }\n      }\n    }\n    // expose normal slots on scopedSlots\n    for (var key$2 in normalSlots) {\n      if (!(key$2 in res)) {\n        res[key$2] = proxyNormalSlot(normalSlots, key$2);\n      }\n    }\n    // avoriaz seems to mock a non-extensible $scopedSlots object\n    // and when that is passed down this would cause an error\n    if (slots && Object.isExtensible(slots)) {\n      (slots)._normalized = res;\n    }\n    def(res, '$stable', isStable);\n    def(res, '$key', key);\n    return res\n  }\n\n  function normalizeScopedSlot(normalSlots, key, fn) {\n    var normalized = function () {\n      var res = arguments.length ? fn.apply(null, arguments) : fn({});\n      res = res && typeof res === 'object' && !Array.isArray(res) ?\n        [res] // single vnode\n        :\n        normalizeChildren(res);\n      return res && res.length === 0 ?\n        undefined :\n        res\n    };\n    // this is a slot using the new v-slot syntax without scope. although it is\n    // compiled as a scoped slot, render fn users would expect it to be present\n    // on this.$slots because the usage is semantically a normal slot.\n    if (fn.proxy) {\n      Object.defineProperty(normalSlots, key, {\n        get: normalized,\n        enumerable: true,\n        configurable: true\n      });\n    }\n    return normalized\n  }\n\n  function proxyNormalSlot(slots, key) {\n    return function () {\n      return slots[key];\n    }\n  }\n\n  /*  */\n\n  /**\n   * Runtime helper for rendering v-for lists.\n   */\n  function renderList(\n    val,\n    render\n  ) {\n    var ret, i, l, keys, key;\n    if (Array.isArray(val) || typeof val === 'string') {\n      ret = new Array(val.length);\n      for (i = 0, l = val.length; i < l; i++) {\n        ret[i] = render(val[i], i);\n      }\n    } else if (typeof val === 'number') {\n      ret = new Array(val);\n      for (i = 0; i < val; i++) {\n        ret[i] = render(i + 1, i);\n      }\n    } else if (isObject(val)) {\n      if (hasSymbol && val[Symbol.iterator]) {\n        ret = [];\n        var iterator = val[Symbol.iterator]();\n        var result = iterator.next();\n        while (!result.done) {\n          ret.push(render(result.value, ret.length));\n          result = iterator.next();\n        }\n      } else {\n        keys = Object.keys(val);\n        ret = new Array(keys.length);\n        for (i = 0, l = keys.length; i < l; i++) {\n          key = keys[i];\n          ret[i] = render(val[key], key, i);\n        }\n      }\n    }\n    if (!isDef(ret)) {\n      ret = [];\n    }\n    (ret)._isVList = true;\n    return ret\n  }\n\n  /*  */\n\n  /**\n   * Runtime helper for rendering <slot>\n   */\n  // 渲染slot组件内容\n  function renderSlot(\n    name,\n    fallback, // slot插槽后备内容\n    props, // 子传给父的值\n    bindObject\n  ) {\n    // scopedSlotFn拿到父组件插槽的执行函数，默认slotname为default\n    var scopedSlotFn = this.$scopedSlots[name];\n    var nodes;\n    // 针对具名插槽，特点是$scopedSlots有值\n    if (scopedSlotFn) { // scoped slot\n      props = props || {};\n      if (bindObject) {\n        if (!isObject(bindObject)) {\n          warn(\n            'slot v-bind without argument expects an Object',\n            this\n          );\n        }\n        props = extend(extend({}, bindObject), props);\n      }\n      // 执行时将子组件传递给父组件的值传入fn\n      nodes = scopedSlotFn(props) || fallback;\n    } else {\n      // 如果父占位符组件没有插槽内容，this.$slots不会有值，此时vnode节点为后备内容节点。\n      nodes = this.$slots[name] || fallback;\n    }\n\n    var target = props && props.slot;\n    if (target) {\n      return this.$createElement('template', {\n        slot: target\n      }, nodes)\n    } else {\n      return nodes\n    }\n  }\n\n  /*  */\n\n  /**\n   * Runtime helper for resolving filters\n   */\n  function resolveFilter(id) {\n    return resolveAsset(this.$options, 'filters', id, true) || identity\n  }\n\n  /*  */\n\n  function isKeyNotMatch(expect, actual) {\n    if (Array.isArray(expect)) {\n      return expect.indexOf(actual) === -1\n    } else {\n      return expect !== actual\n    }\n  }\n\n  /**\n   * Runtime helper for checking keyCodes from config.\n   * exposed as Vue.prototype._k\n   * passing in eventKeyName as last argument separately for backwards compat\n   */\n  function checkKeyCodes(\n    eventKeyCode,\n    key,\n    builtInKeyCode,\n    eventKeyName,\n    builtInKeyName\n  ) {\n    var mappedKeyCode = config.keyCodes[key] || builtInKeyCode;\n    if (builtInKeyName && eventKeyName && !config.keyCodes[key]) {\n      return isKeyNotMatch(builtInKeyName, eventKeyName)\n    } else if (mappedKeyCode) {\n      return isKeyNotMatch(mappedKeyCode, eventKeyCode)\n    } else if (eventKeyName) {\n      return hyphenate(eventKeyName) !== key\n    }\n  }\n\n  /*  */\n\n  /**\n   * Runtime helper for merging v-bind=\"object\" into a VNode's data.\n   */\n  function bindObjectProps(\n    data,\n    tag,\n    value,\n    asProp,\n    isSync\n  ) {\n    if (value) {\n      if (!isObject(value)) {\n        warn(\n          'v-bind without argument expects an Object or Array value',\n          this\n        );\n      } else {\n        if (Array.isArray(value)) {\n          value = toObject(value);\n        }\n        var hash;\n        var loop = function (key) {\n          if (\n            key === 'class' ||\n            key === 'style' ||\n            isReservedAttribute(key)\n          ) {\n            hash = data;\n          } else {\n            var type = data.attrs && data.attrs.type;\n            hash = asProp || config.mustUseProp(tag, type, key) ?\n              data.domProps || (data.domProps = {}) :\n              data.attrs || (data.attrs = {});\n          }\n          var camelizedKey = camelize(key);\n          if (!(key in hash) && !(camelizedKey in hash)) {\n            hash[key] = value[key];\n\n            if (isSync) {\n              var on = data.on || (data.on = {});\n              on[(\"update:\" + camelizedKey)] = function ($event) {\n                value[key] = $event;\n              };\n            }\n          }\n        };\n\n        for (var key in value) loop(key);\n      }\n    }\n    return data\n  }\n\n  /*  */\n\n  /**\n   * Runtime helper for rendering static trees.\n   */\n  function renderStatic(\n    index,\n    isInFor\n  ) {\n    var cached = this._staticTrees || (this._staticTrees = []);\n    var tree = cached[index];\n    // if has already-rendered static tree and not inside v-for,\n    // we can reuse the same tree.\n    if (tree && !isInFor) {\n      return tree\n    }\n    // otherwise, render a fresh tree.\n    tree = cached[index] = this.$options.staticRenderFns[index].call(\n      this._renderProxy,\n      null,\n      this // for render fns generated for functional component templates\n    );\n    markStatic(tree, (\"__static__\" + index), false);\n    return tree\n  }\n\n  /**\n   * Runtime helper for v-once.\n   * Effectively it means marking the node as static with a unique key.\n   */\n  function markOnce(\n    tree,\n    index,\n    key\n  ) {\n    markStatic(tree, (\"__once__\" + index + (key ? (\"_\" + key) : \"\")), true);\n    return tree\n  }\n\n  function markStatic(\n    tree,\n    key,\n    isOnce\n  ) {\n    if (Array.isArray(tree)) {\n      for (var i = 0; i < tree.length; i++) {\n        if (tree[i] && typeof tree[i] !== 'string') {\n          markStaticNode(tree[i], (key + \"_\" + i), isOnce);\n        }\n      }\n    } else {\n      markStaticNode(tree, key, isOnce);\n    }\n  }\n\n  function markStaticNode(node, key, isOnce) {\n    node.isStatic = true;\n    node.key = key;\n    node.isOnce = isOnce;\n  }\n\n  /*  */\n\n  function bindObjectListeners(data, value) {\n    if (value) {\n      if (!isPlainObject(value)) {\n        warn(\n          'v-on without argument expects an Object value',\n          this\n        );\n      } else {\n        var on = data.on = data.on ? extend({}, data.on) : {};\n        for (var key in value) {\n          var existing = on[key];\n          var ours = value[key];\n          on[key] = existing ? [].concat(existing, ours) : ours;\n        }\n      }\n    }\n    return data\n  }\n\n  /*  */\n  // vnode生成阶段针对具名插槽的处理 _u\n  function resolveScopedSlots(\n    fns, // see flow/vnode\n    res,\n    // the following are added in 2.6\n    hasDynamicKeys,\n    contentHashKey\n  ) {\n    res = res || {\n      $stable: !hasDynamicKeys\n    };\n    for (var i = 0; i < fns.length; i++) {\n      var slot = fns[i];\n      if (Array.isArray(slot)) {\n        resolveScopedSlots(slot, res, hasDynamicKeys);\n      } else if (slot) {\n        // marker for reverse proxying v-slot without scope on this.$slots\n        if (slot.proxy) {\n          slot.fn.proxy = true;\n        }\n        res[slot.key] = slot.fn;\n      }\n    }\n    if (contentHashKey) {\n      (res).$key = contentHashKey;\n    }\n    return res\n  }\n  /* 最终的vnode节点的data属性上多了scopedSlots的属性\n  {\n    scopedSlots: [{\n      'header': fn\n    }]\n  }\n  */\n\n  /*  */\n\n  function bindDynamicKeys(baseObj, values) {\n    for (var i = 0; i < values.length; i += 2) {\n      var key = values[i];\n      if (typeof key === 'string' && key) {\n        baseObj[values[i]] = values[i + 1];\n      } else if (key !== '' && key !== null) {\n        // null is a speical value for explicitly removing a binding\n        warn(\n          (\"Invalid value for dynamic directive argument (expected string or null): \" + key),\n          this\n        );\n      }\n    }\n    return baseObj\n  }\n\n  // helper to dynamically append modifier runtime markers to event names.\n  // ensure only append when value is already string, otherwise it will be cast\n  // to string and cause the type check to miss.\n  function prependModifier(value, symbol) {\n    return typeof value === 'string' ? symbol + value : value\n  }\n\n  /*  */\n\n  function installRenderHelpers(target) {\n    target._o = markOnce;\n    target._n = toNumber;\n    target._s = toString;\n    target._l = renderList;\n    target._t = renderSlot;\n    target._q = looseEqual;\n    target._i = looseIndexOf;\n    target._m = renderStatic;\n    target._f = resolveFilter;\n    target._k = checkKeyCodes;\n    target._b = bindObjectProps;\n    target._v = createTextVNode;\n    target._e = createEmptyVNode;\n    target._u = resolveScopedSlots;\n    target._g = bindObjectListeners;\n    target._d = bindDynamicKeys;\n    target._p = prependModifier;\n  }\n\n  /*  */\n\n  function FunctionalRenderContext(\n    data,\n    props,\n    children,\n    parent,\n    Ctor\n  ) {\n    var this$1 = this;\n\n    var options = Ctor.options;\n    // ensure the createElement function in functional components\n    // gets a unique context - this is necessary for correct named slot check\n    var contextVm;\n    if (hasOwn(parent, '_uid')) {\n      contextVm = Object.create(parent);\n      // $flow-disable-line\n      contextVm._original = parent;\n    } else {\n      // the context vm passed in is a functional context as well.\n      // in this case we want to make sure we are able to get a hold to the\n      // real context instance.\n      contextVm = parent;\n      // $flow-disable-line\n      parent = parent._original;\n    }\n    var isCompiled = isTrue(options._compiled);\n    var needNormalization = !isCompiled;\n\n    this.data = data;\n    this.props = props;\n    this.children = children;\n    this.parent = parent;\n    this.listeners = data.on || emptyObject;\n    this.injections = resolveInject(options.inject, parent);\n    this.slots = function () {\n      if (!this$1.$slots) {\n        normalizeScopedSlots(\n          data.scopedSlots,\n          this$1.$slots = resolveSlots(children, parent)\n        );\n      }\n      return this$1.$slots\n    };\n\n    Object.defineProperty(this, 'scopedSlots', ({\n      enumerable: true,\n      get: function get() {\n        return normalizeScopedSlots(data.scopedSlots, this.slots())\n      }\n    }));\n\n    // support for compiled functional template\n    if (isCompiled) {\n      // exposing $options for renderStatic()\n      this.$options = options;\n      // pre-resolve slots for renderSlot()\n      this.$slots = this.slots();\n      this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots);\n    }\n\n    if (options._scopeId) {\n      this._c = function (a, b, c, d) {\n        var vnode = createElement(contextVm, a, b, c, d, needNormalization);\n        if (vnode && !Array.isArray(vnode)) {\n          vnode.fnScopeId = options._scopeId;\n          vnode.fnContext = parent;\n        }\n        return vnode\n      };\n    } else {\n      this._c = function (a, b, c, d) {\n        return createElement(contextVm, a, b, c, d, needNormalization);\n      };\n    }\n  }\n\n  installRenderHelpers(FunctionalRenderContext.prototype);\n\n  function createFunctionalComponent(\n    Ctor,\n    propsData,\n    data,\n    contextVm,\n    children\n  ) {\n    var options = Ctor.options;\n    var props = {};\n    var propOptions = options.props;\n    if (isDef(propOptions)) {\n      for (var key in propOptions) {\n        props[key] = validateProp(key, propOptions, propsData || emptyObject);\n      }\n    } else {\n      if (isDef(data.attrs)) {\n        mergeProps(props, data.attrs);\n      }\n      if (isDef(data.props)) {\n        mergeProps(props, data.props);\n      }\n    }\n\n    var renderContext = new FunctionalRenderContext(\n      data,\n      props,\n      children,\n      contextVm,\n      Ctor\n    );\n\n    var vnode = options.render.call(null, renderContext._c, renderContext);\n\n    if (vnode instanceof VNode) {\n      return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)\n    } else if (Array.isArray(vnode)) {\n      var vnodes = normalizeChildren(vnode) || [];\n      var res = new Array(vnodes.length);\n      for (var i = 0; i < vnodes.length; i++) {\n        res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);\n      }\n      return res\n    }\n  }\n\n  function cloneAndMarkFunctionalResult(vnode, data, contextVm, options, renderContext) {\n    // #7817 clone node before setting fnContext, otherwise if the node is reused\n    // (e.g. it was from a cached normal slot) the fnContext causes named slots\n    // that should not be matched to match.\n    var clone = cloneVNode(vnode);\n    clone.fnContext = contextVm;\n    clone.fnOptions = options; {\n      (clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext;\n    }\n    if (data.slot) {\n      (clone.data || (clone.data = {})).slot = data.slot;\n    }\n    return clone\n  }\n\n  function mergeProps(to, from) {\n    for (var key in from) {\n      to[camelize(key)] = from[key];\n    }\n  }\n\n  /*  */\n\n  /*  */\n\n  /*  */\n\n  /*  */\n\n  // inline hooks to be invoked on component VNodes during patch\n  // 组件内部自带钩子\n  var componentVNodeHooks = {\n    init: function init(vnode, hydrating) {\n      if (\n        vnode.componentInstance &&\n        !vnode.componentInstance._isDestroyed &&\n        vnode.data.keepAlive\n      ) {\n        // kept-alive components, treat as a patch\n        var mountedNode = vnode; // work around flow\n        componentVNodeHooks.prepatch(mountedNode, mountedNode);\n      } else {\n        var child = vnode.componentInstance = createComponentInstanceForVnode(\n          vnode,\n          activeInstance\n        );\n        child.$mount(hydrating ? vnode.elm : undefined, hydrating);\n      }\n    },\n\n    prepatch: function prepatch(oldVnode, vnode) {\n      var options = vnode.componentOptions;\n      var child = vnode.componentInstance = oldVnode.componentInstance;\n      updateChildComponent(\n        child,\n        options.propsData, // updated props\n        options.listeners, // updated listeners\n        vnode, // new parent vnode\n        options.children // new children\n      );\n    },\n\n    insert: function insert(vnode) {\n      var context = vnode.context;\n      var componentInstance = vnode.componentInstance;\n      if (!componentInstance._isMounted) {\n        componentInstance._isMounted = true;\n        callHook(componentInstance, 'mounted');\n      }\n      if (vnode.data.keepAlive) {\n        if (context._isMounted) {\n          // vue-router#1212\n          // During updates, a kept-alive component's child components may\n          // change, so directly walking the tree here may call activated hooks\n          // on incorrect children. Instead we push them into a queue which will\n          // be processed after the whole patch process ended.\n          queueActivatedComponent(componentInstance);\n        } else {\n          activateChildComponent(componentInstance, true /* direct */ );\n        }\n      }\n    },\n\n    destroy: function destroy(vnode) {\n      var componentInstance = vnode.componentInstance;\n      if (!componentInstance._isDestroyed) {\n        if (!vnode.data.keepAlive) {\n          componentInstance.$destroy();\n        } else {\n          deactivateChildComponent(componentInstance, true /* direct */ );\n        }\n      }\n    }\n  };\n\n  var hooksToMerge = Object.keys(componentVNodeHooks);\n\n  // 创建子组件Vnode过程\n  function createComponent(\n    Ctor, // 子类构造器\n    data,\n    context, // vm实例\n    children, // 子节点\n    tag // 子组件占位符\n  ) {\n    if (isUndef(Ctor)) {\n      return\n    }\n    // baseCtor 代表Vue构造器\n    var baseCtor = context.$options._base;\n\n    // plain options object: turn it into a constructor\n    // 针对局部注册组件创建子类构造器\n    if (isObject(Ctor)) {\n      Ctor = baseCtor.extend(Ctor);\n    }\n\n    // if at this stage it's not a constructor or an async component factory,\n    // reject.\n    if (typeof Ctor !== 'function') {\n      {\n        // 组件定义错误\n        warn((\"Invalid Component definition: \" + (String(Ctor))), context);\n      }\n      return\n    }\n\n    // async component\n    // 异步组件分支\n    var asyncFactory;\n    if (isUndef(Ctor.cid)) {\n      // 异步工厂函数\n      asyncFactory = Ctor;\n      Ctor = resolveAsyncComponent(asyncFactory, baseCtor);\n      if (Ctor === undefined) {\n        // return a placeholder node for async component, which is rendered\n        // as a comment node but preserves all the raw information for the node.\n        // the information will be used for async server-rendering and hydration.\n        return createAsyncPlaceholder(\n          asyncFactory,\n          data,\n          context,\n          children,\n          tag\n        )\n      }\n    }\n\n    data = data || {};\n    // resolve constructor options in case global mixins are applied after\n    // component constructor creation\n    // 构造器配置合并\n    resolveConstructorOptions(Ctor);\n\n    // transform component v-model data into props & events\n    if (isDef(data.model)) {\n      // 处理父组件的v-model指令对象\n      transformModel(Ctor.options, data);\n    }\n    // extract props\n    // props规范性校验\n    var propsData = extractPropsFromVNodeData(data, Ctor, tag);\n\n    // functional component\n    // 函数式组件\n    if (isTrue(Ctor.options.functional)) {\n      // Ctor构造器，propsData: 传入组件的props,data: 组件的属性， context: vue实例\n      return createFunctionalComponent(Ctor, propsData, data, context, children)\n    }\n\n    // extract listeners, since these needs to be treated as\n    // child component listeners instead of DOM listeners\n    var listeners = data.on;\n    // replace with listeners with .native modifier\n    // so it gets processed during parent component patch.\n    data.on = data.nativeOn;\n\n    // 抽象组件\n    if (isTrue(Ctor.options.abstract)) {\n      // abstract components do not keep anything\n      // other than props & listeners & slot\n\n      // work around flow\n      var slot = data.slot;\n      data = {};\n      if (slot) {\n        data.slot = slot;\n      }\n    }\n\n    // install component management hooks onto the placeholder node\n    // 挂载组件钩子\n    installComponentHooks(data);\n\n    // return a placeholder vnode\n    var name = Ctor.options.name || tag;\n    // 创建子组件vnode\n    var vnode = new VNode(\n      (\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),\n      data, undefined, undefined, undefined, context, {\n        Ctor: Ctor,\n        propsData: propsData,\n        listeners: listeners,\n        tag: tag,\n        children: children\n      },\n      asyncFactory\n    );\n\n    return vnode\n  }\n\n  function createComponentInstanceForVnode(\n    vnode, // we know it's MountedComponentVNode but flow doesn't\n    parent // activeInstance in lifecycle state\n  ) {\n    var options = {\n      _isComponent: true,\n      _parentVnode: vnode,\n      parent: parent\n    };\n    // check inline-template render functions\n    var inlineTemplate = vnode.data.inlineTemplate;\n    // 内联模板的处理，分别拿到render函数和staticRenderFns\n    if (isDef(inlineTemplate)) {\n      options.render = inlineTemplate.render;\n      options.staticRenderFns = inlineTemplate.staticRenderFns;\n    }\n    // 执行vue子组件实例化\n    return new vnode.componentOptions.Ctor(options)\n  }\n\n  function installComponentHooks(data) {\n    var hooks = data.hook || (data.hook = {});\n    for (var i = 0; i < hooksToMerge.length; i++) {\n      var key = hooksToMerge[i];\n      var existing = hooks[key];\n      var toMerge = componentVNodeHooks[key];\n      if (existing !== toMerge && !(existing && existing._merged)) {\n        hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;\n      }\n    }\n  }\n\n  function mergeHook$1(f1, f2) {\n    var merged = function (a, b) {\n      // flow complains about extra args which is why we use any\n      f1(a, b);\n      f2(a, b);\n    };\n    merged._merged = true;\n    return merged\n  }\n\n  // transform component v-model info (value and callback) into\n  // prop and event handler respectively.\n  function transformModel(options, data) {\n    // prop默认取的是value，除非配置上有model的选项\n    var prop = (options.model && options.model.prop) || 'value';\n    // event默认取的是input，除非配置上有model的选项\n\n    var event = (options.model && options.model.event) || 'input';\n    (data.attrs || (data.attrs = {}))[prop] = data.model.value;\n    var on = data.on || (data.on = {});\n    var existing = on[event];\n    var callback = data.model.callback;\n    if (isDef(existing)) {\n      if (\n        Array.isArray(existing) ?\n        existing.indexOf(callback) === -1 :\n        existing !== callback\n      ) {\n        on[event] = [callback].concat(existing);\n      }\n    } else {\n      on[event] = callback;\n    }\n  }\n\n  /*  */\n\n  var SIMPLE_NORMALIZE = 1;\n  var ALWAYS_NORMALIZE = 2;\n\n  // wrapper function for providing a more flexible interface\n  // without getting yelled at by flow\n  function createElement(\n    context, // vm 实例\n    tag, // vnode标签\n    data, //vnode相关数据\n    children, // 子vnode\n    normalizationType,\n    alwaysNormalize\n  ) {\n    // 对传入参数做处理，可以没有data，如果没有data，则将第三个参数作为第四个参数使用，以此类推\n    if (Array.isArray(data) || isPrimitive(data)) {\n      normalizationType = children;\n      children = data;\n      data = undefined;\n    }\n    // 根据是alwaysNormalize 区分是内部编译使用的，还是用户手写render使用的\n    if (isTrue(alwaysNormalize)) {\n      normalizationType = ALWAYS_NORMALIZE;\n    }\n    return _createElement(context, tag, data, children, normalizationType)\n  }\n\n  function _createElement(\n    context,\n    tag,\n    data,\n    children,\n    normalizationType\n  ) {\n\n    // 数据对象不能是定义在Vue data属性中的响应式数据。\n    if (isDef(data) && isDef((data).__ob__)) {\n      warn(\n        \"Avoid using observed data object as vnode data: \" + (JSON.stringify(data)) + \"\\n\" +\n        'Always create fresh vnode data objects in each render!',\n        context\n      );\n      return createEmptyVNode() // 返回注释节点\n    }\n    if (isDef(data) && isDef(data.is)) {\n      tag = data.is;\n    }\n    if (!tag) {\n      // 防止动态组件 :is 属性设置为false时，需要做特殊处理\n      return createEmptyVNode()\n    }\n    // key值只能为string，number这些原始数据类型\n    if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {\n      {\n        warn(\n          'Avoid using non-primitive value as key, ' +\n          'use string/number value instead.',\n          context\n        );\n      }\n    }\n    // support single function children as default scoped slot\n    if (Array.isArray(children) &&\n      typeof children[0] === 'function'\n    ) {\n      data = data || {};\n      data.scopedSlots = {\n        default: children[0]\n      };\n      children.length = 0;\n    }\n    if (normalizationType === ALWAYS_NORMALIZE) {\n      // 用户定义render函数\n      children = normalizeChildren(children);\n    } else if (normalizationType === SIMPLE_NORMALIZE) {\n      // render 函数是编译生成的\n      children = simpleNormalizeChildren(children);\n    }\n    var vnode, ns;\n    if (typeof tag === 'string') {\n      var Ctor;\n      ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);\n      if (config.isReservedTag(tag)) {\n        // platform built-in elements\n        vnode = new VNode(\n          config.parsePlatformTagName(tag), data, children,\n          undefined, undefined, context\n        );\n        // 如何判断该节点为组件占位符节点？通过判断是否在配置里拥有了该标签的组件选项。\n      } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {\n        // component\n        vnode = createComponent(Ctor, data, context, children, tag);\n      } else {\n        // unknown or unlisted namespaced elements\n        // check at runtime because it may get assigned a namespace when its\n        // parent normalizes children\n        vnode = new VNode(\n          tag, data, children,\n          undefined, undefined, context\n        );\n      }\n    } else {\n      // direct component options / constructor\n      vnode = createComponent(tag, data, context, children);\n    }\n    if (Array.isArray(vnode)) {\n      return vnode\n    } else if (isDef(vnode)) {\n      if (isDef(ns)) {\n        applyNS(vnode, ns);\n      }\n      if (isDef(data)) {\n        registerDeepBindings(data);\n      }\n      return vnode\n    } else {\n      return createEmptyVNode()\n    }\n  }\n\n  function applyNS(vnode, ns, force) {\n    vnode.ns = ns;\n    if (vnode.tag === 'foreignObject') {\n      // use default namespace inside foreignObject\n      ns = undefined;\n      force = true;\n    }\n    if (isDef(vnode.children)) {\n      for (var i = 0, l = vnode.children.length; i < l; i++) {\n        var child = vnode.children[i];\n        if (isDef(child.tag) && (\n            isUndef(child.ns) || (isTrue(force) && child.tag !== 'svg'))) {\n          applyNS(child, ns, force);\n        }\n      }\n    }\n  }\n\n  // ref #5318\n  // necessary to ensure parent re-render when deep bindings like :style and\n  // :class are used on slot nodes\n  function registerDeepBindings(data) {\n    if (isObject(data.style)) {\n      traverse(data.style);\n    }\n    if (isObject(data.class)) {\n      traverse(data.class);\n    }\n  }\n\n  /*  */\n\n  function initRender(vm) {\n    vm._vnode = null; // the root of the child tree\n    vm._staticTrees = null; // v-once cached trees\n    var options = vm.$options;\n    var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree\n    var renderContext = parentVnode && parentVnode.context;\n    vm.$slots = resolveSlots(options._renderChildren, renderContext); // $slots拿到了子占位符节点的_renderchildren即插槽内容，保留作为子实例的属性\n    vm.$scopedSlots = emptyObject;\n    // bind the createElement fn to this instance\n    // so that we get proper render context inside it.\n    // args order: tag, data, children, normalizationType, alwaysNormalize\n    // internal version is used by render functions compiled from templates\n    vm._c = function (a, b, c, d) {\n      return createElement(vm, a, b, c, d, false);\n    };\n    // normalization is always applied for the public version, used in\n    // user-written render functions.\n    vm.$createElement = function (a, b, c, d) {\n      return createElement(vm, a, b, c, d, true);\n    };\n\n    // $attrs & $listeners are exposed for easier HOC creation.\n    // they need to be reactive so that HOCs using them are always updated\n    var parentData = parentVnode && parentVnode.data;\n\n    /* istanbul ignore else */\n    {\n      defineReactive###1(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () {\n        !isUpdatingChildComponent && warn(\"$attrs is readonly.\", vm);\n      }, true);\n      defineReactive###1(vm, '$listeners', options._parentListeners || emptyObject, function () {\n        !isUpdatingChildComponent && warn(\"$listeners is readonly.\", vm);\n      }, true);\n    }\n  }\n\n  var currentRenderingInstance = null;\n\n  function renderMixin(Vue) {\n    // install runtime convenience helpers\n    installRenderHelpers(Vue.prototype);\n\n    Vue.prototype.$nextTick = function (fn) {\n      return nextTick(fn, this)\n    };\n\n    Vue.prototype._render = function () {\n      var vm = this;\n      var ref = vm.$options;\n      var render = ref.render;\n      var _parentVnode = ref._parentVnode;\n      if (_parentVnode) {\n        vm.$scopedSlots = normalizeScopedSlots(\n          _parentVnode.data.scopedSlots,\n          vm.$slots,\n          vm.$scopedSlots\n        );\n      }\n\n      // set parent vnode. this allows render functions to have access\n      // to the data on the placeholder node.\n      vm.$vnode = _parentVnode;\n      // render self\n      var vnode;\n      try {\n        // There's no need to maintain a stack becaues all render fns are called\n        // separately from one another. Nested component's render fns are called\n        // when parent component is patched.\n        currentRenderingInstance = vm;\n        vnode = render.call(vm._renderProxy, vm.$createElement);\n      } catch (e) {\n        handleError(e, vm, \"render\");\n        // return error render result,\n        // or previous vnode to prevent render error causing blank component\n        /* istanbul ignore else */\n        if (vm.$options.renderError) {\n          try {\n            vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);\n          } catch (e) {\n            handleError(e, vm, \"renderError\");\n            vnode = vm._vnode;\n          }\n        } else {\n          vnode = vm._vnode;\n        }\n      } finally {\n        currentRenderingInstance = null;\n      }\n      // if the returned array contains only a single node, allow it\n      if (Array.isArray(vnode) && vnode.length === 1) {\n        vnode = vnode[0];\n      }\n      // return empty vnode in case the render function errored out\n      if (!(vnode instanceof VNode)) {\n        if (Array.isArray(vnode)) {\n          warn(\n            'Multiple root nodes returned from render function. Render function ' +\n            'should return a single root node.',\n            vm\n          );\n        }\n        vnode = createEmptyVNode();\n      }\n      // set parent\n      vnode.parent = _parentVnode;\n      return vnode\n    };\n  }\n\n  /*  */\n\n  function ensureCtor(comp, base) {\n    if (\n      comp.__esModule ||\n      (hasSymbol && comp[Symbol.toStringTag] === 'Module')\n    ) {\n      comp = comp.default;\n    }\n    return isObject(comp) ?\n      base.extend(comp) :\n      comp\n  }\n\n  function createAsyncPlaceholder(\n    factory,\n    data,\n    context,\n    children,\n    tag\n  ) {\n    var node = createEmptyVNode();\n    node.asyncFactory = factory;\n    node.asyncMeta = {\n      data: data,\n      context: context,\n      children: children,\n      tag: tag\n    };\n    return node\n  }\n\n  function resolveAsyncComponent(\n    factory,\n    baseCtor\n  ) {\n    if (isTrue(factory.error) && isDef(factory.errorComp)) {\n      return factory.errorComp\n    }\n\n    if (isDef(factory.resolved)) {\n      return factory.resolved\n    }\n\n    var owner = currentRenderingInstance;\n    if (isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {\n      // already pending\n      factory.owners.push(owner);\n    }\n\n    if (isTrue(factory.loading) && isDef(factory.loadingComp)) {\n      return factory.loadingComp\n    }\n\n    if (!isDef(factory.owners)) {\n      var owners = factory.owners = [owner];\n      var sync = true\n\n      ;\n      (owner).$on('hook:destroyed', function () {\n        return remove(owners, owner);\n      });\n\n      var forceRender = function (renderCompleted) {\n        for (var i = 0, l = owners.length; i < l; i++) {\n          (owners[i]).$forceUpdate();\n        }\n\n        if (renderCompleted) {\n          owners.length = 0;\n        }\n      };\n\n      var resolve = once(function (res) {\n        // cache resolved\n\n        // 转成成组件构造器，并将其缓存到resolved属性中。\n        factory.resolved = ensureCtor(res, baseCtor);\n        // invoke callbacks only if this is not a synchronous resolve\n        // (async resolves are shimmed as synchronous during SSR)\n        if (!sync) {\n          forceRender(true);\n        } else {\n          owners.length = 0;\n        }\n      });\n\n      var reject = once(function (reason) {\n        warn(\n          \"Failed to resolve async component: \" + (String(factory)) +\n          (reason ? (\"\\nReason: \" + reason) : '')\n        );\n        if (isDef(factory.errorComp)) {\n          factory.error = true;\n          forceRender(true);\n        }\n      });\n\n      // 创建子组件时会先执行工厂函数，并将resolve和reject传入\n      var res = factory(resolve, reject);\n\n      // promise异步组件处理\n      if (isObject(res)) {\n        if (isPromise(res)) {\n          // () => Promise\n          if (isUndef(factory.resolved)) {\n            res.then(resolve, reject);\n          }\n        } else if (isPromise(res.component)) {\n          res.component.then(resolve, reject);\n\n          if (isDef(res.error)) {\n            // 异步错误时组件的处理，创建错误组件的子类构造器，并赋值给errorComp\n            factory.errorComp = ensureCtor(res.error, baseCtor);\n          }\n\n          if (isDef(res.loading)) {\n            // 异步加载时组件的处理，创建错误组件的子类构造器，并赋值给errorComp\n            factory.loadingComp = ensureCtor(res.loading, baseCtor);\n            if (res.delay === 0) {\n              factory.loading = true;\n            } else {\n              setTimeout(function () {\n                if (isUndef(factory.resolved) && isUndef(factory.error)) {\n                  factory.loading = true;\n                  forceRender(false);\n                }\n              }, res.delay || 200);\n            }\n          }\n\n          if (isDef(res.timeout)) {\n            setTimeout(function () {\n              // 定时器，规定时间内 resolved没有赋值，则算加载失败\n              if (isUndef(factory.resolved)) {\n                reject(\n                  \"timeout (\" + (res.timeout) + \"ms)\"\n                );\n              }\n            }, res.timeout);\n          }\n        }\n      }\n\n      sync = false;\n      // return in case resolved synchronously\n      return factory.loading ?\n        factory.loadingComp :\n        factory.resolved\n    }\n  }\n\n  /*  */\n\n  function isAsyncPlaceholder(node) {\n    return node.isComment && node.asyncFactory\n  }\n\n  /*  */\n\n  function getFirstComponentChild(children) {\n    if (Array.isArray(children)) {\n      for (var i = 0; i < children.length; i++) {\n        var c = children[i];\n        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {\n          return c\n        }\n      }\n    }\n  }\n\n  /*  */\n\n  /*  */\n\n  function initEvents(vm) {\n    vm._events = Object.create(null);\n    vm._hasHookEvent = false;\n    // init parent attached events\n    var listeners = vm.$options._parentListeners;\n    if (listeners) {\n      updateComponentListeners(vm, listeners);\n    }\n  }\n\n  var target;\n\n  function add(event, fn) {\n    target.$on(event, fn);\n  }\n\n  function remove$1(event, fn) {\n    target.$off(event, fn);\n  }\n\n  function createOnceHandler(event, fn) {\n    var _target = target;\n    return function onceHandler() {\n      var res = fn.apply(null, arguments);\n      if (res !== null) {\n        _target.$off(event, onceHandler);\n      }\n    }\n  }\n\n  function updateComponentListeners(\n    vm,\n    listeners,\n    oldListeners\n  ) {\n    target = vm;\n    updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);\n    target = undefined;\n  }\n\n  function eventsMixin(Vue) {\n    var hookRE = /^hook:/;\n    Vue.prototype.$on = function (event, fn) {\n      var vm = this;\n      if (Array.isArray(event)) {\n        for (var i = 0, l = event.length; i < l; i++) {\n          vm.$on(event[i], fn);\n        }\n      } else {\n        (vm._events[event] || (vm._events[event] = [])).push(fn);\n        // optimize hook:event cost by using a boolean flag marked at registration\n        // instead of a hash lookup\n        if (hookRE.test(event)) {\n          vm._hasHookEvent = true;\n        }\n      }\n      return vm\n    };\n\n    Vue.prototype.$once = function (event, fn) {\n      var vm = this;\n\n      function on() {\n        vm.$off(event, on);\n        fn.apply(vm, arguments);\n      }\n      on.fn = fn;\n      vm.$on(event, on);\n      return vm\n    };\n\n    Vue.prototype.$off = function (event, fn) {\n      var vm = this;\n      // all\n      if (!arguments.length) {\n        vm._events = Object.create(null);\n        return vm\n      }\n      // array of events\n      if (Array.isArray(event)) {\n        for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {\n          vm.$off(event[i$1], fn);\n        }\n        return vm\n      }\n      // specific event\n      var cbs = vm._events[event];\n      if (!cbs) {\n        return vm\n      }\n      if (!fn) {\n        vm._events[event] = null;\n        return vm\n      }\n      // specific handler\n      var cb;\n      var i = cbs.length;\n      while (i--) {\n        cb = cbs[i];\n        if (cb === fn || cb.fn === fn) {\n          cbs.splice(i, 1);\n          break\n        }\n      }\n      return vm\n    };\n\n    Vue.prototype.$emit = function (event) {\n      var vm = this; {\n        var lowerCaseEvent = event.toLowerCase();\n        if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {\n          tip(\n            \"Event \\\"\" + lowerCaseEvent + \"\\\" is emitted in component \" +\n            (formatComponentName(vm)) + \" but the handler is registered for \\\"\" + event + \"\\\". \" +\n            \"Note that HTML attributes are case-insensitive and you cannot use \" +\n            \"v-on to listen to camelCase events when using in-DOM templates. \" +\n            \"You should probably use \\\"\" + (hyphenate(event)) + \"\\\" instead of \\\"\" + event + \"\\\".\"\n          );\n        }\n      }\n      var cbs = vm._events[event];\n      if (cbs) {\n        cbs = cbs.length > 1 ? toArray(cbs) : cbs;\n        var args = toArray(arguments, 1);\n        var info = \"event handler for \\\"\" + event + \"\\\"\";\n        for (var i = 0, l = cbs.length; i < l; i++) {\n          invokeWithErrorHandling(cbs[i], vm, args, vm, info);\n        }\n      }\n      return vm\n    };\n  }\n\n  /*  */\n\n  var activeInstance = null;\n  var isUpdatingChildComponent = false;\n\n  function setActiveInstance(vm) {\n    var prevActiveInstance = activeInstance;\n    activeInstance = vm;\n    return function () {\n      activeInstance = prevActiveInstance;\n    }\n  }\n\n  function initLifecycle(vm) {\n    var options = vm.$options;\n    // locate first non-abstract parent\n    var parent = options.parent;\n    if (parent && !options.abstract) {\n      while (parent.$options.abstract && parent.$parent) {\n        parent = parent.$parent;\n      }\n      parent.$children.push(vm);\n    }\n\n    vm.$parent = parent;\n    vm.$root = parent ? parent.$root : vm;\n\n    vm.$children = [];\n    vm.$refs = {};\n\n    vm._watcher = null;\n    vm._inactive = null;\n    vm._directInactive = false;\n    vm._isMounted = false;\n    vm._isDestroyed = false;\n    vm._isBeingDestroyed = false;\n  }\n\n  function lifecycleMixin(Vue) {\n    Vue.prototype._update = function (vnode, hydrating) {\n      debugger\n      var vm = this;\n      var prevEl = vm.$el;\n      var prevVnode = vm._vnode;\n      var restoreActiveInstance = setActiveInstance(vm);\n      vm._vnode = vnode;\n      // Vue.prototype.__patch__ is injected in entry points\n      // based on the rendering backend used.\n      if (!prevVnode) {\n        // initial render\n        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */ );\n      } else {\n        // updates\n        vm.$el = vm.__patch__(prevVnode, vnode);\n      }\n      restoreActiveInstance();\n      // update __vue__ reference\n      if (prevEl) {\n        prevEl.__vue__ = null;\n      }\n      if (vm.$el) {\n        vm.$el.__vue__ = vm;\n      }\n      // if parent is an HOC, update its $el as well\n      if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {\n        vm.$parent.$el = vm.$el;\n      }\n      // updated hook is called by the scheduler to ensure that children are\n      // updated in a parent's updated hook.\n    };\n\n    Vue.prototype.$forceUpdate = function () {\n      var vm = this;\n      if (vm._watcher) {\n        vm._watcher.update();\n      }\n    };\n\n    Vue.prototype.$destroy = function () {\n      var vm = this;\n      if (vm._isBeingDestroyed) {\n        return\n      }\n      callHook(vm, 'beforeDestroy');\n      vm._isBeingDestroyed = true;\n      // remove self from parent\n      var parent = vm.$parent;\n      if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {\n        remove(parent.$children, vm);\n      }\n      // teardown watchers\n      if (vm._watcher) {\n        vm._watcher.teardown();\n      }\n      var i = vm._watchers.length;\n      while (i--) {\n        vm._watchers[i].teardown();\n      }\n      // remove reference from data ob\n      // frozen object may not have observer.\n      if (vm._data.__ob__) {\n        vm._data.__ob__.vmCount--;\n      }\n      // call the last hook...\n      vm._isDestroyed = true;\n      // invoke destroy hooks on current rendered tree\n      vm.__patch__(vm._vnode, null);\n      // fire destroyed hook\n      callHook(vm, 'destroyed');\n      // turn off all instance listeners.\n      vm.$off();\n      // remove __vue__ reference\n      if (vm.$el) {\n        vm.$el.__vue__ = null;\n      }\n      // release circular reference (#6759)\n      if (vm.$vnode) {\n        vm.$vnode.parent = null;\n      }\n    };\n  }\n\n  function mountComponent(\n    vm,\n    el,\n    hydrating\n  ) {\n    vm.$el = el;\n    if (!vm.$options.render) {\n      vm.$options.render = createEmptyVNode; {\n        /* istanbul ignore if */\n        if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||\n          vm.$options.el || el) {\n          warn(\n            'You are using the runtime-only build of Vue where the template ' +\n            'compiler is not available. Either pre-compile the templates into ' +\n            'render functions, or use the compiler-included build.',\n            vm\n          );\n        } else {\n          warn(\n            'Failed to mount component: template or render function not defined.',\n            vm\n          );\n        }\n      }\n    }\n    callHook(vm, 'beforeMount');\n\n    var updateComponent;\n    /* istanbul ignore if */\n    if (config.performance && mark) {\n      updateComponent = function () {\n        var name = vm._name;\n        var id = vm._uid;\n        var startTag = \"vue-perf-start:\" + id;\n        var endTag = \"vue-perf-end:\" + id;\n\n        mark(startTag);\n        var vnode = vm._render();\n        mark(endTag);\n        measure((\"vue \" + name + \" render\"), startTag, endTag);\n\n        mark(startTag);\n        vm._update(vnode, hydrating);\n        mark(endTag);\n        measure((\"vue \" + name + \" patch\"), startTag, endTag);\n      };\n    } else {\n      updateComponent = function () {\n        // vm._render会生成vnode\n        vm._update(vm._render(), hydrating);\n      };\n    }\n    // we set this to vm._watcher inside the watcher's constructor\n    // since the watcher's initial patch may call $forceUpdate (e.g. inside child\n    // component's mounted hook), which relies on vm._watcher being already defined\n    new Watcher(vm, updateComponent, noop, {\n      before: function before() {\n        if (vm._isMounted && !vm._isDestroyed) {\n          callHook(vm, 'beforeUpdate');\n        }\n      }\n    }, true /* isRenderWatcher */ );\n    hydrating = false;\n\n    // manually mounted instance, call mounted on self\n    // mounted is called for render-created child components in its inserted hook\n    if (vm.$vnode == null) {\n      vm._isMounted = true;\n      callHook(vm, 'mounted');\n    }\n    return vm\n  }\n\n  function updateChildComponent(\n    vm,\n    propsData,\n    listeners,\n    parentVnode,\n    renderChildren\n  ) {\n    {\n      isUpdatingChildComponent = true;\n    }\n\n    // determine whether component has slot children\n    // we need to do this before overwriting $options._renderChildren.\n\n    // check if there are dynamic scopedSlots (hand-written or compiled but with\n    // dynamic slot names). Static scoped slots compiled from template has the\n    // \"$stable\" marker.\n    var newScopedSlots = parentVnode.data.scopedSlots;\n    var oldScopedSlots = vm.$scopedSlots;\n    var hasDynamicScopedSlot = !!(\n      (newScopedSlots && !newScopedSlots.$stable) ||\n      (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||\n      (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)\n    );\n\n    // Any static slot children from the parent may have changed during parent's\n    // update. Dynamic scoped slots may also have changed. In such cases, a forced\n    // update is necessary to ensure correctness.\n    var needsForceUpdate = !!(\n      renderChildren || // has new static slots\n      vm.$options._renderChildren || // has old static slots\n      hasDynamicScopedSlot\n    );\n\n    vm.$options._parentVnode = parentVnode;\n    vm.$vnode = parentVnode; // update vm's placeholder node without re-render\n\n    if (vm._vnode) { // update child tree's parent\n      vm._vnode.parent = parentVnode;\n    }\n    vm.$options._renderChildren = renderChildren;\n\n    // update $attrs and $listeners hash\n    // these are also reactive so they may trigger child update if the child\n    // used them during render\n    vm.$attrs = parentVnode.data.attrs || emptyObject;\n    vm.$listeners = listeners || emptyObject;\n\n    // update props\n    if (propsData && vm.$options.props) {\n      toggleObserving(false);\n      var props = vm._props;\n      var propKeys = vm.$options._propKeys || [];\n      for (var i = 0; i < propKeys.length; i++) {\n        var key = propKeys[i];\n        var propOptions = vm.$options.props; // wtf flow?\n        props[key] = validateProp(key, propOptions, propsData, vm);\n      }\n      toggleObserving(true);\n      // keep a copy of raw propsData\n      vm.$options.propsData = propsData;\n    }\n\n    // update listeners\n    listeners = listeners || emptyObject;\n    var oldListeners = vm.$options._parentListeners;\n    vm.$options._parentListeners = listeners;\n    updateComponentListeners(vm, listeners, oldListeners);\n\n    // resolve slots + force update if has children\n    if (needsForceUpdate) {\n      vm.$slots = resolveSlots(renderChildren, parentVnode.context);\n      vm.$forceUpdate();\n    }\n\n    {\n      isUpdatingChildComponent = false;\n    }\n  }\n\n  function isInInactiveTree(vm) {\n    debugger\n    while (vm && (vm = vm.$parent)) {\n      if (vm._inactive) {\n        return true\n      }\n    }\n    return false\n  }\n\n  function activateChildComponent(vm, direct) {\n    debugger\n    if (direct) {\n      vm._directInactive = false;\n      if (isInInactiveTree(vm)) {\n        return\n      }\n    } else if (vm._directInactive) {\n      return\n    }\n    if (vm._inactive || vm._inactive === null) {\n      vm._inactive = false;\n      for (var i = 0; i < vm.$children.length; i++) {\n        activateChildComponent(vm.$children[i]);\n      }\n      callHook(vm, 'activated');\n    }\n  }\n\n  function deactivateChildComponent(vm, direct) {\n    if (direct) {\n      vm._directInactive = true;\n      if (isInInactiveTree(vm)) {\n        return\n      }\n    }\n    if (!vm._inactive) {\n      vm._inactive = true;\n      for (var i = 0; i < vm.$children.length; i++) {\n        deactivateChildComponent(vm.$children[i]);\n      }\n      callHook(vm, 'deactivated');\n    }\n  }\n\n  function callHook(vm, hook) {\n    // #7573 disable dep collection when invoking lifecycle hooks\n    pushTarget();\n    var handlers = vm.$options[hook];\n    var info = hook + \" hook\";\n    if (handlers) {\n      for (var i = 0, j = handlers.length; i < j; i++) {\n        invokeWithErrorHandling(handlers[i], vm, null, vm, info);\n      }\n    }\n    if (vm._hasHookEvent) {\n      vm.$emit('hook:' + hook);\n    }\n    popTarget();\n  }\n\n  /*  */\n\n  var MAX_UPDATE_COUNT = 100;\n\n  var queue = [];\n  var activatedChildren = [];\n  var has = {};\n  var circular = {};\n  var waiting = false;\n  var flushing = false;\n  var index = 0;\n\n  /**\n   * Reset the scheduler's state.\n   */\n  function resetSchedulerState() {\n    index = queue.length = activatedChildren.length = 0;\n    has = {}; {\n      circular = {};\n    }\n    waiting = flushing = false;\n  }\n\n  // Async edge case #6566 requires saving the timestamp when event listeners are\n  // attached. However, calling performance.now() has a perf overhead especially\n  // if the page has thousands of event listeners. Instead, we take a timestamp\n  // every time the scheduler flushes and use that for all event listeners\n  // attached during that flush.\n  var currentFlushTimestamp = 0;\n\n  // Async edge case fix requires storing an event listener's attach timestamp.\n  var getNow = Date.now;\n\n  // Determine what event timestamp the browser is using. Annoyingly, the\n  // timestamp can either be hi-res (relative to page load) or low-res\n  // (relative to UNIX epoch), so in order to compare time we have to use the\n  // same timestamp type when saving the flush timestamp.\n  if (inBrowser && getNow() > document.createEvent('Event').timeStamp) {\n    // if the low-res timestamp which is bigger than the event timestamp\n    // (which is evaluated AFTER) it means the event is using a hi-res timestamp,\n    // and we need to use the hi-res version for event listeners as well.\n    getNow = function () {\n      return performance.now();\n    };\n  }\n\n  /**\n   * Flush both queues and run the watchers.\n   */\n  function flushSchedulerQueue() {\n    currentFlushTimestamp = getNow();\n    flushing = true;\n    var watcher, id;\n\n    // Sort queue before flush.\n    // This ensures that:\n    // 1. Components are updated from parent to child. (because parent is always\n    //    created before the child)\n    // 2. A component's user watchers are run before its render watcher (because\n    //    user watchers are created before the render watcher)\n    // 3. If a component is destroyed during a parent component's watcher run,\n    //    its watchers can be skipped.\n    queue.sort(function (a, b) {\n      return a.id - b.id;\n    });\n\n    // do not cache length because more watchers might be pushed\n    // as we run existing watchers\n    for (index = 0; index < queue.length; index++) {\n      watcher = queue[index];\n      // 如果watcher定义了before的配置，则优先执行before方法\n      if (watcher.before) {\n        watcher.before();\n      }\n      id = watcher.id;\n      has[id] = null;\n      watcher.run();\n      // in dev build, check and stop circular updates.\n      if (has[id] != null) {\n        circular[id] = (circular[id] || 0) + 1;\n        if (circular[id] > MAX_UPDATE_COUNT) {\n          warn(\n            'You may have an infinite update loop ' + (\n              watcher.user ?\n              (\"in watcher with expression \\\"\" + (watcher.expression) + \"\\\"\") :\n              \"in a component render function.\"\n            ),\n            watcher.vm\n          );\n          break\n        }\n      }\n    }\n\n    // keep copies of post queues before resetting state\n    var activatedQueue = activatedChildren.slice();\n    var updatedQueue = queue.slice();\n\n    resetSchedulerState();\n\n    // call component updated and activated hooks\n    callActivatedHooks(activatedQueue);\n    callUpdatedHooks(updatedQueue);\n\n    // devtool hook\n    /* istanbul ignore if */\n    if (devtools && config.devtools) {\n      devtools.emit('flush');\n    }\n  }\n\n  function callUpdatedHooks(queue) {\n    var i = queue.length;\n    while (i--) {\n      var watcher = queue[i];\n      var vm = watcher.vm;\n      if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {\n        callHook(vm, 'updated');\n      }\n    }\n  }\n\n  /**\n   * Queue a kept-alive component that was activated during patch.\n   * The queue will be processed after the entire tree has been patched.\n   */\n  function queueActivatedComponent(vm) {\n    // setting _inactive to false here so that a render function can\n    // rely on checking whether it's in an inactive tree (e.g. router-view)\n    vm._inactive = false;\n    activatedChildren.push(vm);\n  }\n\n  function callActivatedHooks(queue) {\n    for (var i = 0; i < queue.length; i++) {\n      queue[i]._inactive = true;\n      activateChildComponent(queue[i], true /* true */ );\n    }\n  }\n\n  /**\n   * Push a watcher into the watcher queue.\n   * Jobs with duplicate IDs will be skipped unless it's\n   * pushed when the queue is being flushed.\n   */\n  function queueWatcher(watcher) {\n    var id = watcher.id;\n    // 保证同一个watcher只执行一次\n    if (has[id] == null) {\n      has[id] = true;\n      if (!flushing) {\n        queue.push(watcher);\n      } else {\n        // if already flushing, splice the watcher based on its id\n        // if already past its id, it will be run next immediately.\n        var i = queue.length - 1;\n        while (i > index && queue[i].id > watcher.id) {\n          i--;\n        }\n        queue.splice(i + 1, 0, watcher);\n      }\n      // queue the flush\n      if (!waiting) {\n        waiting = true;\n\n        if (!config.async) {\n          flushSchedulerQueue();\n          return\n        }\n        nextTick(flushSchedulerQueue);\n      }\n    }\n  }\n\n  /*  */\n\n\n\n  var uid$2 = 0;\n\n  /**\n   * A watcher parses an expression, collects dependencies,\n   * and fires callback when the expression value changes.\n   * This is used for both the $watch() api and directives.\n   */\n  var Watcher = function Watcher(\n    vm, // 组件实例\n    expOrFn, // 执行函数\n    cb, // 回调\n    options, // 配置\n    isRenderWatcher // 是否为渲染watcher\n  ) {\n    this.vm = vm;\n    if (isRenderWatcher) {\n      vm._watcher = this;\n    }\n    vm._watchers.push(this);\n    // options\n    if (options) {\n      this.deep = !!options.deep;\n      this.user = !!options.user;\n      this.lazy = !!options.lazy;\n      this.sync = !!options.sync;\n      this.before = options.before;\n    } else {\n      this.deep = this.user = this.lazy = this.sync = false;\n    }\n    this.cb = cb;\n    this.id = ++uid$2; // uid for batching\n    this.active = true;\n    // computed watcher\n    this.dirty = this.lazy; // for lazy watchers\n    this.deps = [];\n    this.newDeps = [];\n    this.depIds = new _Set();\n    this.newDepIds = new _Set();\n    this.expression = expOrFn.toString();\n    // parse expression for getter\n    if (typeof expOrFn === 'function') {\n      this.getter = expOrFn;\n    } else {\n      this.getter = parsePath(expOrFn);\n      if (!this.getter) {\n        this.getter = noop;\n        warn(\n          \"Failed watching path: \\\"\" + expOrFn + \"\\\" \" +\n          'Watcher only accepts simple dot-delimited paths. ' +\n          'For full control, use a function instead.',\n          vm\n        );\n      }\n    }\n    // lazy为计算属性标志，当watcher为计算watcher时，不会理解执行get方法进行求值\n    this.value = this.lazy ?\n      undefined :\n      this.get();\n  };\n\n  /**\n   * Evaluate the getter, and re-collect dependencies.\n   * 重新计算getter，并收集依赖\n   */\n  Watcher.prototype.get = function get() {\n    pushTarget(this);\n    var value;\n    var vm = this.vm;\n    try {\n      value = this.getter.call(vm, vm);\n    } catch (e) {\n      if (this.user) {\n        handleError(e, vm, (\"getter for watcher \\\"\" + (this.expression) + \"\\\"\"));\n      } else {\n        throw e\n      }\n    } finally {\n      // \"touch\" every property so they are all tracked as\n      // dependencies for deep watching\n      if (this.deep) {\n        traverse(value);\n      }\n      // 把Dep.target恢复到上一个状态，依赖收集过程完成\n      popTarget();\n      this.cleanupDeps();\n    }\n    return value\n  };\n\n  /**\n   * Add a dependency to this directive.\n   */\n  Watcher.prototype.addDep = function addDep(dep) {\n    var id = dep.id;\n    if (!this.newDepIds.has(id)) {\n      this.newDepIds.add(id);\n      this.newDeps.push(dep);\n      if (!this.depIds.has(id)) {\n        dep.addSub(this);\n      }\n    }\n  };\n\n  /**\n   * Clean up for dependency collection.\n   */\n  // 依赖清除的过程\n  Watcher.prototype.cleanupDeps = function cleanupDeps() {\n    var i = this.deps.length;\n    while (i--) {\n      var dep = this.deps[i];\n      if (!this.newDepIds.has(dep.id)) {\n        dep.removeSub(this);\n      }\n    }\n    var tmp = this.depIds;\n    this.depIds = this.newDepIds;\n    this.newDepIds = tmp;\n    this.newDepIds.clear();\n    tmp = this.deps;\n    this.deps = this.newDeps;\n    this.newDeps = tmp;\n    this.newDeps.length = 0;\n  };\n\n  /**\n   * Subscriber interface.\n   * Will be called when a dependency changes.\n   */\n  Watcher.prototype.update = function update() {\n    /* istanbul ignore else */\n    if (this.lazy) {\n      this.dirty = true;\n    } else if (this.sync) {\n      this.run();\n    } else {\n      queueWatcher(this);\n    }\n  };\n\n  /**\n   * Scheduler job interface.\n   * Will be called by the scheduler.\n   */\n  Watcher.prototype.run = function run() {\n    if (this.active) {\n      var value = this.get();\n      if (\n        value !== this.value ||\n        // Deep watchers and watchers on Object/Arrays should fire even\n        // when the value is the same, because the value may\n        // have mutated.\n        isObject(value) ||\n        this.deep\n      ) {\n        // set new value\n        var oldValue = this.value;\n        this.value = value;\n        if (this.user) {\n          try {\n            this.cb.call(this.vm, value, oldValue);\n          } catch (e) {\n            handleError(e, this.vm, (\"callback for watcher \\\"\" + (this.expression) + \"\\\"\"));\n          }\n        } else {\n          this.cb.call(this.vm, value, oldValue);\n        }\n      }\n    }\n  };\n\n  /**\n   * Evaluate the value of the watcher.\n   * This only gets called for lazy watchers.\n   */\n  Watcher.prototype.evaluate = function evaluate() {\n    // 对于计算属性而言 evaluate的作用是执行计算回调\n    this.value = this.get();\n    this.dirty = false;\n  };\n\n  /**\n   * Depend on all deps collected by this watcher.\n   */\n  Watcher.prototype.depend = function depend() {\n    var i = this.deps.length;\n    while (i--) {\n      this.deps[i].depend();\n    }\n  };\n\n  /**\n   * Remove self from all dependencies' subscriber list.\n   */\n  Watcher.prototype.teardown = function teardown() {\n    if (this.active) {\n      // remove self from vm's watcher list\n      // this is a somewhat expensive operation so we skip it\n      // if the vm is being destroyed.\n      if (!this.vm._isBeingDestroyed) {\n        remove(this.vm._watchers, this);\n      }\n      var i = this.deps.length;\n      while (i--) {\n        this.deps[i].removeSub(this);\n      }\n      this.active = false;\n    }\n  };\n\n  /*  */\n\n  var sharedPropertyDefinition = {\n    enumerable: true,\n    configurable: true,\n    get: noop,\n    set: noop\n  };\n\n  // 数据代理\n  // vm, _data, key\n  function proxy(target, sourceKey, key) {\n    sharedPropertyDefinition.get = function proxyGetter() {\n      return this[sourceKey][key]\n    };\n    sharedPropertyDefinition.set = function proxySetter(val) {\n      this[sourceKey][key] = val;\n    };\n    Object.defineProperty(target, key, sharedPropertyDefinition);\n  }\n\n  function initState(vm) {\n    vm._watchers = [];\n    var opts = vm.$options;\n    if (opts.props) {\n      initProps(vm, opts.props);\n    }\n    if (opts.methods) {\n      initMethods(vm, opts.methods);\n    }\n    if (opts.data) {\n      initData(vm);\n    } else {\n      observe(vm._data = {}, true /* asRootData */ );\n    }\n    if (opts.computed) {\n      initComputed(vm, opts.computed);\n    }\n    if (opts.watch && opts.watch !== nativeWatch) {\n      initWatch(vm, opts.watch);\n    }\n  }\n\n  function initProps(vm, propsOptions) {\n    var propsData = vm.$options.propsData || {};\n    var props = vm._props = {};\n    // cache prop keys so that future props updates can iterate using Array\n    // instead of dynamic object key enumeration.\n    var keys = vm.$options._propKeys = [];\n    var isRoot = !vm.$parent;\n    // root instance props should be converted\n    if (!isRoot) {\n      toggleObserving(false);\n    }\n    var loop = function (key) {\n      keys.push(key);\n      var value = validateProp(key, propsOptions, propsData, vm);\n      /* istanbul ignore else */\n      {\n        var hyphenatedKey = hyphenate(key);\n        if (isReservedAttribute(hyphenatedKey) ||\n          config.isReservedAttr(hyphenatedKey)) {\n          warn(\n            (\"\\\"\" + hyphenatedKey + \"\\\" is a reserved attribute and cannot be used as component prop.\"),\n            vm\n          );\n        }\n        defineReactive###1(props, key, value, function () {\n          // props在组件内部更新时会触发setting，执行该回调函数\n          if (!isRoot && !isUpdatingChildComponent) {\n            warn(\n              \"Avoid mutating a prop directly since the value will be \" +\n              \"overwritten whenever the parent component re-renders. \" +\n              \"Instead, use a data or computed property based on the prop's \" +\n              \"value. Prop being mutated: \\\"\" + key + \"\\\"\",\n              vm\n            );\n          }\n        });\n      }\n      // static props are already proxied on the component's prototype\n      // during Vue.extend(). We only need to proxy props defined at\n      // instantiation here.\n      if (!(key in vm)) {\n        proxy(vm, \"_props\", key);\n      }\n    };\n\n    for (var key in propsOptions) loop(key);\n    toggleObserving(true);\n  }\n\n  function initData(vm) {\n    var data = vm.$options.data;\n    data = vm._data = typeof data === 'function' ?\n      getData(data, vm) :\n      data || {};\n    if (!isPlainObject(data)) {\n      data = {};\n      warn(\n        'data functions should return an object:\\n' +\n        'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',\n        vm\n      );\n    }\n    // proxy data on instance\n    var keys = Object.keys(data);\n    var props = vm.$options.props;\n    var methods = vm.$options.methods;\n    var i = keys.length;\n    while (i--) {\n      var key = keys[i]; {\n        // 命名不能和方法重复\n        if (methods && hasOwn(methods, key)) {\n          warn(\n            (\"Method \\\"\" + key + \"\\\" has already been defined as a data property.\"),\n            vm\n          );\n        }\n      }\n      // 命名不能和props重复\n      if (props && hasOwn(props, key)) {\n        warn(\n          \"The data property \\\"\" + key + \"\\\" is already declared as a prop. \" +\n          \"Use prop default value instead.\",\n          vm\n        );\n      } else if (!isReserved(key)) {\n        // 数据代理，用户可直接通过vm实例返回data数据\n        proxy(vm, \"_data\", key);\n      }\n    }\n    // observe data\n    observe(data, true /* asRootData */ );\n  }\n\n  function getData(data, vm) {\n    // #7573 disable dep collection when invoking data getters\n    pushTarget();\n    try {\n      return data.call(vm, vm)\n    } catch (e) {\n      handleError(e, vm, \"data()\");\n      return {}\n    } finally {\n      popTarget();\n    }\n  }\n\n  var computedWatcherOptions = {\n    lazy: true\n  };\n\n  function initComputed(vm, computed) {\n    // $flow-disable-line\n    var watchers = vm._computedWatchers = Object.create(null);\n    // computed properties are just getters during SSR\n    var isSSR = isServerRendering();\n\n    for (var key in computed) {\n      var userDef = computed[key];\n      var getter = typeof userDef === 'function' ? userDef : userDef.get;\n      if (getter == null) {\n        warn(\n          (\"Getter is missing for computed property \\\"\" + key + \"\\\".\"),\n          vm\n        );\n      }\n\n      if (!isSSR) {\n        // computed watcher\n        // create internal watcher for the computed property.\n        watchers[key] = new Watcher(\n          vm,\n          getter || noop,\n          noop,\n          computedWatcherOptions\n        );\n      }\n\n      // component-defined computed properties are already defined on the\n      // component prototype. We only need to define computed properties defined\n      // at instantiation here.\n      if (!(key in vm)) {\n        defineComputed(vm, key, userDef);\n      } else {\n        // 不能和props，data命名冲突\n        if (key in vm.$data) {\n          warn((\"The computed property \\\"\" + key + \"\\\" is already defined in data.\"), vm);\n        } else if (vm.$options.props && key in vm.$options.props) {\n          warn((\"The computed property \\\"\" + key + \"\\\" is already defined as a prop.\"), vm);\n        }\n      }\n    }\n  }\n\n  function defineComputed(\n    target,\n    key,\n    userDef\n  ) {\n    var shouldCache = !isServerRendering();\n    if (typeof userDef === 'function') {\n      sharedPropertyDefinition.get = shouldCache ?\n        createComputedGetter(key) :\n        createGetterInvoker(userDef);\n      sharedPropertyDefinition.set = noop;\n    } else {\n      sharedPropertyDefinition.get = userDef.get ?\n        shouldCache && userDef.cache !== false ?\n        createComputedGetter(key) :\n        createGetterInvoker(userDef.get) :\n        noop;\n      sharedPropertyDefinition.set = userDef.set || noop;\n    }\n    if (sharedPropertyDefinition.set === noop) {\n      sharedPropertyDefinition.set = function () {\n        warn(\n          (\"Computed property \\\"\" + key + \"\\\" was assigned to but it has no setter.\"),\n          this\n        );\n      };\n    }\n    Object.defineProperty(target, key, sharedPropertyDefinition);\n  }\n\n  function createComputedGetter(key) {\n    return function computedGetter() {\n      var watcher = this._computedWatchers && this._computedWatchers[key];\n      if (watcher) {\n        if (watcher.dirty) {\n          watcher.evaluate();\n        }\n        // \n        if (Dep.target) {\n          watcher.depend();\n        }\n        return watcher.value\n      }\n    }\n  }\n\n  function createGetterInvoker(fn) {\n    return function computedGetter() {\n      return fn.call(this, this)\n    }\n  }\n\n  function initMethods(vm, methods) {\n    var props = vm.$options.props;\n    for (var key in methods) {\n      {\n        // method必须为函数形式\n        if (typeof methods[key] !== 'function') {\n          warn(\n            \"Method \\\"\" + key + \"\\\" has type \\\"\" + (typeof methods[key]) + \"\\\" in the component definition. \" +\n            \"Did you reference the function correctly?\",\n            vm\n          );\n        }\n        // methods方法名不能和props重复\n        if (props && hasOwn(props, key)) {\n          warn(\n            (\"Method \\\"\" + key + \"\\\" has already been defined as a prop.\"),\n            vm\n          );\n        }\n        //  不能以_ or $.这些Vue保留标志开头\n        if ((key in vm) && isReserved(key)) {\n          warn(\n            \"Method \\\"\" + key + \"\\\" conflicts with an existing Vue instance method. \" +\n            \"Avoid defining component methods that start with _ or $.\"\n          );\n        }\n      }\n      // 直接复制到实例的属性上\n      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);\n    }\n  }\n\n  function initWatch(vm, watch) {\n    for (var key in watch) {\n      var handler = watch[key];\n      if (Array.isArray(handler)) {\n        for (var i = 0; i < handler.length; i++) {\n          createWatcher(vm, key, handler[i]);\n        }\n      } else {\n        createWatcher(vm, key, handler);\n      }\n    }\n  }\n\n  function createWatcher(\n    vm,\n    expOrFn,\n    handler,\n    options\n  ) {\n    if (isPlainObject(handler)) {\n      options = handler;\n      handler = handler.handler;\n    }\n    if (typeof handler === 'string') {\n      handler = vm[handler];\n    }\n    return vm.$watch(expOrFn, handler, options)\n  }\n\n  function stateMixin(Vue) {\n    // flow somehow has problems with directly declared definition object\n    // when using Object.defineProperty, so we have to procedurally build up\n    // the object here.\n    var dataDef = {};\n    dataDef.get = function () {\n      return this._data\n    };\n    var propsDef = {};\n    propsDef.get = function () {\n      return this._props\n    }; {\n      dataDef.set = function () {\n        warn(\n          'Avoid replacing instance root $data. ' +\n          'Use nested data properties instead.',\n          this\n        );\n      };\n      propsDef.set = function () {\n        warn(\"$props is readonly.\", this);\n      };\n    }\n    Object.defineProperty(Vue.prototype, '$data', dataDef);\n    Object.defineProperty(Vue.prototype, '$props', propsDef);\n\n    Vue.prototype.$set = set;\n    Vue.prototype.$delete = del;\n\n    Vue.prototype.$watch = function (\n      expOrFn,\n      cb,\n      options\n    ) {\n      var vm = this;\n      if (isPlainObject(cb)) {\n        return createWatcher(vm, expOrFn, cb, options)\n      }\n      options = options || {};\n      options.user = true;\n      var watcher = new Watcher(vm, expOrFn, cb, options);\n      // 当watch有immediate选项时，立即执行cb方法，即不需要等待属性变化，立刻执行回调。\n      if (options.immediate) {\n        try {\n          cb.call(vm, watcher.value);\n        } catch (error) {\n          handleError(error, vm, (\"callback for immediate watcher \\\"\" + (watcher.expression) + \"\\\"\"));\n        }\n      }\n      return function unwatchFn() {\n        watcher.teardown();\n      }\n    };\n  }\n\n  /*  */\n\n  var uid$3 = 0;\n\n  function initMixin(Vue) {\n    Vue.prototype._init = function (options) {\n      var vm = this;\n      // a uid\n      // 记录实例化多少个vue对象\n      vm._uid = uid$3++;\n\n      var startTag, endTag;\n      /* istanbul ignore if */\n      if (config.performance && mark) {\n        startTag = \"vue-perf-start:\" + (vm._uid);\n        endTag = \"vue-perf-end:\" + (vm._uid);\n        mark(startTag);\n      }\n      // a flag to avoid this being observed\n      vm._isVue = true;\n      // merge options\n      if (options && options._isComponent) {\n        // optimize internal component instantiation\n        // since dynamic options merging is pretty slow, and none of the\n        // internal component options needs special treatment.\n        initInternalComponent(vm, options);\n      } else {\n        // 选项合并，将合并后的选项赋值给实例的$options属性\n        vm.$options = mergeOptions(\n          resolveConstructorOptions(vm.constructor),\n          options || {},\n          vm\n        );\n      }\n      /* istanbul ignore else */\n      {\n        // 对vm实例进行一层代理\n        initProxy(vm);\n      }\n      // expose real self\n      vm._self = vm;\n      initLifecycle(vm);\n      // 初始化事件处理\n      initEvents(vm);\n      // 定义渲染函数\n      initRender(vm);\n      callHook(vm, 'beforeCreate');\n      initInjections(vm); // resolve injections before data/props\n      // debugger\n      // 构建响应式系统\n      initState(vm);\n      initProvide(vm); // resolve provide after data/props\n      callHook(vm, 'created');\n\n      /* istanbul ignore if */\n      if (config.performance && mark) {\n        vm._name = formatComponentName(vm, false);\n        mark(endTag);\n        measure((\"vue \" + (vm._name) + \" init\"), startTag, endTag);\n      }\n\n      if (vm.$options.el) {\n        vm.$mount(vm.$options.el);\n      }\n    };\n  }\n\n  function initInternalComponent(vm, options) {\n    var opts = vm.$options = Object.create(vm.constructor.options);\n    // doing this because it's faster than dynamic enumeration.\n    var parentVnode = options._parentVnode;\n    opts.parent = options.parent;\n    opts._parentVnode = parentVnode;\n    var vnodeComponentOptions = parentVnode.componentOptions;\n    opts.propsData = vnodeComponentOptions.propsData;\n    opts._parentListeners = vnodeComponentOptions.listeners;\n    opts._renderChildren = vnodeComponentOptions.children;\n    opts._componentTag = vnodeComponentOptions.tag;\n\n    if (options.render) {\n      opts.render = options.render;\n      opts.staticRenderFns = options.staticRenderFns;\n    }\n  }\n\n  function resolveConstructorOptions(Ctor) {\n    var options = Ctor.options;\n    if (Ctor.super) {\n      var superOptions = resolveConstructorOptions(Ctor.super);\n      var cachedSuperOptions = Ctor.superOptions;\n      if (superOptions !== cachedSuperOptions) {\n        // super option changed,\n        // need to resolve new options.\n        Ctor.superOptions = superOptions;\n        // check if there are any late-modified/attached options (#4976)\n        var modifiedOptions = resolveModifiedOptions(Ctor);\n        // update base extend options\n        if (modifiedOptions) {\n          extend(Ctor.extendOptions, modifiedOptions);\n        }\n        options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);\n        if (options.name) {\n          options.components[options.name] = Ctor;\n        }\n      }\n    }\n    return options\n  }\n\n  function resolveModifiedOptions(Ctor) {\n    var modified;\n    var latest = Ctor.options;\n    var sealed = Ctor.sealedOptions;\n    for (var key in latest) {\n      if (latest[key] !== sealed[key]) {\n        if (!modified) {\n          modified = {};\n        }\n        modified[key] = latest[key];\n      }\n    }\n    return modified\n  }\n  // Vue 构造函数\n  function Vue(options) {\n    if (!(this instanceof Vue)) {\n      warn('Vue is a constructor and should be called with the `new` keyword');\n    }\n    this._init(options);\n  }\n\n  // 在引进Vue时，会执行initMixin方法，这个方法只单纯在vue的原型上定义一个init的方法，而init方法只在在实例化Vue时执行\n  initMixin(Vue);\n  stateMixin(Vue);\n  eventsMixin(Vue); //定义事件相关处理函数\n  lifecycleMixin(Vue); // 定义渲染相关的周期函数\n  renderMixin(Vue); // 定义渲染相关的函数\n\n  /*  */\n\n  function initUse(Vue) {\n    Vue.use = function (plugin) {\n      // 如果插件已经注册，则返回注册过的插件\n      var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));\n      if (installedPlugins.indexOf(plugin) > -1) {\n        return this\n      }\n\n      // additional parameters\n      var args = toArray(arguments, 1);\n      args.unshift(this);\n      // 1. plugin有install方法，则调用插件的install，没有install方法且本身为函数，则调用plugin函数执行\n      if (typeof plugin.install === 'function') {\n        plugin.install.apply(plugin, args);\n      } else if (typeof plugin === 'function') {\n        plugin.apply(null, args);\n      }\n      installedPlugins.push(plugin);\n      return this\n    };\n  }\n\n  /*  */\n\n  function initMixin$1(Vue) {\n    Vue.mixin = function (mixin) {\n      this.options = mergeOptions(this.options, mixin);\n      return this\n    };\n  }\n\n  /*  */\n\n  function initExtend(Vue) {\n    /**\n     * Each instance constructor, including Vue, has a unique\n     * cid. This enables us to create wrapped \"child\n     * constructors\" for prototypal inheritance and cache them.\n     */\n    Vue.cid = 0;\n    var cid = 1;\n\n    /**\n     * Class inheritance\n     */\n    Vue.extend = function (extendOptions) {\n      extendOptions = extendOptions || {};\n      var Super = this;\n      var SuperId = Super.cid;\n      var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});\n      if (cachedCtors[SuperId]) {\n        return cachedCtors[SuperId]\n      }\n\n      var name = extendOptions.name || Super.options.name;\n      if (name) {\n        validateComponentName(name); // 校验子类的名称是否符合规范\n      }\n\n      var Sub = function VueComponent(options) { // 子类构造器\n        this._init(options);\n      };\n      Sub.prototype = Object.create(Super.prototype); // 子类继承于父类\n      Sub.prototype.constructor = Sub;\n      Sub.cid = cid++;\n      // 父类配置和子类配置合并\n      Sub.options = mergeOptions(\n        Super.options,\n        extendOptions\n      );\n      // 子类构造器的super属性指向父类构造器\n      Sub['super'] = Super;\n\n      // For props and computed properties, we define the proxy getters on\n      // the Vue instances at extension time, on the extended prototype. This\n      // avoids Object.defineProperty calls for each instance created.\n      if (Sub.options.props) {\n        initProps$1(Sub);\n      }\n      if (Sub.options.computed) {\n        initComputed$1(Sub);\n      }\n\n      // allow further extension/mixin/plugin usage\n      // 同时子类构造器拥有extend等方法\n      Sub.extend = Super.extend;\n      Sub.mixin = Super.mixin;\n      Sub.use = Super.use;\n\n      // create asset registers, so extended classes\n      // can have their private assets too.\n      ASSET_TYPES.forEach(function (type) {\n        Sub[type] = Super[type];\n      });\n      // enable recursive self-lookup\n      if (name) {\n        Sub.options.components[name] = Sub;\n      }\n\n      // keep a reference to the super options at extension time.\n      // later at instantiation we can check if Super's options have\n      // been updated.\n      Sub.superOptions = Super.options;\n      Sub.extendOptions = extendOptions;\n      Sub.sealedOptions = extend({}, Sub.options);\n\n      // cache constructor\n      cachedCtors[SuperId] = Sub;\n      return Sub\n    };\n  }\n\n  function initProps$1(Comp) {\n    var props = Comp.options.props;\n    for (var key in props) {\n      proxy(Comp.prototype, \"_props\", key);\n    }\n  }\n\n  function initComputed$1(Comp) {\n    var computed = Comp.options.computed;\n    for (var key in computed) {\n      defineComputed(Comp.prototype, key, computed[key]);\n    }\n  }\n\n  /*  */\n\n  function initAssetRegisters(Vue) {\n    /**\n     * Create asset registration methods.\n     */\n    ASSET_TYPES.forEach(function (type) {\n      Vue[type] = function (\n        id,\n        definition\n      ) {\n        if (!definition) {\n          // 直接返回注册组件的构造函数\n          return this.options[type + 's'][id]\n        } else {\n          /* istanbul ignore if */\n          if (type === 'component') {\n            // 验证component组件名字是否合法\n            validateComponentName(id);\n          }\n          if (type === 'component' && isPlainObject(definition)) {\n            // 组件名称设置\n            definition.name = definition.name || id;\n            // Vue.extend() 创建子组件，返回子类构造器\n            definition = this.options._base.extend(definition);\n          }\n          if (type === 'directive' && typeof definition === 'function') {\n            definition = {\n              bind: definition,\n              update: definition\n            };\n          }\n          // 将创建的子类构造器存储到根Vue配置下的component中。\n          this.options[type + 's'][id] = definition;\n          return definition\n        }\n      };\n    });\n  }\n\n  /*  */\n\n\n\n  function getComponentName(opts) {\n    return opts && (opts.Ctor.options.name || opts.tag)\n  }\n\n  function matches(pattern, name) {\n    if (Array.isArray(pattern)) {\n      return pattern.indexOf(name) > -1\n    } else if (typeof pattern === 'string') {\n      return pattern.split(',').indexOf(name) > -1\n    } else if (isRegExp(pattern)) {\n      return pattern.test(name)\n    }\n    /* istanbul ignore next */\n    return false\n  }\n\n  function pruneCache(keepAliveInstance, filter) {\n    var cache = keepAliveInstance.cache;\n    var keys = keepAliveInstance.keys;\n    var _vnode = keepAliveInstance._vnode;\n    for (var key in cache) {\n      var cachedNode = cache[key];\n      if (cachedNode) {\n        var name = getComponentName(cachedNode.componentOptions);\n        if (name && !filter(name)) {\n          pruneCacheEntry(cache, key, keys, _vnode);\n        }\n      }\n    }\n  }\n\n  function pruneCacheEntry(\n    cache,\n    key,\n    keys,\n    current\n  ) {\n    var cached###1 = cache[key];\n    if (cached###1 && (!current || cached###1.tag !== current.tag)) {\n      cached###1.componentInstance.$destroy();\n    }\n    cache[key] = null;\n    remove(keys, key);\n  }\n\n  var patternTypes = [String, RegExp, Array];\n\n  // keepalive组件选项\n  var KeepAlive = {\n    name: 'keep-alive',\n    abstract: true,\n\n    props: {\n      include: patternTypes,\n      exclude: patternTypes,\n      max: [String, Number]\n    },\n\n    created: function created() {\n      // 缓存组件vnode\n      this.cache = Object.create(null);\n      // 组件名\n      this.keys = [];\n    },\n\n    destroyed: function destroyed() {\n      for (var key in this.cache) {\n        pruneCacheEntry(this.cache, key, this.keys);\n      }\n    },\n\n    mounted: function mounted() {\n      var this$1 = this;\n      // 动态include和exclude\n      this.$watch('include', function (val) {\n        pruneCache(this$1, function (name) {\n          return matches(val, name);\n        });\n      });\n      this.$watch('exclude', function (val) {\n        pruneCache(this$1, function (name) {\n          return !matches(val, name);\n        });\n      });\n    },\n\n    render: function render() {\n      // 拿到keep-alive下插槽的值\n      var slot = this.$slots.default;\n      // 第一个vnode节点\n      var vnode = getFirstComponentChild(slot);\n      // 拿到第一个组件实例\n      var componentOptions = vnode && vnode.componentOptions;\n      // keep-alive的第一个子组件实例存在\n      if (componentOptions) {\n        // check pattern\n        //拿到第一个vnode节点的name\n        var name = getComponentName(componentOptions);\n        var ref = this;\n        var include = ref.include;\n        var exclude = ref.exclude;\n        // 通过判断子组件是否满足缓存匹配\n        if (\n          // not included\n          (include && (!name || !matches(include, name))) ||\n          // excluded\n          (exclude && name && matches(exclude, name))\n        ) {\n          return vnode\n        }\n\n        var ref$1 = this;\n        var cache = ref$1.cache;\n        var keys = ref$1.keys;\n        var key = vnode.key == null\n          // same constructor may get registered as different local components\n          // so cid alone is not enough (#3269)\n          ?\n          componentOptions.Ctor.cid + (componentOptions.tag ? (\"::\" + (componentOptions.tag)) : '') :\n          vnode.key;\n        if (cache[key]) {\n          vnode.componentInstance = cache[key].componentInstance;\n          // make current key freshest\n          remove(keys, key);\n          keys.push(key);\n        } else {\n          cache[key] = vnode;\n          keys.push(key);\n          // prune oldest entry\n          if (this.max && keys.length > parseInt(this.max)) {\n            pruneCacheEntry(cache, keys[0], keys, this._vnode);\n          }\n        }\n\n        vnode.data.keepAlive = true;\n      }\n      return vnode || (slot && slot[0])\n    }\n  };\n\n  var builtInComponents = {\n    KeepAlive: KeepAlive\n  };\n\n  /*  */\n\n  function initGlobalAPI(Vue) {\n    // config\n    var configDef = {};\n    configDef.get = function () {\n      return config;\n    }; {\n      configDef.set = function () {\n        warn(\n          'Do not replace the Vue.config object, set individual fields instead.'\n        );\n      };\n    }\n    Object.defineProperty(Vue, 'config', configDef);\n\n    // exposed util methods.\n    // NOTE: these are not considered part of the public API - avoid relying on\n    // them unless you are aware of the risk.\n    Vue.util = {\n      warn: warn,\n      extend: extend,\n      mergeOptions: mergeOptions,\n      defineReactive: defineReactive###1\n    };\n\n    Vue.set = set;\n    Vue.delete = del;\n    Vue.nextTick = nextTick;\n\n    // 2.6 explicit observable API\n    Vue.observable = function (obj) {\n      observe(obj);\n      return obj\n    };\n\n    Vue.options = Object.create(null);\n    ASSET_TYPES.forEach(function (type) {\n      Vue.options[type + 's'] = Object.create(null);\n    });\n\n    // this is used to identify the \"base\" constructor to extend all plain-object\n    // components with in Weex's multi-instance scenarios.\n    // options里的_base属性存储Vue构造器\n    Vue.options._base = Vue;\n    extend(Vue.options.components, builtInComponents);\n\n    initUse(Vue);\n    initMixin$1(Vue);\n    // 定义extend扩展子类构造器的方法\n    initExtend(Vue);\n    // \n    initAssetRegisters(Vue);\n  }\n\n  // 初始化全局的api\n  initGlobalAPI(Vue);\n\n  Object.defineProperty(Vue.prototype, '$isServer', {\n    get: isServerRendering\n  });\n\n  Object.defineProperty(Vue.prototype, '$ssrContext', {\n    get: function get() {\n      /* istanbul ignore next */\n      return this.$vnode && this.$vnode.ssrContext\n    }\n  });\n\n  // expose FunctionalRenderContext for ssr runtime helper installation\n  Object.defineProperty(Vue, 'FunctionalRenderContext', {\n    value: FunctionalRenderContext\n  });\n\n  Vue.version = '2.6.8';\n\n  /*  */\n\n  // these are reserved for web because they are directly compiled away\n  // during template compilation\n  var isReservedAttr = makeMap('style,class');\n\n  // attributes that should be using props for binding\n  var acceptValue = makeMap('input,textarea,option,select,progress');\n  var mustUseProp = function (tag, type, attr) {\n    return (\n      (attr === 'value' && acceptValue(tag)) && type !== 'button' ||\n      (attr === 'selected' && tag === 'option') ||\n      (attr === 'checked' && tag === 'input') ||\n      (attr === 'muted' && tag === 'video')\n    )\n  };\n\n  var isEnumeratedAttr = makeMap('contenteditable,draggable,spellcheck');\n\n  var isValidContentEditableValue = makeMap('events,caret,typing,plaintext-only');\n\n  var convertEnumeratedValue = function (key, value) {\n    return isFalsyAttrValue(value) || value === 'false' ?\n      'false'\n      // allow arbitrary string value for contenteditable\n      :\n      key === 'contenteditable' && isValidContentEditableValue(value) ?\n      value :\n      'true'\n  };\n\n  var isBooleanAttr = makeMap(\n    'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +\n    'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +\n    'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +\n    'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +\n    'required,reversed,scoped,seamless,selected,sortable,translate,' +\n    'truespeed,typemustmatch,visible'\n  );\n\n  var xlinkNS = 'http://www.w3.org/1999/xlink';\n\n  var isXlink = function (name) {\n    return name.charAt(5) === ':' && name.slice(0, 5) === 'xlink'\n  };\n\n  var getXlinkProp = function (name) {\n    return isXlink(name) ? name.slice(6, name.length) : ''\n  };\n\n  var isFalsyAttrValue = function (val) {\n    return val == null || val === false\n  };\n\n  /*  */\n\n  function genClassForVnode(vnode) {\n    var data = vnode.data;\n    var parentNode = vnode;\n    var childNode = vnode;\n    while (isDef(childNode.componentInstance)) {\n      childNode = childNode.componentInstance._vnode;\n      if (childNode && childNode.data) {\n        data = mergeClassData(childNode.data, data);\n      }\n    }\n    while (isDef(parentNode = parentNode.parent)) {\n      if (parentNode && parentNode.data) {\n        data = mergeClassData(data, parentNode.data);\n      }\n    }\n    return renderClass(data.staticClass, data.class)\n  }\n\n  function mergeClassData(child, parent) {\n    return {\n      staticClass: concat(child.staticClass, parent.staticClass),\n      class: isDef(child.class) ?\n        [child.class, parent.class] :\n        parent.class\n    }\n  }\n\n  function renderClass(\n    staticClass,\n    dynamicClass\n  ) {\n    if (isDef(staticClass) || isDef(dynamicClass)) {\n      return concat(staticClass, stringifyClass(dynamicClass))\n    }\n    /* istanbul ignore next */\n    return ''\n  }\n\n  function concat(a, b) {\n    return a ? b ? (a + ' ' + b) : a : (b || '')\n  }\n\n  function stringifyClass(value) {\n    if (Array.isArray(value)) {\n      return stringifyArray(value)\n    }\n    if (isObject(value)) {\n      return stringifyObject(value)\n    }\n    if (typeof value === 'string') {\n      return value\n    }\n    /* istanbul ignore next */\n    return ''\n  }\n\n  function stringifyArray(value) {\n    var res = '';\n    var stringified;\n    for (var i = 0, l = value.length; i < l; i++) {\n      if (isDef(stringified = stringifyClass(value[i])) && stringified !== '') {\n        if (res) {\n          res += ' ';\n        }\n        res += stringified;\n      }\n    }\n    return res\n  }\n\n  function stringifyObject(value) {\n    var res = '';\n    for (var key in value) {\n      if (value[key]) {\n        if (res) {\n          res += ' ';\n        }\n        res += key;\n      }\n    }\n    return res\n  }\n\n  /*  */\n\n  var namespaceMap = {\n    svg: 'http://www.w3.org/2000/svg',\n    math: 'http://www.w3.org/1998/Math/MathML'\n  };\n\n  var isHTMLTag = makeMap(\n    'html,body,base,head,link,meta,style,title,' +\n    'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +\n    'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +\n    'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +\n    's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +\n    'embed,object,param,source,canvas,script,noscript,del,ins,' +\n    'caption,col,colgroup,table,thead,tbody,td,th,tr,' +\n    'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +\n    'output,progress,select,textarea,' +\n    'details,dialog,menu,menuitem,summary,' +\n    'content,element,shadow,template,blockquote,iframe,tfoot'\n  );\n\n  // this map is intentionally selective, only covering SVG elements that may\n  // contain child elements.\n  var isSVG = makeMap(\n    'svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' +\n    'foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' +\n    'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view',\n    true\n  );\n\n  var isPreTag = function (tag) {\n    return tag === 'pre';\n  };\n\n  var isReservedTag = function (tag) {\n    return isHTMLTag(tag) || isSVG(tag)\n  };\n\n  function getTagNamespace(tag) {\n    if (isSVG(tag)) {\n      return 'svg'\n    }\n    // basic support for MathML\n    // note it doesn't support other MathML elements being component roots\n    if (tag === 'math') {\n      return 'math'\n    }\n  }\n\n  var unknownElementCache = Object.create(null);\n\n  function isUnknownElement(tag) {\n    /* istanbul ignore if */\n    if (!inBrowser) {\n      return true\n    }\n    if (isReservedTag(tag)) {\n      return false\n    }\n    tag = tag.toLowerCase();\n    /* istanbul ignore if */\n    if (unknownElementCache[tag] != null) {\n      return unknownElementCache[tag]\n    }\n    var el = document.createElement(tag);\n    if (tag.indexOf('-') > -1) {\n      // http://stackoverflow.com/a/28210364/1070244\n      return (unknownElementCache[tag] = (\n        el.constructor === window.HTMLUnknownElement ||\n        el.constructor === window.HTMLElement\n      ))\n    } else {\n      return (unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()))\n    }\n  }\n\n  var isTextInputType = makeMap('text,number,password,search,email,tel,url');\n\n  /*  */\n\n  /**\n   * Query an element selector if it's not an element already.\n   */\n  function query(el) {\n    if (typeof el === 'string') {\n      var selected = document.querySelector(el);\n      if (!selected) {\n        warn(\n          'Cannot find element: ' + el\n        );\n        return document.createElement('div')\n      }\n      return selected\n    } else {\n      return el\n    }\n  }\n\n  /*  */\n\n  function createElement$1(tagName, vnode) {\n    var elm = document.createElement(tagName);\n    if (tagName !== 'select') {\n      return elm\n    }\n    // false or null will remove the attribute but undefined will not\n    if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {\n      elm.setAttribute('multiple', 'multiple');\n    }\n    return elm\n  }\n\n  function createElementNS(namespace, tagName) {\n    return document.createElementNS(namespaceMap[namespace], tagName)\n  }\n\n  function createTextNode(text) {\n    return document.createTextNode(text)\n  }\n\n  function createComment(text) {\n    return document.createComment(text)\n  }\n\n  function insertBefore(parentNode, newNode, referenceNode) {\n    parentNode.insertBefore(newNode, referenceNode);\n  }\n\n  function removeChild(node, child) {\n    node.removeChild(child);\n  }\n\n  function appendChild(node, child) {\n    node.appendChild(child);\n  }\n\n  function parentNode(node) {\n    return node.parentNode\n  }\n\n  function nextSibling(node) {\n    return node.nextSibling\n  }\n\n  function tagName(node) {\n    return node.tagName\n  }\n\n  function setTextContent(node, text) {\n    node.textContent = text;\n  }\n\n  function setStyleScope(node, scopeId) {\n    node.setAttribute(scopeId, '');\n  }\n\n  // 封装了一系列DOM操作的方法\n  var nodeOps = /*#__PURE__*/ Object.freeze({\n    createElement: createElement$1,\n    createElementNS: createElementNS,\n    createTextNode: createTextNode,\n    createComment: createComment,\n    insertBefore: insertBefore,\n    removeChild: removeChild,\n    appendChild: appendChild,\n    parentNode: parentNode,\n    nextSibling: nextSibling,\n    tagName: tagName,\n    setTextContent: setTextContent,\n    setStyleScope: setStyleScope\n  });\n\n  /*  */\n\n  var ref = {\n    create: function create(_, vnode) {\n      registerRef(vnode);\n    },\n    update: function update(oldVnode, vnode) {\n      if (oldVnode.data.ref !== vnode.data.ref) {\n        registerRef(oldVnode, true);\n        registerRef(vnode);\n      }\n    },\n    destroy: function destroy(vnode) {\n      registerRef(vnode, true);\n    }\n  };\n\n  function registerRef(vnode, isRemoval) {\n    var key = vnode.data.ref;\n    if (!isDef(key)) {\n      return\n    }\n\n    var vm = vnode.context;\n    var ref = vnode.componentInstance || vnode.elm;\n    var refs = vm.$refs;\n    if (isRemoval) {\n      if (Array.isArray(refs[key])) {\n        remove(refs[key], ref);\n      } else if (refs[key] === ref) {\n        refs[key] = undefined;\n      }\n    } else {\n      if (vnode.data.refInFor) {\n        if (!Array.isArray(refs[key])) {\n          refs[key] = [ref];\n        } else if (refs[key].indexOf(ref) < 0) {\n          // $flow-disable-line\n          refs[key].push(ref);\n        }\n      } else {\n        refs[key] = ref;\n      }\n    }\n  }\n\n  /**\n   * Virtual DOM patching algorithm based on Snabbdom by\n   * Simon Friis Vindum (@paldepind)\n   * Licensed under the MIT License\n   * https://github.com/paldepind/snabbdom/blob/master/LICENSE\n   *\n   * modified by Evan You (@yyx990803)\n   *\n   * Not type-checking this because this file is perf-critical and the cost\n   * of making flow understand it is not worth it.\n   */\n\n  var emptyNode = new VNode('', {}, []);\n\n  var hooks = ['create', 'activate', 'update', 'remove', 'destroy'];\n\n  function sameVnode(a, b) {\n    return (\n      a.key === b.key && (\n        (\n          a.tag === b.tag &&\n          a.isComment === b.isComment &&\n          isDef(a.data) === isDef(b.data) &&\n          sameInputType(a, b)\n        ) || (\n          isTrue(a.isAsyncPlaceholder) &&\n          a.asyncFactory === b.asyncFactory &&\n          isUndef(b.asyncFactory.error)\n        )\n      )\n    )\n  }\n\n  function sameInputType(a, b) {\n    if (a.tag !== 'input') {\n      return true\n    }\n    var i;\n    var typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type;\n    var typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type;\n    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)\n  }\n\n  function createKeyToOldIdx(children, beginIdx, endIdx) {\n    var i, key;\n    var map = {};\n    for (i = beginIdx; i <= endIdx; ++i) {\n      key = children[i].key;\n      if (isDef(key)) {\n        map[key] = i;\n      }\n    }\n    return map\n  }\n\n  function createPatchFunction(backend) {\n    var i, j;\n    var cbs = {};\n\n    var modules = backend.modules;\n    var nodeOps = backend.nodeOps;\n\n    for (i = 0; i < hooks.length; ++i) {\n      cbs[hooks[i]] = [];\n      for (j = 0; j < modules.length; ++j) {\n        if (isDef(modules[j][hooks[i]])) {\n          cbs[hooks[i]].push(modules[j][hooks[i]]);\n        }\n      }\n    }\n\n    function emptyNodeAt(elm) {\n      return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)\n    }\n\n    function createRmCb(childElm, listeners) {\n      function remove###1() {\n        if (--remove###1.listeners === 0) {\n          removeNode(childElm);\n        }\n      }\n      remove###1.listeners = listeners;\n      return remove###1\n    }\n\n    function removeNode(el) {\n      var parent = nodeOps.parentNode(el);\n      // element may have already been removed due to v-html / v-text\n      if (isDef(parent)) {\n        nodeOps.removeChild(parent, el);\n      }\n    }\n\n    function isUnknownElement###1(vnode, inVPre) {\n      return (\n        !inVPre &&\n        !vnode.ns &&\n        !(\n          config.ignoredElements.length &&\n          config.ignoredElements.some(function (ignore) {\n            return isRegExp(ignore) ?\n              ignore.test(vnode.tag) :\n              ignore === vnode.tag\n          })\n        ) &&\n        config.isUnknownElement(vnode.tag)\n      )\n    }\n\n    var creatingElmInVPre = 0;\n\n    // 创建真实dom\n    function createElm(\n      vnode,\n      insertedVnodeQueue,\n      parentElm,\n      refElm,\n      nested,\n      ownerArray,\n      index\n    ) {\n      if (isDef(vnode.elm) && isDef(ownerArray)) {\n        // This vnode was used in a previous render!\n        // now it's used as a new node, overwriting its elm would cause\n        // potential patch errors down the road when it's used as an insertion\n        // reference node. Instead, we clone the node on-demand before creating\n        // associated DOM element for it.\n        vnode = ownerArray[index] = cloneVNode(vnode);\n      }\n\n      vnode.isRootInsert = !nested; // for transition enter check\n\n      // 递归创建子组件真实节点\n      if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {\n        return\n      }\n\n      var data = vnode.data;\n      // 子vnode节点\n      var children = vnode.children;\n      var tag = vnode.tag;\n      if (isDef(tag)) {\n        {\n          if (data && data.pre) {\n            creatingElmInVPre++;\n          }\n          if (isUnknownElement###1(vnode, creatingElmInVPre)) {\n            warn(\n              'Unknown custom element: <' + tag + '> - did you ' +\n              'register the component correctly? For recursive components, ' +\n              'make sure to provide the \"name\" option.',\n              vnode.context\n            );\n          }\n        }\n\n        vnode.elm = vnode.ns ?\n          nodeOps.createElementNS(vnode.ns, tag) :\n          nodeOps.createElement(tag, vnode);\n        setScope(vnode);\n\n        /* istanbul ignore if */\n        {\n          createChildren(vnode, children, insertedVnodeQueue);\n          // 处理data属性相关逻辑\n          if (isDef(data)) {\n            invokeCreateHooks(vnode, insertedVnodeQueue);\n          }\n          insert(parentElm, vnode.elm, refElm);\n        }\n\n        if (data && data.pre) {\n          creatingElmInVPre--;\n        }\n      } else if (isTrue(vnode.isComment)) {\n        vnode.elm = nodeOps.createComment(vnode.text);\n        insert(parentElm, vnode.elm, refElm);\n      } else {\n        vnode.elm = nodeOps.createTextNode(vnode.text);\n        insert(parentElm, vnode.elm, refElm);\n      }\n    }\n\n    function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {\n      var i = vnode.data;\n      if (isDef(i)) {\n        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;\n        if (isDef(i = i.hook) && isDef(i = i.init)) {\n          i(vnode, false /* hydrating */ );\n        }\n        // after calling the init hook, if the vnode is a child component\n        // it should've created a child instance and mounted it. the child\n        // component also has set the placeholder vnode's elm.\n        // in that case we can just return the element and be done.\n        if (isDef(vnode.componentInstance)) {\n          // 其中一个作用是保留真实dom到vnode中\n          initComponent(vnode, insertedVnodeQueue);\n          insert(parentElm, vnode.elm, refElm);\n          if (isTrue(isReactivated)) {\n            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);\n          }\n          return true\n        }\n      }\n    }\n\n    function initComponent(vnode, insertedVnodeQueue) {\n      if (isDef(vnode.data.pendingInsert)) {\n        insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);\n        vnode.data.pendingInsert = null;\n      }\n      // vnode保留真实节点\n      vnode.elm = vnode.componentInstance.$el;\n      if (isPatchable(vnode)) {\n        invokeCreateHooks(vnode, insertedVnodeQueue);\n        setScope(vnode);\n      } else {\n        // empty component root.\n        // skip all element-related modules except for ref (#3455)\n        registerRef(vnode);\n        // make sure to invoke the insert hook\n        insertedVnodeQueue.push(vnode);\n      }\n    }\n\n    function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {\n      var i;\n      // hack for #4339: a reactivated component with inner transition\n      // does not trigger because the inner node's created hooks are not called\n      // again. It's not ideal to involve module-specific logic in here but\n      // there doesn't seem to be a better way to do it.\n      var innerNode = vnode;\n      while (innerNode.componentInstance) {\n        innerNode = innerNode.componentInstance._vnode;\n        if (isDef(i = innerNode.data) && isDef(i = i.transition)) {\n          for (i = 0; i < cbs.activate.length; ++i) {\n            cbs.activate[i](emptyNode, innerNode);\n          }\n          insertedVnodeQueue.push(innerNode);\n          break\n        }\n      }\n      // unlike a newly created component,\n      // a reactivated keep-alive component doesn't insert itself\n      insert(parentElm, vnode.elm, refElm);\n    }\n\n    function insert(parent, elm, ref###1) {\n      if (isDef(parent)) {\n        if (isDef(ref###1)) {\n          if (nodeOps.parentNode(ref###1) === parent) {\n            nodeOps.insertBefore(parent, elm, ref###1);\n          }\n        } else {\n          nodeOps.appendChild(parent, elm);\n        }\n      }\n    }\n\n    function createChildren(vnode, children, insertedVnodeQueue) {\n      if (Array.isArray(children)) {\n        {\n          checkDuplicateKeys(children);\n        }\n        for (var i = 0; i < children.length; ++i) {\n          createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);\n        }\n      } else if (isPrimitive(vnode.text)) {\n        nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));\n      }\n    }\n\n    function isPatchable(vnode) {\n      while (vnode.componentInstance) {\n        vnode = vnode.componentInstance._vnode;\n      }\n      return isDef(vnode.tag)\n    }\n\n    function invokeCreateHooks(vnode, insertedVnodeQueue) {\n      for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {\n        cbs.create[i$1](emptyNode, vnode);\n      }\n      i = vnode.data.hook; // Reuse variable\n      if (isDef(i)) {\n        if (isDef(i.create)) {\n          i.create(emptyNode, vnode);\n        }\n        if (isDef(i.insert)) {\n          insertedVnodeQueue.push(vnode);\n        }\n      }\n    }\n\n    // set scope id attribute for scoped CSS.\n    // this is implemented as a special case to avoid the overhead\n    // of going through the normal attribute patching process.\n    function setScope(vnode) {\n      var i;\n      if (isDef(i = vnode.fnScopeId)) {\n        nodeOps.setStyleScope(vnode.elm, i);\n      } else {\n        var ancestor = vnode;\n        while (ancestor) {\n          if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {\n            nodeOps.setStyleScope(vnode.elm, i);\n          }\n          ancestor = ancestor.parent;\n        }\n      }\n      // for slot content they should also get the scopeId from the host instance.\n      if (isDef(i = activeInstance) &&\n        i !== vnode.context &&\n        i !== vnode.fnContext &&\n        isDef(i = i.$options._scopeId)\n      ) {\n        nodeOps.setStyleScope(vnode.elm, i);\n      }\n    }\n\n    function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {\n      for (; startIdx <= endIdx; ++startIdx) {\n        createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx);\n      }\n    }\n\n    function invokeDestroyHook(vnode) {\n      var i, j;\n      var data = vnode.data;\n      if (isDef(data)) {\n        if (isDef(i = data.hook) && isDef(i = i.destroy)) {\n          i(vnode);\n        }\n        for (i = 0; i < cbs.destroy.length; ++i) {\n          cbs.destroy[i](vnode);\n        }\n      }\n      if (isDef(i = vnode.children)) {\n        for (j = 0; j < vnode.children.length; ++j) {\n          invokeDestroyHook(vnode.children[j]);\n        }\n      }\n    }\n\n    function removeVnodes(parentElm, vnodes, startIdx, endIdx) {\n      debugger\n      for (; startIdx <= endIdx; ++startIdx) {\n        var ch = vnodes[startIdx];\n        if (isDef(ch)) {\n          if (isDef(ch.tag)) {\n            removeAndInvokeRemoveHook(ch);\n            invokeDestroyHook(ch);\n          } else { // Text node\n            removeNode(ch.elm);\n          }\n        }\n      }\n    }\n\n    function removeAndInvokeRemoveHook(vnode, rm) {\n      if (isDef(rm) || isDef(vnode.data)) {\n        var i;\n        var listeners = cbs.remove.length + 1;\n        if (isDef(rm)) {\n          // we have a recursively passed down rm callback\n          // increase the listeners count\n          rm.listeners += listeners;\n        } else {\n          // directly removing\n          rm = createRmCb(vnode.elm, listeners);\n        }\n        // recursively invoke hooks on child component root node\n        if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {\n          removeAndInvokeRemoveHook(i, rm);\n        }\n        for (i = 0; i < cbs.remove.length; ++i) {\n          cbs.remove[i](vnode, rm);\n        }\n        if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {\n          i(vnode, rm);\n        } else {\n          rm();\n        }\n      } else {\n        removeNode(vnode.elm);\n      }\n    }\n\n    function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {\n      var oldStartIdx = 0;\n      var newStartIdx = 0;\n      var oldEndIdx = oldCh.length - 1;\n      var oldStartVnode = oldCh[0];\n      var oldEndVnode = oldCh[oldEndIdx];\n      var newEndIdx = newCh.length - 1;\n      var newStartVnode = newCh[0];\n      var newEndVnode = newCh[newEndIdx];\n      var oldKeyToIdx, idxInOld, vnodeToMove, refElm;\n\n      // removeOnly is a special flag used only by <transition-group>\n      // to ensure removed elements stay in correct relative positions\n      // during leaving transitions\n      var canMove = !removeOnly;\n\n      {\n        checkDuplicateKeys(newCh);\n      }\n\n      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {\n        if (isUndef(oldStartVnode)) {\n          oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left\n        } else if (isUndef(oldEndVnode)) {\n          oldEndVnode = oldCh[--oldEndIdx];\n        } else if (sameVnode(oldStartVnode, newStartVnode)) {\n          patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);\n          oldStartVnode = oldCh[++oldStartIdx];\n          newStartVnode = newCh[++newStartIdx];\n        } else if (sameVnode(oldEndVnode, newEndVnode)) {\n          patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);\n          oldEndVnode = oldCh[--oldEndIdx];\n          newEndVnode = newCh[--newEndIdx];\n        } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right\n          patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);\n          canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));\n          oldStartVnode = oldCh[++oldStartIdx];\n          newEndVnode = newCh[--newEndIdx];\n        } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left\n          patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);\n          canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);\n          oldEndVnode = oldCh[--oldEndIdx];\n          newStartVnode = newCh[++newStartIdx];\n        } else {\n          // key的作用: 如果有key会建立一个 key 和 索引的字典，方便搜索key值相同的节点，如果没有字典则需要调用findIdxInOld去搜索\n          if (isUndef(oldKeyToIdx)) {\n            oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);\n          }\n          idxInOld = isDef(newStartVnode.key) ?\n            oldKeyToIdx[newStartVnode.key] :\n            findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);\n          if (isUndef(idxInOld)) { // New element\n            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);\n          } else {\n            vnodeToMove = oldCh[idxInOld];\n            if (sameVnode(vnodeToMove, newStartVnode)) {\n              patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);\n              oldCh[idxInOld] = undefined;\n              canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);\n            } else {\n              // same key but different element. treat as new element\n              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);\n            }\n          }\n          newStartVnode = newCh[++newStartIdx];\n        }\n      }\n      if (oldStartIdx > oldEndIdx) {\n        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;\n        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);\n      } else if (newStartIdx > newEndIdx) {\n        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);\n      }\n    }\n\n    function checkDuplicateKeys(children) {\n      var seenKeys = {};\n      for (var i = 0; i < children.length; i++) {\n        var vnode = children[i];\n        var key = vnode.key;\n        if (isDef(key)) {\n          if (seenKeys[key]) {\n            warn(\n              (\"Duplicate keys detected: '\" + key + \"'. This may cause an update error.\"),\n              vnode.context\n            );\n          } else {\n            seenKeys[key] = true;\n          }\n        }\n      }\n    }\n\n    function findIdxInOld(node, oldCh, start, end) {\n      for (var i = start; i < end; i++) {\n        var c = oldCh[i];\n        if (isDef(c) && sameVnode(node, c)) {\n          return i\n        }\n      }\n    }\n\n    function patchVnode(\n      oldVnode,\n      vnode,\n      insertedVnodeQueue,\n      ownerArray,\n      index,\n      removeOnly\n    ) {\n      if (oldVnode === vnode) {\n        return\n      }\n      if (isDef(vnode.elm) && isDef(ownerArray)) {\n        // clone reused vnode\n        vnode = ownerArray[index] = cloneVNode(vnode);\n      }\n\n      var elm = vnode.elm = oldVnode.elm;\n\n      if (isTrue(oldVnode.isAsyncPlaceholder)) {\n        if (isDef(vnode.asyncFactory.resolved)) {\n          hydrate(oldVnode.elm, vnode, insertedVnodeQueue);\n        } else {\n          vnode.isAsyncPlaceholder = true;\n        }\n        return\n      }\n\n      // reuse element for static trees.\n      // note we only do this if the vnode is cloned -\n      // if the new node is not cloned it means the render functions have been\n      // reset by the hot-reload-api and we need to do a proper re-render.\n      if (isTrue(vnode.isStatic) &&\n        isTrue(oldVnode.isStatic) &&\n        vnode.key === oldVnode.key &&\n        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))\n      ) {\n        vnode.componentInstance = oldVnode.componentInstance;\n        return\n      }\n\n      var i;\n      var data = vnode.data;\n      // 新vnode\n      if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {\n        i(oldVnode, vnode);\n      }\n\n      var oldCh = oldVnode.children;\n      var ch = vnode.children;\n      if (isDef(data) && isPatchable(vnode)) {\n        for (i = 0; i < cbs.update.length; ++i) {\n          cbs.update[i](oldVnode, vnode);\n        }\n        if (isDef(i = data.hook) && isDef(i = i.update)) {\n          i(oldVnode, vnode);\n        }\n      }\n      if (isUndef(vnode.text)) {\n        if (isDef(oldCh) && isDef(ch)) {\n          if (oldCh !== ch) {\n            updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);\n          }\n        } else if (isDef(ch)) {\n          {\n            checkDuplicateKeys(ch);\n          }\n          if (isDef(oldVnode.text)) {\n            nodeOps.setTextContent(elm, '');\n          }\n          addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);\n        } else if (isDef(oldCh)) {\n          removeVnodes(elm, oldCh, 0, oldCh.length - 1);\n        } else if (isDef(oldVnode.text)) {\n          nodeOps.setTextContent(elm, '');\n        }\n      } else if (oldVnode.text !== vnode.text) {\n        nodeOps.setTextContent(elm, vnode.text);\n      }\n      if (isDef(data)) {\n        if (isDef(i = data.hook) && isDef(i = i.postpatch)) {\n          i(oldVnode, vnode);\n        }\n      }\n    }\n\n    function invokeInsertHook(vnode, queue, initial) {\n      // delay insert hooks for component root nodes, invoke them after the\n      // element is really inserted\n      if (isTrue(initial) && isDef(vnode.parent)) {\n        vnode.parent.data.pendingInsert = queue;\n      } else {\n        for (var i = 0; i < queue.length; ++i) {\n          queue[i].data.hook.insert(queue[i]);\n        }\n      }\n    }\n\n    var hydrationBailed = false;\n    // list of modules that can skip create hook during hydration because they\n    // are already rendered on the client or has no need for initialization\n    // Note: style is excluded because it relies on initial clone for future\n    // deep updates (#7063).\n    var isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key');\n\n    // Note: this is a browser-only function so we can assume elms are DOM nodes.\n    function hydrate(elm, vnode, insertedVnodeQueue, inVPre) {\n      var i;\n      var tag = vnode.tag;\n      var data = vnode.data;\n      var children = vnode.children;\n      inVPre = inVPre || (data && data.pre);\n      vnode.elm = elm;\n\n      if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {\n        vnode.isAsyncPlaceholder = true;\n        return true\n      }\n      // assert node match\n      {\n        if (!assertNodeMatch(elm, vnode, inVPre)) {\n          return false\n        }\n      }\n      if (isDef(data)) {\n        if (isDef(i = data.hook) && isDef(i = i.init)) {\n          i(vnode, true /* hydrating */ );\n        }\n        if (isDef(i = vnode.componentInstance)) {\n          // child component. it should have hydrated its own tree.\n          initComponent(vnode, insertedVnodeQueue);\n          return true\n        }\n      }\n      if (isDef(tag)) {\n        if (isDef(children)) {\n          // empty element, allow client to pick up and populate children\n          if (!elm.hasChildNodes()) {\n            createChildren(vnode, children, insertedVnodeQueue);\n          } else {\n            // v-html and domProps: innerHTML\n            if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {\n              if (i !== elm.innerHTML) {\n                /* istanbul ignore if */\n                if (typeof console !== 'undefined' &&\n                  !hydrationBailed\n                ) {\n                  hydrationBailed = true;\n                  console.warn('Parent: ', elm);\n                  console.warn('server innerHTML: ', i);\n                  console.warn('client innerHTML: ', elm.innerHTML);\n                }\n                return false\n              }\n            } else {\n              // iterate and compare children lists\n              var childrenMatch = true;\n              var childNode = elm.firstChild;\n              for (var i$1 = 0; i$1 < children.length; i$1++) {\n                if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) {\n                  childrenMatch = false;\n                  break\n                }\n                childNode = childNode.nextSibling;\n              }\n              // if childNode is not null, it means the actual childNodes list is\n              // longer than the virtual children list.\n              if (!childrenMatch || childNode) {\n                /* istanbul ignore if */\n                if (typeof console !== 'undefined' &&\n                  !hydrationBailed\n                ) {\n                  hydrationBailed = true;\n                  console.warn('Parent: ', elm);\n                  console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children);\n                }\n                return false\n              }\n            }\n          }\n        }\n        if (isDef(data)) {\n          var fullInvoke = false;\n          for (var key in data) {\n            if (!isRenderedModule(key)) {\n              fullInvoke = true;\n              invokeCreateHooks(vnode, insertedVnodeQueue);\n              break\n            }\n          }\n          if (!fullInvoke && data['class']) {\n            // ensure collecting deps for deep class bindings for future updates\n            traverse(data['class']);\n          }\n        }\n      } else if (elm.data !== vnode.text) {\n        elm.data = vnode.text;\n      }\n      return true\n    }\n\n    function assertNodeMatch(node, vnode, inVPre) {\n      if (isDef(vnode.tag)) {\n        return vnode.tag.indexOf('vue-component') === 0 || (\n          !isUnknownElement###1(vnode, inVPre) &&\n          vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())\n        )\n      } else {\n        return node.nodeType === (vnode.isComment ? 8 : 3)\n      }\n    }\n\n    return function patch(oldVnode, vnode, hydrating, removeOnly) {\n      if (isUndef(vnode)) {\n        if (isDef(oldVnode)) {\n          invokeDestroyHook(oldVnode);\n        }\n        return\n      }\n      var isInitialPatch = false;\n      var insertedVnodeQueue = [];\n\n      if (isUndef(oldVnode)) {\n        // empty mount (likely as component), create new root element\n        isInitialPatch = true;\n        createElm(vnode, insertedVnodeQueue);\n      } else {\n        // 判断是否为真实节点\n        var isRealElement = isDef(oldVnode.nodeType);\n        if (!isRealElement && sameVnode(oldVnode, vnode)) {\n          // patch existing root node\n          patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);\n        } else {\n          if (isRealElement) {\n            // mounting to a real element\n            // check if this is server-rendered content and if we can perform\n            // a successful hydration.\n            if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {\n              oldVnode.removeAttribute(SSR_ATTR);\n              hydrating = true;\n            }\n            if (isTrue(hydrating)) {\n              if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {\n                invokeInsertHook(vnode, insertedVnodeQueue, true);\n                return oldVnode\n              } else {\n                warn(\n                  'The client-side rendered virtual DOM tree is not matching ' +\n                  'server-rendered content. This is likely caused by incorrect ' +\n                  'HTML markup, for example nesting block-level elements inside ' +\n                  '<p>, or missing <tbody>. Bailing hydration and performing ' +\n                  'full client-side render.'\n                );\n              }\n            }\n            // either not server-rendered, or hydration failed.\n            // create an empty node and replace it\n            oldVnode = emptyNodeAt(oldVnode);\n          }\n\n          // replacing existing element\n          var oldElm = oldVnode.elm;\n          var parentElm = nodeOps.parentNode(oldElm);\n\n          // create new node\n          createElm(\n            vnode,\n            insertedVnodeQueue,\n            // extremely rare edge case: do not insert if old element is in a\n            // leaving transition. Only happens when combining transition +\n            // keep-alive + HOCs. (#4590)\n            oldElm._leaveCb ? null : parentElm,\n            nodeOps.nextSibling(oldElm)\n          );\n\n          // update parent placeholder node element, recursively\n          if (isDef(vnode.parent)) {\n            var ancestor = vnode.parent;\n            var patchable = isPatchable(vnode);\n            while (ancestor) {\n              for (var i = 0; i < cbs.destroy.length; ++i) {\n                cbs.destroy[i](ancestor);\n              }\n              ancestor.elm = vnode.elm;\n              if (patchable) {\n                for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {\n                  cbs.create[i$1](emptyNode, ancestor);\n                }\n                // #6513\n                // invoke insert hooks that may have been merged by create hooks.\n                // e.g. for directives that uses the \"inserted\" hook.\n                var insert = ancestor.data.hook.insert;\n                if (insert.merged) {\n                  // start at index 1 to avoid re-invoking component mounted hook\n                  for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {\n                    insert.fns[i$2]();\n                  }\n                }\n              } else {\n                registerRef(ancestor);\n              }\n              ancestor = ancestor.parent;\n            }\n          }\n\n          // destroy old node\n          if (isDef(parentElm)) {\n            removeVnodes(parentElm, [oldVnode], 0, 0);\n          } else if (isDef(oldVnode.tag)) {\n            invokeDestroyHook(oldVnode);\n          }\n        }\n      }\n\n      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);\n      return vnode.elm\n    }\n  }\n\n  /*  */\n\n  var directives = {\n    create: updateDirectives,\n    update: updateDirectives,\n    destroy: function unbindDirectives(vnode) {\n      updateDirectives(vnode, emptyNode);\n    }\n  };\n\n  function updateDirectives(oldVnode, vnode) {\n    if (oldVnode.data.directives || vnode.data.directives) {\n      _update(oldVnode, vnode);\n    }\n  }\n\n  function _update(oldVnode, vnode) {\n    var isCreate = oldVnode === emptyNode;\n    var isDestroy = vnode === emptyNode;\n    var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);\n    var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);\n\n    var dirsWithInsert = [];\n    var dirsWithPostpatch = [];\n\n    var key, oldDir, dir;\n    for (key in newDirs) {\n      oldDir = oldDirs[key];\n      dir = newDirs[key];\n      if (!oldDir) {\n        // new directive, bind\n        callHook$1(dir, 'bind', vnode, oldVnode);\n        if (dir.def && dir.def.inserted) {\n          dirsWithInsert.push(dir);\n        }\n      } else {\n        // existing directive, update\n        dir.oldValue = oldDir.value;\n        dir.oldArg = oldDir.arg;\n        callHook$1(dir, 'update', vnode, oldVnode);\n        if (dir.def && dir.def.componentUpdated) {\n          dirsWithPostpatch.push(dir);\n        }\n      }\n    }\n\n    if (dirsWithInsert.length) {\n      var callInsert = function () {\n        for (var i = 0; i < dirsWithInsert.length; i++) {\n          callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);\n        }\n      };\n      if (isCreate) {\n        mergeVNodeHook(vnode, 'insert', callInsert);\n      } else {\n        callInsert();\n      }\n    }\n\n    if (dirsWithPostpatch.length) {\n      mergeVNodeHook(vnode, 'postpatch', function () {\n        for (var i = 0; i < dirsWithPostpatch.length; i++) {\n          callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);\n        }\n      });\n    }\n\n    if (!isCreate) {\n      for (key in oldDirs) {\n        if (!newDirs[key]) {\n          // no longer present, unbind\n          callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);\n        }\n      }\n    }\n  }\n\n  var emptyModifiers = Object.create(null);\n\n  function normalizeDirectives$1(\n    dirs,\n    vm\n  ) {\n    var res = Object.create(null);\n    if (!dirs) {\n      // $flow-disable-line\n      return res\n    }\n    var i, dir;\n    for (i = 0; i < dirs.length; i++) {\n      dir = dirs[i];\n      if (!dir.modifiers) {\n        // $flow-disable-line\n        dir.modifiers = emptyModifiers;\n      }\n      res[getRawDirName(dir)] = dir;\n      dir.def = resolveAsset(vm.$options, 'directives', dir.name, true);\n    }\n    // $flow-disable-line\n    return res\n  }\n\n  function getRawDirName(dir) {\n    return dir.rawName || ((dir.name) + \".\" + (Object.keys(dir.modifiers || {}).join('.')))\n  }\n\n  function callHook$1(dir, hook, vnode, oldVnode, isDestroy) {\n    var fn = dir.def && dir.def[hook];\n    if (fn) {\n      try {\n        fn(vnode.elm, dir, vnode, oldVnode, isDestroy);\n      } catch (e) {\n        handleError(e, vnode.context, (\"directive \" + (dir.name) + \" \" + hook + \" hook\"));\n      }\n    }\n  }\n\n  var baseModules = [\n    ref,\n    directives\n  ];\n\n  /*  */\n\n  function updateAttrs(oldVnode, vnode) {\n    var opts = vnode.componentOptions;\n    if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {\n      return\n    }\n    if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {\n      return\n    }\n    var key, cur, old;\n    var elm = vnode.elm;\n    var oldAttrs = oldVnode.data.attrs || {};\n    var attrs = vnode.data.attrs || {};\n    // clone observed objects, as the user probably wants to mutate it\n    if (isDef(attrs.__ob__)) {\n      attrs = vnode.data.attrs = extend({}, attrs);\n    }\n\n    for (key in attrs) {\n      cur = attrs[key];\n      old = oldAttrs[key];\n      if (old !== cur) {\n        setAttr(elm, key, cur);\n      }\n    }\n    // #4391: in IE9, setting type can reset value for input[type=radio]\n    // #6666: IE/Edge forces progress value down to 1 before setting a max\n    /* istanbul ignore if */\n    if ((isIE || isEdge) && attrs.value !== oldAttrs.value) {\n      setAttr(elm, 'value', attrs.value);\n    }\n    for (key in oldAttrs) {\n      if (isUndef(attrs[key])) {\n        if (isXlink(key)) {\n          elm.removeAttributeNS(xlinkNS, getXlinkProp(key));\n        } else if (!isEnumeratedAttr(key)) {\n          elm.removeAttribute(key);\n        }\n      }\n    }\n  }\n\n  function setAttr(el, key, value) {\n    if (el.tagName.indexOf('-') > -1) {\n      baseSetAttr(el, key, value);\n    } else if (isBooleanAttr(key)) {\n      // set attribute for blank value\n      // e.g. <option disabled>Select one</option>\n      if (isFalsyAttrValue(value)) {\n        el.removeAttribute(key);\n      } else {\n        // technically allowfullscreen is a boolean attribute for <iframe>,\n        // but Flash expects a value of \"true\" when used on <embed> tag\n        value = key === 'allowfullscreen' && el.tagName === 'EMBED' ?\n          'true' :\n          key;\n        el.setAttribute(key, value);\n      }\n    } else if (isEnumeratedAttr(key)) {\n      el.setAttribute(key, convertEnumeratedValue(key, value));\n    } else if (isXlink(key)) {\n      if (isFalsyAttrValue(value)) {\n        el.removeAttributeNS(xlinkNS, getXlinkProp(key));\n      } else {\n        el.setAttributeNS(xlinkNS, key, value);\n      }\n    } else {\n      baseSetAttr(el, key, value);\n    }\n  }\n\n  function baseSetAttr(el, key, value) {\n    if (isFalsyAttrValue(value)) {\n      el.removeAttribute(key);\n    } else {\n      // #7138: IE10 & 11 fires input event when setting placeholder on\n      // <textarea>... block the first input event and remove the blocker\n      // immediately.\n      /* istanbul ignore if */\n      if (\n        isIE && !isIE9 &&\n        el.tagName === 'TEXTAREA' &&\n        key === 'placeholder' && value !== '' && !el.__ieph\n      ) {\n        var blocker = function (e) {\n          e.stopImmediatePropagation();\n          el.removeEventListener('input', blocker);\n        };\n        el.addEventListener('input', blocker);\n        // $flow-disable-line\n        el.__ieph = true; /* IE placeholder patched */\n      }\n      el.setAttribute(key, value);\n    }\n  }\n\n  var attrs = {\n    create: updateAttrs,\n    update: updateAttrs\n  };\n\n  /*  */\n\n  function updateClass(oldVnode, vnode) {\n    var el = vnode.elm;\n    var data = vnode.data;\n    var oldData = oldVnode.data;\n    if (\n      isUndef(data.staticClass) &&\n      isUndef(data.class) && (\n        isUndef(oldData) || (\n          isUndef(oldData.staticClass) &&\n          isUndef(oldData.class)\n        )\n      )\n    ) {\n      return\n    }\n\n    var cls = genClassForVnode(vnode);\n\n    // handle transition classes\n    var transitionClass = el._transitionClasses;\n    if (isDef(transitionClass)) {\n      cls = concat(cls, stringifyClass(transitionClass));\n    }\n\n    // set the class\n    if (cls !== el._prevClass) {\n      el.setAttribute('class', cls);\n      el._prevClass = cls;\n    }\n  }\n\n  var klass = {\n    create: updateClass,\n    update: updateClass\n  };\n\n  /*  */\n\n  var validDivisionCharRE = /[\\w).+\\-_$\\]]/;\n\n  function parseFilters(exp) {\n    var inSingle = false;\n    var inDouble = false;\n    var inTemplateString = false;\n    var inRegex = false;\n    var curly = 0;\n    var square = 0;\n    var paren = 0;\n    var lastFilterIndex = 0;\n    var c, prev, i, expression, filters;\n\n    for (i = 0; i < exp.length; i++) {\n      prev = c;\n      c = exp.charCodeAt(i);\n      if (inSingle) {\n        if (c === 0x27 && prev !== 0x5C) {\n          inSingle = false;\n        }\n      } else if (inDouble) {\n        if (c === 0x22 && prev !== 0x5C) {\n          inDouble = false;\n        }\n      } else if (inTemplateString) {\n        if (c === 0x60 && prev !== 0x5C) {\n          inTemplateString = false;\n        }\n      } else if (inRegex) {\n        if (c === 0x2f && prev !== 0x5C) {\n          inRegex = false;\n        }\n      } else if (\n        c === 0x7C && // pipe\n        exp.charCodeAt(i + 1) !== 0x7C &&\n        exp.charCodeAt(i - 1) !== 0x7C &&\n        !curly && !square && !paren\n      ) {\n        if (expression === undefined) {\n          // first filter, end of expression\n          lastFilterIndex = i + 1;\n          expression = exp.slice(0, i).trim();\n        } else {\n          pushFilter();\n        }\n      } else {\n        switch (c) {\n          case 0x22:\n            inDouble = true;\n            break // \"\n          case 0x27:\n            inSingle = true;\n            break // '\n          case 0x60:\n            inTemplateString = true;\n            break // `\n          case 0x28:\n            paren++;\n            break // (\n          case 0x29:\n            paren--;\n            break // )\n          case 0x5B:\n            square++;\n            break // [\n          case 0x5D:\n            square--;\n            break // ]\n          case 0x7B:\n            curly++;\n            break // {\n          case 0x7D:\n            curly--;\n            break // }\n        }\n        if (c === 0x2f) { // /\n          var j = i - 1;\n          var p = (void 0);\n          // find first non-whitespace prev char\n          for (; j >= 0; j--) {\n            p = exp.charAt(j);\n            if (p !== ' ') {\n              break\n            }\n          }\n          if (!p || !validDivisionCharRE.test(p)) {\n            inRegex = true;\n          }\n        }\n      }\n    }\n\n    if (expression === undefined) {\n      expression = exp.slice(0, i).trim();\n    } else if (lastFilterIndex !== 0) {\n      pushFilter();\n    }\n\n    function pushFilter() {\n      (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());\n      lastFilterIndex = i + 1;\n    }\n\n    if (filters) {\n      for (i = 0; i < filters.length; i++) {\n        expression = wrapFilter(expression, filters[i]);\n      }\n    }\n\n    return expression\n  }\n\n  function wrapFilter(exp, filter) {\n    var i = filter.indexOf('(');\n    if (i < 0) {\n      // _f: resolveFilter\n      return (\"_f(\\\"\" + filter + \"\\\")(\" + exp + \")\")\n    } else {\n      var name = filter.slice(0, i);\n      var args = filter.slice(i + 1);\n      return (\"_f(\\\"\" + name + \"\\\")(\" + exp + (args !== ')' ? ',' + args : args))\n    }\n  }\n\n  /*  */\n\n\n\n  /* eslint-disable no-unused-vars */\n  function baseWarn(msg, range) {\n    console.error((\"[Vue compiler]: \" + msg));\n  }\n  /* eslint-enable no-unused-vars */\n\n  function pluckModuleFunction(\n    modules,\n    key\n  ) {\n    return modules ?\n      modules.map(function (m) {\n        return m[key];\n      }).filter(function (_) {\n        return _;\n      }) :\n      []\n  }\n\n  function addProp(el, name, value, range, dynamic) {\n    (el.props || (el.props = [])).push(rangeSetItem({\n      name: name,\n      value: value,\n      dynamic: dynamic\n    }, range));\n    el.plain = false;\n  }\n\n  function addAttr(el, name, value, range, dynamic) {\n    var attrs = dynamic ?\n      (el.dynamicAttrs || (el.dynamicAttrs = [])) :\n      (el.attrs || (el.attrs = []));\n    attrs.push(rangeSetItem({\n      name: name,\n      value: value,\n      dynamic: dynamic\n    }, range));\n    el.plain = false;\n  }\n\n  // add a raw attr (use this in preTransforms)\n  function addRawAttr(el, name, value, range) {\n    el.attrsMap[name] = value;\n    el.attrsList.push(rangeSetItem({\n      name: name,\n      value: value\n    }, range));\n  }\n\n  function addDirective(\n    el,\n    name,\n    rawName,\n    value,\n    arg,\n    isDynamicArg,\n    modifiers,\n    range\n  ) {\n    (el.directives || (el.directives = [])).push(rangeSetItem({\n      name: name,\n      rawName: rawName,\n      value: value,\n      arg: arg,\n      isDynamicArg: isDynamicArg,\n      modifiers: modifiers\n    }, range));\n    el.plain = false;\n  }\n\n  function prependModifierMarker(symbol, name, dynamic) {\n    return dynamic ?\n      (\"_p(\" + name + \",\\\"\" + symbol + \"\\\")\") :\n      symbol + name // mark the event as captured\n  }\n\n  function addHandler(\n    el,\n    name,\n    value,\n    modifiers,\n    important,\n    warn,\n    range,\n    dynamic\n  ) {\n    modifiers = modifiers || emptyObject;\n    // passive 和 prevent不能同时使用，可以参照官方文档说明\n    if (\n      warn &&\n      modifiers.prevent && modifiers.passive\n    ) {\n      warn(\n        'passive and prevent can\\'t be used together. ' +\n        'Passive handler can\\'t prevent default event.',\n        range\n      );\n    }\n\n    // normalize click.right and click.middle since they don't actually fire\n    // this is technically browser-specific, but at least for now browsers are\n    // the only target envs that have right/middle clicks.\n    if (modifiers.right) {\n      if (dynamic) {\n        name = \"(\" + name + \")==='click'?'contextmenu':(\" + name + \")\";\n      } else if (name === 'click') {\n        name = 'contextmenu';\n        delete modifiers.right;\n      }\n    } else if (modifiers.middle) {\n      if (dynamic) {\n        name = \"(\" + name + \")==='click'?'mouseup':(\" + name + \")\";\n      } else if (name === 'click') {\n        name = 'mouseup';\n      }\n    }\n\n    // check capture modifier\n    // 这部分的逻辑会对特殊的修饰符做字符串拼接的处理，以备后续的使用\n    if (modifiers.capture) {\n      delete modifiers.capture;\n      name = prependModifierMarker('!', name, dynamic);\n    }\n    if (modifiers.once) {\n      delete modifiers.once;\n      name = prependModifierMarker('~', name, dynamic);\n    }\n    /* istanbul ignore if */\n    if (modifiers.passive) {\n      delete modifiers.passive;\n      name = prependModifierMarker('&', name, dynamic);\n    }\n\n    var events;\n    if (modifiers.native) {\n      delete modifiers.native;\n      events = el.nativeEvents || (el.nativeEvents = {});\n    } else {\n      events = el.events || (el.events = {});\n    }\n\n    var newHandler = rangeSetItem({\n      value: value.trim(),\n      dynamic: dynamic\n    }, range);\n    if (modifiers !== emptyObject) {\n      newHandler.modifiers = modifiers;\n    }\n\n    var handlers = events[name];\n    /* istanbul ignore if */\n    if (Array.isArray(handlers)) {\n      important ? handlers.unshift(newHandler) : handlers.push(newHandler);\n    } else if (handlers) {\n      events[name] = important ? [newHandler, handlers] : [handlers, newHandler];\n    } else {\n      events[name] = newHandler;\n    }\n\n    el.plain = false;\n  }\n\n  function getRawBindingAttr(\n    el,\n    name\n  ) {\n    return el.rawAttrsMap[':' + name] ||\n      el.rawAttrsMap['v-bind:' + name] ||\n      el.rawAttrsMap[name]\n  }\n\n  function getBindingAttr(\n    el,\n    name,\n    getStatic\n  ) {\n    var dynamicValue =\n      getAndRemoveAttr(el, ':' + name) ||\n      getAndRemoveAttr(el, 'v-bind:' + name);\n    if (dynamicValue != null) {\n      return parseFilters(dynamicValue)\n    } else if (getStatic !== false) {\n      var staticValue = getAndRemoveAttr(el, name);\n      if (staticValue != null) {\n        return JSON.stringify(staticValue)\n      }\n    }\n  }\n\n  // note: this only removes the attr from the Array (attrsList) so that it\n  // doesn't get processed by processAttrs.\n  // By default it does NOT remove it from the map (attrsMap) because the map is\n  // needed during codegen.\n  function getAndRemoveAttr(\n    el,\n    name,\n    removeFromMap\n  ) {\n    var val;\n    if ((val = el.attrsMap[name]) != null) {\n      var list = el.attrsList;\n      for (var i = 0, l = list.length; i < l; i++) {\n        if (list[i].name === name) {\n          list.splice(i, 1);\n          break\n        }\n      }\n    }\n    if (removeFromMap) {\n      delete el.attrsMap[name];\n    }\n    return val\n  }\n\n  function getAndRemoveAttrByRegex(\n    el,\n    name\n  ) {\n    var list = el.attrsList;\n    for (var i = 0, l = list.length; i < l; i++) {\n      var attr = list[i];\n      if (name.test(attr.name)) {\n        list.splice(i, 1);\n        return attr\n      }\n    }\n  }\n\n  function rangeSetItem(\n    item,\n    range\n  ) {\n    if (range) {\n      if (range.start != null) {\n        item.start = range.start;\n      }\n      if (range.end != null) {\n        item.end = range.end;\n      }\n    }\n    return item\n  }\n\n  /*  */\n\n  /**\n   * Cross-platform code generation for component v-model\n   */\n  function genComponentModel(\n    el,\n    value,\n    modifiers\n  ) {\n    var ref = modifiers || {};\n    var number = ref.number;\n    var trim = ref.trim;\n\n    var baseValueExpression = '###v';\n    var valueExpression = baseValueExpression;\n    if (trim) {\n      valueExpression =\n        \"(typeof \" + baseValueExpression + \" === 'string'\" +\n        \"? \" + baseValueExpression + \".trim()\" +\n        \": \" + baseValueExpression + \")\";\n    }\n    if (number) {\n      valueExpression = \"_n(\" + valueExpression + \")\";\n    }\n    var assignment = genAssignmentCode(value, valueExpression);\n    // 在ast树上添加model属性，其中有value，expression，callback属性\n    el.model = {\n      value: (\"(\" + value + \")\"),\n      expression: JSON.stringify(value),\n      callback: (\"function (\" + baseValueExpression + \") {\" + assignment + \"}\")\n    };\n  }\n\n  /**\n   * Cross-platform codegen helper for generating v-model value assignment code.\n   */\n  function genAssignmentCode(\n    value,\n    assignment\n  ) {\n    // 处理v-model的格式，v-model=\"a.b\" v-model=\"a[b]\"\n    var res = parseModel(value);\n    if (res.key === null) {\n      // 普通情形\n      return (value + \"=\" + assignment)\n    } else {\n      // 对象形式\n      return (\"$set(\" + (res.exp) + \", \" + (res.key) + \", \" + assignment + \")\")\n    }\n  }\n\n  /**\n   * Parse a v-model expression into a base path and a final key segment.\n   * Handles both dot-path and possible square brackets.\n   *\n   * Possible cases:\n   *\n   * - test\n   * - test[key]\n   * - test[test1[key]]\n   * - test[\"a\"][key]\n   * - xxx.test[a[a].test1[key]]\n   * - test.xxx.a[\"asa\"][test1[key]]\n   *\n   */\n\n  var len, str, chr, index$1, expressionPos, expressionEndPos;\n\n\n\n  function parseModel(val) {\n    // Fix https://github.com/vuejs/vue/pull/7730\n    // allow v-model=\"obj.val \" (trailing whitespace)\n    val = val.trim();\n    len = val.length;\n    // v-model=\"obj\" v-model=\"obj.a\"\n    if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {\n      index$1 = val.lastIndexOf('.');\n      if (index$1 > -1) {\n        return {\n          exp: val.slice(0, index$1),\n          key: '\"' + val.slice(index$1 + 1) + '\"'\n        }\n      } else {\n        return {\n          exp: val,\n          key: null\n        }\n      }\n    }\n    //  v-model=\"obj[a]\"\n    str = val;\n    index$1 = expressionPos = expressionEndPos = 0;\n\n    while (!eof()) {\n      chr = next();\n      /* istanbul ignore if */\n      if (isStringStart(chr)) {\n        parseString(chr);\n      } else if (chr === 0x5B) {\n        parseBracket(chr);\n      }\n    }\n\n    return {\n      exp: val.slice(0, expressionPos),\n      key: val.slice(expressionPos + 1, expressionEndPos)\n    }\n  }\n\n  function next() {\n    return str.charCodeAt(++index$1)\n  }\n\n  function eof() {\n    return index$1 >= len\n  }\n\n  function isStringStart(chr) {\n    return chr === 0x22 || chr === 0x27\n  }\n\n  function parseBracket(chr) {\n    var inBracket = 1;\n    expressionPos = index$1;\n    while (!eof()) {\n      chr = next();\n      if (isStringStart(chr)) {\n        parseString(chr);\n        continue\n      }\n      if (chr === 0x5B) {\n        inBracket++;\n      }\n      if (chr === 0x5D) {\n        inBracket--;\n      }\n      if (inBracket === 0) {\n        expressionEndPos = index$1;\n        break\n      }\n    }\n  }\n\n  function parseString(chr) {\n    var stringQuote = chr;\n    while (!eof()) {\n      chr = next();\n      if (chr === stringQuote) {\n        break\n      }\n    }\n  }\n\n  /*  */\n\n  var warn$1;\n\n  // in some cases, the event used has to be determined at runtime\n  // so we used some reserved tokens during compile.\n  var RANGE_TOKEN = '__r';\n  var CHECKBOX_RADIO_TOKEN = '__c';\n\n  function model(\n    el,\n    dir,\n    _warn\n  ) {\n    warn$1 = _warn;\n    // 绑定的值\n    var value = dir.value;\n    var modifiers = dir.modifiers;\n    var tag = el.tag;\n    var type = el.attrsMap.type;\n\n    {\n      // inputs with type=\"file\" are read only and setting the input's\n      // value will throw an error.\n      // type === file的input输入框，不允许使用v-model进行双向绑定，因为File inputs是只读的\n      if (tag === 'input' && type === 'file') {\n        warn$1(\n          \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\" type=\\\"file\\\">:\\n\" +\n          \"File inputs are read only. Use a v-on:change listener instead.\",\n          el.rawAttrsMap['v-model']\n        );\n      }\n    }\n    //组件上v-model的处理\n    if (el.component) {\n      genComponentModel(el, value, modifiers);\n      // component v-model doesn't need extra runtime\n      return false\n    } else if (tag === 'select') {\n      // select表单\n      genSelect(el, value, modifiers);\n    } else if (tag === 'input' && type === 'checkbox') {\n      // checkbox表单\n      genCheckboxModel(el, value, modifiers);\n    } else if (tag === 'input' && type === 'radio') {\n      // radio表单\n      genRadioModel(el, value, modifiers);\n    } else if (tag === 'input' || tag === 'textarea') {\n      // 普通input，如 text, textarea\n      genDefaultModel(el, value, modifiers);\n    } else if (!config.isReservedTag(tag)) {\n      genComponentModel(el, value, modifiers);\n      // component v-model doesn't need extra runtime\n      return false\n    } else {\n      warn$1(\n        \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\">: \" +\n        \"v-model is not supported on this element type. \" +\n        'If you are working with contenteditable, it\\'s recommended to ' +\n        'wrap a library dedicated for that purpose inside a custom component.',\n        el.rawAttrsMap['v-model']\n      );\n    }\n\n    // ensure runtime directive metadata\n    // \n    return true\n  }\n\n  function genCheckboxModel(\n    el,\n    value,\n    modifiers\n  ) {\n    var number = modifiers && modifiers.number;\n    var valueBinding = getBindingAttr(el, 'value') || 'null';\n    var trueValueBinding = getBindingAttr(el, 'true-value') || 'true';\n    var falseValueBinding = getBindingAttr(el, 'false-value') || 'false';\n    addProp(el, 'checked',\n      \"Array.isArray(\" + value + \")\" +\n      \"?_i(\" + value + \",\" + valueBinding + \")>-1\" + (\n        trueValueBinding === 'true' ?\n        (\":(\" + value + \")\") :\n        (\":_q(\" + value + \",\" + trueValueBinding + \")\")\n      )\n    );\n    addHandler(el, 'change',\n      \"var ###a=\" + value + \",\" +\n      '###el=$event.target,' +\n      \"###c=###el.checked?(\" + trueValueBinding + \"):(\" + falseValueBinding + \");\" +\n      'if(Array.isArray(###a)){' +\n      \"var ###v=\" + (number ? '_n(' + valueBinding + ')' : valueBinding) + \",\" +\n      '###i=_i(###a,###v);' +\n      \"if(###el.checked){###i<0&&(\" + (genAssignmentCode(value, '###a.concat([###v])')) + \")}\" +\n      \"else{###i>-1&&(\" + (genAssignmentCode(value, '###a.slice(0,###i).concat(###a.slice(###i+1))')) + \")}\" +\n      \"}else{\" + (genAssignmentCode(value, '###c')) + \"}\",\n      null, true\n    );\n  }\n\n  function genRadioModel(\n    el,\n    value,\n    modifiers\n  ) {\n    var number = modifiers && modifiers.number;\n    var valueBinding = getBindingAttr(el, 'value') || 'null';\n    valueBinding = number ? (\"_n(\" + valueBinding + \")\") : valueBinding;\n    addProp(el, 'checked', (\"_q(\" + value + \",\" + valueBinding + \")\"));\n    addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true);\n  }\n\n  function genSelect(\n    el,\n    value,\n    modifiers\n  ) {\n    var number = modifiers && modifiers.number;\n    var selectedVal = \"Array.prototype.filter\" +\n      \".call($event.target.options,function(o){return o.selected})\" +\n      \".map(function(o){var val = \\\"_value\\\" in o ? o._value : o.value;\" +\n      \"return \" + (number ? '_n(val)' : 'val') + \"})\";\n\n    var assignment = '$event.target.multiple ? ###selectedVal : ###selectedVal[0]';\n    var code = \"var ###selectedVal = \" + selectedVal + \";\";\n    code = code + \" \" + (genAssignmentCode(value, assignment));\n    addHandler(el, 'change', code, null, true);\n  }\n\n  function genDefaultModel(\n    el,\n    value,\n    modifiers\n  ) {\n    var type = el.attrsMap.type;\n\n    // warn if v-bind:value conflicts with v-model\n    // except for inputs with v-bind:type\n    // v-model和v-bind值相同值，有冲突会报错\n    {\n      var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];\n      var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];\n      if (value$1 && !typeBinding) {\n        var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';\n        warn$1(\n          binding + \"=\\\"\" + value$1 + \"\\\" conflicts with v-model on the same element \" +\n          'because the latter already expands to a value binding internally',\n          el.rawAttrsMap[binding]\n        );\n      }\n    }\n\n    var ref = modifiers || {};\n    var lazy = ref.lazy;\n    var number = ref.number;\n    var trim = ref.trim;\n    var needCompositionGuard = !lazy && type !== 'range';\n    // lazy修饰符将触发同步的事件从input改为change\n    var event = lazy ?\n      'change' :\n      type === 'range' ?\n      RANGE_TOKEN :\n      'input';\n\n    var valueExpression = '$event.target.value';\n    // 过滤用户输入的首尾空白符\n    if (trim) {\n      valueExpression = \"$event.target.value.trim()\";\n    }\n    // 将用户输入转为数值类型\n    if (number) {\n      valueExpression = \"_n(\" + valueExpression + \")\";\n    }\n\n    var code = genAssignmentCode(value, valueExpression);\n    if (needCompositionGuard) {\n      //  保证了不会在输入法组合文字过程中得到更新\n      code = \"if($event.target.composing)return;\" + code;\n    }\n    //  添加value属性\n    addProp(el, 'value', (\"(\" + value + \")\"));\n    // 绑定事件\n    addHandler(el, event, code, null, true);\n    if (trim || number) {\n      addHandler(el, 'blur', '$forceUpdate()');\n    }\n  }\n\n  /*  */\n\n  // normalize v-model event tokens that can only be determined at runtime.\n  // it's important to place the event as the first in the array because\n  // the whole point is ensuring the v-model callback gets called before\n  // user-attached handlers.\n  function normalizeEvents(on) {\n    /* istanbul ignore if */\n    if (isDef(on[RANGE_TOKEN])) {\n      // IE input[type=range] only supports `change` event\n      var event = isIE ? 'change' : 'input';\n      on[event] = [].concat(on[RANGE_TOKEN], on[event] || []);\n      delete on[RANGE_TOKEN];\n    }\n    // This was originally intended to fix #4521 but no longer necessary\n    // after 2.5. Keeping it for backwards compat with generated code from < 2.4\n    /* istanbul ignore if */\n    if (isDef(on[CHECKBOX_RADIO_TOKEN])) {\n      on.change = [].concat(on[CHECKBOX_RADIO_TOKEN], on.change || []);\n      delete on[CHECKBOX_RADIO_TOKEN];\n    }\n  }\n\n  var target$1;\n\n  function createOnceHandler$1(event, handler, capture) {\n    var _target = target$1; // save current target element in closure\n    return function onceHandler() {\n      var res = handler.apply(null, arguments);\n      if (res !== null) {\n        remove$2(event, onceHandler, capture, _target);\n      }\n    }\n  }\n\n  // #9446: Firefox <= 53 (in particular, ESR 52) has incorrect Event.timeStamp\n  // implementation and does not fire microtasks in between event propagation, so\n  // safe to exclude.\n  var useMicrotaskFix = isUsingMicroTask && !(isFF && Number(isFF[1]) <= 53);\n\n  function add$1(\n    name,\n    handler,\n    capture,\n    passive\n  ) {\n    // async edge case #6566: inner click event triggers patch, event handler\n    // attached to outer element during patch, and triggered again. This\n    // happens because browsers fire microtask ticks between event propagation.\n    // the solution is simple: we save the timestamp when a handler is attached,\n    // and the handler would only fire if the event passed to it was fired\n    // AFTER it was attached.\n    if (useMicrotaskFix) {\n      var attachedTimestamp = currentFlushTimestamp;\n      var original = handler;\n      handler = original._wrapper = function (e) {\n        if (\n          // no bubbling, should always fire.\n          // this is just a safety net in case event.timeStamp is unreliable in\n          // certain weird environments...\n          e.target === e.currentTarget ||\n          // event is fired after handler attachment\n          e.timeStamp >= attachedTimestamp ||\n          // #9462 bail for iOS 9 bug: event.timeStamp is 0 after history.pushState\n          e.timeStamp === 0 ||\n          // #9448 bail if event is fired in another document in a multi-page\n          // electron/nw.js app, since event.timeStamp will be using a different\n          // starting reference\n          e.target.ownerDocument !== document\n        ) {\n          return original.apply(this, arguments)\n        }\n      };\n    }\n    target$1.addEventListener(\n      name,\n      handler,\n      supportsPassive ?\n      {\n        capture: capture,\n        passive: passive\n      } :\n      capture\n    );\n  }\n\n  function remove$2(\n    name,\n    handler,\n    capture,\n    _target\n  ) {\n    (_target || target$1).removeEventListener(\n      name,\n      handler._wrapper || handler,\n      capture\n    );\n  }\n\n  function updateDOMListeners(oldVnode, vnode) {\n    // on是事件指令的标志\n    if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {\n      return\n    }\n    // 新旧节点不同的事件绑定\n    var on = vnode.data.on || {};\n    var oldOn = oldVnode.data.on || {};\n    target$1 = vnode.elm;\n    normalizeEvents(on);\n    updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);\n    target$1 = undefined;\n  }\n\n  var events = {\n    create: updateDOMListeners,\n    update: updateDOMListeners\n  };\n\n  /*  */\n\n  var svgContainer;\n\n  function updateDOMProps(oldVnode, vnode) {\n    if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {\n      return\n    }\n    var key, cur;\n    var elm = vnode.elm;\n    var oldProps = oldVnode.data.domProps || {};\n    var props = vnode.data.domProps || {};\n    // clone observed objects, as the user probably wants to mutate it\n    if (isDef(props.__ob__)) {\n      props = vnode.data.domProps = extend({}, props);\n    }\n\n    for (key in oldProps) {\n      if (isUndef(props[key])) {\n        elm[key] = '';\n      }\n    }\n    for (key in props) {\n      cur = props[key];\n      // ignore children if the node has textContent or innerHTML,\n      // as these will throw away existing DOM nodes and cause removal errors\n      // on subsequent patches (#3360)\n      //  textContent针对v-text指令， innerHTML针对v-html指令,如果模板节点有子节点，会自动忽略，删除vnode的children，以及真实的子节点\n      if (key === 'textContent' || key === 'innerHTML') {\n        if (vnode.children) {\n          vnode.children.length = 0;\n        }\n        if (cur === oldProps[key]) {\n          continue\n        }\n        // #6601 work around Chrome version <= 55 bug where single textNode\n        // replaced by innerHTML/textContent retains its parentNode property\n        if (elm.childNodes.length === 1) {\n          elm.removeChild(elm.childNodes[0]);\n        }\n      }\n      //    value针对v-model指令的value\n      if (key === 'value' && elm.tagName !== 'PROGRESS') {\n        // store value as _value as well since\n        // non-string values will be stringified\n        elm._value = cur;\n        // avoid resetting cursor position when value is the same\n        var strCur = isUndef(cur) ? '' : String(cur);\n        if (shouldUpdateValue(elm, strCur)) {\n          elm.value = strCur;\n        }\n      } else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) {\n        // IE doesn't support innerHTML for SVG elements\n        svgContainer = svgContainer || document.createElement('div');\n        svgContainer.innerHTML = \"<svg>\" + cur + \"</svg>\";\n        var svg = svgContainer.firstChild;\n        while (elm.firstChild) {\n          elm.removeChild(elm.firstChild);\n        }\n        while (svg.firstChild) {\n          elm.appendChild(svg.firstChild);\n        }\n      } else if (\n        // skip the update if old and new VDOM state is the same.\n        // `value` is handled separately because the DOM value may be temporarily\n        // out of sync with VDOM state due to focus, composition and modifiers.\n        // This  #4521 by skipping the unnecesarry `checked` update.\n        cur !== oldProps[key]\n      ) {\n        // some property updates can throw\n        // e.g. `value` on <progress> w/ non-finite value\n        try {\n          elm[key] = cur;\n        } catch (e) {}\n      }\n    }\n  }\n\n  // check platforms/web/util/attrs.js acceptValue\n\n\n  function shouldUpdateValue(elm, checkVal) {\n    return (!elm.composing && (\n      elm.tagName === 'OPTION' ||\n      isNotInFocusAndDirty(elm, checkVal) ||\n      isDirtyWithModifiers(elm, checkVal)\n    ))\n  }\n\n  function isNotInFocusAndDirty(elm, checkVal) {\n    // return true when textbox (.number and .trim) loses focus and its value is\n    // not equal to the updated value\n    var notInFocus = true;\n    // #6157\n    // work around IE bug when accessing document.activeElement in an iframe\n    try {\n      notInFocus = document.activeElement !== elm;\n    } catch (e) {}\n    return notInFocus && elm.value !== checkVal\n  }\n\n  function isDirtyWithModifiers(elm, newVal) {\n    var value = elm.value;\n    var modifiers = elm._vModifiers; // injected by v-model runtime\n    if (isDef(modifiers)) {\n      if (modifiers.number) {\n        return toNumber(value) !== toNumber(newVal)\n      }\n      if (modifiers.trim) {\n        return value.trim() !== newVal.trim()\n      }\n    }\n    return value !== newVal\n  }\n\n  var domProps = {\n    create: updateDOMProps,\n    update: updateDOMProps\n  };\n\n  /*  */\n\n  var parseStyleText = cached(function (cssText) {\n    var res = {};\n    var listDelimiter = /;(?![^(]*\\))/g;\n    var propertyDelimiter = /:(.+)/;\n    cssText.split(listDelimiter).forEach(function (item) {\n      if (item) {\n        var tmp = item.split(propertyDelimiter);\n        tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim());\n      }\n    });\n    return res\n  });\n\n  // merge static and dynamic style data on the same vnode\n  function normalizeStyleData(data) {\n    var style = normalizeStyleBinding(data.style);\n    // static style is pre-processed into an object during compilation\n    // and is always a fresh object, so it's safe to merge into it\n    return data.staticStyle ?\n      extend(data.staticStyle, style) :\n      style\n  }\n\n  // normalize possible array / string values into Object\n  function normalizeStyleBinding(bindingStyle) {\n    if (Array.isArray(bindingStyle)) {\n      return toObject(bindingStyle)\n    }\n    if (typeof bindingStyle === 'string') {\n      return parseStyleText(bindingStyle)\n    }\n    return bindingStyle\n  }\n\n  /**\n   * parent component style should be after child's\n   * so that parent component's style could override it\n   */\n  function getStyle(vnode, checkChild) {\n    var res = {};\n    var styleData;\n\n    if (checkChild) {\n      var childNode = vnode;\n      while (childNode.componentInstance) {\n        childNode = childNode.componentInstance._vnode;\n        if (\n          childNode && childNode.data &&\n          (styleData = normalizeStyleData(childNode.data))\n        ) {\n          extend(res, styleData);\n        }\n      }\n    }\n\n    if ((styleData = normalizeStyleData(vnode.data))) {\n      extend(res, styleData);\n    }\n\n    var parentNode = vnode;\n    while ((parentNode = parentNode.parent)) {\n      if (parentNode.data && (styleData = normalizeStyleData(parentNode.data))) {\n        extend(res, styleData);\n      }\n    }\n    return res\n  }\n\n  /*  */\n\n  var cssVarRE = /^--/;\n  var importantRE = /\\s*!important$/;\n  var setProp = function (el, name, val) {\n    /* istanbul ignore if */\n    if (cssVarRE.test(name)) {\n      el.style.setProperty(name, val);\n    } else if (importantRE.test(val)) {\n      el.style.setProperty(hyphenate(name), val.replace(importantRE, ''), 'important');\n    } else {\n      var normalizedName = normalize(name);\n      if (Array.isArray(val)) {\n        // Support values array created by autoprefixer, e.g.\n        // {display: [\"-webkit-box\", \"-ms-flexbox\", \"flex\"]}\n        // Set them one by one, and the browser will only set those it can recognize\n        for (var i = 0, len = val.length; i < len; i++) {\n          el.style[normalizedName] = val[i];\n        }\n      } else {\n        el.style[normalizedName] = val;\n      }\n    }\n  };\n\n  var vendorNames = ['Webkit', 'Moz', 'ms'];\n\n  var emptyStyle;\n  var normalize = cached(function (prop) {\n    emptyStyle = emptyStyle || document.createElement('div').style;\n    prop = camelize(prop);\n    if (prop !== 'filter' && (prop in emptyStyle)) {\n      return prop\n    }\n    var capName = prop.charAt(0).toUpperCase() + prop.slice(1);\n    for (var i = 0; i < vendorNames.length; i++) {\n      var name = vendorNames[i] + capName;\n      if (name in emptyStyle) {\n        return name\n      }\n    }\n  });\n\n  function updateStyle(oldVnode, vnode) {\n    var data = vnode.data;\n    var oldData = oldVnode.data;\n\n    if (isUndef(data.staticStyle) && isUndef(data.style) &&\n      isUndef(oldData.staticStyle) && isUndef(oldData.style)\n    ) {\n      return\n    }\n\n    var cur, name;\n    var el = vnode.elm;\n    var oldStaticStyle = oldData.staticStyle;\n    var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};\n\n    // if static style exists, stylebinding already merged into it when doing normalizeStyleData\n    var oldStyle = oldStaticStyle || oldStyleBinding;\n\n    var style = normalizeStyleBinding(vnode.data.style) || {};\n\n    // store normalized style under a different key for next diff\n    // make sure to clone it if it's reactive, since the user likely wants\n    // to mutate it.\n    vnode.data.normalizedStyle = isDef(style.__ob__) ?\n      extend({}, style) :\n      style;\n\n    var newStyle = getStyle(vnode, true);\n\n    for (name in oldStyle) {\n      if (isUndef(newStyle[name])) {\n        setProp(el, name, '');\n      }\n    }\n    for (name in newStyle) {\n      cur = newStyle[name];\n      if (cur !== oldStyle[name]) {\n        // ie9 setting to null has no effect, must use empty string\n        setProp(el, name, cur == null ? '' : cur);\n      }\n    }\n  }\n\n  var style = {\n    create: updateStyle,\n    update: updateStyle\n  };\n\n  /*  */\n\n  var whitespaceRE = /\\s+/;\n\n  /**\n   * Add class with compatibility for SVG since classList is not supported on\n   * SVG elements in IE\n   */\n  function addClass(el, cls) {\n    /* istanbul ignore if */\n    if (!cls || !(cls = cls.trim())) {\n      return\n    }\n\n    /* istanbul ignore else */\n    if (el.classList) {\n      if (cls.indexOf(' ') > -1) {\n        cls.split(whitespaceRE).forEach(function (c) {\n          return el.classList.add(c);\n        });\n      } else {\n        el.classList.add(cls);\n      }\n    } else {\n      var cur = \" \" + (el.getAttribute('class') || '') + \" \";\n      if (cur.indexOf(' ' + cls + ' ') < 0) {\n        el.setAttribute('class', (cur + cls).trim());\n      }\n    }\n  }\n\n  /**\n   * Remove class with compatibility for SVG since classList is not supported on\n   * SVG elements in IE\n   */\n  function removeClass(el, cls) {\n    /* istanbul ignore if */\n    if (!cls || !(cls = cls.trim())) {\n      return\n    }\n\n    /* istanbul ignore else */\n    if (el.classList) {\n      if (cls.indexOf(' ') > -1) {\n        cls.split(whitespaceRE).forEach(function (c) {\n          return el.classList.remove(c);\n        });\n      } else {\n        el.classList.remove(cls);\n      }\n      if (!el.classList.length) {\n        el.removeAttribute('class');\n      }\n    } else {\n      var cur = \" \" + (el.getAttribute('class') || '') + \" \";\n      var tar = ' ' + cls + ' ';\n      while (cur.indexOf(tar) >= 0) {\n        cur = cur.replace(tar, ' ');\n      }\n      cur = cur.trim();\n      if (cur) {\n        el.setAttribute('class', cur);\n      } else {\n        el.removeAttribute('class');\n      }\n    }\n  }\n\n  /*  */\n\n  function resolveTransition(def###1) {\n    if (!def###1) {\n      return\n    }\n    /* istanbul ignore else */\n    if (typeof def###1 === 'object') {\n      var res = {};\n      if (def###1.css !== false) {\n        extend(res, autoCssTransition(def###1.name || 'v'));\n      }\n      extend(res, def###1);\n      return res\n    } else if (typeof def###1 === 'string') {\n      return autoCssTransition(def###1)\n    }\n  }\n\n  var autoCssTransition = cached(function (name) {\n    return {\n      enterClass: (name + \"-enter\"),\n      enterToClass: (name + \"-enter-to\"),\n      enterActiveClass: (name + \"-enter-active\"),\n      leaveClass: (name + \"-leave\"),\n      leaveToClass: (name + \"-leave-to\"),\n      leaveActiveClass: (name + \"-leave-active\")\n    }\n  });\n\n  var hasTransition = inBrowser && !isIE9;\n  var TRANSITION = 'transition';\n  var ANIMATION = 'animation';\n\n  // Transition property/event sniffing\n  var transitionProp = 'transition';\n  var transitionEndEvent = 'transitionend';\n  var animationProp = 'animation';\n  var animationEndEvent = 'animationend';\n  if (hasTransition) {\n    /* istanbul ignore if */\n    if (window.ontransitionend === undefined &&\n      window.onwebkittransitionend !== undefined\n    ) {\n      transitionProp = 'WebkitTransition';\n      transitionEndEvent = 'webkitTransitionEnd';\n    }\n    if (window.onanimationend === undefined &&\n      window.onwebkitanimationend !== undefined\n    ) {\n      animationProp = 'WebkitAnimation';\n      animationEndEvent = 'webkitAnimationEnd';\n    }\n  }\n\n  // binding to window is necessary to make hot reload work in IE in strict mode\n  var raf = inBrowser ?\n    window.requestAnimationFrame ?\n    window.requestAnimationFrame.bind(window) :\n    setTimeout :\n    /* istanbul ignore next */ function (fn) {\n      return fn();\n    };\n\n  function nextFrame(fn) {\n    raf(function () {\n      raf(fn);\n    });\n  }\n\n  function addTransitionClass(el, cls) {\n    var transitionClasses = el._transitionClasses || (el._transitionClasses = []);\n    if (transitionClasses.indexOf(cls) < 0) {\n      transitionClasses.push(cls);\n      addClass(el, cls);\n    }\n  }\n\n  function removeTransitionClass(el, cls) {\n    if (el._transitionClasses) {\n      remove(el._transitionClasses, cls);\n    }\n    removeClass(el, cls);\n  }\n\n  function whenTransitionEnds(\n    el,\n    expectedType,\n    cb\n  ) {\n    var ref = getTransitionInfo(el, expectedType);\n    var type = ref.type;\n    var timeout = ref.timeout;\n    var propCount = ref.propCount;\n    if (!type) {\n      return cb()\n    }\n    var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;\n    var ended = 0;\n    var end = function () {\n      el.removeEventListener(event, onEnd);\n      cb();\n    };\n    var onEnd = function (e) {\n      if (e.target === el) {\n        if (++ended >= propCount) {\n          end();\n        }\n      }\n    };\n    setTimeout(function () {\n      if (ended < propCount) {\n        end();\n      }\n    }, timeout + 1);\n    el.addEventListener(event, onEnd);\n  }\n\n  var transformRE = /\\b(transform|all)(,|$)/;\n\n  function getTransitionInfo(el, expectedType) {\n    var styles = window.getComputedStyle(el);\n    // JSDOM may return undefined for transition properties\n    var transitionDelays = (styles[transitionProp + 'Delay'] || '').split(', ');\n    var transitionDurations = (styles[transitionProp + 'Duration'] || '').split(', ');\n    var transitionTimeout = getTimeout(transitionDelays, transitionDurations);\n    var animationDelays = (styles[animationProp + 'Delay'] || '').split(', ');\n    var animationDurations = (styles[animationProp + 'Duration'] || '').split(', ');\n    var animationTimeout = getTimeout(animationDelays, animationDurations);\n\n    var type;\n    var timeout = 0;\n    var propCount = 0;\n    /* istanbul ignore if */\n    if (expectedType === TRANSITION) {\n      if (transitionTimeout > 0) {\n        type = TRANSITION;\n        timeout = transitionTimeout;\n        propCount = transitionDurations.length;\n      }\n    } else if (expectedType === ANIMATION) {\n      if (animationTimeout > 0) {\n        type = ANIMATION;\n        timeout = animationTimeout;\n        propCount = animationDurations.length;\n      }\n    } else {\n      timeout = Math.max(transitionTimeout, animationTimeout);\n      type = timeout > 0 ?\n        transitionTimeout > animationTimeout ?\n        TRANSITION :\n        ANIMATION :\n        null;\n      propCount = type ?\n        type === TRANSITION ?\n        transitionDurations.length :\n        animationDurations.length :\n        0;\n    }\n    var hasTransform =\n      type === TRANSITION &&\n      transformRE.test(styles[transitionProp + 'Property']);\n    return {\n      type: type,\n      timeout: timeout,\n      propCount: propCount,\n      hasTransform: hasTransform\n    }\n  }\n\n  function getTimeout(delays, durations) {\n    /* istanbul ignore next */\n    while (delays.length < durations.length) {\n      delays = delays.concat(delays);\n    }\n\n    return Math.max.apply(null, durations.map(function (d, i) {\n      return toMs(d) + toMs(delays[i])\n    }))\n  }\n\n  // Old versions of Chromium (below 61.0.3163.100) formats floating pointer numbers\n  // in a locale-dependent way, using a comma instead of a dot.\n  // If comma is not replaced with a dot, the input will be rounded down (i.e. acting\n  // as a floor function) causing unexpected behaviors\n  function toMs(s) {\n    return Number(s.slice(0, -1).replace(',', '.')) * 1000\n  }\n\n  /*  */\n\n  function enter(vnode, toggleDisplay) {\n    var el = vnode.elm;\n\n    // call leave callback now\n    if (isDef(el._leaveCb)) {\n      el._leaveCb.cancelled = true;\n      el._leaveCb();\n    }\n\n    var data = resolveTransition(vnode.data.transition);\n    if (isUndef(data)) {\n      return\n    }\n\n    /* istanbul ignore if */\n    if (isDef(el._enterCb) || el.nodeType !== 1) {\n      return\n    }\n\n    var css = data.css;\n    var type = data.type;\n    var enterClass = data.enterClass;\n    var enterToClass = data.enterToClass;\n    var enterActiveClass = data.enterActiveClass;\n    var appearClass = data.appearClass;\n    var appearToClass = data.appearToClass;\n    var appearActiveClass = data.appearActiveClass;\n    var beforeEnter = data.beforeEnter;\n    var enter = data.enter;\n    var afterEnter = data.afterEnter;\n    var enterCancelled = data.enterCancelled;\n    var beforeAppear = data.beforeAppear;\n    var appear = data.appear;\n    var afterAppear = data.afterAppear;\n    var appearCancelled = data.appearCancelled;\n    var duration = data.duration;\n\n    // activeInstance will always be the <transition> component managing this\n    // transition. One edge case to check is when the <transition> is placed\n    // as the root node of a child component. In that case we need to check\n    // <transition>'s parent for appear check.\n    var context = activeInstance;\n    var transitionNode = activeInstance.$vnode;\n    while (transitionNode && transitionNode.parent) {\n      transitionNode = transitionNode.parent;\n      context = transitionNode.context;\n    }\n\n    var isAppear = !context._isMounted || !vnode.isRootInsert;\n\n    if (isAppear && !appear && appear !== '') {\n      return\n    }\n\n    var startClass = isAppear && appearClass ?\n      appearClass :\n      enterClass;\n    var activeClass = isAppear && appearActiveClass ?\n      appearActiveClass :\n      enterActiveClass;\n    var toClass = isAppear && appearToClass ?\n      appearToClass :\n      enterToClass;\n\n    var beforeEnterHook = isAppear ?\n      (beforeAppear || beforeEnter) :\n      beforeEnter;\n    var enterHook = isAppear ?\n      (typeof appear === 'function' ? appear : enter) :\n      enter;\n    var afterEnterHook = isAppear ?\n      (afterAppear || afterEnter) :\n      afterEnter;\n    var enterCancelledHook = isAppear ?\n      (appearCancelled || enterCancelled) :\n      enterCancelled;\n\n    var explicitEnterDuration = toNumber(\n      isObject(duration) ?\n      duration.enter :\n      duration\n    );\n\n    if (explicitEnterDuration != null) {\n      checkDuration(explicitEnterDuration, 'enter', vnode);\n    }\n\n    var expectsCSS = css !== false && !isIE9;\n    var userWantsControl = getHookArgumentsLength(enterHook);\n\n    var cb = el._enterCb = once(function () {\n      if (expectsCSS) {\n        removeTransitionClass(el, toClass);\n        removeTransitionClass(el, activeClass);\n      }\n      if (cb.cancelled) {\n        if (expectsCSS) {\n          removeTransitionClass(el, startClass);\n        }\n        enterCancelledHook && enterCancelledHook(el);\n      } else {\n        afterEnterHook && afterEnterHook(el);\n      }\n      el._enterCb = null;\n    });\n\n    if (!vnode.data.show) {\n      // remove pending leave element on enter by injecting an insert hook\n      mergeVNodeHook(vnode, 'insert', function () {\n        var parent = el.parentNode;\n        var pendingNode = parent && parent._pending && parent._pending[vnode.key];\n        if (pendingNode &&\n          pendingNode.tag === vnode.tag &&\n          pendingNode.elm._leaveCb\n        ) {\n          pendingNode.elm._leaveCb();\n        }\n        enterHook && enterHook(el, cb);\n      });\n    }\n\n    // start enter transition\n    beforeEnterHook && beforeEnterHook(el);\n    if (expectsCSS) {\n      addTransitionClass(el, startClass);\n      addTransitionClass(el, activeClass);\n      nextFrame(function () {\n        removeTransitionClass(el, startClass);\n        if (!cb.cancelled) {\n          addTransitionClass(el, toClass);\n          if (!userWantsControl) {\n            if (isValidDuration(explicitEnterDuration)) {\n              setTimeout(cb, explicitEnterDuration);\n            } else {\n              whenTransitionEnds(el, type, cb);\n            }\n          }\n        }\n      });\n    }\n\n    if (vnode.data.show) {\n      toggleDisplay && toggleDisplay();\n      enterHook && enterHook(el, cb);\n    }\n\n    if (!expectsCSS && !userWantsControl) {\n      cb();\n    }\n  }\n\n  function leave(vnode, rm) {\n    var el = vnode.elm;\n\n    // call enter callback now\n    if (isDef(el._enterCb)) {\n      el._enterCb.cancelled = true;\n      el._enterCb();\n    }\n\n    var data = resolveTransition(vnode.data.transition);\n    if (isUndef(data) || el.nodeType !== 1) {\n      return rm()\n    }\n\n    /* istanbul ignore if */\n    if (isDef(el._leaveCb)) {\n      return\n    }\n\n    var css = data.css;\n    var type = data.type;\n    var leaveClass = data.leaveClass;\n    var leaveToClass = data.leaveToClass;\n    var leaveActiveClass = data.leaveActiveClass;\n    var beforeLeave = data.beforeLeave;\n    var leave = data.leave;\n    var afterLeave = data.afterLeave;\n    var leaveCancelled = data.leaveCancelled;\n    var delayLeave = data.delayLeave;\n    var duration = data.duration;\n\n    var expectsCSS = css !== false && !isIE9;\n    var userWantsControl = getHookArgumentsLength(leave);\n\n    var explicitLeaveDuration = toNumber(\n      isObject(duration) ?\n      duration.leave :\n      duration\n    );\n\n    if (isDef(explicitLeaveDuration)) {\n      checkDuration(explicitLeaveDuration, 'leave', vnode);\n    }\n\n    var cb = el._leaveCb = once(function () {\n      if (el.parentNode && el.parentNode._pending) {\n        el.parentNode._pending[vnode.key] = null;\n      }\n      if (expectsCSS) {\n        removeTransitionClass(el, leaveToClass);\n        removeTransitionClass(el, leaveActiveClass);\n      }\n      if (cb.cancelled) {\n        if (expectsCSS) {\n          removeTransitionClass(el, leaveClass);\n        }\n        leaveCancelled && leaveCancelled(el);\n      } else {\n        rm();\n        afterLeave && afterLeave(el);\n      }\n      el._leaveCb = null;\n    });\n\n    if (delayLeave) {\n      delayLeave(performLeave);\n    } else {\n      performLeave();\n    }\n\n    function performLeave() {\n      // the delayed leave may have already been cancelled\n      if (cb.cancelled) {\n        return\n      }\n      // record leaving element\n      if (!vnode.data.show && el.parentNode) {\n        (el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key)] = vnode;\n      }\n      beforeLeave && beforeLeave(el);\n      if (expectsCSS) {\n        addTransitionClass(el, leaveClass);\n        addTransitionClass(el, leaveActiveClass);\n        nextFrame(function () {\n          removeTransitionClass(el, leaveClass);\n          if (!cb.cancelled) {\n            addTransitionClass(el, leaveToClass);\n            if (!userWantsControl) {\n              if (isValidDuration(explicitLeaveDuration)) {\n                setTimeout(cb, explicitLeaveDuration);\n              } else {\n                whenTransitionEnds(el, type, cb);\n              }\n            }\n          }\n        });\n      }\n      leave && leave(el, cb);\n      if (!expectsCSS && !userWantsControl) {\n        cb();\n      }\n    }\n  }\n\n  // only used in dev mode\n  function checkDuration(val, name, vnode) {\n    if (typeof val !== 'number') {\n      warn(\n        \"<transition> explicit \" + name + \" duration is not a valid number - \" +\n        \"got \" + (JSON.stringify(val)) + \".\",\n        vnode.context\n      );\n    } else if (isNaN(val)) {\n      warn(\n        \"<transition> explicit \" + name + \" duration is NaN - \" +\n        'the duration expression might be incorrect.',\n        vnode.context\n      );\n    }\n  }\n\n  function isValidDuration(val) {\n    return typeof val === 'number' && !isNaN(val)\n  }\n\n  /**\n   * Normalize a transition hook's argument length. The hook may be:\n   * - a merged hook (invoker) with the original in .fns\n   * - a wrapped component method (check ._length)\n   * - a plain function (.length)\n   */\n  function getHookArgumentsLength(fn) {\n    if (isUndef(fn)) {\n      return false\n    }\n    var invokerFns = fn.fns;\n    if (isDef(invokerFns)) {\n      // invoker\n      return getHookArgumentsLength(\n        Array.isArray(invokerFns) ?\n        invokerFns[0] :\n        invokerFns\n      )\n    } else {\n      return (fn._length || fn.length) > 1\n    }\n  }\n\n  function _enter(_, vnode) {\n    if (vnode.data.show !== true) {\n      enter(vnode);\n    }\n  }\n\n  var transition = inBrowser ? {\n    create: _enter,\n    activate: _enter,\n    remove: function remove###1(vnode, rm) {\n      /* istanbul ignore else */\n      if (vnode.data.show !== true) {\n        leave(vnode, rm);\n      } else {\n        rm();\n      }\n    }\n  } : {};\n\n  // 定义了模块的钩子函数\n  var platformModules = [\n    attrs,\n    klass,\n    events,\n    domProps,\n    style,\n    transition\n  ];\n\n  /*  */\n\n  // the directive module should be applied last, after all\n  // built-in modules have been applied.\n  var modules = platformModules.concat(baseModules);\n\n  var patch = createPatchFunction({\n    nodeOps: nodeOps,\n    modules: modules\n  });\n\n  /**\n   * Not type checking this file because flow doesn't like attaching\n   * properties to Elements.\n   */\n\n  /* istanbul ignore if */\n  if (isIE9) {\n    // http://www.matts411.com/post/internet-explorer-9-oninput/\n    document.addEventListener('selectionchange', function () {\n      var el = document.activeElement;\n      if (el && el.vmodel) {\n        trigger(el, 'input');\n      }\n    });\n  }\n\n  var directive = {\n    inserted: function inserted(el, binding, vnode, oldVnode) {\n      if (vnode.tag === 'select') {\n        // #6903\n        if (oldVnode.elm && !oldVnode.elm._vOptions) {\n          mergeVNodeHook(vnode, 'postpatch', function () {\n            directive.componentUpdated(el, binding, vnode);\n          });\n        } else {\n          setSelected(el, binding, vnode.context);\n        }\n        el._vOptions = [].map.call(el.options, getValue);\n      } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {\n        el._vModifiers = binding.modifiers;\n        if (!binding.modifiers.lazy) {\n          el.addEventListener('compositionstart', onCompositionStart);\n          el.addEventListener('compositionend', onCompositionEnd);\n          // Safari < 10.2 & UIWebView doesn't fire compositionend when\n          // switching focus before confirming composition choice\n          // this also fixes the issue where some browsers e.g. iOS Chrome\n          // fires \"change\" instead of \"input\" on autocomplete.\n          el.addEventListener('change', onCompositionEnd);\n          /* istanbul ignore if */\n          if (isIE9) {\n            el.vmodel = true;\n          }\n        }\n      }\n    },\n\n    componentUpdated: function componentUpdated(el, binding, vnode) {\n      if (vnode.tag === 'select') {\n        setSelected(el, binding, vnode.context);\n        // in case the options rendered by v-for have changed,\n        // it's possible that the value is out-of-sync with the rendered options.\n        // detect such cases and filter out values that no longer has a matching\n        // option in the DOM.\n        var prevOptions = el._vOptions;\n        var curOptions = el._vOptions = [].map.call(el.options, getValue);\n        if (curOptions.some(function (o, i) {\n            return !looseEqual(o, prevOptions[i]);\n          })) {\n          // trigger change event if\n          // no matching option found for at least one value\n          var needReset = el.multiple ?\n            binding.value.some(function (v) {\n              return hasNoMatchingOption(v, curOptions);\n            }) :\n            binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions);\n          if (needReset) {\n            trigger(el, 'change');\n          }\n        }\n      }\n    }\n  };\n\n  function setSelected(el, binding, vm) {\n    actuallySetSelected(el, binding, vm);\n    /* istanbul ignore if */\n    if (isIE || isEdge) {\n      setTimeout(function () {\n        actuallySetSelected(el, binding, vm);\n      }, 0);\n    }\n  }\n\n  function actuallySetSelected(el, binding, vm) {\n    var value = binding.value;\n    var isMultiple = el.multiple;\n    if (isMultiple && !Array.isArray(value)) {\n      warn(\n        \"<select multiple v-model=\\\"\" + (binding.expression) + \"\\\"> \" +\n        \"expects an Array value for its binding, but got \" + (Object.prototype.toString.call(value).slice(8, -1)),\n        vm\n      );\n      return\n    }\n    var selected, option;\n    for (var i = 0, l = el.options.length; i < l; i++) {\n      option = el.options[i];\n      if (isMultiple) {\n        selected = looseIndexOf(value, getValue(option)) > -1;\n        if (option.selected !== selected) {\n          option.selected = selected;\n        }\n      } else {\n        if (looseEqual(getValue(option), value)) {\n          if (el.selectedIndex !== i) {\n            el.selectedIndex = i;\n          }\n          return\n        }\n      }\n    }\n    if (!isMultiple) {\n      el.selectedIndex = -1;\n    }\n  }\n\n  function hasNoMatchingOption(value, options) {\n    return options.every(function (o) {\n      return !looseEqual(o, value);\n    })\n  }\n\n  function getValue(option) {\n    return '_value' in option ?\n      option._value :\n      option.value\n  }\n\n  function onCompositionStart(e) {\n    e.target.composing = true;\n  }\n\n  function onCompositionEnd(e) {\n    // prevent triggering an input event for no reason\n    if (!e.target.composing) {\n      return\n    }\n    e.target.composing = false;\n    trigger(e.target, 'input');\n  }\n\n  function trigger(el, type) {\n    var e = document.createEvent('HTMLEvents');\n    e.initEvent(type, true, true);\n    el.dispatchEvent(e);\n  }\n\n  /*  */\n\n  // recursively search for possible transition defined inside the component root\n  function locateNode(vnode) {\n    return vnode.componentInstance && (!vnode.data || !vnode.data.transition) ?\n      locateNode(vnode.componentInstance._vnode) :\n      vnode\n  }\n\n  var show = {\n    bind: function bind(el, ref, vnode) {\n      var value = ref.value;\n\n      vnode = locateNode(vnode);\n      var transition###1 = vnode.data && vnode.data.transition;\n      var originalDisplay = el.__vOriginalDisplay =\n        el.style.display === 'none' ? '' : el.style.display;\n      if (value && transition###1) {\n        vnode.data.show = true;\n        enter(vnode, function () {\n          el.style.display = originalDisplay;\n        });\n      } else {\n        el.style.display = value ? originalDisplay : 'none';\n      }\n    },\n\n    update: function update(el, ref, vnode) {\n      var value = ref.value;\n      var oldValue = ref.oldValue;\n\n      /* istanbul ignore if */\n      if (!value === !oldValue) {\n        return\n      }\n      vnode = locateNode(vnode);\n      var transition###1 = vnode.data && vnode.data.transition;\n      if (transition###1) {\n        vnode.data.show = true;\n        if (value) {\n          enter(vnode, function () {\n            el.style.display = el.__vOriginalDisplay;\n          });\n        } else {\n          leave(vnode, function () {\n            el.style.display = 'none';\n          });\n        }\n      } else {\n        el.style.display = value ? el.__vOriginalDisplay : 'none';\n      }\n    },\n\n    unbind: function unbind(\n      el,\n      binding,\n      vnode,\n      oldVnode,\n      isDestroy\n    ) {\n      if (!isDestroy) {\n        el.style.display = el.__vOriginalDisplay;\n      }\n    }\n  };\n\n  var platformDirectives = {\n    model: directive,\n    show: show\n  };\n\n  /*  */\n\n  var transitionProps = {\n    name: String,\n    appear: Boolean,\n    css: Boolean,\n    mode: String,\n    type: String,\n    enterClass: String,\n    leaveClass: String,\n    enterToClass: String,\n    leaveToClass: String,\n    enterActiveClass: String,\n    leaveActiveClass: String,\n    appearClass: String,\n    appearActiveClass: String,\n    appearToClass: String,\n    duration: [Number, String, Object]\n  };\n\n  // in case the child is also an abstract component, e.g. <keep-alive>\n  // we want to recursively retrieve the real component to be rendered\n  function getRealChild(vnode) {\n    var compOptions = vnode && vnode.componentOptions;\n    if (compOptions && compOptions.Ctor.options.abstract) {\n      return getRealChild(getFirstComponentChild(compOptions.children))\n    } else {\n      return vnode\n    }\n  }\n\n  function extractTransitionData(comp) {\n    var data = {};\n    var options = comp.$options;\n    // props\n    for (var key in options.propsData) {\n      data[key] = comp[key];\n    }\n    // events.\n    // extract listeners and pass them directly to the transition methods\n    var listeners = options._parentListeners;\n    for (var key$1 in listeners) {\n      data[camelize(key$1)] = listeners[key$1];\n    }\n    return data\n  }\n\n  function placeholder(h, rawChild) {\n    if (/\\d-keep-alive$/.test(rawChild.tag)) {\n      return h('keep-alive', {\n        props: rawChild.componentOptions.propsData\n      })\n    }\n  }\n\n  function hasParentTransition(vnode) {\n    while ((vnode = vnode.parent)) {\n      if (vnode.data.transition) {\n        return true\n      }\n    }\n  }\n\n  function isSameChild(child, oldChild) {\n    return oldChild.key === child.key && oldChild.tag === child.tag\n  }\n\n  var isNotTextNode = function (c) {\n    return c.tag || isAsyncPlaceholder(c);\n  };\n\n  var isVShowDirective = function (d) {\n    return d.name === 'show';\n  };\n\n  var Transition = {\n    name: 'transition',\n    props: transitionProps,\n    abstract: true,\n\n    render: function render(h) {\n      var this$1 = this;\n\n      var children = this.$slots.default;\n      if (!children) {\n        return\n      }\n\n      // filter out text nodes (possible whitespaces)\n      children = children.filter(isNotTextNode);\n      /* istanbul ignore if */\n      if (!children.length) {\n        return\n      }\n\n      // warn multiple elements\n      if (children.length > 1) {\n        warn(\n          '<transition> can only be used on a single element. Use ' +\n          '<transition-group> for lists.',\n          this.$parent\n        );\n      }\n\n      var mode = this.mode;\n\n      // warn invalid mode\n      if (mode && mode !== 'in-out' && mode !== 'out-in') {\n        warn(\n          'invalid <transition> mode: ' + mode,\n          this.$parent\n        );\n      }\n\n      var rawChild = children[0];\n\n      // if this is a component root node and the component's\n      // parent container node also has transition, skip.\n      if (hasParentTransition(this.$vnode)) {\n        return rawChild\n      }\n\n      // apply transition data to child\n      // use getRealChild() to ignore abstract components e.g. keep-alive\n      var child = getRealChild(rawChild);\n      /* istanbul ignore if */\n      if (!child) {\n        return rawChild\n      }\n\n      if (this._leaving) {\n        return placeholder(h, rawChild)\n      }\n\n      // ensure a key that is unique to the vnode type and to this transition\n      // component instance. This key will be used to remove pending leaving nodes\n      // during entering.\n      var id = \"__transition-\" + (this._uid) + \"-\";\n      child.key = child.key == null ?\n        child.isComment ?\n        id + 'comment' :\n        id + child.tag :\n        isPrimitive(child.key) ?\n        (String(child.key).indexOf(id) === 0 ? child.key : id + child.key) :\n        child.key;\n\n      var data = (child.data || (child.data = {})).transition = extractTransitionData(this);\n      var oldRawChild = this._vnode;\n      var oldChild = getRealChild(oldRawChild);\n\n      // mark v-show\n      // so that the transition module can hand over the control to the directive\n      if (child.data.directives && child.data.directives.some(isVShowDirective)) {\n        child.data.show = true;\n      }\n\n      if (\n        oldChild &&\n        oldChild.data &&\n        !isSameChild(child, oldChild) &&\n        !isAsyncPlaceholder(oldChild) &&\n        // #6687 component root is a comment node\n        !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)\n      ) {\n        // replace old child transition data with fresh one\n        // important for dynamic transitions!\n        var oldData = oldChild.data.transition = extend({}, data);\n        // handle transition mode\n        if (mode === 'out-in') {\n          // return placeholder node and queue update when leave finishes\n          this._leaving = true;\n          mergeVNodeHook(oldData, 'afterLeave', function () {\n            this$1._leaving = false;\n            this$1.$forceUpdate();\n          });\n          return placeholder(h, rawChild)\n        } else if (mode === 'in-out') {\n          if (isAsyncPlaceholder(child)) {\n            return oldRawChild\n          }\n          var delayedLeave;\n          var performLeave = function () {\n            delayedLeave();\n          };\n          mergeVNodeHook(data, 'afterEnter', performLeave);\n          mergeVNodeHook(data, 'enterCancelled', performLeave);\n          mergeVNodeHook(oldData, 'delayLeave', function (leave) {\n            delayedLeave = leave;\n          });\n        }\n      }\n\n      return rawChild\n    }\n  };\n\n  /*  */\n\n  var props = extend({\n    tag: String,\n    moveClass: String\n  }, transitionProps);\n\n  delete props.mode;\n\n  var TransitionGroup = {\n    props: props,\n\n    beforeMount: function beforeMount() {\n      var this$1 = this;\n\n      var update = this._update;\n      this._update = function (vnode, hydrating) {\n        var restoreActiveInstance = setActiveInstance(this$1);\n        // force removing pass\n        this$1.__patch__(\n          this$1._vnode,\n          this$1.kept,\n          false, // hydrating\n          true // removeOnly (!important, avoids unnecessary moves)\n        );\n        this$1._vnode = this$1.kept;\n        restoreActiveInstance();\n        update.call(this$1, vnode, hydrating);\n      };\n    },\n\n    render: function render(h) {\n      var tag = this.tag || this.$vnode.data.tag || 'span';\n      var map = Object.create(null);\n      var prevChildren = this.prevChildren = this.children;\n      var rawChildren = this.$slots.default || [];\n      var children = this.children = [];\n      var transitionData = extractTransitionData(this);\n\n      for (var i = 0; i < rawChildren.length; i++) {\n        var c = rawChildren[i];\n        if (c.tag) {\n          if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {\n            children.push(c);\n            map[c.key] = c;\n            (c.data || (c.data = {})).transition = transitionData;\n          } else {\n            var opts = c.componentOptions;\n            var name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag;\n            warn((\"<transition-group> children must be keyed: <\" + name + \">\"));\n          }\n        }\n      }\n\n      if (prevChildren) {\n        var kept = [];\n        var removed = [];\n        for (var i$1 = 0; i$1 < prevChildren.length; i$1++) {\n          var c$1 = prevChildren[i$1];\n          c$1.data.transition = transitionData;\n          c$1.data.pos = c$1.elm.getBoundingClientRect();\n          if (map[c$1.key]) {\n            kept.push(c$1);\n          } else {\n            removed.push(c$1);\n          }\n        }\n        this.kept = h(tag, null, kept);\n        this.removed = removed;\n      }\n\n      return h(tag, null, children)\n    },\n\n    updated: function updated() {\n      var children = this.prevChildren;\n      var moveClass = this.moveClass || ((this.name || 'v') + '-move');\n      if (!children.length || !this.hasMove(children[0].elm, moveClass)) {\n        return\n      }\n\n      // we divide the work into three loops to avoid mixing DOM reads and writes\n      // in each iteration - which helps prevent layout thrashing.\n      children.forEach(callPendingCbs);\n      children.forEach(recordPosition);\n      children.forEach(applyTranslation);\n\n      // force reflow to put everything in position\n      // assign to this to avoid being removed in tree-shaking\n      // $flow-disable-line\n      this._reflow = document.body.offsetHeight;\n\n      children.forEach(function (c) {\n        if (c.data.moved) {\n          var el = c.elm;\n          var s = el.style;\n          addTransitionClass(el, moveClass);\n          s.transform = s.WebkitTransform = s.transitionDuration = '';\n          el.addEventListener(transitionEndEvent, el._moveCb = function cb(e) {\n            if (e && e.target !== el) {\n              return\n            }\n            if (!e || /transform$/.test(e.propertyName)) {\n              el.removeEventListener(transitionEndEvent, cb);\n              el._moveCb = null;\n              removeTransitionClass(el, moveClass);\n            }\n          });\n        }\n      });\n    },\n\n    methods: {\n      hasMove: function hasMove(el, moveClass) {\n        /* istanbul ignore if */\n        if (!hasTransition) {\n          return false\n        }\n        /* istanbul ignore if */\n        if (this._hasMove) {\n          return this._hasMove\n        }\n        // Detect whether an element with the move class applied has\n        // CSS transitions. Since the element may be inside an entering\n        // transition at this very moment, we make a clone of it and remove\n        // all other transition classes applied to ensure only the move class\n        // is applied.\n        var clone = el.cloneNode();\n        if (el._transitionClasses) {\n          el._transitionClasses.forEach(function (cls) {\n            removeClass(clone, cls);\n          });\n        }\n        addClass(clone, moveClass);\n        clone.style.display = 'none';\n        this.$el.appendChild(clone);\n        var info = getTransitionInfo(clone);\n        this.$el.removeChild(clone);\n        return (this._hasMove = info.hasTransform)\n      }\n    }\n  };\n\n  function callPendingCbs(c) {\n    /* istanbul ignore if */\n    if (c.elm._moveCb) {\n      c.elm._moveCb();\n    }\n    /* istanbul ignore if */\n    if (c.elm._enterCb) {\n      c.elm._enterCb();\n    }\n  }\n\n  function recordPosition(c) {\n    c.data.newPos = c.elm.getBoundingClientRect();\n  }\n\n  function applyTranslation(c) {\n    var oldPos = c.data.pos;\n    var newPos = c.data.newPos;\n    var dx = oldPos.left - newPos.left;\n    var dy = oldPos.top - newPos.top;\n    if (dx || dy) {\n      c.data.moved = true;\n      var s = c.elm.style;\n      s.transform = s.WebkitTransform = \"translate(\" + dx + \"px,\" + dy + \"px)\";\n      s.transitionDuration = '0s';\n    }\n  }\n\n  var platformComponents = {\n    Transition: Transition,\n    TransitionGroup: TransitionGroup\n  };\n\n  /*  */\n\n  // install platform specific utils\n  Vue.config.mustUseProp = mustUseProp;\n  Vue.config.isReservedTag = isReservedTag;\n  Vue.config.isReservedAttr = isReservedAttr;\n  Vue.config.getTagNamespace = getTagNamespace;\n  Vue.config.isUnknownElement = isUnknownElement;\n  // install platform runtime directives & components\n  extend(Vue.options.directives, platformDirectives);\n  extend(Vue.options.components, platformComponents);\n\n  // install platform patch function\n  // 浏览器端才有DOM，服务端没有dom，所以patch为一个空函数\n  Vue.prototype.__patch__ = inBrowser ? patch : noop;\n\n  // public mount method\n  Vue.prototype.$mount = function (\n    el,\n    hydrating\n  ) {\n    el = el && inBrowser ? query(el) : undefined;\n    return mountComponent(this, el, hydrating)\n  };\n\n  // devtools global hook\n  /* istanbul ignore next */\n  if (inBrowser) {\n    setTimeout(function () {\n      if (config.devtools) {\n        if (devtools) {\n          devtools.emit('init', Vue);\n        } else {\n          console[console.info ? 'info' : 'log'](\n            'Download the Vue Devtools extension for a better development experience:\\n' +\n            'https://github.com/vuejs/vue-devtools'\n          );\n        }\n      }\n      if (config.productionTip !== false &&\n        typeof console !== 'undefined'\n      ) {\n        console[console.info ? 'info' : 'log'](\n          \"You are running Vue in development mode.\\n\" +\n          \"Make sure to turn on production mode when deploying for production.\\n\" +\n          \"See more tips at https://vuejs.org/guide/deployment.html\"\n        );\n      }\n    }, 0);\n  }\n\n  /*  */\n\n  var defaultTagRE = /\\{\\{((?:.|\\r?\\n)+?)\\}\\}/g;\n  var regexEscapeRE = /[-.*+?^${}()|[\\]\\/\\\\]/g;\n\n  var buildRegex = cached(function (delimiters) {\n    var open = delimiters[0].replace(regexEscapeRE, '\\\\$&');\n    var close = delimiters[1].replace(regexEscapeRE, '\\\\$&');\n    return new RegExp(open + '((?:.|\\\\n)+?)' + close, 'g')\n  });\n\n\n\n  function parseText(\n    text,\n    delimiters\n  ) {\n    var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;\n    if (!tagRE.test(text)) {\n      return\n    }\n    var tokens = [];\n    var rawTokens = [];\n    var lastIndex = tagRE.lastIndex = 0;\n    var match, index, tokenValue;\n    while ((match = tagRE.exec(text))) {\n      index = match.index;\n      // push text token\n      if (index > lastIndex) {\n        rawTokens.push(tokenValue = text.slice(lastIndex, index));\n        tokens.push(JSON.stringify(tokenValue));\n      }\n      // tag token\n      var exp = parseFilters(match[1].trim());\n      tokens.push((\"_s(\" + exp + \")\"));\n      rawTokens.push({\n        '@binding': exp\n      });\n      lastIndex = index + match[0].length;\n    }\n    if (lastIndex < text.length) {\n      rawTokens.push(tokenValue = text.slice(lastIndex));\n      tokens.push(JSON.stringify(tokenValue));\n    }\n    return {\n      expression: tokens.join('+'),\n      tokens: rawTokens\n    }\n  }\n\n  /*  */\n\n  function transformNode(el, options) {\n    var warn = options.warn || baseWarn;\n    var staticClass = getAndRemoveAttr(el, 'class');\n    if (staticClass) {\n      var res = parseText(staticClass, options.delimiters);\n      if (res) {\n        warn(\n          \"class=\\\"\" + staticClass + \"\\\": \" +\n          'Interpolation inside attributes has been removed. ' +\n          'Use v-bind or the colon shorthand instead. For example, ' +\n          'instead of <div class=\"{{ val }}\">, use <div :class=\"val\">.',\n          el.rawAttrsMap['class']\n        );\n      }\n    }\n    if (staticClass) {\n      el.staticClass = JSON.stringify(staticClass);\n    }\n    var classBinding = getBindingAttr(el, 'class', false /* getStatic */ );\n    if (classBinding) {\n      el.classBinding = classBinding;\n    }\n  }\n\n  function genData(el) {\n    var data = '';\n    if (el.staticClass) {\n      data += \"staticClass:\" + (el.staticClass) + \",\";\n    }\n    if (el.classBinding) {\n      data += \"class:\" + (el.classBinding) + \",\";\n    }\n    return data\n  }\n\n  var klass$1 = {\n    staticKeys: ['staticClass'],\n    transformNode: transformNode,\n    genData: genData\n  };\n\n  /*  */\n\n  function transformNode$1(el, options) {\n    var warn = options.warn || baseWarn;\n    var staticStyle = getAndRemoveAttr(el, 'style');\n    if (staticStyle) {\n      /* istanbul ignore if */\n      {\n        var res = parseText(staticStyle, options.delimiters);\n        if (res) {\n          warn(\n            \"style=\\\"\" + staticStyle + \"\\\": \" +\n            'Interpolation inside attributes has been removed. ' +\n            'Use v-bind or the colon shorthand instead. For example, ' +\n            'instead of <div style=\"{{ val }}\">, use <div :style=\"val\">.',\n            el.rawAttrsMap['style']\n          );\n        }\n      }\n      el.staticStyle = JSON.stringify(parseStyleText(staticStyle));\n    }\n\n    var styleBinding = getBindingAttr(el, 'style', false /* getStatic */ );\n    if (styleBinding) {\n      el.styleBinding = styleBinding;\n    }\n  }\n\n  function genData$1(el) {\n    var data = '';\n    if (el.staticStyle) {\n      data += \"staticStyle:\" + (el.staticStyle) + \",\";\n    }\n    if (el.styleBinding) {\n      data += \"style:(\" + (el.styleBinding) + \"),\";\n    }\n    return data\n  }\n\n  var style$1 = {\n    staticKeys: ['staticStyle'],\n    transformNode: transformNode$1,\n    genData: genData$1\n  };\n\n  /*  */\n\n  var decoder;\n\n  var he = {\n    decode: function decode(html) {\n      decoder = decoder || document.createElement('div');\n      decoder.innerHTML = html;\n      return decoder.textContent\n    }\n  };\n\n  /*  */\n\n  var isUnaryTag = makeMap(\n    'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +\n    'link,meta,param,source,track,wbr'\n  );\n\n  // Elements that you can, intentionally, leave open\n  // (and which close themselves)\n  var canBeLeftOpenTag = makeMap(\n    'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'\n  );\n\n  // HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3\n  // Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content\n  var isNonPhrasingTag = makeMap(\n    'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +\n    'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +\n    'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +\n    'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +\n    'title,tr,track'\n  );\n\n  /**\n   * Not type-checking this file because it's mostly vendor code.\n   */\n\n  // Regular Expressions for parsing tags and attributes\n  var attribute = /^\\s*([^\\s\"'<>\\/=]+)(?:\\s*(=)\\s*(?:\"([^\"]*)\"+|'([^']*)'+|([^\\s\"'=<>`]+)))?/;\n  var dynamicArgAttribute = /^\\s*((?:v-[\\w-]+:|@|:|#)\\[[^=]+\\][^\\s\"'<>\\/=]*)(?:\\s*(=)\\s*(?:\"([^\"]*)\"+|'([^']*)'+|([^\\s\"'=<>`]+)))?/;\n  var ncname = \"[a-zA-Z_][\\\\-\\\\.0-9_a-zA-Z\" + (unicodeRegExp.source) + \"]*\";\n  var qnameCapture = \"((?:\" + ncname + \"\\\\:)?\" + ncname + \")\";\n  var startTagOpen = new RegExp((\"^<\" + qnameCapture));\n  var startTagClose = /^\\s*(\\/?)>/;\n  var endTag = new RegExp((\"^<\\\\/\" + qnameCapture + \"[^>]*>\"));\n  var doctype = /^<!DOCTYPE [^>]+>/i;\n  // #7298: escape - to avoid being pased as HTML comment when inlined in page\n  var comment = /^<!\\--/;\n  var conditionalComment = /^<!\\[/;\n\n  // Special Elements (can contain anything)\n  var isPlainTextElement = makeMap('script,style,textarea', true);\n  var reCache = {};\n\n  var decodingMap = {\n    '&lt;': '<',\n    '&gt;': '>',\n    '&quot;': '\"',\n    '&amp;': '&',\n    '&#10;': '\\n',\n    '&#9;': '\\t',\n    '&#39;': \"'\"\n  };\n  var encodedAttr = /&(?:lt|gt|quot|amp|#39);/g;\n  var encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g;\n\n  // #5992\n  var isIgnoreNewlineTag = makeMap('pre,textarea', true);\n  var shouldIgnoreFirstNewline = function (tag, html) {\n    return tag && isIgnoreNewlineTag(tag) && html[0] === '\\n';\n  };\n\n  function decodeAttr(value, shouldDecodeNewlines) {\n    var re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;\n    return value.replace(re, function (match) {\n      return decodingMap[match];\n    })\n  }\n\n  function parseHTML(html, options) {\n    var stack = [];\n    var expectHTML = options.expectHTML;\n    var isUnaryTag###1 = options.isUnaryTag || no;\n    var canBeLeftOpenTag###1 = options.canBeLeftOpenTag || no;\n    var index = 0;\n    var last, lastTag;\n    while (html) {\n      last = html;\n      // Make sure we're not in a plaintext content element like script/style\n      if (!lastTag || !isPlainTextElement(lastTag)) {\n        var textEnd = html.indexOf('<');\n        if (textEnd === 0) {\n          // Comment:\n          if (comment.test(html)) {\n            var commentEnd = html.indexOf('-->');\n\n            if (commentEnd >= 0) {\n              if (options.shouldKeepComment) {\n                options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);\n              }\n              advance(commentEnd + 3);\n              continue\n            }\n          }\n\n          // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment\n          if (conditionalComment.test(html)) {\n            var conditionalEnd = html.indexOf(']>');\n\n            if (conditionalEnd >= 0) {\n              advance(conditionalEnd + 2);\n              continue\n            }\n          }\n\n          // Doctype:\n          var doctypeMatch = html.match(doctype);\n          if (doctypeMatch) {\n            advance(doctypeMatch[0].length);\n            continue\n          }\n\n          // End tag:\n          var endTagMatch = html.match(endTag);\n          if (endTagMatch) {\n            var curIndex = index;\n            advance(endTagMatch[0].length);\n            parseEndTag(endTagMatch[1], curIndex, index);\n            continue\n          }\n\n          // Start tag:\n          var startTagMatch = parseStartTag();\n          if (startTagMatch) {\n            handleStartTag(startTagMatch);\n            if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {\n              advance(1);\n            }\n            continue\n          }\n        }\n\n        var text = (void 0),\n          rest = (void 0),\n          next = (void 0);\n        if (textEnd >= 0) {\n          rest = html.slice(textEnd);\n          while (\n            !endTag.test(rest) &&\n            !startTagOpen.test(rest) &&\n            !comment.test(rest) &&\n            !conditionalComment.test(rest)\n          ) {\n            // < in plain text, be forgiving and treat it as text\n            next = rest.indexOf('<', 1);\n            if (next < 0) {\n              break\n            }\n            textEnd += next;\n            rest = html.slice(textEnd);\n          }\n          text = html.substring(0, textEnd);\n        }\n\n        if (textEnd < 0) {\n          text = html;\n        }\n\n        if (text) {\n          advance(text.length);\n        }\n\n        if (options.chars && text) {\n          options.chars(text, index - text.length, index);\n        }\n      } else {\n        var endTagLength = 0;\n        var stackedTag = lastTag.toLowerCase();\n        var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\\\s\\\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));\n        var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {\n          endTagLength = endTag.length;\n          if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {\n            text = text\n              .replace(/<!\\--([\\s\\S]*?)-->/g, '$1') // #7298\n              .replace(/<!\\[CDATA\\[([\\s\\S]*?)]]>/g, '$1');\n          }\n          if (shouldIgnoreFirstNewline(stackedTag, text)) {\n            text = text.slice(1);\n          }\n          if (options.chars) {\n            options.chars(text);\n          }\n          return ''\n        });\n        index += html.length - rest$1.length;\n        html = rest$1;\n        parseEndTag(stackedTag, index - endTagLength, index);\n      }\n\n      if (html === last) {\n        options.chars && options.chars(html);\n        if (!stack.length && options.warn) {\n          options.warn((\"Mal-formatted tag at end of template: \\\"\" + html + \"\\\"\"), {\n            start: index + html.length\n          });\n        }\n        break\n      }\n    }\n\n    // Clean up any remaining tags\n    parseEndTag();\n\n    function advance(n) {\n      index += n;\n      html = html.substring(n);\n    }\n\n    function parseStartTag() {\n      var start = html.match(startTagOpen);\n      if (start) {\n        var match = {\n          tagName: start[1],\n          attrs: [],\n          start: index\n        };\n        advance(start[0].length);\n        var end, attr;\n        while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {\n          attr.start = index;\n          advance(attr[0].length);\n          attr.end = index;\n          match.attrs.push(attr);\n        }\n        if (end) {\n          match.unarySlash = end[1];\n          advance(end[0].length);\n          match.end = index;\n          return match\n        }\n      }\n    }\n\n    function handleStartTag(match) {\n      var tagName = match.tagName;\n      var unarySlash = match.unarySlash;\n\n      if (expectHTML) {\n        if (lastTag === 'p' && isNonPhrasingTag(tagName)) {\n          parseEndTag(lastTag);\n        }\n        if (canBeLeftOpenTag###1(tagName) && lastTag === tagName) {\n          parseEndTag(tagName);\n        }\n      }\n\n      var unary = isUnaryTag###1(tagName) || !!unarySlash;\n\n      var l = match.attrs.length;\n      var attrs = new Array(l);\n      for (var i = 0; i < l; i++) {\n        var args = match.attrs[i];\n        var value = args[3] || args[4] || args[5] || '';\n        var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' ?\n          options.shouldDecodeNewlinesForHref :\n          options.shouldDecodeNewlines;\n        attrs[i] = {\n          name: args[1],\n          value: decodeAttr(value, shouldDecodeNewlines)\n        };\n        if (options.outputSourceRange) {\n          attrs[i].start = args.start + args[0].match(/^\\s*/).length;\n          attrs[i].end = args.end;\n        }\n      }\n\n      if (!unary) {\n        stack.push({\n          tag: tagName,\n          lowerCasedTag: tagName.toLowerCase(),\n          attrs: attrs,\n          start: match.start,\n          end: match.end\n        });\n        lastTag = tagName;\n      }\n\n      if (options.start) {\n        options.start(tagName, attrs, unary, match.start, match.end);\n      }\n    }\n\n    function parseEndTag(tagName, start, end) {\n      var pos, lowerCasedTagName;\n      if (start == null) {\n        start = index;\n      }\n      if (end == null) {\n        end = index;\n      }\n\n      // Find the closest opened tag of the same type\n      if (tagName) {\n        lowerCasedTagName = tagName.toLowerCase();\n        for (pos = stack.length - 1; pos >= 0; pos--) {\n          if (stack[pos].lowerCasedTag === lowerCasedTagName) {\n            break\n          }\n        }\n      } else {\n        // If no tag name is provided, clean shop\n        pos = 0;\n      }\n\n      if (pos >= 0) {\n        // Close all the open elements, up the stack\n        for (var i = stack.length - 1; i >= pos; i--) {\n          if (i > pos || !tagName &&\n            options.warn\n          ) {\n            options.warn(\n              (\"tag <\" + (stack[i].tag) + \"> has no matching end tag.\"), {\n                start: stack[i].start,\n                end: stack[i].end\n              }\n            );\n          }\n          if (options.end) {\n            options.end(stack[i].tag, start, end);\n          }\n        }\n\n        // Remove the open elements from the stack\n        stack.length = pos;\n        lastTag = pos && stack[pos - 1].tag;\n      } else if (lowerCasedTagName === 'br') {\n        if (options.start) {\n          options.start(tagName, [], true, start, end);\n        }\n      } else if (lowerCasedTagName === 'p') {\n        if (options.start) {\n          options.start(tagName, [], false, start, end);\n        }\n        if (options.end) {\n          options.end(tagName, start, end);\n        }\n      }\n    }\n  }\n\n  /*  */\n\n  var onRE = /^@|^v-on:/;\n  var dirRE = /^v-|^@|^:/;\n  var forAliasRE = /([\\s\\S]*?)\\s+(?:in|of)\\s+([\\s\\S]*)/;\n  var forIteratorRE = /,([^,\\}\\]]*)(?:,([^,\\}\\]]*))?$/;\n  var stripParensRE = /^\\(|\\)$/g;\n  var dynamicArgRE = /^\\[.*\\]$/;\n\n  var argRE = /:(.*)$/;\n  var bindRE = /^:|^\\.|^v-bind:/;\n  var modifierRE = /\\.[^.\\]]+(?=[^\\]]*$)/g;\n\n  var slotRE = /^v-slot(:|$)|^#/;\n\n  var lineBreakRE = /[\\r\\n]/;\n  var whitespaceRE$1 = /\\s+/g;\n\n  var invalidAttributeRE = /[\\s\"'<>\\/=]/;\n\n  var decodeHTMLCached = cached(he.decode);\n\n  var emptySlotScopeToken = \"_empty_\";\n\n  // configurable state\n  var warn$2;\n  var delimiters;\n  var transforms;\n  var preTransforms;\n  var postTransforms;\n  var platformIsPreTag;\n  var platformMustUseProp;\n  var platformGetTagNamespace;\n  var maybeComponent;\n\n  function createASTElement(\n    tag,\n    attrs,\n    parent\n  ) {\n    return {\n      type: 1,\n      tag: tag,\n      attrsList: attrs,\n      attrsMap: makeAttrsMap(attrs),\n      rawAttrsMap: {},\n      parent: parent,\n      children: []\n    }\n  }\n\n  /**\n   * Convert HTML string to AST.\n   */\n  function parse(\n    template,\n    options\n  ) {\n    warn$2 = options.warn || baseWarn;\n\n    platformIsPreTag = options.isPreTag || no;\n    platformMustUseProp = options.mustUseProp || no;\n    platformGetTagNamespace = options.getTagNamespace || no;\n    var isReservedTag = options.isReservedTag || no;\n    maybeComponent = function (el) {\n      return !!el.component || !isReservedTag(el.tag);\n    };\n\n    transforms = pluckModuleFunction(options.modules, 'transformNode');\n    preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');\n    postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');\n\n    delimiters = options.delimiters;\n\n    var stack = [];\n    var preserveWhitespace = options.preserveWhitespace !== false;\n    var whitespaceOption = options.whitespace;\n    var root;\n    var currentParent;\n    var inVPre = false;\n    var inPre = false;\n    var warned = false;\n\n    function warnOnce(msg, range) {\n      if (!warned) {\n        warned = true;\n        warn$2(msg, range);\n      }\n    }\n\n    function closeElement(element) {\n      trimEndingWhitespace(element);\n      if (!inVPre && !element.processed) {\n        element = processElement(element, options);\n      }\n      // tree management\n      if (!stack.length && element !== root) {\n        // allow root elements with v-if, v-else-if and v-else\n        if (root.if && (element.elseif || element.else)) {\n          {\n            checkRootConstraints(element);\n          }\n          addIfCondition(root, {\n            exp: element.elseif,\n            block: element\n          });\n        } else {\n          warnOnce(\n            \"Component template should contain exactly one root element. \" +\n            \"If you are using v-if on multiple elements, \" +\n            \"use v-else-if to chain them instead.\", {\n              start: element.start\n            }\n          );\n        }\n      }\n      if (currentParent && !element.forbidden) {\n        if (element.elseif || element.else) {\n          processIfConditions(element, currentParent);\n        } else {\n          if (element.slotScope) {\n            // scoped slot\n            // keep it in the children list so that v-else(-if) conditions can\n            // find it as the prev node.\n            var name = element.slotTarget || '\"default\"';\n            (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;\n          }\n          currentParent.children.push(element);\n          element.parent = currentParent;\n        }\n      }\n\n      // final children cleanup\n      // filter out scoped slots\n      element.children = element.children.filter(function (c) {\n        return !(c).slotScope;\n      });\n      // remove trailing whitespace node again\n      trimEndingWhitespace(element);\n\n      // check pre state\n      if (element.pre) {\n        inVPre = false;\n      }\n      if (platformIsPreTag(element.tag)) {\n        inPre = false;\n      }\n      // apply post-transforms\n      for (var i = 0; i < postTransforms.length; i++) {\n        postTransforms[i](element, options);\n      }\n    }\n\n    function trimEndingWhitespace(el) {\n      // remove trailing whitespace node\n      if (!inPre) {\n        var lastNode;\n        while (\n          (lastNode = el.children[el.children.length - 1]) &&\n          lastNode.type === 3 &&\n          lastNode.text === ' '\n        ) {\n          el.children.pop();\n        }\n      }\n    }\n\n    function checkRootConstraints(el) {\n      if (el.tag === 'slot' || el.tag === 'template') {\n        warnOnce(\n          \"Cannot use <\" + (el.tag) + \"> as component root element because it may \" +\n          'contain multiple nodes.', {\n            start: el.start\n          }\n        );\n      }\n      if (el.attrsMap.hasOwnProperty('v-for')) {\n        warnOnce(\n          'Cannot use v-for on stateful component root element because ' +\n          'it renders multiple elements.',\n          el.rawAttrsMap['v-for']\n        );\n      }\n    }\n\n    parseHTML(template, {\n      warn: warn$2,\n      expectHTML: options.expectHTML,\n      isUnaryTag: options.isUnaryTag,\n      canBeLeftOpenTag: options.canBeLeftOpenTag,\n      shouldDecodeNewlines: options.shouldDecodeNewlines,\n      shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,\n      shouldKeepComment: options.comments,\n      outputSourceRange: options.outputSourceRange,\n      start: function start(tag, attrs, unary, start$1, end) {\n        // check namespace.\n        // inherit parent ns if there is one\n        var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);\n\n        // handle IE svg bug\n        /* istanbul ignore if */\n        if (isIE && ns === 'svg') {\n          attrs = guardIESVGBug(attrs);\n        }\n\n        var element = createASTElement(tag, attrs, currentParent);\n        if (ns) {\n          element.ns = ns;\n        }\n\n        {\n          if (options.outputSourceRange) {\n            element.start = start$1;\n            element.end = end;\n            element.rawAttrsMap = element.attrsList.reduce(function (cumulated, attr) {\n              cumulated[attr.name] = attr;\n              return cumulated\n            }, {});\n          }\n          attrs.forEach(function (attr) {\n            if (invalidAttributeRE.test(attr.name)) {\n              warn$2(\n                \"Invalid dynamic argument expression: attribute names cannot contain \" +\n                \"spaces, quotes, <, >, / or =.\", {\n                  start: attr.start + attr.name.indexOf(\"[\"),\n                  end: attr.start + attr.name.length\n                }\n              );\n            }\n          });\n        }\n\n        if (isForbiddenTag(element) && !isServerRendering()) {\n          element.forbidden = true;\n          warn$2(\n            'Templates should only be responsible for mapping the state to the ' +\n            'UI. Avoid placing tags with side-effects in your templates, such as ' +\n            \"<\" + tag + \">\" + ', as they will not be parsed.', {\n              start: element.start\n            }\n          );\n        }\n\n        // apply pre-transforms\n        for (var i = 0; i < preTransforms.length; i++) {\n          element = preTransforms[i](element, options) || element;\n        }\n\n        if (!inVPre) {\n          processPre(element);\n          if (element.pre) {\n            inVPre = true;\n          }\n        }\n        if (platformIsPreTag(element.tag)) {\n          inPre = true;\n        }\n        if (inVPre) {\n          processRawAttrs(element);\n        } else if (!element.processed) {\n          // structural directives\n          processFor(element);\n          processIf(element);\n          processOnce(element);\n        }\n\n        if (!root) {\n          root = element; {\n            checkRootConstraints(root);\n          }\n        }\n\n        if (!unary) {\n          currentParent = element;\n          stack.push(element);\n        } else {\n          closeElement(element);\n        }\n      },\n\n      end: function end(tag, start, end$1) {\n        var element = stack[stack.length - 1];\n        // pop stack\n        stack.length -= 1;\n        currentParent = stack[stack.length - 1];\n        if (options.outputSourceRange) {\n          element.end = end$1;\n        }\n        closeElement(element);\n      },\n\n      chars: function chars(text, start, end) {\n        if (!currentParent) {\n          {\n            if (text === template) {\n              warnOnce(\n                'Component template requires a root element, rather than just text.', {\n                  start: start\n                }\n              );\n            } else if ((text = text.trim())) {\n              warnOnce(\n                (\"text \\\"\" + text + \"\\\" outside root element will be ignored.\"), {\n                  start: start\n                }\n              );\n            }\n          }\n          return\n        }\n        // IE textarea placeholder bug\n        /* istanbul ignore if */\n        if (isIE &&\n          currentParent.tag === 'textarea' &&\n          currentParent.attrsMap.placeholder === text\n        ) {\n          return\n        }\n        var children = currentParent.children;\n        if (inPre || text.trim()) {\n          text = isTextTag(currentParent) ? text : decodeHTMLCached(text);\n        } else if (!children.length) {\n          // remove the whitespace-only node right after an opening tag\n          text = '';\n        } else if (whitespaceOption) {\n          if (whitespaceOption === 'condense') {\n            // in condense mode, remove the whitespace node if it contains\n            // line break, otherwise condense to a single space\n            text = lineBreakRE.test(text) ? '' : ' ';\n          } else {\n            text = ' ';\n          }\n        } else {\n          text = preserveWhitespace ? ' ' : '';\n        }\n        if (text) {\n          if (whitespaceOption === 'condense') {\n            // condense consecutive whitespaces into single space\n            text = text.replace(whitespaceRE$1, ' ');\n          }\n          var res;\n          var child;\n          if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {\n            child = {\n              type: 2,\n              expression: res.expression,\n              tokens: res.tokens,\n              text: text\n            };\n          } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {\n            child = {\n              type: 3,\n              text: text\n            };\n          }\n          if (child) {\n            if (options.outputSourceRange) {\n              child.start = start;\n              child.end = end;\n            }\n            children.push(child);\n          }\n        }\n      },\n      comment: function comment(text, start, end) {\n        // adding anyting as a sibling to the root node is forbidden\n        // comments should still be allowed, but ignored\n        if (currentParent) {\n          var child = {\n            type: 3,\n            text: text,\n            isComment: true\n          };\n          if (options.outputSourceRange) {\n            child.start = start;\n            child.end = end;\n          }\n          currentParent.children.push(child);\n        }\n      }\n    });\n    return root\n  }\n\n  function processPre(el) {\n    if (getAndRemoveAttr(el, 'v-pre') != null) {\n      el.pre = true;\n    }\n  }\n\n  function processRawAttrs(el) {\n    var list = el.attrsList;\n    var len = list.length;\n    if (len) {\n      var attrs = el.attrs = new Array(len);\n      for (var i = 0; i < len; i++) {\n        attrs[i] = {\n          name: list[i].name,\n          value: JSON.stringify(list[i].value)\n        };\n        if (list[i].start != null) {\n          attrs[i].start = list[i].start;\n          attrs[i].end = list[i].end;\n        }\n      }\n    } else if (!el.pre) {\n      // non root node in pre blocks with no attributes\n      el.plain = true;\n    }\n  }\n\n  function processElement(\n    element,\n    options\n  ) {\n    processKey(element);\n\n    // determine whether this is a plain element after\n    // removing structural attributes\n    element.plain = (\n      !element.key &&\n      !element.scopedSlots &&\n      !element.attrsList.length\n    );\n\n    processRef(element);\n    processSlotContent(element);\n    processSlotOutlet(element);\n    processComponent(element);\n    for (var i = 0; i < transforms.length; i++) {\n      element = transforms[i](element, options) || element;\n    }\n    processAttrs(element);\n    return element\n  }\n\n  function processKey(el) {\n    var exp = getBindingAttr(el, 'key');\n    if (exp) {\n      {\n        if (el.tag === 'template') {\n          warn$2(\n            \"<template> cannot be keyed. Place the key on real elements instead.\",\n            getRawBindingAttr(el, 'key')\n          );\n        }\n        if (el.for) {\n          var iterator = el.iterator2 || el.iterator1;\n          var parent = el.parent;\n          if (iterator && iterator === exp && parent && parent.tag === 'transition-group') {\n            warn$2(\n              \"Do not use v-for index as key on <transition-group> children, \" +\n              \"this is the same as not using keys.\",\n              getRawBindingAttr(el, 'key'),\n              true /* tip */\n            );\n          }\n        }\n      }\n      el.key = exp;\n    }\n  }\n\n  function processRef(el) {\n    var ref = getBindingAttr(el, 'ref');\n    if (ref) {\n      el.ref = ref;\n      el.refInFor = checkInFor(el);\n    }\n  }\n\n  function processFor(el) {\n    var exp;\n    if ((exp = getAndRemoveAttr(el, 'v-for'))) {\n      var res = parseFor(exp);\n      if (res) {\n        extend(el, res);\n      } else {\n        warn$2(\n          (\"Invalid v-for expression: \" + exp),\n          el.rawAttrsMap['v-for']\n        );\n      }\n    }\n  }\n\n\n\n  function parseFor(exp) {\n    var inMatch = exp.match(forAliasRE);\n    if (!inMatch) {\n      return\n    }\n    var res = {};\n    res.for = inMatch[2].trim();\n    var alias = inMatch[1].trim().replace(stripParensRE, '');\n    var iteratorMatch = alias.match(forIteratorRE);\n    if (iteratorMatch) {\n      res.alias = alias.replace(forIteratorRE, '').trim();\n      res.iterator1 = iteratorMatch[1].trim();\n      if (iteratorMatch[2]) {\n        res.iterator2 = iteratorMatch[2].trim();\n      }\n    } else {\n      res.alias = alias;\n    }\n    return res\n  }\n\n  function processIf(el) {\n    var exp = getAndRemoveAttr(el, 'v-if');\n    if (exp) {\n      el.if = exp;\n      addIfCondition(el, {\n        exp: exp,\n        block: el\n      });\n    } else {\n      if (getAndRemoveAttr(el, 'v-else') != null) {\n        el.else = true;\n      }\n      var elseif = getAndRemoveAttr(el, 'v-else-if');\n      if (elseif) {\n        el.elseif = elseif;\n      }\n    }\n  }\n\n  function processIfConditions(el, parent) {\n    var prev = findPrevElement(parent.children);\n    if (prev && prev.if) {\n      addIfCondition(prev, {\n        exp: el.elseif,\n        block: el\n      });\n    } else {\n      warn$2(\n        \"v-\" + (el.elseif ? ('else-if=\"' + el.elseif + '\"') : 'else') + \" \" +\n        \"used on element <\" + (el.tag) + \"> without corresponding v-if.\",\n        el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']\n      );\n    }\n  }\n\n  function findPrevElement(children) {\n    var i = children.length;\n    while (i--) {\n      if (children[i].type === 1) {\n        return children[i]\n      } else {\n        if (children[i].text !== ' ') {\n          warn$2(\n            \"text \\\"\" + (children[i].text.trim()) + \"\\\" between v-if and v-else(-if) \" +\n            \"will be ignored.\",\n            children[i]\n          );\n        }\n        children.pop();\n      }\n    }\n  }\n\n  function addIfCondition(el, condition) {\n    if (!el.ifConditions) {\n      el.ifConditions = [];\n    }\n    el.ifConditions.push(condition);\n  }\n\n  function processOnce(el) {\n    var once###1 = getAndRemoveAttr(el, 'v-once');\n    if (once###1 != null) {\n      el.once = true;\n    }\n  }\n\n  // handle content being passed to a component as slot,\n  // e.g. <template slot=\"xxx\">, <div slot-scope=\"xxx\">\n  function processSlotContent(el) {\n    var slotScope;\n    if (el.tag === 'template') {\n      slotScope = getAndRemoveAttr(el, 'scope');\n      /* istanbul ignore if */\n      if (slotScope) {\n        warn$2(\n          \"the \\\"scope\\\" attribute for scoped slots have been deprecated and \" +\n          \"replaced by \\\"slot-scope\\\" since 2.5. The new \\\"slot-scope\\\" attribute \" +\n          \"can also be used on plain elements in addition to <template> to \" +\n          \"denote scoped slots.\",\n          el.rawAttrsMap['scope'],\n          true\n        );\n      }\n      el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope');\n    } else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {\n      /* istanbul ignore if */\n      if (el.attrsMap['v-for']) {\n        warn$2(\n          \"Ambiguous combined usage of slot-scope and v-for on <\" + (el.tag) + \"> \" +\n          \"(v-for takes higher priority). Use a wrapper <template> for the \" +\n          \"scoped slot to make it clearer.\",\n          el.rawAttrsMap['slot-scope'],\n          true\n        );\n      }\n      el.slotScope = slotScope;\n    }\n\n    // slot=\"xxx\"\n    var slotTarget = getBindingAttr(el, 'slot');\n    if (slotTarget) {\n      el.slotTarget = slotTarget === '\"\"' ? '\"default\"' : slotTarget;\n      el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']);\n      // preserve slot as an attribute for native shadow DOM compat\n      // only for non-scoped slots.\n      if (el.tag !== 'template' && !el.slotScope) {\n        addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'));\n      }\n    }\n\n    // 2.6 v-slot syntax\n    {\n      // 针对插槽<template v-slot:header></template>\n      if (el.tag === 'template') {\n        // v-slot on <template>\n        var slotBinding = getAndRemoveAttrByRegex(el, slotRE);\n        if (slotBinding) {\n          {\n            if (el.slotTarget || el.slotScope) {\n              warn$2(\n                \"Unexpected mixed usage of different slot syntaxes.\",\n                el\n              );\n            }\n            if (el.parent && !maybeComponent(el.parent)) {\n              warn$2(\n                \"<template v-slot> can only appear at the root level inside \" +\n                \"the receiving the component\",\n                el\n              );\n            }\n          }\n          var ref = getSlotName(slotBinding);\n          var name = ref.name;\n          var dynamic = ref.dynamic;\n          el.slotTarget = name;\n          el.slotTargetDynamic = dynamic;\n          el.slotScope = slotBinding.value || emptySlotScopeToken; // force it into a scoped slot for perf\n        }\n      } else {\n        // v-slot on component, denotes default slot\n        var slotBinding$1 = getAndRemoveAttrByRegex(el, slotRE);\n        if (slotBinding$1) {\n          {\n            if (!maybeComponent(el)) {\n              warn$2(\n                \"v-slot can only be used on components or <template>.\",\n                slotBinding$1\n              );\n            }\n            if (el.slotScope || el.slotTarget) {\n              warn$2(\n                \"Unexpected mixed usage of different slot syntaxes.\",\n                el\n              );\n            }\n            if (el.scopedSlots) {\n              warn$2(\n                \"To avoid scope ambiguity, the default slot should also use \" +\n                \"<template> syntax when there are other named slots.\",\n                slotBinding$1\n              );\n            }\n          }\n          // add the component's children to its default slot\n          var slots = el.scopedSlots || (el.scopedSlots = {});\n          var ref$1 = getSlotName(slotBinding$1);\n          var name$1 = ref$1.name;\n          var dynamic$1 = ref$1.dynamic;\n          var slotContainer = slots[name$1] = createASTElement('template', [], el);\n          slotContainer.slotTarget = name$1;\n          slotContainer.slotTargetDynamic = dynamic$1;\n          slotContainer.children = el.children.filter(function (c) {\n            if (!c.slotScope) {\n              c.parent = slotContainer;\n              return true\n            }\n          });\n          slotContainer.slotScope = slotBinding$1.value || emptySlotScopeToken;\n          // remove children as they are returned from scopedSlots now\n          el.children = [];\n          // mark el non-plain so data gets generated\n          el.plain = false;\n        }\n      }\n    }\n  }\n\n  function getSlotName(binding) {\n    var name = binding.name.replace(slotRE, '');\n    if (!name) {\n      if (binding.name[0] !== '#') {\n        name = 'default';\n      } else {\n        warn$2(\n          \"v-slot shorthand syntax requires a slot name.\",\n          binding\n        );\n      }\n    }\n    return dynamicArgRE.test(name)\n      // dynamic [name]\n      ?\n      {\n        name: name.slice(1, -1),\n        dynamic: true\n      }\n      // static name\n      :\n      {\n        name: (\"\\\"\" + name + \"\\\"\"),\n        dynamic: false\n      }\n  }\n\n  // handle <slot/> outlets\n  function processSlotOutlet(el) {\n    if (el.tag === 'slot') {\n      el.slotName = getBindingAttr(el, 'name');\n      if (el.key) {\n        warn$2(\n          \"`key` does not work on <slot> because slots are abstract outlets \" +\n          \"and can possibly expand into multiple elements. \" +\n          \"Use the key on a wrapping element instead.\",\n          getRawBindingAttr(el, 'key')\n        );\n      }\n    }\n  }\n\n  //  针对动态组件的解析\n  function processComponent(el) {\n    var binding;\n    // 拿到is属性所对应的值\n    if ((binding = getBindingAttr(el, 'is'))) {\n      // ast树上多了component的属性\n      el.component = binding;\n    }\n    if (getAndRemoveAttr(el, 'inline-template') != null) {\n      el.inlineTemplate = true;\n    }\n  }\n\n  function processAttrs(el) {\n    var list = el.attrsList;\n    var i, l, name, rawName, value, modifiers, syncGen, isDynamic;\n    for (i = 0, l = list.length; i < l; i++) {\n      name = rawName = list[i].name; // v-on:click\n      value = list[i].value; // doThis\n      if (dirRE.test(name)) { // 匹配v-或者@开头的指令\n        el.hasBindings = true;\n        modifiers = parseModifiers(name.replace(dirRE, '')); // parseModifiers('on:click')\n        if (modifiers) {\n          name = name.replace(modifierRE, '');\n        }\n        if (bindRE.test(name)) { // v-bind分支\n          name = name.replace(bindRE, '');\n          value = parseFilters(value);\n          isDynamic = dynamicArgRE.test(name);\n          if (isDynamic) {\n            name = name.slice(1, -1);\n          }\n          if (\n            value.trim().length === 0\n          ) {\n            warn$2(\n              (\"The value for a v-bind expression cannot be empty. Found in \\\"v-bind:\" + name + \"\\\"\")\n            );\n          }\n          if (modifiers) {\n            if (modifiers.prop && !isDynamic) {\n              name = camelize(name);\n              if (name === 'innerHtml') {\n                name = 'innerHTML';\n              }\n            }\n            if (modifiers.camel && !isDynamic) {\n              name = camelize(name);\n            }\n            if (modifiers.sync) {\n              syncGen = genAssignmentCode(value, \"$event\");\n              if (!isDynamic) {\n                addHandler(\n                  el,\n                  (\"update:\" + (camelize(name))),\n                  syncGen,\n                  null,\n                  false,\n                  warn$2,\n                  list[i]\n                );\n                if (hyphenate(name) !== camelize(name)) {\n                  addHandler(\n                    el,\n                    (\"update:\" + (hyphenate(name))),\n                    syncGen,\n                    null,\n                    false,\n                    warn$2,\n                    list[i]\n                  );\n                }\n              } else {\n                // handler w/ dynamic event name\n                addHandler(\n                  el,\n                  (\"\\\"update:\\\"+(\" + name + \")\"),\n                  syncGen,\n                  null,\n                  false,\n                  warn$2,\n                  list[i],\n                  true // dynamic\n                );\n              }\n            }\n          }\n          if ((modifiers && modifiers.prop) || (\n              !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)\n            )) {\n            addProp(el, name, value, list[i], isDynamic);\n          } else {\n            addAttr(el, name, value, list[i], isDynamic);\n          }\n        } else if (onRE.test(name)) { // v-on分支\n          name = name.replace(onRE, ''); // 拿到真正的事件click\n          isDynamic = dynamicArgRE.test(name); // 动态事件绑定\n          if (isDynamic) {\n            name = name.slice(1, -1);\n          }\n          addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);\n        } else { // normal directives\n          // 除了v-bind，v-on之外的普通指令\n          name = name.replace(dirRE, '');\n          // parse arg\n          var argMatch = name.match(argRE);\n          var arg = argMatch && argMatch[1];\n          isDynamic = false;\n          if (arg) {\n            name = name.slice(0, -(arg.length + 1));\n            if (dynamicArgRE.test(arg)) {\n              arg = arg.slice(1, -1);\n              isDynamic = true;\n            }\n          }\n          // 普通指令会在AST树上添加directives属性\n          addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]);\n          if (name === 'model') {\n            checkForAliasModel(el, value);\n          }\n        }\n      } else {\n        // literal attribute\n        {\n          var res = parseText(value, delimiters);\n          if (res) {\n            warn$2(\n              name + \"=\\\"\" + value + \"\\\": \" +\n              'Interpolation inside attributes has been removed. ' +\n              'Use v-bind or the colon shorthand instead. For example, ' +\n              'instead of <div id=\"{{ val }}\">, use <div :id=\"val\">.',\n              list[i]\n            );\n          }\n        }\n        addAttr(el, name, JSON.stringify(value), list[i]);\n        // #6887 firefox doesn't update muted state if set via attribute\n        // even immediately after element creation\n        if (!el.component &&\n          name === 'muted' &&\n          platformMustUseProp(el.tag, el.attrsMap.type, name)) {\n          addProp(el, name, 'true', list[i]);\n        }\n      }\n    }\n  }\n\n  function checkInFor(el) {\n    var parent = el;\n    while (parent) {\n      if (parent.for !== undefined) {\n        return true\n      }\n      parent = parent.parent;\n    }\n    return false\n  }\n\n  function parseModifiers(name) { // 拿到事件相关的修饰符\n    var match = name.match(modifierRE); // ['.stop']\n    if (match) {\n      var ret = {};\n      match.forEach(function (m) {\n        ret[m.slice(1)] = true;\n      });\n      return ret\n    }\n  }\n\n  function makeAttrsMap(attrs) {\n    var map = {};\n    for (var i = 0, l = attrs.length; i < l; i++) {\n      if (\n        map[attrs[i].name] && !isIE && !isEdge\n      ) {\n        warn$2('duplicate attribute: ' + attrs[i].name, attrs[i]);\n      }\n      map[attrs[i].name] = attrs[i].value;\n    }\n    return map\n  }\n\n  // for script (e.g. type=\"x/template\") or style, do not decode content\n  function isTextTag(el) {\n    return el.tag === 'script' || el.tag === 'style'\n  }\n\n  function isForbiddenTag(el) {\n    return (\n      el.tag === 'style' ||\n      (el.tag === 'script' && (\n        !el.attrsMap.type ||\n        el.attrsMap.type === 'text/javascript'\n      ))\n    )\n  }\n\n  var ieNSBug = /^xmlns:NS\\d+/;\n  var ieNSPrefix = /^NS\\d+:/;\n\n  /* istanbul ignore next */\n  function guardIESVGBug(attrs) {\n    var res = [];\n    for (var i = 0; i < attrs.length; i++) {\n      var attr = attrs[i];\n      if (!ieNSBug.test(attr.name)) {\n        attr.name = attr.name.replace(ieNSPrefix, '');\n        res.push(attr);\n      }\n    }\n    return res\n  }\n\n  function checkForAliasModel(el, value) {\n    var _el = el;\n    while (_el) {\n      if (_el.for && _el.alias === value) {\n        warn$2(\n          \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\">: \" +\n          \"You are binding v-model directly to a v-for iteration alias. \" +\n          \"This will not be able to modify the v-for source array because \" +\n          \"writing to the alias is like modifying a function local variable. \" +\n          \"Consider using an array of objects and use v-model on an object property instead.\",\n          el.rawAttrsMap['v-model']\n        );\n      }\n      _el = _el.parent;\n    }\n  }\n\n  /*  */\n\n  function preTransformNode(el, options) {\n    if (el.tag === 'input') {\n      var map = el.attrsMap;\n      if (!map['v-model']) {\n        return\n      }\n\n      var typeBinding;\n      if (map[':type'] || map['v-bind:type']) {\n        typeBinding = getBindingAttr(el, 'type');\n      }\n      if (!map.type && !typeBinding && map['v-bind']) {\n        typeBinding = \"(\" + (map['v-bind']) + \").type\";\n      }\n\n      if (typeBinding) {\n        var ifCondition = getAndRemoveAttr(el, 'v-if', true);\n        var ifConditionExtra = ifCondition ? (\"&&(\" + ifCondition + \")\") : \"\";\n        var hasElse = getAndRemoveAttr(el, 'v-else', true) != null;\n        var elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true);\n        // 1. checkbox\n        var branch0 = cloneASTElement(el);\n        // process for on the main node\n        processFor(branch0);\n        addRawAttr(branch0, 'type', 'checkbox');\n        processElement(branch0, options);\n        branch0.processed = true; // prevent it from double-processed\n        branch0.if = \"(\" + typeBinding + \")==='checkbox'\" + ifConditionExtra;\n        addIfCondition(branch0, {\n          exp: branch0.if,\n          block: branch0\n        });\n        // 2. add radio else-if condition\n        var branch1 = cloneASTElement(el);\n        getAndRemoveAttr(branch1, 'v-for', true);\n        addRawAttr(branch1, 'type', 'radio');\n        processElement(branch1, options);\n        addIfCondition(branch0, {\n          exp: \"(\" + typeBinding + \")==='radio'\" + ifConditionExtra,\n          block: branch1\n        });\n        // 3. other\n        var branch2 = cloneASTElement(el);\n        getAndRemoveAttr(branch2, 'v-for', true);\n        addRawAttr(branch2, ':type', typeBinding);\n        processElement(branch2, options);\n        addIfCondition(branch0, {\n          exp: ifCondition,\n          block: branch2\n        });\n\n        if (hasElse) {\n          branch0.else = true;\n        } else if (elseIfCondition) {\n          branch0.elseif = elseIfCondition;\n        }\n\n        return branch0\n      }\n    }\n  }\n\n  function cloneASTElement(el) {\n    return createASTElement(el.tag, el.attrsList.slice(), el.parent)\n  }\n\n  var model$1 = {\n    preTransformNode: preTransformNode\n  };\n\n  var modules$1 = [\n    klass$1,\n    style$1,\n    model$1\n  ];\n\n  /*  */\n\n  function text(el, dir) {\n    if (dir.value) {\n      addProp(el, 'textContent', (\"_s(\" + (dir.value) + \")\"), dir);\n    }\n  }\n\n  /*  */\n\n  function html(el, dir) {\n    if (dir.value) {\n      addProp(el, 'innerHTML', (\"_s(\" + (dir.value) + \")\"), dir);\n    }\n  }\n\n  var directives$1 = {\n    model: model,\n    text: text,\n    html: html\n  };\n\n  /*  */\n\n  var baseOptions = {\n    expectHTML: true,\n    modules: modules$1,\n    directives: directives$1,\n    isPreTag: isPreTag,\n    isUnaryTag: isUnaryTag,\n    mustUseProp: mustUseProp,\n    canBeLeftOpenTag: canBeLeftOpenTag,\n    isReservedTag: isReservedTag,\n    getTagNamespace: getTagNamespace,\n    staticKeys: genStaticKeys(modules$1)\n  };\n\n  /*  */\n\n  var isStaticKey;\n  var isPlatformReservedTag;\n\n  var genStaticKeysCached = cached(genStaticKeys$1);\n\n  /**\n   * Goal of the optimizer: walk the generated template AST tree\n   * and detect sub-trees that are purely static, i.e. parts of\n   * the DOM that never needs to change.\n   *\n   * Once we detect these sub-trees, we can:\n   *\n   * 1. Hoist them into constants, so that we no longer need to\n   *    create fresh nodes for them on each re-render;\n   * 2. Completely skip them in the patching process.\n   */\n  function optimize(root, options) {\n    if (!root) {\n      return\n    }\n    isStaticKey = genStaticKeysCached(options.staticKeys || '');\n    isPlatformReservedTag = options.isReservedTag || no;\n    // first pass: mark all non-static nodes.\n    markStatic$1(root);\n    // second pass: mark static roots.\n    markStaticRoots(root, false);\n  }\n\n  function genStaticKeys$1(keys) {\n    return makeMap(\n      'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +\n      (keys ? ',' + keys : '')\n    )\n  }\n\n  function markStatic$1(node) {\n    node.static = isStatic(node);\n    if (node.type === 1) {\n      // do not make component slot content static. this avoids\n      // 1. components not able to mutate slot nodes\n      // 2. static slot content fails for hot-reloading\n      if (\n        !isPlatformReservedTag(node.tag) &&\n        node.tag !== 'slot' &&\n        node.attrsMap['inline-template'] == null\n      ) {\n        return\n      }\n      for (var i = 0, l = node.children.length; i < l; i++) {\n        var child = node.children[i];\n        markStatic$1(child);\n        if (!child.static) {\n          node.static = false;\n        }\n      }\n      if (node.ifConditions) {\n        for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {\n          var block = node.ifConditions[i$1].block;\n          markStatic$1(block);\n          if (!block.static) {\n            node.static = false;\n          }\n        }\n      }\n    }\n  }\n\n  function markStaticRoots(node, isInFor) {\n    if (node.type === 1) {\n      if (node.static || node.once) {\n        node.staticInFor = isInFor;\n      }\n      // For a node to qualify as a static root, it should have children that\n      // are not just static text. Otherwise the cost of hoisting out will\n      // outweigh the benefits and it's better off to just always render it fresh.\n      if (node.static && node.children.length && !(\n          node.children.length === 1 &&\n          node.children[0].type === 3\n        )) {\n        node.staticRoot = true;\n        return\n      } else {\n        node.staticRoot = false;\n      }\n      if (node.children) {\n        for (var i = 0, l = node.children.length; i < l; i++) {\n          markStaticRoots(node.children[i], isInFor || !!node.for);\n        }\n      }\n      if (node.ifConditions) {\n        for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {\n          markStaticRoots(node.ifConditions[i$1].block, isInFor);\n        }\n      }\n    }\n  }\n\n  function isStatic(node) {\n    if (node.type === 2) { // expression\n      return false\n    }\n    if (node.type === 3) { // text\n      return true\n    }\n    return !!(node.pre || (\n      !node.hasBindings && // no dynamic bindings\n      !node.if && !node.for && // not v-if or v-for or v-else\n      !isBuiltInTag(node.tag) && // not a built-in\n      isPlatformReservedTag(node.tag) && // not a component\n      !isDirectChildOfTemplateFor(node) &&\n      Object.keys(node).every(isStaticKey)\n    ))\n  }\n\n  function isDirectChildOfTemplateFor(node) {\n    while (node.parent) {\n      node = node.parent;\n      if (node.tag !== 'template') {\n        return false\n      }\n      if (node.for) {\n        return true\n      }\n    }\n    return false\n  }\n\n  /*  */\n\n  var fnExpRE = /^([\\w$_]+|\\([^)]*?\\))\\s*=>|^function\\s*\\(/; // () => {} or function() {}\n  var fnInvokeRE = /\\([^)]*?\\);*$/;\n  var simplePathRE = /^[A-Za-z_$][\\w$]*(?:\\.[A-Za-z_$][\\w$]*|\\['[^']*?']|\\[\"[^\"]*?\"]|\\[\\d+]|\\[[A-Za-z_$][\\w$]*])*$/; // 普通函数名\n\n  // KeyboardEvent.keyCode aliases\n  var keyCodes = {\n    esc: 27,\n    tab: 9,\n    enter: 13,\n    space: 32,\n    up: 38,\n    left: 37,\n    right: 39,\n    down: 40,\n    'delete': [8, 46]\n  };\n\n  // KeyboardEvent.key aliases\n  var keyNames = {\n    // #7880: IE11 and Edge use `Esc` for Escape key name.\n    esc: ['Esc', 'Escape'],\n    tab: 'Tab',\n    enter: 'Enter',\n    // #9112: IE11 uses `Spacebar` for Space key name.\n    space: [' ', 'Spacebar'],\n    // #7806: IE11 uses key names without `Arrow` prefix for arrow keys.\n    up: ['Up', 'ArrowUp'],\n    left: ['Left', 'ArrowLeft'],\n    right: ['Right', 'ArrowRight'],\n    down: ['Down', 'ArrowDown'],\n    // #9112: IE11 uses `Del` for Delete key name.\n    'delete': ['Backspace', 'Delete', 'Del']\n  };\n\n  // #4868: modifiers that prevent the execution of the listener\n  // need to explicitly return null so that we can determine whether to remove\n  // the listener for .once\n  var genGuard = function (condition) {\n    return (\"if(\" + condition + \")return null;\");\n  };\n\n  var modifierCode = {\n    stop: '$event.stopPropagation();',\n    prevent: '$event.preventDefault();',\n    self: genGuard(\"$event.target !== $event.currentTarget\"),\n    ctrl: genGuard(\"!$event.ctrlKey\"),\n    shift: genGuard(\"!$event.shiftKey\"),\n    alt: genGuard(\"!$event.altKey\"),\n    meta: genGuard(\"!$event.metaKey\"),\n    left: genGuard(\"'button' in $event && $event.button !== 0\"),\n    middle: genGuard(\"'button' in $event && $event.button !== 1\"),\n    right: genGuard(\"'button' in $event && $event.button !== 2\")\n  };\n\n  function genHandlers(\n    events,\n    isNative\n  ) {\n    var prefix = isNative ? 'nativeOn:' : 'on:';\n    var staticHandlers = \"\";\n    var dynamicHandlers = \"\";\n    // 遍历ast树解析好的event对象\n    for (var name in events) {\n      //genHandler本质上是将事件对象转换成可拼接的字符串\n      var handlerCode = genHandler(events[name]);\n      if (events[name] && events[name].dynamic) {\n        dynamicHandlers += name + \",\" + handlerCode + \",\";\n      } else {\n        staticHandlers += \"\\\"\" + name + \"\\\":\" + handlerCode + \",\";\n      }\n    }\n    staticHandlers = \"{\" + (staticHandlers.slice(0, -1)) + \"}\";\n    if (dynamicHandlers) {\n      return prefix + \"_d(\" + staticHandlers + \",[\" + (dynamicHandlers.slice(0, -1)) + \"])\"\n    } else {\n      return prefix + staticHandlers\n    }\n  }\n\n  function genHandler(handler) {\n    if (!handler) {\n      return 'function(){}'\n    }\n    // 事件绑定可以多个，多个在解析ast树时会以数组的形式存在，如果有多个则会递归调用getHandler方法返回数组。\n    if (Array.isArray(handler)) {\n      return (\"[\" + (handler.map(function (handler) {\n        return genHandler(handler);\n      }).join(',')) + \"]\")\n    }\n    // value： doThis 可以有三种方式\n    var isMethodPath = simplePathRE.test(handler.value); // doThis\n    var isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}\n    var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)\n\n    // 没有任何修饰符\n    if (!handler.modifiers) {\n      // 符合函数定义规范，则直接返回调用函数名 doThis\n      if (isMethodPath || isFunctionExpression) {\n        return handler.value\n      }\n      return (\"function($event){\" + (isFunctionInvocation ? (\"return \" + (handler.value)) : handler.value) + \"}\") // inline statement\n    } else {\n      var code = '';\n      var genModifierCode = '';\n      var keys = [];\n      for (var key in handler.modifiers) {\n        if (modifierCode[key]) {\n          genModifierCode += modifierCode[key];\n          // left/right\n          if (keyCodes[key]) {\n            keys.push(key);\n          }\n        } else if (key === 'exact') {\n          var modifiers = (handler.modifiers);\n          genModifierCode += genGuard(\n            ['ctrl', 'shift', 'alt', 'meta']\n            .filter(function (keyModifier) {\n              return !modifiers[keyModifier];\n            })\n            .map(function (keyModifier) {\n              return (\"$event.\" + keyModifier + \"Key\");\n            })\n            .join('||')\n          );\n        } else {\n          keys.push(key);\n        }\n      }\n      if (keys.length) {\n        code += genKeyFilter(keys);\n      }\n      // Make sure modifiers like prevent and stop get executed after key filtering\n      if (genModifierCode) {\n        code += genModifierCode;\n      }\n      var handlerCode = isMethodPath ?\n        (\"return \" + (handler.value) + \"($event)\") :\n        isFunctionExpression ?\n        (\"return (\" + (handler.value) + \")($event)\") :\n        isFunctionInvocation ?\n        (\"return \" + (handler.value)) :\n        handler.value;\n      return (\"function($event){\" + code + handlerCode + \"}\")\n    }\n  }\n\n  function genKeyFilter(keys) {\n    return (\n      // make sure the key filters only apply to KeyboardEvents\n      // #9441: can't use 'keyCode' in $event because Chrome autofill fires fake\n      // key events that do not have keyCode property...\n      \"if(!$event.type.indexOf('key')&&\" +\n      (keys.map(genFilterCode).join('&&')) + \")return null;\"\n    )\n  }\n\n  function genFilterCode(key) {\n    var keyVal = parseInt(key, 10);\n    if (keyVal) {\n      return (\"$event.keyCode!==\" + keyVal)\n    }\n    var keyCode = keyCodes[key];\n    var keyName = keyNames[key];\n    return (\n      \"_k($event.keyCode,\" +\n      (JSON.stringify(key)) + \",\" +\n      (JSON.stringify(keyCode)) + \",\" +\n      \"$event.key,\" +\n      \"\" + (JSON.stringify(keyName)) +\n      \")\"\n    )\n  }\n\n  /*  */\n\n  function on(el, dir) {\n    if (dir.modifiers) {\n      warn(\"v-on without argument does not support modifiers.\");\n    }\n    el.wrapListeners = function (code) {\n      return (\"_g(\" + code + \",\" + (dir.value) + \")\");\n    };\n  }\n\n  /*  */\n\n  function bind$1(el, dir) {\n    el.wrapData = function (code) {\n      return (\"_b(\" + code + \",'\" + (el.tag) + \"',\" + (dir.value) + \",\" + (dir.modifiers && dir.modifiers.prop ? 'true' : 'false') + (dir.modifiers && dir.modifiers.sync ? ',true' : '') + \")\")\n    };\n  }\n\n  /*  */\n\n  var baseDirectives = {\n    on: on,\n    bind: bind$1,\n    cloak: noop\n  };\n\n  /*  */\n\n\n  var CodegenState = function CodegenState(options) {\n    this.options = options;\n    this.warn = options.warn || baseWarn;\n    this.transforms = pluckModuleFunction(options.modules, 'transformCode');\n    this.dataGenFns = pluckModuleFunction(options.modules, 'genData');\n    this.directives = extend(extend({}, baseDirectives), options.directives);\n    var isReservedTag = options.isReservedTag || no;\n    this.maybeComponent = function (el) {\n      return !!el.component || !isReservedTag(el.tag);\n    };\n    this.onceId = 0;\n    this.staticRenderFns = [];\n    this.pre = false;\n  };\n\n\n\n  function generate(\n    ast,\n    options\n  ) {\n    var state = new CodegenState(options);\n    var code = ast ? genElement(ast, state) : '_c(\"div\")';\n    return {\n      render: (\"with(this){return \" + code + \"}\"),\n      staticRenderFns: state.staticRenderFns\n    }\n  }\n\n  function genElement(el, state) {\n    if (el.parent) {\n      el.pre = el.pre || el.parent.pre;\n    }\n\n    if (el.staticRoot && !el.staticProcessed) {\n      return genStatic(el, state)\n    } else if (el.once && !el.onceProcessed) {\n      return genOnce(el, state)\n    } else if (el.for && !el.forProcessed) {\n      return genFor(el, state)\n    } else if (el.if && !el.ifProcessed) {\n      return genIf(el, state)\n    } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {\n      return genChildren(el, state) || 'void 0'\n    } else if (el.tag === 'slot') {\n      return genSlot(el, state)\n    } else {\n      // component or element\n      var code;\n      if (el.component) {\n        code = genComponent(el.component, el, state);\n      } else {\n        var data;\n        if (!el.plain || (el.pre && state.maybeComponent(el))) {\n          data = genData$2(el, state);\n        }\n\n        var children = el.inlineTemplate ? null : genChildren(el, state, true);\n        code = \"_c('\" + (el.tag) + \"'\" + (data ? (\",\" + data) : '') + (children ? (\",\" + children) : '') + \")\";\n      }\n      // module transforms\n      for (var i = 0; i < state.transforms.length; i++) {\n        code = state.transforms[i](el, code);\n      }\n      return code\n    }\n  }\n\n  // hoist static sub-trees out\n  function genStatic(el, state) {\n    el.staticProcessed = true;\n    // Some elements (templates) need to behave differently inside of a v-pre\n    // node.  All pre nodes are static roots, so we can use this as a location to\n    // wrap a state change and reset it upon exiting the pre node.\n    var originalPreState = state.pre;\n    if (el.pre) {\n      state.pre = el.pre;\n    }\n    state.staticRenderFns.push((\"with(this){return \" + (genElement(el, state)) + \"}\"));\n    state.pre = originalPreState;\n    return (\"_m(\" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + \")\")\n  }\n\n  // v-once\n  function genOnce(el, state) {\n    el.onceProcessed = true;\n    if (el.if && !el.ifProcessed) {\n      return genIf(el, state)\n    } else if (el.staticInFor) {\n      var key = '';\n      var parent = el.parent;\n      while (parent) {\n        if (parent.for) {\n          key = parent.key;\n          break\n        }\n        parent = parent.parent;\n      }\n      if (!key) {\n        state.warn(\n          \"v-once can only be used inside v-for that is keyed. \",\n          el.rawAttrsMap['v-once']\n        );\n        return genElement(el, state)\n      }\n      return (\"_o(\" + (genElement(el, state)) + \",\" + (state.onceId++) + \",\" + key + \")\")\n    } else {\n      return genStatic(el, state)\n    }\n  }\n\n  function genIf(\n    el,\n    state,\n    altGen,\n    altEmpty\n  ) {\n    el.ifProcessed = true; // avoid recursion\n    return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)\n  }\n\n  function genIfConditions(\n    conditions,\n    state,\n    altGen,\n    altEmpty\n  ) {\n    if (!conditions.length) {\n      return altEmpty || '_e()'\n    }\n\n    var condition = conditions.shift();\n    if (condition.exp) {\n      return (\"(\" + (condition.exp) + \")?\" + (genTernaryExp(condition.block)) + \":\" + (genIfConditions(conditions, state, altGen, altEmpty)))\n    } else {\n      return (\"\" + (genTernaryExp(condition.block)))\n    }\n\n    // v-if with v-once should generate code like (a)?_m(0):_m(1)\n    function genTernaryExp(el) {\n      return altGen ?\n        altGen(el, state) :\n        el.once ?\n        genOnce(el, state) :\n        genElement(el, state)\n    }\n  }\n\n  function genFor(\n    el,\n    state,\n    altGen,\n    altHelper\n  ) {\n    var exp = el.for;\n    var alias = el.alias;\n    var iterator1 = el.iterator1 ? (\",\" + (el.iterator1)) : '';\n    var iterator2 = el.iterator2 ? (\",\" + (el.iterator2)) : '';\n\n    if (state.maybeComponent(el) &&\n      el.tag !== 'slot' &&\n      el.tag !== 'template' &&\n      !el.key\n    ) {\n      state.warn(\n        \"<\" + (el.tag) + \" v-for=\\\"\" + alias + \" in \" + exp + \"\\\">: component lists rendered with \" +\n        \"v-for should have explicit keys. \" +\n        \"See https://vuejs.org/guide/list.html#key for more info.\",\n        el.rawAttrsMap['v-for'],\n        true /* tip */\n      );\n    }\n\n    el.forProcessed = true; // avoid recursion\n    return (altHelper || '_l') + \"((\" + exp + \"),\" +\n      \"function(\" + alias + iterator1 + iterator2 + \"){\" +\n      \"return \" + ((altGen || genElement)(el, state)) +\n      '})'\n  }\n\n  function genData$2(el, state) {\n    var data = '{';\n\n    // directives first.\n    // directives may mutate the el's other properties before they are generated.\n    var dirs = genDirectives(el, state);\n    // \"directives:[{name:\"model\",rawName:\"v-model\",value:(value1),expression:\"value1\"}]\"\n    if (dirs) {\n      data += dirs + ',';\n    }\n\n    // key\n    if (el.key) {\n      data += \"key:\" + (el.key) + \",\";\n    }\n    // ref\n    if (el.ref) {\n      data += \"ref:\" + (el.ref) + \",\";\n    }\n    if (el.refInFor) {\n      data += \"refInFor:true,\";\n    }\n    // pre\n    if (el.pre) {\n      data += \"pre:true,\";\n    }\n    // record original tag name for components using \"is\" attribute\n    // 针对动态组件的处理， 以{ tag: XXX }的形式存在\n    if (el.component) {\n      data += \"tag:\\\"\" + (el.tag) + \"\\\",\";\n    }\n    // module data generation functions\n    for (var i = 0; i < state.dataGenFns.length; i++) {\n      data += state.dataGenFns[i](el);\n    }\n    // attributes\n    if (el.attrs) {\n      data += \"attrs:\" + (genProps(el.attrs)) + \",\";\n    }\n    // DOM props\n    if (el.props) {\n      data += \"domProps:\" + (genProps(el.props)) + \",\";\n    }\n    // event handlers\n    if (el.events) {\n      data += (genHandlers(el.events, false)) + \",\";\n    }\n    if (el.nativeEvents) {\n      data += (genHandlers(el.nativeEvents, true)) + \",\";\n    }\n    // slot target\n    // only for non-scoped slots\n    if (el.slotTarget && !el.slotScope) {\n      data += \"slot:\" + (el.slotTarget) + \",\";\n    }\n    // scoped slots\n    if (el.scopedSlots) {\n      data += (genScopedSlots(el, el.scopedSlots, state)) + \",\";\n    }\n    // component v-model\n    // v-model组件的render函数处理\n    if (el.model) {\n      data += \"model:{value:\" + (el.model.value) + \",callback:\" + (el.model.callback) + \",expression:\" + (el.model.expression) + \"},\";\n    }\n    // inline-template\n    if (el.inlineTemplate) {\n      var inlineTemplate = genInlineTemplate(el, state);\n      if (inlineTemplate) {\n        data += inlineTemplate + \",\";\n      }\n    }\n    data = data.replace(/,$/, '') + '}';\n    // v-bind dynamic argument wrap\n    // v-bind with dynamic arguments must be applied using the same v-bind object\n    // merge helper so that class/style/mustUseProp attrs are handled correctly.\n    if (el.dynamicAttrs) {\n      data = \"_b(\" + data + \",\\\"\" + (el.tag) + \"\\\",\" + (genProps(el.dynamicAttrs)) + \")\";\n    }\n    // v-bind data wrap\n    if (el.wrapData) {\n      data = el.wrapData(data);\n    }\n    // v-on data wrap\n    if (el.wrapListeners) {\n      data = el.wrapListeners(data);\n    }\n    return data\n  }\n  // directives render字符串的生成\n  function genDirectives(el, state) {\n    // 拿到指令对象\n    var dirs = el.directives;\n    if (!dirs) {\n      return\n    }\n    var res = 'directives:[';\n    var hasRuntime = false;\n    var i, l, dir, needRuntime;\n    for (i = 0, l = dirs.length; i < l; i++) {\n      dir = dirs[i];\n      needRuntime = true;\n      var gen = state.directives[dir.name];\n      if (gen) {\n        // compile-time directive that manipulates AST.\n        // returns true if it also needs a runtime counterpart.\n        needRuntime = !!gen(el, dir, state.warn);\n      }\n      if (needRuntime) {\n        hasRuntime = true;\n        res += \"{name:\\\"\" + (dir.name) + \"\\\",rawName:\\\"\" + (dir.rawName) + \"\\\"\" + (dir.value ? (\",value:(\" + (dir.value) + \"),expression:\" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (\",arg:\" + (dir.isDynamicArg ? dir.arg : (\"\\\"\" + (dir.arg) + \"\\\"\"))) : '') + (dir.modifiers ? (\",modifiers:\" + (JSON.stringify(dir.modifiers))) : '') + \"},\";\n      }\n    }\n    if (hasRuntime) {\n      return res.slice(0, -1) + ']'\n    }\n  }\n\n  function genInlineTemplate(el, state) {\n    var ast = el.children[0];\n    if (el.children.length !== 1 || ast.type !== 1) {\n      state.warn(\n        'Inline-template components must have exactly one child element.', {\n          start: el.start\n        }\n      );\n    }\n    if (ast && ast.type === 1) {\n      var inlineRenderFns = generate(ast, state.options);\n      return (\"inlineTemplate:{render:function(){\" + (inlineRenderFns.render) + \"},staticRenderFns:[\" + (inlineRenderFns.staticRenderFns.map(function (code) {\n        return (\"function(){\" + code + \"}\");\n      }).join(',')) + \"]}\")\n    }\n  }\n\n  function genScopedSlots(\n    el,\n    slots,\n    state\n  ) {\n    // by default scoped slots are considered \"stable\", this allows child\n    // components with only scoped slots to skip forced updates from parent.\n    // but in some cases we have to bail-out of this optimization\n    // for example if the slot contains dynamic names, has v-if or v-for on them...\n    var needsForceUpdate = el.for || Object.keys(slots).some(function (key) {\n      var slot = slots[key];\n      return (\n        slot.slotTargetDynamic ||\n        slot.if ||\n        slot.for ||\n        containsSlotChild(slot) // is passing down slot from parent which may be dynamic\n      )\n    });\n\n    // #9534: if a component with scoped slots is inside a conditional branch,\n    // it's possible for the same component to be reused but with different\n    // compiled slot content. To avoid that, we generate a unique key based on\n    // the generated code of all the slot contents.\n    var needsKey = !!el.if;\n\n    // OR when it is inside another scoped slot or v-for (the reactivity may be\n    // disconnected due to the intermediate scope variable)\n    // #9438, #9506\n    // TODO: this can be further optimized by properly analyzing in-scope bindings\n    // and skip force updating ones that do not actually use scope variables.\n    if (!needsForceUpdate) {\n      var parent = el.parent;\n      while (parent) {\n        if (\n          (parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||\n          parent.for\n        ) {\n          needsForceUpdate = true;\n          break\n        }\n        if (parent.if) {\n          needsKey = true;\n        }\n        parent = parent.parent;\n      }\n    }\n\n    var generatedSlots = Object.keys(slots)\n      .map(function (key) {\n        return genScopedSlot(slots[key], state);\n      })\n      .join(',');\n\n    return (\"scopedSlots:_u([\" + generatedSlots + \"]\" + (needsForceUpdate ? \",null,true\" : \"\") + (!needsForceUpdate && needsKey ? (\",null,false,\" + (hash(generatedSlots))) : \"\") + \")\")\n  }\n\n  function hash(str) {\n    var hash = 5381;\n    var i = str.length;\n    while (i) {\n      hash = (hash * 33) ^ str.charCodeAt(--i);\n    }\n    return hash >>> 0\n  }\n\n  function containsSlotChild(el) {\n    if (el.type === 1) {\n      if (el.tag === 'slot') {\n        return true\n      }\n      return el.children.some(containsSlotChild)\n    }\n    return false\n  }\n\n  function genScopedSlot(\n    el,\n    state\n  ) {\n    var isLegacySyntax = el.attrsMap['slot-scope'];\n    if (el.if && !el.ifProcessed && !isLegacySyntax) {\n      return genIf(el, state, genScopedSlot, \"null\")\n    }\n    if (el.for && !el.forProcessed) {\n      return genFor(el, state, genScopedSlot)\n    }\n    var slotScope = el.slotScope === emptySlotScopeToken ?\n      \"\" :\n      String(el.slotScope);\n    var fn = \"function(\" + slotScope + \"){\" +\n      \"return \" + (el.tag === 'template' ?\n        el.if && isLegacySyntax ?\n        (\"(\" + (el.if) + \")?\" + (genChildren(el, state) || 'undefined') + \":undefined\") :\n        genChildren(el, state) || 'undefined' :\n        genElement(el, state)) + \"}\";\n    // reverse proxy v-slot without scope on this.$slots\n    var reverseProxy = slotScope ? \"\" : \",proxy:true\";\n    return (\"{key:\" + (el.slotTarget || \"\\\"default\\\"\") + \",fn:\" + fn + reverseProxy + \"}\")\n  }\n\n  function genChildren(\n    el,\n    state,\n    checkSkip,\n    altGenElement,\n    altGenNode\n  ) {\n    var children = el.children;\n    if (children.length) {\n      var el$1 = children[0];\n      // optimize single v-for\n      if (children.length === 1 &&\n        el$1.for &&\n        el$1.tag !== 'template' &&\n        el$1.tag !== 'slot'\n      ) {\n        var normalizationType = checkSkip ?\n          state.maybeComponent(el$1) ? \",1\" : \",0\" :\n          \"\";\n        return (\"\" + ((altGenElement || genElement)(el$1, state)) + normalizationType)\n      }\n      var normalizationType$1 = checkSkip ?\n        getNormalizationType(children, state.maybeComponent) :\n        0;\n      var gen = altGenNode || genNode;\n      return (\"[\" + (children.map(function (c) {\n        return gen(c, state);\n      }).join(',')) + \"]\" + (normalizationType$1 ? (\",\" + normalizationType$1) : ''))\n    }\n  }\n\n  // determine the normalization needed for the children array.\n  // 0: no normalization needed\n  // 1: simple normalization needed (possible 1-level deep nested array)\n  // 2: full normalization needed\n  function getNormalizationType(\n    children,\n    maybeComponent\n  ) {\n    var res = 0;\n    for (var i = 0; i < children.length; i++) {\n      var el = children[i];\n      if (el.type !== 1) {\n        continue\n      }\n      if (needsNormalization(el) ||\n        (el.ifConditions && el.ifConditions.some(function (c) {\n          return needsNormalization(c.block);\n        }))) {\n        res = 2;\n        break\n      }\n      if (maybeComponent(el) ||\n        (el.ifConditions && el.ifConditions.some(function (c) {\n          return maybeComponent(c.block);\n        }))) {\n        res = 1;\n      }\n    }\n    return res\n  }\n\n  function needsNormalization(el) {\n    return el.for !== undefined || el.tag === 'template' || el.tag === 'slot'\n  }\n\n  function genNode(node, state) {\n    if (node.type === 1) {\n      return genElement(node, state)\n    } else if (node.type === 3 && node.isComment) {\n      return genComment(node)\n    } else {\n      return genText(node)\n    }\n  }\n\n  function genText(text) {\n    return (\"_v(\" + (text.type === 2 ?\n      text.expression // no need for () because already wrapped in _s()\n      :\n      transformSpecialNewlines(JSON.stringify(text.text))) + \")\")\n  }\n\n  function genComment(comment) {\n    return (\"_e(\" + (JSON.stringify(comment.text)) + \")\")\n  }\n\n  function genSlot(el, state) {\n    var slotName = el.slotName || '\"default\"';\n    // 如果子组件的插槽还有子元素，则会递归调执行子元素的创建过程\n    var children = genChildren(el, state);\n    var res = \"_t(\" + slotName + (children ? (\",\" + children) : '');\n    // 拿到<slot :name=\"name\"></slot>中的属性name\n    var attrs = el.attrs || el.dynamicAttrs ?\n      genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(function (attr) {\n        return ({\n          // slot props are camelized\n          name: camelize(attr.name),\n          value: attr.value,\n          dynamic: attr.dynamic\n        });\n      })) :\n      null;\n    var bind###1 = el.attrsMap['v-bind'];\n    if ((attrs || bind###1) && !children) {\n      res += \",null\";\n    }\n    if (attrs) {\n      res += \",\" + attrs;\n    }\n    if (bind###1) {\n      res += (attrs ? '' : ',null') + \",\" + bind###1;\n    }\n    return res + ')'\n  }\n\n  // componentName is el.component, take it as argument to shun flow's pessimistic refinement\n  // 针对动态组件的处理\n  function genComponent(\n    componentName,\n    el,\n    state\n  ) {\n    // 拥有inlineTemplate属性时，children为null\n    var children = el.inlineTemplate ? null : genChildren(el, state, true);\n    return (\"_c(\" + componentName + \",\" + (genData$2(el, state)) + (children ? (\",\" + children) : '') + \")\")\n  }\n\n  function genProps(props) {\n    var staticProps = \"\";\n    var dynamicProps = \"\";\n    for (var i = 0; i < props.length; i++) {\n      var prop = props[i];\n      var value = transformSpecialNewlines(prop.value);\n      if (prop.dynamic) {\n        dynamicProps += (prop.name) + \",\" + value + \",\";\n      } else {\n        staticProps += \"\\\"\" + (prop.name) + \"\\\":\" + value + \",\";\n      }\n    }\n    staticProps = \"{\" + (staticProps.slice(0, -1)) + \"}\";\n    if (dynamicProps) {\n      return (\"_d(\" + staticProps + \",[\" + (dynamicProps.slice(0, -1)) + \"])\")\n    } else {\n      return staticProps\n    }\n  }\n\n  // #3895, #4268\n  function transformSpecialNewlines(text) {\n    return text\n      .replace(/\\u2028/g, '\\\\u2028')\n      .replace(/\\u2029/g, '\\\\u2029')\n  }\n\n  /*  */\n\n\n\n  // these keywords should not appear inside expressions, but operators like\n  // typeof, instanceof and in are allowed\n  var prohibitedKeywordRE = new RegExp('\\\\b' + (\n    'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +\n    'super,throw,while,yield,delete,export,import,return,switch,default,' +\n    'extends,finally,continue,debugger,function,arguments'\n  ).split(',').join('\\\\b|\\\\b') + '\\\\b');\n\n  // these unary operators should not be used as property/method names\n  var unaryOperatorsRE = new RegExp('\\\\b' + (\n    'delete,typeof,void'\n  ).split(',').join('\\\\s*\\\\([^\\\\)]*\\\\)|\\\\b') + '\\\\s*\\\\([^\\\\)]*\\\\)');\n\n  // strip strings in expressions\n  var stripStringRE = /'(?:[^'\\\\]|\\\\.)*'|\"(?:[^\"\\\\]|\\\\.)*\"|`(?:[^`\\\\]|\\\\.)*\\$\\{|\\}(?:[^`\\\\]|\\\\.)*`|`(?:[^`\\\\]|\\\\.)*`/g;\n\n  // detect problematic expressions in a template\n  function detectErrors(ast, warn) {\n    if (ast) {\n      checkNode(ast, warn);\n    }\n  }\n\n  function checkNode(node, warn) {\n    if (node.type === 1) {\n      for (var name in node.attrsMap) {\n        if (dirRE.test(name)) {\n          var value = node.attrsMap[name];\n          if (value) {\n            var range = node.rawAttrsMap[name];\n            if (name === 'v-for') {\n              checkFor(node, (\"v-for=\\\"\" + value + \"\\\"\"), warn, range);\n            } else if (onRE.test(name)) {\n              checkEvent(value, (name + \"=\\\"\" + value + \"\\\"\"), warn, range);\n            } else {\n              checkExpression(value, (name + \"=\\\"\" + value + \"\\\"\"), warn, range);\n            }\n          }\n        }\n      }\n      if (node.children) {\n        for (var i = 0; i < node.children.length; i++) {\n          checkNode(node.children[i], warn);\n        }\n      }\n    } else if (node.type === 2) {\n      checkExpression(node.expression, node.text, warn, node);\n    }\n  }\n\n  function checkEvent(exp, text, warn, range) {\n    var stipped = exp.replace(stripStringRE, '');\n    var keywordMatch = stipped.match(unaryOperatorsRE);\n    if (keywordMatch && stipped.charAt(keywordMatch.index - 1) !== '$') {\n      warn(\n        \"avoid using JavaScript unary operator as property name: \" +\n        \"\\\"\" + (keywordMatch[0]) + \"\\\" in expression \" + (text.trim()),\n        range\n      );\n    }\n    checkExpression(exp, text, warn, range);\n  }\n\n  function checkFor(node, text, warn, range) {\n    checkExpression(node.for || '', text, warn, range);\n    checkIdentifier(node.alias, 'v-for alias', text, warn, range);\n    checkIdentifier(node.iterator1, 'v-for iterator', text, warn, range);\n    checkIdentifier(node.iterator2, 'v-for iterator', text, warn, range);\n  }\n\n  function checkIdentifier(\n    ident,\n    type,\n    text,\n    warn,\n    range\n  ) {\n    if (typeof ident === 'string') {\n      try {\n        new Function((\"var \" + ident + \"=_\"));\n      } catch (e) {\n        warn((\"invalid \" + type + \" \\\"\" + ident + \"\\\" in expression: \" + (text.trim())), range);\n      }\n    }\n  }\n\n  function checkExpression(exp, text, warn, range) {\n    try {\n      new Function((\"return \" + exp));\n    } catch (e) {\n      var keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE);\n      if (keywordMatch) {\n        warn(\n          \"avoid using JavaScript keyword as property name: \" +\n          \"\\\"\" + (keywordMatch[0]) + \"\\\"\\n  Raw expression: \" + (text.trim()),\n          range\n        );\n      } else {\n        warn(\n          \"invalid expression: \" + (e.message) + \" in\\n\\n\" +\n          \"    \" + exp + \"\\n\\n\" +\n          \"  Raw expression: \" + (text.trim()) + \"\\n\",\n          range\n        );\n      }\n    }\n  }\n\n  /*  */\n\n  var range = 2;\n\n  function generateCodeFrame(\n    source,\n    start,\n    end\n  ) {\n    if (start === void 0) start = 0;\n    if (end === void 0) end = source.length;\n\n    var lines = source.split(/\\r?\\n/);\n    var count = 0;\n    var res = [];\n    for (var i = 0; i < lines.length; i++) {\n      count += lines[i].length + 1;\n      if (count >= start) {\n        for (var j = i - range; j <= i + range || end > count; j++) {\n          if (j < 0 || j >= lines.length) {\n            continue\n          }\n          res.push((\"\" + (j + 1) + (repeat$1(\" \", 3 - String(j + 1).length)) + \"|  \" + (lines[j])));\n          var lineLength = lines[j].length;\n          if (j === i) {\n            // push underline\n            var pad = start - (count - lineLength) + 1;\n            var length = end > count ? lineLength - pad : end - start;\n            res.push(\"   |  \" + repeat$1(\" \", pad) + repeat$1(\"^\", length));\n          } else if (j > i) {\n            if (end > count) {\n              var length$1 = Math.min(end - count, lineLength);\n              res.push(\"   |  \" + repeat$1(\"^\", length$1));\n            }\n            count += lineLength + 1;\n          }\n        }\n        break\n      }\n    }\n    return res.join('\\n')\n  }\n\n  function repeat$1(str, n) {\n    var result = '';\n    if (n > 0) {\n      while (true) { // eslint-disable-line\n        if (n & 1) {\n          result += str;\n        }\n        n >>>= 1;\n        if (n <= 0) {\n          break\n        }\n        str += str;\n      }\n    }\n    return result\n  }\n\n  /*  */\n\n\n\n  function createFunction(code, errors) {\n    try {\n      return new Function(code)\n    } catch (err) {\n      errors.push({\n        err: err,\n        code: code\n      });\n      return noop\n    }\n  }\n\n  function createCompileToFunctionFn(compile) {\n    var cache = Object.create(null);\n\n    return function compileToFunctions(\n      template,\n      options,\n      vm\n    ) {\n      options = extend({}, options);\n      var warn###1 = options.warn || warn;\n      delete options.warn;\n\n      /* istanbul ignore if */\n      {\n        // detect possible CSP restriction\n        try {\n          new Function('return 1');\n        } catch (e) {\n          if (e.toString().match(/unsafe-eval|CSP/)) {\n            warn###1(\n              'It seems you are using the standalone build of Vue.js in an ' +\n              'environment with Content Security Policy that prohibits unsafe-eval. ' +\n              'The template compiler cannot work in this environment. Consider ' +\n              'relaxing the policy to allow unsafe-eval or pre-compiling your ' +\n              'templates into render functions.'\n            );\n          }\n        }\n      }\n\n      // check cache\n      var key = options.delimiters ?\n        String(options.delimiters) + template :\n        template;\n      // 缓存的作用：避免重复编译同个模板造成性能的浪费\n      if (cache[key]) {\n        return cache[key]\n      }\n\n      // compile\n      var compiled = compile(template, options);\n\n      // check compilation errors/tips\n      {\n        if (compiled.errors && compiled.errors.length) {\n          if (options.outputSourceRange) {\n            compiled.errors.forEach(function (e) {\n              warn###1(\n                \"Error compiling template:\\n\\n\" + (e.msg) + \"\\n\\n\" +\n                generateCodeFrame(template, e.start, e.end),\n                vm\n              );\n            });\n          } else {\n            warn###1(\n              \"Error compiling template:\\n\\n\" + template + \"\\n\\n\" +\n              compiled.errors.map(function (e) {\n                return (\"- \" + e);\n              }).join('\\n') + '\\n',\n              vm\n            );\n          }\n        }\n        if (compiled.tips && compiled.tips.length) {\n          if (options.outputSourceRange) {\n            compiled.tips.forEach(function (e) {\n              return tip(e.msg, vm);\n            });\n          } else {\n            compiled.tips.forEach(function (msg) {\n              return tip(msg, vm);\n            });\n          }\n        }\n      }\n\n      // turn code into functions\n      var res = {};\n      var fnGenErrors = [];\n      res.render = createFunction(compiled.render, fnGenErrors);\n      res.staticRenderFns = compiled.staticRenderFns.map(function (code) {\n        return createFunction(code, fnGenErrors)\n      });\n\n      // check function generation errors.\n      // this should only happen if there is a bug in the compiler itself.\n      // mostly for codegen development use\n      /* istanbul ignore if */\n      {\n        if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {\n          warn###1(\n            \"Failed to generate render function:\\n\\n\" +\n            fnGenErrors.map(function (ref) {\n              var err = ref.err;\n              var code = ref.code;\n\n              return ((err.toString()) + \" in\\n\\n\" + code + \"\\n\");\n            }).join('\\n'),\n            vm\n          );\n        }\n      }\n\n      return (cache[key] = res)\n    }\n  }\n\n  /*  */\n\n  function createCompilerCreator(baseCompile) {\n    return function createCompiler(baseOptions) {\n      // 内部定义compile方法\n      function compile(template, options) {\n        var finalOptions = Object.create(baseOptions);\n        var errors = [];\n        var tips = [];\n        var warn = function (msg, range, tip) {\n          (tip ? tips : errors).push(msg);\n        };\n        // 选项合并\n        if (options) {\n          if (options.outputSourceRange) {\n            // $flow-disable-line\n            var leadingSpaceLength = template.match(/^\\s*/)[0].length;\n\n            warn = function (msg, range, tip) {\n              var data = {\n                msg: msg\n              };\n              if (range) {\n                if (range.start != null) {\n                  data.start = range.start + leadingSpaceLength;\n                }\n                if (range.end != null) {\n                  data.end = range.end + leadingSpaceLength;\n                }\n              }\n              (tip ? tips : errors).push(data);\n            };\n          }\n          // merge custom modules\n          if (options.modules) {\n            finalOptions.modules =\n              (baseOptions.modules || []).concat(options.modules);\n          }\n          // merge custom directives\n          // 指令\n          if (options.directives) {\n            finalOptions.directives = extend(\n              Object.create(baseOptions.directives || null),\n              options.directives\n            );\n          }\n          // copy other options\n          for (var key in options) {\n            if (key !== 'modules' && key !== 'directives') {\n              finalOptions[key] = options[key];\n            }\n          }\n        }\n\n        finalOptions.warn = warn;\n        // 将剔除空格后的模板以及合并选项后的配置作为参数传递给baseCompile方法\n        var compiled = baseCompile(template.trim(), finalOptions); {\n          detectErrors(compiled.ast, warn);\n        }\n        compiled.errors = errors;\n        compiled.tips = tips;\n        return compiled\n      }\n      return {\n        compile: compile,\n        compileToFunctions: createCompileToFunctionFn(compile)\n      }\n    }\n  }\n\n  /*  */\n\n  // `createCompilerCreator` allows creating compilers that use alternative\n  // parser/optimizer/codegen, e.g the SSR optimizing compiler.\n  // Here we just export a default compiler using the default parts.\n  var createCompiler = createCompilerCreator(function baseCompile(\n    template,\n    options\n  ) {\n    var ast = parse(template.trim(), options);\n    if (options.optimize !== false) {\n      optimize(ast, options);\n    }\n    var code = generate(ast, options);\n    return {\n      ast: ast,\n      render: code.render,\n      staticRenderFns: code.staticRenderFns\n    }\n  });\n\n  /*  */\n\n  var ref$1 = createCompiler(baseOptions);\n  var compile = ref$1.compile;\n  var compileToFunctions = ref$1.compileToFunctions;\n\n  /*  */\n\n  // check whether current browser encodes a char inside attribute values\n  var div;\n\n  function getShouldDecode(href) {\n    div = div || document.createElement('div');\n    div.innerHTML = href ? \"<a href=\\\"\\n\\\"/>\" : \"<div a=\\\"\\n\\\"/>\";\n    return div.innerHTML.indexOf('&#10;') > 0\n  }\n\n  // #3663: IE encodes newlines inside attribute values while other browsers don't\n  var shouldDecodeNewlines = inBrowser ? getShouldDecode(false) : false;\n  // #6828: chrome encodes content in a[href]\n  var shouldDecodeNewlinesForHref = inBrowser ? getShouldDecode(true) : false;\n\n  /*  */\n\n  var idToTemplate = cached(function (id) {\n    var el = query(id);\n    return el && el.innerHTML\n  });\n\n  var mount = Vue.prototype.$mount;\n  Vue.prototype.$mount = function (\n    el,\n    hydrating\n  ) {\n    el = el && query(el);\n\n    /* istanbul ignore if */\n    if (el === document.body || el === document.documentElement) {\n      warn(\n        \"Do not mount Vue to <html> or <body> - mount to normal elements instead.\"\n      );\n      return this\n    }\n\n    var options = this.$options;\n    // resolve template/el and convert to render function\n    if (!options.render) {\n      var template = options.template;\n      if (template) {\n        if (typeof template === 'string') {\n          if (template.charAt(0) === '#') {\n            template = idToTemplate(template);\n            /* istanbul ignore if */\n            if (!template) {\n              warn(\n                (\"Template element not found or is empty: \" + (options.template)),\n                this\n              );\n            }\n          }\n        } else if (template.nodeType) {\n          template = template.innerHTML;\n        } else {\n          {\n            warn('invalid template option:' + template, this);\n          }\n          return this\n        }\n      } else if (el) {\n        template = getOuterHTML(el);\n      }\n      if (template) {\n        /* istanbul ignore if */\n        if (config.performance && mark) {\n          mark('compile');\n        }\n\n        var ref = compileToFunctions(template, {\n          outputSourceRange: \"development\" !== 'production',\n          shouldDecodeNewlines: shouldDecodeNewlines,\n          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,\n          delimiters: options.delimiters,\n          comments: options.comments\n        }, this);\n        var render = ref.render;\n        var staticRenderFns = ref.staticRenderFns;\n        options.render = render;\n        options.staticRenderFns = staticRenderFns;\n\n        /* istanbul ignore if */\n        if (config.performance && mark) {\n          mark('compile end');\n          measure((\"vue \" + (this._name) + \" compile\"), 'compile', 'compile end');\n        }\n      }\n    }\n    return mount.call(this, el, hydrating)\n  };\n\n  /**\n   * Get outerHTML of elements, taking care\n   * of SVG elements in IE as well.\n   */\n  function getOuterHTML(el) {\n    if (el.outerHTML) {\n      return el.outerHTML\n    } else {\n      var container = document.createElement('div');\n      container.appendChild(el.cloneNode(true));\n      return container.innerHTML\n    }\n  }\n\n  Vue.compile = compileToFunctions;\n\n  return Vue;\n\n}));"
  },
  {
    "path": "src/vue插槽，你想了解的都在这里.md",
    "content": "> Vue组件的另一个重要概念是插槽，它允许你以一种不同于严格的父子关系的方式组合组件。插槽为你提供了一个将内容放置到新位置或使组件更通用的出口。这一节将围绕官网对插槽内容的介绍思路，按照普通插槽，具名插槽，再到作用域插槽的思路，逐步深入内部的实现原理,有对插槽使用不熟悉的，可以先参考官网对[插槽](https://cn.vuejs.org/v2/guide/components-slots.html)的介绍。\n\n## 10.1 普通插槽\n插槽将```<slot></slot>```作为子组件承载分发的载体，简单的用法如下\n### 10.1.1 基础用法\n```\nvar child = {\n  template: `<div class=\"child\"><slot></slot></div>`\n}\nvar vm = new Vue({\n  el: '#app',\n  components: {\n    child\n  },\n  template: `<div id=\"app\"><child>test</child></div>`\n})\n// 最终渲染结果\n<div class=\"child\">test</div>\n```\n### 10.1.2 组件挂载原理\n插槽的原理，贯穿了整个组件系统编译到渲染的过程，所以首先需要回顾一下对组件相关编译渲染流程，简单总结一下几点：\n1. 从根实例入手进行实例的挂载，如果有手写的```render```函数，则直接进入```$mount```挂载流程。\n2. 只有```template```模板则需要对模板进行解析，这里分为两个阶段，一个是将模板解析为```AST```树，另一个是根据不同平台生成执行代码，例如```render```函数。\n3. ```$mount```流程也分为两步，第一步是将```render```函数生成```Vnode```树，如果遇到子组件会先生成子组件，子组件会以```vue-componet-```为```tag```标记，另一步是把```Vnode```渲染成真正的DOM节点。\n4. 创建真实节点过程中，如果遇到子的占位符组件会进行子组件的实例化过程，这个过程又将回到流程的第一步。\n\n接下来我们对```slot```的分析将围绕这四个具体的流程展开。\n\n\n### 10.1.3 父组件处理\n回到组件实例流程中，父组件会优先于子组件进行实例的挂载，模板的解析和```render```函数的生成阶段在处理上没有特殊的差异，这里就不展开分析。接下来是```render```函数生成```Vnode```的过程，在这个阶段会遇到子的占位符节点(即：```child```),因此会为子组件创建子的```Vnode```。```createComponent```执行了创建子占位节点```Vnode```的过程。我们把重点放在最终```Vnode```代码的生成。\n```js\n// 创建子Vnode过程\n  function createComponent (\n    Ctor, // 子类构造器\n    data,\n    context, // vm实例\n    children, // 父组件需要分发的内容\n    tag // 子组件占位符\n  ){\n    ···\n    // 创建子vnode，其中父保留的children属性会以选项的形式传递给Vnode\n    var vnode = new VNode(\n      (\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),\n      data, undefined, undefined, undefined, context,\n      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },\n      asyncFactory\n    );\n  }\n// Vnode构造器\nvar VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) {\n  ···\n  this.componentOptions = componentOptions; // 子组件的选项相关\n}\n```\n`createComponent`函数接收的第四个参数```children```就是父组件需要分发的内容。在创建子```Vnode```过程中，会以会```componentOptions```配置传入```Vnode```构造器中。**最终```Vnode```中父组件需要分发的内容以```componentOptions```属性的形式存在，这是插槽分析的第一步**。\n\n### 10.1.4 子组件流程\n父组件的最后一个阶段是将```Vnode```渲染为真正的DOM节点，在这个过程中如果遇到子```Vnode```会优先实例化子组件并进行一系列子组件的渲染流程。子组件初始化会先调用```_init```方法，并且和父组件不同的是，子组件会调用```initInternalComponent```方法拿到父组件拥有的相关配置信息，并赋值给子组件自身的配置选项。\n\n```js\n// 子组件的初始化\nVue.prototype._init = function(options) {\n  if (options && options._isComponent) {\n    initInternalComponent(vm, options);\n  }\n  initRender(vm)\n}\nfunction initInternalComponent (vm, options) {\n    var opts = vm.$options = Object.create(vm.constructor.options);\n    var parentVnode = options._parentVnode;\n    opts.parent = options.parent;\n    opts._parentVnode = parentVnode;\n    // componentOptions为子vnode记录的相关信息\n    var vnodeComponentOptions = parentVnode.componentOptions;\n    opts.propsData = vnodeComponentOptions.propsData;\n    opts._parentListeners = vnodeComponentOptions.listeners;\n    // 父组件需要分发的内容赋值给子选项配置的_renderChildren\n    opts._renderChildren = vnodeComponentOptions.children;\n    opts._componentTag = vnodeComponentOptions.tag;\n\n    if (options.render) {\n      opts.render = options.render;\n      opts.staticRenderFns = options.staticRenderFns;\n    }\n  }\n```\n最终在**子组件实例的配置中拿到了父组件保存的分发内容，记录在组件实例```$options._renderChildren```中，这是第二步的重点**。\n\n接下来是子组件的实例化会进入```initRender```阶段，在这个过程会**将配置的```_renderChildren```属性做规范化处理，并将他赋值给子实例上的```$slot```属性，这是第三步的重点**。\n\n```js\nfunction initRender(vm) {\n  ···\n  vm.$slots = resolveSlots(options._renderChildren, renderContext);// $slots拿到了子占位符节点的_renderchildren(即需要分发的内容)，保留作为子实例的属性\n}\n\nfunction resolveSlots (children,context) {\n    // children是父组件需要分发到子组件的Vnode节点，如果不存在，则没有分发内容\n    if (!children || !children.length) {\n      return {}\n    }\n    var slots = {};\n    for (var i = 0, l = children.length; i < l; i++) {\n      var child = children[i];\n      var data = child.data;\n      // remove slot attribute if the node is resolved as a Vue slot node\n      if (data && data.attrs && data.attrs.slot) {\n        delete data.attrs.slot;\n      }\n      // named slots should only be respected if the vnode was rendered in the\n      // same context.\n      // 分支1为具名插槽的逻辑，放后分析\n      if ((child.context === context || child.fnContext === context) &&\n        data && data.slot != null\n      ) {\n        var name = data.slot;\n        var slot = (slots[name] || (slots[name] = []));\n        if (child.tag === 'template') {\n          slot.push.apply(slot, child.children || []);\n        } else {\n          slot.push(child);\n        }\n      } else {\n      // 普通插槽的重点，核心逻辑是构造{ default: [children] }对象返回\n        (slots.default || (slots.default = [])).push(child);\n      }\n    }\n    return slots\n  }\n```\n其中普通插槽的处理逻辑核心在```(slots.default || (slots.default = [])).push(child);```，即以数组的形式赋值给```default```属性，并以```$slot```属性的形式保存在子组件的实例中。\n\n\n随后子组件也会走挂载的流程，同样会经历```template```模板到```render```函数，再到```Vnode```,最后渲染真实```DOM```的过程。解析```AST```阶段，```slot```标签和其他普通标签处理相同，**不同之处在于```AST```生成```render```函数阶段，对```slot```标签的处理，会使用```_t函数```进行包裹。这是关键步骤的第四步**\n\n子组件渲染的大致流程简单梳理如下:\n```js\n// ast 生成 render函数\nvar code = generate(ast, options);\n// generate实现\nfunction generate(ast, options) {\n  var state = new CodegenState(options);\n  var code = ast ? genElement(ast, state) : '_c(\"div\")';\n  return {\n    render: (\"with(this){return \" + code + \"}\"),\n    staticRenderFns: state.staticRenderFns\n  }\n}\n// genElement实现\nfunction genElement(el, state) {\n  // 针对slot标签的处理走```genSlot```分支\n  if (el.tag === 'slot') {\n    return genSlot(el, state)\n  }\n}\n// 核心genSlot原理\nfunction genSlot (el, state) {\n    // slotName记录着插槽的唯一标志名，默认为default\n    var slotName = el.slotName || '\"default\"';\n    // 如果子组件的插槽还有子元素，则会递归调执行子元素的创建过程\n    var children = genChildren(el, state);\n    // 通过_t函数包裹\n    var res = \"_t(\" + slotName + (children ? (\",\" + children) : '');\n    // 具名插槽的其他处理\n    ···    \n    return res + ')'\n  }\n```\n最终子组件的```render```函数为：\n```js\n\"with(this){return _c('div',{staticClass:\"child\"},[_t(\"default\")],2)}\"\n```\n\n**第五步到了子组件渲染为```Vnode```的过程。```render```函数执行阶段会执行```_t()```函数，```_t```函数是```renderSlot```函数简写，它会在```Vnode```树中进行分发内容的替换**，具体看看实现逻辑。\n```js\n\n// target._t = renderSlot;\n\n// render函数渲染Vnode函数\nVue.prototype._render = function() {\n  var _parentVnode = ref._parentVnode;\n  if (_parentVnode) {\n    // slots的规范化处理并赋值给$scopedSlots属性。\n    vm.$scopedSlots = normalizeScopedSlots(\n      _parentVnode.data.scopedSlots,\n      vm.$slots, // 记录父组件的插槽内容\n      vm.$scopedSlots\n    );\n  }\n}\n```\n\n`normalizeScopedSlots`的逻辑较长，但并不是本节的重点。拿到```$scopedSlots```属性后会执行真正的```render```函数,其中```_t```的执行逻辑如下：\n```js\n// 渲染slot组件内容\n  function renderSlot (\n    name,\n    fallback, // slot插槽后备内容(针对后备内容)\n    props, // 子传给父的值(作用域插槽)\n    bindObject\n  ) {\n    // scopedSlotFn拿到父组件插槽的执行函数，默认slotname为default\n    var scopedSlotFn = this.$scopedSlots[name];\n    var nodes;\n    // 具名插槽分支(暂时忽略)\n    if (scopedSlotFn) { // scoped slot\n      props = props || {};\n      if (bindObject) {\n        if (!isObject(bindObject)) {\n          warn(\n            'slot v-bind without argument expects an Object',\n            this\n          );\n        }\n        props = extend(extend({}, bindObject), props);\n      }\n      // 执行时将子组件传递给父组件的值传入fn\n      nodes = scopedSlotFn(props) || fallback;\n    } else {\n      // 如果父占位符组件没有插槽内容，this.$slots不会有值，此时vnode节点为后备内容节点。\n      nodes = this.$slots[name] || fallback;\n    }\n\n    var target = props && props.slot;\n    if (target) {\n      return this.$createElement('template', { slot: target }, nodes)\n    } else {\n      return nodes\n    }\n  }\n```\n`renderSlot`执行过程会拿到父组件需要分发的内容，最终```Vnode```树将父元素的插槽替换掉子组件的```slot```组件。\n\n**最后一步就是子组件真实节点的渲染了，这点没有什么特别点，和以往介绍的流程一致**。\n\n至此，一个完整且简单的插槽流程分析完毕。接下来看插槽深层次的用法。\n\n## 10.2 具有后备内容的插槽\n有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的，它只会在没有提供内容的时候被渲染。查看源码发现后备内容插槽的逻辑也很好理解。\n```js\nvar child = {\n  template: `<div class=\"child\"><slot>后备内容</slot></div>`\n}\nvar vm = new Vue({\n  el: '#app',\n  components: {\n    child\n  },\n  template: `<div id=\"app\"><child></child></div>`\n})\n// 父没有插槽内容，子的slot会渲染后备内容\n<div class=\"child\">后备内容</div>\n```\n父组件没有需要分发的内容，子组件会默认显示插槽里面的内容。源码中的不同体现在下面的几点。\n1. 父组件渲染过程由于没有需要分发的子节点，所以不再需要拥有```componentOptions.children```属性来记录内容。\n2. 因此子组件也拿不到```$slot```属性的内容.\n3. 子组件的```render```函数最后在```_t```函数参数会携带第二个参数，该参数以数组的形式传入```slot```插槽的后备内容。例```with(this){return _c('div',{staticClass:\"child\"},[_t(\"default\",[_v(\"test\")])],2)}```\n4. 渲染子```Vnode```会执行```renderSlot(即：_t)```函数时，第二个参数```fallback```有值，且```this.$slots```没值，```vnode```会直接返回后备内容作为渲染对象。\n\n```js\nfunction renderSlot (\n    name,\n    fallback, // slot插槽后备内容(针对后备内容)\n    props, // 子传给父的值(作用域插槽)\n    bindObject\n){\n    if() {\n      ···\n    }else{\n      //fallback为后备内容\n      // 如果父占位符组件没有插槽内容，this.$slots不会有值，此时vnode节点为后备内容节点。\n      nodes = this.$slots[name] || fallback;\n    }\n}\n    \n```\n\n最终，在父组件没有提供内容时，```slot```的后备内容被渲染。\n\n有了这些基础，我们再来看官网给的一条规则。\n\n> 父级模板里的所有内容都是在父级作用域中编译的；子模板里的所有内容都是在子作用域中编译的。\n\n父组件模板的内容在父组件编译阶段就确定了,并且保存在```componentOptions```属性中，而子组件有自身初始化```init```的过程，这个过程同样会进行子作用域的模板编译，因此两部分内容是相对独立的。\n\n## 10.3 具名插槽\n往往我们需要灵活的使用插槽进行通用组件的开发，要求父组件每个模板对应子组件中每个插槽，这时我们可以使用```<slot>```的```name```属性，同样举个简单的例子。\n```js\nvar child = {\n  template: `<div class=\"child\"><slot name=\"header\"></slot><slot name=\"footer\"></slot></div>`,\n}\nvar vm = new Vue({\n  el: '#app',\n  components: {\n    child\n  },\n  template: `<div id=\"app\"><child><template v-slot:header><span>头部</span></template><template v-slot:footer><span>底部</span></template></child></div>`,\n})\n```\n渲染结果：\n```js\n<div class=\"child\"><span>头部</span><span>底部</span></div>\n```\n接下来我们在普通插槽的基础上，看看源码在具名插槽实现上的区别。\n\n### 10.3.1 模板编译的差别\n父组件在编译```AST```阶段和普通节点的过程不同，具名插槽一般会在```template```模板中用```v-slot:```来标注指定插槽，这一阶段会在编译阶段特殊处理。最终的```AST```树会携带```scopedSlots```用来记录具名插槽的内容\n```js\n{\n  scopedSlots： {\n    footer: { ··· },\n    header: { ··· }\n  }\n}\n```\n`AST`生成```render```函数的过程也不详细分析了，我们只分析父组件最终返回的结果(如果对```parse, generate```感兴趣的同学，可以直接看源码分析,编译阶段冗长且难以讲解，跳过这部分分析)\n\n```js\nwith(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('child',{scopedSlots:_u([{key:\"header\",fn:function(){return [_c('span',[_v(\"头部\")])]},proxy:true},{key:\"footer\",fn:function(){return [_c('span',[_v(\"底部\")])]},proxy:true}])})],1)}\n```\n很明显，父组件的插槽内容用```_u```函数封装成数组的形式，并赋值到```scopedSlots```属性中，而每一个插槽以对象形式描述，```key```代表插槽名，```fn```是一个返回执行结果的函数。\n\n### 10.3.2 父组件vnode生成阶段\n\n照例进入父组件生成```Vnode```阶段，其中```_u```函数的原形是```resolveScopedSlots```,其中第一个参数就是插槽数组。\n```js\n// vnode生成阶段针对具名插槽的处理 _u      (target._u = resolveScopedSlots)\n  function resolveScopedSlots (fns,res,hasDynamicKeys,contentHashKey) {\n    res = res || { $stable: !hasDynamicKeys };\n    for (var i = 0; i < fns.length; i++) {\n      var slot = fns[i];\n      // fn是数组需要递归处理。\n      if (Array.isArray(slot)) {\n        resolveScopedSlots(slot, res, hasDynamicKeys);\n      } else if (slot) {\n        // marker for reverse proxying v-slot without scope on this.$slots\n        if (slot.proxy) { //  针对proxy的处理\n          slot.fn.proxy = true;\n        }\n        // 最终返回一个对象，对象以slotname作为属性，以fn作为值\n        res[slot.key] = slot.fn;\n      }\n    }\n    if (contentHashKey) {\n      (res).$key = contentHashKey;\n    }\n    return res\n  }\n```\n最终父组件的```vnode```节点的```data```属性上多了```scopedSlots```数组。**回顾一下，具名插槽和普通插槽实现上有明显的不同，普通插槽是以```componentOptions.child```的形式保留在父组件中，而具名插槽是以```scopedSlots```属性的形式存储到```data```属性中。**\n```js\n// vnode\n{\n  scopedSlots: [{\n    'header': fn,\n    'footer': fn\n  }]\n}\n```\n\n### 10.3.3 子组件渲染Vnode过程\n\n子组件在解析成```AST```树阶段的不同，在于对```slot```标签的```name```属性的解析,而在```render```生成```Vnode```过程中，```slot```的规范化处理针对具名插槽会进行特殊的处理，回到```normalizeScopedSlots```的代码\n```js\nvm.$scopedSlots = normalizeScopedSlots(\n  _parentVnode.data.scopedSlots, // 此时的第一个参数会拿到父组件插槽相关的数据\n  vm.$slots, // 记录父组件的插槽内容\n  vm.$scopedSlots\n);\n\n```\n最终子组件实例上的```$scopedSlots```属性会携带父组件插槽相关的内容。\n```js\n// 子组件Vnode\n{\n  $scopedSlots: [{\n    'header': f,\n    'footer': f\n  }]\n}\n```\n\n### 10.3.4 子组件渲染真实dom\n\n和普通插槽类似，子组件渲染真实节点的过程会执行子```render```函数中的```_t```方法，这部分的源码会和普通插槽走不同的分支，其中```this.$scopedSlots```根据上面分析会记录着父组件插槽内容相关的数据，所以会和普通插槽走不同的分支。而最终的核心是执行```nodes = scopedSlotFn(props)```,也就是执行```function(){return [_c('span',[_v(\"头部\")])]}```,具名插槽之所以是函数的形式执行而不是直接返回结果，我们在后面揭晓。\n```js\nfunction renderSlot (\n    name,\n    fallback, // slot插槽后备内容\n    props, // 子传给父的值\n    bindObject\n  ){\n    var scopedSlotFn = this.$scopedSlots[name];\n    var nodes;\n    // 针对具名插槽，特点是$scopedSlots有值\n    if (scopedSlotFn) { // scoped slot\n      props = props || {};\n      if (bindObject) {\n        if (!isObject(bindObject)) {\n          warn('slot v-bind without argument expects an Object',this);\n        }\n        props = extend(extend({}, bindObject), props);\n      }\n      // 执行时将子组件传递给父组件的值传入fn\n      nodes = scopedSlotFn(props) || fallback;\n    }···\n  }\n```\n至此子组件通过```slotName```找到了对应父组件的插槽内容。\n\n\n## 10.4 作用域插槽\n最后说说作用域插槽，我们可以利用作用域插槽让父组件的插槽内容访问到子组件的数据，具体的用法是在子组件中以属性的方式记录在子组件中，父组件通过```v-slot:[name]=[props]```的形式拿到子组件传递的值。子组件```<slot>```元素上的特性称为**插槽```Props```**,另外，vue2.6以后的版本已经弃用了```slot-scoped```，采用```v-slot```代替。\n```js\nvar child = {\n  template: `<div><slot :user=\"user\"></div>`,\n  data() {\n    return {\n      user: {\n        firstname: 'test'\n      }\n    }\n  }\n}\nvar vm = new Vue({\n  el: '#app',\n  components: {\n    child\n  },\n  template: `<div id=\"app\"><child><template v-slot:default=\"slotProps\">{{slotProps.user.firstname}}</template></child></div>`\n})\n```\n\n作用域插槽和具名插槽的原理类似，我们接着往下看。\n\n### 10.4.1 父组件编译阶段\n作用域插槽和具名插槽在父组件的用法基本相同，区别在于```v-slot```定义了一个插槽```props```的名字，参考对于具名插槽的分析，生成```render```函数阶段```fn```函数会携带```props```参数传入。即：\n\n```js\nwith(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('child',{scopedSlots:_u([{key:\"default\",fn:function(slotProps){return [_v(_s(slotProps.user.firstname))]}}])})],1)}\n```\n\n### 10.4.2 子组件渲染\n在子组件编译阶段，```:user=\"user\"```会以属性的形式解析，最终在```render```函数生成阶段以对象参数的形式传递```_t```函数。\n\n```js\nwith(this){return _c('div',[_t(\"default\",null,{\"user\":user})],2)}\n```\n\n子组件渲染Vnode阶段，根据前面分析会执行```renderSlot```函数，这个函数前面分析过，对于作用域插槽的处理，集中体现在函数传入的第三个参数。\n```js\n// 渲染slot组件vnode\nfunction renderSlot(\n  name,\n  fallback,\n  props, // 子传给父的值 { user: user }\n  bindObject\n) {\n    // scopedSlotFn拿到父组件插槽的执行函数，默认slotname为default\n    var scopedSlotFn = this.$scopedSlots[name];\n    var nodes;\n    // 具名插槽分支\n    if (scopedSlotFn) { // scoped slot\n      props = props || {};\n      if (bindObject) {\n        if (!isObject(bindObject)) {\n          warn(\n            'slot v-bind without argument expects an Object',\n            this\n          );\n        }\n        // 合并props\n        props = extend(extend({}, bindObject), props);\n      }\n      // 执行时将子组件传递给父组件的值传入fn\n      nodes = scopedSlotFn(props) || fallback;\n    }\n```\n最终将子组件的插槽```props```作为参数传递给执行函数执行。**回过头看看为什么具名插槽是函数的形式执行而不是直接返回结果。学完作用域插槽我们发现这就是设计巧妙的地方，函数的形式让执行过程更加灵活，作用域插槽只需要以参数的形式将插槽```props```传入便可以得到想要的结果。**\n\n### 10.4.3 思考\n作用域插槽这个概念一开始我很难理解，单纯从定义和源码的结论上看，父组件的插槽内容可以访问到子组件的数据，这不是明显的子父之间的信息通信吗，在事件章节我们知道，子父组件之间的通信完全可以通过事件```$emit,$on```的形式来完成，那么为什么还需要增加一个插槽```props```的概念呢。我们看看作者的解释。\n\n> 插槽 ```prop``` 允许我们将插槽转换为可复用的模板，这些模板可以基于输入的 ```prop``` 渲染出不同的内容\n\n从我自身的角度理解，作用域插槽提供了一种方式，当你需要封装一个通用，可复用的逻辑模块，并且这个模块给外部使用者提供了一个便利，允许你在使用组件时自定义部分布局，这时候作用域插槽就派上大用场了，再到具体的思想，我们可以看看几个工具库[Vue Virtual Scroller](https://github.com/Akryum/vue-virtual-scroller), [Vue Promised](https://github.com/posva/vue-promised)对这一思想的应用。\n\n"
  },
  {
    "path": "src/丰富的选项合并策略.md",
    "content": "## 1.1 Vue的引入\n`Vue`的使用按照官方的说法支持```CDN```和```NPM```两种方式，```CDN```的方式是以```script```的方式将打包好的```vue.js```引入页面脚本中，而```NPM```的方式是和诸如 ```webpack``` 或 ```Browserify``` 模块打包器配置使用，以```npm install vue```的方式引入，这也是我们开发应用的主要形式。而从单纯分析源码思路和实现细节的角度来讲，打包后的```vue.js```在分析和提炼源码方面会更加方便，所以这个系列的源码分析，使用的是打包后的```vue```脚本，**版本号是```v2.6.8```**\n### 1.1.1 基础使用\n分析的开始当然是```vue```的基础使用，我们引入了```vue.js```并且```new```了一个```Vue```实例，并将它挂载到```#app```上，这是最基础的用法。\n```js\n<div id=\"app\"></div>\n<script src=\"https://cdn.jsdelivr.net/npm/vue@2.6.8/dist/vue.js\"></script>\n<script>\nvar vm = new Vue({\n  el: '#app',\n  data: {\n    message: '选项合并'\n  },\n})\n</script>\n```\n虽然这一节的重点是阐述```Vue```的选项配置，从选项配置入手也是我们从零开始品读源码最容易开始的思路，但是为了分析的完整性，避免后续出现未知的概念，有必要先大致了解一下```vue```在脚本引入之后分别做了什么。\n\n### 1.1.2 Vue构造器\n打包后的源码是遵从```UMD```规范的，它是```commonjs```和```amd```的整合。而```Vue```的本质是一个构造器,并且它保证了只能通过```new```实例的形式去调用，而不能直接通过函数的形式使用。\n```js\n(function (global, factory) {\n  // 遵循UMD规范\n  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :\n  typeof define === 'function' && define.amd ? define(factory) :\n  (global = global || self, global.Vue = factory());\n}(this, function () { 'use strict';\n  ···\n  // Vue 构造函数\n  function Vue (options) {\n    // 保证了无法直接通过Vue()去调用，只能通过new的方式去创建实例\n    if (!(this instanceof Vue)\n    ) {\n      warn('Vue is a constructor and should be called with the `new` keyword');\n    }\n    this._init(options);\n  }\n  return Vue\n})\n```\n\n### 1.1.3 定义原型属性方法\n\nVue之所以能适应基础的开发场景，除了经常提到的支持组件化开发，以及完善的响应式系统等外，还有重要的一点是它提供了丰富的```api```方法，不管是静态还是原型方法，它们都丰富到足以满足我们日常基础的开发需求。所以熟练阅读[vue-api](https://cn.vuejs.org/v2/api/)文档并精准使用```api```方法是迈向熟练开发的前提。接下来我们看看这些方法属性是在哪里定义的，**注意，该小节会忽略大部分属性方法具体的实现，这些详细的细节会贯穿在后续系列的分析中**。\n\n首先是原型上的属性方法，在构造函数的定义之后，有这样五个函数，他们分别针对不同场景定义了```Vue```原型上的属性和方法。\n```js\n  // 定义Vue原型上的init方法(内部方法)\n  initMixin(Vue);\n  // 定义原型上跟数据相关的属性方法\n  stateMixin(Vue);\n  //定义原型上跟事件相关的属性方法\n  eventsMixin(Vue);\n  // 定义原型上跟生命周期相关的方法\n  lifecycleMixin(Vue);\n  // 定义渲染相关的函数\n  renderMixin(Vue); \n```\n我们一个个看，首先```initMixin```定义了**内部在实例化```Vue```时会执行的初始化代码**，它是一个内部使用的方法。\n```js\nfunction initMixin (Vue) {\n  Vue.prototype._init = function (options) {}\n}\n```\n\n`stateMixin`方法会定义跟数据相关的属性方法，例如代理数据的访问，我们可以在实例上通过```this.$data```和```this.$props```访问到```data,props```的值，并且也定义了使用频率较高的```this.$set,this.$delte```等方法。\n\n```js\nfunction stateMixin (Vue) {\n    var dataDef = {};\n    dataDef.get = function () { return this._data };\n    var propsDef = {};\n    propsDef.get = function () { return this._props };\n    {\n      dataDef.set = function () {\n        warn(\n          'Avoid replacing instance root $data. ' +\n          'Use nested data properties instead.',\n          this\n        );\n      };\n      propsDef.set = function () {\n        warn(\"$props is readonly.\", this);\n      };\n    }\n    // 代理了_data,_props的访问\n    Object.defineProperty(Vue.prototype, '$data', dataDef);\n    Object.defineProperty(Vue.prototype, '$props', propsDef);\n    // $set, $del\n    Vue.prototype.$set = set;\n    Vue.prototype.$delete = del;\n\n    // $watch\n    Vue.prototype.$watch = function (expOrFn,cb,options) {};\n  }\n```\n\n`eventsMixin`会对原型上的事件相关方法做定义，文档中提到的```vm.$on,vm.$once,vm.$off,vm.$emit```也就是在这里定义的。\n```js\nfunction eventsMixin(Vue) {\n  // 自定义事件监听\n  Vue.prototype.$on = function (event, fn) {};\n  // 自定义事件监听,只触发一次\n  Vue.prototype.$once = function (event, fn) {}\n  // 自定义事件解绑\n  Vue.prototype.$off = function (event, fn) {}\n  // 自定义事件通知\n  Vue.prototype.$emit = function (event, fn) {\n}\n```\n`lifecycleMixin,renderMixin`两个都可以算是对生命周期渲染方法的定义，例如```$forceUpdate```触发实例的强制刷新，```$nextTick```将回调延迟到下次 ```DOM``` 更新循环之后执行等。\n```js\n// 定义跟生命周期相关的方法\n  function lifecycleMixin (Vue) {\n    Vue.prototype._update = function (vnode, hydrating) {};\n\n    Vue.prototype.$forceUpdate = function () {};\n\n    Vue.prototype.$destroy = function () {}\n  }\n\n// 定义原型上跟渲染相关的方法\n  function renderMixin (Vue) {\n    Vue.prototype.$nextTick = function (fn) {};\n    // _render函数，后面会着重讲\n    Vue.prototype._render = function () {};\n  }\n```\n\n### 1.1.4 定义静态属性方法\n除了原型方法外，```Vue```还提供了丰富的全局```api```方法，这些都是在```initGlobalAPI```中定义的。\n```js\n/* 初始化构造器的api */\nfunction initGlobalAPI (Vue) {\n    // config\n    var configDef = {};\n    configDef.get = function () { return config; };\n    {\n      configDef.set = function () {\n        warn(\n          'Do not replace the Vue.config object, set individual fields instead.'\n        );\n      };\n    }\n    // 通过Vue.config拿到配置信息\n    Object.defineProperty(Vue, 'config', configDef);\n\n    // 工具类不作为公共暴露的API使用\n    Vue.util = {\n      warn: warn,\n      extend: extend,\n      mergeOptions: mergeOptions,\n      defineReactive: defineReactive###1\n    };\n\n    // Vue.set = Vue.prototype.$set\n    Vue.set = set;\n    // Vue.delete = Vue.prototype.$delete\n    Vue.delete = del;\n    // Vue.nextTick = Vue.prototype.$nextTick\n    Vue.nextTick = nextTick;\n\n    // 2.6 explicit observable API\n    Vue.observable = function (obj) {\n      observe(obj);\n      return obj\n    };\n\n    // 构造函数的默认选项默认为components,directive,filter, _base\n    Vue.options = Object.create(null);\n    ASSET_TYPES.forEach(function (type) {\n      Vue.options[type + 's'] = Object.create(null);\n    });\n\n    // options里的_base属性存储Vue构造器\n    Vue.options._base = Vue;\n    extend(Vue.options.components, builtInComponents);\n    // Vue.use()\n    initUse(Vue);\n    // Vue.mixin()\n    initMixin$1(Vue);\n    // 定义extend扩展子类构造器的方法\n    // Vue.extend()\n    initExtend(Vue);\n    // Vue.components, Vue.directive, Vue.filter\n    initAssetRegisters(Vue);\n  }\n\n```\n看着源码对静态方法的定义做一个汇总。\n1. 为源码里的```config```配置做一层代理，可以通过```Vue.config```拿到默认的配置，并且可以修改它的属性值，具体哪些可以配置修改，可以先参照官方文档。\n2. 定义内部使用的工具方法，例如警告提示，对象合并等。\n3. 定义```set,delet,nextTick```方法，本质上原型上也有这些方法的定义。\n4. 对```Vue.components,Vue.directive,Vue.filter```的定义，这些是默认的资源选项，后续会重点分析。\n5. 定义```Vue.use()```方法\n6. 定义```Vue.mixin()```方法\n7. 定义```Vue.extend()```方法\n\n\n现在我相信你已经对引入```Vue```的阶段有了一个大致的认识，在源码分析的初期阶段，我们不需要死磕每个方法，思路的实现细节，只需要对大致的结构有基本的认识。有了这些基础，我们开始进入这个章节的主线。\n\n## 1.2 构造器的默认选项\n我们回到最开始的例子，在实例化```Vue```时，我们会将选项对象传递给构造器进行初始化，这个选项对象描述了你想要的行为，例如以```data```定义实例中的响应式数据，以```computed```描述实例中的计算属性，以```components```来进行组件注册，甚至是定义各个阶段执行的生命周期钩子等。然而```Vue```内部本身会自带一些默认的选项，这些选项和用户自定义的选项会在后续一起参与到```Vue```实例的初始化中。\n\n在```initGlobalAPI```方法中有几行默认选项的定义。```Vue```内部的默认选项会保留在静态的```options```属性上，从源码看```Vue```自身有四个默认配置选项，分别是```component，directive， filter```以及返回自身构造器的```_base```。\n```js\nvar ASSET_TYPES = [\n  'component',\n  'directive',\n  'filter'\n];\n// 原型上创建了一个指向为空对象的options属性\nVue.options = Object.create(null); \nASSET_TYPES.forEach(function (type) {\n  Vue.options[type + 's'] = Object.create(null);\n});\nVue.options._base = Vue;\n```\n\n很明显我们开发者对这几个选项是非常熟悉的，```components```是需要注册的组件选项，```directives```是需要注册的指令，而```filter```则代表需要注册的过滤器。从代码的实现细节看，```Vue```为```components```提供了```keepAlive,transition,transitionGroup```的内置组件，为```directives```提供了```v-model,v-show```的内置指令，而过滤器则没有默认值。\n\n```js\n// Vue内置组件\nvar builtInComponents = {\n  KeepAlive: KeepAlive\n};\nvar platformComponents = {\n  Transition: Transition,\n  TransitionGroup: TransitionGroup\n};\n// Vue 内置指令，例如： v-model, v-show\nvar platformDirectives = {\n  model: directive,\n  show: show\n}\nextend(Vue.options.components, builtInComponents); \nextend(Vue.options.components, platformComponents); // 扩展内置组件\nextend(Vue.options.directives, platformDirectives);  // 扩展内置指令\n```\n其中```extend```方法实现了对象的合并，如果属性相同，则用新的属性值覆盖旧值。\n\n```js\n// 将_from对象合并到to对象，属性相同时，则覆盖to对象的属性\nfunction extend (to, _from) {\n  for (var key in _from) {\n    to[key] = _from[key];\n  }\n  return to\n}\n```\n\n因此做为构造器而言，```Vue```默认的资源选项配置如下：\n```js\nVue.options = {\n  components: {\n    KeepAlive: {}\n    Transition: {}\n    TransitionGroup: {}\n  },\n  directives: {\n    model: {inserted: ƒ, componentUpdated: ƒ}\n    show: {bind: ƒ, update: ƒ, unbind: ƒ}\n  },\n  filters: {}\n  _base\n}\n```\n\n\n## 1.3 选项检验\n介绍完```Vue```自身拥有的选项后，我们回过头来看看，实例化```Vue```的阶段发生了什么。从构造器的定义我们很容易发现，实例化```Vue```做的核心操作便是执行```_init```方法进行初始化。初始化操作会经过选项合并配置，初始化生命周期，初始化事件中心，乃至构建数据响应式系统等。而关键的第一步就是对选项的合并。合并后的选项会挂载到实例的```$options```属性中。(你可以先在实例中通过```this.$options```访问最终的选项)\n```js\nfunction initMixin (Vue) {\n  Vue.prototype._init = function (options) {\n    var vm = this;\n    // a uid\n    // 记录实例化多少个vue对象\n    vm._uid = uid$3++;\n\n    // 选项合并，将合并后的选项赋值给实例的$options属性\n    vm.$options = mergeOptions(\n      resolveConstructorOptions(vm.constructor), // 返回Vue构造函数自身的配置项\n      options || {},\n      vm\n    );\n  };\n}\n```\n从代码中可以看到，选项合并的重点是将用户自身传递的```options```选项和```Vue```构造函数自身的选项配置合并。我们看看```mergeOptions```函数的实现。\n\n```js\nfunction mergeOptions (parent,child,vm) {\n    {\n      checkComponents(child);\n    }\n    if (typeof child === 'function') {\n      child = child.options;\n    }\n    // props,inject,directives的校验和规范化\n    normalizeProps(child, vm);\n    normalizeInject(child, vm);\n    normalizeDirectives(child);\n    \n    // 针对extends扩展的子类构造器\n    if (!child._base) {\n      // extends\n      if (child.extends) {\n        parent = mergeOptions(parent, child.extends, vm);\n      }\n      // mixins\n      if (child.mixins) {\n        for (var i = 0, l = child.mixins.length; i < l; i++) {\n          parent = mergeOptions(parent, child.mixins[i], vm);\n        }\n      }\n    }\n\n    var options = {};\n    var key;\n    for (key in parent) {\n      mergeField(key);\n    }\n    for (key in child) {\n      if (!hasOwn(parent, key)) {\n        mergeField(key);\n      }\n    }\n    function mergeField (key) {\n      // 拿到各个选择指定的选项配置，如果没有则用默认的配置\n      var strat = strats[key] || defaultStrat;\n      // 执行各自的合并策略\n      options[key] = strat(parent[key], child[key], vm, key);\n    }\n    // console.log(options)\n    return options\n  }\n```\n**选项合并过程中更多的不可控在于不知道用户传递了哪些配置选项，这些配置是否符合规范，是否达到合并配置的要求。因此每个选项的书写规则需要严格限定，原则上不允许用户脱离规则外来传递选项。**因此在合并选项之前，很大的一部分工作是对选项的校验。其中```components,prop,inject,directive```等都是检验的重点。\n\n### 1.3.1 components规范检验\n如果项目中需要使用到组件，我们会在```vue```实例化时传入组件选项以此来注册组件。因此，组件命名需要遵守很多规范，比如组件名不能用```html```保留的标签(如：```img,p```),也不能包含非法的字符等。这些都会在```validateComponentName```函数做校验。\n\n```js\n// components规范检查函数\nfunction checkComponents (options) {\n  // 遍历components对象，对每个属性值校验。\n  for (var key in options.components) {\n    validateComponentName(key);\n  }\n}\nfunction validateComponentName (name) {\n  if (!new RegExp((\"^[a-zA-Z][\\\\-\\\\.0-9_\" + (unicodeRegExp.source) + \"]*$\")).test(name)) {\n    // 正则判断检测是否为非法的标签，例如数字开头\n    warn(\n      'Invalid component name: \"' + name + '\". Component names ' +\n      'should conform to valid custom element name in html5 specification.'\n    );\n  }\n  // 不能使用Vue自身自定义的组件名，如slot, component,不能使用html的保留标签，如 h1, svg等\n  if (isBuiltInTag(name) || config.isReservedTag(name)) {\n    warn(\n      'Do not use built-in or reserved HTML elements as component ' +\n      'id: ' + name\n    );\n  }\n}\n```\n\n### 1.3.2 props规范检验\n`Vue`的官方文档规定了```props```选项的书写形式有两种，分别是\n1. 数组形式 ```{ props: ['a', 'b', 'c'] }```,\n2. 带校验规则的对象形式 ```{ props: { a: { type: 'String', default: 'prop校验' } }}```\n从源码上看，**两种形式最终都会转换成对象的形式。**\n\n```js\n// props规范校验\n  function normalizeProps (options, vm) {\n    var props = options.props;\n    if (!props) { return }\n    var res = {};\n    var i, val, name;\n    // props选项数据有两种形式，一种是['a', 'b', 'c'],一种是{ a: { type: 'String', default: 'hahah' }}\n    // 数组\n    if (Array.isArray(props)) {\n      i = props.length;\n      while (i--) {\n        val = props[i];\n        if (typeof val === 'string') {\n          name = camelize(val);\n          // 默认将数组形式的props转换为对象形式。\n          res[name] = { type: null }; \n        } else {\n          // 规则：保证是字符串\n          warn('props must be strings when using array syntax.');\n        }\n      }\n    } else if (isPlainObject(props)) {\n      for (var key in props) {\n        val = props[key];\n        name = camelize(key);\n        res[name] = isPlainObject(val)\n          ? val\n          : { type: val };\n      }\n    } else {\n      // 非数组，非对象则判定props选项传递非法\n      warn(\n        \"Invalid value for option \\\"props\\\": expected an Array or an Object, \" +\n        \"but got \" + (toRawType(props)) + \".\",\n        vm\n      );\n    }\n    options.props = res;\n  }\n```\n\n### 1.3.3 inject的规范校验\n`provide/inject`这对组合在我们日常开发中可能使用得比较少，当我们需要在父组件中提供数据或者方法给后代组件使用时可以用到```provide/inject```,注意关键是后代，而不单纯指子代，这是有别于```props```的使用场景。官方把它被称为依赖注入，依赖注入使得组件后代都能访问到父代注入的数据/方法，且后代不需要知道数据的来源。重要的一点，依赖提供的数据是非响应式的。\n\n基本的使用如下：\n```js\n// 父组件\nvar Provider = {\n  provide: {\n    foo: 'bar'\n  },\n  // ...\n}\n// 后代组件\nvar Child = {\n  // 数组写法\n  inject: ['foo'],\n  // 对象写法\n  inject: {\n    foo: {\n      from: 'foo',\n      default: 'bardefault'\n    }\n  }\n}\n```\n`inject`选项有两种写法，数组的方式以及对象的方式，和```props```的校验规则一致，最终```inject```都会转换为对象的形式存在。\n```js\n// inject的规范化\nfunction normalizeInject (options, vm) {\n    var inject = options.inject;\n    if (!inject) { return }\n    var normalized = options.inject = {};\n    //数组的形式\n    if (Array.isArray(inject)) {\n      for (var i = 0; i < inject.length; i++) {\n        // from: 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)\n        normalized[inject[i]] = { from: inject[i] };\n      }\n    } else if (isPlainObject(inject)) {\n      // 对象的处理\n      for (var key in inject) {\n        var val = inject[key];\n        normalized[key] = isPlainObject(val)\n          ? extend({ from: key }, val)\n          : { from: val };\n      }\n    } else {\n      // 非法规则\n      warn(\n        \"Invalid value for option \\\"inject\\\": expected an Array or an Object, \" +\n        \"but got \" + (toRawType(inject)) + \".\",\n        vm\n      );\n    }\n  }\n```\n\n### 1.3.4 directive的规范校验\n我们先看看指令选项的用法，```Vue```允许我们自定义指令，并且它提供了五个钩子函数```bind, inserted, update, componentUpdated, unbind```,具体的用法可以参考[官方-自定义指令](https://cn.vuejs.org/v2/guide/custom-directive.html)文档,而除了可以以对象的形式去定义钩子函数外，官方还提供了一种函数的简写，例如：\n```js\n{\n  directives: {\n    'color-swatch': function(el, binding) {\n        el.style.backgroundColor = binding.value\n    }\n  }\n}\n\n```\n函数的写法会在```bind,update```钩子中触发相同的行为，并且不关心其他钩子。这个行为就是定义的函数。因此在对```directives```进行规范化时，针对函数的写法会将行为赋予```bind,update```钩子。\n```js\nfunction normalizeDirectives (options) {\n    var dirs = options.directives;\n    if (dirs) {\n      for (var key in dirs) {\n        var def###1 = dirs[key];\n        // 函数简写同样会转换成对象的形式\n        if (typeof def###1 === 'function') {\n          dirs[key] = { bind: def###1, update: def###1 };\n        }\n      }\n    }\n  }\n```\n\n### 1.3.5 函数缓存\n这个内容跟选项的规范化无关，当读到上面规范检测的代码时，笔者发现有一段函数优化的代码值得我们学习。它将每次执行函数后的值进行缓存，当再次执行的时候直接调用缓存的数据而不是重复执行函数，以此提高前端性能，这是典型的用空间换时间的优化，也是经典的偏函数应用。\n\n```js\nfunction cached (fn) {\n  var cache = Object.create(null); // 创建空对象作为缓存对象\n  return (function cachedFn (str) {\n    var hit = cache[str];\n    return hit || (cache[str] = fn(str)) // 每次执行时缓存对象有值则不需要执行函数方法，没有则执行并缓存起来\n  })\n}\n\nvar camelizeRE = /-(\\w)/g;\n\n// 缓存会保存每次进行驼峰转换的结果\nvar camelize = cached(function (str) {\n  // 将诸如 'a-b'的写法统一处理成驼峰写法'aB'\n  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })\n});\n\n```\n\n## 1.4 子类构造器\n选项校验介绍完后，在正式进入合并策略之前，还需要先了解一个东西：子类构造器。为什么需要先提到子类构造器呢？\n\n按照前面的知识，```Vue```内部提供了四个默认选项，关键的三个是```components,directives,filter```。那么当我们传递一个选项配置到```Vue```进行初始化，所需要合并的选项好像也仅仅是那关键的三个默认选项而已，那么源码中大篇幅做的选项合并策略又是针对什么场景呢？答案就是这个子类构造器。\n\n**`Vue`提供了一个```Vue.extend```的静态方法，它是基于基础的```Vue```构造器创建一个“子类”，而这个子类所传递的选项配置会和父类的选项配置进行合并。这是选项合并场景的由来。**\n\n因此有不要先了解子类构造器的实现。下面例子中，我们创建了一个```Child```的子类，它继承于父类```Parent```,最终将子类挂载到```#app```元素上。最终获取的```data```便是选项合并后的结果。\n```js\nvar Parent = Vue.extend({\n  data() {\n    test: '父类'，\n    test1: '父类1'\n  }\n})\nvar Child = Parent.extend({\n  data() {\n    test: '子类',\n    test2: '子类1'\n  }\n})\nvar vm = new Child().$mount('#app');\nconsole.log(vm.$data);\n// 结果 \n{\n  test: '子类',\n  test1: '父类1',\n  test2: '子类1'\n}\n```\n\n`Vue.extend`的实现思路很清晰，创建了一个```Sub```的类，这个类的原型指向了父类，并且子类的```options```会和父类的```options```进行合并，```mergeOptions```的其他细节接下来会重点分析。\n```js\nVue.extend = function (extendOptions) {\n  extendOptions = extendOptions || {};\n  var Super = this;\n\n  var name = extendOptions.name || Super.options.name;\n  if (name) {\n    validateComponentName(name); // 校验子类的名称是否符合规范\n  }\n\n  // 创建子类构造器\n  var Sub = function VueComponent (options) {\n    this._init(options);\n  };\n  Sub.prototype = Object.create(Super.prototype); // 子类继承于父类\n  Sub.prototype.constructor = Sub;\n  Sub.cid = cid++;\n  // 子类和父类构造器的配置选项进行合并\n  Sub.options = mergeOptions(\n    Super.options,\n    extendOptions\n  );\n\n  return Sub // 返回子类构造函数\n};\n```\n\n## 1.5 合并策略\n\n合并策略之所以是难点，其中一个是合并选项类型繁多，合并规则随着选项的不同也呈现差异。概括起来思路主要是以下两点：\n1. `Vue`针对每个规定的选项都有定义好的合并策略，例如```data,component,mounted```等。如果合并的子父配置都具有相同的选项，则只需要按照规定好的策略进行选项合并即可。\n2. 由于```Vue```传递的选项是开放式的，所有也存在传递的选项没有自定义选项的情况，这时候由于选项不存在默认的合并策略，所以处理的原则是有子类配置选项则默认使用子类配置选项，没有则选择父类配置选项。\n\n我们通过这两个思想去分析源码的实现，先看看```mergeOptions```除了规范检测后的逻辑。\n```js\nfunction mergeOptions ( parent, child, vm ) {\n  ···\n  var options = {};\n  var key;\n  for (key in parent) {\n    mergeField(key);\n  }\n  for (key in child) {\n    if (!hasOwn(parent, key)) {\n      mergeField(key);\n    }\n  }\n  function mergeField (key) {\n    // 如果有自定义选项策略，则使用自定义选项策略，否则选择使用默认策略。\n    var strat = strats[key] || defaultStrat; \n    options[key] = strat(parent[key], child[key], vm, key);\n  }\n\n  return options\n}\n```\n**两个```for```循环规定了合并的顺序，以自定义选项策略优先，如果没有才会使用默认策略。而```strats```下每个```key```对应的便是每个特殊选项的合并策略**\n\n### 1.5.1 默认策略\n我们可以用丰富的选项去定义实例的行为，大致可以分为以下几类：\n1. 用```data,props,computed```等选项定义实例数据\n1. 用```mounted, created, destoryed```等定义生命周期函数\n1. 用```components```注册组件\n1. 用```methods```选项定义实例方法\n\n当然还有诸如```watch,inject,directives,filter```等选项，总而言之，```Vue```提供的配置项是丰富的。除此之外，我们也可以使用没有默认配置策略的选项，典型的例子是状态管理```Vuex```和配套路由```vue-router```的引入：\n```js\nnew Vue({\n  store, // vuex\n  router// vue-router\n})\n```\n\n不管是插件也好，还是用户自定义的选项，他们的合并策略会遵循思路的第二点：**子配置存在则取子配置，不存在则取父配置，即用子去覆盖父。。**它的描述在```defaultStrat```中。\n\n```js\n// 用户自定义选项策略\nvar defaultStrat = function (parentVal, childVal) {\n  // 子不存在则用父，子存在则用子配置\n  return childVal === undefined\n    ? parentVal\n    : childVal\n};\n```\n\n接下来会进入某些具体的合并策略的分析，大致分为五类：\n\n**1. 常规选项合并**\n\n**2. 自带资源选项合并**\n\n**3. 生命周期钩子合并**\n\n**4. `watch`选项合并**\n\n**5. `props,methods, inject, computed`类似选项合并**\n\n## 1.6 常规选项的合并\n\n### 1.6.1 el的合并\n\n`el`提供一个在页面上已存在的 ```DOM``` 元素作为 ```Vue``` 实例的挂载目标,因此它只在创建```Vue```实例才存在，在子类或者子组件中无法定义```el```选项，因此```el```的合并策略是在保证选项只存在于根的```Vue```实例的情形下使用默认策略进行合并。\n```js\nstrats.el = function (parent, child, vm, key) {\n  if (!vm) {  // 只允许vue实例才拥有el属性，其他子类构造器不允许有el属性\n    warn(\n      \"option \\\"\" + key + \"\\\" can only be used during instance \" +\n      'creation with the `new` keyword.'\n    );\n  }\n  // 默认策略\n  return defaultStrat(parent, child)\n};\n\n```\n\n### 1.6.2 data合并\n常规选项的重点部分是在于```data```的合并，读完这部分源码，可能可以解开你心中的一个疑惑，为什么```data```在```vue```创建实例时传递的是一个对象，而在组件内部定义时只能传递一个函数。\n\n```js\n// data的合并\nstrats.data = function (parentVal, childVal, vm) {\n  // vm代表是否为Vue创建的实例，否则是子父类的关系\n  if (!vm) {\n    if (childVal && typeof childVal !== 'function') { // 必须保证子类的data类型是一个函数而不是一个对象\n      warn('The \"data\" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.',vm);\n      return parentVal\n    }\n    return mergeDataOrFn(parentVal, childVal)\n  }\n  return mergeDataOrFn(parentVal, childVal, vm); // vue实例时需要传递vm作为函数的第三个参数\n};\n```\n`data`策略最终调用的```mergeDataOrFn```方法，区别在于当前```vm```是否是实例，或者是单纯的子父类的关系。如果是子父类的关系，需要对```data```选项进行规范校验，保证它的类型是一个函数而不是对象。\n\n```js\nfunction mergeDataOrFn ( parentVal, childVal, vm ) {\n  // 子父类\n  if (!vm) {\n    if (!childVal) { // 子类不存在data选项，则合并结果为父类data选项\n      return parentVal\n    }\n    if (!parentVal) { // 父类不存在data选项，则合并结果为子类data选项\n      return childVal\n    }\n    return function mergedDataFn () { // data选项在父类和子类同时存在的情况下返回的是一个函数\n      // 子类实例和父类实例，分别将子类和父类实例中data函数执行后返回的对象传递给mergeData函数做数据合并\n      return mergeData(\n        typeof childVal === 'function' ? childVal.call(this, this) : childVal,\n        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal\n      )\n    }\n  } else {\n  // Vue实例\n    // vue构造函数实例对象\n    return function mergedInstanceDataFn () {\n      var instanceData = typeof childVal === 'function'\n        ? childVal.call(vm, vm)\n        : childVal;\n      var defaultData = typeof parentVal === 'function'\n        ? parentVal.call(vm, vm)\n        : parentVal;\n      if (instanceData) {\n        // 当实例中传递data选项时，将实例的data对象和Vm构造函数上的data属性选项合并\n        return mergeData(instanceData, defaultData)\n      } else {\n        // 当实例中不传递data时，默认返回Vm构造函数上的data属性选项\n        return defaultData\n      }\n    }\n  }\n}\n```\n从源码的实现看，```data```的合并不是简单的将两个数据对象进行合并，而是直接返回一个```mergedDataFn```或者```mergedInstanceDataFn```函数，而真正合并的时机是在后续初始化数据响应式系统的环节进行的，初始化数据响应式系统的第一步就是拿到合并后的数据，也就是执行```mergeData```逻辑。\n(关于响应式系统的构建请移步后面的章节)\n\n```js\nfunction mergeData (to, from) {\n  if (!from) { return to }\n  var key, toVal, fromVal;\n  // Reflect.ownKeys可以拿到Symbol属性\n  var keys = hasSymbol\n    ? Reflect.ownKeys(from)\n    : Object.keys(from);\n\n  for (var i = 0; i < keys.length; i++) {\n    key = keys[i];\n    toVal = to[key];\n    fromVal = from[key];\n    if (!hasOwn(to, key)) {\n      // 子的数据父没有，则将新增的数据加入响应式系统中。\n      set(to, key, fromVal); \n    } else if (\n      toVal !== fromVal &&\n      isPlainObject(toVal) &&\n      isPlainObject(fromVal)\n    ) {\n      // 处理深层对象，当合并的数据为多层嵌套对象时，需要递归调用mergeData进行比较合并\n      mergeData(toVal, fromVal);\n    }\n  }\n  return to\n}\n```\n`mergeData`方法的两个参数是父```data```选项和子```data```选项的结果，也就是两个```data```对象，从源码上看数据合并的原则是，将父类的数据整合到子类的数据选项中， 如若父类数据和子类数据冲突时，保留子类数据。如果对象有深层嵌套，则需要递归调用```mergeData```进行数据合并。\n\n\n最后回过头来思考一个问题，为什么```Vue```组件的```data```是一个函数，而不是一个对象呢？\n我觉得可以这样解释：**组件设计的目的是为了复用，每次通过函数创建相当于在一个独立的内存空间中生成一个```data```的副本，这样每个组件之间的数据不会互相影响。**\n\n\n\n\n## 1.7 自带资源选项合并\n在1.2中我们看到了```Vue```默认会带几个选项，分别是```components```组件, ```directive```指令, ```filter```过滤器,所有无论是根实例，还是父子实例，都需要和系统自带的资源选项进行合并。它的定义如下：\n```js\n// 资源选项\nvar ASSET_TYPES = [\n  'component',\n  'directive',\n  'filter'\n];\n\n// 定义资源合并的策略\nASSET_TYPES.forEach(function (type) {\n  strats[type + 's'] = mergeAssets; // 定义默认策略\n});\n\n```\n这些资源选项的合并逻辑很简单，首先会创建一个原型指向父类资源选项的空对象，再将子类选项赋值给空对象。\n\n```js\n// 资源选项自定义合并策略\nfunction mergeAssets (parentVal,childVal,vm,key) {\n  var res = Object.create(parentVal || null); // 创建一个空对象，其原型指向父类的资源选项。\n  if (childVal) {\n    assertObjectType(key, childVal, vm); // components,filters,directives选项必须为对象\n    return extend(res, childVal) // 子类选项赋值给空对象\n  } else {\n    return res\n  }\n}\n```\n结合下面的例子，我们看具体合并后的结果：\n\n```js\nvar vm = new Vue({\n  components: {\n    componentA: {}\n  },\n  directives: {\n    'v-boom': {}\n  }\n})\n\nconsole.log(vm.$options.components)\n// 根实例的选项和资源默认选项合并后的结果\n{\n  components: {\n    componentA: {},\n    __proto__: {\n      KeepAlive: {}\n      Transition: {}\n      TransitionGroup: {}\n    } \n  },\n  directives: {\n    'v-boom': {},\n    __proto__: {\n      'v-show': {},\n      'v-model': {}\n    }\n  }\n}\n\n```\n简单总结一下，对于 ```directives、filters``` 以及 ```components``` 等资源选项，父类选项将以原型链的形式被处理。子类必须通过原型链才能查找并使用内置组件和内置指令。\n\n\n## 1.8 生命周期钩子函数的合并\n在学习```Vue```时，有一个重要的思想，生命周期。它是我们使用```Vue```高效开发组件的基础，我们可以在组件实例的不同阶段去定义需要执行的函数，让组件的功能更加丰富。在介绍生命周期钩子函数的选项合并前，我们有必要复习以下官方的生命周期图。\n\n![](./img/1.1.png)\n\n然而从源码中我们可以看到```Vue```的生命周期钩子不止这些，它有多达12个之多，每个钩子的执行时机我们暂且不深究，它们会在以后的章节中逐一出现。我们关心的是：子父组件的生命周期钩子函数是遵循什么样的规则合并。\n\n```js\nvar LIFECYCLE_HOOKS = [\n  'beforeCreate',\n  'created',\n  'beforeMount',\n  'mounted',\n  'beforeUpdate',\n  'updated',\n  'beforeDestroy',\n  'destroyed',\n  'activated',\n  'deactivated',\n  'errorCaptured',\n  'serverPrefetch'\n];\nLIFECYCLE_HOOKS.forEach(function (hook) {\n  strats[hook] = mergeHook; // 对生命周期钩子选项的合并都执行mergeHook策略\n});\n```\n`mergeHook`是生命周期钩子合并的策略，简单的对代码进行总结，钩子函数的合并原则是：\n1. 如果子类和父类都拥有相同钩子选项，则将子类选项和父类选项合并。\n2. 如果父类不存在钩子选项，子类存在时，则以数组形式返回子类钩子选项。\n3. 当子类不存在钩子选项时，则以父类选项返回。\n4. 子父合并时，是将子类选项放在数组的末尾，这样在执行钩子时，永远是父类选项优先于子类选项执行。\n\n```js\n// 生命周期钩子选项合并策略\nfunction mergeHook (\n    parentVal,\n    childVal\n  ) {\n    // 1.如果子类和父类都拥有钩子选项，则将子类选项和父类选项合并, \n    // 2.如果父类不存在钩子选项，子类存在时，则以数组形式返回子类钩子选项，\n    // 3.当子类不存在钩子选项时，则以父类选项返回。\n    var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal; \n    return res\n      ? dedupeHooks(res)\n      : res\n  }\n// 防止多个组件实例钩子选项相互影响\n  function dedupeHooks (hooks) {\n    var res = [];\n    for (var i = 0; i < hooks.length; i++) {\n      if (res.indexOf(hooks[i]) === -1) {\n        res.push(hooks[i]);\n      }\n    }\n    return res\n  }\n```\n\n下面结合具体的例子看合并结果。\n```js\nvar Parent = Vue.extend({\n  mounted() {\n    console.log('parent')\n  }\n})\nvar Child = Parent.extend({\n  mounted() {\n    console.log('child')\n  }\n})\nvar vm = new Child().$mount('#app');\n\n// 输出结果：\nparent\nchild\n```\n\n简单总结一下：**对于生命周期钩子选项，子类和父类相同的选项将合并成数组，这样在执行子类钩子函数时，父类钩子选项也会执行，并且父会优先于子执行。**\n\n## 1.9 watch选项合并\n在使用```Vue```进行开发时，我们有时需要自定义侦听器来响应数据的变化，当需要在数据变化时执行异步或者开销较大的操作时，```watch```往往是高效的。对于 ```watch``` 选项的合并处理，它类似于生命周期钩子，只要父选项有相同的观测字段，则和子的选项合并为数组，在监测字段改变时同时执行父类选项的监听代码。处理方式和生命钩子选项的区别在于，生命周期钩子选项必须是函数，而```watch```选项最终在合并的数组中可以是包含选项的对象，也可以是对应的回调函数，或者方法名。\n\n```js\nstrats.watch = function (parentVal,childVal,vm,key) {\n    //火狐浏览器在Object的原型上拥有watch方法，这里对这一现象做了兼容\n    // var nativeWatch = ({}).watch;\n    if (parentVal === nativeWatch) { parentVal = undefined; }\n    if (childVal === nativeWatch) { childVal = undefined; }\n    // 没有子，则默认用父选项\n    if (!childVal) { return Object.create(parentVal || null) }\n    {\n      // 保证watch选项是一个对象\n      assertObjectType(key, childVal, vm);\n    }\n    // 没有父则直接用子选项\n    if (!parentVal) { return childVal }\n    var ret = {};\n    extend(ret, parentVal);\n    for (var key$1 in childVal) {\n      var parent = ret[key$1];\n      var child = childVal[key$1];\n      // 父的选项先转换成数组\n      if (parent && !Array.isArray(parent)) {\n        parent = [parent];\n      }\n      ret[key$1] = parent\n        ? parent.concat(child)\n        : Array.isArray(child) ? child : [child];\n    }\n    return ret\n  };\n```\n\n下面结合具体的例子看合并结果：\n```js\nvar Parent = Vue.extend({\n  watch: {\n    'test': function() {\n      console.log('parent change')\n    }\n  }\n})\nvar Child = Parent.extend({\n  watch: {\n    'test': {\n      handler: function() {\n        console.log('child change')\n      }\n    }\n  },\n  data() {\n    return {\n      test: 1\n    }\n  }\n})\nvar vm = new Child().$mount('#app');\nvm.test = 2;\n// 输出结果\nparent change\nchild change\n```\n\n\n简单总结一下：**对于watch选项的合并，最终和父类选项合并成数组，并且数组的选项成员，可以是回调函数，选项对象，或者函数名。**\n\n\n\n## 1.10 props methods inject computed合并\n\n\n源码的设计将```props.methods,inject,computed```归结为一类，他们的配置策略一致，简单概括就是，如果父类不存在选项，则返回子类选项，子类父类都存在时，用子类选项去覆盖父类选项。\n```js\n// 其他选项合并策略\nstrats.props =\nstrats.methods =\nstrats.inject =\nstrats.computed = function (parentVal,childVal,vm,key) {\n  if (childVal && \"development\" !== 'production') {\n    assertObjectType(key, childVal, vm);\n  }\n  if (!parentVal) { return childVal } // 父类不存在该选项，则返回子类的选项\n  var ret = Object.create(null);\n  extend(ret, parentVal); // \n  if (childVal) { \n    // 子类选项会覆盖父类选项的值\n    extend(ret, childVal); } \n  return ret\n};\n\n```\n## 1.11 小结\n至此，五类选项合并的策略分析到此结束，回顾一下这一章节的内容，这一节是```Vue```源码分析的起手式，所以我们从```Vue```的引入出发，先大致了解了```Vue```在代码引入阶段做的操作，主要是对静态属性方法和原型上属性方法的定义和声明，这里并不需要精确了解到每个方法的功能和实现细节，当然我也相信你已经在实战中或多或少接触过这些方法的使用。接下来到文章的重点，```new Vue```是我们正确使用```Vue```进行开发的关键，而实例化阶段会对调用```_init```方法进行初始化，选项合并是初始化的第一步。选项合并会对系统内部定义的选项和子父类的选项进行合并。而```Vue```有相当丰富的选项合并策略，不管是内部的选项还是用户自定义的选项，他们都遵循内部约定好的合并策略。有了丰富的选项和严格的合并策略，```Vue```在指导开发上才显得更加完备。下一节会分析一个重要的概念，数据代理，它也是响应式系统的基础。"
  },
  {
    "path": "src/你真的了解v-model的语法糖了吗.md",
    "content": "> 双向数据绑定这个概念或者大家并不陌生，视图影响数据，数据同样影响视图，两者间有双向依赖的关系。在响应式系统构建的上，中，下篇我已经对数据影响视图的原理详细阐述清楚了。而如何完成视图影响数据这一关联？这就是本节讨论的重点：指令```v-model```。\n\n由于```v-model```和前面介绍的插槽，事件一致，都属于vue提供的指令，所以我们对```v-model```的分析方式和以往大同小异。分析会围绕模板的编译，```render```函数的生成，到最后真实节点的挂载顺序执行。最终我们依然会得到一个结论，**v-model无论什么使用场景，本质上都是一个语法糖**。\n\n\n## 11.1 表单绑定\n### 11.1.1 基础使用\n`v-model`和表单脱离不了关系，之所以视图能影响数据，本质上这个视图需要可交互的，因此表单是实现这一交互的前提。表单的使用以```<input > <textarea> <select>```为核心，更细的划分结合```v-model```的使用如下：\n```html\n// 普通输入框\n<input type=\"text\" v-model=\"value1\">\n\n// 多行文本框\n<textarea v-model=\"value2\" cols=\"30\" rows=\"10\"></textarea>\n\n// 单选框\n<div class=\"group\">\n  <input type=\"radio\" value=\"one\" v-model=\"value3\"> one\n  <input type=\"radio\" value=\"two\" v-model=\"value3\"> two\n</div> \n\n// 原生单选框的写法 注：原生单选框的写法需要通过name绑定一组单选，两个radio的name属性相同，才能表现为互斥\n<div class=\"group\">\n  <input type=\"radio\" name=\"number\" value=\"one\">one\n  <input type=\"radio\" name=\"number\" value=\"two\">two\n</div>\n\n\n// 多选框  (原始值： value4: [])\n<div class=\"group\">\n  <input type=\"checkbox\" value=\"jack\" v-model=\"value4\">jack\n  <input type=\"checkbox\" value=\"lili\" v-model=\"value4\">lili\n</div>\n\n// 下拉选项\n<select name=\"\" id=\"\" v-model=\"value5\">\n  <option value=\"apple\">apple</option>\n  <option value=\"banana\">banana</option>\n  <option value=\"bear\">bear</option>\n</select>\n\n```\n接下来的分析，我们以普通输入框为例\n```js\n<div id=\"app\">\n  <input type=\"text\" v-model=\"value1\">\n</div>\n\nnew Vue({\n  el: '#app',\n  data() {\n    return {\n      value1: ''\n    }\n  }\n})\n```\n进入正文前先回顾一下模板到真实节点的过程。\n1. 模板解析成```AST```树;\n2. ```AST```树生成可执行的```render```函数;\n3. ```render```函数转换为```Vnode```对象;\n4. 根据```Vnode```对象生成真实的```Dom```节点。\n\n接下来，我们先看看模板解析为```AST```树的过程。\n\n### 11.1.2 AST树的解析\n模板的编译阶段，会调用```var ast = parse(template.trim(), options)```生成```AST```树，```parse```函数的其他细节这里不展开分析，前面的文章或多或少都涉及过，我们还是把关注点放在模板属性上的解析，也就是```processAttrs```函数上。\n\n使用过```vue```写模板的都知道，```vue```模板属性由两部分组成，一部分是指令，另一部分是普通```html```标签属性。z这也是属性处理的两大分支。而在指令的细分领域，又将```v-on，v-bind```做特殊的处理，其他的普通分支会执行```addDirective```过程。\n```js\n// 处理模板属性\nfunction processAttrs(el) {\n  var list = el.attrsList;\n  var i, l, name, rawName, value, modifiers, syncGen, isDynamic;\n  for (i = 0, l = list.length; i < l; i++) {\n    name = rawName = list[i].name; // v-on:click\n    value = list[i].value; // doThis\n    if (dirRE.test(name)) { // 1.针对指令的属性处理\n      ···\n      if (bindRE.test(name)) { // v-bind分支\n        ···\n      } else if(onRE.test(name)) { // v-on分支\n        ···\n      } else { // 除了v-bind，v-on之外的普通指令\n        ···\n        // 普通指令会在AST树上添加directives属性\n        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]);\n        if (name === 'model') {\n          checkForAliasModel(el, value);\n        }\n      }\n    } else {\n      // 2. 普通html标签属性\n    }\n\n  }\n}\n```\n在揭秘事件机制这一节，我们介绍了```AST```产生阶段对事件指令```v-on```的处理是为```AST```树添加```events```属性。类似的，普通指令会在```AST```树上添加```directives```属性，具体看```addDirective```函数。\n\n```js\n// 添加directives属性\nfunction addDirective (el,name,rawName,value,arg,isDynamicArg,modifiers,range) {\n    (el.directives || (el.directives = [])).push(rangeSetItem({\n      name: name,\n      rawName: rawName,\n      value: value,\n      arg: arg,\n      isDynamicArg: isDynamicArg,\n      modifiers: modifiers\n    }, range));\n    el.plain = false;\n  }\n```\n最终```AST```树多了一个属性对象，其中```modifiers```代表模板中添加的修饰符，如：```.lazy, .number, .trim```。\n```js\n// AST\n{\n  directives: {\n    {\n      rawName: 'v-model',\n      value: 'value',\n      name: 'v-model',\n      modifiers: undefined\n    }\n  }\n}\n```\n### 11.1.3 render函数生成\n\n`render`函数生成阶段，也就是前面分析了数次的```generate```逻辑，其中```genData```会对模板的诸多属性进行处理,最终返回拼接好的字符串模板，而对指令的处理会进入```genDirectives```流程。\n```js\nfunction genData(el, state) {\n  var data = '{';\n  // 指令的处理\n  var dirs = genDirectives(el, state);\n  ··· // 其他属性，指令的处理\n  // 针对组件的v-model处理，放到后面分析\n  if (el.model) {\n    data += \"model:{value:\" + (el.model.value) + \",callback:\" + (el.model.callback) + \",expression:\" + (el.model.expression) + \"},\";\n  }\n  return data\n}\n```\n`genDirectives`逻辑并不复杂,他会拿到之前```AST```树中保留的```directives```对象，并遍历解析指令对象，最终以```'directives:['```包裹的字符串返回。\n```js\n// directives render字符串的生成\n  function genDirectives (el, state) {\n    // 拿到指令对象\n    var dirs = el.directives;\n    if (!dirs) { return }\n    // 字符串拼接\n    var res = 'directives:[';\n    var hasRuntime = false;\n    var i, l, dir, needRuntime;\n    for (i = 0, l = dirs.length; i < l; i++) {\n      dir = dirs[i];\n      needRuntime = true;\n      // 对指令ast树的重新处理\n      var gen = state.directives[dir.name];\n      if (gen) {\n        // compile-time directive that manipulates AST.\n        // returns true if it also needs a runtime counterpart.\n        needRuntime = !!gen(el, dir, state.warn);\n      }\n      if (needRuntime) {\n        hasRuntime = true;\n        res += \"{name:\\\"\" + (dir.name) + \"\\\",rawName:\\\"\" + (dir.rawName) + \"\\\"\" + (dir.value ? (\",value:(\" + (dir.value) + \"),expression:\" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (\",arg:\" + (dir.isDynamicArg ? dir.arg : (\"\\\"\" + (dir.arg) + \"\\\"\"))) : '') + (dir.modifiers ? (\",modifiers:\" + (JSON.stringify(dir.modifiers))) : '') + \"},\";\n      }\n    }\n    if (hasRuntime) {\n      return res.slice(0, -1) + ']'\n    }\n  }\n```\n这里有一句关键的代码```var gen = state.directives[dir.name]```,为了了解其来龙去脉，我们回到Vue源码中的编译流程，在以往的文章中，我们完整的介绍过```template```模板的编译流程,这一部分的设计是非常复杂且巧妙的，其中大量运用了偏函数的思想，即分离了不同平台不同的编译过程，也为同一个平台每次提供相同的配置选项进行了合并处理，并很好的将配置进行了缓存。其中针对浏览器端有三个重要的指令选项。\n```js\nvar directive$1 = {\n  model: model,\n  text: text,\n  html, html\n}\nvar baseOptions = {\n  ···\n  // 指令选项\n  directives: directives$1,\n};\n// 编译时传入选项配置\ncreateCompiler(baseOptions)\n```\n而这个```state.directives['model']```也就是对应的```model```函数，所以我们先把焦点聚焦在```model```函数的逻辑。\n```js\nfunction model (el,dir,_warn) {\n    warn$1 = _warn;\n    // 绑定的值\n    var value = dir.value;\n    var modifiers = dir.modifiers;\n    var tag = el.tag;\n    var type = el.attrsMap.type;\n    {\n      // 这里遇到type是file的html，如果还使用双向绑定会报出警告。\n      // 因为File inputs是只读的\n      if (tag === 'input' && type === 'file') {\n        warn$1(\n          \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\" type=\\\"file\\\">:\\n\" +\n          \"File inputs are read only. Use a v-on:change listener instead.\",\n          el.rawAttrsMap['v-model']\n        );\n      }\n    }\n    //组件上v-model的处理\n    if (el.component) {\n      genComponentModel(el, value, modifiers);\n      // component v-model doesn't need extra runtime\n      return false\n    } else if (tag === 'select') {\n      // select表单\n      genSelect(el, value, modifiers);\n    } else if (tag === 'input' && type === 'checkbox') {\n      // checkbox表单\n      genCheckboxModel(el, value, modifiers);\n    } else if (tag === 'input' && type === 'radio') {\n      // radio表单\n      genRadioModel(el, value, modifiers);\n    } else if (tag === 'input' || tag === 'textarea') {\n      // 普通input，如 text, textarea\n      genDefaultModel(el, value, modifiers);\n    } else if (!config.isReservedTag(tag)) {\n      genComponentModel(el, value, modifiers);\n      // component v-model doesn't need extra runtime\n      return false\n    } else {\n      // 如果不是表单使用v-model，同样会报出警告，双向绑定只针对表单控件。\n      warn$1(\n        \"<\" + (el.tag) + \" v-model=\\\"\" + value + \"\\\">: \" +\n        \"v-model is not supported on this element type. \" +\n        'If you are working with contenteditable, it\\'s recommended to ' +\n        'wrap a library dedicated for that purpose inside a custom component.',\n        el.rawAttrsMap['v-model']\n      );\n    }\n    // ensure runtime directive metadata\n    // \n    return true\n  }\n```\n显然，**```model```会对表单控件的```AST```树做进一步的处理**，在上面的基础用法中，我们知道**表单有不同的类型，每种类型对应的事件处理响应机制也不同**。因此我们需要针对不同的表单控件生成不同的```render```函数，因此需要产生不同的```AST```属性。```model```针对不同类型的表单控件有不同的处理分支。我们重点分析普通```input```标签的处理，```genDefaultModel```分支，其他类型的分支，可以仿照下面的分析过程。\n\n\n```js\nfunction genDefaultModel (el,value,modifiers) {\n    var type = el.attrsMap.type;\n\n    // v-model和v-bind值相同值，有冲突会报错\n    {\n      var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];\n      var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];\n      if (value$1 && !typeBinding) {\n        var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';\n        warn$1(\n          binding + \"=\\\"\" + value$1 + \"\\\" conflicts with v-model on the same element \" +\n          'because the latter already expands to a value binding internally',\n          el.rawAttrsMap[binding]\n        );\n      }\n    }\n    // modifiers存贮的是v-model的修饰符。\n    var ref = modifiers || {};\n    // lazy,trim,number是可供v-model使用的修饰符\n    var lazy = ref.lazy;\n    var number = ref.number;\n    var trim = ref.trim;\n    var needCompositionGuard = !lazy && type !== 'range';\n    // lazy修饰符将触发同步的事件从input改为change\n    var event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input';\n\n    var valueExpression = '$event.target.value';\n    // 过滤用户输入的首尾空白符\n    if (trim) {\n      valueExpression = \"$event.target.value.trim()\";\n    }\n    // 将用户输入转为数值类型\n    if (number) {\n      valueExpression = \"_n(\" + valueExpression + \")\";\n    }\n    // genAssignmentCode函数是为了处理v-model的格式，允许使用以下的形式： v-model=\"a.b\" v-model=\"a[b]\"\n    var code = genAssignmentCode(value, valueExpression);\n    if (needCompositionGuard) {\n      //  保证了不会在输入法组合文字过程中得到更新\n      code = \"if($event.target.composing)return;\" + code;\n    }\n    //  添加value属性\n    addProp(el, 'value', (\"(\" + value + \")\"));\n    // 绑定事件\n    addHandler(el, event, code, null, true);\n    if (trim || number) {\n      addHandler(el, 'blur', '$forceUpdate()');\n    }\n  }\n\nfunction genAssignmentCode (value,assignment) {\n  // 处理v-model的格式，v-model=\"a.b\" v-model=\"a[b]\"\n  var res = parseModel(value);\n  if (res.key === null) {\n    // 普通情形\n    return (value + \"=\" + assignment)\n  } else {\n    // 对象形式\n    return (\"$set(\" + (res.exp) + \", \" + (res.key) + \", \" + assignment + \")\")\n  }\n}\n```\n`genDefaultModel`的逻辑有两部分，**一部分是针对修饰符产生不同的事件处理字符串，二是为```v-model```产生的```AST```树添加属性和事件相关的属性**。其中最重要的两行代码是\n```js\n//  添加value属性\naddProp(el, 'value', (\"(\" + value + \")\"));\n// 绑定事件属性\naddHandler(el, event, code, null, true);\n```\n`addHandler`在之前介绍事件时分析过，他会为```AST```树添加事件相关的属性,同样的```addProp```也会为```AST```树添加```props```属性。最终```AST```树新增了两个属性：\n\n![](./img/11.1.png)\n\n\n回到```genData```,通过```genDirectives```处理后，原先的```AST```树新增了两个属性，因此在字符串生成阶段同样需要处理```props```和```events```的分支。\n```js\nfunction genData$2 (el, state) {\n  var data = '{';\n  // 已经分析过的genDirectives\n  var dirs = genDirectives(el, state);\n  // 处理props\n  if (el.props) {\n    data += \"domProps:\" + (genProps(el.props)) + \",\";\n  }\n  // 处理事件\n  if (el.events) {\n    data += (genHandlers(el.events, false)) + \",\";\n  }\n}\n```\n最终```render```函数的结果为：\n\n```js\n\"_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(message),expression:\"message\"}],attrs:{\"type\":\"text\"},domProps:{\"value\":(message)},on:{\"input\":function($event){if($event.target.composing)return;message=$event.target.value}}})\"\n\n```\n\n```js\n<input type=\"text\" v-model=\"value\">\n```\n\n如果觉得上面的流程分析啰嗦，可以直接看下面的结论，对比模板和生成的```render```函数,我们可以得到：\n\n1. `input`标签所有属性，包括指令相关的内容都是以```data```属性的形式作为参数的整体传入```_c(即：createElement)```函数。\n2. `input type`的类型，在```data```属性中，以```attrs```键值对存在。\n3. `v-model`会有对应的```directives```属性描述指令的相关信息。\n4. **为什么说```v-model```是一个语法糖，从```render```函数的最终结果可以看出，它最终以两部分形式存在于```input```标签中，一个是将```value1```以```props```的形式存在(```domProps```)中，另一个是以事件的形式存储```input```事件，并保留在```on```属性中。**\n5. 重要的一个关键，事件用```$event.target.composing```属性来保证不会在输入法组合文字过程中更新数据,这点我们后面会再次提到。\n\n\n### 11.1.4 patch真实节点\n\n在```patch```之前还有一个生成```vnode```的过程，这个过程没有什么特别之处，所有的包括指令，属性会以```data```属性的形式传递到构造函数```Vnode```中，最终的```Vnode```拥有```directives,domProps,on```属性：\n\n![](./img/11.2.png)\n\n有了```Vnode```之后紧接着会执行```patchVnode```,```patchVnode```过程是一个真实节点创建的过程，其中的关键是```createElm```方法，这个方法我们在不同的场合也分析过，前面的源码得到指令相关的信息也会保留在```vnode```的```data```属性里，所以对属性的处理也会走```invokeCreateHooks```逻辑。\n\n```js\nfunction createElm() {\n  ···\n  // 针对指令的处理\n   if (isDef(data)) {\n      invokeCreateHooks(vnode, insertedVnodeQueue);\n    }\n}\n```\n`invokeCreateHooks`会调用定义好的钩子函数，对```vnode```上定义的属性，指令，事件等进行真实DOM的处理，步骤包括以下(不包含全部)：\n1. `updateDOMProps`会利用```vnode data```上的```domProps```更新```input```标签的```value```值;\n2. `updateAttrs`会利用```vnode data```上的```attrs```属性更新节点的属性值;\n3. `updateDomListeners`利用```vnode data```上的```on```属性添加事件监听。\n\n**因此```v-model```语法糖最终反应的结果，是通过监听表单控件自身的```input```事件(其他类型有不同的监听事件类型)，去影响自身的```value```值**。如果没有```v-model```的语法糖，我们可以这样写：\n`<input type=\"text\" :value=\"message\" @input=\"(e) => { this.message = e.target.value }\" >`\n\n\n### 11.1.5 语法糖的背后\n\n**然而```v-model```仅仅是起到合并语法，创建一个新的语法糖的意义吗？**\n**显然答案是否定的，对于需要使用输入法 (如中文、日文、韩文等) 的语言，你会发现 ```v-model``` 不会在输入法组合文字过程中得到更新。**这就是```v-model```的一个重要的特点。它会在事件处理这一层添加新的事件监听```compositionstart,compositionend```，他们会分别在语言输入的开始和结束时监听到变化，只要借助```$event.target.composing```，就可以设计出只会在输入法组合文字的结束阶段才更新数据，这有利于提高用户的使用体验。这一部分我想借助脱离框架的表单来帮助理解。\n\n\n脱离框架的一个视图响应数据的实现（效果类似于v-model）：\n```js\n// html\n<input type=\"text\" id=\"inputValue\">\n<span id=\"showValue\"></span>\n\n// js\n\n<script>\n    let input = document.getElementById('inputValue');\n    let show = document.getElementById('showValue');\n    input.value = 123;\n    show.innerText = input.value\n\n    function onCompositionStart(e) {\n      e.target.composing = true;\n    }\n\n    function onCompositionEnd(e) {\n      if (!e.target.composing) {\n        return\n      }\n      e.target.composing = false;\n      show.innerText = e.target.value\n    }\n    function onInputChange(e) {\n      // e.target.composing表示是否还在输入中\n      if(e.target.composing)return;\n      show.innerText = e.target.value\n    }\n    input.addEventListener('input', onInputChange)\n    input.addEventListener('compositionstart', onCompositionStart)// 组合输入开始\n    input.addEventListener('compositionend', onCompositionEnd) // 组合输入结束\n</script>\n```\n\n\n## 11.2 组件使用v-model\n最后我们简单说说在父组件中使用```v-model```,可以先看结论，**组件上使用```v-model```本质上是子父组件通信的语法糖**。先看一个简单的使用例子。\n\n```js\n var child = {\n    template: '<div><input type=\"text\" :value=\"value\" @input=\"emitEvent\">{{value}}</div>',\n    methods: {\n      emitEvent(e) {\n        this.$emit('input', e.target.value)\n      }\n    },\n    props: ['value']\n  }\n new Vue({\n   data() {\n     return {\n       message: 'test'\n     }\n   },\n   components: {\n     child\n   },\n   template: '<div id=\"app\"><child v-model=\"message\"></child></div>',\n   el: '#app'\n })\n```\n父组件上使用```v-model```, 子组件默认会利用名为 ```value``` 的 ```prop``` 和名为 ```input``` 的事件，当然像```select```表单会以其他默认事件的形式存在。分析源码的过程也大致类似，这里只列举几个特别的地方。\n\n`AST`生成阶段和普通表单控件的区别在于，当遇到```child```时，由于不是普通的```html```标签，会执行```getComponentModel```的过程,而```getComponentModel```的结果是在```AST```树上添加```model```的属性。\n```js\nfunction model() {\n  if (!config.isReservedTag(tag)) {\n    genComponentModel(el, value, modifiers);\n  }\n}\n\nfunction genComponentModel (el,value,modifiers) {\n    var ref = modifiers || {};\n    var number = ref.number;\n    var trim = ref.trim;\n\n    var baseValueExpression = '$$v';\n    var valueExpression = baseValueExpression;\n    if (trim) {\n      valueExpression =\n        \"(typeof \" + baseValueExpression + \" === 'string'\" +\n        \"? \" + baseValueExpression + \".trim()\" +\n        \": \" + baseValueExpression + \")\";\n    }\n    if (number) {\n      valueExpression = \"_n(\" + valueExpression + \")\";\n    }\n    var assignment = genAssignmentCode(value, valueExpression);\n    // 在ast树上添加model属性，其中有value，expression，callback属性\n    el.model = {\n      value: (\"(\" + value + \")\"),\n      expression: JSON.stringify(value),\n      callback: (\"function (\" + baseValueExpression + \") {\" + assignment + \"}\")\n    };\n  }\n```\n最终```AST```树的结果：\n```js\n{\n  model: {\n    callback: \"function ($$v) {message=$$v}\"\n    expression: \"\"message\"\"\n    value: \"(message)\"\n  }\n}\n```\n经过对```AST```树的处理后，回到```genData$2```的流程，由于有了```model```属性，父组件拼接的字符串会做进一步处理。\n```js\nfunction genData$2 (el, state) { \n  var data = '{';\n  var dirs = genDirectives(el, state);\n  ···\n  // v-model组件的render函数处理\n  if (el.model) {\n    data += \"model:{value:\" + (el.model.value) + \",callback:\" + (el.model.callback) + \",expression:\" + (el.model.expression) + \"},\";\n  }\n  ···\n  return data\n}\n```\n因此，父组件最终的```render```函数表现为：\n```js\n\"_c('child',{model:{value:(message),callback:function ($$v) {message=$$v},expression:\"message\"}})\"\n```\n\n子组件的创建阶段照例会执行```createComponent ```，其中针对```model```的逻辑需要特别说明。\n\n```js\nfunction createComponent() {\n  // transform component v-model data into props & events\n  if (isDef(data.model)) {\n    // 处理父组件的v-model指令对象\n    transformModel(Ctor.options, data);\n  }\n}\n```\n\n```js\nfunction transformModel (options, data) {\n  // prop默认取的是value，除非配置上有model的选项\n  var prop = (options.model && options.model.prop) || 'value';\n\n  // event默认取的是input，除非配置上有model的选项\n  var event = (options.model && options.model.event) || 'input'\n  // vnode上新增props的属性，值为value\n  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value;\n\n  // vnode上新增on属性，标记事件\n  var on = data.on || (data.on = {});\n  var existing = on[event];\n  var callback = data.model.callback;\n  if (isDef(existing)) {\n    if (\n      Array.isArray(existing)\n        ? existing.indexOf(callback) === -1\n        : existing !== callback\n    ) {\n      on[event] = [callback].concat(existing);\n    }\n  } else {\n    on[event] = callback;\n  }\n}\n```\n\n从```transformModel```的逻辑可以看出，子组件```vnode```会为```data.props``` 添加 ```data.model.value```，并且给```data.on``` 添加```data.model.callback```。因此父组件```v-model```语法糖本质上可以修改为\n```'<child :value=\"message\" @input=\"function(e){message = e}\"></child>' ```\n\n\n**显然，这种写法就是事件通信的写法，这个过程又回到对事件指令的分析过程了。因此我们可以很明显的意识到，组件使用```v-model```本质上还是一个子父组件通信的语法糖。**\n"
  },
  {
    "path": "src/动态组件的深入分析.md",
    "content": "> 前面花了两节的内容介绍了组件，从组件的原理讲到组件的应用，包括异步组件和函数式组件的实现和使用场景。众所周知，组件是贯穿整个Vue设计理念的东西，并且也是指导我们开发的核心思想，所以接下来的几篇文章，将重新回到组件的内容去做源码分析，首先会从常用的动态组件开始，包括内联模板的原理，最后会简单的提到内置组件的概念，为之后的文章埋下伏笔。\n\n## 12.1 动态组件\n动态组件我相信大部分在开发的过程中都会用到，当我们需要在不同的组件之间进行状态切换时，动态组件可以很好的满足我们的需求，其中的核心是```component```标签和```is```属性的使用。\n\n### 12.1.1 基本用法\n例子是一个动态组件的基本使用场景，当点击按钮时，视图根据```this.chooseTabs```值在组件```child1,child2,child3```间切换。\n```html\n// vue\n<div id=\"app\">\n  <button @click=\"changeTabs('child1')\">child1</button>\n  <button @click=\"changeTabs('child2')\">child2</button>\n  <button @click=\"changeTabs('child3')\">child3</button>\n  <component :is=\"chooseTabs\">\n  </component>\n</div>\n```\n```js\n// js\nvar child1 = {\n  template: '<div>content1</div>',\n}\nvar child2 = {\n  template: '<div>content2</div>'\n}\nvar child3 = {\n  template: '<div>content3</div>'\n}\nvar vm = new Vue({\n  el: '#app',\n  components: {\n    child1,\n    child2,\n    child3\n  },\n  methods: {\n    changeTabs(tab) {\n      this.chooseTabs = tab;\n    }\n  }\n})\n```\n### 12.1.2 AST解析\n`<component>`的解读和前面几篇内容一致，会从```AST```解析阶段说起，过程也不会专注每一个细节，而是把和以往处理方式不同的地方特别说明。针对动态组件解析的差异，集中在```processComponent```上，由于**标签上```is```属性的存在，它会在最终的```ast```树上打上```component```属性的标志。**\n```js\n//  针对动态组件的解析\nfunction processComponent (el) {\n  var binding;\n  // 拿到is属性所对应的值\n  if ((binding = getBindingAttr(el, 'is'))) {\n    // ast树上多了component的属性\n    el.component = binding;\n  }\n  if (getAndRemoveAttr(el, 'inline-template') != null) {\n    el.inlineTemplate = true;\n  }\n}\n```\n\n最终的```ast```树如下：\n\n![](./img/12.1.png)\n\n\n\n### 12.1.3 render函数\n有了```ast```树，接下来是根据```ast```树生成可执行的```render```函数，由于有```component```属性，```render```函数的产生过程会走```genComponent```分支。\n```js\n// render函数生成函数\nvar code = generate(ast, options);\n\n// generate函数的实现\nfunction generate (ast,options) {\n  var state = new CodegenState(options);\n  var code = ast ? genElement(ast, state) : '_c(\"div\")';\n  return {\n    render: (\"with(this){return \" + code + \"}\"),\n    staticRenderFns: state.staticRenderFns\n  }\n}\n\nfunction genElement(el, state) {\n  ···\n  var code;\n  // 动态组件分支\n  if (el.component) {\n    code = genComponent(el.component, el, state);\n  }\n}\n\n```\n针对动态组件的处理逻辑其实很简单，当没有内联模板标志时(后面会讲),拿到后续的子节点进行拼接，和普通组件唯一的区别在于，```_c```的第一个参数不再是一个指定的字符串，而是一个代表组件的变量。\n```js\n// 针对动态组件的处理\n  function genComponent (\n    componentName,\n    el,\n    state\n  ) {\n    // 拥有inlineTemplate属性时，children为null\n    var children = el.inlineTemplate ? null : genChildren(el, state, true);\n    return (\"_c(\" + componentName + \",\" + (genData$2(el, state)) + (children ? (\",\" + children) : '') + \")\")\n  }\n```\n\n\n### 12.1.4 普通组件和动态组件的对比\n\n其实我们可以对比普通组件和动态组件在```render```函数上的区别，结果一目了然。\n\n#### 普通组件的render函数\n\n```js\n\"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('child1',[_v(_s(test))])],1)}\"\n```\n\n#### 动态组件的render函数\n\n```js\n\"with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c(chooseTabs,{tag:\"component\"})],1)}\"\n```\n\n\n\n简单的总结，动态组件和普通组件的区别在于：\n\n1. `ast`阶段新增了```component```属性，这是动态组件的标志\n2. 产生```render```函数阶段由于```component```属性的存在，会执行```genComponent```分支，```genComponent```会针对动态组件的执行函数进行特殊的处理，和普通组件不同的是，```_c```的第一个参数不再是不变的字符串，而是指定的组件名变量。\n3. `render`到```vnode```阶段和普通组件的流程相同，只是字符串换成了变量，并有```{ tag: 'component' }```的```data```属性。例子中```chooseTabs```此时取的是```child1```。\n\n**有了```render```函数，接下来从vnode到真实节点的过程和普通组件在流程和思路上基本一致，这一阶段可以回顾之前介绍组件流程的分析**\n\n\n\n### 12.1.5 疑惑\n由于自己对源码的理解还不够透彻,读了动态组件的创建流程之后，心中产生了一个疑问，从原理的过程分析，动态组件的核心其实是```is```这个关键字，它在编译阶段就以```component```属性将该组件定义为动态组件，而```component```作为标签好像并没有特别大的用途，只要有```is```关键字的存在，组件标签名设置为任意自定义标签都可以达到动态组件的效果？(```componenta, componentb```)。这个字符串仅以```{ tag: 'component' }```的形式存在于```vnode```的```data```属性存在。那是不是说明，所谓动态组件只是由于```is```的单方面限制？那```component```标签的意义又在哪里？\n\n\n\n## 12.2 内联模板\n由于动态组件除了有```is```作为传值外，还可以有```inline-template```作为配置,借此前提，刚好可以理清楚```Vue```中内联模板的原理和设计思想。```Vue```在官网有一句醒目的话，提示我们```inline-template``` 会让模板的作用域变得更加难以理解。因此建议尽量使用```template```选项来定义模板，而不是用内联模板的形式。接下来，我们通过源码去定位一下所谓作用域难以理解的原因。\n\n我们先简单调整上面的例子，从使用角度上入手：\n```html\n// html\n<div id=\"app\">\n  <button @click=\"changeTabs('child1')\">child1</button>\n  <button @click=\"changeTabs('child2')\">child2</button>\n  <button @click=\"changeTabs('child3')\">child3</button>\n  <component :is=\"chooseTabs\" inline-template>\n    <span>{{test}}</span>\n  </component>\n</div>\n```\n```js\n// js\nvar child1 = {\n  data() {\n    return {\n      test: 'content1'\n    }\n  }\n}\nvar child2 = {\n  data() {\n    return {\n      test: 'content2'\n    }\n  }\n}\nvar child3 = {\n  data() {\n    return {\n      test: 'content3'\n    }\n  }\n}\nvar vm = new Vue({\n  el: '#app',\n  components: {\n    child1,\n    child2,\n    child3\n  },\n  data() {\n    return {\n      chooseTabs: 'child1',\n    }\n  },\n  methods: {\n    changeTabs(tab) {\n      this.chooseTabs = tab;\n    }\n  }\n})\n```\n例子中达到的效果和文章第一个例子一致，很明显和以往认知最大的差异在于，父组件里的环境可以访问到子组件内部的环境变量。初看觉得挺不可思议的。我们回忆一下之前父组件能访问到子组件的情形，从大的方向上有两个:\n\n**1. 采用事件机制，子组件通过```$emit```事件，将子组件的状态告知父组件，达到父访问子的目的。**\n\n**2. 利用作用域插槽的方式，将子的变量通过```props```的形式传递给父，而父通过```v-slot```的语法糖去接收，而我们之前分析的结果是，这种方式本质上还是通过事件派发的形式去通知父组件。**\n\n之前分析过程也有提过父组件无法访问到子环境的变量，其核心的原因在于：\n**父级模板里的所有内容都是在父级作用域中编译的；子模板里的所有内容都是在子作用域中编译的。**\n那么我们有理由猜想，内联模板是不是违背了这一原则，让父的内容放到了子组件创建过程去编译呢？我们接着往下看：\n\n\n回到```ast```解析阶段，前面分析到，针对动态组件的解析，关键在于```processComponent```函数对```is```属性的处理，其中还有一个关键是对```inline-template```的处理，它会在```ast```树上增加```inlineTemplate```属性。\n```js\n//  针对动态组件的解析\n  function processComponent (el) {\n    var binding;\n    // 拿到is属性所对应的值\n    if ((binding = getBindingAttr(el, 'is'))) {\n      // ast树上多了component的属性\n      el.component = binding;\n    }\n    // 添加inlineTemplate属性\n    if (getAndRemoveAttr(el, 'inline-template') != null) {\n      el.inlineTemplate = true;\n    }\n  }\n```\n\n\n`render`函数生成阶段由于```inlineTemplate```的存在，**父的```render```函数的子节点为```null```,这一步也决定了```inline-template```下的模板并不是在父组件阶段编译的**,那模板是如何传递到子组件的编译过程呢？**答案是模板以属性的形式存在，待到子实例时拿到属性值**\n\n```js\nfunction genComponent (componentName,el,state) {\n  // 拥有inlineTemplate属性时，children为null\n  var children = el.inlineTemplate ? null : genChildren(el, state, true);\n  return (\"_c(\" + componentName + \",\" + (genData$2(el, state)) + (children ? (\",\" + children) : '') + \")\")\n}\n```\n\n我们看看最终```render```函数的结果，其中模板以```{render: function(){···}}```的形式存在于父组件的```inlineTemplate```属性中。\n\n\n```js\n\"_c('div',{attrs:{\"id\":\"app\"}},[_c(chooseTabs,{tag:\"component\",inlineTemplate:{render:function(){with(this){return _c('span',[_v(_s(test))])}},staticRenderFns:[]}})],1)\"\n```\n\n最终```vnode```结果也显示，```inlineTemplate```对象会保留在父组件的```data```属性中。\n\n```js\n// vnode结果\n{\n  data: {\n    inlineTemplate: {\n      render: function() {}\n    },\n    tag: 'component'\n  },\n  tag: \"vue-component-1-child1\"\n}\n```\n\n有了```vnode```后，来到了关键的最后一步，根据```vnode```生成真实节点的过程。从根节点开始，遇到```vue-component-1-child1```，会经历实例化创建子组件的过程，实例化子组件前会先对```inlineTemplate```属性进行处理。\n\n```js\nfunction createComponentInstanceForVnode (vnode,parent) {\n    // 子组件的默认选项\n    var options = {\n      _isComponent: true,\n      _parentVnode: vnode,\n      parent: parent\n    };\n    var inlineTemplate = vnode.data.inlineTemplate;\n    // 内联模板的处理，分别拿到render函数和staticRenderFns\n    if (isDef(inlineTemplate)) {\n      options.render = inlineTemplate.render;\n      options.staticRenderFns = inlineTemplate.staticRenderFns;\n    }\n    // 执行vue子组件实例化\n    return new vnode.componentOptions.Ctor(options)\n  }\n```\n子组件的默认选项配置会根据```vnode```上的```inlineTemplate```属性拿到模板的```render```函数。分析到这一步结论已经很清楚了。**内联模板的内容最终会在子组件中解析，所以模板中可以拿到子组件的作用域这个现象也不足为奇了。**\n\n\n\n\n\n\n\n## 12.3 内置组件\n最后说说```Vue```思想中的另一个概念，内置组件，其实```vue```的官方文档有对内置组件进行了列举，分别是```component, transition, transition-group, keep-alive, slot```，其中```<slot>```我们在插槽这一节已经详细介绍过，而```component```的使用这一节也花了大量的篇幅从使用到原理进行了分析。然而学习了```slot,component```之后，我开始意识到```slot```和```component```并不是真正的内置组件。**内置组件是已经在源码初始化阶段就全局注册好的组件。**而```<slot>```和```<component>```并没有被当成一个组件去处理，因此也没有组件的生命周期。```slot```只会在```render```函数阶段转换成```renderSlot```函数进行处理，而```component```也只是借助```is```属性将```createElement```的第一个参数从字符串转换为变量，仅此而已。因此重新回到概念的理解，**内置组件是源码自身提供的组件，**所以这一部分内容的重点，会放在内置组件是什么时候注册的，编译时有哪些不同这两个问题上来。这一部分只是一个抛砖引玉，接下来会有文章专门详细介绍```keep-alive，transition, transition-group```的实现原理。\n\n\n### 12.3.1 构造器定义组件\n`Vue`初始化阶段会在构造器的```components```属性添加三个组件对象,每个组件对象的写法和我们在自定义组件过程的写法一致，有```render```函数，有生命周期，也会定义各种数据。\n```js\n// keep-alive组件选项\nvar KeepAlive = {\n  render: function() {}\n}\n\n// transition 组件选项\nvar Transition = {\n  render: function() {}\n}\n\n// transition-group 组件选项\nvar TransitionGroup = {\n  render: function() {},\n  methods: {},\n  ···\n}\n\nvar builtInComponents = {\n  KeepAlive: KeepAlive\n};\n\nvar platformComponents = {\n  Transition: Transition,\n  TransitionGroup: TransitionGroup\n};\n\n// Vue构造器的选项配置，compoents选项合并\nextend(Vue.options.components, builtInComponents);\nextend(Vue.options.components, platformComponents);\n```\n\n\n`extend`方法我们在系列的开头，分析选项合并的时候有说过，将对象上的属性合并到源对象中，属性相同则覆盖。\n```js\n// 将_from对象合并到to对象，属性相同时，则覆盖to对象的属性\nfunction extend (to, _from) {\n  for (var key in _from) {\n    to[key] = _from[key];\n  }\n  return to\n}\n\n```\n最终```Vue```构造器拥有了三个组件的配置选项。\n```js\nVue.components = {\n  keepAlive: {},\n  transition: {},\n  transition-group: {},\n}\n```\n\n### 12.3.2 注册内置组件\n仅仅有定义是不够的。组件需要被全局使用还得进行全局的注册，这其实在选项合并章节已经阐述清楚了。Vue实例在初始化过程中，最重要的第一步是进行选项的合并，而像内置组件这些资源类选项会有专门的选项合并策略，**最终构造器上的组件选项会以原型链的形式注册到实例的```compoonents```选项中(指令和过滤器同理)。**\n\n```js\n// 资源选项\nvar ASSET_TYPES = [\n  'component',\n  'directive',\n  'filter'\n];\n\n// 定义资源合并的策略\nASSET_TYPES.forEach(function (type) {\n  strats[type + 's'] = mergeAssets; // 定义默认策略\n});\n\nfunction mergeAssets (parentVal,childVal,vm,key) {\n    var res = Object.create(parentVal || null); // 以parentVal为原型创建一个空对象\n    if (childVal) {\n      assertObjectType(key, childVal, vm); // components,filters,directives选项必须为对象\n      return extend(res, childVal) // 子类选项赋值给空对象\n    } else {\n      return res\n    }\n  }\n```\n\n关键的两步一个是```var res = Object.create(parentVal || null);```，它会以```parentVal```为原型创建一个空对象，最后是通过```extend```将用户自定义的```component```选项复制到空对象中。选项合并之后，内置组件也因此在全局完成了注册。\n\n```json\n{\n  components: {\n    child1,\n    __proto__: {\n      keepAlive: {},\n      transition: {},\n      transitionGroup: {}\n    }\n  }\n}\n```\n\n最后我们看看内置组件对象中并没有```template```模板，而是```render```函数，除了减少了耗性能的模板解析过程，我认为重要的原因是内置组件并没有渲染的实体。最后的最后，让我们一起期待后续对```keep-alive```原理分析，敬请期待。\n\n\n### 12.4 小结\n这节我们详细的介绍了动态组件的原理，我们经常使用```<component :is=\"\">```以达到不同组件切换的目的，实际上是由于```is```这个关键字让模板编译成```render```函数时，组件```render```的标签是变量，这样在渲染阶段，随着数据的不同会渲染不同的组件。动态组件还有一种用法是使用内联模板去访问子组件的数据，这又增加了一种子父组件通信的方法。但是官方并不建议我们这样做，因为内联模板会让作用域变得混乱。内联组件实现父子通信的原理是它让父组件的编译过程放到了子组件，这样顺利成章的父组件就可以访问到子组件的变量。文章的最后引出了内置组件，```Vue```中真正的内置组件只有```keep-alive, transition, transition-group```三种，他们本质上是在内部定义好组件选项，并进行全局注册。下一节，我们将进入```keep-alive```内置组件的深度分析。"
  },
  {
    "path": "src/基础的数据代理检测.md",
    "content": "\n> 简单回顾一下这个系列的前两节，前两节花了大量的篇幅介绍了```Vue```的选项合并，选项合并是```Vue```实例初始化的开始，```Vue```为开发者提供了丰富的选项配置，而每个选项都严格规定了合并的策略。然而这只是初始化中的第一步，这一节我们将对另一个重点的概念深入的分析，他就是**数据代理**，我们知道```Vue```大量利用了代理的思想，而除了响应式系统外，还有哪些场景也需要进行数据代理呢？这是我们这节分析的重点。\n\n\n## 2.1 数据代理的含义\n数据代理的另一个说法是数据劫持，当我们在访问或者修改对象的某个属性时，数据劫持可以拦截这个行为并进行额外的操作或者修改返回的结果。而我们知道```Vue```响应式系统的核心就是数据代理，代理使得数据在访问时进行依赖收集，在修改更新时对依赖进行更新，这是响应式系统的核心思路。而这一切离不开```Vue```对数据做了拦截代理。然而响应式并不是本节讨论的重点，这一节我们将看看数据代理在其他场景下的应用。在分析之前，我们需要掌握两种实现数据代理的方法：\n```Object.defineProperty``` 和 ```Proxy```。\n\n\n### 2.1.1 Object.defineProperty\n> 官方定义：```Object.defineProperty()```方法会直接在一个对象上定义一个新属性，或者修改一个对象的现有属性， 并返回这个对象。\n\n基本用法： \n```js\nObject.defineProperty(obj, prop, descriptor)\n```\n\n`Object.defineProperty()`可以用来精确添加或修改对象的属性，只需要在```descriptor```对象中将属性特性描述清楚，```descriptor```的属性描述符有两种形式，一种是数据描述符，另一种是存取描述符，我们分别看看各自的特点。\n\n1. 数据描述符，它拥有四个属性配置\n\n- `configurable`：数据是否可删除，可配置\n- `enumerable`：属性是否可枚举\n- `value`：属性值,默认为```undefined```\n- `writable`：属性是否可读写\n\n2. 存取描述符，它同样拥有四个属性选项\n\n- `configurable`：数据是否可删除，可配置\n- `enumerable`：属性是否可枚举\n- `get`:一个给属性提供 ```getter``` 的方法，如果没有 ```getter``` 则为 ```undefined```。\n- `set`:一个给属性提供 ```setter``` 的方法，如果没有 ```setter``` 则为 ```undefined```。\n\n**需要注意的是: 数据描述符的```value，writable``` 和 存取描述符中的```get, set```属性不能同时存在，否则会抛出异常。**\n有了```Object.defineProperty```方法，我们可以方便的利用存取描述符中的```getter/setter```来进行数据的监听,这也是响应式构建的雏形。```getter```方法可以让我们在访问数据时做额外的操作处理，```setter```方法使得我们可以在数据更新时修改返回的结果。看看下面的例子,由于设置了数据代理，当我们访问对象```o```的```a```属性时，会触发```getter```执行钩子函数，当修改```a```属性的值时，会触发```setter```钩子函数去修改返回的结果。\n```js\nvar o = {}\nvar value;\nObject.defineProperty(o, 'a', {\n    get() {\n        console.log('获取值')\n        return value\n    },\n    set(v) {\n        console.log('设置值')\n        value = qqq\n    }\n})\no.a = 'sss' \n// 设置值\nconsole.log(o.a)\n// 获取值\n// 'qqq'\n\n```\n\n前面说到```Object.defineProperty```的```get```和```set```方法是对对象进行监测并响应变化，那么数组类型是否也可以监测呢，参照监听属性的思路，我们用数组的下标作为属性，数组的元素作为拦截对象，看看```Object.defineProperty```是否可以对数组的数据进行监控拦截。\n```js\nvar arr = [1,2,3];\narr.forEach((item, index) => {\n    Object.defineProperty(arr, index, {\n        get() {\n            console.log('数组被getter拦截')\n            return item\n        },\n        set(value) {\n            console.log('数组被setter拦截')\n            return item = value\n        }\n    })\n})\n\narr[1] = 4;\nconsole.log(arr)\n// 结果\n数组被setter拦截\n数组被getter拦截\n4\n```\n显然，**已知长度的数组是可以通过索引属性来设置属性的访问器属性的。**但是数组的添加确无法进行拦截，这个也很好理解，不管是通过```arr.push()```还是```arr[10] = 10```添加的数据，数组所添加的索引值并没有预先加入数据拦截中，所以自然无法进行拦截处理。这个也是使用```Object.defineProperty```进行数据代理的弊端。为了解决这个问题，```Vue```在响应式系统中对数组的方法进行了重写，间接的解决了这个问题，详细细节可以参考后续的响应式系统分析。\n\n另外如果需要拦截的对象属性嵌套多层，如果没有递归去调用```Object.defineProperty```进行拦截，深层次的数据也依然无法监测。\n\n### 2.1.2 Proxy\n为了解决像数组这类无法进行数据拦截，以及深层次的嵌套问题，```es6```引入了```Proxy```的概念，它是真正在语言层面对数据拦截的定义。和```Object.defineProperty```一样，```Proxy```可以修改某些操作的默认行为，但是不同的是，**```Proxy```针对目标对象会创建一个新的实例对象，并将目标对象代理到新的实例对象上，**。 本质的区别是后者会创建一个新的对象对原对象做代理，外界对原对象的访问，都必须先通过这层代理进行拦截处理。而拦截的结果是**我们只要通过操作新的实例对象就能间接的操作真正的目标对象了**。针对```Proxy```，下面是基础的写法:\n```js\nvar obj = {}\nvar nobj = new Proxy(obj, {\n    get(target, key, receiver) {\n        console.log('获取值')\n        return Reflect.get(target, key, receiver)\n    },\n    set(target, key, value, receiver) {\n        console.log('设置值')\n        return Reflect.set(target, key, value, receiver)\n    }\n})\n\nnobj.a = '代理'\nconsole.log(obj)\n// 结果\n设置值\n{a: \"代理\"}\n```\n\n\n上面的```get,set```是```Proxy```支持的拦截方法，而```Proxy``` 支持的拦截操作有13种之多，具体可以参照[ES6-Proxy](http://es6.ruanyifeng.com/#docs/proxy)文档,前面提到，```Object.defineProperty```的```getter```和```setter```方法并不适合监听拦截数组的变化，那么新引入的```Proxy```又能否做到呢？我们看下面的例子。\n\n```js\nvar arr = [1, 2, 3]\nlet obj = new Proxy(arr, {\n    get: function (target, key, receiver) {\n        // console.log(\"获取数组元素\" + key);\n        return Reflect.get(target, key, receiver);\n    },\n    set: function (target, key, receiver) {\n        console.log('设置数组');\n        return Reflect.set(target, key, receiver);\n    }\n})\n// 1. 改变已存在索引的数据\nobj[2] = 3\n// result: 设置数组\n// 2. push,unshift添加数据\nobj.push(4)\n// result: 设置数组 * 2 (索引和length属性都会触发setter)\n// // 3. 直接通过索引添加数组\nobj[5] = 5\n// result: 设置数组 * 2\n// // 4. 删除数组元素\nobj.splice(1, 1)\n\n```\n\n显然```Proxy```完美的解决了数组的监听检测问题，针对数组添加数据，删除数据的不同方法，代理都能很好的拦截处理。另外```Proxy```也很好的解决了深层次嵌套对象的问题，具体读者可以自行举例分析。\n\n## 2.2 initProxy\n数据拦截的思想除了为构建响应式系统准备，它也可以为**数据进行筛选过滤**，我们接着往下看初始化的代码，在合并选项后，```vue```接下来会为```vm```实例设置一层代理，这层代理可以为**vue在模板渲染时进行一层数据筛选**，这个过程究竟怎么发生的，我们看代码的实现。\n\n```js\nVue.prototype._init = function(options) {\n    // 选项合并\n    ...\n    {\n        // 对vm实例进行一层代理\n        initProxy(vm);\n    }\n    ...\n}\n```\n`initProxy`的实现如下：\n```js\n// 代理函数\nvar initProxy = function initProxy (vm) {\n    \n    if (hasProxy) {\n        var options = vm.$options;\n        var handlers = options.render && options.render._withStripped\n            ? getHandler\n            : hasHandler;\n        // 代理vm实例到vm属性_renderProxy\n        vm._renderProxy = new Proxy(vm, handlers);\n    } else {\n        vm._renderProxy = vm;\n    }\n};\n```\n\n首先是判断浏览器是否支持原生的```proxy```。\n```js\nvar hasProxy =\n      typeof Proxy !== 'undefined' && isNative(Proxy);\n```\n当浏览器支持```Proxy```时，```vm._renderProxy```会代理```vm```实例，并且代理过程也会随着参数的不同呈现不同的效果；当浏览器不支持```Proxy```时，直接将```vm```赋值给```vm._renderProxy```。\n\n读到这里，我相信大家会有很多的疑惑。\n**1. 这层代理的访问时机是什么，也就是说什么场景会触发这层代理**\n**2. 参数```options.render._withStripped```代表着什么，```getHandler```和```hasHandler```又有什么不同。**\n**3. 如何理解为模板数据的访问进行数据筛选过滤。到底有什么数据需要过滤。**\n**4. 只有在支持原生```proxy```环境下才会建立这层代理，那么在旧的浏览器，非法的数据又将如何展示。**\n\n带着这些疑惑，我们接着往下分析。\n\n### 2.2.1 触发代理\n源码中```vm._renderProxy```的使用出现在```Vue```实例的```_render```方法中，```Vue.prototype._render```是将渲染函数转换成```Virtual DOM```的方法，这部分是关于实例的挂载和模板引擎的解析，笔者并不会在这一章节中深入分析，我们只需要先有一个认知，**```Vue```内部在```js```和真实```DOM```节点中设立了一个中间层，这个中间层就是```Virtual DOM```，遵循```js -> virtual -> 真实dom```的转换过程,而```Vue.prototype._render```是前半段的转换，**当我们调用```render```函数时，代理的```vm._renderProxy```对象便会访问到。\n```js\nVue.prototype._render = function () {\n    ···\n    // 调用vm._renderProxy\n    vnode = render.call(vm._renderProxy, vm.$createElement);\n}\n```\n\n那么代理的处理函数又是什么？我们回过头看看代理选项```handlers```的实现。\n```handers```函数会根据 ```options.render._withStripped```的不同执行不同的代理函数，**当使用类似```webpack```这样的打包工具时，通常会使用```vue-loader```插件进行模板的编译，这个时候```options.render```是存在的，并且```_withStripped```的属性也会设置为```true```**(关于编译版本和运行时版本的区别可以参考后面章节)，所以此时代理的选项是```hasHandler```,在其他场景下，代理的选项是```getHandler```。```getHandler,hasHandler```的逻辑相似，我们只分析使用```vue-loader```场景下```hasHandler```的逻辑。另外的逻辑，读者可以自行分析。\n\n```js\nvar hasHandler = {\n    // key in obj或者with作用域时，会触发has的钩子\n    has: function has (target, key) {\n        ···\n    }\n};\n```\n`hasHandler`函数定义了```has```的钩子，前面介绍过,```proxy```的钩子有13个之多，而```has```是其中一个，它用来拦截```propKey in proxy```的操作，返回一个布尔值。而除了拦截 ```in``` 操作符外，```has```钩子同样可以用来拦截```with```语句下的作用对象。例如:\n```js\nvar obj = {\n    a: 1\n}\nvar nObj = new Proxy(obj, {\n    has(target, key) {\n        console.log(target) // { a: 1 }\n        console.log(key) // a\n        return true\n    }\n})\n\nwith(nObj) {\n    a = 2\n}\n```\n那么这两个触发条件是否跟```_render```过程有直接的关系呢？答案是肯定的。```vnode = render.call(vm._renderProxy, vm.$createElement);```的主体是```render```函数，而这个```render```函数就是包装成```with```的执行语句,**在执行```with```语句的过程中，该作用域下变量的访问都会触发```has```钩子，这也是模板渲染时之所有会触发代理拦截的原因。**我们通过代码来观察```render```函数的原形。\n```js\nvar vm = new Vue({\n    el: '#app'     \n})\nconsole.log(vm.$options.render)\n\n//输出, 模板渲染使用with语句\nƒ anonymous() {\n    with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_v(_s(message)+_s(_test))])}\n}\n```\n\n\n### 2.2.2 数据过滤\n我们已经大致知道了```Proxy```代理的访问时机，那么设置这层代理的作用又在哪里呢？首先思考一个问题，我们通过```data```选项去设置实例数据，那么这些数据可以随着个人的习惯任意命名吗？显然不是的，如果你使用```js```的关键字(像```Object,Array,NaN```)去命名,这是不被允许的。另一方面，```Vue```源码内部使用了以```$,_```作为开头的内部变量，所以以```$,_```开头的变量名也是不被允许的，这就构成了数据过滤监测的前提。接下来我们具体看```hasHandler```的细节实现。\n\n```js\nvar hasHandler = {\n    has: function has (target, key) {\n        var has = key in target;\n        // isAllowed用来判断模板上出现的变量是否合法。\n        var isAllowed = allowedGlobals(key) ||\n            (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));\n            // _和$开头的变量不允许出现在定义的数据中，因为他是vue内部保留属性的开头。\n        // 1. warnReservedPrefix: 警告不能以$ _开头的变量\n        // 2. warnNonPresent: 警告模板出现的变量在vue实例中未定义\n        if (!has && !isAllowed) {\n            if (key in target.$data) { warnReservedPrefix(target, key); }\n            else { warnNonPresent(target, key); }\n        }\n        return has || !isAllowed\n    }\n};\n\n// 模板中允许出现的非vue实例定义的变量\nvar allowedGlobals = makeMap(\n    'Infinity,undefined,NaN,isFinite,isNaN,' +\n    'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +\n    'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +\n    'require' // for Webpack/Browserify\n);\n```\n首先```allowedGlobals```定义了```javascript```保留的关键字，这些关键字是不允许作为用户变量存在的。```(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)```的逻辑对以```$,_```开头，或者是否是```data```中未定义的变量做判断过滤。这里对未定义变量的场景多解释几句，前面说到，代理的对象```vm.renderProxy```是在执行```_render```函数中访问的，而在使用了```template```模板的情况下，```render```函数是对模板的解析结果，换言之，之所以会触发数据代理拦截是因为模板中使用了变量，例如```<div>{{message}}}</div>```。而如果我们在模板中使用了未定义的变量，这个过程就被```proxy```拦截，并定义为不合法的变量使用。\n\n\n我们可以看看两个报错信息的源代码(是不是很熟悉):\n```js\n// 模板使用未定义的变量\nvar warnNonPresent = function (target, key) {\n    warn(\n    \"Property or method \\\"\" + key + \"\\\" is not defined on the instance but \" +\n    'referenced during render. Make sure that this property is reactive, ' +\n    'either in the data option, or for class-based components, by ' +\n    'initializing the property. ' +\n    'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',\n    target\n    );\n};\n\n// 使用$,_开头的变量\nvar warnReservedPrefix = function (target, key) {\n    warn(\n    \"Property \\\"\" + key + \"\\\" must be accessed with \\\"$data.\" + key + \"\\\" because \" +\n    'properties starting with \"$\" or \"_\" are not proxied in the Vue instance to ' +\n    'prevent conflicts with Vue internals' +\n    'See: https://vuejs.org/v2/api/#data',\n    target\n    );\n};\n```\n\n分析到这里，前面的疑惑只剩下最后一个问题。只有在浏览器支持```proxy```的情况下，才会执行```initProxy```设置代理，那么在不支持的情况下，数据过滤就失效了，此时非法的数据定义还能正常运行吗？我们先对比下面两个结论。\n\n```js\n// 模板中使用_开头的变量，且在data选项中有定义\n<div id=\"app\">{{_test}}</div>\nnew Vue({\n    el: '#app',\n    data: {\n        _test: 'proxy'\n    }\n})\n```\n\n\n1. 支持```proxy```浏览器的结果\n\n![](./img/2.1.png)\n\n2. 不支持```proxy```浏览器的结果\n\n![](./img/2.2.png)\n\n\n显然，在没有经过代理的情况下，使用```_```开头的变量依旧会\n报错，但是它变成了```js```语言层面的错误，表示该变量没有被声明。但是这个报错无法在```Vue```这一层知道错误的详细信息，而这就是能使用```Proxy```的好处。接着我们会思考，既然已经在```data```选项中定义了```_test```变量，为什么访问时还是找不到变量的定义呢？\n原来在初始化数据阶段，```Vue```已经为数据进行了一层筛选的代理。具体看```initData```对数据的代理，其他实现细节不在本节讨论范围内。\n\n```js\nfunction initData(vm) {\n    vm._data = typeof data === 'function' ? getData(data, vm) : data || {}\n    if (!isReserved(key)) {\n        // 数据代理，用户可直接通过vm实例返回data数据\n        proxy(vm, \"_data\", key);\n    }\n}\n\nfunction isReserved (str) {\n    var c = (str + '').charCodeAt(0);\n    // 首字符是$, _的字符串\n    return c === 0x24 || c === 0x5F\n  }\n```\n`vm._data`可以拿到最终```data```选项合并的结果，```isReserved```会过滤以```$,_```开头的变量，```proxy```会为实例数据的访问做代理，当我们访问```this.message```时，实际上访问的是```this._data.message```,而有了```isReserved```的筛选，即使```this._data._test```存在，我们依旧无法在访问```this._test```时拿到```_test```变量。这就解释了为什么会有变量没有被声明的语法错误，而```proxy```的实现，又是基于上述提到的```Object.defineProperty```来实现的。\n\n```js\nfunction proxy (target, sourceKey, key) {\n    sharedPropertyDefinition.get = function proxyGetter () {\n        // 当访问this[key]时，会代理访问this._data[key]的值\n        return this[sourceKey][key]\n    };\n    sharedPropertyDefinition.set = function proxySetter (val) {\n        this[sourceKey][key] = val;\n    };\n    Object.defineProperty(target, key, sharedPropertyDefinition);\n}\n```\n\n\n\n## 2.3 小结\n这一节内容，详细的介绍了数据代理在```Vue```的实现思路和另一个应用场景，数据代理是一种设计模式，也是一种编程思想，```Object.defineProperty```和```Proxy```都可以实现数据代理，但是他们各有优劣，前者兼容性较好，但是却无法对数组或者嵌套的对象进行代理监测，而```Proxy```基本可以解决所有的问题，但是对兼容性要求很高。```Vue```中的响应式系统是以```Object.defineProperty```实现的，但是这并不代表没有```Proxy```的应用。```initProxy```就是其中的例子，这层代理会在模板渲染时对一些非法或者没有定义的变量进行筛选判断，和没有数据代理相比，非法的数据定义错误会提前到应用层捕获，这也有利于开发者对错误的排查。"
  },
  {
    "path": "src/完整渲染流程.md",
    "content": "> 继上一节内容，我们将```Vue```复杂的挂载流程通过图解流程，代码分析的方式简单梳理了一遍，最后也讲到了模板编译的大致流程。然而在挂载的核心处，我们并没有分析模板编译后渲染函数是如何转换为可视化```DOM```节点的。因此这一章节，我们将重新回到```Vue```实例挂载的最后一个环节：渲染```DOM```节点。在渲染真实```DOM```的过程中，```Vue```引进了虚拟```DOM```的概念，这是```Vue```架构设计中另一个重要的理念。虚拟```DOM```作为```JS```对象和真实```DOM```中间的一个缓冲层，对```JS```频繁操作```DOM```的引起的性能问题有很好的缓解作用。\n\n## 4.1 Virtual DOM\n\n### 4.1.1 浏览器的渲染流程\n当浏览器接收到一个```Html```文件时，```JS```引擎和浏览器的渲染引擎便开始工作了。从渲染引擎的角度，它首先会将```html```文件解析成一个```DOM```树，与此同时，浏览器将识别并加载```CSS```样式，并和```DOM```树一起合并为一个渲染树。有了渲染树后，渲染引擎将计算所有元素的位置信息，最后通过绘制，在屏幕上打印最终的内容。```JS```引擎和渲染引擎虽然是两个独立的线程，但是JS引擎却可以触发渲染引擎工作，当我们通过脚本去修改元素位置或外观时，```JS```引擎会利用```DOM```相关的```API```方法去操作```DOM```对象,此时渲染引擎变开始工作，渲染引擎会触发回流或者重绘。下面是回流重绘的两个概念：\n\n- 回流： 当我们对```DOM```的修改引发了元素尺寸的变化时，浏览器需要重新计算元素的大小和位置，最后将重新计算的结果绘制出来，这个过程称为回流。\n- 重绘： 当我们对```DOM```的修改只单纯改变元素的颜色时，浏览器此时并不需要重新计算元素的大小和位置，而只要重新绘制新样式。这个过程称为重绘。\n\n**很显然回流比重绘更加耗费性能**。\n\n通过了解浏览器基本的渲染机制，我们很容易联想到当不断的通过```JS```修改```DOM```时，不经意间会触发到渲染引擎的回流或者重绘，这个性能开销是非常巨大的。因此为了降低开销，我们需要做的是尽可能减少```DOM```操作。有什么方法可以做到呢？\n\n### 4.1.2 缓冲层-虚拟DOM\n虚拟```DOM```是为了解决频繁操作```DOM```引发性能问题的产物。虚拟```DOM```(下面称为```Virtual DOM```)是将页面的状态抽象为```JS```对象的形式，本质上是```JS```和真实```DOM```的中间层，当我们想用```JS```脚本大批量进行```DOM```操作时，会优先作用于```Virtual DOM```这个```JS```对象，最后通过对比将要改动的部分通知并更新到真实的```DOM```。尽管最终还是操作真实的```DOM```，但```Virtual DOM```可以将多个改动合并成一个批量的操作，从而减少 ```DOM``` 重排的次数，进而缩短了生成渲染树和绘制所花的时间。\n\n我们看一个真实的```DOM```包含了什么：\n\n![](./img/4.1.png)\n浏览器将一个真实```DOM```设计得很复杂，不仅包含了自身的属性描述，大小位置等定义，也囊括了```DOM```拥有的浏览器事件等。正因为如此复杂的结构，我们频繁去操作```DOM```或多或少会带来浏览器的性能问题。而作为数据和真实```DOM```之间的一层缓冲，```Virtual DOM``` 只是用来映射到真实```DOM```的渲染，因此不需要包含操作 ```DOM``` 的方法，它只要在对象中重点关注几个属性即可。\n```js\n// 真实DOM\n<div id=\"real\"><span>dom</span></div>\n\n// 真实DOM对应的JS对象\n{\n    tag: 'div',\n    data: {\n        id: 'real'\n    },\n    children: [{\n        tag: 'span',\n        children: 'dom'\n    }]\n}\n```\n\n## 4.2 Vnode\n`Vue`在渲染机制的优化上，同样引进了```virtual dom```的概念，它是用```Vnode```这个构造函数去描述一个```DOM```节点。\n\n### 4.2.1 Vnode构造函数\n```js\nvar VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) {\n    this.tag = tag; // 标签\n    this.data = data;  // 数据\n    this.children = children; // 子节点\n    this.text = text;\n    ···\n    ···\n  };\n```\n`Vnode`定义的属性差不多有20几个，显然用```Vnode```对象要比真实```DOM```对象描述的内容要简单得多，它只用来单纯描述节点的关键属性，例如标签名，数据，子节点等。并没有保留跟浏览器相关的```DOM```方法。除此之外，```Vnode```也会有其他的属性用来扩展```Vue```的灵活性。\n\n源码中也定义了创建```Vnode```的相关方法。\n\n\n### 4.2.2 创建Vnode注释节点 \n```js\n// 创建注释vnode节点\nvar createEmptyVNode = function (text) {\n    if ( text === void 0 ) text = '';\n\n    var node = new VNode();\n    node.text = text;\n    node.isComment = true; // 标记注释节点\n    return node\n};\n```\n\n### 4.2.3 创建Vnode文本节点\n```js\n// 创建文本vnode节点\nfunction createTextVNode (val) {\n    return new VNode(undefined, undefined, undefined, String(val))\n}\n```\n### 4.2.4 克隆vnode\n```js\nfunction cloneVNode (vnode) {\n    var cloned = new VNode(\n      vnode.tag,\n      vnode.data,\n      vnode.children && vnode.children.slice(),\n      vnode.text,\n      vnode.elm,\n      vnode.context,\n      vnode.componentOptions,\n      vnode.asyncFactory\n    );\n    cloned.ns = vnode.ns;\n    cloned.isStatic = vnode.isStatic;\n    cloned.key = vnode.key;\n    cloned.isComment = vnode.isComment;\n    cloned.fnContext = vnode.fnContext;\n    cloned.fnOptions = vnode.fnOptions;\n    cloned.fnScopeId = vnode.fnScopeId;\n    cloned.asyncMeta = vnode.asyncMeta;\n    cloned.isCloned = true;\n    return cloned\n  }\n```\n**注意：```cloneVnode```对```Vnode```的克隆只是一层浅拷贝，它不会对子节点进行深度克隆。**\n\n## 4.3 Virtual DOM的创建\n先简单回顾一下挂载的流程，挂载的过程是调用```Vue```实例上```$mount```方法，而```$mount```的核心是```mountComponent```函数。如果我们传递的是```template```模板，模板会先经过编译器的解析，并最终根据不同平台生成对应代码，此时对应的就是将```with```语句封装好的```render```函数;如果传递的是```render```函数，则跳过模板编译过程，直接进入下一个阶段。下一阶段是拿到```render```函数，调用```vm._render()```方法将```render```函数转化为```Virtual DOM```，并最终通过```vm._update()```方法将```Virtual DOM```渲染为真实的```DOM```节点。\n\n```js\nVue.prototype.$mount = function(el, hydrating) {\n    ···\n    return mountComponent(this, el)\n}\nfunction mountComponent() {\n    ···\n    updateComponent = function () {\n        vm._update(vm._render(), hydrating);\n    };\n}\n\n```\n我们先看看```vm._render()```方法是如何**将render函数转化为Virtual DOM**的。\n\n回顾一下第一章节内容，文章介绍了```Vue```在代码引入时会定义很多属性和方法，其中有一个```renderMixin```过程，我们之前只提到了它会定义跟渲染有关的函数，实际上它只定义了两个重要的方法，```_render```函数就是其中一个。\n\n```js\n// 引入Vue时，执行renderMixin方法，该方法定义了Vue原型上的几个方法，其中一个便是 _render函数\nrenderMixin();//\nfunction renderMixin() {\n    Vue.prototype._render = function() {\n        var ref = vm.$options;\n        var render = ref.render;\n        ···\n        try {\n            vnode = render.call(vm._renderProxy, vm.$createElement);\n        } catch (e) {\n            ···\n        }\n        ···\n        return vnode\n    }\n}\n```\n抛开其他代码，_render函数的核心是```render.call(vm._renderProxy, vm.$createElement)```部分，```vm._renderProxy```在数据代理分析过，本质上是为了做数据过滤检测，它也绑定了```render```函数执行时的```this```指向。```vm.$createElement```方法会作为```render```函数的参数传入。**回忆一下，在手写```render```函数时，我们会利用```render```函数的第一个参数```createElement```进行渲染函数的编写，这里的```createElement```参数就是定义好的```$createElement```方法。**\n\n```js\nnew Vue({\n    el: '#app',\n    render: function(createElement) {\n        return createElement('div', {}, this.message)\n    },\n    data() {\n        return {\n            message: 'dom'\n        }\n    }\n})\n```\n初始化```_init```时，有一个```initRender```函数，它就是用来定义渲染函数方法的，其中就有``````vm.$createElement``````方法的定义，除了```$createElement```，```_c```方法的定义也类似。其中 ```vm._c``` 是```template```内部编译成```render```函数时调用的方法，```vm.$createElement```是手写```render```函数时调用的方法。**两者的唯一区别仅仅是最后一个参数的不同。通过模板生成的```render```方法可以保证子节点都是```Vnode```，而手写的```render```需要一些检验和转换。**\n\n\n```js\nfunction initRender(vm) {\n    vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false); }\n    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };\n}\n```\n\n`createElement` 方法实际上是对 ```_createElement``` 方法的封装，在调用```_createElement```前，它会先对传入的参数进行处理，毕竟手写的```render```函数参数规格不统一。举一个简单的例子。\n```js\n// 没有data\nnew Vue({\n    el: '#app',\n    render: function(createElement) {\n        return createElement('div', this.message)\n    },\n    data() {\n        return {\n            message: 'dom'\n        }\n    }\n})\n// 有data\nnew Vue({\n    el: '#app',\n    render: function(createElement) {\n        return createElement('div', {}, this.message)\n    },\n    data() {\n        return {\n            message: 'dom'\n        }\n    }\n})\n```\n这里如果第二个参数是变量或者数组，则默认是没有传递```data```,因为```data```一般是对象形式存在。\n\n```js\nfunction createElement (\n    context, // vm 实例\n    tag, // 标签\n    data, // 节点相关数据，属性\n    children, // 子节点\n    normalizationType,\n    alwaysNormalize // 区分内部编译生成的render还是手写render\n  ) {\n    // 对传入参数做处理，如果没有data，则将第三个参数作为第四个参数使用，往上类推。\n    if (Array.isArray(data) || isPrimitive(data)) {\n      normalizationType = children;\n      children = data;\n      data = undefined;\n    }\n    // 根据是alwaysNormalize 区分是内部编译使用的，还是用户手写render使用的\n    if (isTrue(alwaysNormalize)) {\n      normalizationType = ALWAYS_NORMALIZE;\n    }\n    return _createElement(context, tag, data, children, normalizationType) // 真正生成Vnode的方法\n  }\n```\n\n### 4.3.1 数据规范检测\n\n`Vue`既然暴露给用户用```render```函数去手写渲染模板，就需要考虑用户操作带来的不确定性，因此```_createElement```在创建```Vnode```前会先数据的规范性进行检测，将不合法的数据类型错误提前暴露给用户。接下来将列举几个在实际场景中容易犯的错误，也方便我们理解源码中对这类错误的处理。\n\n1. 用响应式对象做```data```属性\n```js\nnew Vue({\n    el: '#app',\n    render: function (createElement, context) {\n       return createElement('div', this.observeData, this.show)\n    },\n    data() {\n        return {\n            show: 'dom',\n            observeData: {\n                attr: {\n                    id: 'test'\n                }\n            }\n        }\n    }\n})\n```\n2. 当特殊属性key的值为非字符串，非数字类型时\n```js\nnew Vue({\n    el: '#app',\n    render: function(createElement) {\n        return createElement('div', { key: this.lists }, this.lists.map(l => {\n           return createElement('span', l.name)\n        }))\n    },\n    data() {\n        return {\n            lists: [{\n              name: '111'\n            },\n            {\n              name: '222'\n            }\n          ],\n        }\n    }\n})\n```\n这些规范都会在创建```Vnode```节点之前发现并报错，源代码如下：\n```js\nfunction _createElement (context,tag,data,children,normalizationType) {\n    // 1. 数据对象不能是定义在Vue data属性中的响应式数据。\n    if (isDef(data) && isDef((data).__ob__)) {\n      warn(\n        \"Avoid using observed data object as vnode data: \" + (JSON.stringify(data)) + \"\\n\" +\n        'Always create fresh vnode data objects in each render!',\n        context\n      );\n      return createEmptyVNode() // 返回注释节点\n    }\n    if (isDef(data) && isDef(data.is)) {\n      tag = data.is;\n    }\n    if (!tag) {\n      // 防止动态组件 :is 属性设置为false时，需要做特殊处理\n      return createEmptyVNode()\n    }\n    // 2. key值只能为string，number这些原始数据类型\n    if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)\n    ) {\n      {\n        warn(\n          'Avoid using non-primitive value as key, ' +\n          'use string/number value instead.',\n          context\n        );\n      }\n    }\n    ···\n  }\n```\n这些规范性检测保证了后续```Virtual DOM tree```的完整生成。\n\n### 4.3.2 子节点children规范化\n\n\n`Virtual DOM tree`是由每个```Vnode```以树状形式拼成的虚拟```DOM```树，我们在转换真实节点时需要的就是这样一个完整的```Virtual DOM tree```，因此我们需要保证每一个子节点都是```Vnode```类型,这里分两种场景分析。\n- 模板编译```render```函数，理论上```template```模板通过编译生成的```render```函数都是```Vnode```类型，但是有一个例外，函数式组件返回的是一个数组(这个特殊例子，可以看函数式组件的文章分析),这个时候```Vue```的处理是将整个```children```拍平成一维数组。\n- 用户定义```render```函数，这个时候又分为两种情况，一个是当```chidren```为文本节点时，这时候通过前面介绍的```createTextVNode``` 创建一个文本节点的 ```VNode```; 另一种相对复杂，当```children```中有```v-for```的时候会出现嵌套数组，这时候的处理逻辑是，遍历```children```，对每个节点进行判断，如果依旧是数组，则继续递归调用，直到类型为基础类型时，调用```createTextVnode```方法转化为```Vnode```。这样经过递归，```children```也变成了一个类型为```Vnode```的数组。\n\n```js\nfunction _createElement() {\n    ···\n    if (normalizationType === ALWAYS_NORMALIZE) {\n      // 用户定义render函数\n      children = normalizeChildren(children);\n    } else if (normalizationType === SIMPLE_NORMALIZE) {\n      // 模板编译生成的的render函数\n      children = simpleNormalizeChildren(children);\n    }\n}\n\n// 处理编译生成的render 函数\nfunction simpleNormalizeChildren (children) {\n    for (var i = 0; i < children.length; i++) {\n        // 子节点为数组时，进行开平操作，压成一维数组。\n        if (Array.isArray(children[i])) {\n        return Array.prototype.concat.apply([], children)\n        }\n    }\n    return children\n}\n\n// 处理用户定义的render函数\nfunction normalizeChildren (children) {\n    // 递归调用，直到子节点是基础类型，则调用创建文本节点Vnode\n    return isPrimitive(children)\n      ? [createTextVNode(children)]\n      : Array.isArray(children)\n        ? normalizeArrayChildren(children)\n        : undefined\n  }\n\n// 判断是否基础类型\nfunction isPrimitive (value) {\n    return (\n      typeof value === 'string' ||\n      typeof value === 'number' ||\n      typeof value === 'symbol' ||\n      typeof value === 'boolean'\n    )\n  }\n```\n\n### 4.3.4 实际场景\n在数据检测和组件规范化后，接下来通过```new VNode()```便可以生成一棵完整的```VNode```树，注意在```_render```过程中会遇到子组件，这个时候会优先去做子组件的初始化，这部分放到组件环节专门分析。我们用一个实际的例子，结束```render```函数到```Virtual DOM```的分析。\n\n- `template`模板形式\n```js\nvar vm = new Vue({\n  el: '#app',\n  template: '<div><span>virtual dom</span></div>'\n})\n```\n- 模板编译生成```render```函数\n```js\n(function() {\n  with(this){\n    return _c('div',[_c('span',[_v(\"virual dom\")])])\n  }\n})\n```\n- `Virtual DOM tree`的结果(省略版)\n```js\n{\n  tag: 'div',\n  children: [{\n    tag: 'span',\n    children: [{\n      tag: undefined,\n      text: 'virtual dom'\n    }]\n  }]\n}\n```\n\n\n## 4.4 虚拟Vnode映射成真实DOM\n回到 ```updateComponent```的最后一个过程,虚拟的```DOM```树在生成```virtual dom```后，会调用```Vue```原型上```_update```方法，将虚拟```DOM```映射成为真实的```DOM```。从源码上可以知道，```_update```的调用时机有两个，一个是发生在初次渲染阶段，另一个发生数据更新阶段。\n\n```js\nupdateComponent = function () {\n    // render生成虚拟DOM，update渲染真实DOM\n    vm._update(vm._render(), hydrating);\n};\n```\n`vm._update`方法的定义在```lifecycleMixin```中。\n```js\nlifecycleMixin()\nfunction lifecycleMixin() {\n    Vue.prototype._update = function (vnode, hydrating) {\n        var vm = this;\n        var prevEl = vm.$el;\n        var prevVnode = vm._vnode; // prevVnode为旧vnode节点\n        // 通过是否有旧节点判断是初次渲染还是数据更新\n        if (!prevVnode) {\n            // 初次渲染\n            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)\n        } else {\n            // 数据更新\n            vm.$el = vm.__patch__(prevVnode, vnode);\n        }\n}\n```\n`_update`的核心是```__patch__```方法，如果是服务端渲染，由于没有```DOM```，```_patch```方法是一个空函数，在有```DOM```对象的浏览器环境下，```__patch__```是```patch```函数的引用。\n```  \n// 浏览器端才有DOM，服务端没有dom，所以patch为一个空函数\n  Vue.prototype.__patch__ = inBrowser ? patch : noop;\n```\n\n而```patch```方法又是```createPatchFunction```方法的返回值，```createPatchFunction```方法传递一个对象作为参数，对象拥有两个属性，```nodeOps```和```modules```，```nodeOps```封装了一系列操作原生```DOM```对象的方法。而```modules```定义了模块的钩子函数。\n```js\n var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });\n\n// 将操作dom对象的方法合集做冻结操作\n var nodeOps = /*#__PURE__*/Object.freeze({\n    createElement: createElement$1,\n    createElementNS: createElementNS,\n    createTextNode: createTextNode,\n    createComment: createComment,\n    insertBefore: insertBefore,\n    removeChild: removeChild,\n    appendChild: appendChild,\n    parentNode: parentNode,\n    nextSibling: nextSibling,\n    tagName: tagName,\n    setTextContent: setTextContent,\n    setStyleScope: setStyleScope\n  });\n\n// 定义了模块的钩子函数\n  var platformModules = [\n    attrs,\n    klass,\n    events,\n    domProps,\n    style,\n    transition\n  ];\n\nvar modules = platformModules.concat(baseModules);\n```\n\n真正的```createPatchFunction```函数有一千多行代码，这里就不方便列举出来了，它的内部首先定义了一系列辅助的方法，而核心是通过调用```createElm```方法进行```dom```操作，创建节点，插入子节点，递归创建一个完整的```DOM```树并插入到```Body```中。并且在产生真实阶段阶段，会有```diff```算法来判断前后```Vnode```的差异，以求最小化改变真实阶段。后面会有一个章节的内容去讲解```diff```算法。```createPatchFunction```的过程只需要先记住一些结论，函数内部会调用封装好的```DOM api```，根据```Virtual DOM```的结果去生成真实的节点。其中如果遇到组件```Vnode```时，会递归调用子组件的挂载过程，这个过程我们也会放到后面章节去分析。\n\n## 4.5 小结\n这一节分析了```mountComponent```的两个核心方法，```render```和```update```,在分析前重点介绍了存在于```JS```操作和```DOM```渲染的桥梁：```Virtual DOM```。```JS```对```DOM```节点的批量操作会先直接反应到```Virtual DOM```这个描述对象上,最终的结果才会直接作用到真实节点上。可以说，```Virtual DOM```很大程度提高了渲染的性能。文章重点介绍了```render```函数转换成```Virtual DOM```的过程，并大致描述了```_update```函数的实现思路。其实这两个过程都牵扯到组件，所以这一节对很多环节都无法深入分析，下一节开始会进入组件的专题。我相信分析完组件后，读者会对整个渲染过程会有更深刻的理解和思考。\n"
  },
  {
    "path": "src/实例挂载流程和模板编译.md",
    "content": ">前面几节我们从```new Vue```创建实例开始，介绍了创建实例时执行初始化流程中的重要两步，配置选项的资源合并,以及响应式系统的核心思想，数据代理。在合并章节，我们对```Vue```丰富的选项合并策略有了基本的认知，在数据代理章节我们又对代理拦截的意义和使用场景有了深入的认识。按照```Vue```源码的设计思路，初始化过程还会进行很多操作，例如组件之间创建关联，初始化事件中心，初始化数据并建立响应式系统等，并最终将模板和数据渲染成为```dom```节点。如果直接按流程的先后顺序分析每个步骤的实现细节，会有很多概念很难理解。因此在这一章节，我们先重点分析一个概念，**实例的挂载渲染流程。**\n\n## 3.1 Runtime Only VS Runtime + Compiler\n在正文开始之前，我们先了解一下```vue```基于源码构建的两个版本，一个是```runtime only```(一个只包含运行时的版本)，另一个是```runtime + compiler```(一个同时包含编译器和运行时的版本)。而两个版本的区别仅在于后者包含了一个编译器。\n\n什么是编译器，百度百科这样解释道：\n\n>简单讲，编译器就是将“一种语言（通常为高级语言）”翻译为“另一种语言（通常为低级语言）”的程序。一个现代编译器的主要工作流程：源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)。\n\n通俗点讲，编译器是一个提供了将**源代码**转化为**目标代码**的工具。从```Vue```的角度出发，内置的编译器实现了将```template```模板转换编译为可执行```javascript```脚本的功能。\n\n\n### 3.1.1 Runtime + Compiler\n一个完整的```Vue```版本是包含编译器的，我们可以使用```template```进行模板编写。编译器会自动将模板字符串编译成渲染函数的代码,源码中就是```render```函数。\n如果你需要在客户端编译模板 (比如传入一个字符串给 ```template``` 选项，或挂载到一个元素上并以其 ```DOM``` 内部的 HTML 作为模板)，就需要一个包含编译器的版本。\n```js\n// 需要编译器的版本\nnew Vue({\n  template: '<div>{{ hi }}</div>'\n})\n```\n\n\n### 3.1.2 Runtime Only\n只包含运行时的代码拥有创建```Vue```实例、渲染并处理```Virtual DOM```等功能，基本上就是除去编译器外的完整代码。```Runtime Only```的适用场景有两种：\n1.我们在选项中通过手写```render```函数去定义渲染过程，这个时候并不需要包含编译器的版本便可完整执行。\n\n```js\n// 不需要编译器\nnew Vue({\n  render (h) {\n    return h('div', this.hi)\n  }\n})\n```\n2.借助```vue-loader```这样的编译工具进行编译，当我们利用```webpack```进行```Vue```的工程化开发时，常常会利用```vue-loader```对```.vue```进行编译，尽管我们也是利用```template```模板标签去书写代码，但是此时的```Vue```已经不需要利用编译器去负责模板的编译工作了，这个过程交给了插件去实现。\n\n\n很明显，编译过程对性能会造成一定的损耗，并且由于加入了编译的流程代码，```Vue```代码的总体积也更加庞大(运行时版本相比完整版体积要小大约 30%)。因此在实际开发中，我们需要借助像```webpack```的```vue-loader```这类工具进行编译，将```Vue```对模板的编译阶段合并到```webpack```的构建流程中，这样不仅减少了生产环境代码的体积，也大大提高了运行时的性能，一举两得。\n\n\n## 3.2 实例挂载的基本思路\n有了上面的基础，我们回头看初始化```_init```的代码，在代码中我们观察到```initProxy```后有一系列的函数调用，这些函数包括了创建组件关联，初始化事件处理，定义渲染函数，构建数据响应式系统等，最后还有一段代码,在```el```存在的情况下，实例会调用```$mount```进行实例挂载。\n```js\nVue.prototype._init = function (options) {\n  ···\n  // 选项合并\n  vm.$options = mergeOptions(\n    resolveConstructorOptions(vm.constructor),\n    options || {},\n    vm\n  );\n  // 数据代理\n  initProxy(vm);\n  vm._self = vm;\n  initLifecycle(vm);\n  // 初始化事件处理\n  initEvents(vm);\n  // 定义渲染函数\n  initRender(vm);\n  // 构建响应式系统\n  initState(vm);\n  // 等等\n  ···\n  if (vm.$options.el) {\n    vm.$mount(vm.$options.el);\n  }\n}\n```\n以手写```template```模板为例，理清楚什么是挂载。**我们会在选项中传递```template```为属性的模板字符串，如```<div>{{message}}</div>```，最终这个模板字符串通过中间过程将其转成真实的```DOM```节点，并挂载到选项中```el```代表的根节点上完成视图渲染。这个中间过程就是接下来要分析的挂载流程。**\n\n\n`Vue`挂载的流程是比较复杂的，接下来我将通过**流程图，代码分析**两种方式为大家展示挂载的真实过程。\n\n### 3.2.1 流程图\n\n![](./img/3.1.png)\n如果用一句话概括挂载的过程，可以描述为**确认挂载节点,编译模板为```render```函数，渲染函数转换```Virtual DOM```,创建真实节点。**\n\n### 3.2.2 代码分析\n接下来我们从代码的角度去剖析挂载的流程。挂载的代码较多，下面只提取骨架相关的部分代码。\n\n```js\n// 内部真正实现挂载的方法\nVue.prototype.$mount = function (el, hydrating) {\n  el = el && inBrowser ? query(el) : undefined;\n  // 调用mountComponent方法挂载\n  return mountComponent(this, el, hydrating)\n};\n// 缓存了原型上的 $mount 方法\nvar mount = Vue.prototype.$mount;\n\n// 重新定义$mount,为包含编译器和不包含编译器的版本提供不同封装，最终调用的是缓存原型上的$mount方法\nVue.prototype.$mount = function (el, hydrating) {\n  // 获取挂载元素\n  el = el && query(el);\n  // 挂载元素不能为跟节点\n  if (el === document.body || el === document.documentElement) {\n    warn(\n      \"Do not mount Vue to <html> or <body> - mount to normal elements instead.\"\n    );\n    return this\n  }\n  var options = this.$options;\n  // 需要编译 or 不需要编译\n  // render选项不存在，代表是template模板的形式，此时需要进行模板的编译过程\n  if (!options.render) {\n    ···\n    // 使用内部编译器编译模板\n  }\n  // 无论是template模板还是手写render函数最终调用缓存的$mount方法\n  return mount.call(this, el, hydrating)\n}\n// mountComponent方法思路\nfunction mountComponent(vm, el, hydrating) {\n  // 定义updateComponent方法，在watch回调时调用。\n  updateComponent = function () {\n    // render函数渲染成虚拟DOM， 虚拟DOM渲染成真实的DOM\n    vm._update(vm._render(), hydrating);\n  };\n  // 实例化渲染watcher\n  new Watcher(vm, updateComponent, noop, {})\n}\n\n```\n\n我们用语言描述挂载流程的基本思路。\n\n- 确定挂载的```DOM```元素,这个```DOM```需要保证不能为```html，body```这类跟节点。\n- 我们知道渲染有两种方式，一种是通过```template```模板字符串，另一种是手写```render```函数，前面提到```template```模板需要运行时进行编译，而后一个可以直接用```render```选项作为渲染函数。因此挂载阶段会有两条分支，```template```模板会先经过模板的解析，最终编译成```render```渲染函数参与实例挂载，而手写```render```函数可以绕过编译阶段，直接调用挂载的```$mount```方法。\n- 针对```template```而言，它会利用```Vue```内部的编译器进行模板的编译，字符串模板会转换为抽象的语法树，即```AST```树，并最终转化为一个类似```function(){with(){}}```的渲染函数，这是我们后面讨论的重点。\n- 无论是```template```模板还是手写```render```函数，最终都将进入```mountComponent```过程,这个阶段会实例化一个渲染```watcher```,具体```watcher```的内容，另外放章节讨论。我们先知道一个结论，渲染```watcher```的回调函数有两个执行时机，一个是在初始化时执行，另一个是当```vm```实例检测到数据发生变化时会再次执行回调函数。\n- 回调函数是执行```updateComponent```的过程，这个方法有两个阶段，一个是```vm._render```,另一个是```vm._update```。 ```vm._render```会执行前面生成的```render```渲染函数，并生成一个```Virtual Dom tree```,而```vm._update```会将这个```Virtual Dom tree```转化为真实的```DOM```节点。\n\n\n\n\n\n## 3.3 模板编译\n通过文章前半段的学习，我们对```Vue```的挂载流程有了一个初略的认识。这里有两个大的流程需要我们详细去理解，一个是```template```模板的编译，另一个是```updateComponent```的实现细节。```updateComponent```的过程，我们放到下一章节重点分析，而这一节剩余的内容我们将会围绕模板编译的设计思路展开。\n\n(编译器的实现细节是异常复杂的，要在短篇幅内将整个编译的过程掌握是不切实际的，并且从大方向上也不需要完全理清编译的流程。因此针对模板，文章分析只是浅尝即止，更多的细节读者可以自行分析)\n\n\n## 3.3.1 template的三种写法\n`template`模板的编写有三种方式，分别是：\n\n- 字符串模板\n\n```js\nvar vm = new Vue({\n  el: '#app',\n  template: '<div>模板字符串</div>'\n})\n```\n-  选择符匹配元素的 ```innerHTML```模板\n\n```js\n<div id=\"app\">\n  <div>test1</div>\n  <script type=\"x-template\" id=\"test\">\n    <p>test</p>\n  </script>\n</div>\nvar vm = new Vue({\n  el: '#app',\n  template: '#test'\n})\n```\n\n- `dom`元素匹配元素的```innerHTML```模板\n\n```js\n<div id=\"app\">\n  <div>test1</div>\n  <span id=\"test\"><div class=\"test2\">test2</div></span>\n</div>\nvar vm = new Vue({\n  el: '#app',\n  template: document.querySelector('#test')\n})\n\n```\n模板编译的前提需要对```template```模板字符串的合法性进行检测，三种写法对应代码的三个不同分支。\n```js\nVue.prototype.$mount = function () {\n  ···\n  if(!options.render) {\n    var template = options.template;\n    if (template) {\n      // 针对字符串模板和选择符匹配模板\n      if (typeof template === 'string') {\n        // 选择符匹配模板，以'#'为前缀的选择器\n        if (template.charAt(0) === '#') {\n          // 获取匹配元素的innerHTML\n          template = idToTemplate(template);\n          /* istanbul ignore if */\n          if (!template) {\n            warn(\n              (\"Template element not found or is empty: \" + (options.template)),\n              this\n            );\n          }\n        }\n      // 针对dom元素匹配\n      } else if (template.nodeType) {\n        // 获取匹配元素的innerHTML\n        template = template.innerHTML;\n      } else {\n        // 其他类型则判定为非法传入\n        {\n          warn('invalid template option:' + template, this);\n        }\n        return this\n      }\n    } else if (el) {\n      // 如果没有传入template模板，则默认以el元素所属的根节点作为基础模板\n      template = getOuterHTML(el);\n    }\n  }\n}\n\n// 判断el元素是否存在\nfunction query (el) {\n    if (typeof el === 'string') {\n      var selected = document.querySelector(el);\n      if (!selected) {\n        warn(\n          'Cannot find element: ' + el\n        );\n        return document.createElement('div')\n      }\n      return selected\n    } else {\n      return el\n    }\n  }\nvar idToTemplate = cached(function (id) {\n  var el = query(id);\n  return el && el.innerHTML\n});\n```\n**注意：其中X-Template模板的方式一般用于模板特别大的 demo 或极小型的应用，官方不建议在其他情形下使用，因为这会将模板和组件的其它定义分离开。**\n\n\n## 3.3.2 编译流程图解\n`vue`源码中编译的设计思路是比较绕，涉及的函数处理逻辑比较多，实现流程中巧妙的运用了偏函数的技巧将配置项处理和编译核心逻辑抽取出来，为了理解这个设计思路，我画了一个逻辑图帮助理解。\n\n![](./img/3.2.png)\n\n\n## 3.3.3 逻辑解析\n即便有流程图，编译逻辑理解起来依然比较晦涩，接下来，结合代码分析每个环节的执行过程。\n```js\nVue.prototype.$mount = function () {\n  ···\n  if(!options.render) {\n    var template = options.template;\n    if (template) {\n      var ref = compileToFunctions(template, {\n          outputSourceRange: \"development\" !== 'production',\n          shouldDecodeNewlines: shouldDecodeNewlines,\n          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,\n          delimiters: options.delimiters,\n          comments: options.comments\n        }, this);\n        var render = ref.render;\n    }\n    ...\n  }\n}\n```\n`compileToFunctions`有三个参数，一个是```template```模板，另一个是编译的配置信息，并且这个方法是对外暴露的编译方法，用户可以自定义配置信息进行模板的编译。最后一个参数是```Vue```实例。\n\n```js\n// 将compileToFunction方法暴露给Vue作为静态方法存在\nVue.compile = compileToFunctions;\n```\n\n在```Vue```的官方文档中，```Vue.compile```只允许传递一个```template```模板参数，这是否意味着用户无法决定某些编译的行为？显然不是的，我们看回代码，有两个选项配置可以提供给用户，用户只需要在实例化```Vue```时传递选项改变配置，他们分别是：\n\n1.`delimiters`： 该选项可以改变纯文本插入分隔符，当不传递值时，```Vue```默认的分隔符为 ```{{}}```。如果我们想使用其他模板，可以通过```delimiters```修改。\n\n2.`comments` ： 当设为 ```true``` 时，将会保留且渲染模板中的 ```HTML```注释。默认行为是舍弃它们。\n\n**注意，由于这两个选项是在完整版的编译流程读取的配置，所以在运行时版本配置这两个选项是无效的**\n\n接着我们一步步寻找```compileToFunctions```的根源。\n\n首先我们需要有一个认知，**不同平台对```Vue```的编译过程是不一样的，也就是说基础的编译方法会随着平台的不同有区别，编译阶段的配置选项也因为平台的不同呈现差异。但是设计者又不希望在相同平台下编译不同模板时，每次都要传入相同的配置选项。这才有了源码中较为复杂的编译实现。**\n\n```js\nvar createCompiler = createCompilerCreator(function baseCompile (template,options) {\n  //把模板解析成抽象的语法树\n  var ast = parse(template.trim(), options);\n  // 配置中有代码优化选项则会对Ast语法树进行优化\n  if (options.optimize !== false) {\n    optimize(ast, options);\n  }\n  var code = generate(ast, options);\n  return {\n    ast: ast,\n    render: code.render,\n    staticRenderFns: code.staticRenderFns\n  }\n});\n\nvar ref$1 = createCompiler(baseOptions);\nvar compile = ref$1.compile;\nvar compileToFunctions = ref$1.compileToFunctions;\n```\n这部分代码是在```Vue```引入阶段定义的，```createCompilerCreator```在传递了一个```baseCompile```函数作为参数后，返回了一个编译器的生成器，也就是```createCompiler```,有了这个生成器，当将编译配置选项```baseOptions```传入后,这个编译器生成器便**生成了一个指定环境指定配置下的编译器**，而其中编译执行函数就是返回对象的```compileToFunctions```。\n\n这里的```baseCompile```是真正执行编译功能的地方，也就是前面说到的特定平台的编译方法。它在源码初始化时就已经作为参数的形式保存在内存变量中。我们先看看```baseCompile```的大致流程。\n\n`baseCompile`函数的参数有两个，一个是后续传入的```template```模板,另一个是编译需要的配置参数。函数实现的功能如下几个：\n- 1.把模板解析成抽象的语法树，简称```AST```，代码中对应```parse```部分。\n- 2.可选：优化```AST```语法树，执行```optimize```方法。\n- 3.根据不同平台将```AST```语法树转换成渲染函数，对应的```generate```函数\n\n\n接下来具体看看```createCompilerCreator```的实现：\n```js\nfunction createCompilerCreator (baseCompile) {\n    return function createCompiler (baseOptions) {\n      // 内部定义compile方法\n      function compile (template, options) {\n        ···\n      }\n      return {\n        compile: compile,\n        compileToFunctions: createCompileToFunctionFn(compile)\n      }\n    }\n  } \n```\n`createCompilerCreator`函数只有一个作用，利用**偏函数**的思想将```baseCompile```这一基础的编译方法缓存，并返回一个编程器生成器，当执行```var ref$1 = createCompiler(baseOptions);```时，```createCompiler```会将内部定义的```compile```和```compileToFunctions```返回。\n\n我们继续关注```compileToFunctions```的由来，它是```createCompileToFunctionFn```函数以```compile```为参数返回的方法，接着看```createCompileToFunctionFn```的实现逻辑。\n\n\n```js\n function createCompileToFunctionFn (compile) {\n    var cache = Object.create(null);\n\n    return function compileToFunctions (template,options,vm) {\n      options = extend({}, options);\n      ···\n      // 缓存的作用：避免重复编译同个模板造成性能的浪费\n      if (cache[key]) {\n        return cache[key]\n      }\n      // 执行编译方法\n      var compiled = compile(template, options);\n      ···\n      // turn code into functions\n      var res = {};\n      var fnGenErrors = [];\n      // 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数\n      res.render = createFunction(compiled.render, fnGenErrors);\n      res.staticRenderFns = compiled.staticRenderFns.map(function (code) {\n        return createFunction(code, fnGenErrors)\n      });\n      ···\n      return (cache[key] = res)\n    }\n  }\n```\n\n`createCompileToFunctionFn`利用了闭包的概念，将编译过的模板进行缓存,```cache```会将之前编译过的结果保留下来，利用缓存可以避免重复编译引起的浪费性能。```createCompileToFunctionFn```最终会将```compileToFunctions```方法返回。\n\n接下来，我们分析一下```compileToFunctions```的实现逻辑。在判断不使用缓存的编译结果后，```compileToFunctions```会执行```compile```方法，这个方法是前面分析```createCompiler```时，返回的内部```compile```方法，所以我们需要先看看```compile```的实现。\n\n```js\nfunction createCompiler (baseOptions) {\n  function compile (template, options) {\n        var finalOptions = Object.create(baseOptions);\n        var errors = [];\n        var tips = [];\n        var warn = function (msg, range, tip) {\n          (tip ? tips : errors).push(msg);\n        };\n        // 选项合并\n        if (options) {\n          ···\n          // 这里会将用户传递的配置和系统自带编译配置进行合并\n        }\n\n        finalOptions.warn = warn;\n        // 将剔除空格后的模板以及合并选项后的配置作为参数传递给baseCompile方法\n        var compiled = baseCompile(template.trim(), finalOptions);\n        {\n          detectErrors(compiled.ast, warn);\n        }\n        compiled.errors = errors;\n        compiled.tips = tips;\n        return compiled\n      }\n      return {\n        compile: compile,\n        compileToFunctions: createCompileToFunctionFn(compile)\n      }\n}\n```\n我们看到```compile```真正执行的方法，是一开始在创建编译器生成器时，传入的基础编译方法```baseCompile```，```baseCompile```真正执行的时候，会将用户传递的编译配置和系统自带的编译配置选项合并，这也是开头提到编译器设计思想的精髓。\n\n执行完```compile```会返回一个对象,```ast```顾名思义是模板解析成的抽象语法树，```render```是最终生成的```with```语句,```staticRenderFns```是以数组形式存在的静态```render```。\n```js\n{\n  ast: ast,\n  render: code.render,\n  staticRenderFns: code.staticRenderFns\n}\n```\n\n而```createCompileToFunctionFn```最终会返回另外两个包装过的属性```render, staticRenderFns```，他们的核心是**将 ```with```语句封装成执行函数。**\n\n```js\n// 编译出的函数体字符串作为参数传递给createFunction,返回最终的render函数\n  res.render = createFunction(compiled.render, fnGenErrors);\n  res.staticRenderFns = compiled.staticRenderFns.map(function (code) {\n    return createFunction(code, fnGenErrors)\n  });\n\n  function createFunction (code, errors) {\n    try {\n      return new Function(code)\n    } catch (err) {\n      errors.push({ err: err, code: code });\n      return noop\n    }\n  }\n```\n\n\n至此，```Vue```中关于编译器的设计思路也基本梳理清楚了，一开始看代码的时候，总觉得编译逻辑的设计特别的绕，分析完代码后发现，这正是作者思路巧妙的地方。```Vue```在不同平台上有不同的编译过程，而每个编译过程的```baseOptions```选项会有所不同，同时也提供了一些选项供用户去配置，整个设计思想深刻的应用了偏函数的设计思想，而偏函数又是闭包的应用。作者利用偏函数将不同平台的编译方式进行缓存，同时剥离出编译相关的选项合并，这些方式都是值得我们日常学习的。\n\n编译的核心是```parse,generate```过程，这两个过程笔者并没有分析，原因是抽象语法树的解析分支较多，需要结合实际的代码场景才更好理解。这两部分的代码会在后面介绍到具体逻辑功能章节时再次提及。\n\n\n\n## 3.4 小结\n这一节的内容有两大块，首先详细的介绍了实例在挂载阶段的完整流程，当我们传入选项进行实例化时，最终的目的是将选项渲染成页面真实的可视节点。这个选项有两种形式，一个是以```template```模板字符串传入，另一个是手写```render```函数形式传入，不论哪种，最终会以```render```函数的形式参与挂载，```render```是一个用函数封装好的```with```语句。渲染真实节点前需要将```render```函数解析成虚拟```DOM```,虚拟```DOM```是```js```和真实```DOM```之间的桥梁。最终的```_update```过程让将虚拟```DOM```渲染成真实节点。第二个大块主要介绍了作者在编译器设计时巧妙的实现思路。过程大量运用了偏函数的概念，将编译过程进行缓存并且将选项合并从编译过程中剥离。这些设计理念、思想都是值得我们开发者学习和借鉴的。"
  },
  {
    "path": "src/彻底搞懂Vue中keep-alive的魔法-上.md",
    "content": "> 前言：上一节最后稍微提到了```Vue```内置组件的相关内容，从这一节开始，将会对某个具体的内置组件进行分析。首先是```keep-alive```，它是我们日常开发中经常使用的组件，我们在不同组件间切换时，经常要求保持组件的状态，以避免重复渲染组件造成的性能损耗，而```keep-alive```经常和上一节介绍的动态组件结合起来使用。由于内容过多，```keep-alive```的源码分析将分为上下两部分，这一节主要围绕```keep-alive```的首次渲染展开。\n\n\n\n## 13.1 基本用法\n`keep-alive`的使用只需要在动态组件的最外层添加标签即可。\n\n```html\n<div id=\"app\">\n    <button @click=\"changeTabs('child1')\">child1</button>\n    <button @click=\"changeTabs('child2')\">child2</button>\n    <keep-alive>\n        <component :is=\"chooseTabs\">\n        </component>\n    </keep-alive>\n</div>\n```\n\n\n```js\n\nvar child1 = {\n    template: '<div><button @click=\"add\">add</button><p>{{num}}</p></div>',\n    data() {\n        return {\n            num: 1\n        }\n    },\n    methods: {\n        add() {\n            this.num++\n        }\n    },\n}\nvar child2 = {\n    template: '<div>child2</div>'\n}\nvar vm = new Vue({\n    el: '#app',\n    components: {\n        child1,\n        child2,\n    },\n    data() {\n        return {\n            chooseTabs: 'child1',\n        }\n    },\n    methods: {\n        changeTabs(tab) {\n            this.chooseTabs = tab;\n        }\n    }\n})\n```\n\n\n\n简单的结果如下，动态组件在```child1,child2```之间来回切换，当第二次切到```child1```时，```child1```保留着原来的数据状态，```num = 5```。\n\n![](./img/13.1.gif)\n\n## 13.2 从模板编译到生成vnode\n按照以往分析的经验，我们会从模板的解析开始说起，第一个疑问便是：内置组件和普通组件在编译过程有区别吗？答案是没有的，不管是内置的还是用户定义组件，本质上组件在模板编译成```render```函数的处理方式是一致的，这里的细节不展开分析，有疑惑的可以参考前几节的原理分析。最终针对```keep-alive```的```render```函数的结果如下：\n\n```js\nwith(this){···_c('keep-alive',{attrs:{\"include\":\"child2\"}},[_c(chooseTabs,{tag:\"component\"})],1)}\n```\n\n\n有了```render```函数，接下来从子开始到父会执行生成```Vnode```对象的过程，```_c('keep-alive'···)```的处理，会执行```createElement```生成组件```Vnode```,其中由于```keep-alive```是组件，所以会调用```createComponent```函数去创建子组件```Vnode```,```createComponent```之前也有分析过，这个环节和创建普通组件```Vnode```不同之处在于，```keep-alive```的```Vnode```会剔除多余的属性内容，**由于```keep-alive```除了```slot```属性之外，其他属性在组件内部并没有意义，例如```class```样式，```<keep-alive clas=\"test\"></keep-alive>```等，所以在```Vnode```层剔除掉多余的属性是有意义的。而```<keep-alive slot=\"test\">```的写法在2.6以上的版本也已经被废弃。**(其中```abstract```作为抽象组件的标志，以及其作用我们后面会讲到)\n\n```js\n// 创建子组件Vnode过程\nfunction createComponent(Ctordata,context,children,tag) {\n    // abstract是内置组件(抽象组件)的标志\n    if (isTrue(Ctor.options.abstract)) {\n        // 只保留slot属性，其他标签属性都被移除，在vnode对象上不再存在\n        var slot = data.slot;\n        data = {};\n        if (slot) {\n            data.slot = slot;\n        }\n    }\n}\n```\n\n## 13.3 初次渲染\n\n`keep-alive`之所以特别，是因为它不会重复渲染相同的组件，只会利用初次渲染保留的缓存去更新节点。所以为了全面了解它的实现原理，我们需要从```keep-alive```的首次渲染开始说起。\n\n\n### 13.3.1 流程图\n为了理清楚流程，我大致画了一个流程图，流程图大致覆盖了初始渲染```keep-alive```所执行的过程，接下来会照着这个过程进行源码分析。\n\n![](./img/13.2.png)\n\n和渲染普通组件相同的是，```Vue```会拿到前面生成的```Vnode```对象执行真实节点创建的过程，也就是熟悉的```patch```过程,```patch```执行阶段会调用```createElm```创建真实```dom```，在创建节点途中，```keep-alive```的```vnode```对象会被认定是一个组件```Vnode```,因此针对组件```Vnode```又会执行```createComponent```函数，它会对```keep-alive```组件进行初始化和实例化。\n\n```js\nfunction createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {\n      var i = vnode.data;\n      if (isDef(i)) {\n        // isReactivated用来判断组件是否缓存。\n        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;\n        if (isDef(i = i.hook) && isDef(i = i.init)) {\n            // 执行组件初始化的内部钩子 init\n          i(vnode, false /* hydrating */);\n        }\n        if (isDef(vnode.componentInstance)) {\n          // 其中一个作用是保留真实dom到vnode中\n          initComponent(vnode, insertedVnodeQueue);\n          insert(parentElm, vnode.elm, refElm);\n          if (isTrue(isReactivated)) {\n            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);\n          }\n          return true\n        }\n      }\n    }\n```\n`keep-alive`组件会先调用内部钩子```init```方法进行初始化操作，我们先看看```init```过程做了什么操作。\n\n```js\n// 组件内部钩子\nvar componentVNodeHooks = {\n    init: function init (vnode, hydrating) {\n      if (\n        vnode.componentInstance &&\n        !vnode.componentInstance._isDestroyed &&\n        vnode.data.keepAlive\n      ) {\n        // kept-alive components, treat as a patch\n        var mountedNode = vnode; // work around flow\n        componentVNodeHooks.prepatch(mountedNode, mountedNode);\n      } else {\n          // 将组件实例赋值给vnode的componentInstance属性\n        var child = vnode.componentInstance = createComponentInstanceForVnode(\n          vnode,\n          activeInstance\n        );\n        child.$mount(hydrating ? vnode.elm : undefined, hydrating);\n      }\n    },\n    // 后面分析\n    prepatch： function() {}\n}\n```\n第一次执行，很明显组件```vnode```没有```componentInstance```属性，```vnode.data.keepAlive```也没有值，所以会**调用```createComponentInstanceForVnode```方法进行组件实例化并将组件实例赋值给```vnode```的```componentInstance```属性，** 最终执行组件实例的```$mount```方法进行实例挂载。\n\n`createComponentInstanceForVnode`就是组件实例化的过程，而组件实例化从系列的第一篇就开始说了，无非就是一系列选项合并，初始化事件，生命周期等初始化操作。\n\n```js\nfunction createComponentInstanceForVnode (vnode, parent) {\n    var options = {\n      _isComponent: true,\n      _parentVnode: vnode,\n      parent: parent\n    };\n    // 内联模板的处理，忽略这部分代码\n    ···\n    // 执行vue子组件实例化\n    return new vnode.componentOptions.Ctor(options)\n  }\n```\n\n### 13.3.2 内置组件选项\n我们在使用组件的时候经常利用对象的形式定义组件选项，包括```data,method,computed```等，并在父组件或根组件中注册。```keep-alive```同样遵循这个道理，内置两字也说明了```keep-alive```是在```Vue```源码中内置好的选项配置，并且也已经注册到全局，这一部分的源码可以参考组态组件小节末尾对内置组件构造器和注册过程的介绍。这一部分我们重点关注一下```keep-alive```的具体选项。\n\n```js\n// keepalive组件选项\n  var KeepAlive = {\n    name: 'keep-alive',\n    // 抽象组件的标志\n    abstract: true,\n    // keep-alive允许使用的props\n    props: {\n      include: patternTypes,\n      exclude: patternTypes,\n      max: [String, Number]\n    },\n\n    created: function created () {\n      // 缓存组件vnode\n      this.cache = Object.create(null);\n      // 缓存组件名\n      this.keys = [];\n    },\n\n    destroyed: function destroyed () {\n      for (var key in this.cache) {\n        pruneCacheEntry(this.cache, key, this.keys);\n      }\n    },\n\n    mounted: function mounted () {\n      var this$1 = this;\n      // 动态include和exclude\n      // 对include exclue的监听\n      this.$watch('include', function (val) {\n        pruneCache(this$1, function (name) { return matches(val, name); });\n      });\n      this.$watch('exclude', function (val) {\n        pruneCache(this$1, function (name) { return !matches(val, name); });\n      });\n    },\n    // keep-alive的渲染函数\n    render: function render () {\n      // 拿到keep-alive下插槽的值\n      var slot = this.$slots.default;\n      // 第一个vnode节点\n      var vnode = getFirstComponentChild(slot);\n      // 拿到第一个组件实例\n      var componentOptions = vnode && vnode.componentOptions;\n      // keep-alive的第一个子组件实例存在\n      if (componentOptions) {\n        // check pattern\n        //拿到第一个vnode节点的name\n        var name = getComponentName(componentOptions);\n        var ref = this;\n        var include = ref.include;\n        var exclude = ref.exclude;\n        // 通过判断子组件是否满足缓存匹配\n        if (\n          // not included\n          (include && (!name || !matches(include, name))) ||\n          // excluded\n          (exclude && name && matches(exclude, name))\n        ) {\n          return vnode\n        }\n\n        var ref$1 = this;\n        var cache = ref$1.cache;\n        var keys = ref$1.keys;\n        var key = vnode.key == null\n          ? componentOptions.Ctor.cid + (componentOptions.tag ? (\"::\" + (componentOptions.tag)) : '')\n          : vnode.key;\n          // 再次命中缓存\n        if (cache[key]) {\n          vnode.componentInstance = cache[key].componentInstance;\n          // make current key freshest\n          remove(keys, key);\n          keys.push(key);\n        } else {\n        // 初次渲染时，将vnode缓存\n          cache[key] = vnode;\n          keys.push(key);\n          // prune oldest entry\n          if (this.max && keys.length > parseInt(this.max)) {\n            pruneCacheEntry(cache, keys[0], keys, this._vnode);\n          }\n        }\n        // 为缓存组件打上标志\n        vnode.data.keepAlive = true;\n      }\n      // 将渲染的vnode返回\n      return vnode || (slot && slot[0])\n    }\n  };\n```\n`keep-alive`选项跟我们平时写的组件选项还是基本类似的，唯一的不同是```keep-ailve```组件没有用```template```而是使用```render```函数。```keep-alive```本质上只是存缓存和拿缓存的过程，并没有实际的节点渲染，所以使用```render```处理是最优的选择。\n\n### 13.3.3 缓存vnode\n\n还是先回到流程图的分析。上面说到```keep-alive```在执行组件实例化之后会进行组件的挂载。而挂载```$mount```又回到```vm._render(),vm._update()```的过程。由于```keep-alive```拥有```render```函数，所以我们可以直接将焦点放在```render```函数的实现上。\n\n- 首先是获取```keep-alive```下插槽的内容，也就是```keep-alive```需要渲染的子组件,例子中是```chil1 Vnode```对象，源码中对应```getFirstComponentChild```函数。\n\n```js\n  function getFirstComponentChild (children) {\n    if (Array.isArray(children)) {\n      for (var i = 0; i < children.length; i++) {\n        var c = children[i];\n        // 组件实例存在，则返回，理论上返回第一个组件vnode\n        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {\n          return c\n        }\n      }\n    }\n  }\n```\n\n- 判断组件满足缓存的匹配条件，在```keep-alive```组件的使用过程中，```Vue```源码允许我们是用```include, exclude```来定义匹配条件，```include```规定了只有名称匹配的组件才会被缓存，```exclude```规定了任何名称匹配的组件都不会被缓存。更者，我们可以使用```max```来限制可以缓存多少匹配实例，而为什么要做数量的限制呢？我们后文会提到。\n\n拿到子组件的实例后，我们需要先进行是否满足匹配条件的判断,**其中匹配的规则允许使用数组，字符串，正则的形式。**\n\n```js\nvar include = ref.include;\nvar exclude = ref.exclude;\n// 通过判断子组件是否满足缓存匹配\nif (\n    // not included\n    (include && (!name || !matches(include, name))) ||\n    // excluded\n    (exclude && name && matches(exclude, name))\n) {\n    return vnode\n}\n\n// matches\nfunction matches (pattern, name) {\n    // 允许使用数组['child1', 'child2']\n    if (Array.isArray(pattern)) {\n        return pattern.indexOf(name) > -1\n    } else if (typeof pattern === 'string') {\n        // 允许使用字符串 child1,child2\n        return pattern.split(',').indexOf(name) > -1\n    } else if (isRegExp(pattern)) {\n        // 允许使用正则 /^child{1,2}$/g\n        return pattern.test(name)\n    }\n    /* istanbul ignore next */\n    return false\n}\n```\n\n如果组件不满足缓存的要求，则直接返回组件的```vnode```,不做任何处理,此时组件会进入正常的挂载环节。\n\n3. `render`函数执行的关键一步是缓存```vnode```,由于是第一次执行```render```函数，选项中的```cache```和```keys```数据都没有值，其中```cache```是一个空对象，我们将用它来缓存```{ name: vnode }```枚举，而```keys```我们用来缓存组件名。\n**因此我们在第一次渲染```keep-alive```时，会将需要渲染的子组件```vnode```进行缓存。**\n```js\n    cache[key] = vnode;\n    keys.push(key);\n```\n\n4. 将已经缓存的```vnode```打上标记, 并将子组件的```Vnode```返回。\n```vnode.data.keepAlive = true```\n\n\n### 13.3.4 真实节点的保存\n\n我们再回到```createComponent```的逻辑，之前提到```createComponent```会先执行```keep-alive```组件的初始化流程，也包括了子组件的挂载。并且我们通过```componentInstance```拿到了```keep-alive```组件的实例，而接下来**重要的一步是将真实的```dom```保存再```vnode```中**。\n\n```js\nfunction createComponent(vnode, insertedVnodeQueue) {\n    ···\n    if (isDef(vnode.componentInstance)) {\n        // 其中一个作用是保留真实dom到vnode中\n        initComponent(vnode, insertedVnodeQueue);\n        // 将真实节点添加到父节点中\n        insert(parentElm, vnode.elm, refElm);\n        if (isTrue(isReactivated)) {\n            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);\n        }\n        return true\n    }\n}\n```\n`insert`的源码不列举出来，它只是简单的调用操作```dom```的```api```,将子节点插入到父节点中，我们可以重点看看```initComponent```关键步骤的逻辑。\n\n```js\nfunction initComponent() {\n    ···\n    // vnode保留真实节点\n    vnode.elm = vnode.componentInstance.$el;\n    ···\n}\n```\n\n**因此，我们很清晰的回到之前遗留下来的问题，为什么```keep-alive```需要一个```max```来限制缓存组件的数量。原因就是```keep-alive```缓存的组件数据除了包括```vnode```这一描述对象外，还保留着真实的```dom```节点,而我们知道真实节点对象是庞大的，所以大量保留缓存组件是耗费性能的。因此我们需要严格控制缓存的组件数量，而在缓存策略上也需要做优化，这点我们在下一篇文章也继续提到。**\n\n由于```isReactivated```为```false```,```reactivateComponent```函数也不会执行。至此```keep-alive```的初次渲染流程分析完毕。\n\n**如果忽略步骤的分析，只对初次渲染流程做一个总结：内置的```keep-alive```组件，让子组件在第一次渲染的时候将```vnode```和真实的```elm```进行了缓存。**\n\n\n\n## 13.4 抽象组件\n这一节的最后顺便提一下上文提到的抽象组件的概念。```Vue```提供的内置组件都有一个描述组件类型的选项，这个选项就是```{ astract: true }```,它表明了该组件是抽象组件。什么是抽象组件，为什么要有这一类型的区别呢？我觉得归根究底有两个方面的原因。\n1. 抽象组件没有真实的节点，它在组件渲染阶段不会去解析渲染成真实的```dom```节点，而只是作为中间的数据过渡层处理，在```keep-alive```中是对组件缓存的处理。\n2. 在我们介绍组件初始化的时候曾经说到父子组件会显式的建立一层关系，这层关系奠定了父子组件之间通信的基础。我们可以再次回顾一下```initLifecycle```的代码。\n\n```js\nVue.prototype._init = function() {\n    ···\n    var vm = this;\n    initLifecycle(vm)\n}\n\nfunction initLifecycle (vm) {\n    var options = vm.$options;\n    \n    var parent = options.parent;\n    if (parent && !options.abstract) {\n        // 如果有abstract属性，一直往上层寻找，直到不是抽象组件\n      while (parent.$options.abstract && parent.$parent) {\n        parent = parent.$parent;\n      }\n      parent.$children.push(vm);\n    }\n    ···\n  }\n```\n子组件在注册阶段会把父实例挂载到自身选项的```parent```属性上，在```initLifecycle```过程中，会反向拿到```parent```上的父组件```vnode```,并为其```$children```属性添加该子组件```vnode```,如果在反向找父组件的过程中，父组件拥有```abstract```属性，即可判定该组件为抽象组件，此时利用```parent```的链条往上寻找，直到组件不是抽象组件为止。```initLifecycle```的处理，让每个组件都能找到上层的父组件以及下层的子组件，使得组件之间形成一个紧密的关系树。\n\n### 13.5 小结\n这一节介绍了```Vue```内置组件中一个最重要的，也是最常用的组件```keep-alive```，在日常开发中，我们经常将```keep-alive```配合动态组件```is```使用，达到切换组件的同时，将旧的组件缓存。最终达到保留初始状态的目的。在第一次组件渲染时，```keep-alive```会将组件```Vnode```以及对应的真实节点进行缓存。而当再次渲染组件时，```keep-alive```是如何利用这些缓存的？源码又对缓存进行了优化？并且```keep-alive```组件的生命周期又包括哪些，这些疑问我们将在下一节一一展开。\n"
  },
  {
    "path": "src/彻底搞懂Vue中keep-alive的魔法-下.md",
    "content": "> 上一节，我们对```keep-alive```组件的初始渲染流程以及组件的配置信息进行了源码分析。初始渲染流程最关键的一步是对渲染的组件```Vnode```进行缓存，其中也包括了组件的真实节点存储。有了第一次的缓存，当再次渲染组件时，```keep-alive```又拥有哪些魔法呢？接下来我们将彻底揭开这一层面纱。\n\n## 13.5 准备工作\n上一节对```keep-alive```组件的分析，是从我画的一个流程图开始的。如果不想回过头看上一节的内容，可以参考以下的简单总结。\n1. `keep-alive`是源码内部定义的组件选项配置，它会先注册为全局组件供开发者全局使用，其中```render```函数定义了它的渲染过程\n2. 和普通组件一致，当父在创建真实节点的过程中，遇到```keep-alive```的组件会进行组件的初始化和实例化。\n3. 实例化会执行挂载```$mount```的过程，这一步会执行```keep-alive```选项中的```render```函数。\n4. `render`函数在初始渲染时，会将渲染的子```Vnode```进行缓存。同时**对应的子真实节点也会被缓存起来**。\n\n那么，当再次需要渲染到已经被渲染过的组件时，```keep-alive```的处理又有什么不同呢？\n\n### 13.5.1 基础使用\n为了文章的完整性，我依旧把基础的使用展示出来，其中加入了生命周期的使用，方便后续对```keep-alive```生命周期的分析。\n```html\n<div id=\"app\">\n    <button @click=\"changeTabs('child1')\">child1</button>\n    <button @click=\"changeTabs('child2')\">child2</button>\n    <keep-alive>\n        <component :is=\"chooseTabs\">\n        </component>\n    </keep-alive>\n</div>\n```\n```js\nvar child1 = {\n    template: '<div><button @click=\"add\">add</button><p>{{num}}</p></div>',\n    data() {\n        return {\n            num: 1\n        }\n    },\n    methods: {\n        add() {\n            this.num++\n        }\n    },\n    mounted() {\n        console.log('child1 mounted')\n    },\n    activated() {\n        console.log('child1 activated')\n    },\n    deactivated() {\n        console.log('child1 deactivated')\n    },\n    destoryed() {\n        console.log('child1 destoryed')\n    }\n}\nvar child2 = {\n    template: '<div>child2</div>',\n    mounted() {\n        console.log('child2 mounted')\n    },\n    activated() {\n        console.log('child2 activated')\n    },\n    deactivated() {\n        console.log('child2 deactivated')\n    },\n    destoryed() {\n        console.log('child2 destoryed')\n    }\n}\n\nvar vm = new Vue({\n    el: '#app',\n    components: {\n        child1,\n        child2,\n    },\n    data() {\n        return {\n            chooseTabs: 'child1',\n        }\n    },\n    methods: {\n        changeTabs(tab) {\n            this.chooseTabs = tab;\n        }\n    }\n})\n```\n### 13.5.2 流程图\n和首次渲染的分析一致，再次渲染的过程我依旧画了一个简单的流程图。\n\n![](./img/13.3.png)\n\n## 13.6 流程分析\n### 13.6.1 重新渲染组件\n再次渲染的流程从数据改变说起，在这个例子中，动态组件中```chooseTabs```数据的变化会引起依赖派发更新的过程(这个系列有三篇文章详细介绍了vue响应式系统的底层实现，感兴趣的同学可以借鉴)。简单来说，```chooseTabs```这个数据在初始化阶段会收集使用到该数据的相关依赖。当数据发生改变时，收集过的依赖会进行派发更新操作。\n\n其中，父组件中负责实例挂载的过程作为依赖会被执行，即执行父组件的```vm._update(vm._render(), hydrating);```。```_render```和```_update```分别代表两个过程，其中```_render```函数会根据数据的变化为组件生成新的```Vnode```节点，而```_update```最终会为新的```Vnode```生成真实的节点。而在生成真实节点的过程中，会利用```vitrual dom```的```diff```算法对前后```vnode```节点进行对比，使之尽可能少的更改真实节点，这一部分内容可以回顾[深入剖析Vue源码 - 来，跟我一起实现diff算法!](https://juejin.im/post/5d3967a56fb9a07efc49cca1)，里面详细阐述了利用```diff```算法进行节点差异对比的思路。\n\n`patch`是新旧```Vnode```节点对比的过程，而```patchVnode```是其中核心的步骤，我们忽略```patchVnode```其他的流程，关注到其中对子组件执行```prepatch```钩子的过程中。\n\n```js\nfunction patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {\n    ···\n    // 新vnode  执行prepatch钩子\n    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {\n        i(oldVnode, vnode);\n    }\n    ···\n}\n```\n执行```prepatch```钩子时会拿到新旧组件的实例并执行```updateChildComponent```函数。而```updateChildComponent```会对针对新的组件实例对旧实例进行状态的更新，包括```props,listeners```等，最终会**调用```vue```提供的全局```vm.$forceUpdate()```方法进行实例的重新渲染。**\n\n```js\nvar componentVNodeHooks = {\n    // 之前分析的init钩子 \n    init: function() {},\n    prepatch: function prepatch (oldVnode, vnode) {\n        // 新组件实例\n      var options = vnode.componentOptions;\n      // 旧组件实例\n      var child = vnode.componentInstance = oldVnode.componentInstance;\n      updateChildComponent(\n        child,\n        options.propsData, // updated props\n        options.listeners, // updated listeners\n        vnode, // new parent vnode\n        options.children // new children\n      );\n    },\n}\n\nfunction updateChildComponent() {\n    // 更新旧的状态，不分析这个过程\n    ···\n    // 迫使实例重新渲染。\n    vm.$forceUpdate();\n}\n```\n\n先看看```$forceUpdate```做了什么操作。```$forceUpdate```是源码对外暴露的一个api，他们迫使```Vue```实例重新渲染，本质上是执行实例所收集的依赖，在例子中```watcher```对应的是```keep-alive```的```vm._update(vm._render(), hydrating);```过程。\n```js\nVue.prototype.$forceUpdate = function () {\n    var vm = this;\n    if (vm._watcher) {\n      vm._watcher.update();\n    }\n  };\n```\n\n### 13.6.2 重用缓存组件\n由于```vm.$forceUpdate()```会强迫```keep-alive```组件进行重新渲染，因此```keep-alive```组件会再一次执行```render```过程。这一次由于第一次对```vnode```的缓存，```keep-alive```在实例的```cache```对象中找到了缓存的组件。\n\n```js\n// keepalive组件选项\nvar keepAlive = {\n    name: 'keep-alive',\n    abstract: true,\n    render: function render () {\n      // 拿到keep-alive下插槽的值\n      var slot = this.$slots.default;\n      // 第一个vnode节点\n      var vnode = getFirstComponentChild(slot);\n      // 拿到第一个组件实例\n      var componentOptions = vnode && vnode.componentOptions;\n      // keep-alive的第一个子组件实例存在\n      if (componentOptions) {\n        // check pattern\n        //拿到第一个vnode节点的name\n        var name = getComponentName(componentOptions);\n        var ref = this;\n        var include = ref.include;\n        var exclude = ref.exclude;\n        // 通过判断子组件是否满足缓存匹配\n        if (\n          // not included\n          (include && (!name || !matches(include, name))) ||\n          // excluded\n          (exclude && name && matches(exclude, name))\n        ) {\n          return vnode\n        }\n\n        var ref$1 = this;\n        var cache = ref$1.cache;\n        var keys = ref$1.keys;\n        var key = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? (\"::\" + (componentOptions.tag)) : '')\n          : vnode.key;\n          // ==== 关注点在这里 ====\n        if (cache[key]) {\n          // 直接取出缓存组件\n          vnode.componentInstance = cache[key].componentInstance;\n          // keys命中的组件名移到数组末端\n          remove(keys, key);\n          keys.push(key);\n        } else {\n        // 初次渲染时，将vnode缓存\n          cache[key] = vnode;\n          keys.push(key);\n          // prune oldest entry\n          if (this.max && keys.length > parseInt(this.max)) {\n            pruneCacheEntry(cache, keys[0], keys, this._vnode);\n          }\n        }\n\n        vnode.data.keepAlive = true;\n      }\n      return vnode || (slot && slot[0])\n    }\n}\n\n```\n`render`函数前面逻辑可以参考前一篇文章，由于```cache```对象中存储了再次使用的```vnode```对象，所以直接通过```cache[key]```取出缓存的组件实例并赋值给```vnode```的```componentInstance```属性。可能在读到这里的时候，会对源码中```keys```这个数组的作用，以及```pruneCacheEntry```的功能有疑惑，这里我们放到文章末尾讲缓存优化策略时解答。\n\n\n### 13.6.3 真实节点的替换\n\n执行了```keep-alive```组件的```_render```过程，接下来是```_update```产生真实的节点，同样的，```keep-alive```下有```child1```子组件，所以```_update```过程会调用```createComponent```递归创建子组件```vnode```,这个过程在初次渲染时也有分析过，我们可以对比一下，再次渲染时流程有哪些不同。\n\n```js\nfunction createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {\n    // vnode为缓存的vnode\n      var i = vnode.data;\n      if (isDef(i)) {\n        // 此时isReactivated为true\n        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;\n        if (isDef(i = i.hook) && isDef(i = i.init)) {\n          i(vnode, false /* hydrating */);\n        }\n        if (isDef(vnode.componentInstance)) {\n          // 其中一个作用是保留真实dom到vnode中\n          initComponent(vnode, insertedVnodeQueue);\n          insert(parentElm, vnode.elm, refElm);\n          if (isTrue(isReactivated)) {\n            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);\n          }\n          return true\n        }\n      }\n    }\n```\n**此时的```vnode```是缓存取出的子组件```vnode```**，并且由于在第一次渲染时对组件进行了标记```vnode.data.keepAlive = true;```,所以```isReactivated```的值为```true```,```i.init```依旧会执行子组件的初始化过程。但是这个过程由于有缓存，所以执行过程也不完全相同。\n\n```js\nvar componentVNodeHooks = {\n    init: function init (vnode, hydrating) {\n      if (\n        vnode.componentInstance &&\n        !vnode.componentInstance._isDestroyed &&\n        vnode.data.keepAlive\n      ) {\n        // 当有keepAlive标志时，执行prepatch钩子\n        var mountedNode = vnode; // work around flow\n        componentVNodeHooks.prepatch(mountedNode, mountedNode);\n      } else {\n        var child = vnode.componentInstance = createComponentInstanceForVnode(\n          vnode,\n          activeInstance\n        );\n        child.$mount(hydrating ? vnode.elm : undefined, hydrating);\n      }\n    },\n}\n```\n显然因为有```keepAlive```的标志，所以子组件不再走挂载流程，只是执行```prepatch```钩子对组件状态进行更新。并且很好的利用了缓存```vnode```之前保留的真实节点进行节点的替换。\n\n\n## 13.7 生命周期\n我们通过例子来观察```keep-alive```生命周期和普通组件的不同。\n\n![](./img/13.4.gif)\n\n在我们从```child1```切换到```child2```,再切回```child1```过程中，```chil1```不会再执行```mounted```钩子，只会执行```activated```钩子，而```child2```也不会执行```destoryed```钩子，只会执行```deactivated```钩子，这是为什么？```child2```的```deactivated```钩子又要比```child1```的```activated```提前执行，这又是为什么？\n\n### 13.7.1 deactivated\n我们先从组件的销毁开始说起，当```child1```切换到```child2```时，```child1```会执行```deactivated```钩子而不是```destoryed```钩子，这是为什么？\n前面分析```patch```过程会对新旧节点的改变进行对比，从而尽可能范围小的去操作真实节点，当完成```diff```算法并对节点操作完毕后，接下来还有一个重要的步骤是**对旧的组件执行销毁移除操作**。这一步的代码如下：\n\n```js\nfunction patch(···) {\n  // 分析过的patchVnode过程\n  // 销毁旧节点\n  if (isDef(parentElm)) {\n    removeVnodes(parentElm, [oldVnode], 0, 0);\n  } else if (isDef(oldVnode.tag)) {\n    invokeDestroyHook(oldVnode);\n  }\n}\n\nfunction removeVnodes (parentElm, vnodes, startIdx, endIdx) {\n  // startIdx,endIdx都为0\n  for (; startIdx <= endIdx; ++startIdx) {\n    // ch 会拿到需要销毁的组件\n    var ch = vnodes[startIdx];\n    if (isDef(ch)) {\n      if (isDef(ch.tag)) {\n        // 真实节点的移除操作\n        removeAndInvokeRemoveHook(ch);\n        invokeDestroyHook(ch);\n      } else { // Text node\n        removeNode(ch.elm);\n      }\n    }\n  }\n}\n```\n\n`removeAndInvokeRemoveHook`会对旧的节点进行移除操作，其中关键的一步是会将真实节点从父元素中删除，有兴趣可以自行查看这部分逻辑。```invokeDestroyHook```是执行销毁组件钩子的核心。如果该组件下存在子组件，会递归去调用```invokeDestroyHook```执行销毁操作。销毁过程会执行组件内部的```destory```钩子。\n```js\nfunction invokeDestroyHook (vnode) {\n    var i, j;\n    var data = vnode.data;\n    if (isDef(data)) {\n      if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); }\n      // 执行组件内部destroy钩子\n      for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); }\n    }\n    // 如果组件存在子组件，则遍历子组件去递归调用invokeDestoryHook执行钩子\n    if (isDef(i = vnode.children)) {\n      for (j = 0; j < vnode.children.length; ++j) {\n        invokeDestroyHook(vnode.children[j]);\n      }\n    }\n  }\n```\n组件内部钩子前面已经介绍了```init```和```prepatch```钩子，而```destroy```钩子的逻辑更加简单。\n```js\nvar componentVNodeHooks = {\n  destroy: function destroy (vnode) {\n    // 组件实例\n    var componentInstance = vnode.componentInstance;\n    // 如果实例还未被销毁\n    if (!componentInstance._isDestroyed) {\n      // 不是keep-alive组件则执行销毁操作\n      if (!vnode.data.keepAlive) {\n        componentInstance.$destroy();\n      } else {\n        // 如果是已经缓存的组件\n        deactivateChildComponent(componentInstance, true /* direct */);\n      }\n    }\n  }\n}\n```\n当组件是```keep-alive```缓存过的组件，即已经用```keepAlive```标记过，则不会执行实例的销毁，即```componentInstance.$destroy()```的过程。```$destroy```过程会做一系列的组件销毁操作，其中的```beforeDestroy,destoryed```钩子也是在```$destory```过程中调用，而```deactivateChildComponent```的处理过程却完全不同。\n\n```js\nfunction deactivateChildComponent (vm, direct) {\n  if (direct) {\n    // \n    vm._directInactive = true;\n    if (isInInactiveTree(vm)) {\n      return\n    }\n  }\n  if (!vm._inactive) {\n    // 已经被停用\n    vm._inactive = true;\n    // 对子组件同样会执行停用处理\n    for (var i = 0; i < vm.$children.length; i++) {\n      deactivateChildComponent(vm.$children[i]);\n    }\n    // 最终调用deactivated钩子\n    callHook(vm, 'deactivated');\n  }\n}\n```\n`_directInactive`是用来标记这个被打上停用标签的组件是否是最顶层的组件。而```_inactive```是停用的标志，同样的子组件也需要递归去调用```deactivateChildComponent```,打上停用的标记。**最终会执行用户定义的```deactivated```钩子。**\n\n### 13.7.2 activated\n现在回过头看看```activated```的执行时机，同样是```patch```过程，在对旧节点移除并执行销毁或者停用的钩子后，对新节点也会执行相应的钩子。**这也是停用的钩子比启用的钩子先执行的原因。**\n```js\nfunction patch(···) {\n  // patchVnode过程\n  // 销毁旧节点\n  {\n    if (isDef(parentElm)) {\n      removeVnodes(parentElm, [oldVnode], 0, 0);\n    } else if (isDef(oldVnode.tag)) {\n      invokeDestroyHook(oldVnode);\n    }\n  }\n  // 执行组件内部的insert钩子\n  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);\n}\n\nfunction invokeInsertHook (vnode, queue, initial) {\n  // delay insert hooks for component root nodes, invoke them after the\n  // 当节点已经被插入时，会延迟执行insert钩子\n  if (isTrue(initial) && isDef(vnode.parent)) {\n    vnode.parent.data.pendingInsert = queue;\n  } else {\n    for (var i = 0; i < queue.length; ++i) {\n      queue[i].data.hook.insert(queue[i]);\n    }\n  }\n}\n```\n同样的组件内部的```insert```钩子逻辑如下：\n```js\n// 组件内部自带钩子\n  var componentVNodeHooks = {\n    insert: function insert (vnode) {\n      var context = vnode.context;\n      var componentInstance = vnode.componentInstance;\n      // 实例已经被挂载\n      if (!componentInstance._isMounted) {\n        componentInstance._isMounted = true;\n        callHook(componentInstance, 'mounted');\n      }\n      if (vnode.data.keepAlive) {\n        if (context._isMounted) {\n          // vue-router#1212\n          // During updates, a kept-alive component's child components may\n          // change, so directly walking the tree here may call activated hooks\n          // on incorrect children. Instead we push them into a queue which will\n          // be processed after the whole patch process ended.\n          queueActivatedComponent(componentInstance);\n        } else {\n          activateChildComponent(componentInstance, true /* direct */);\n        }\n      }\n    },\n  }\n```\n当第一次实例化组件时，由于实例的```_isMounted```不存在，所以会调用```mounted```钩子，当我们从```child2```再次切回```child1```时，由于```child1```只是被停用而没有被销毁，所以不会再调用```mounted```钩子，此时会执行```activateChildComponent```函数对组件的状态进行处理。有了分析```deactivateChildComponent```的基础，```activateChildComponent```的逻辑也很好理解，同样的```_inactive```标记为已启用，并且对子组件递归调用```activateChildComponent```做状态处理。\n```js\nfunction activateChildComponent (vm, direct) {\n  if (direct) {\n    vm._directInactive = false;\n    if (isInInactiveTree(vm)) {\n      return\n    }\n  } else if (vm._directInactive) {\n    return\n  }\n  if (vm._inactive || vm._inactive === null) {\n    vm._inactive = false;\n    for (var i = 0; i < vm.$children.length; i++) {\n      activateChildComponent(vm.$children[i]);\n    }\n    callHook(vm, 'activated');\n  }\n}\n```\n\n## 13.8 缓存优化 - LRU\n程序的内存空间是有限的，所以我们无法无节制的对数据进行存储，这时候需要有策略去淘汰不那么重要的数据，保持最大数据存储量的一致。这种类型的策略称为缓存优化策略，根据淘汰的机制不同，常用的有以下三类。\n\n**1.FIFO： 先进先出策略，我们通过记录数据使用的时间，当缓存大小即将溢出时，优先清除离当前时间最远的数据。**\n\n**2.LRU： 最近最少使用。LRU策略遵循的原则是，如果数据最近被访问(使用)过，那么将来被访问的几率会更高，如果以一个数组去记录数据，当有一数据被访问时，该数据会被移动到数组的末尾，表明最近被使用过，当缓存溢出时，会删除数组的头部数据，即将最不频繁使用的数据移除。**\n\n**3.LFU: 计数最少策略。用次数去标记数据使用频率，次数最少的会在缓存溢出时被淘汰。**\n\n\n这三种缓存算法各有优劣，各自适用不同场景，而我们看```keep-alive```在缓存时的优化处理，很明显利用了```LRU```的缓存策略。我们看关键的代码\n```js\nvar keepAlive = {\n  render: function() {\n    ···\n    if (cache[key]) {\n      vnode.componentInstance = cache[key].componentInstance;\n      remove(keys, key);\n      keys.push(key);\n    } else {\n      cache[key] = vnode;\n      keys.push(key);\n      if (this.max && keys.length > parseInt(this.max)) {\n        pruneCacheEntry(cache, keys[0], keys, this._vnode);\n      }\n    }\n  }\n}\n\nfunction remove (arr, item) {\n  if (arr.length) {\n    var index = arr.indexOf(item);\n    if (index > -1) {\n      return arr.splice(index, 1)\n    }\n  }\n}\n```\n结合一个实际的例子分析缓存逻辑的实现。\n1.有三个组件```child1,child2,child3```,```keep-alive```的最大缓存个数设置为2\n2.用```cache```对象去存储组件```vnode```,```key```为组件名字，```value```为组件```vnode```对象，用```keys```数组去记录组件名字，由于是数组，所以```keys```为有序。\n3.`child1,child2`组件依次访问，缓存结果为\n```js\nkeys = ['child1', 'child2']\ncache = {\n  child1: child1Vnode,\n  child2: child2Vnode\n}\n```\n4.再次访问到```child1```组件，由于命中了缓存，会调用```remove```方法把```keys```中的```child1```删除，并通过数组的```push```方法将```child1```推到尾部。缓存结果修改为\n```js\nkeys = ['child2', 'child1']\ncache = {\n  child1: child1Vnode,\n  child2: child2Vnode\n}\n```\n5.访问到```child3```时，由于缓存个数限制，初次缓存会执行```pruneCacheEntry```方法对最少访问到的数据进行删除。```pruneCacheEntry```的定义如下\n```js\nfunction pruneCacheEntry (cache,key,keys,current) {\n    var cached###1 = cache[key];\n    // 销毁实例\n    if (cached###1 && (!current || cached###1.tag !== current.tag)) {\n      cached###1.componentInstance.$destroy();\n    }\n    cache[key] = null;\n    remove(keys, key);\n  }\n\n```\n删除缓存时会把```keys[0]```代表的组件删除，由于之前的处理，最近被访问到的元素会位于数组的尾部，所以头部的数据往往是最少访问的，因此会优先删除头部的元素。并且会再次调用```remove```方法，将```keys```的首个元素删除。\n\n这就是```vue```中对```keep-alive```缓存处理的优化过程。\n"
  },
  {
    "path": "src/揭秘Vue的事件机制.md",
    "content": "> 这个系列讲到这里，Vue基本核心的东西已经分析完，但是Vue之所以强大，离不开它提供给用户的一些实用功能，开发者可以更偏向于业务逻辑而非基本功能的实现。例如，在日常开发中，我们将```@click=***```用得飞起，但是我们是否思考，Vue如何在后面为我们的模板做事件相关的处理，并且我们经常利用组件的自定义事件去实现父子间的通信，那这个事件和和原生dom事件又有不同的地方吗，能够实现通信的原理又是什么，带着疑惑，我们深入源码展开分析。\n\n## 9.1. 模板编译\n\n`Vue`在挂载实例前，有相当多的工作是进行模板的编译，将```template```模板进行编译，解析成```AST```树，再转换成```render```函数，而有了```render```函数后才会进入实例挂载过程。对于事件而言，我们经常使用```v-on```或者```@```在模板上绑定事件。因此对事件的第一步处理，就是在编译阶段对事件指令做收集处理。\n\n从一个简单的用法分析编译阶段收集的信息：\n```html\n<div id=\"app\">\n    <div v-on:click.stop=\"doThis\">点击</div>\n    <span>{{count}}</span>\n</div>\n```\n```js\n\n<script>\nvar vm = new Vue({\n    el: '#app',\n    data() {\n        return {\n            count: 1\n        }\n    },\n    methods: {\n        doThis() {\n            ++this.count\n        }\n    }\n})\n</script>\n```\n\n我们之前在将模板编译的时候大致说过编译的流程，模板编译的入口是在```var ast = parse(template.trim(), options);```中，```parse```通过拆分模板字符串，将其解析为一个```AST```树，其中对于属性的处理，在```processAttr```中,由于分支较多，我们只分析例子中的流程。\n\n```js\nvar dirRE = /^v-|^@|^:/;\n\nfunction processAttrs (el) {\n    var list = el.attrsList;\n    var i, l, name, rawName, value, modifiers, syncGen, isDynamic;\n    for (i = 0, l = list.length; i < l; i++) {\n      name = rawName = list[i].name; // v-on:click\n      value = list[i].value; // doThis\n      if (dirRE.test(name)) { // 匹配v-或者@开头的指令\n        el.hasBindings = true;\n        modifiers = parseModifiers(name.replace(dirRE, ''));// parseModifiers('on:click')\n        if (modifiers) {\n          name = name.replace(modifierRE, '');\n        }\n        if (bindRE.test(name)) { // v-bind分支\n          // ...留到v-bind指令时分析\n        } else if (onRE.test(name)) { // v-on分支\n          name = name.replace(onRE, ''); // 拿到真正的事件click\n          isDynamic = dynamicArgRE.test(name);// 动态事件绑定\n          if (isDynamic) {\n            name = name.slice(1, -1);\n          }\n          addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);\n        } else { // normal directives\n         // 其他指令相关逻辑\n      } else {}\n    }\n  }\n```\n\n`processAttrs`的逻辑虽然较多，但是理解起来较为简单，```var dirRE = /^v-|^@|^:/;```是匹配事件相关的正则，命中匹配的记过会得到事件指令相关内容，包括事件本身，事件回调以及事件修饰符。最终通过```addHandler```方法，为```AST```树添加事件相关的属性。而```addHandler```还有一个重要功能是对事件修饰符进行特殊处理。\n\n```js\n// el是当前解析的AST树\nfunction addHandler (el,name,value,modifiers,important,warn,range,dynamic) {\n    modifiers = modifiers || emptyObject;\n    // passive 和 prevent不能同时使用，可以参照官方文档说明\n    if (\n      warn &&\n      modifiers.prevent && modifiers.passive\n    ) {\n      warn(\n        'passive and prevent can\\'t be used together. ' +\n        'Passive handler can\\'t prevent default event.',\n        range\n      );\n    }\n    // 这部分的逻辑会对特殊的修饰符做字符串拼接的处理，以备后续的使用\n    if (modifiers.right) {\n      if (dynamic) {\n        name = \"(\" + name + \")==='click'?'contextmenu':(\" + name + \")\";\n      } else if (name === 'click') {\n        name = 'contextmenu';\n        delete modifiers.right;\n      }\n    } else if (modifiers.middle) {\n      if (dynamic) {\n        name = \"(\" + name + \")==='click'?'mouseup':(\" + name + \")\";\n      } else if (name === 'click') {\n        name = 'mouseup';\n      }\n    }\n    if (modifiers.capture) {\n      delete modifiers.capture;\n      name = prependModifierMarker('!', name, dynamic);\n    }\n    if (modifiers.once) {\n      delete modifiers.once;\n      name = prependModifierMarker('~', name, dynamic);\n    }\n    /* istanbul ignore if */\n    if (modifiers.passive) {\n      delete modifiers.passive;\n      name = prependModifierMarker('&', name, dynamic);\n    }\n    // events 用来记录绑定的事件\n    var events;\n    if (modifiers.native) {\n      delete modifiers.native;\n      events = el.nativeEvents || (el.nativeEvents = {});\n    } else {\n      events = el.events || (el.events = {});\n    }\n\n    var newHandler = rangeSetItem({ value: value.trim(), dynamic: dynamic }, range);\n    if (modifiers !== emptyObject) {\n      newHandler.modifiers = modifiers;\n    }\n\n    var handlers = events[name];\n    /* istanbul ignore if */\n    // 绑定的事件可以多个，回调也可以多个，最终会合并到数组中\n    if (Array.isArray(handlers)) {\n      important ? handlers.unshift(newHandler) : handlers.push(newHandler);\n    } else if (handlers) {\n      events[name] = important ? [newHandler, handlers] : [handlers, newHandler];\n    } else {\n      events[name] = newHandler;\n    }\n    el.plain = false;\n  }\n```\n修饰符的处理会改变最终字符串的拼接结果，我们看最终转换的```AST```树：\n\n![](./img/9.1.png)\n\n## 9.2. 代码生成\n\n模板编译的最后一步是根据解析完的```AST```树生成对应平台的渲染函数，也就是```render```函数的生成过程, 对应```var code = generate(ast, options);```。\n```js\nfunction generate (ast,options) {\n    var state = new CodegenState(options);\n    var code = ast ? genElement(ast, state) : '_c(\"div\")';\n    return {\n      render: (\"with(this){return \" + code + \"}\"), // with函数\n      staticRenderFns: state.staticRenderFns\n    }\n  }\n```\n其中核心处理在```genElement```中,```genElement```函数会根据不同指令类型处理不同的分支，对于普通模板的编译会进入```genData```函数中处理，同样分析只针对事件相关的处理，从前面解析出的```AST```树明显看出，```AST```树中多了```events```的属性,```genHandlers```函数会为```event```属性做逻辑处理。\n```js\nfunction genData (el, state) {\n    var data = '{';\n\n    // directives first.\n    // directives may mutate the el's other properties before they are generated.\n    var dirs = genDirectives(el, state);\n    if (dirs) { data += dirs + ','; }\n    //其他处理\n    ···\n\n    // event handlers\n    if (el.events) {\n      data += (genHandlers(el.events, false)) + \",\";\n    }\n\n    ···\n\n    return data\n  }\n```\n`genHandlers`的逻辑，会遍历解析好的```AST```树，拿到```event```对象属性，并根据属性上的事件对象拼接成字符串。\n```js\nfunction genHandlers (events,isNative) {\n    var prefix = isNative ? 'nativeOn:' : 'on:';\n    var staticHandlers = \"\";\n    var dynamicHandlers = \"\";\n    // 遍历ast树解析好的event对象\n    for (var name in events) {\n      //genHandler本质上是将事件对象转换成可拼接的字符串\n      var handlerCode = genHandler(events[name]);\n      if (events[name] && events[name].dynamic) {\n        dynamicHandlers += name + \",\" + handlerCode + \",\";\n      } else {\n        staticHandlers += \"\\\"\" + name + \"\\\":\" + handlerCode + \",\";\n      }\n    }\n    staticHandlers = \"{\" + (staticHandlers.slice(0, -1)) + \"}\";\n    if (dynamicHandlers) {\n      return prefix + \"_d(\" + staticHandlers + \",[\" + (dynamicHandlers.slice(0, -1)) + \"])\"\n    } else {\n      return prefix + staticHandlers\n    }\n  }\n// 事件模板书写匹配\nvar isMethodPath = simplePathRE.test(handler.value); // doThis\nvar isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}\nvar isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)\n\n\nfunction genHandler (handler) {\n    if (!handler) {\n      return 'function(){}'\n    }\n    // 事件绑定可以多个，多个在解析ast树时会以数组的形式存在，如果有多个则会递归调用getHandler方法返回数组。\n    if (Array.isArray(handler)) {\n      return (\"[\" + (handler.map(function (handler) { return genHandler(handler); }).join(',')) + \"]\")\n    }\n    // value： doThis 可以有三种方式\n    var isMethodPath = simplePathRE.test(handler.value); // doThis\n    var isFunctionExpression = fnExpRE.test(handler.value); // () => {} or function() {}\n    var isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, '')); // doThis($event)\n\n    // 没有任何修饰符\n    if (!handler.modifiers) {\n      // 符合函数定义规范，则直接返回调用函数名 doThis\n      if (isMethodPath || isFunctionExpression) {\n        return handler.value\n      }\n      // 不符合则通过function函数封装返回\n      return (\"function($event){\" + (isFunctionInvocation ? (\"return \" + (handler.value)) : handler.value) + \"}\") // inline statement\n    } else {\n    // 包含修饰符的场景\n    }\n  }\n```\n模板中事件的写法有三种,分别对应上诉上个正则匹配的内容。\n1. `<div @click=\"doThis\"></div>`\n2. `<div @click=\"doThis($event)\"></div>`\n3. `<div @click=\"()=>{}\"></div> <div @click=\"function(){}\"></div>`\n\n上述对事件对象的转换，如果事件不带任何修饰符，并且满足正确的模板写法，则直接返回调用事件名，如果不满足，则有可能是```<div @click=\"console.log(11)\"></div>```的写法，此时会封装到```function($event){}```中。\n\n包含修饰符的场景较多，我们单独列出分析。以上文中的例子说明，```modifiers: { stop: true }```会拿到```stop```对应需要添加的逻辑脚本```'$event.stopPropagation();'```,并将它添加到函数字符串中返回。\n```js\nfunction genHandler() {\n  // ···\n  } else {\n    var code = '';\n    var genModifierCode = '';\n    var keys = [];\n    // 遍历modifiers上记录的修饰符\n    for (var key in handler.modifiers) {\n      if (modifierCode[key]) {\n        // 根据修饰符添加对应js的代码\n        genModifierCode += modifierCode[key];\n        // left/right\n        if (keyCodes[key]) {\n          keys.push(key);\n        }\n        // 针对exact的处理\n      } else if (key === 'exact') {\n        var modifiers = (handler.modifiers);\n        genModifierCode += genGuard(\n          ['ctrl', 'shift', 'alt', 'meta']\n            .filter(function (keyModifier) { return !modifiers[keyModifier]; })\n            .map(function (keyModifier) { return (\"$event.\" + keyModifier + \"Key\"); })\n            .join('||')\n        );\n      } else {\n        keys.push(key);\n      }\n    }\n    if (keys.length) {\n      code += genKeyFilter(keys);\n    }\n    // Make sure modifiers like prevent and stop get executed after key filtering\n    if (genModifierCode) {\n      code += genModifierCode;\n    }\n    // 根据三种不同的书写模板返回不同的字符串\n    var handlerCode = isMethodPath\n      ? (\"return \" + (handler.value) + \"($event)\")\n      : isFunctionExpression\n        ? (\"return (\" + (handler.value) + \")($event)\")\n        : isFunctionInvocation\n          ? (\"return \" + (handler.value))\n          : handler.value;\n    return (\"function($event){\" + code + handlerCode + \"}\")\n  }\n}\nvar modifierCode = {\n  stop: '$event.stopPropagation();',\n  prevent: '$event.preventDefault();',\n  self: genGuard(\"$event.target !== $event.currentTarget\"),\n  ctrl: genGuard(\"!$event.ctrlKey\"),\n  shift: genGuard(\"!$event.shiftKey\"),\n  alt: genGuard(\"!$event.altKey\"),\n  meta: genGuard(\"!$event.metaKey\"),\n  left: genGuard(\"'button' in $event && $event.button !== 0\"),\n  middle: genGuard(\"'button' in $event && $event.button !== 1\"),\n  right: genGuard(\"'button' in $event && $event.button !== 2\")\n};\n```\n\n经过这一转换后，生成```with```封装的```render```函数如下：\n\n```js\n\"_c('div',{attrs:{\"id\":\"app\"}},[_c('div',{on:{\"click\":function($event){$event.stopPropagation();return doThis($event)}}},[_v(\"点击\")]),_v(\" \"),_c('span',[_v(_s(count))])])\"\n\n```\n\n## 9.3. 事件绑定\n\n前面花了大量的篇幅介绍了模板上的事件标记在构建```AST```树上是怎么处理，并且如何根据构建的```AST```树返回正确的```render```渲染函数，**但是真正事件绑定还是离不开绑定注册事件**。这一个阶段就是发生在组件挂载的阶段。\n有了```render```函数，自然可以生成实例挂载需要的```Vnode```树，并且会进行```patchVnode```的环节进行真实节点的构建，如果发现过程已经遗忘，可以回顾以往章节。\n`Vnode`树的构建过程和之前介绍的内容没有明显的区别，所以这个过程就不做赘述，最终生成的```vnode```如下：\n\n![](./img/9.2.png)\n\n有了```Vnode```,接下来会遍历子节点递归调用```createElm```为每个子节点创建真实的```DOM```,由于```Vnode```中有```data```属性，在创建真实```DOM```时会进行注册相关钩子的过程，其中一个就是注册事件相关处理。\n\n```js\nfunction createElm() {\n  ···\n  // 针对指令的处理\n   if (isDef(data)) {\n      invokeCreateHooks(vnode, insertedVnodeQueue);\n    }\n}\n\n\nfunction invokeCreateHooks (vnode, insertedVnodeQueue) {\n  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {\n    cbs.create[i$1](emptyNode, vnode);\n  }\n  i = vnode.data.hook; // Reuse variable\n  if (isDef(i)) {\n    if (isDef(i.create)) { i.create(emptyNode, vnode); }\n    if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }\n  }\n}\n\nvar events = {\n  create: updateDOMListeners,\n  update: updateDOMListeners\n};\n```\n\n我们经常会在```template```模板中定义```v-on```事件，```v-bind```动态属性，```v-text```动态指令等，和```v-on```事件指令一样，他们都会在编译阶段和```Vnode```生成阶段创建```data```属性，因此```invokeCreateHooks```就是一个模板指令处理的任务，他分别针对不同的指令为真实阶段创建不同的任务。针对事件，这里会调用```updateDOMListeners```对真实的```DOM```节点注册事件任务。\n\n```js\nfunction updateDOMListeners (oldVnode, vnode) {\n  // on是事件指令的标志\n  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {\n    return\n  }\n  // 新旧节点不同的事件绑定解绑\n  var on = vnode.data.on || {};\n  var oldOn = oldVnode.data.on || {};\n  // 拿到需要添加事件的真实DOM节点\n  target$1 = vnode.elm;\n  // normalizeEvents是对事件兼容性的处理\n  normalizeEvents(on);\n  updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);\n  target$1 = undefined;\n}\n```\n其中```normalizeEvents```是针对```v-model```的处理,例如在IE下不支持```change```事件，只能用```input```事件代替。\n\n`updateListeners`的逻辑也很简单，它会遍历```on```事件对新节点事件绑定注册事件，对旧节点移除事件监听，它即要处理原生```DOM```事件的添加和移除，也要处理自定义事件的添加和移除，关于自定义事件，后续内容再分析。\n```js\nfunction updateListeners (on,oldOn,add,remove###1,createOnceHandler,vm) {\n    var name, def###1, cur, old, event;\n    // 遍历事件\n    for (name in on) {\n      def###1 = cur = on[name];\n      old = oldOn[name];\n      event = normalizeEvent(name);\n      if (isUndef(cur)) {\n        // 事件名非法的报错处理\n        warn(\n          \"Invalid handler for event \\\"\" + (event.name) + \"\\\": got \" + String(cur),\n          vm\n        );\n      } else if (isUndef(old)) {\n        // 旧节点不存在\n        if (isUndef(cur.fns)) {\n          // createFunInvoker返回事件最终执行的回调函数\n          cur = on[name] = createFnInvoker(cur, vm);\n        }\n        // 只触发一次的事件\n        if (isTrue(event.once)) {\n          cur = on[name] = createOnceHandler(event.name, cur, event.capture);\n        }\n        // 执行真正注册事件的执行函数\n        add(event.name, cur, event.capture, event.passive, event.params);\n      } else if (cur !== old) {\n        old.fns = cur;\n        on[name] = old;\n      }\n    }\n    // 旧节点存在，接触旧节点上的绑定事件\n    for (name in oldOn) {\n      if (isUndef(on[name])) {\n        event = normalizeEvent(name);\n        remove###1(event.name, oldOn[name], event.capture);\n      }\n    }\n  }\n```\n\n在初始构建实例时，旧节点是不存在的,此时会调用```createFnInvoker```函数对事件回调函数做一层封装，由于单个事件的回调可以有多个，因此```createFnInvoker```的作用是对单个，多个回调事件统一封装处理，返回一个当事件触发时真正执行的匿名函数。\n\n```js\nfunction createFnInvoker (fns, vm) {\n  // 当事件触发时，执行invoker方法，方法执行fns\n  function invoker () {\n    var arguments$1 = arguments;\n\n    var fns = invoker.fns;\n    // fns是多个回调函数组成的数组\n    if (Array.isArray(fns)) {\n      var cloned = fns.slice();\n      for (var i = 0; i < cloned.length; i++) {\n        // 遍历执行真正的回调函数\n        invokeWithErrorHandling(cloned[i], null, arguments$1, vm, \"v-on handler\");\n      }\n    } else {\n      // return handler return value for single handlers\n      return invokeWithErrorHandling(fns, null, arguments, vm, \"v-on handler\")\n    }\n  }\n  invoker.fns = fns;\n  // 返回最终事件执行的回调函数\n  return invoker\n}\n```\n其中```invokeWithErrorHandling```会执行定义好的回调函数，这里做了同步异步回调的错误处理。```try-catch```用于同步回调捕获异常错误，```Promise.catch```用于捕获异步任务返回错误。\n```js\nfunction invokeWithErrorHandling (handler,context,args,vm,info) {\n    var res;\n    try {\n      res = args ? handler.apply(context, args) : handler.call(context);\n      if (res && !res._isVue && isPromise(res)) {\n        // issue #9511\n        // reassign to res to avoid catch triggering multiple times when nested calls\n        // 当生命周期钩子函数内部执行返回promise对象是，如果捕获异常，则会对异常信息做一层包装返回\n        res = res.catch(function (e) { return handleError(e, vm, info + \" (Promise/async)\"); });\n      }\n    } catch (e) {\n      handleError(e, vm, info);\n    }\n    return res\n  }\n```\n如果事件只触发一次(即使用了```once```修饰符)，则调用```createOnceHandler```匿名，在执行完回调之后，移除事件绑定。\n```js\nfunction createOnceHandler (event, handler, capture) {\n    var _target = target$1; \n    return function onceHandler () {\n      //调用事件回调\n      var res = handler.apply(null, arguments);\n      if (res !== null) {\n        // 移除事件绑定\n        remove$2(event, onceHandler, capture, _target);\n      }\n    }\n  }\n```\n**`add`和```remove```是真正在```DOM```上绑定事件和解绑事件的过程，它的实现也是利用了原生```DOM```的```addEventListener,removeEventListener api```。**\n```js\nfunction add (name,handler,capture,passive){\n  ···\n  target$1.addEventListener(name,handler,\n      supportsPassive\n        ? { capture: capture, passive: passive }\n        : capture);\n}\nfunction remove (name,handler,capture,_target) {\n  (_target || target$1).removeEventListener(\n    name,\n    handler._wrapper || handler,\n    capture\n  );\n}\n```\n另外事件的解绑除了发生在只触发一次的事件，也发生在组件更新```patchVnode```过程，具体不展开分析，可以参考之前介绍组件更新的内容研究```updateListeners```的过程。\n\n## 9.4. 自定义事件\n\n`Vue`如何处理原生的```Dom```事件基本流程已经讲完，然而针对事件还有一个重要的概念不可忽略，那就是组件的自定义事件。我们知道父子组件可以利用事件进行通信，子组件通过```vm.$emit```向父组件分发事件，父组件通过```v-on:(event)```接收信息并处理回调。因此针对自定义事件在源码中自然有不同的处理逻辑。我们先通过简单的例子展开。\n```js\n<script>\n    var child = {\n      template: `<div @click=\"emitToParent\">点击传递信息给父组件</div>`,\n      methods: {\n        emitToParent() {\n          this.$emit('myevent', 1)\n        }\n      }\n    }\n    new Vue({\n      el: '#app',\n      components: {\n        child\n      },\n      template: `<div id=\"app\"><child @myevent=\"myevent\" @click.native=\"nativeClick\"></child></div>`,\n      methods: {\n        myevent(num) {\n          console.log(num)\n        },\n        nativeClick() {\n          console.log('nativeClick')\n        }\n      }\n    })\n  </script>\n```\n从例子中可以看出，普通节点只能使用原生```DOM```事件，而组件上却可以使用自定义的事件和原生的```DOM```事件，并且通过```native```修饰符区分，有了原生```DOM```对于事件处理的基础，接下来我们看看自定义事件有什么特别之处。\n\n### 9.4.1 模板编译\n\n回过头来看看事件的模板编译，在生成```AST```树阶段，之前分析说过```addHandler```方法会对事件的修饰符做不同的处理，当遇到```native```修饰符时，事件相关属性方法会添加到```nativeEvents```属性中。\n下图是```child```生成的```AST```树:\n\n![](./img/9.3.png)\n\n### 9.4.2 代码生成\n\n不管是组件还是普通标签，事件处理代码都在```genData```的过程中，和之前分析原生事件一致，```genHandlers```用来处理事件对象并拼接成字符串。\n```js\nfunction genData() {\n  ···\n  if (el.events) {\n    data += (genHandlers(el.events, false)) + \",\";\n  }\n  if (el.nativeEvents) {\n    data += (genHandlers(el.nativeEvents, true)) + \",\";\n  }\n}\n```\n`getHandlers`的逻辑前面已经讲过，处理组件原生事件和自定义事件的区别在```isNative```选项上，我们看最终生成的代码为：\n\n```js\nwith(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('child',{on:{\"myevent\":myevent},nativeOn:{\"click\":function($event){return nativeClick($event)}}})],1)}\n```\n\n有了```render```函数接下来会根据它创建```Vnode```实例，其中遇到组件占位符节点时会创建子组件```Vnode```， 此时为```on,nativeOn```做了一层特殊的转换，将```nativeOn```赋值给```on```,这样后续的处理方式和普通节点一致。另外，将```on```赋值给```listeners```,在创建```VNode```时以组件配置```componentOptions```传入。\n\n```js\n // 创建子组件过程\nfunction createComponent (){\n  ···\n  var listeners = data.on;\n  // replace with listeners with .native modifier\n  // so it gets processed during parent component patch.\n  data.on = data.nativeOn;\n  ···\n\n  var vnode = new VNode(\n    (\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),\n    data, undefined, undefined, undefined, context,\n    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },\n    asyncFactory\n  );\n\n  return vnode\n}\n```\n### 9.4.3 子组件实例\n\n接下来是通过```Vnode```生成真实节点的过程，这个过程遇到子```Vnode```会实例化子组件实例。实例化子类构造器的过程又回到之前文章分析的初始化选项配置的过程，在系列最开始的时候分析```Vue.prototype.init```的过程，跳过了组件初始化的流程，其中针对自定义事件的处理的关键如下\n```js\nVue.prototype._init = function(options) {\n  ···\n  // 针对子组件的事件处理逻辑\n  if (options && options._isComponent) {\n    // 初始化内部组件\n    initInternalComponent(vm, options);\n  } else {\n    // 选项合并，将合并后的选项赋值给实例的$options属性\n    vm.$options = mergeOptions(\n      resolveConstructorOptions(vm.constructor),\n      options || {},\n      vm\n    );\n  }\n  // 初始化事件处理\n  initEvents(vm);\n}\nfunction initInternalComponent (vm, options) {\n  var opts = vm.$options = Object.create(vm.constructor.options);\n  ···\n  opts._parentListeners = vnodeComponentOptions.listeners;\n  ···\n}\n```\n**此时，子组件拿到了父占位符节点定义的```@myevent=\"myevent\"```事件**。接下来进行子组件的初始化事件处理，此时```vm.$options._parentListeners```会拿到父组件自定义的事件。而带有自定义事件的组件会执行```updateComponentListeners```函数。\n```js\nfunction initEvents (vm) {\n  vm._events = Object.create(null);\n  vm._hasHookEvent = false;\n  // init parent attached events\n  var listeners = vm.$options._parentListeners;\n  if (listeners) {\n    // 带有自定义事件属性的实例\n    updateComponentListeners(vm, listeners);\n  }\n}\n```\n之后又回到了之前分析的```updateListeners```过程，和原生```DOM```事件不同的是，自定义事件的添加移除的方法不同。\n```js\nvar target = vm;\n\nfunction add (event, fn) {\n  target.$on(event, fn);\n}\n\nfunction remove$1 (event, fn) {\n  target.$off(event, fn);\n} \n\nfunction updateComponentListeners (vm,listeners,oldListeners) {\n  target = vm;\n  updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);\n  target = undefined;\n}\n\n```\n### 9.4.4 事件API\n我们回头来看看```Vue```在引入阶段对事件的处理还做了哪些初始化操作。```Vue```在实例上用一个```_events```属性存贮管理事件的派发和更新，暴露出```$on, $once, $off, $emit```方法给外部管理事件和派发执行事件。\n```js\n  eventsMixin(Vue); // 定义事件相关函数\n\n  function eventsMixin (Vue) {\n    var hookRE = /^hook:/;\n    // $on方法用来监听事件，执行回调\n    Vue.prototype.$on = function (event, fn) {\n      var vm = this;\n      // event支持数组形式。\n      if (Array.isArray(event)) {\n        for (var i = 0, l = event.length; i < l; i++) {\n          vm.$on(event[i], fn);\n        }\n      } else {\n        // _events数组中记录需要监听的事件以及事件触发的回调\n        (vm._events[event] || (vm._events[event] = [])).push(fn);\n        if (hookRE.test(event)) {\n          vm._hasHookEvent = true;\n        }\n      }\n      return vm\n    };\n    // $once方法用来监听一次事件，执行回调\n    Vue.prototype.$once = function (event, fn) {\n      var vm = this;\n      // 对fn做一层包装，先解除绑定再执行fn回调\n      function on () {\n        vm.$off(event, on);\n        fn.apply(vm, arguments);\n      }\n      on.fn = fn;\n      vm.$on(event, on);\n      return vm\n    };\n    // $off方法用来解除事件监听\n    Vue.prototype.$off = function (event, fn) {\n      var vm = this;\n      // 如果$off方法没有传递任何参数时，将_events属性清空。\n      if (!arguments.length) {\n        vm._events = Object.create(null);\n        return vm\n      }\n      // 数组处理\n      if (Array.isArray(event)) {\n        for (var i$1 = 0, l = event.length; i$1 < l; i$1++) {\n          vm.$off(event[i$1], fn);\n        }\n        return vm\n      }\n      var cbs = vm._events[event];\n      if (!cbs) {\n        return vm\n      }\n      if (!fn) {\n        vm._events[event] = null;\n        return vm\n      }\n      // specific handler\n      var cb;\n      var i = cbs.length;\n      while (i--) {\n        cb = cbs[i];\n        if (cb === fn || cb.fn === fn) {\n          // 将监听的事件回调移除\n          cbs.splice(i, 1);\n          break\n        }\n      }\n      return vm\n    };\n    // $emit方法用来触发事件，执行回调\n    Vue.prototype.$emit = function (event) {\n      var vm = this;\n      {\n        var lowerCaseEvent = event.toLowerCase();\n        if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {\n          tip(\n            \"Event \\\"\" + lowerCaseEvent + \"\\\" is emitted in component \" +\n            (formatComponentName(vm)) + \" but the handler is registered for \\\"\" + event + \"\\\". \" +\n            \"Note that HTML attributes are case-insensitive and you cannot use \" +\n            \"v-on to listen to camelCase events when using in-DOM templates. \" +\n            \"You should probably use \\\"\" + (hyphenate(event)) + \"\\\" instead of \\\"\" + event + \"\\\".\"\n          );\n        }\n      }\n      var cbs = vm._events[event];\n      // 找到已经监听事件的回调，执行\n      if (cbs) {\n        cbs = cbs.length > 1 ? toArray(cbs) : cbs;\n        var args = toArray(arguments, 1);\n        var info = \"event handler for \\\"\" + event + \"\\\"\";\n        for (var i = 0, l = cbs.length; i < l; i++) {\n          invokeWithErrorHandling(cbs[i], vm, args, vm, info);\n        }\n      }\n      return vm\n    };\n  }\n```\n\n有了这些事件api，自定义事件的添加移除理解起来也简单很多。组件通过```this.$emit```在组件实例中派发了事件，而在这之前，组件已经将需要监听的事件以及回调添加到实例的```_events```属性中，触发事件时便可以直接执行监听事件的回调。\n\n**最后，我们换一个角度理解父子组件通信，组件自定义事件的触发和监听本质上都是在当前的组件实例中进行，之所以能产生父子组件通信的效果是因为事件监听的回调函数写在了父组件中。**\n\n\n## 9.5 小结\n事件是我们日常开发中必不可少的功能点，```Vue```在应用层暴露了```@,v-on```的指令供开发者在模板中绑定事件。事件指令在模板编译阶段会以属性的形式存在，而在真实节点渲染阶段会根据事件属性去绑定相关的事件。对于组件的事件而言，我们可以利用事件进行子父组件间的通信，他本质上是在同个子组件内部维护了一个事件总线，从分析结果可以看出，之所以有子父组件通信的效果，原因仅仅是因为回调函数写在了父组件中。\n"
  },
  {
    "path": "src/来，跟我一起实现diff算法.md",
    "content": "> 这一节，依然是**深入剖析Vue源码系列**，上几节内容介绍了```Virtual DOM```是Vue在渲染机制上做的优化，而渲染的核心在于数据变化时，如何高效的更新节点，这就是diff算法。由于源码中关于```diff```算法部分流程复杂，直接剖析每个流程不易于理解，所以这一节我们换一个思路，参考源码来手动实现一个简易版的```diff```算法。\n\n之前讲到```Vue```在渲染机制的优化上，引入了```Virtual DOM```的概念，利用```Virtual DOM```描述一个真实的```DOM```,本质上是在```JS```和真实```DOM```之间架起了一层缓冲层。当我们通过大量的```JS```运算,并将最终结果反应到浏览器进行渲染时，```Virtual DOM```可以将多个改动合并成一个批量的操作，从而减少 ```dom``` 重排的次数，进而缩短了生成渲染树和绘制节点所花的时间，达到渲染优化的目的。之前的章节，我们简单的介绍了```Vue```中```Vnode```的概念，以及创建```Vnode```到```渲染Vnode```再到真实```DOM```的过程。如果有忘记流程的，可以参考前面的章节分析。\n\n\n**从```render```函数到创建虚拟```DOM```,再到渲染真实节点，这一过程是完整的，也是容易理解的。然而引入虚拟```DOM```的核心不在这里，而在于当数据发生变化时，如何最优化数据变动到视图更新的过程。这一个过程才是```Vnode```更新视图的核心，也就是常说的```diff```算法。**下面跟着我来实现一个简易版的```diff```算法\n\n## 8.1 创建基础类\n代码编写过程会遇到很多基本类型的判断，第一步需要先将这些方法封装。\n```js\nclass Util {\n  constructor() {}\n  // 检测基础类型\n  _isPrimitive(value) {\n    return (typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol' || typeof value === 'boolean')\n  }\n  // 判断值不为空\n  _isDef(v) {\n    return v !== undefined && v !== null\n  }\n}\n// 工具类的使用\nconst util = new Util()\n```\n\n## 8.2 创建Vnode\n`Vnode`这个类在之前章节已经分析过源码，本质上是用一个对象去描述一个真实的```DOM```元素，简易版关注点在于元素的```tag```标签，元素的属性集合```data```,元素的子节点```children```,```text```为元素的文本节点,简单的描述类如下：\n```js\nclass VNode {\n  constructor(tag, data, children) {\n    this.tag = tag;\n    this.data = data;\n    this.children = children;\n    this.elm = ''\n    // text属性用于标志Vnode节点没有其他子节点，只有纯文本\n    this.text = util._isPrimitive(this.children) ? this.children : ''\n  }\n}\n```\n## 8.3 模拟渲染过程\n接下来需要创建另一个类模拟将```render```函数转换为```Vnode```,并将```Vnode```渲染为真实```DOM```的过程，我们将这个类定义为```Vn```,```Vn```具有两个基本的方法```createVnode, createElement```, 分别实现创建虚拟```Vnode```,和创建真实```DOM```的过程。\n\n### 8.3.1 createVnode\n`createVnode`模拟```Vue```中```render```函数的实现思路，目的是将数据转换为虚拟的```Vnode```,先看具体的使用和定义。\n\n```js\n// index.html\n\n<script src=\"diff.js\">\n<script>\n\n// 创建Vnode\n\nlet createVnode = function() {\n  let _c = vn.createVnode;\n  return _c('div', { attrs: { id: 'test' } }, arr.map(a => _c(a.tag, {}, a.text)))\n}\n\n// 元素内容结构\nlet arr = \n  [{\n    tag: 'i',\n    text: 2\n  }, {\n    tag: 'span',\n    text: 3\n  }, {\n    tag: 'strong',\n    text: 4\n  }]\n</script>\n\n\n\n// diff.js\n(function(global) {\n  class Vn {\n    constructor() {}\n    // 创建虚拟Vnode\n    createVnode(tag, data, children) {\n      return new VNode(tag, data, children)\n    }\n  }\n  global.vn = new Vn()\n}(this))\n\n```\n这是一个完整的```Vnode```对象，我们已经可以用这个对象来简单的描述一个```DOM```节点，而```createElement```就是将这个对象对应到真实节点的过程。最终我们希望的结果是这样的。\n\n**Vnode对象**\n\n![](./img/8.1.png)\n\n\n**渲染结果**\n\n![](./img/8.2.png)\n### 8.3.2 createElement\n渲染真实```DOM```的过程就是遍历```Vnode```对象，递归创建真实节点的过程，这个不是本文的重点，所以我们可以粗糙的实现。\n```js\nclass Vn {\n  createElement(vnode, options) {\n      let el = options.el;\n      if(!el || !document.querySelector(el)) return console.error('无法找到根节点')\n      let _createElement = vnode => {\n        const { tag, data, children } = vnode;\n        const ele = document.createElement(tag);\n        // 添加属性\n        this.setAttr(ele, data);\n        // 简单的文本节点，只要创建文本节点即可\n        if (util._isPrimitive(children)) {\n          const testEle = document.createTextNode(children);\n          ele.appendChild(testEle)\n        } else {\n        // 复杂的子节点需要遍历子节点递归创建节点。\n          children.map(c => ele.appendChild(_createElement(c)))\n        }\n        return ele\n      }\n      document.querySelector(el).appendChild(_createElement(vnode))\n    }\n}\n```\n### 8.3.3 setAttr\n`setAttr`是为节点设置属性的方法，利用```DOM```原生的```setAttribute```为每个节点设置属性值。\n```js\nclass Vn {\n  setAttr(el, data) {\n    if (!el) return\n    const attrs = data.attrs;\n    if (!attrs) return;\n    Object.keys(attrs).forEach(a => {\n      el.setAttribute(a, attrs[a]);\n    })\n  }\n}\n```\n至此一个简单的 **数据 -> ```Virtual DOM``` => 真实```DOM```**的模型搭建成功,这也是数据变化、比较、更新的基础。\n\n\n## 8.4 diff算法实现\n更新组件的过程首先是响应式数据发生了变化,数据频繁的修改如果直接渲染到真实```DOM```上会引起整个```DOM```树的重绘和重排，频繁的重绘和重排是极其消耗性能的。如何优化这一渲染过程，```Vue```源码中给出了两个具体的思路，其中一个是在介绍响应式系统时提到的将多次修改推到一个队列中，在下一个```tick```去执行视图更新，另一个就是接下来要着重介绍的```diff```算法，将需要修改的数据进行比较，并只渲染必要的```DOM```。\n\n数据的改变最终会导致节点的改变，所以```diff```算法的核心在于在尽可能小变动的前提下找到需要更新的节点，直接调用原生相关```DOM```方法修改视图。不管是真实```DOM```还是前面创建的```Virtual DOM```,都可以理解为一颗```DOM```树，**算法比较节点不同时，只会进行同层节点的比较，不会跨层进行比较，这也大大减少了算法复杂度。**\n\n\n### 8.4.1 diffVnode\n在之前的基础上，我们实现一个思路，1秒之后数据发生改变。\n```js\n// index.html\nsetTimeout(function() {\n  arr = [{\n    tag: 'span',\n    text: 1\n  },{\n    tag: 'strong',\n    text: 2\n  },{\n    tag: 'i',\n    text: 3\n  },{\n    tag: 'i',\n    text: 4\n  }]\n  // newVnode 表示改变后新的Vnode树\n  const newVnode = createVnode();\n  // diffVnode会比较新旧Vnode树，并完成视图更新\n  vn.diffVnode(newVnode, preVnode);\n})\n```\n`diffVnode`的逻辑，会对比新旧节点的不同，并完成视图渲染更新\n```js\nclass Vn {\n  ···\n  diffVnode(nVnode, oVnode) {\n    if (!this._sameVnode(nVnode, oVnode)) {\n      // 直接更新根节点及所有子节点\n      return ***\n    }\n    this.generateElm(vonde);\n    this.patchVnode(nVnode, oVnode);\n  }\n}\n```\n### 8.4.2 _sameVnode\n新旧节点的对比是算法的第一步，如果新旧节点的根节点不是同一个节点，则直接替换节点。这遵从上面提到的原则，**只进行同层节点的比较，节点不一致，直接用新节点及其子节点替换旧节点**。为了理解方便，我们假定节点相同的判断是```tag```标签是否一致(实际源码要复杂)。\n```js\nclass Vn {\n  _sameVnode(n, o) {\n    return n.tag === o.tag;\n  }\n}\n```\n### 8.4.3 generateElm\n`generateElm`的作用是跟踪每个节点实际的真实节点，方便在对比虚拟节点后实时更新真实```DOM```节点。虽然```Vue```源码中做法不同，但是这不是分析```diff```的重点。\n```js\nclass Vn {\n  generateElm(vnode) {\n    const traverseTree = (v, parentEl) => {\n      let children = v.children;\n      if(Array.isArray(children)) {\n        children.forEach((c, i) => {\n          c.elm = parentEl.childNodes[i];\n          traverseTree(c, c.elm)\n        })\n      }\n    }\n    traverseTree(vnode, this.el);\n  }\n}\n```\n执行```generateElm```方法后，我们可以在旧节点的```Vnode```中跟踪到每个```Virtual DOM```的真实节点信息。\n\n### 8.4.4 patchVnode\n`patchVnode`是新旧```Vnode```对比的核心方法，对比的逻辑如下。\n1. 节点相同，且节点除了拥有文本节点外没有其他子节点。这种情况下直接替换文本内容。\n2. 新节点没有子节点，旧节点有子节点，则删除旧节点所有子节点。\n3. 旧节点没有子节点，新节点有子节点，则用新的所有子节点去更新旧节点。\n4. 新旧都存在子节点。则对比子节点内容做操作。\n\n代码逻辑如下：\n```js\nclass Vn {\n  patchVnode(nVnode, oVnode) {\n    \n    if(nVnode.text && nVnode.text !== oVnode) {\n      // 当前真实dom元素\n      let ele = oVnode.elm\n      // 子节点为文本节点\n      ele.textContent = nVnode.text;\n    } else {\n      const oldCh = oVnode.children;\n      const newCh = nVnode.children;\n      // 新旧节点都存在。对比子节点\n      if (util._isDef(oldCh) && util._isDef(newCh)) {\n        this.updateChildren(ele, newCh, oldCh)\n      } else if (util._isDef(oldCh)) {\n        // 新节点没有子节点\n      } else {\n        // 老节点没有子节点\n      }\n    }\n  }\n}\n```\n上述例子在```patchVnode```过程中，新旧子节点都存在，所以会走```updateChildren```分支。\n\n### 8.4.5 updateChildren\n子节点的对比，我们通过文字和画图的形式分析，通过图解的形式可以很清晰看到```diff```算法的巧妙之处。\n\n大致逻辑是：\n 1. 旧节点的起始位置为```oldStartIndex```,截至位置为```oldEndIndex```,新节点的起始位置为```newStartIndex```,截至位置为```newEndIndex```。\n 2. 新旧```children```的起始位置的元素两两对比，顺序是```newStartVnode, oldStartVnode```; ```newEndVnode, oldEndVnode```;```newEndVnode, oldStartVnode```;```newStartIndex, oldEndIndex```\n 3. ```newStartVnode, oldStartVnode```节点相同，执行一次```patchVnode```过程，也就是递归对比相应子节点，并替换节点的过程。```oldStartIndex，newStartIndex```都像右移动一位。\n 4. ```newEndVnode, oldEndVnode```节点相同，执行一次```patchVnode```过程，递归对比相应子节点，并替换节点。```oldEndIndex， newEndIndex```都像左移动一位。\n 5. ```newEndVnode, oldStartVnode```节点相同，执行一次```patchVnode```过程，并将旧的```oldStartVnode```移动到尾部,```oldStartIndex```右移一味，```newEndIndex```左移一位。\n 6. ```newStartIndex, oldEndIndex```节点相同，执行一次```patchVnode```过程，并将旧的```oldEndVnode```移动到头部,```oldEndIndex```左移一味，```newStartIndex```右移一位。\n 7. 四种组合都不相同，则会搜索旧节点所有子节点，找到将这个旧节点和```newStartVnode```执行```patchVnode```过程。\n 8. 不断对比的过程使得```oldStartIndex```不断逼近```oldEndIndex```，```newStartIndex```不断逼近```newEndIndex```。当```oldEndIndex <= oldStartIndex```说明旧节点已经遍历完了，此时只要批量增加新节点即可。当```newEndIndex <= newStartIndex```说明旧节点还有剩下，此时只要批量删除旧节点即可。\n\n\n结合前面的例子：\n \n \n第一步： \n\n![](./img/8.3.png)\n\n第二步：\n\n![](./img/8.4.png)\n\n第三步：\n\n![](./img/8.5.png)\n\n第三步：\n\n\n![](./img/8.6.png)\n\n第四步：\n\n![](./img/8.7.png)\n\n根据这些步骤，代码实现如下： \n\n```js\nclass Vn {\n  updateChildren(el, newCh, oldCh) {\n    // 新children开始标志\n    let newStartIndex = 0;\n    // 旧children开始标志\n    let oldStartIndex = 0;\n    // 新children结束标志\n    let newEndIndex = newCh.length - 1;\n    // 旧children结束标志\n    let oldEndIndex = oldCh.length - 1;\n    let oldKeyToId;\n    let idxInOld;\n    let newStartVnode = newCh[newStartIndex];\n    let oldStartVnode = oldCh[oldStartIndex];\n    let newEndVnode = newCh[newEndIndex];\n    let oldEndVnode = oldCh[oldEndIndex];\n    // 遍历结束条件\n    while (newStartIndex <= newEndIndex && oldStartIndex <= oldEndIndex) {\n      // 新children开始节点和旧开始节点相同\n      if (this._sameVnode(newStartVnode, oldStartVnode)) {\n        this.patchVnode(newCh[newStartIndex], oldCh[oldStartIndex]);\n        newStartVnode = newCh[++newStartIndex];\n        oldStartVnode = oldCh[++oldStartIndex]\n      } else if (this._sameVnode(newEndVnode, oldEndVnode)) {\n      // 新childre结束节点和旧结束节点相同\n        this.patchVnode(newCh[newEndIndex], oldCh[oldEndIndex])\n        oldEndVnode = oldCh[--oldEndIndex];\n        newEndVnode = newCh[--newEndIndex]\n      } else if (this._sameVnode(newEndVnode, oldStartVnode)) {\n      // 新childre结束节点和旧开始节点相同\n        this.patchVnode(newCh[newEndIndex], oldCh[oldStartIndex])\n        // 旧的oldStartVnode移动到尾部\n        el.insertBefore(oldCh[oldStartIndex].elm, null);\n        oldStartVnode = oldCh[++oldStartIndex];\n        newEndVnode = newCh[--newEndIndex];\n      } else if (this._sameVnode(newStartVnode, oldEndVnode)) {\n        // 新children开始节点和旧结束节点相同\n        this.patchVnode(newCh[newStartIndex], oldCh[oldEndIndex]);\n        el.insertBefore(oldCh[oldEndIndex].elm, oldCh[oldStartIndex].elm);\n        oldEndVnode = oldCh[--oldEndIndex];\n        newStartVnode = newCh[++newStartIndex];\n      } else {\n        // 都不符合的处理，查找新节点中与对比旧节点相同的vnode\n        this.findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);\n      }\n    }\n    // 新节点比旧节点多，批量增加节点\n    if(oldEndIndex <= oldStartIndex) {\n      for (let i = newStartIndex; i <= newEndIndex; i++) {\n        // 批量增加节点\n        this.createElm(oldCh[oldEndIndex].elm, newCh[i])\n      }\n    }\n  }\n\n  createElm(el, vnode) {\n    let tag = vnode.tag;\n    const ele = document.createElement(tag);\n    this._setAttrs(ele, vnode.data);\n    const testEle = document.createTextNode(vnode.children);\n    ele.appendChild(testEle)\n    el.parentNode.insertBefore(ele, el.nextSibling)\n  }\n\n  // 查找匹配值\n  findIdxInOld(newStartVnode, oldCh, start, end) {\n    for (var i = start; i < end; i++) {\n      var c = oldCh[i];\n      if (util.isDef(c) && this.sameVnode(newStartVnode, c)) { return i }\n    }\n  }\n}\n```\n\n## 8.5 diff算法优化\n前面有个分支，当四种比较节点都找不到匹配时，会调用```findIdxInOld```找到旧节点中和新的比较节点一致的节点。节点搜索在数量级较大时是缓慢的。查看```Vue```的源码，发现它在这一个环节做了优化，也就是我们经常在编写列表时被要求加入的唯一属性**key**，有了这个唯一的标志位，我们可以对旧节点建立简单的字典查询，只要有```key```值便可以方便的搜索到符合要求的旧节点。修改代码：\n```js\nclass Vn {\n  updateChildren() {\n    ···\n    } else {\n      // 都不符合的处理，查找新节点中与对比旧节点相同的vnode\n      if (!oldKeyToId) oldKeyToId = this.createKeyMap(oldCh, oldStartIndex, oldEndIndex);\n      idxInOld = util._isDef(newStartVnode.key) ? oldKeyToId[newStartVnode.key] : this.findIdxInOld(newStartVnode, oldCh, oldStartIndex, oldEndIndex);\n      // 后续操作\n    }\n  }\n  // 建立字典\n  createKeyMap(oldCh, start, old) {\n    const map = {};\n    for(let i = start; i < old; i++) {\n      if(oldCh.key) map[key] = i;\n    }\n    return map;\n  }\n}\n\n\n```\n\n## 8.6 问题思考\n最后我们思考一个问题，```Virtual DOM``` 的重绘性能真的比单纯的```innerHTML```要好吗，其实并不是这样的，作者的[解释](https://www.zhihu.com/question/31809713/answer/53544875)\n\n>  - `innerHTML:  render html string O(template size) +` 重新创建所有 ```DOM``` 元素 ```O(DOM size)```\n\n> - `Virtual DOM: render Virtual DOM + diff O(template size) +` 必要的 ```DOM``` 更新 ```O(DOM change)```\n\n> - `Virtual DOM render + diff` 显然比渲染 html 字符串要慢，但是！它依然是纯 js 层面的计算，比起后面的 ```DOM``` 操作来说，依然便宜了太多。可以看到，```innerHTML``` 的总计算量不管是 ```js``` 计算还是 ```DOM ```操作都是和整个界面的大小相关，但```Virtual DOM``` 的计算量里面，只有 ```js``` 计算和界面大小相关，DOM 操作是和数据的变动量相关的。\n"
  },
  {
    "path": "src/深入响应式系统构建-上.md",
    "content": "> 从这一小节开始，正式进入```Vue```源码的核心，也是难点之一，响应式系统的构建。这一节将作为分析响应式构建过程源码的入门，主要分为两大块,第一块是针对响应式数据```props,methods,data,computed,wather```初始化过程的分析，另一块则是在保留源码设计理念的前提下，尝试手动构建一个基础的响应式系统。有了这两个基础内容的铺垫，下一篇进行源码具体细节的分析会更加得心应手。\n\n## 7.1 数据初始化\n回顾一下之前的内容，我们对```Vue```源码的分析是从初始化开始，初始化```_init```会执行一系列的过程，这个过程包括了配置选项的合并，数据的监测代理，最后才是实例的挂载。而在实例挂载前还有意忽略了一个重要的过程，**数据的初始化**(即```initState(vm)```)。```initState```的过程，是对数据进行响应式设计的过程，过程会针对```props,methods,data,computed```和```watch```做数据的初始化处理，并将他们转换为响应式对象，接下来我们会逐步分析每一个过程。\n```js\nfunction initState (vm) {\n  vm._watchers = [];\n  var opts = vm.$options;\n  // 初始化props\n  if (opts.props) { initProps(vm, opts.props); }\n  // 初始化methods\n  if (opts.methods) { initMethods(vm, opts.methods); }\n  // 初始化data\n  if (opts.data) {\n    initData(vm);\n  } else {\n    // 如果没有定义data，则创建一个空对象，并设置为响应式\n    observe(vm._data = {}, true /* asRootData */);\n  }\n  // 初始化computed\n  if (opts.computed) { initComputed(vm, opts.computed); }\n  // 初始化watch\n  if (opts.watch && opts.watch !== nativeWatch) {\n    initWatch(vm, opts.watch);\n  }\n}\n```\n\n## 7.2 initProps\n简单回顾一下```props```的用法，父组件通过属性的形式将数据传递给子组件，子组件通过```props```属性接收父组件传递的值。\n```js\n// 父组件\n<child :test=\"test\"></child>\nvar vm = new Vue({\n  el: '#app',\n  data() {\n    return {\n      test: 'child'\n    }\n  }\n})\n// 子组件\nVue.component('child', {\n  template: '<div>{{test}}</div>',\n  props: ['test']\n})\n```\n因此分析```props```需要分析父组件和子组件的两个过程，我们先看父组件对传递值的处理。按照以往文章介绍的那样，父组件优先进行模板编译得到一个```render```函数，在解析过程中遇到子组件的属性，```:test=test```会被解析成```{ attrs: {test： test}}```并作为子组件的```render```函数存在，如下所示:\n```js\nwith(){..._c('child',{attrs:{\"test\":test}})}\n```\n`render`解析```Vnode```的过程遇到```child```这个子占位符节点，因此会进入创建子组件```Vnode```的过程，创建子```Vnode```过程是调用```createComponent```,这个阶段我们在组件章节有分析过，在组件的高级用法也有分析过，最终会调用```new Vnode```去创建子```Vnode```。而对于```props```的处理，```extractPropsFromVNodeData```会对```attrs```属性进行规范校验后，最后会把校验后的结果以```propsData```属性的形式传入```Vnode```构造器中。总结来说，```props```传递给占位符组件的写法，会以```propsData```的形式作为子组件```Vnode```的属性存在。下面会分析具体的细节。\n\n\n```js\n// 创建子组件过程\nfunction createComponent() {\n  // props校验\n  var propsData = extractPropsFromVNodeData(data, Ctor, tag);\n  ···\n  // 创建子组件vnode\n  var vnode = new VNode(\n    (\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),\n    data, undefined, undefined, undefined, context,\n    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },\n    asyncFactory\n  );\n}\n```\n\n### 7.2.1 props的命名规范\n\n先看检测```props```规范性的过程。**```props```编译后的结果有两种，其中```attrs```前面分析过，是编译生成```render```函数针对属性的处理，而```props```是针对用户自写```render```函数的属性值。**因此需要同时对这两种方式进行校验。\n```js\nfunction extractPropsFromVNodeData (data,Ctor,tag) {\n  // Ctor为子类构造器\n  ···\n  var res = {};\n  // 子组件props选项\n  var propOptions = Ctor.options.props;\n  // data.attrs针对编译生成的render函数，data.props针对用户自定义的render函数\n  var attrs = data.attrs;\n  var props = data.props;\n  if (isDef(attrs) || isDef(props)) {\n    for (var key in propOptions) {\n      // aB 形式转成 a-b\n      var altKey = hyphenate(key);\n      {\n          var keyInLowerCase = key.toLowerCase();\n          if (\n            key !== keyInLowerCase &&\n            attrs && hasOwn(attrs, keyInLowerCase)\n          ) {\n            // 警告\n          }\n        }\n    }\n  }\n}\n```\n重点说一下源码在这一部分的处理，**HTML对大小写是不敏感的，所有的浏览器会把大写字符解释为小写字符，因此我们在使用```DOM```中的模板时，cameCase(驼峰命名法)的```props```名需要使用其等价的 ```kebab-case``` (短横线分隔命名) 命代替**。\n**即： ```<child :aB=\"test\"></child>```需要写成```<child :a-b=\"test\"></child>```**\n\n### 7.2.2 响应式数据props\n刚才说到分析```props```需要两个过程，前面已经针对父组件对```props```的处理做了描述，而对于子组件而言，我们是通过```props```选项去接收父组件传递的值。我们再看看子组件对```props```的处理：\n\n\n子组件处理```props```的过程，是发生在父组件```_update```阶段，这个阶段是```Vnode```生成真实节点的过程，期间会遇到子```Vnode```,这时会调用```createComponent```去实例化子组件。而实例化子组件的过程又回到了```_init```初始化，此时又会经历选项的合并，针对```props```选项，最终会统一成```{props: { test: { type: null }}}```的写法。接着会调用```initProps```, ```initProps```做的事情，简单概括一句话就是，将组件的```props```数据设置为响应式数据。\n\n```js\nfunction initProps (vm, propsOptions) {\n  var propsData = vm.$options.propsData || {};\n  var loop = function(key) {\n    ···\n    defineReactive(props,key,value,cb)；\n    if (!(key in vm)) {\n      proxy(vm, \"_props\", key);\n    }\n  }\n  // 遍历props，执行loop设置为响应式数据。\n  for (var key in propsOptions) loop( key );\n}\n```\n其中```proxy(vm, \"_props\", key);```为```props```做了一层代理，用户通过```vm.XXX```可以代理访问到```vm._props```上的值。针对```defineReactive```,本质上是利用```Object.defineProperty```对数据的```getter,setter```方法进行重写，具体的原理可以参考数据代理章节的内容，在这小节后半段也会有一个基本的实现。\n\n\n## 7.3 initMethods\n`initMethod`方法和这一节介绍的响应式没有任何的关系，他的实现也相对简单，主要是保证```methods```方法定义必须是函数，且命名不能和```props```重复，最终会将定义的方法都挂载到根实例上。\n```js\nfunction initMethods (vm, methods) {\n    var props = vm.$options.props;\n    for (var key in methods) {\n      {\n        // method必须为函数形式\n        if (typeof methods[key] !== 'function') {\n          warn(\n            \"Method \\\"\" + key + \"\\\" has type \\\"\" + (typeof methods[key]) + \"\\\" in the component definition. \" +\n            \"Did you reference the function correctly?\",\n            vm\n          );\n        }\n        // methods方法名不能和props重复\n        if (props && hasOwn(props, key)) {\n          warn(\n            (\"Method \\\"\" + key + \"\\\" has already been defined as a prop.\"),\n            vm\n          );\n        }\n        //  不能以_ or $.这些Vue保留标志开头\n        if ((key in vm) && isReserved(key)) {\n          warn(\n            \"Method \\\"\" + key + \"\\\" conflicts with an existing Vue instance method. \" +\n            \"Avoid defining component methods that start with _ or $.\"\n          );\n        }\n      }\n      // 直接挂载到实例的属性上,可以通过vm[method]访问。\n      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);\n    }\n  }\n```\n\n\n## 7.4 initData\n`data`在初始化选项合并时会生成一个函数，只有在执行函数时才会返回真正的数据，所以```initData```方法会先执行拿到组件的```data```数据，并且会对对象每个属性的命名进行校验，保证不能和```props，methods```重复。最后的核心方法是```observe```,```observe```方法是将**数据对象标记为响应式对象**，并对对象的每个属性进行响应式处理。与此同时，和```props```的代理处理方式一样，```proxy```会对```data```做一层代理，直接通过```vm.XXX```可以代理访问到```vm._data```上挂载的对象属性。\n\n```js\nfunction initData(vm) {\n  var data = vm.$options.data;\n  // 根实例时，data是一个对象，子组件的data是一个函数，其中getData会调用函数返回data对象\n  data = vm._data = typeof data === 'function'? getData(data, vm): data || {};\n  var keys = Object.keys(data);\n  var props = vm.$options.props;\n  var methods = vm.$options.methods;\n  var i = keys.length;\n  while (i--) {\n    var key = keys[i];\n    {\n      // 命名不能和方法重复\n      if (methods && hasOwn(methods, key)) {\n        warn((\"Method \\\"\" + key + \"\\\" has already been defined as a data property.\"),vm);\n      }\n    }\n    // 命名不能和props重复\n    if (props && hasOwn(props, key)) {\n      warn(\"The data property \\\"\" + key + \"\\\" is already declared as a prop. \" + \"Use prop default value instead.\",vm);\n    } else if (!isReserved(key)) {\n      // 数据代理，用户可直接通过vm实例返回data数据\n      proxy(vm, \"_data\", key);\n    }\n  }\n  // observe data\n  observe(data, true /* asRootData */);\n}\n```\n**最后讲讲```observe```,```observe```具体的行为是将数据对象添加一个不可枚举的属性```__ob__```，标志对象是一个响应式对象，并且拿到每个对象的属性值，重写```getter,setter```方法，使得每个属性值都是响应式数据。详细的代码我们后面分析。**\n\n\n## 7.5 initComputed\n和上面的分析方法一样，```initComputed```是```computed```数据的初始化,不同之处在于以下几点：\n 1. `computed`可以是对象，也可以是函数，但是对象必须有```getter```方法,因此如果```computed```中的属性值是对象时需要进行验证。\n 2. 针对```computed```的每个属性，要创建一个监听的依赖，也就是实例化一个```watcher```,```watcher```的定义，可以暂时理解为数据使用的依赖本身，一个```watcher```实例代表多了一个需要被监听的数据依赖。\n\n除了不同点，```initComputed```也会将每个属性设置成响应式的数据，同样的，也会对```computed```的命名做检测，防止与```props,data```冲突。\n\n```js\nfunction initComputed (vm, computed) {\n  ···\n  for (var key in computed) {\n      var userDef = computed[key];\n      var getter = typeof userDef === 'function' ? userDef : userDef.get;\n      // computed属性为对象时，要保证有getter方法\n      if (getter == null) {\n        warn((\"Getter is missing for computed property \\\"\" + key + \"\\\".\"),vm);\n      }\n      if (!isSSR) {\n        // 创建computed watcher\n        watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions);\n      }\n      if (!(key in vm)) {\n        // 设置为响应式数据\n        defineComputed(vm, key, userDef);\n      } else {\n        // 不能和props，data命名冲突\n        if (key in vm.$data) {\n          warn((\"The computed property \\\"\" + key + \"\\\" is already defined in data.\"), vm);\n        } else if (vm.$options.props && key in vm.$options.props) {\n          warn((\"The computed property \\\"\" + key + \"\\\" is already defined as a prop.\"), vm);\n        }\n      }\n    }\n}\n```\n\n显然```Vue```提供了很多种数据供开发者使用，但是分析完后发现每个处理的核心都是将数据转化成响应式数据，有了响应式数据，如何构建一个响应式系统呢？前面提到的```watcher```又是什么东西？构建响应式系统还需要其他的东西吗？接下来我们尝试着去实现一个极简风的响应式系统。\n\n## 7.6 极简风的响应式系统\n`Vue`的响应式系统构建是比较复杂的，直接进入源码分析构建的每一个流程会让理解变得困难，因此我觉得在尽可能保留源码的设计逻辑下,用最小的代码构建一个最基础的响应式系统是有必要的。对```Dep,Watcher,Observer```概念的初步认识，也有助于下一篇对响应式系统设计细节的分析。\n\n### 7.6.1 框架搭建\n我们以```MyVue```作为类响应式框架，框架的搭建不做赘述。我们模拟```Vue```源码的实现思路，实例化```MyVue```时会传递一个选项配置，精简的代码只有一个```id```挂载元素和一个数据对象```data```。模拟源码的思路，我们在实例化时会先进行数据的初始化，这一步就是响应式的构建，我们稍后分析。数据初始化后开始进行真实```DOM```的挂载。\n```js\nvar vm = new MyVue({\n  id: '#app',\n  data: {\n    test: 12\n  }\n})\n// myVue.js\n(function(global) {\n  class MyVue {\n      constructor(options) {\n        this.options = options;\n        // 数据的初始化\n        this.initData(options);\n        let el = this.options.id;\n        // 实例的挂载\n        this.$mount(el);\n      }\n      initData(options) {\n      }\n      $mount(el) {\n      }\n    }\n}(window))\n```\n### 7.6.2 设置响应式对象 - Observer\n首先引入一个类```Observer```,这个类的目的是将数据变成响应式对象，利用```Object.defineProperty```对数据的```getter,setter```方法进行改写。在数据读取```getter```阶段我们会进行**依赖的收集**，在数据的修改```setter```阶段，我们会进行**依赖的更新**(这两个概念的介绍放在后面)。因此在数据初始化阶段，我们会利用```Observer```这个类将数据对象修改为相应式对象，而这是所有流程的基础。\n```js\nclass MyVue {\n  initData(options) {\n    if(!options.data) return;\n    this.data = options.data;\n    // 将数据重置getter，setter方法\n    new Observer(options.data);\n  }\n}\n// Observer类的定义\nclass Observer {\n  constructor(data) {\n    // 实例化时执行walk方法对每个数据属性重写getter，setter方法\n    this.walk(data)\n  }\n\n  walk(obj) {\n    const keys = Object.keys(obj);\n    for(let i = 0;i< keys.length; i++) {\n      // Object.defineProperty的处理逻辑\n      defineReactive(obj, keys[i])\n    }\n  }\n}\n```\n### 7.6.3 依赖本身 - Watcher\n我们可以这样理解，一个```Watcher```实例就是一个依赖，数据不管是在渲染模板时使用还是在用户计算时使用，都可以算做一个需要监听的依赖，```watcher```中记录着这个依赖监听的状态，以及如何更新操作的方法。\n```js\n// 监听的依赖\nclass Watcher {\n  constructor(expOrFn, isRenderWatcher) {\n    this.getter = expOrFn;\n    // Watcher.prototype.get的调用会进行状态的更新。\n    this.get();\n  }\n\n  get() {}\n}\n```\n那么哪个时间点会实例化```watcher```并更新数据状态呢？显然在渲染数据到真实```DOM```时可以创建```watcher```。```$mount```流程前面章节介绍过，会经历模板生成```render```函数和```render```函数渲染真实```DOM```的过程。我们对代码做了精简，```updateView```浓缩了这一过程。\n```js\nclass MyVue {\n  $mount(el) {\n    // 直接改写innerHTML\n    const updateView = _ => {\n      let innerHtml = document.querySelector(el).innerHTML;\n      let key = innerHtml.match(/{(\\w+)}/)[1];\n      document.querySelector(el).innerHTML = this.options.data[key]\n    }\n    // 创建一个渲染的依赖。\n    new Watcher(updateView, true)\n  }\n}\n```\n### 7.6.4 依赖管理 - Dep\n`watcher`如果理解为每个数据需要监听的依赖，那么```Dep``` 可以理解为对依赖的一种管理。数据可以在渲染中使用，也可以在计算属性中使用。相应的每个数据对应的```watcher```也有很多。而我们在更新数据时，如何通知到数据相关的每一个依赖，这就需要```Dep```进行通知管理了。并且浏览器同一时间只能更新一个```watcher```,所以也需要一个属性去记录当前更新的```watcher```。而```Dep```这个类只需要做两件事情，将依赖进行收集，派发依赖进行更新。\n```js\nlet uid = 0;\nclass Dep {\n  constructor() {\n    this.id = uid++;\n    this.subs = []\n  }\n  // 依赖收集\n  depend() {\n    if(Dep.target) {\n      // Dep.target是当前的watcher,将当前的依赖推到subs中\n      this.subs.push(Dep.target)\n    }\n  }\n  // 派发更新\n  notify() {\n    const subs = this.subs.slice();\n    for (var i = 0, l = subs.length; i < l; i++) { \n      // 遍历dep中的依赖，对每个依赖执行更新操作\n      subs[i].update();\n    }\n  }\n}\n\nDep.target = null;\n```\n\n### 7.6.5 依赖管理过程 - defineReactive\n我们看看数据拦截的过程。前面的```Observer```实例化最终会调用```defineReactive```重写```getter,setter```方法。这个方法开始会实例化一个```Dep```,也就是创建一个数据的依赖管理。在重写的```getter```方法中会进行依赖的收集，也就是调用```dep.depend```的方法。在```setter```阶段，比较两个数不同后，会调用依赖的派发更新。即```dep.notify```\n```js\nconst defineReactive = (obj, key) => {\n  const dep = new Dep();\n  const property = Object.getOwnPropertyDescriptor(obj);\n  let val = obj[key]\n  if(property && property.configurable === false) return;\n  Object.defineProperty(obj, key, {\n    configurable: true,\n    enumerable: true,\n    get() {\n      // 做依赖的收集\n      if(Dep.target) {\n        dep.depend()\n      }\n      return val\n    },\n    set(nval) {\n      if(nval === val) return\n      // 派发更新\n      val = nval\n      dep.notify();\n    }\n  })\n}\n```\n回过头来看```watcher```,实例化```watcher```时会将```Dep.target```设置为当前的```watcher```,执行完状态更新函数之后，再将```Dep.target```置空。这样在收集依赖时只要将```Dep.target```当前的```watcher push```到```Dep```的```subs```数组即可。而在派发更新阶段也只需要重新更新状态即可。\n\n```js\nclass Watcher {\n  constructor(expOrFn, isRenderWatcher) {\n    this.getter = expOrFn;\n    // Watcher.prototype.get的调用会进行状态的更新。\n    this.get();\n  }\n\n  get() {\n    // 当前执行的watcher\n    Dep.target = this\n    this.getter()\n    Dep.target = null;\n  }\n  update() {\n    this.get()\n  }\n}\n```\n### 7.6.6 结果\n一个极简的响应式系统搭建完成。在精简代码的同时，保持了源码设计的思想和逻辑。有了这一步的基础，接下来深入分析源码中每个环节的实现细节会更加简单。\n\n\n## 7.7 小结\n这一节内容，我们正式进入响应式系统的介绍，前面在数据代理章节，我们学过```Object.defineProperty```,这是一个用来进行数据拦截的方法，而响应式系统构建的基础就是数据的拦截。我们先介绍了```Vue```内部在初始化数据的过程，最终得出的结论是，不管是```data,computed```,还是其他的用户定义数据，最终都是调用```Object.defineProperty```进行数据拦截。而文章的最后，我们在保留源码设计思想和逻辑的前提下，构建出了一个简化版的响应式系统。完整的功能有助于我们下一节对源码具体实现细节的分析和思考。"
  },
  {
    "path": "src/深入响应式系统构建-下.md",
    "content": "> 上一节，我们深入分析了以```data,computed```为数据创建响应式系统的过程，并对其中依赖收集和派发更新的过程进行了详细的分析。然而在使用和分析过程中依然存在或多或少的问题，这一节我们将针对这些问题展开分析，最后我们也会分析一下```watch```的响应式过程。这篇文章将作为响应式系统分析的完结篇。\n\n## 7.12 数组检测\n在之前介绍数据代理章节，我们已经详细介绍过```Vue```数据代理的技术是利用了```Object.defineProperty```,```Object.defineProperty```让我们可以方便的利用存取描述符中的```getter/setter```来进行数据的监听,在```get,set```钩子中分别做不同的操作，达到数据拦截的目的。然而```Object.defineProperty```的```get,set```方法只能检测到对象属性的变化，对于数组的变化(例如插入删除数组元素等操作)，```Object.defineProperty```却无法达到目的,这也是利用```Object.defineProperty```进行数据监控的缺陷，虽然```es6```中的```proxy```可以完美解决这一问题，但毕竟有兼容性问题，所以我们还需要研究```Vue```在```Object.defineProperty```的基础上如何对数组进行监听检测。\n\n### 7.12.1 数组方法的重写\n既然数组已经不能再通过数据的```getter,setter```方法去监听变化了，```Vue```的做法是对数组方法进行重写，在保留原数组功能的前提下，对数组进行额外的操作处理。也就是重新定义了数组方法。\n\n```js\nvar arrayProto = Array.prototype;\n// 新建一个继承于Array的对象\nvar arrayMethods = Object.create(arrayProto);\n\n// 数组拥有的方法\nvar methodsToPatch = [\n  'push',\n  'pop',\n  'shift',\n  'unshift',\n  'splice',\n  'sort',\n  'reverse'\n];\n```\n`arrayMethods`是基于原始```Array```类为原型继承的一个对象类，由于原型链的继承，```arrayMethod```拥有数组的所有方法，接下来对这个新的数组类的方法进行改写。\n```js\nmethodsToPatch.forEach(function (method) {\n  // 缓冲原始数组的方法\n  var original = arrayProto[method];\n  // 利用Object.defineProperty对方法的执行进行改写\n  def(arrayMethods, method, function mutator () {});\n});\n\nfunction def (obj, key, val, enumerable) {\n    Object.defineProperty(obj, key, {\n      value: val,\n      enumerable: !!enumerable,\n      writable: true,\n      configurable: true\n    });\n  }\n\n```\n\n这里对数组方法设置了代理，当执行```arrayMethods```的数组方法时，会代理执行```mutator```函数，这个函数的具体实现，我们放到数组的派发更新中介绍。\n\n\n**仅仅创建一个新的数组方法合集是不够的，我们在访问数组时，如何不调用原生的数组方法，而是将过程指向这个新的类，这是下一步的重点。**\n\n回到数据初始化过程，也就是执行```initData```阶段，上一篇内容花了大篇幅介绍过数据初始化会为```data```数据创建一个```Observer```类，当时我们只讲述了```Observer```类会为每个非数组的属性进行数据拦截，重新定义```getter,setter```方法,除此之外对于数组类型的数据，我们有意跳过分析了。这里，我们重点看看对于数组拦截的处理。\n\n```js\nvar Observer = function Observer (value) {\n  this.value = value;\n  this.dep = new Dep();\n  this.vmCount = 0;\n  // 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。\n  def(value, '__ob__', this);\n  // 数组处理\n  if (Array.isArray(value)) {\n    if (hasProto) {\n      protoAugment(value, arrayMethods);\n    } else {\n      copyAugment(value, arrayMethods, arrayKeys);\n    }\n    this.observeArray(value);\n  } else {\n  // 对象处理\n    this.walk(value);\n  }\n}\n```\n数组处理的分支分为两个，```hasProto```的判断条件，```hasProto```用来判断当前环境下是否支持```__proto__```属性。而数组的处理会根据是否支持这一属性来决定执行```protoAugment, copyAugment```过程，\n\n```js\n// __proto__属性的判断\nvar hasProto = '__proto__' in {};\n```\n\n**当支持```__proto__```时，执行```protoAugment```会将当前数组的原型指向新的数组类```arrayMethods```,如果不支持```__proto__```，则通过代理设置，在访问数组方法时代理访问新数组类中的数组方法。**\n```js\n//直接通过原型指向的方式\n\nfunction protoAugment (target, src) {\n  target.__proto__ = src;\n}\n\n// 通过数据代理的方式\nfunction copyAugment (target, src, keys) {\n  for (var i = 0, l = keys.length; i < l; i++) {\n    var key = keys[i];\n    def(target, key, src[key]);\n  }\n}\n```\n有了这两步的处理，接下来我们在实例内部调用```push, unshift```等数组的方法时，会执行```arrayMethods```类的方法。这也是数组进行依赖收集和派发更新的前提。\n\n\n### 7.12.2 依赖收集\n由于数据初始化阶段会利用```Object.definePrototype```进行数据访问的改写，数组的访问同样会被```getter```所拦截。由于是数组，拦截过程会做特殊处理，后面我们再看看```dependArray```的原理。\n```js\nfunction defineReactive###1() {\n  ···\n  var childOb = !shallow && observe(val);\n\n  Object.defineProperty(obj, key, {\n        enumerable: true,\n        configurable: true,\n        get: function reactiveGetter () {\n          var value = getter ? getter.call(obj) : val;\n          if (Dep.target) {\n            dep.depend();\n            if (childOb) {\n              childOb.dep.depend();\n              if (Array.isArray(value)) {\n                dependArray(value);\n              }\n            }\n          }\n          return value\n        },\n        set() {}\n}\n \n```\n`childOb`是标志属性值是否为基础类型的标志，```observe```如果遇到基本类型数据，则直接返回，不做任何处理，如果遇到对象或者数组则会递归实例化```Observer```，会为每个子属性设置响应式数据，最终返回```Observer```实例。而实例化```Observer```又回到之前的老流程：\n  **添加```__ob__```属性，如果遇到数组则进行原型重指向，遇到对象则定义```getter,setter```，这一过程前面分析过，就不再阐述。**\n\n\n在访问到数组时，由于```childOb```的存在，会执行```childOb.dep.depend();```进行依赖收集，该```Observer```实例的```dep```属性会收集当前的```watcher```作为依赖保存，```dependArray```保证了如果数组元素是数组或者对象，需要递归去为内部的元素收集相关的依赖。\n```js\nfunction dependArray (value) {\n    for (var e = (void 0), i = 0, l = value.length; i < l; i++) {\n      e = value[i];\n      e && e.__ob__ && e.__ob__.dep.depend();\n      if (Array.isArray(e)) {\n        dependArray(e);\n      }\n    }\n  }\n\n```\n\n我们可以通过截图看最终依赖收集的结果。\n\n收集前\n\n![](./img/7.1.png)\n\n收集后\n\n![](./img/7.2.png)\n\n\n### 7.12.3 派发更新\n当调用数组的方法去添加或者删除数据时，数据的```setter```方法是无法拦截的，所以我们唯一可以拦截的过程就是调用数组方法的时候，前面介绍过，数组方法的调用会代理到新类```arrayMethods```的方法中,而```arrayMethods```的数组方法是进行重写过的。具体我们看他的定义。\n\n```js\n methodsToPatch.forEach(function (method) {\n    var original = arrayProto[method];\n    def(arrayMethods, method, function mutator () {\n      var args = [], len = arguments.length;\n      while ( len-- ) args[ len ] = arguments[ len ];\n      // 执行原数组方法\n      var result = original.apply(this, args);\n      var ob = this.__ob__;\n      var inserted;\n      switch (method) {\n        case 'push':\n        case 'unshift':\n          inserted = args;\n          break\n        case 'splice':\n          inserted = args.slice(2);\n          break\n      }\n      if (inserted) { ob.observeArray(inserted); }\n      // notify change\n      ob.dep.notify();\n      return result\n    });\n  });\n\n```\n`mutator`是重写的数组方法，首先会调用原始的数组方法进行运算，这保证了与原始数组类型的方法一致性，```args```保存了数组方法调用传递的参数。之后取出数组的```__ob__```也就是之前保存的```Observer```实例，调用```ob.dep.notify();```进行依赖的派发更新，前面知道了。```Observer```实例的```dep```是```Dep```的实例，他收集了需要监听的```watcher```依赖，而```notify```会对依赖进行重新计算并更新。具体看```Dep.prototype.notify = function notify () {}```函数的分析，这里也不重复赘述。\n\n回到代码中，```inserted```变量用来标志数组是否是增加了元素，如果增加的元素不是原始类型，而是数组对象类型，则需要触发```observeArray```方法，对每个元素进行依赖收集。\n\n```js\nObserver.prototype.observeArray = function observeArray (items) {\n  for (var i = 0, l = items.length; i < l; i++) {\n    observe(items[i]);\n  }\n};\n```\n**总的来说。数组的改变不会触发```setter```进行依赖更新，所以```Vue```创建了一个新的数组类，重写了数组的方法，将数组方法指向了新的数组类。同时在访问到数组时依旧触发```getter```进行依赖收集，在更改数组时，触发数组新方法运算，并进行依赖的派发。**\n\n现在我们回过头看看Vue的官方文档对于数组检测时的注意事项：\n> ```Vue``` 不能检测以下数组的变动:\n> - 当你利用索引直接设置一个数组项时，例如：```vm.items[indexOfItem] = newValue```\n> - 当你修改数组的长度时，例如：```vm.items.length = newLength```\n\n显然有了上述的分析我们很容易理解数组检测带来的弊端，即使```Vue```重写了数组的方法，以便在设置数组时进行拦截处理，但是不管是通过索引还是直接修改长度，都是无法触发依赖更新的。\n\n\n## 7.13 对象检测异常\n我们在实际开发中经常遇到一种场景，对象```test: { a: 1 }```要添加一个属性```b```,这时如果我们使用```test.b = 2```的方式去添加，这个过程```Vue```是无法检测到的，理由也很简单。我们在对对象进行依赖收集的时候，会为对象的每个属性都进行收集依赖，而直接通过```test.b```添加的新属性并没有依赖收集的过程，因此当之后数据```b```发生改变时也不会进行依赖的更新。\n\n了解决这一问题，```Vue```提供了```Vue.set(object, propertyName, value)```的静态方法和```vm.$set(object, propertyName, value)```的实例方法，我们看具体怎么完成新属性的依赖收集过程。\n```js\nVue.set = set\nfunction set (target, key, val) {\n    //target必须为非空对象\n    if (isUndef(target) || isPrimitive(target)\n    ) {\n      warn((\"Cannot set reactive property on undefined, null, or primitive value: \" + ((target))));\n    }\n    // 数组场景，调用重写的splice方法，对新添加属性收集依赖。\n    if (Array.isArray(target) && isValidArrayIndex(key)) {\n      target.length = Math.max(target.length, key);\n      target.splice(key, 1, val);\n      return val\n    }\n    // 新增对象的属性存在时，直接返回新属性，触发依赖收集\n    if (key in target && !(key in Object.prototype)) {\n      target[key] = val;\n      return val\n    }\n    // 拿到目标源的Observer 实例\n    var ob = (target).__ob__;\n    if (target._isVue || (ob && ob.vmCount)) {\n      warn(\n        'Avoid adding reactive properties to a Vue instance or its root $data ' +\n        'at runtime - declare it upfront in the data option.'\n      );\n      return val\n    }\n    // 目标源对象本身不是一个响应式对象，则不需要处理\n    if (!ob) {\n      target[key] = val;\n      return val\n    }\n    // 手动调用defineReactive，为新属性设置getter,setter\n    defineReactive###1(ob.value, key, val);\n    ob.dep.notify();\n    return val\n  }\n```\n按照分支分为不同的四个处理逻辑：\n1. 目标对象必须为非空的对象，可以是数组，否则抛出异常。\n2. 如果目标对象是数组时，调用数组的```splice```方法，而前面分析数组检测时，遇到数组新增元素的场景，会调用```ob.observeArray(inserted)```对数组新增的元素收集依赖。\n3. 新增的属性值在原对象中已经存在，则手动访问新的属性值，这一过程会触发依赖收集。\n4. 手动定义新属性的```getter,setter```方法，并通过```notify```触发依赖更新。\n\n\n## 7.14 nextTick\n\n在上一节的内容中，我们说到数据修改时会触发```setter```方法进行依赖的派发更新，而更新时会将每个```watcher```推到队列中，等待下一个```tick```到来时再执行```DOM```的渲染更新操作。这个就是异步更新的过程。为了说明异步更新的概念，需要牵扯到浏览器的事件循环机制和最优的渲染时机问题。由于这不是文章的主线，我只用简单的语言概述。\n\n### 7.14.1 事件循环机制\n\n1. 完整的事件循环机制需要了解两种异步队列：```macro-task```和```micro-task```\n2. ```macro-task```常见的有 ```setTimeout, setInterval, setImmediate, script脚本, I/O操作，UI渲染```\n3. ```micro-task```常见的有 ```promise, process.nextTick, MutationObserver```等\n4. 完整事件循环流程为：\n  4.1 ```micro-task```空，```macro-task```队列只有```script```脚本，推出```macro-task```的```script```任务执行，脚本执行期间产生的```macro-task，micro-task```推到对应的队列中\n  4.2 执行全部```micro-task```里的微任务事件\n  4.3 执行```DOM```操作，渲染更新页面\n  4.4 执行```web worker```等相关任务\n  4.5 循环，取出```macro-task```中一个宏任务事件执行，重复4的操作。\n\n\n从上面的流程中我们可以发现，最好的渲染过程发生在微任务队列的执行过程中，此时他离页面渲染过程最近，因此我们可以借助微任务队列来实现异步更新，它可以让复杂批量的运算操作运行在JS层面，而视图的渲染只关心最终的结果，这大大降低了性能的损耗。\n  \n举一个这一做法好处的例子： \n  由于```Vue```是数据驱动视图更新渲染，如果我们在一个操作中重复对一个响应式数据进行计算，例如 在一个循环中执行```this.num ++ ```一千次，由于响应式系统的存在，数据变化触发```setter```，```setter```触发依赖派发更新，更新调用```run```进行视图的重新渲染。这一次循环，视图渲染要执行一千次，很明显这是很浪费性能的，我们只需要关注最后第一千次在界面上更新的结果而已。所以利用异步更新显得格外重要。\n\n### 7.14.2 基本实现\n\n`Vue`用一个```queue```收集依赖的执行，在下次微任务执行的时候统一执行```queue```中```Watcher```的```run```操作,与此同时，相同```id```的```watcher```不会重复添加到```queue```中,因此也不会重复执行多次的视图渲染。我们看```nextTick```的实现。\n\n```js\n// 原型上定义的方法\nVue.prototype.$nextTick = function (fn) {\n  return nextTick(fn, this)\n};\n// 构造函数上定义的方法\nVue.nextTick = nextTick;\n\n// 实际的定义\nvar callbacks = [];\nfunction nextTick (cb, ctx) {\n    var _resolve;\n    // callbacks是维护微任务的数组。\n    callbacks.push(function () {\n      if (cb) {\n        try {\n          cb.call(ctx);\n        } catch (e) {\n          handleError(e, ctx, 'nextTick');\n        }\n      } else if (_resolve) {\n        _resolve(ctx);\n      }\n    });\n    if (!pending) {\n      pending = true;\n      // 将维护的队列推到微任务队列中维护\n      timerFunc();\n    }\n    // nextTick没有传递参数，且浏览器支持Promise,则返回一个promise对象\n    if (!cb && typeof Promise !== 'undefined') {\n      return new Promise(function (resolve) {\n        _resolve = resolve;\n      })\n    }\n  }\n```\n\n`nextTick`定义为一个函数，使用方式为```Vue.nextTick( [callback, context] )```,当```callback```经过```nextTick```封装后，```callback```会在下一个```tick```中执行调用。从实现上，```callbacks```是一个维护了需要在下一个```tick```中执行的任务的队列，它的每个元素都是需要执行的函数。```pending```是判断是否在等待执行微任务队列的标志。而```timerFunc```是真正将任务队列推到微任务队列中的函数。我们看```timerFunc```的实现。\n\n\n1.如果浏览器执行```Promise```,那么默认以```Promsie```将执行过程推到微任务队列中。\n\n```js\nvar timerFunc;\n\nif (typeof Promise !== 'undefined' && isNative(Promise)) {\n  var p = Promise.resolve();\n  timerFunc = function () {\n    p.then(flushCallbacks);\n    // 手机端的兼容代码\n    if (isIOS) { setTimeout(noop); }\n  };\n  // 使用微任务队列的标志\n  isUsingMicroTask = true;\n}\n```\n\n`flushCallbacks`是异步更新的函数，他会取出callbacks数组的每一个任务，执行任务，具体定义如下：\n```js\nfunction flushCallbacks () {\n  pending = false;\n  var copies = callbacks.slice(0);\n  // 取出callbacks数组的每一个任务，执行任务\n  callbacks.length = 0;\n  for (var i = 0; i < copies.length; i++) {\n    copies[i]();\n  }\n}\n```\n\n2.不支持```promise```,支持```MutataionObserver```\n\n```js\nelse if (!isIE && typeof MutationObserver !== 'undefined' && (\n    isNative(MutationObserver) ||\n    // PhantomJS and iOS 7.x\n    MutationObserver.toString() === '[object MutationObserverConstructor]'\n  )) {\n    var counter = 1;\n    var observer = new MutationObserver(flushCallbacks);\n    var textNode = document.createTextNode(String(counter));\n    observer.observe(textNode, {\n      characterData: true\n    });\n    timerFunc = function () {\n      counter = (counter + 1) % 2;\n      textNode.data = String(counter);\n    };\n    isUsingMicroTask = true;\n  }\n```\n\n3.如果不支持微任务方法，则会使用宏任务方法，```setImmediate```会先被使用\n\n```js\n else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {\n    // Fallback to setImmediate.\n    // Techinically it leverages the (macro) task queue,\n    // but it is still a better choice than setTimeout.\n    timerFunc = function () {\n      setImmediate(flushCallbacks);\n    };\n  }\n```\n4.所有方法都不适合，会使用宏任务方法中的```setTimeout```\n\n```js\nelse {\n  timerFunc = function () {\n    setTimeout(flushCallbacks, 0);\n  };\n}\n```\n\n**当```nextTick```不传递任何参数时，可以作为一个```promise```用**，例如：\n```js\nnextTick().then(() => {})\n```\n\n### 7.14.3 使用场景\n说了这么多原理性的东西，回过头来看看```nextTick```的使用场景，由于异步更新的原理，我们在某一时间改变的数据并不会触发视图的更新，而是需要等下一个```tick```到来时才会更新视图，下面是一个典型场景：\n\n```js\n<input v-if=\"show\" type=\"text\" ref=\"myInput\">\n\n// js\ndata() {\n  show: false\n},\nmounted() {\n  this.show = true;\n  this.$refs.myInput.focus();// 报错\n}\n```\n数据改变时，视图并不会同时改变，因此需要使用```nextTick```\n```js\nmounted() {\n  this.show = true;\n  this.$nextTick(function() {\n    this.$refs.myInput.focus();// 正常\n  })\n}\n```\n\n## 7.15 watch\n到这里，关于响应式系统的分析大部分内容已经分析完毕，我们上一节还遗留着一个问题，```Vue```对用户手动添加的```watch```如何进行数据拦截。我们先看看两种基本的使用形式。\n```js\n// watch选项\nvar vm = new Vue({\n  el: '#app',\n  data() {\n    return {\n      num: 12\n    }\n  },\n  watch: {\n    num() {}\n  }\n})\nvm.num = 111\n\n// $watch api方式\nvm.$watch('num', function() {}, {\n  deep: ,\n  immediate: ,\n})\n```\n\n### 7.15.1 依赖收集\n我们以```watch```选项的方式来分析```watch```的细节，同样从初始化说起，初始化数据会执行```initWatch```,```initWatch```的核心是```createWatcher```。\n\n```js\nfunction initWatch (vm, watch) {\n    for (var key in watch) {\n      var handler = watch[key];\n      // handler可以是数组的形式，执行多个回调\n      if (Array.isArray(handler)) {\n        for (var i = 0; i < handler.length; i++) {\n          createWatcher(vm, key, handler[i]);\n        }\n      } else {\n        createWatcher(vm, key, handler);\n      }\n    }\n  }\n\n  function createWatcher (vm,expOrFn,handler,options) {\n    // 针对watch是对象的形式，此时回调回选项中的handler\n    if (isPlainObject(handler)) {\n      options = handler;\n      handler = handler.handler;\n    }\n    if (typeof handler === 'string') {\n      handler = vm[handler];\n    }\n    return vm.$watch(expOrFn, handler, options)\n  }\n```\n无论是选项的形式，还是```api```的形式，最终都会调用实例的```$watch```方法，其中```expOrFn```是监听的字符串，```handler```是监听的回调函数，```options```是相关配置。我们重点看看```$watch```的实现。\n```js\nVue.prototype.$watch = function (expOrFn,cb,options) {\n    var vm = this;\n    if (isPlainObject(cb)) {\n      return createWatcher(vm, expOrFn, cb, options)\n    }\n    options = options || {};\n    options.user = true;\n    var watcher = new Watcher(vm, expOrFn, cb, options);\n    // 当watch有immediate选项时，立即执行cb方法，即不需要等待属性变化，立刻执行回调。\n    if (options.immediate) {\n      try {\n        cb.call(vm, watcher.value);\n      } catch (error) {\n        handleError(error, vm, (\"callback for immediate watcher \\\"\" + (watcher.expression) + \"\\\"\"));\n      }\n    }\n    return function unwatchFn () {\n      watcher.teardown();\n    }\n  };\n}\n```\n`$watch`的核心是创建一个```user watcher```,```options.user```是当前用户定义```watcher```的标志。如果有```immediate```属性，则立即执行回调函数。\n而实例化```watcher```时会执行一次```getter```求值，这时，```user watcher```会作为依赖被数据所收集。这个过程可以参考```data```的分析。\n\n```js\nvar Watcher = function Watcher() {\n  ···\n  this.value = this.lazy\n      ? undefined\n      : this.get();\n}\n\nWatcher.prototype.get = function get() {\n  ···\n  try {\n    // getter回调函数，触发依赖收集\n    value = this.getter.call(vm, vm);\n  } \n}\n```\n\n### 7.15.2 派发更新\n`watch`派发更新的过程很好理解，数据发生改变时，```setter```拦截对依赖进行更新，而此前```user watcher```已经被当成依赖收集了。这个时候依赖的更新就是回调函数的执行。\n\n\n\n## 7.16 小结\n这一节是响应式系统构建的完结篇，```data,computed```如何进行响应式系统设计，这在上一节内容已经详细分析，这一节针对一些特殊场景做了分析。例如由于```Object.defineProperty```自身的缺陷，无法对数组的新增删除进行拦截检测，因此```Vue```对数组进行了特殊处理，重写了数组的方法，并在方法中对数据进行拦截。我们也重点介绍了```nextTick```的原理，利用浏览器的事件循环机制来达到最优的渲染时机。文章的最后补充了```watch```在响应式设计的原理，用户自定义的```watch```会创建一个依赖，这个依赖在数据改变时会执行回调。\n"
  },
  {
    "path": "src/深入响应式系统构建-中.md",
    "content": ">为了深入介绍响应式系统的内部实现原理，我们花了一整节的篇幅介绍了数据(包括```data, computed,props```)如何初始化成为响应式对象的过程。有了响应式数据对象的知识，上一节的后半部分我们还在保留源码结构的基础上构建了一个以```data```为数据的响应式系统，而这一节，我们继续深入响应式系统内部构建的细节，详细分析```Vue```在响应式系统中对```data,computed```的处理。\n\n## 7.8 相关概念\n在构建简易式响应式系统的时候，我们引出了几个重要的概念，他们都是响应式原理设计的核心，我们先简单回顾一下：\n- `Observer`类，实例化一个```Observer```类会通过```Object.defineProperty```对数据的```getter,setter```方法进行改写，在```getter```阶段进行**依赖的收集**,在数据发生更新阶段，触发```setter```方法进行**依赖的更新**\n- `watcher`类，实例化```watcher```类相当于创建一个依赖，简单的理解是数据在哪里被使用就需要产生了一个依赖。当数据发生改变时，会通知到每个依赖进行更新，前面提到的渲染```wathcer```便是渲染```dom```时使用数据产生的依赖。\n- `Dep`类，既然```watcher```理解为每个数据需要监听的依赖，那么对这些依赖的收集和通知则需要另一个类来管理，这个类便是```Dep```,```Dep```需要做的只有两件事，收集依赖和派发更新依赖。\n\n\n这是响应式系统构建的三个基本核心概念，也是这一节的基础，如果还没有印象，请先回顾上一节对**极简风响应式系统的构建**。\n\n\n## 7.9 data\n### 7.9.1 问题思考\n在开始分析```data```之前，我们先抛出几个问题让读者思考，而答案都包含在接下来内容分析中。\n\n- 前面已经知道，```Dep```是作为管理依赖的容器，那么这个容器在什么时候产生？也就是实例化```Dep```发生在什么时候？\n\n- `Dep`收集了什么类型的依赖？即```watcher```作为依赖的分类有哪些，分别是什么场景，以及区别在哪里？\n- `Observer`这个类具体对```getter,setter```方法做了哪些事情？\n- 手写的```watcher```和页面数据渲染监听的```watch```如果同时监听到数据的变化，优先级怎么排？\n- 有了依赖的收集是不是还有依赖的解除，依赖解除的意义在哪里？\n\n带着这几个问题，我们开始对```data```的响应式细节展开分析。\n\n### 7.9.2 依赖收集\n`data`在初始化阶段会实例化一个```Observer```类，这个类的定义如下(忽略数组类型的```data```):\n```js\n// initData \nfunction initData(data) {\n  ···\n  observe(data, true)\n}\n// observe\nfunction observe(value, asRootData) {\n  ···\n  ob = new Observer(value);\n  return ob\n}\n\n// 观察者类，对象只要设置成拥有观察属性，则对象下的所有属性都会重写getter和setter方法，而getter，setting方法会进行依赖的收集和派发更新\nvar Observer = function Observer (value) {\n    ···\n    // 将__ob__属性设置成不可枚举属性。外部无法通过遍历获取。\n    def(value, '__ob__', this);\n    // 数组处理\n    if (Array.isArray(value)) {\n        ···\n    } else {\n      // 对象处理\n      this.walk(value);\n    }\n  };\n\nfunction def (obj, key, val, enumerable) {\n  Object.defineProperty(obj, key, {\n    value: val,\n    enumerable: !!enumerable, // 是否可枚举\n    writable: true,\n    configurable: true\n  });\n}\n```\n`Observer`会为```data```添加一个```__ob__```属性， ```__ob__```属性是作为响应式对象的标志，同时```def```方法确保了该属性是不可枚举属性，即外界无法通过遍历获取该属性值。除了标志响应式对象外，```Observer```类还调用了原型上的```walk```方法，遍历对象上每个属性进行```getter,setter```的改写。\n```js\nObserver.prototype.walk = function walk (obj) {\n    // 获取对象所有属性，遍历调用defineReactive###1进行改写\n    var keys = Object.keys(obj);\n    for (var i = 0; i < keys.length; i++) {\n        defineReactive###1(obj, keys[i]);\n    }\n};\n```\n\n\n\n`defineReactive###1`是响应式构建的核心，它会先**实例化一个```Dep```类，即为每个数据都创建一个依赖的管理**，之后利用```Object.defineProperty```重写```getter,setter```方法。这里我们只分析依赖收集的代码。\n```js\nfunction defineReactive###1 (obj,key,val,customSetter,shallow) {\n    // 每个数据实例化一个Dep类，创建一个依赖的管理\n    var dep = new Dep();\n\n    var property = Object.getOwnPropertyDescriptor(obj, key);\n    // 属性必须满足可配置\n    if (property && property.configurable === false) {\n      return\n    }\n    // cater for pre-defined getter/setters\n    var getter = property && property.get;\n    var setter = property && property.set;\n    // 这一部分的逻辑是针对深层次的对象，如果对象的属性是一个对象，则会递归调用实例化Observe类，让其属性值也转换为响应式对象\n    var childOb = !shallow && observe(val);\n    Object.defineProperty(obj, key, {\n      enumerable: true,\n      configurable: true,s\n      get: function reactiveGetter () {\n        var value = getter ? getter.call(obj) : val;\n        if (Dep.target) {\n          // 为当前watcher添加dep数据\n          dep.depend();\n          if (childOb) {\n            childOb.dep.depend();\n            if (Array.isArray(value)) {\n              dependArray(value);\n            }\n          }\n        }\n        return value\n      },\n      set: function reactiveSetter (newVal) {}\n    });\n  }\n```\n\n主要看```getter```的逻辑，我们知道当```data```中属性值被访问时，会被```getter```函数拦截，根据我们旧有的知识体系可以知道，实例挂载前会创建一个渲染```watcher```。\n```js\nnew Watcher(vm, updateComponent, noop, {\n  before: function before () {\n    if (vm._isMounted && !vm._isDestroyed) {\n      callHook(vm, 'beforeUpdate');\n    }\n  }\n}, true /* isRenderWatcher */);\n```\n与此同时，```updateComponent```的逻辑会执行实例的挂载，在这个过程中，模板会被优先解析为```render```函数，而```render```函数转换成```Vnode```时，会访问到定义的```data```数据，这个时候会触发```gettter```进行依赖收集。而此时数据收集的依赖就是这个渲染```watcher```本身。\n\n代码中依赖收集阶段会做下面几件事：\n1. **为当前的```watcher```(该场景下是渲染```watcher```)添加拥有的数据**。\n2. **为当前的数据收集需要监听的依赖**\n\n如何理解这两点？我们先看代码中的实现。```getter```阶段会执行```dep.depend()```,这是```Dep```这个类定义在原型上的方法。\n```js\ndep.depend();\n\n\nDep.prototype.depend = function depend () {\n    if (Dep.target) {\n      Dep.target.addDep(this);\n    }\n  };\n```\n`Dep.target`为当前执行的```watcher```,在渲染阶段，```Dep.target```为组件挂载时实例化的渲染```watcher```,因此```depend```方法又会调用当前```watcher```的```addDep```方法为```watcher```添加依赖的数据。\n\n```js\nWatcher.prototype.addDep = function addDep (dep) {\n    var id = dep.id;\n    if (!this.newDepIds.has(id)) {\n      // newDepIds和newDeps记录watcher拥有的数据\n      this.newDepIds.add(id);\n      this.newDeps.push(dep);\n      // 避免重复添加同一个data收集器\n      if (!this.depIds.has(id)) {\n        dep.addSub(this);\n      }\n    }\n  };\n```\n\n其中```newDepIds```是具有唯一成员是```Set```数据结构，```newDeps```是数组，他们用来记录当前```watcher```所拥有的数据，这一过程会进行逻辑判断，避免同一数据添加多次。\n\n`addSub`为每个数据依赖收集器添加需要被监听的```watcher```。\n\n```js\nDep.prototype.addSub = function addSub (sub) {\n  //将当前watcher添加到数据依赖收集器中\n    this.subs.push(sub);\n};\n```\n\n3. **`getter`如果遇到属性值为对象时，会为该对象的每个值收集依赖**\n\n这句话也很好理解，如果我们将一个值为基本类型的响应式数据改变成一个对象，此时新增对象里的属性，也需要设置成响应式数据。\n\n4. **遇到属性值为数组时，进行特殊处理**，这点放到后面讲。\n\n**通俗的总结一下依赖收集的过程，每个数据就是一个依赖管理器，而每个使用数据的地方就是一个依赖。当访问到数据时，会将当前访问的场景作为一个依赖收集到依赖管理器中，同时也会为这个场景的依赖收集拥有的数据。**\n\n\n### 7.9.3 派发更新\n在分析依赖收集的过程中，可能会有不少困惑，为什么要维护这么多的关系？在数据更新时，这些关系会起到什么作用？带着疑惑，我们来看看派发更新的过程。\n在数据发生改变时，会执行定义好的```setter```方法，我们先看源码。\n```js\nObject.defineProperty(obj,key, {\n  ···\n  set: function reactiveSetter (newVal) {\n      var value = getter ? getter.call(obj) : val;\n      // 新值和旧值相等时，跳出操作\n      if (newVal === value || (newVal !== newVal && value !== value)) {\n        return\n      }\n      ···\n      // 新值为对象时，会为新对象进行依赖收集过程\n      childOb = !shallow && observe(newVal);\n      dep.notify();\n    }\n})\n```\n派发更新阶段会做以下几件事：\n- **判断数据更改前后是否一致，如果数据相等则不进行任何派发更新操作**。\n- **新值为对象时，会对该值的属性进行依赖收集过程**。\n- **通知该数据收集的```watcher```依赖,遍历每个```watcher```进行数据更新**,这个阶段是调用该数据依赖收集器的```dep.notify```方法进行更新的派发。\n```js\nDep.prototype.notify = function notify () {\n    var subs = this.subs.slice();\n    if (!config.async) {\n      // 根据依赖的id进行排序\n      subs.sort(function (a, b) { return a.id - b.id; });\n    }\n    for (var i = 0, l = subs.length; i < l; i++) {\n      // 遍历每个依赖，进行更新数据操作。\n      subs[i].update();\n    }\n  };\n```\n- **更新时会将每个```watcher```推到队列中，等待下一个```tick```到来时取出每个```watcher```进行```run```操作**\n```js\n Watcher.prototype.update = function update () {\n    ···\n    queueWatcher(this);\n  };\n```\n`queueWatcher`方法的调用，会将数据所收集的依赖依次推到```queue```数组中,数组会在下一个事件循环```'tick'```中根据缓冲结果进行视图更新。而在执行视图更新过程中，难免会因为数据的改变而在渲染模板上添加新的依赖，这样又会执行```queueWatcher```的过程。所以需要有一个标志位来记录是否处于异步更新过程的队列中。这个标志位为```flushing```,当处于异步更新过程时，新增的```watcher```会插入到```queue```中。\n```js\nfunction queueWatcher (watcher) {\n    var id = watcher.id;\n    // 保证同一个watcher只执行一次\n    if (has[id] == null) {\n      has[id] = true;\n      if (!flushing) {\n        queue.push(watcher);\n      } else {\n        var i = queue.length - 1;\n        while (i > index && queue[i].id > watcher.id) {\n          i--;\n        }\n        queue.splice(i + 1, 0, watcher);\n      }\n      ···\n      nextTick(flushSchedulerQueue);\n    }\n  }\n```\n`nextTick`的原理和实现先不讲，概括来说，```nextTick```会缓冲多个数据处理过程，等到下一个事件循环```tick```中再去执行```DOM```操作，**它的原理，本质是利用事件循环的微任务队列实现异步更新**。\n\n\n当下一个```tick```到来时，会执行```flushSchedulerQueue```方法，它会拿到收集的```queue```数组(这是一个```watcher```的集合),并对数组依赖进行排序。为什么进行排序呢？源码中解释了三点：\n\n> - 组件创建是先父后子，所以组件的更新也是先父后子，因此需要保证父的渲染```watcher```优先于子的渲染```watcher```更新。\n> - **用户自定义的```watcher```,称为```user watcher```。 ```user watcher```和```render watcher```执行也有先后，由于```user watchers```比```render watcher```要先创建，所以```user watcher```要优先执行**。\n> - 如果一个组件在父组件的 ```watcher``` 执行阶段被销毁，那么它对应的 ```watcher``` 执行都可以被跳过。\n\n\n```js\nfunction flushSchedulerQueue () {\n    currentFlushTimestamp = getNow();\n    flushing = true;\n    var watcher, id;\n    // 对queue的watcher进行排序\n    queue.sort(function (a, b) { return a.id - b.id; });\n    // 循环执行queue.length，为了确保由于渲染时添加新的依赖导致queue的长度不断改变。\n    for (index = 0; index < queue.length; index++) {\n      watcher = queue[index];\n      // 如果watcher定义了before的配置，则优先执行before方法\n      if (watcher.before) {\n        watcher.before();\n      }\n      id = watcher.id;\n      has[id] = null;\n      watcher.run();\n      // in dev build, check and stop circular updates.\n      if (has[id] != null) {\n        circular[id] = (circular[id] || 0) + 1;\n        if (circular[id] > MAX_UPDATE_COUNT) {\n          warn(\n            'You may have an infinite update loop ' + (\n              watcher.user\n                ? (\"in watcher with expression \\\"\" + (watcher.expression) + \"\\\"\")\n                : \"in a component render function.\"\n            ),\n            watcher.vm\n          );\n          break\n        }\n      }\n    }\n\n    // keep copies of post queues before resetting state\n    var activatedQueue = activatedChildren.slice();\n    var updatedQueue = queue.slice();\n    // 重置恢复状态，清空队列\n    resetSchedulerState();\n\n    // 视图改变后，调用其他钩子\n    callActivatedHooks(activatedQueue);\n    callUpdatedHooks(updatedQueue);\n\n    // devtool hook\n    /* istanbul ignore if */\n    if (devtools && config.devtools) {\n      devtools.emit('flush');\n    }\n  }\n```\n\n\n`flushSchedulerQueue`阶段，重要的过程可以总结为四点：\n> - 对```queue```中的```watcher```进行排序，原因上面已经总结。\n> - 遍历```watcher```,如果当前```watcher```有```before```配置，则执行```before```方法，对应前面的渲染```watcher```:在渲染```watcher```实例化时，我们传递了```before```函数，即在下个```tick```更新视图前，会调用```beforeUpdate```生命周期钩子。\n> - 执行```watcher.run```进行修改的操作。\n> - 重置恢复状态，这个阶段会将一些流程控制的状态变量恢复为初始值，并清空记录```watcher```的队列。\n\n```js\nnew Watcher(vm, updateComponent, noop, {\n  before: function before () {\n    if (vm._isMounted && !vm._isDestroyed) {\n      callHook(vm, 'beforeUpdate');\n    }\n  }\n}, true /* isRenderWatcher */);\n```\n\n\n重点看看```watcher.run()```的操作。\n```js\nWatcher.prototype.run = function run () {\n    if (this.active) {\n      var value = this.get();\n      if ( value !== this.value || isObject(value) || this.deep ) {\n        // 设置新值\n        var oldValue = this.value;\n        this.value = value;\n        // 针对user watcher，暂时不分析\n        if (this.user) {\n          try {\n            this.cb.call(this.vm, value, oldValue);\n          } catch (e) {\n            handleError(e, this.vm, (\"callback for watcher \\\"\" + (this.expression) + \"\\\"\"));\n          }\n        } else {\n          this.cb.call(this.vm, value, oldValue);\n        }\n      }\n    }\n  };\n```\n首先会执行```watcher.prototype.get```的方法，得到数据变化后的当前值，之后会对新值做判断，如果判断满足条件，则执行```cb```,```cb```为实例化```watcher```时传入的回调。\n\n在分析```get```方法前，回头看看```watcher```构造函数的几个属性定义\n```js\nvar watcher = function Watcher(\n  vm, // 组件实例\n  expOrFn, // 执行函数\n  cb, // 回调\n  options, // 配置\n  isRenderWatcher // 是否为渲染watcher\n) {\n  this.vm = vm;\n    if (isRenderWatcher) {\n      vm._watcher = this;\n    }\n    vm._watchers.push(this);\n    // options\n    if (options) {\n      this.deep = !!options.deep;\n      this.user = !!options.user;\n      this.lazy = !!options.lazy;\n      this.sync = !!options.sync;\n      this.before = options.before;\n    } else {\n      this.deep = this.user = this.lazy = this.sync = false;\n    }\n    this.cb = cb;\n    this.id = ++uid$2; // uid for batching\n    this.active = true;\n    this.dirty = this.lazy; // for lazy watchers\n    this.deps = [];\n    this.newDeps = [];\n    this.depIds = new _Set();\n    this.newDepIds = new _Set();\n    this.expression = expOrFn.toString();\n    // parse expression for getter\n    if (typeof expOrFn === 'function') {\n      this.getter = expOrFn;\n    } else {\n      this.getter = parsePath(expOrFn);\n      if (!this.getter) {\n        this.getter = noop;\n        warn(\n          \"Failed watching path: \\\"\" + expOrFn + \"\\\" \" +\n          'Watcher only accepts simple dot-delimited paths. ' +\n          'For full control, use a function instead.',\n          vm\n        );\n      }\n    }\n    // lazy为计算属性标志，当watcher为计算watcher时，不会理解执行get方法进行求值\n    this.value = this.lazy\n      ? undefined\n      : this.get();\n  \n}\n```\n方法```get```的定义如下：\n```js\nWatcher.prototype.get = function get () {\n    pushTarget(this);\n    var value;\n    var vm = this.vm;\n    try {\n      value = this.getter.call(vm, vm);\n    } catch (e) {\n     ···\n    } finally {\n      ···\n      // 把Dep.target恢复到上一个状态，依赖收集过程完成\n      popTarget();\n      this.cleanupDeps();\n    }\n    return value\n  };\n```\n`get`方法会执行```this.getter```进行求值，在当前渲染```watcher```的条件下,```getter```会执行视图更新的操作。这一阶段会**重新渲染页面组件**\n```js\nnew Watcher(vm, updateComponent, noop, { before: () => {} }, true);\n\nupdateComponent = function () {\n  vm._update(vm._render(), hydrating);\n};\n```\n\n执行完```getter```方法后，最后一步会进行依赖的清除，也就是```cleanupDeps```的过程。\n\n> 关于依赖清除的作用，我们列举一个场景： 我们经常会使用```v-if```来进行模板的切换，切换过程中会执行不同的模板渲染，如果A模板监听a数据，B模板监听b数据，当渲染模板B时，如果不进行旧依赖的清除，在B模板的场景下，a数据的变化同样会引起依赖的重新渲染更新，这会造成性能的浪费。因此旧依赖的清除在优化阶段是有必要。\n\n```js\n// 依赖清除的过程\n  Watcher.prototype.cleanupDeps = function cleanupDeps () {\n    var i = this.deps.length;\n    while (i--) {\n      var dep = this.deps[i];\n      if (!this.newDepIds.has(dep.id)) {\n        dep.removeSub(this);\n      }\n    }\n    var tmp = this.depIds;\n    this.depIds = this.newDepIds;\n    this.newDepIds = tmp;\n    this.newDepIds.clear();\n    tmp = this.deps;\n    this.deps = this.newDeps;\n    this.newDeps = tmp;\n    this.newDeps.length = 0;\n  };\n```\n\n把上面分析的总结成依赖派发更新的最后两个点\n- **执行```run```操作会执行```getter```方法,也就是重新计算新值，针对渲染```watcher```而言，会重新执行```updateComponent```进行视图更新**\n- **重新计算```getter```后，会进行依赖的清除**\n\n\n## 7.10 computed\n计算属性设计的初衷是用于简单运算的，毕竟在模板中放入太多的逻辑会让模板过重且难以维护。在分析```computed```时，我们依旧遵循依赖收集和派发更新两个过程进行分析。\n### 7.10.1 依赖收集\n`computed`的初始化过程，**会遍历```computed```的每一个属性值，并为每一个属性实例化一个```computed watcher```**，其中```{ lazy: true}```是```computed watcher```的标志，最终会调用```defineComputed```将数据设置为响应式数据，对应源码如下：\n\n```js\nfunction initComputed() {\n  ···\n  for(var key in computed) {\n    watchers[key] = new Watcher(\n        vm,\n        getter || noop,\n        noop,\n        computedWatcherOptions\n      );\n  }\n  if (!(key in vm)) {\n    defineComputed(vm, key, userDef);\n  }\n}\n\n// computed watcher的标志，lazy属性为true\nvar computedWatcherOptions = { lazy: true };\n```\n`defineComputed`的逻辑和分析```data```的逻辑相似，最终调用```Object.defineProperty```进行数据拦截。具体的定义如下：\n```js\nfunction defineComputed (target,key,userDef) {\n  // 非服务端渲染会对getter进行缓存\n  var shouldCache = !isServerRendering();\n  if (typeof userDef === 'function') {\n    // \n    sharedPropertyDefinition.get = shouldCache\n      ? createComputedGetter(key)\n      : createGetterInvoker(userDef);\n    sharedPropertyDefinition.set = noop;\n  } else {\n    sharedPropertyDefinition.get = userDef.get\n      ? shouldCache && userDef.cache !== false\n        ? createComputedGetter(key)\n        : createGetterInvoker(userDef.get)\n      : noop;\n    sharedPropertyDefinition.set = userDef.set || noop;\n  }\n  if (sharedPropertyDefinition.set === noop) {\n    sharedPropertyDefinition.set = function () {\n      warn(\n        (\"Computed property \\\"\" + key + \"\\\" was assigned to but it has no setter.\"),\n        this\n      );\n    };\n  }\n  Object.defineProperty(target, key, sharedPropertyDefinition);\n}\n```\n\n在非服务端渲染的情形，计算属性的计算结果会被缓存，缓存的意义在于，**只有在相关响应式数据发生变化时，```computed```才会重新求值，其余情况多次访问计算属性的值都会返回之前计算的结果，这就是缓存的优化**，```computed```属性有两种写法，一种是函数，另一种是对象，其中对象的写法需要提供```getter```和```setter```方法。\n\n当访问到```computed```属性时，会触发```getter```方法进行依赖收集，看看```createComputedGetter```的实现。\n```js\nfunction createComputedGetter (key) {\n    return function computedGetter () {\n      var watcher = this._computedWatchers && this._computedWatchers[key];\n      if (watcher) {\n        if (watcher.dirty) {\n          watcher.evaluate();\n        }\n        if (Dep.target) {\n          watcher.depend();\n        }\n        return watcher.value\n      }\n    }\n  }\n```\n`createComputedGetter`返回的函数在执行过程中会先拿到属性的```computed watcher```,```dirty```是标志是否已经执行过计算结果，如果执行过则不会执行```watcher.evaluate```重复计算，这也是缓存的原理。\n```js\nWatcher.prototype.evaluate = function evaluate () {\n    // 对于计算属性而言 evaluate的作用是执行计算回调\n    this.value = this.get();\n    this.dirty = false;\n  };\n```\n`get`方法前面介绍过，会调用实例化```watcher```时传递的执行函数，在```computer watcher```的场景下，执行函数是计算属性的计算函数，他可以是一个函数，也可以是对象的```getter```方法。\n\n> 列举一个场景避免和```data```的处理脱节，```computed```在计算阶段，如果访问到```data```数据的属性值，会触发```data```数据的```getter```方法进行依赖收集，根据前面分析，```data```的```Dep```收集器会将当前```watcher```作为依赖进行收集，而这个```watcher```就是```computed watcher```，并且会为当前的```watcher```添加访问的数据```Dep```\n\n\n回到计算执行函数的```this.get()```方法，```getter```执行完成后同样会进行依赖的清除，原理和目的参考```data```阶段的分析。```get```执行完毕后会进入```watcher.depend```进行依赖的收集。收集过程和```data```一致,将当前的```computed watcher```作为依赖收集到数据的依赖收集器```Dep```中。\n\n这就是```computed```依赖收集的完整过程，对比```data```的依赖收集，```computed```会对运算的结果进行缓存，避免重复执行运算过程。\n\n\n### 7.10.2 派发更新\n派发更新的条件是```data```中数据发生改变，所以大部分的逻辑和分析```data```时一致，我们做一个总结。\n- 当计算属性依赖的数据发生更新时，由于数据的```Dep```收集过```computed watch```这个依赖，所以会调用```dep```的```notify```方法，对依赖进行状态更新。\n- 此时```computed watcher```和之前介绍的```watcher```不同，它不会立刻执行依赖的更新操作，而是通过一个```dirty```进行标记。我们再回头看```依赖更新```的代码。\n\n```js\nDep.prototype.notify = function() {\n  ···\n   for (var i = 0, l = subs.length; i < l; i++) {\n      subs[i].update();\n    }\n}\n\nWatcher.prototype.update = function update () {\n  // 计算属性分支  \n  if (this.lazy) {\n    this.dirty = true;\n  } else if (this.sync) {\n    this.run();\n  } else {\n    queueWatcher(this);\n  }\n};\n```\n\n由于```lazy```属性的存在，```update```过程不会执行状态更新的操作，只会将```dirty```标记为```true```。\n- 由于```data```数据拥有渲染```watcher```这个依赖，所以同时会执行```updateComponent```进行视图重新渲染,而```render```过程中会访问到计算属性,此时由于```this.dirty```值为```true```,又会对计算属性重新求值。\n\n\n## 7.11 小结\n我们在上一节的理论基础上深入分析了```Vue```如何利用```data,computed```构建响应式系统。响应式系统的核心是利用```Object.defineProperty```对数据的```getter,setter```进行拦截处理，处理的核心是在访问数据时对数据所在场景的依赖进行收集，在数据发生更改时，通知收集过的依赖进行更新。这一节我们详细的介绍了```data,computed```对响应式的处理，两者处理逻辑存在很大的相似性但却各有的特性。源码中会```computed```的计算结果进行缓存，避免了在多个地方使用时频繁重复计算的问题。由于篇幅有限，对于用户自定义的```watcher```我们会放到下一小节分析。文章还留有一个疑惑，依赖收集时如果遇到的数据是数组时应该怎么处理，这些疑惑都会在之后的文章一一解开。"
  },
  {
    "path": "src/组件基础剖析.md",
    "content": "> 组件是```Vue```的一个重要核心，我们在进行项目工程化时，会将页面的结构组件化。组件化意味着独立和共享,而两个结论并不矛盾，独立的组件开发可以让开发者专注于某个功能项的开发和扩展，而组件的设计理念又使得功能项更加具有复用性，不同的页面可以进行组件功能的共享。对于开发者而言，编写```Vue```组件是掌握```Vue```开发的核心基础，```Vue```官网也花了大量的篇幅介绍了组件的体系和各种使用方法。这一节内容，我们会深入```Vue```组件内部的源码，了解**组件注册的实现思路，并结合上一节介绍的实例挂载分析组件渲染挂载的基本流程，最后我们将分析组件和组件之间是如何建立联系的**。我相信，掌握这些底层的实现思路对于我们今后在解决```vue```组件相关问题上会有明显的帮助。\n\n## 5.1 组件两种注册方式\n熟悉```Vue```开发流程的都知道，```Vue```组件在使用之前需要进行注册，而注册的方式有两种，全局注册和局部注册。在进入源码分析之前，我们先回忆一下两者的用法，以便后续掌握两者的差异。\n\n### 5.1.1 全局注册\n```js\nVue.component('my-test', {\n    template: '<div>{{test}}</div>',\n    data () {\n        return {\n            test: 1212\n        }\n    }\n})\nvar vm = new Vue({\n    el: '#app',\n    template: '<div id=\"app\"><my-test><my-test/></div>'\n})\n```\n**其中组件的全局注册需要在全局实例化Vue前调用**,注册之后可以用在任何新创建的```Vue```实例中调用。\n### 5.1.2 局部注册\n```js\nvar myTest = {\n    template: '<div>{{test}}</div>',\n    data () {\n        return {\n            test: 1212\n        }\n    }\n}\nvar vm = new Vue({\n    el: '#app',\n    component: {\n        myTest\n    }\n})\n```\n当只需要在某个局部用到某个组件时，可以使用局部注册的方式进行组件注册，此时局部注册的组件只能在注册该组件内部使用。\n\n### 5.1.3 注册过程\n在简单回顾组件的两种注册方式后，我们来看注册过程到底发生了什么，我们以全局组件注册为例。它通过```Vue.component(name, {...})```进行组件注册，```Vue.component```是在```Vue```源码引入阶段定义的静态方法。\n```js\n// 初始化全局api\ninitAssetRegisters(Vue);\nvar ASSET_TYPES = [\n    'component',\n    'directive',\n    'filter'\n];\nfunction initAssetRegisters(Vue){\n    // 定义ASSET_TYPES中每个属性的方法，其中包括component\n    ASSET_TYPES.forEach(function (type) {\n    // type: component,directive,filter\n      Vue[type] = function (id,definition) {\n          if (!definition) {\n            // 直接返回注册组件的构造函数\n            return this.options[type + 's'][id]\n          }\n          ...\n          if (type === 'component') {\n            // 验证component组件名字是否合法\n            validateComponentName(id);\n          }\n          if (type === 'component' && isPlainObject(definition)) {\n            // 组件名称设置\n            definition.name = definition.name || id;\n            // Vue.extend() 创建子组件，返回子类构造器\n            definition = this.options._base.extend(definition);\n          }\n          // 为Vue.options 上的component属性添加将子类构造器\n          this.options[type + 's'][id] = definition;\n          return definition\n        }\n    });\n}\n```\n\n`Vue.components`有两个参数，一个是需要注册组件的组件名，另一个是组件选项，如果第二个参数没有传递，则会直接返回注册过的组件选项。否则意味着需要对该组件进行注册，注册过程先会对组件名的合法性进行检测，要求组件名不允许出现非法的标签，包括```Vue```内置的组件名，如```slot, component```等。\n```js\nfunction validateComponentName(name) {\n    if (!new RegExp((\"^[a-zA-Z][\\\\-\\\\.0-9_\" + (unicodeRegExp.source) + \"]*$\")).test(name)) {\n      // 正则判断检测是否为非法的标签\n      warn(\n        'Invalid component name: \"' + name + '\". Component names ' +\n        'should conform to valid custom element name in html5 specification.'\n      );\n    }\n    // 不能使用Vue自身自定义的组件名，如slot, component,不能使用html的保留标签，如 h1, svg等\n    if (isBuiltInTag(name) || config.isReservedTag(name)) {\n      warn(\n        'Do not use built-in or reserved HTML elements as component ' +\n        'id: ' + name\n      );\n    }\n  }\n```\n在经过组件名的合法性检测后，会调用```extend```方法为组件创建一个子类构造器，此时的```this.options._base```代表的就是```Vue```构造器。```extend```方法的定义在介绍选项合并章节有重点介绍过，它会**基于父类去创建一个子类**，此时的父类是```Vue```，并且创建过程子类会继承父类的方法，并会和父类的选项进行合并，最终返回一个子类构造器。\n\n代码处还有一个逻辑，```Vue.component()```默认会把第一个参数作为组件名称，但是如果组件选项有```name```属性时，```name```属性值会将组件名覆盖。\n\n\n**总结起来，全局注册组件就是```Vue```实例化前创建一个基于```Vue```的子类构造器，并将组件的信息加载到实例```options.components```对象中。**\n\n\n**接下来自然而然会想到一个问题，局部注册和全局注册在实现上的区别体现在哪里？**我们不急着分析局部组件的注册流程，先以全局注册的组件为基础，看看作为组件，它的挂载流程有什么不同。\n\n\n## 5.2 组件Vnode创建\n上一节内容我们介绍了```Vue```如何将一个模板，通过```render```函数的转换，最终生成一个```Vnode tree```的，在不包含组件的情况下，```_render```函数的最后一步是直接调用```new Vnode```去创建一个完整的```Vnode tree```。然而有一大部分的分支我们并没有分析，那就是遇到组件占位符的场景。执行阶段如果遇到组件，处理过程要比想像中复杂得多，我们通过一张流程图展开分析。\n\n### 5.2.1 Vnode创建流程图\n\n![](./img/5.1.png)\n\n### 5.2.2 具体流程分析\n我们结合实际的例子对照着流程图分析一下这个过程：\n\n- 场景\n```js\nVue.component('test', {\n  template: '<span></span>'\n})\nvar vm = new Vue({\n  el: '#app',\n  template: '<div><test></test></div>'\n})\n```\n- 父```render```函数\n```js\nfunction() {\n  with(this){return _c('div',[_c('test')],1)}\n}\n```\n\n\n-  `Vue`根实例初始化会执行 ```vm.$mount(vm.$options.el)```实例挂载的过程，按照之前的逻辑，完整流程会经历```render```函数生成```Vnode```,以及```Vnode```生成真实```DOM```的过程。\n- `render`函数生成```Vnode```过程中，子会优先父执行生成```Vnode```过程,也就是```_c('test')```函数会先被执行。```'test'```会先判断是普通的```html```标签还是组件的占位符。\n- 如果为一般标签，会执行```new Vnode```过程，这也是上一章节我们分析的过程；如果是组件的占位符，则会在判断组件已经被注册过的前提下进入```createComponent```创建子组件```Vnode```的过程。\n- `createComponent`是创建组件```Vnode```的过程，创建过程会再次合并选项配置，并安装组件相关的内部钩子(后面文章会再次提到内部钩子的作用)，最后通过```new Vnode()```生成以```vue-component```开头的```Virtual DOM```\n- `render`函数执行过程也是一个循环递归调用创建```Vnode```的过程，执行3，4步之后，完整的生成了一个包含各个子组件的```Vnode tree```\n\n\n`_createElement`函数的实现之前章节分析过一部分，我们重点看看组件相关的操作。\n\n```js\n// 内部执行将render函数转化为Vnode的函数\nfunction _createElement(context,tag,data,children,normalizationType) {\n  ···\n  if (typeof tag === 'string') {\n    // 子节点的标签为普通的html标签，直接创建Vnode\n    if (config.isReservedTag(tag)) {\n      vnode = new VNode(\n        config.parsePlatformTagName(tag), data, children,\n        undefined, undefined, context\n      );\n    // 子节点标签为注册过的组件标签名，则子组件Vnode的创建过程\n    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {\n      // 创建子组件Vnode\n      vnode = createComponent(Ctor, data, context, children, tag);\n    }\n  }\n}\n```\n`config.isReservedTag(tag)`用来判断标签是否为普通的```html```标签，如果是普通节点会直接创建```Vnode```节点，如果不是，则需要判断这个占位符组件是否已经注册到，我们可以通过```context.$options.components[组件名]```拿到注册后的组件选项。如何判断组件是否已经全局注册，看看```resolveAsset```的实现。\n\n```js\n// 需要明确组件是否已经被注册\n  function resolveAsset (options,type,id,warnMissing) {\n    // 标签为字符串\n    if (typeof id !== 'string') {\n      return\n    }\n    // 这里是 options.component\n    var assets = options[type];\n    // 这里的分支分别支持大小写，驼峰的命名规范\n    if (hasOwn(assets, id)) { return assets[id] }\n    var camelizedId = camelize(id);\n    if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }\n    var PascalCaseId = capitalize(camelizedId);\n    if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }\n    // fallback to prototype chain\n    var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];\n    if (warnMissing && !res) {\n      warn(\n        'Failed to resolve ' + type.slice(0, -1) + ': ' + id,\n        options\n      );\n    }\n    // 最终返回子类的构造器\n    return res\n  }\n```\n\n拿到注册过的子类构造器后，调用```createComponent```方法创建子组件```Vnode```\n\n```js\n // 创建子组件过程\n  function createComponent (\n    Ctor, // 子类构造器\n    data,\n    context, // vm实例\n    children, // 子节点\n    tag // 子组件占位符\n  ) {\n    ···\n    // Vue.options里的_base属性存储Vue构造器\n    var baseCtor = context.$options._base;\n\n    // 针对局部组件注册场景\n    if (isObject(Ctor)) {\n      Ctor = baseCtor.extend(Ctor);\n    }\n    data = data || {};\n    // 构造器配置合并\n    resolveConstructorOptions(Ctor);\n    // 挂载组件钩子\n    installComponentHooks(data);\n\n    // return a placeholder vnode\n    var name = Ctor.options.name || tag;\n    // 创建子组件vnode，名称以 vue-component- 开头\n    var vnode = new VNode((\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),data, undefined, undefined, undefined, context,{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },asyncFactory);\n\n    return vnode\n  }\n```\n这里将大部分的代码都拿掉了，只留下创建```Vnode```相关的代码，最终会通过```new Vue```实例化一个名称以```vue-component-```开头的```Vnode```节点。其中两个关键的步骤是配置合并和安装组件钩子函数，选项合并的内容可以查看这个系列的前两节，这里看看```installComponentHooks```安装组件钩子函数时做了哪些操作。\n```js\n  // 组件内部自带钩子\n var componentVNodeHooks = {\n    init: function init (vnode, hydrating) {\n    },\n    prepatch: function prepatch (oldVnode, vnode) {\n    },\n    insert: function insert (vnode) {\n    },\n    destroy: function destroy (vnode) {\n    }\n  };\nvar hooksToMerge = Object.keys(componentVNodeHooks);\n// 将componentVNodeHooks 钩子函数合并到组件data.hook中 \nfunction installComponentHooks (data) {\n    var hooks = data.hook || (data.hook = {});\n    for (var i = 0; i < hooksToMerge.length; i++) {\n      var key = hooksToMerge[i];\n      var existing = hooks[key];\n      var toMerge = componentVNodeHooks[key];\n      // 如果钩子函数存在，则执行mergeHook$1方法合并\n      if (existing !== toMerge && !(existing && existing._merged)) {\n        hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;\n      }\n    }\n  }\nfunction mergeHook$1 (f1, f2) {\n  // 返回一个依次执行f1,f2的函数\n    var merged = function (a, b) {\n      f1(a, b);\n      f2(a, b);\n    };\n    merged._merged = true;\n    return merged\n  }\n```\n组件默认自带的这几个钩子函数会在后续```patch```过程的不同阶段执行，这部分内容不在本节的讨论范围。\n\n\n### 5.2.3 局部注册和全局注册的区别\n在说到全局注册和局部注册的用法时留下了一个问题，局部注册和全局注册两者的区别在哪里。其实局部注册的原理同样简单，我们使用局部注册组件时会通过在父组件选项配置中的```components```添加子组件的对象配置，这和全局注册后在```Vue```的```options.component```添加子组件构造器的结果很相似。区别在于：\n\n**1.局部注册添加的对象配置是在某个组件下，而全局注册添加的子组件是在根实例下。**\n\n**2.局部注册添加的是一个子组件的配置对象，而全局注册添加的是一个子类构造器。**\n\n因此局部注册中缺少了一步构建子类构造器的过程，这个过程放在哪里进行呢？ 回到```createComponent```的源码,源码中根据选项是对象还是函数来区分局部和全局注册组件，**如果选项的值是对象，则该组件是局部注册的组件，此时在创建子```Vnode```时会调用 父类的```extend```方法去创建一个子类构造器。**\n```js\nfunction createComponent (...) {\n  ...\n  var baseCtor = context.$options._base;\n\n  // 针对局部组件注册场景\n  if (isObject(Ctor)) {\n      Ctor = baseCtor.extend(Ctor);\n  }\n}\n\n```\n\n## 5.3 组件Vnode渲染真实DOM\n根据前面的分析，不管是全局注册的组件还是局部注册的组件，组件并没有进行实例化，那么组件实例化的过程发生在哪个阶段呢？我们接着看```Vnode tree```渲染真实```DOM```的过程。\n\n### 5.3.1 真实节点渲染流程图\n\n![](./img/5.2.png)\n\n\n### 5.3.2 具体流程分析\n1. 经过```vm._render()```生成完整的```Virtual Dom```树后，紧接着执行```Vnode```渲染真实```DOM```的过程,这个过程是```vm.update()```方法的执行，而其核心是```vm.__patch__```。\n2. ```vm.__patch__```内部会通过 ```createElm```去创建真实的```DOM```元素，期间遇到子```Vnode```会递归调用```createElm```方法。\n3. 递归调用过程中，判断该节点类型是否为组件类型是通过```createComponent```方法判断的，该方法和渲染```Vnode```阶段的方法```createComponent```不同，他会调用子组件的```init```初始化钩子函数，并完成组件的```DOM```插入。\n4. ```init```初始化钩子函数的核心是```new```实例化这个子组件并将子组件进行挂载，实例化子组件的过程又回到合并配置，初始化生命周期，初始化事件中心，初始化渲染的过程。实例挂载又会执行```$mount```过程。\n5. 完成所有子组件的实例化和节点挂载后，最后才回到根节点的挂载。\n\n\n`__patch__`核心代码是通过```createElm```创建真实节点，当创建过程中遇到子```vnode```时，会调用```createChildren```,```createChildren```的目的是对子```vnode```递归调用```createElm```创建子组件节点。\n```js\n// 创建真实dom\nfunction createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {\n  ···\n  // 递归创建子组件真实节点,直到完成所有子组件的渲染才进行根节点的真实节点插入\n  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {\n    return\n  }\n  ···\n  var children = vnode.children;\n  // \n  createChildren(vnode, children, insertedVnodeQueue);\n  ···\n  insert(parentElm, vnode.elm, refElm);\n}\nfunction createChildren(vnode, children, insertedVnodeQueue) {\n  for (var i = 0; i < children.length; ++i) {\n    // 遍历子节点，递归调用创建真实dom节点的方法 - createElm\n    createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);\n  }\n}\n```\n`createComponent`方法会对子组件```Vnode```进行处理中，还记得在```Vnode```生成阶段为子```Vnode```安装了一系列的钩子函数吗，在这个步骤我们可以通过是否拥有这些定义好的钩子来判断是否是已经注册过的子组件，如果条件满足，则执行组件的```init```钩子。\n\n`init`钩子做的事情只有两个，**实例化组件构造器，执行子组件的挂载流程。**(```keep-alive```分支看具体的文章分析)\n\n```js\nfunction createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {\n  var i = vnode.data;\n  // 是否有钩子函数可以作为判断是否为组件的唯一条件\n  if (isDef(i = i.hook) && isDef(i = i.init)) {\n    // 执行init钩子函数\n    i(vnode, false /* hydrating */);\n  }\n  ···\n}\nvar componentVNodeHooks = {\n  // 忽略keepAlive过程\n  // 实例化\n  var child = vnode.componentInstance = createComponentInstanceForVnode(vnode,activeInstance);\n  // 挂载\n  child.$mount(hydrating ? vnode.elm : undefined, hydrating);\n}\nfunction createComponentInstanceForVnode(vnode, parent) {\n  ···\n  // 实例化Vue子组件实例\n  return new vnode.componentOptions.Ctor(options)\n}\n\n```\n显然```Vnode```生成真实```DOM```的过程也是一个不断递归创建子节点的过程，```patch```过程如果遇到子```Vnode```,会优先实例化子组件，并且执行子组件的挂载流程，而挂载流程又会回到```_render,_update```的过程。在所有的子```Vnode```递归挂载后，最终才会真正挂载根节点。\n\n## 5.4 建立组件联系\n\n日常开发中，我们可以通过```vm.$parent```拿到父实例，也可以在父实例中通过```vm.$children```拿到实例中的子组件。显然，```Vue```在组件和组件之间建立了一层关联。接下来的内容，我们将探索如何建立组件之间的联系。\n\n不管是父实例还是子实例，在初始化实例阶段有一个```initLifecycle```的过程。这个过程会**把当前实例添加到父实例的```$children```属性中，并设置自身的```$parent```属性指向父实例。**举一个具体的应用场景：\n```js\n<div id=\"app\">\n    <component-a></component-a>\n</div>\nVue.component('component-a', {\n    template: '<div>a</div>'\n})\nvar vm = new Vue({ el: '#app'})\nconsole.log(vm) // 将实例对象输出\n``` \n由于```vue```实例向上没有父实例，所以```vm.$parent```为```undefined```，```vm```的```$children```属性指向子组件```componentA``` 的实例。\n\n![](./img/5.3.png)\n\n子组件```componentA```的 ```$parent```属性指向它的父级```vm```实例，它的```$children```属性指向为空\n\n![](./img/5.4.png)\n\n\n源码解析如下: \n```js\nfunction initLifecycle (vm) {\n    var options = vm.$options;\n    // 子组件注册时，会把父组件的实例挂载到自身选项的parent上\n    var parent = options.parent;\n    // 如果是子组件，并且该组件不是抽象组件时，将该组件的实例添加到父组件的$parent属性上，如果父组件是抽象组件，则一直往上层寻找，直到该父级组件不是抽象组件，并将，将该组件的实例添加到父组件的$parent属性\n    if (parent && !options.abstract) {\n        while (parent.$options.abstract && parent.$parent) {\n        parent = parent.$parent;\n        }\n        parent.$children.push(vm);\n    }\n    // 将自身的$parent属性指向父实例。\n    vm.$parent = parent;\n    vm.$root = parent ? parent.$root : vm;\n\n    vm.$children = [];\n    vm.$refs = {};\n\n    vm._watcher = null;\n    vm._inactive = null;\n    vm._directInactive = false;\n    // 该实例是否挂载\n    vm._isMounted = false;\n    // 该实例是否被销毁\n    vm._isDestroyed = false;\n    // 该实例是否正在被销毁\n    vm._isBeingDestroyed = false;\n}\n\n```\n最后简单讲讲抽象组件，在```vue```中有很多内置的抽象组件，例如```<keep-alive></keep-alive>,<slot><slot>```等，这些抽象组件并不会出现在子父级的路径上，并且它们也不会参与```DOM```的渲染。\n\n\n## 5.5 小结\n这一小节，结合了实际的例子分析了组件注册流程到组件挂载渲染流程，```Vue```中我们可以定义全局的组件，也可以定义局部的组件，全局组件需要进行全局注册，核心方法是```Vue.component```,他需要在根组件实例化前进行声明注册，原因是我们需要在实例化前拿到组件的配置信息并合并到```options.components```选项中。注册的本质是调用```extend```创建一个子类构造器，全局和局部的不同是局部创建子类构造器是发生在创建子组件```Vnode```阶段。而创建子```Vnode```阶段最关键的一步是定义了很多内部使用的钩子。有了一个完整的```Vnode tree```接下来会进入真正```DOM```的生成，在这个阶段如果遇到子组件```Vnode```会进行子构造器的实例化，并完成子组件的挂载。递归完成子组件的挂载后，最终才又回到根组件的挂载。\n  有了组件的基本知识，下一节我们重点分析一下组件的进阶用法。\n"
  },
  {
    "path": "src/组件高级用法.md",
    "content": "> 我们知道，组件是```Vue```体系的核心，熟练使用组件是掌握```Vue```进行开发的基础。上一节中，我们深入了解了```Vue```组件注册到使用渲染的完整流程。这一节我们会在上一节的基础上介绍组件的两个高级用法：异步组件和函数式组件。\n\n## 6.1 异步组件\n### 6.1.1 使用场景\n`Vue`作为单页面应用遇到最棘手的问题是首屏加载时间的问题，单页面应用会把页面脚本打包成一个文件，这个文件包含着所有业务和非业务的代码，而脚本文件过大也是造成首页渲染速度缓慢的原因。因此作为首屏性能优化的课题，最常用的处理方法是对文件的拆分和代码的分离。按需加载的概念也是在这个前提下引入的。我们往往会把一些非首屏的组件设计成异步组件，部分不影响初次视觉体验的组件也可以设计为异步组件。这个思想就是**按需加载**。通俗点理解，按需加载的思想让应用在需要使用某个组件时才去请求加载组件代码。我们借助```webpack```打包后的结果会更加直观。\n\n\n![](./img/6.1.png)\n\n![](./img/6.2.png)\n`webpack`遇到异步组件，会将其从主脚本中分离，减少脚本体积，加快首屏加载时间。当遇到场景需要使用该组件时，才会去加载组件脚本。\n\n\n### 6.1.2 工厂函数\n\n`Vue`中允许用户通过工厂函数的形式定义组件，这个工厂函数会异步解析组件定义，组件需要渲染的时候才会触发该工厂函数，加载结果会进行缓存，以供下一次调用组件时使用。\n具体使用：\n```js\n// 全局注册：\nVue.component('asyncComponent', function(resolve, reject) {\n  require(['./test.vue'], resolve)\n})\n// 局部注册：\nvar vm = new Vue({\n  el: '#app',\n  template: '<div id=\"app\"><asyncComponent></asyncComponent></div>',\n  components: {\n    asyncComponent: (resolve, reject) => require(['./test.vue'], resolve),\n    // 另外写法\n    asyncComponent: () => import('./test.vue'),\n  }\n})\n```\n\n\n\n\n### 6.1.3 流程分析\n\n有了上一节组件注册的基础，我们来分析异步组件的实现逻辑。简单回忆一下上一节的流程，实例的挂载流程分为根据渲染函数创建```Vnode```和根据```Vnode```产生真实节点的过程。期间创建```Vnode```过程，如果遇到子的占位符节点会调用```creatComponent```,这里会为子组件做选项合并和钩子挂载的操作，并创建一个以```vue-component-```为标记的子```Vnode```,而异步组件的处理逻辑也是在这个阶段处理。\n\n```js\n// 创建子组件过程\n  function createComponent (\n    Ctor, // 子类构造器\n    data,\n    context, // vm实例\n    children, // 子节点\n    tag // 子组件占位符\n  ) {\n    ···\n    // 针对局部注册组件创建子类构造器\n    if (isObject(Ctor)) {\n      Ctor = baseCtor.extend(Ctor);\n    }\n    // 异步组件分支\n    var asyncFactory;\n    if (isUndef(Ctor.cid)) {\n      // 异步工厂函数\n      asyncFactory = Ctor;\n      // 创建异步组件函数\n      Ctor = resolveAsyncComponent(asyncFactory, baseCtor);\n      if (Ctor === undefined) {\n        return createAsyncPlaceholder(\n          asyncFactory,\n          data,\n          context,\n          children,\n          tag\n        )\n      }\n    }\n    ···\n    // 创建子组件vnode\n    var vnode = new VNode(\n      (\"vue-component-\" + (Ctor.cid) + (name ? (\"-\" + name) : '')),\n      data, undefined, undefined, undefined, context,\n      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },\n      asyncFactory\n    );\n\n    return vnode\n  }\n```\n**工厂函数的用法使得```Vue.component(name, options)```的第二个参数不是一个对象，因此不论是全局注册还是局部注册，都不会执行```Vue.extend```生成一个子组件的构造器，**所以```Ctor.cid```不会存在，代码会进入异步组件的分支。\n\n异步组件分支的核心是```resolveAsyncComponent```,它的处理逻辑分支众多，我们先关心工厂函数处理部分。\n```js\nfunction resolveAsyncComponent (\n    factory,\n    baseCtor\n  ) {\n    if (!isDef(factory.owners)) {\n\n      // 异步请求成功处理\n      var resolve = function() {}\n      // 异步请求失败处理\n      var reject = function() {}\n\n      // 创建子组件时会先执行工厂函数，并将resolve和reject传入\n      var res = factory(resolve, reject);\n\n      // resolved 同步返回\n      return factory.loading\n        ? factory.loadingComp\n        : factory.resolved\n    }\n  }\n```\n如果经常使用```promise```进行开发，我们很容易发现，这部分代码像极了```promsie```原理内部的实现，针对异步组件工厂函数的写法，大致可以总结出以下三个步骤：\n1. 定义异步请求成功的函数处理，定义异步请求失败的函数处理；\n2. 执行组件定义的工厂函数；\n3. 同步返回请求成功的函数处理。\n\n`resolve, reject`的实现，都是```once```方法执行的结果，所以我们先关注一下高级函数```once```的原理。**为了防止当多个地方调用异步组件时，```resolve,reject```不会重复执行，```once```函数保证了函数在代码只执行一次。也就是说，```once```缓存了已经请求过的异步组件**\n\n```js\n// once函数保证了这个调用函数只在系统中调用一次\nfunction once (fn) {\n  // 利用闭包特性将called作为标志位\n  var called = false;\n  return function () {\n    // 调用过则不再调用\n    if (!called) {\n      called = true;\n      fn.apply(this, arguments);\n    }\n  }\n}\n```\n\n成功```resolve```和失败```reject```的详细处理逻辑如下： \n```js\n// 成功处理\nvar resolve = once(function (res) {\n  // 转成组件构造器，并将其缓存到resolved属性中。\n  factory.resolved = ensureCtor(res, baseCtor);\n  if (!sync) {\n    //强制更新渲染视图\n    forceRender(true);\n  } else {\n    owners.length = 0;\n  }\n});\n// 失败处理\nvar reject = once(function (reason) {\n  warn(\n    \"Failed to resolve async component: \" + (String(factory)) +\n    (reason ? (\"\\nReason: \" + reason) : '')\n  );\n  if (isDef(factory.errorComp)) {\n    factory.error = true;\n    forceRender(true);\n  }\n});\n```\n异步组件加载完毕，会调用```resolve```定义的方法，方法会通过```ensureCtor```将加载完成的组件转换为组件构造器，并存储在```resolved```属性中，其中 ```ensureCtor```的定义为：\n```js\nfunction ensureCtor (comp, base) {\n    if (comp.__esModule ||(hasSymbol && comp[Symbol.toStringTag] === 'Module')) {\n      comp = comp.default;\n    }\n    // comp结果为对象时，调用extend方法创建一个子类构造器\n    return isObject(comp)\n      ? base.extend(comp)\n      : comp\n  }\n```\n组件构造器创建完毕，会进行一次视图的重新渲染，**由于```Vue```是数据驱动视图渲染的，而组件在加载到完毕的过程中，并没有数据发生变化，因此需要手动强制更新视图。**```forceRender```函数的内部会拿到每个调用异步组件的实例，执行原型上的```$forceUpdate```方法，这部分的知识等到响应式系统时介绍。\n\n异步组件加载失败后，会调用```reject```定义的方法，方法会提示并标记错误，最后同样会强制更新视图。\n\n\n回到异步组件创建的流程，执行异步过程会同步为加载中的异步组件创建一个注释节点```Vnode```\n```js\n  function createComponent (){\n    ···\n    // 创建异步组件函数\n    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);\n    if (Ctor === undefined) {\n      // 创建注释节点\n      return createAsyncPlaceholder(asyncFactory,data,context,children,tag)\n    }\n  }\n```\n`createAsyncPlaceholder`的定义也很简单,其中```createEmptyVNode```之前有介绍过，是创建一个注释节点```vnode```，而```asyncFactory,asyncMeta```都是用来标注该节点为异步组件的临时节点和相关属性。\n```js\n// 创建注释Vnode\nfunction createAsyncPlaceholder (factory,data,context,children,tag) {\n  var node = createEmptyVNode();\n  node.asyncFactory = factory;\n  node.asyncMeta = { data: data, context: context, children: children, tag: tag };\n  return node\n}\n```\n执行```forceRender```触发组件的重新渲染过程时，又会再次调用```resolveAsyncComponent```,这时返回值```Ctor```不再为 ```undefined```了，因此会正常走组件的```render,patch```过程。这时，旧的注释节点也会被取代。\n\n### 6.1.4 Promise异步组件\n异步组件的第二种写法是在工厂函数中返回一个```promise```对象，我们知道```import```是```es6```引入模块加载的用法，但是```import```是一个静态加载的方法，它会优先模块内的其他语句执行。因此引入了```import()```,```import()```是一个运行时加载模块的方法，可以用来类比```require()```方法，区别在于前者是一个异步方法，后者是同步的，且```import()```会返回一个```promise```对象。\n\n\n具体用法：\n```js\nVue.component('asyncComponent', () => import('./test.vue'))\n```\n源码依然走着异步组件处理分支，并且大部分的处理过程还是工厂函数的逻辑处理，区别在于执行异步函数后会返回一个```promise```对象，成功加载则执行```resolve```,失败加载则执行```reject```.\n```js\nvar res = factory(resolve, reject);\n// res是返回的promise\nif (isObject(res)) {\n  if (isPromise(res)) {\n    if (isUndef(factory.resolved)) {\n      // 核心处理\n      res.then(resolve, reject);\n    }\n  }\n}\n```\n其中```promise```对象的判断最简单的是判断是否有```then```和```catch```方法：\n```js\n // 判断promise对象的方法\n  function isPromise (val) {\n    return (isDef(val) && typeof val.then === 'function' && typeof val.catch === 'function')\n  }\n```\n\n### 6.1.5 高级异步组件\n为了在操作上更加灵活，比如使用```loading```组件处理组件加载时间过长的等待问题，使用```error```组件处理加载组件失败的错误提示等，```Vue```在2.3.0+版本新增了返回对象形式的异步组件格式，对象中可以定义需要加载的组件```component```,加载中显示的组件```loading```,加载失败的组件```error```,以及各种延时超时设置，源码同样进入异步组件分支。\n```js\nVue.component('asyncComponent', () => ({\n  // 需要加载的组件 (应该是一个 `Promise` 对象)\n  component: import('./MyComponent.vue'),\n  // 异步组件加载时使用的组件\n  loading: LoadingComponent,\n  // 加载失败时使用的组件\n  error: ErrorComponent,\n  // 展示加载时组件的延时时间。默认值是 200 (毫秒)\n  delay: 200,\n  // 如果提供了超时时间且组件加载也超时了，\n  // 则使用加载失败时使用的组件。默认值是：`Infinity`\n  timeout: 3000\n}))\n```\n异步组件函数执行后返回一个对象，并且对象的```component```执行会返回一个```promise```对象，因此进入高级异步组件处理分支。\n```js\nif (isObject(res)) {\n  if (isPromise(res)) {}\n  // 返回对象，且res.component返回一个promise对象，进入分支\n  // 高级异步组件处理分支\n  else if (isPromise(res.component)) {\n    // 和promise异步组件处理方式相同\n    res.component.then(resolve, reject);\n    ···\n  }\n}\n```\n异步组件会等待响应成功失败的结果，与此同时，代码继续同步执行。高级选项设置中如果设置了```error```和```loading```组件，会同时创建两个子类的构造器,\n```js\nif (isDef(res.error)) {\n  // 异步错误时组件的处理，创建错误组件的子类构造器，并赋值给errorComp\n  factory.errorComp = ensureCtor(res.error, baseCtor);\n}\n\nif (isDef(res.loading)) {\n  // 异步加载时组件的处理，创建错误组件的子类构造器，并赋值给errorComp\n  factory.loadingComp = ensureCtor(res.loading, baseCtor);\n}\n```\n如果存在```delay```属性,则通过```settimeout```设置```loading```组件显示的延迟时间。```factory.loading```属性用来标注是否是显示```loading```组件。\n```js\nif (res.delay === 0) {\n  factory.loading = true;\n} else {\n  // 超过时间会成功加载，则执行失败结果\n  setTimeout(function () {\n    if (isUndef(factory.resolved) && isUndef(factory.error)) {\n      factory.loading = true;\n      forceRender(false);\n    }\n  }, res.delay || 200);\n}\n```\n如果在```timeout```时间内，异步组件还未执行```resolve```的成功结果，即```resolve```没有赋值,则进行```reject```失败处理。\n\n接下来依然是渲染注释节点或者渲染```loading```组件，等待异步处理结果，根据处理结果重新渲染视图节点，相似过程不再阐述。\n\n### 6.1.6 wepack异步组件用法\n`webpack`作为```Vue```应用构建工具的标配，我们需要知道```Vue```如何结合```webpack ```进行异步组件的代码分离，并且需要关注分离后的文件名，这个名字在```webpack```中称为```chunkName```。```webpack```为异步组件的加载提供了两种写法。\n- `require.ensure`:它是```webpack```传统提供给异步组件的写法，在编译时，```webpack```会静态地解析代码中的 ```require.ensure()```，同时将模块添加到一个分开的 ```chunk``` 中，其中函数的第三个参数为分离代码块的名字。修改后的代码写法如下：\n\n```js\nVue.component('asyncComponent', function (resolve, reject) {\n   require.ensure([], function () {\n     resolve(require('./test.vue'));\n   }, 'asyncComponent'); // asyncComponent为chunkname\n})\n``` \n\n- `import(/* webpackChunkName: \"asyncComponent\" */, component)`: 有了```es6```,```import```的写法是现今官方最推荐的做法，其中通过注释```webpackChunkName```来指定分离后组件模块的命名。修改后的写法如下：\n\n```js\nVue.component('asyncComponent', () => import(/* webpackChunkName: \"asyncComponent\" */, './test.vue'))\n```\n\n至此，我们已经掌握了所有异步组件的写法，并深入了解了其内部的实现细节。我相信全面的掌握异步组件对今后单页面性能优化方面会起到积极的指导作用。\n\n## 6.2 函数式组件\n`Vue`提供了一种可以让组件变为无状态、无实例的函数化组件。从原理上说，一般子组件都会经过实例化的过程，而单纯的函数组件并没有这个过程，它可以简单理解为一个中间层，只处理数据，不创建实例，也是由于这个行为，它的渲染开销会低很多。实际的应用场景是，当我们需要在多个组件中选择一个来代为渲染，或者在将```children,props,data```等数据传递给子组件前进行数据处理时，我们都可以用函数式组件来完成，它本质上也是对组件的一个外部包装。\n\n### 6.2.1 使用场景\n\n- 定义两个组件对象，```test1，test2```\n```js\nvar test1 = {\n  props: ['msg'],\n  render: function (createElement, context) {\n    return createElement('h1', this.msg)\n  }\n}\nvar test2 = {\n  props: ['msg'],\n  render: function (createElement, context) {\n    return createElement('h2', this.msg)\n  }\n}\n```\n- 定义一个函数式组件，它会根据计算结果选择其中一个组件进行选项\n```js\nVue.component('test3', {\n  // 函数式组件的标志 functional设置为true\n  functional: true,\n  props: ['msg'],\n  render: function (createElement, context) {\n    var get = function() {\n      return test1\n    }\n    return createElement(get(), context)\n  }\n})\n```\n- 函数式组件的使用\n```js\n<test3 :msg=\"msg\" id=\"test\">\n</test3>\nnew Vue({\n  el: '#app',\n  data: {\n    msg: 'test'\n  }\n})\n```\n- 最终渲染的结果为：\n```js\n<h2>test</h2>\n```\n\n### 6.2.2 源码分析\n函数式组件会在组件的对象定义中，将```functional```属性设置为```true```，这个属性是区别普通组件和函数式组件的关键。同样的在遇到子组件占位符时，会进入```createComponent```进行子组件```Vnode```的创建。**由于```functional```属性的存在，代码会进入函数式组件的分支中，并返回```createFunctionalComponent```调用的结果。**注意，执行完```createFunctionalComponent```后，后续创建子```Vnode```的逻辑不会执行，这也是之后在创建真实节点过程中不会有子```Vnode```去实例化子组件的原因。(无实例)\n```js\nfunction createComponent(){\n  ···\n  if (isTrue(Ctor.options.functional)) {\n    return createFunctionalComponent(Ctor, propsData, data, context, children)\n  }\n}\n```\n`createFunctionalComponent`方法会对传入的数据进行检测和合并，实例化```FunctionalRenderContext```，最终调用函数式组件自定义的```render```方法执行渲染过程。\n```js\nfunction createFunctionalComponent(\n  Ctor, // 函数式组件构造器\n  propsData, // 传入组件的props\n  data, // 占位符组件传入的attr属性\n  context, // vue实例\n  children// 子节点\n){\n  // 数据检测合并\n  var options = Ctor.options;\n  var props = {};\n  var propOptions = options.props;\n  if (isDef(propOptions)) {\n    for (var key in propOptions) {\n      props[key] = validateProp(key, propOptions, propsData || emptyObject);\n    }\n  } else {\n    // 合并attrs\n    if (isDef(data.attrs)) { mergeProps(props, data.attrs); }\n    // 合并props\n    if (isDef(data.props)) { mergeProps(props, data.props); }\n  }\n  var renderContext = new FunctionalRenderContext(data,props,children,contextVm,Ctor);\n  // 调用函数式组件中自定的render函数\n  var vnode = options.render.call(null, renderContext._c, renderContext)\n}\n```\n而```FunctionalRenderContext```这个类最终的目的是定义一个和真实组件渲染不同的```render```方法。\n```js\nfunction FunctionalRenderContext() {\n  // 省略其他逻辑\n  this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };\n}\n```\n执行```render```函数的过程，又会递归调用```createElement```的方法，这时的组件已经是真实的组件，开始执行正常的组件挂载流程。\n\n问题：为什么函数式组件需要定义一个不同的```createElement```方法？- 函数式组件```createElement```和以往唯一的不同是，最后一个参数的不同，之前章节有说到，```createElement```会根据最后一个参数决定是否对子```Vnode```进行拍平，一般情况下，```children```编译生成结果都是```Vnode```类型，只有函数式组件比较特殊，它可以返回一个数组，这时候拍平就是有必要的。我们看下面的例子：\n```js\nVue.component('test', {  \n  functional: true,  \n  render: function (createElement, context) {  \n    return context.slots().default  \n  }  \n}) \n\n<test> \n     <p>slot1</p> \n     <p>slot</p> \n</test>\n```\n此时函数式组件```test```的```render```函数返回的是两个```slot```的```Vnode```，它是以数组的形式存在的,这就是需要拍平的场景。\n\n简单总结一下函数式组件，从源码中可以看出，函数式组件并不会像普通组件那样有实例化组件的过程，因此包括组件的生命周期，组件的数据管理这些过程都没有，它只会原封不动的接收传递给组件的数据做处理，并渲染需要的内容。因此作为纯粹的函数可以也大大降低渲染的开销。\n\n\n## 6.3 小结\n这一小节在组件基础之上介绍了两个进阶的用法，异步组件和函数式组件。它们都是为了解决某些类型场景引入的高级组件用法。其中异步组件是首屏性能优化的一个解决方案，并且```Vue```提供了多达三种的使用方法，高级配置的用法更让异步组件的使用更加灵活。当然大部分情况下，我们会结合```webpack```进行使用。另外，函数式组件在多组件中选择渲染内容的场景作用非凡，由于是一个无实例的组件，它在渲染开销上比普通组件的性能更好。\n"
  }
]