payload: {"action":"opened","issue":{"url":"https://api.github.com/repos/lhz960904/Blog/issues/6","repository_url":"https://api.github.com/repos/lhz960904/Blog","labels_url":"https://api.github.com/repos/lhz960904/Blog/issues/6/labels{/name}","comments_url":"https://api.github.com/repos/lhz960904/Blog/issues/6/comments","events_url":"https://api.github.com/repos/lhz960904/Blog/issues/6/events","html_url":"https://github.com/lhz960904/Blog/issues/6","id":427068503,"node_id":"MDU6SXNzdWU0MjcwNjg1MDM=","number":6,"title":"Vue的mergeOptions函数分析-下【Vue源码学习】","user":{"login":"lhz960904","id":29157111,"node_id":"MDQ6VXNlcjI5MTU3MTEx","avatar_url":"https://avatars3.githubusercontent.com/u/29157111?v=4","gravatar_id":"","url":"https://api.github.com/users/lhz960904","html_url":"https://github.com/lhz960904","followers_url":"https://api.github.com/users/lhz960904/followers","following_url":"https://api.github.com/users/lhz960904/following{/other_user}","gists_url":"https://api.github.com/users/lhz960904/gists{/gist_id}","starred_url":"https://api.github.com/users/lhz960904/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lhz960904/subscriptions","organizations_url":"https://api.github.com/users/lhz960904/orgs","repos_url":"https://api.github.com/users/lhz960904/repos","events_url":"https://api.github.com/users/lhz960904/events{/privacy}","received_events_url":"https://api.github.com/users/lhz960904/received_events","type":"User","site_admin":false},"labels":[{"id":1252946589,"node_id":"MDU6TGFiZWwxMjUyOTQ2NTg5","url":"https://api.github.com/repos/lhz960904/Blog/labels/Vue","name":"Vue","color":"3eaf7c","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2019-03-29T16:20:01Z","updated_at":"2019-03-29T16:20:01Z","closed_at":null,"author_association":"OWNER","body":"上篇文章分析了mergeOptions函数的主要逻辑，最后知道是分别遍历俩个选项对象都去执行mergeField函数，其中`mergeField`函数其实是根据不同的key值来获取到相应的合并策略，从而执行真正的合并。接下来我们主要分析下Vue针对不同的内部选项实施的合并策略\r\n\r\n## defaultStrat\r\n我们再看一下`mergeField`函数，当`strats[key]`不存在时，会采取`defaultStrat`作为合并策略。也就是说如果我们不向`Vue.config.optionMergeStrategies`添加额外的策略，那就会采取默认的合并策略。\r\n```javascript\r\nfunction mergeField (key) {\r\n  const strat = strats[key] || defaultStrat\r\n  options[key] = strat(parent[key], child[key], vm, key)\r\n}\r\n```\r\n我们可以在当前文件中找到`defaultStrat`函数如下，默认合并策略超简单，就是当子选项`childVal`存在时，就会采用子选项。也就是**覆盖式**的合并。\r\n```javascript\r\nconst defaultStrat = function (parentVal: any, childVal: any): any {\r\n  return childVal === undefined\r\n    ? parentVal\r\n    : childVal\r\n}\r\n```\r\n我们可以通过`Vue.util.mergeOptions()`来看一看`defaultStrat`的合并效果，如下图，当我们父选项`parentVal`、子选项`childVal`上都存在`name`属性时，合并的对象会采用子选项上的值。但是当我们设置`Vue.config.optionMergeStrategies.name`后，`mergeField`函数就会采用我们设置好的合并策略，结果会将俩个值相加。\r\n![默认合并](http://cdn.ihaoze.cn/WechatIMG244.png)\r\n![optionMergeStrategies](http://cdn.ihaoze.cn/WechatIMG246.png)\r\n\r\n## el和propsData\r\n```javascript\r\nif (process.env.NODE_ENV !== 'production') {\r\n  strats.el = strats.propsData = function (parent, child, vm, key) {\r\n    // 没有传vm说明 不是实例化时候 调用的mergeOptions\r\n    if (!vm) {\r\n      warn(\r\n        `option \"${key}\" can only be used during instance ` +\r\n        'creation with the `new` keyword.'\r\n      )\r\n    }\r\n    // 采用默认的策略\r\n    return defaultStrat(parent, child)\r\n  }\r\n}\r\n```\r\n可以发现，el和propsData的合并就是采用了默认的合并策略(覆盖式)，但在非生产环境下，会多一步判断，判断如果没有传`vm`参数则给出警告，`el`、`propsData`参数只能用于实例化。那根据`vm`就可以判断出是否是实例化时候调用的嘛？这里是肯定的。[前文](/_posts/vue-merge-options1/)我们**提到过`Vue.extend`、`Vue.mixin`调用`mergeOptions`是不传入第三个参数的，`mergeOptions`调用`mergeField`函数又会把vm传入进去，所以说vm没有传就为`undefined`，就可以说明不是实例化时调用的**。再说一点，**用`vm`也可以判断出是否是处理子组件选项，因为子组件的实现方式是通过实例化子类完成的，而子类又是通过`Vue.extend`创造出来的。**\r\n## data\r\n```javascript\r\nstrats.data = function ( parentVal: any, childVal: any, vm?: Component): ?Function {\r\n  if (!vm) {\r\n    if (childVal && typeof childVal !== 'function') {\r\n      process.env.NODE_ENV !== 'production' && warn(\r\n        'The \"data\" option should be a function ' +\r\n        'that returns a per-instance value in component ' +\r\n        'definitions.',\r\n        vm\r\n      )\r\n      return parentVal\r\n    }\r\n    return mergeDataOrFn(parentVal, childVal)\r\n  }\r\n  return mergeDataOrFn(parentVal, childVal, vm)\r\n}\r\n```\r\n根据`?Function`可以知道`data`的合并会返回一个函数，这里也会先判断有没有传入`vm`，如果没有传入，会判断子选项data是否是函数，不是函数的话直接返回父选项data并且给出警告。这个警告应该在我们刚开始用Vue都有遇到过，这是为了防止对象引用造成修改会影响到其他组件的`data`。如果是函数，则调用`mergeDataOrFn`函数。那我们可以发现，不管传没传`vm`参数，都会调用`mergeDataOrFn`函数来返回一个函数。那接下来我们看一下`mergeDataOrFn`函数干了什么。\r\n```javascript\r\nexport function mergeDataOrFn ( parentVal: any, childVal: any, vm?: Component): ?Function {\r\n  // 没有vm参数，代表是用 Vue.extend、Vue.mixin合并，\r\n  if (!vm) {\r\n    // 如果没有childVal,返回parentVal \r\n    if (!childVal) {\r\n      return parentVal\r\n    }\r\n    // 如果没有parentVal,返回childVal\r\n    if (!parentVal) {\r\n      return childVal\r\n    }\r\n    // 返回一个合并data函数\r\n    return function mergedDataFn () {\r\n       // 当调用mergedDataFn才会执行mergeData\r\n      return mergeData(\r\n        typeof childVal === 'function' ? childVal.call(this, this) : childVal,\r\n        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal\r\n      )\r\n    }\r\n  } else {\r\n    // 返回一个合并data函数\r\n    return function mergedInstanceDataFn () {\r\n      // 实例化合并，判断是否是函数，函数执行得到对象。\r\n      const instanceData = typeof childVal === 'function'\r\n        ? childVal.call(vm, vm)\r\n        : childVal\r\n      const defaultData = typeof parentVal === 'function'\r\n        ? parentVal.call(vm, vm)\r\n        : parentVal\r\n      if (instanceData) {\r\n        // 如果子选项data有值，则通过mergeData合并。\r\n        // 当调用mergedInstanceDataFn才会执行mergeData\r\n        return mergeData(instanceData, defaultData)\r\n      } else {\r\n        // 子选项data没有值，直接返回默认data\r\n        return defaultData\r\n      }\r\n    }\r\n  }\r\n}\r\n```\r\n上面`mergeDataOrFn`函数分为俩种情况，一种是没有`vm`参数说明是处理子组件合并data选项的，另一种是有`vm`参数说明是实例化处理data的合并。\r\n\r\n**我们先看一下处理子组件的情况**，首先判断没有`childVal`返回`parentVal`，没有`parentVal`返回`childVal`。如果俩个都有值，则返回`mergedDataFn`函数。下图分别展示了三种情况的合并效果。\r\n- 通过`Parent.mixin()`触发合并，此时`parentVal`已经经过`Vue.extend`合并过。`childVal`没有`data`选项\r\n\r\n![parentVal有、childVal没有](http://cdn.ihaoze.cn/2019-03-29-102521.jpg)\r\n\r\n- 通过`Vue.extend()`触发合并，此时`parentVal`是`Vue.options`没有data选项，`childVal`有`data`选项\r\n\r\n![parentVal没有、childVal有](http://cdn.ihaoze.cn/2019-03-29-103617.jpg)\r\n\r\n- `parentVal`、`childVal`都有值时，返回`mergedDataFn`函数\r\n\r\n![parentVal和childVal都有](http://cdn.ihaoze.cn/2019-03-29-132000.jpg)\r\n\r\n\r\n**再看一下处理实例化时data合并的情况**，处理实例化时data合并直接返回`mergedInstanceDataFn`函数。\r\n![实例化合并data](http://cdn.ihaoze.cn/2019-03-29-133211.jpg)\r\n\r\n我们可以发现，俩种情况都是返回函数，并且函数中都是先判断`parentVal`、`childVal`是否是函数，是函数的话直接执行函数获取纯对象，最后都通过`mergeData`来返回合并后的纯对象。所以说`mergeData`函数是真正用来合并选项对象的，那我们在来看一下这个函数。\r\n\r\n```javascript\r\n// 将from的属性添加到to上，最后返回to\r\nfunction mergeData (to: Object, from: ?Object): Object {\r\n  // 如果没有from、直接返回to\r\n  if (!from) return to\r\n  let key, toVal, fromVal\r\n  // 取到from的key值，用于遍历\r\n  const keys = hasSymbol\r\n    ? Reflect.ownKeys(from)\r\n    : Object.keys(from)\r\n  \r\n  for (let i = 0; i < keys.length; i++) {\r\n    key = keys[i]\r\n    // 对象被观察了，会有__ob__属性，__ob__不作处理\r\n    if (key === '__ob__') continue\r\n    toVal = to[key]\r\n    fromVal = from[key]\r\n    // 如果to上没有该属性，则直接将from对应的值赋值给to[key]\r\n    if (!hasOwn(to, key)) {\r\n      set(to, key, fromVal)\r\n    } else if (\r\n      toVal !== fromVal &&\r\n      isPlainObject(toVal) &&\r\n      isPlainObject(fromVal)\r\n    ) {\r\n      // 如果 to、from都有值，并且不相同，而且都是纯对象的话，\r\n      // 则递归调用mergeData进行合并\r\n      mergeData(toVal, fromVal)\r\n    }\r\n  }\r\n  return to\r\n}\r\n```\r\n`mergeData`函数很简单，就是将parentVal的data纯对象(from)所拥有的属性添加到childVal的data纯对象(to)，最后返回合并的纯对象。如果其中俩个纯对象上有相同的key值，则比较是否相等，如果相等什么都不用做，不相等的话，则递归合并。\r\n## Hook钩子函数\r\n```javascript\r\n// 钩子函数当做数组合并来处理，最后返回数组\r\nfunction mergeHook (\r\n  parentVal: ?Array<Function>,\r\n  childVal: ?Function | ?Array<Function>\r\n): ?Array<Function> {\r\n  const res = childVal\r\n    ? parentVal  // childVal有值\r\n      ? parentVal.concat(childVal) // parentVal有值，与childVal直接数组拼接\r\n      : Array.isArray(childVal) // parentVal没有值，将childVal变成数组\r\n        ? childVal\r\n        : [childVal]\r\n    // childVal没有值直接返回parentVal\r\n    : parentVal\r\n  return res\r\n    ? dedupeHooks(res)\r\n    : res\r\n}\r\n// 去重操作\r\nfunction dedupeHooks (hooks) {\r\n  const res = []\r\n  for (let i = 0; i < hooks.length; i++) {\r\n    if (res.indexOf(hooks[i]) === -1) {\r\n      res.push(hooks[i])\r\n    }\r\n  }\r\n  return res\r\n}\r\n// src/shared/constants 文件夹定义\r\nconst LIFECYCLE_HOOKS = [\r\n  'beforeCreate',\r\n  'created',\r\n  'beforeMount',\r\n  'mounted',\r\n  'beforeUpdate',\r\n  'updated',\r\n  'beforeDestroy',\r\n  'destroyed',\r\n  'activated',\r\n  'deactivated',\r\n  'errorCaptured',\r\n  'serverPrefetch'\r\n]\r\n// 所有钩子函数采用一种合并策略\r\nLIFECYCLE_HOOKS.forEach(hook => {\r\n  strats[hook] = mergeHook\r\n})\r\n```\r\n生命周期的钩子选项合并也非常简单，就是把合并当做数组拼接。如果其中一个纯对象没有，则把另一方变成数组返回。这里**有俩点需要提一下**:\r\n1. 在*判断`childVal`没有直接返回`parentVal`*和*当俩个对象都有的时候通过`parentVal.concat()`拼接*，都直接把`parentVal`当做数组来处理。说明`parentVal`一定是数组，因为如果`parentVal`有值，那一定是被mergeOptions处理过一次啦，所以会变成数组。\r\n\r\n2. 上面有判断`Array.isArray(childVal)`，而不是直接变成数组。，说明`childVal`可以是个数组，如下图，我们可以给`created`传入数组也可以\r\n![传入数组](http://cdn.ihaoze.cn/2019-03-29-143039.jpg)\r\n\r\n## component、directive、filter\r\n```javascript\r\nfunction mergeAssets (\r\n  parentVal: ?Object,\r\n  childVal: ?Object,\r\n  vm?: Component,\r\n  key: string\r\n): Object {\r\n  // 创建一个空对象，通过res.__proto__可以访问到parentVal\r\n  const res = Object.create(parentVal || null)\r\n  // 如果childVal有值，则校验childVal[key]是否是对象，不是给出警告。\r\n  // extend函数是将childVal的属性添加到res上，\r\n  if (childVal) {\r\n    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)\r\n    return extend(res, childVal)\r\n  } else {\r\n    return res\r\n  }\r\n}\r\n// component、directive、filter\r\nASSET_TYPES.forEach(function (type) {\r\n  strats[type + 's'] = mergeAssets\r\n})\r\n```\r\n`component`、`directive`、`filter`选项的合并是先将`parentVal`添加到`res.__proto__`上，然后把`childVal`添加到`res`上。当我们使用组件时，Vue会一层一层的向上查找。这也就是为什么我们没有引入`KeepAlive`、`Transition`、`TransitionGroup`内置组件，却可以直接在template中使用，因为在合并时，就已经将内置组件合并到components对象的原型链上。如下图：\r\n![原型链component](http://cdn.ihaoze.cn/2019-03-29-144927.jpg)\r\n\r\n## watch\r\n```javascript\r\nstrats.watch = function (\r\n  parentVal: ?Object,\r\n  childVal: ?Object,\r\n  vm?: Component,\r\n  key: string\r\n): ?Object {\r\n  // Firefox浏览器自带watch，如果是原生watch，则置空\r\n  if (parentVal === nativeWatch) parentVal = undefined\r\n  if (childVal === nativeWatch) childVal = undefined\r\n  // 如果没有childVal，则创建返回空对象，通过__proto__可以访问parentVal\r\n  if (!childVal) return Object.create(parentVal || null)\r\n  // 非正式环境检验校验childVal[key]是否是对象，不是给出警告。\r\n  if (process.env.NODE_ENV !== 'production') {\r\n    assertObjectType(key, childVal, vm)\r\n  }\r\n  // 如果没有parentVal，返回childVal\r\n  if (!parentVal) return childVal\r\n  // parentVal和childVal都有值的情况\r\n  const ret = {}\r\n  // 把parentVal属性添加到ret\r\n  extend(ret, parentVal)\r\n  // 遍历childVal\r\n  for (const key in childVal) {\r\n    let parent = ret[key]\r\n    const child = childVal[key]\r\n    // 如果parent存在，则变成数组\r\n    if (parent && !Array.isArray(parent)) {\r\n      parent = [parent]\r\n    }\r\n    // 返回数组\r\n    ret[key] = parent\r\n      ? parent.concat(child)\r\n      : Array.isArray(child) ? child : [child]\r\n  }\r\n  return ret\r\n}\r\n```\r\n`watch`的选项合并简单说就是判断父子是否都有监听同一个值，如果同时监听了，就变成一个数组。否则就正常合并到一个纯对象上就可以。`watch`也可以为一个值创建监听数组，例如：\r\n```javascript\r\nexport default {\r\n  watch: {\r\n    key: [\r\n      function() {\r\n        console.log('key 改变1')\r\n      },\r\n      function() {\r\n        console.log('key 改变2')\r\n      }\r\n    ]\r\n  }\r\n}\r\n```\r\n## props、methods、inject、computed\r\n```javascript\r\nstrats.props =\r\nstrats.methods =\r\nstrats.inject =\r\nstrats.computed = function (\r\n  parentVal: ?Object,\r\n  childVal: ?Object,\r\n  vm?: Component,\r\n  key: string\r\n): ?Object {\r\n  // 非正式环境检验校验childVal[key]是否是对象，不是给出警告。\r\n  if (childVal && process.env.NODE_ENV !== 'production') {\r\n    assertObjectType(key, childVal, vm)\r\n  }\r\n  // 如果没有parentVal 返回childVal\r\n  if (!parentVal) return childVal\r\n  const ret = Object.create(null)\r\n  // 将parentVal属性添加到ret\r\n  extend(ret, parentVal)\r\n  // 如果childVal有值，也将属性添加到ret\r\n  if (childVal) extend(ret, childVal)\r\n  return ret\r\n}\r\n```\r\n`props`、`methods`、`inject`、`computed`选项的合并是合并到同一个纯对象上，对于父子有同样的`key`值，采取子选型上对应的值。\r\n## provide\r\n```javascript\r\n// provide选型合并采用data选项的合并策略\r\nstrats.provide = mergeDataOrFn\r\n```\r\n\r\n## 参考\r\n- [比这篇更好、更易懂、更全面的一篇文章](http://hcysun.me/vue-design/art/4vue-normalize.html)\r\n- [不容错过的视频教程](https://coding.imooc.com/class/228.html)"}}
other: {"actor":{"display_login":"lhz960904"}}