Vue 源碼閱讀筆記

注: 路人讀者請(qǐng)移步 => Huang Yi 老師的Vue源碼閱讀點(diǎn)這里, 我寫這一篇筆記更傾向于以自問自答的形式增加一些自己的理解 , 內(nèi)容包含面試題范圍但超出更多.

自己提出的問題自己解決:

  1. core/vdom/patch.js setScope如何做到// set scope id attribute for scoped CSS.?
    目前看到了它調(diào)用了nodeOps.setStyleScope(vnode.elm, i),即vnode.elm.setAttribute(i, ' ')

1 Vue.util

Vue.util.extend這個(gè)函數(shù)為例, 查找順序?yàn)?

  • src/platforms/web/entry-runtime-with-compiler.js
    import Vue from './runtime/index'
    • src/platforms/web/runtime/index.js
      import Vue from 'core/index'
      import Vue from './instance/index'
      initGlobalAPI(Vue) from import { initGlobalAPI } from './global-api/index'
      Vue.util = { warn, extend, mergeOptions, defineReactive } from
      import { warn, extend, nextTick, mergeOptions, defineReactive } from '../util/index'
      export * from 'shared/util'
// in shared/util
/**
 * Mix properties into target object.
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

可以看出這個(gè) extend 函數(shù)只支持2個(gè)參數(shù), 這也印證了源碼中提到的"不要依賴 Vue 的 util 函數(shù)因?yàn)椴环€(wěn)定" , 實(shí)測(cè):

2 Vue 數(shù)據(jù)綁定

//調(diào)用例子: proxy(vm,`_data`,"message")
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

實(shí)現(xiàn)的效果就是訪問this.message實(shí)際上通過get訪問到了vm._data.message,而訪問this.message = 'msg'則是通過set訪問了this.message.set('msg')this._data.message = 'msg'
而初始值是在initMixininitData方法中通過

data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}

來賦予初值

2.2 vm.$mount

入口: initMixin中結(jié)尾的時(shí)候

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

取得 el 對(duì)應(yīng)的 dom

el = el && query(el)
//&& 和 || :
//進(jìn)行布爾值的且和或的運(yùn)算暇屋。當(dāng)運(yùn)算到某一個(gè)變量就得出最終結(jié)果之后,就返回哪個(gè)變量胡控。所以上式能返回 dom.
//再舉個(gè)例子: 1 && undefined === undefined , 1 && {1:2} === {1:2}
為什么 Vue 不允許掛載在html | body上?

因?yàn)樗翘鎿Q對(duì)應(yīng)的節(jié)點(diǎn),如果 html 或 body 被替換的話整個(gè)文檔就出錯(cuò)了

template

$options 中有 template 的話

  • 優(yōu)先選用<template></>innerHTML來作為模板,
  • 或者選用template:#id對(duì)應(yīng)的query('#id')innerHTML,
  • 最后才是會(huì)選用getOuterHTML(el)來作為模板

最后,創(chuàng)建好render函數(shù)并掛載到vm上等待執(zhí)行.

2.3 vm._render link

Vue 的 _render 方法是實(shí)例的一個(gè)私有方法暴凑,它用來把實(shí)例渲染成一個(gè)虛擬 Node凡泣。它的定義在 src/core/instance/render.js 文件中

2.3.1 ES6 Proxy link

2.4 Virtual DOM

Virtual DOM 是簡(jiǎn)化的 DOM 形式的結(jié)構(gòu), 以下面例子為例

<body>
 <div id="app">
   <span>{{message}}</span>
 </div>

</body>
<script src="./vue.js"></script>
<script>
 var app = new Vue({
   el: "#app",
   data() {
     return {
       message: 'test'
     }
   }
 })
</script>

斷點(diǎn)位置:執(zhí)行 mountComponent時(shí),vm._update(vm._render(),hydrating)這里, render函數(shù)會(huì)返回如下的 VNODE (簡(jiǎn)化版):

{
  tag:"div",
  children:{
    tag:"span",
    children:{
      tag:undefined,
      text:"test"
    }
  }
}

Vnode 其它屬性中, 原 dom 的 attr 存在了 data 中, 還有更多的屬性不逐個(gè)列舉了

data : {
  attrs: {data-brackets-id: "149", id: "app", editable: ""}
  class: "test"
  staticClass: "origin"
  staticStyle: {color: "red"}
  __proto__: Object
}
提問: 從app的哪個(gè)屬性可以訪問到vnode?

答: app.$vnode不可以, 但是app._vnode可以.
一般情況下的約定中, 以$開頭的屬性名是 Vue 暴露出來的接口(例如this.$store.state), _開頭的是私有屬性不建議訪問, 而普通的(例如app.message)則是從 data 代理過來的數(shù)據(jù).

2.4.1 children 規(guī)范化(normalize)

_createElement 接收的第 4 個(gè)參數(shù) children 是任意類型的藏鹊,因此我們需要把它們規(guī)范成 VNode 類型赔嚎。

正常的由 template 得到的 VNode 是不需要序列化的, 觸發(fā)序列化的有如下幾種情況:

  1. functional component 函數(shù)式組件返回的是一個(gè)數(shù)組而不是一個(gè)根節(jié)點(diǎn)時(shí), 會(huì)調(diào)用simpleNormalizeChildren, 通過 Array.prototype.concat 方法把整個(gè) children 數(shù)組打平膘盖,讓它的深度只有一層
  2. render 函數(shù)是用戶手寫時(shí),當(dāng) children 由基礎(chǔ)類型組成時(shí),Vue會(huì)調(diào)用 normalizeChildren中的createTextVNode 創(chuàng)建一個(gè)文本節(jié)點(diǎn)的 VNode衔憨;
  3. 當(dāng)編譯 slot叶圃、v-for 的時(shí)候會(huì)產(chǎn)生嵌套數(shù)組的情況(測(cè)試發(fā)現(xiàn) template中有簡(jiǎn)單嵌套v-for 的時(shí)候并不觸發(fā)該條規(guī)則,可能強(qiáng)制要求手寫 render 或 復(fù)雜component),會(huì)調(diào)用 normalizeArrayChildren 方法, 遞歸 調(diào)用自己來處理 Vnode , 同時(shí)如果遇到VList則用nestedIndex維護(hù)一下它的key

學(xué)習(xí)一下手寫 render 函數(shù): link

Vue.component('anchored-heading', {
  render: function(h) {
    return h('h' + this.level, this.$slots.default )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

2.5 vm._update(vnode:VNode,hydrating?:boolean)

_update 方法的作用是把 VNode 渲染成真實(shí)的 DOM践图,hydrating表示是否是服務(wù)端渲染 , 它的定義在 src/core/instance/lifecycle.js

vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )

調(diào)用__patch__來xx

//可以看出__patch__是用來用第二個(gè)參數(shù)去替換第一個(gè)參數(shù), 
//而第一個(gè)參數(shù)可以是舊的 Vnode 也可以是原生 DOM
vm.$el = vm.__patch__(prevVnode, vnode)

__patch__的定義查找順序,platform/web/runtime/index.js => patch.js => 取出后端的 node-ops.js 中各種方法,傳遞給 cor/vdom/patch 的 createPatchFunction()

第一次 update 時(shí)return function patch中的關(guān)鍵語句就是:
oldVnode = emptyNodeAt(oldVnode)


{ 該花括號(hào)用于指示分析文字的作用域

先創(chuàng)建一個(gè)空的根節(jié)點(diǎn) Vnode(只有 tag的那種)
createElm(...)
根據(jù) Vnode 創(chuàng)建實(shí)際的 DOM 并插入到原 DOM. 其中遞歸調(diào)用了createChildren

createChildren 我get到的理解

createChildren(vnode, children, insertedVnodeQueue)

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children)
    }
    for (let i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
  }
}

在這里要注意如果原 template 中一個(gè)節(jié)點(diǎn)只有1個(gè)子節(jié)點(diǎn), 那么該 vnode 的 children 屬性將為一個(gè)長(zhǎng)度為1的 Array, 所以仍會(huì)進(jìn)入第一個(gè) if 分支. 也就是說children 要么為一個(gè)長(zhǎng)度至少為1的 Array,要么就是 undefined
createChildren執(zhí)行完之后 this._vnode.elm 就是構(gòu)建完成的原生 DOM 了,接下來執(zhí)行insert(parentElm, vnode.elm, refElm);來把它插入到合適的位置(此時(shí)還未刪除原節(jié)點(diǎn),如下圖)

insert(parentElm, vnode.elm, refElm); 的結(jié)果

} 至此 createElm 終于結(jié)束了

                              // destroy old node
                              if (isDef(parentElm$1)) {
                                removeVnodes(parentElm$1, [oldVnode], 0, 0);
                              } else if (isDef(oldVnode.tag)) {
                                invokeDestroyHook(oldVnode);
                              }
                            }
                          }

                          invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
                          return vnode.elm

__patch__的最后, 根據(jù)之前保存的parentElm$1(在例子中是 body)和oldVnode來移除之前的原生 DOM 節(jié)點(diǎn), 調(diào)用invokeInsertHook(畢竟它插入新節(jié)點(diǎn)和刪除舊節(jié)點(diǎn)都完成了嘛,是時(shí)候向上級(jí)報(bào)告啦!),至此__patch__全部完成, 返回值用于更新vm.$el,
接下來做了約10行的收尾工作(這一章不涉及組件的話vm.$vnode = undefined也看不出什么來)
至此, _update函數(shù)全部完成!

2.6 第二章數(shù)據(jù)綁定總結(jié):

3 組件化

3.1 createComponent

這一小節(jié)主要講了把一個(gè)組件構(gòu)建為 vnode 的過程

測(cè)試時(shí)使用的例子:

<body>
  <div id="app" class="origin" :class="message" style="color:red" editable ref="a1">
    <span>{{message}}</span>
    <cc></cc>
  </div>
</body>
<script src="./vue.js"></script>
<script>
  Vue.component('cc',{
    template:'<strong>I am component</strong>'
  })
  var app = new Vue({
    el: "#app",
    data() {
      return {
        message: 'test'
      }
    }
  })
</script>

小細(xì)節(jié): 組件在創(chuàng)建 Vnode 時(shí), children, text, elm為空,但componentOptions屬性包含了所需要的內(nèi)容

const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

最終我們的例子的組件會(huì)返回如下 vnode(不寫的屬性默認(rèn)為 undefined):

vnode:{
  tag: "vue-component-1-cc",
  test: undefined,
  children: undefined,
  data: {
    attrs: { },
    hook: { destroy, init, insert ,prepatch, on }
  },
  context: Vue,
  componentOptions: {
    Ctor:{  
      extendOptions: { name:"cc",template:"<strong>I am component</strong>"},
      options:{ components, _Ctor, _base,  name:"cc",template:"<strong>I am component</strong>"}
    },
    tag: "cc"
  }
}

3.1 patch - 從組件 vnode 構(gòu)建組件 DOM

了解 patch 的整體流程和插入順序

  • activeInstance
  • $vnode
  • _vnode
  • patch的整體流程: createComponent => 子組件初始化 => 子組件 render => 子組件 patch
  • activeInstance為當(dāng)前激活的 vm 實(shí)例; vm.$vnode為組件的占位符 vnode ; vm._vnode為組件的渲染 vnode

3.2 mergeOptions 合并配置

入口core/instance/index.js其中//...是暫時(shí)略去無關(guān)代碼的意思

function Vue (options) {
  //...
  this._init(options)
}

接著來到core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    //...
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    //...
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

執(zhí)行new Vue(options)時(shí)resolveConstructorOptions返回的就是大Vue本身, 接著繼續(xù)看mergeOptions(在src/core/util/options.js里)

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  //...
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

3.2.1 默認(rèn)策略

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined ? parentVal : childVal
}

3.2.2 strats.el

使用默認(rèn)策略(要求 vm 必須實(shí)例化)

3.3.3 strats.data

若 vm 為空則要求傳入的 data必須是個(gè)函數(shù), 然后返回mergeDataOrFn. 簡(jiǎn)言之, 就是嘗試去獲取 ___Val.call(vm,vm)或 Val 本身(取決于它是不是函數(shù))來得到數(shù)據(jù), 然后調(diào)用一個(gè)無依賴的函數(shù)mergeData進(jìn)行深拷貝形式的 merge

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    //...
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

3.3.4 props methods inject computed

先斷言 childVal 是 Object , 然后使用簡(jiǎn)單的 extend函數(shù)將二者合并

strats.props =
strats.methods =
strats.inject =
strats.computed = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  if (childVal && process.env.NODE_ENV !== 'production') {
    assertObjectType(key, childVal, vm)
  }
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  if (childVal) extend(ret, childVal)
  return ret
}

3.3.5 生命周期鉤子, 例如 created

主要有這些鉤子:

export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
]

生命周期鉤子的合并策略 strats[hook]都被賦值為 mergeHook, 具體過程是把不同的 created 函數(shù)串成一串(即存入一個(gè) array 中), 形式是[created1,created2 ,... ]

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

3.3.6 ASSET , 例如components

在這里, parent 的KeepAlive Transition TransitionGroup被傳入的 child 的 components 所替代

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    return extend(res, childVal)
  } else {
    return res
  }
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

3.4 生命周期

callHook調(diào)用不同鉤子時(shí), 我們的 vm 對(duì)象都有哪些參數(shù)呢?

3.4.1 beforeCreated

這個(gè)時(shí)候已經(jīng)執(zhí)行了Init Events & Lifecycle & render , 可以看到 vm 對(duì)象的$options已經(jīng)執(zhí)行完畢合并配置. 但這時(shí) $data為空

3.4.1.1 未執(zhí)行任意一個(gè) init 時(shí)

可以看到已經(jīng)執(zhí)行過了mergeOption合并配置

3.4.1.2 執(zhí)行完initLifecycle

多了$children, $parent, $refs, $root

3.4.1.3 執(zhí)行完initEvents

多了_events , _hasHookEvent

3.4.1.4 執(zhí)行完initRender

多了_vnode, _staticTrees, $slots, $scopedSlots, $_c, $createElement, 還未真的執(zhí)行渲染. 至此, vm 這個(gè)對(duì)象已經(jīng)初始化完成, 調(diào)用beforeCreated的 hook.

3.4.2 created

執(zhí)行了

  • initInjections, 暫不明
  • initState 把 props data methods computed watch 掛載上了,可以訪問 this.message 了 , 但此時(shí)不能訪問 this.el
  • initProvide, 暫不明

3.4.3 beforeMount

入口是vm.$mount(vm.$options.el) 可以看到此時(shí) el 進(jìn)入了我們的視野,可以訪問 vm.el 了, 此時(shí)的el 是一個(gè)原生HTMLElement
根據(jù) el 來生成 render 函數(shù)
在控制臺(tái)中看不到 vm._render, 但是可以執(zhí)行 vm._render(), 應(yīng)該和 vm._renderProxy 有關(guān)


beforeMount 鉤子的執(zhí)行順序先父后子

3.4.4 mounted

子組件的 mounted 優(yōu)先于父組件.

vm._update(vm._render(), ...)

先執(zhí)行_render(), 再把它更新到 DOM 上. 如果檢測(cè)父 vnode(vm.$vnode)為空,說明自己就是 root , 則可以調(diào)用 mount 的 hook.
后續(xù)進(jìn)入等待狀態(tài), 等待數(shù)據(jù)更新帶來的beforeUpdate/ update

3.4.5 beforeDestroy destroy

前者先父后子, 后者先子后父, 同 mount 類似

3.4.6 總結(jié)

created鉤子中可以訪問到數(shù)據(jù), 在mounted鉤子中可以訪問到 DOM, 在destroyed鉤子中可以做一些定時(shí)器的銷毀工作.

3.5 組件注冊(cè)

3.5.1 全局注冊(cè)

推薦用-分割組件名
全局注冊(cè)的行為必須在根 Vue 實(shí)例 (通過 new Vue) 創(chuàng)建之前發(fā)生

Vue.component('component-a', { /* ... */ })
Vue.component('component-b', { /* ... */ })
Vue.component('component-c', { /* ... */ })

new Vue({ el: '#app' })
//index.html
<div id="app">
  <component-a></component-a>
  <component-b></component-b>
  <component-c></component-c>
</div>

全局注冊(cè)的組件在各自內(nèi)部也都可以相互使用

3.5.2 局部注冊(cè)

全局注冊(cè)往往是不夠理想的掺冠。比如,如果你使用一個(gè)像 webpack 這樣的構(gòu)建系統(tǒng)码党,全局注冊(cè)所有的組件意味著即便你已經(jīng)不再使用一個(gè)組件了德崭,它仍然會(huì)被包含在你最終的構(gòu)建結(jié)果中。這造成了用戶下載的 JavaScript 的無謂的增加
局部注冊(cè)的方法有:
1.js 形式

new Vue({
  el: '#app',
  components: {
    'component-a': ComponentA,
    'component-b': ComponentB
  }
})
  1. 模塊系統(tǒng)中
import ComponentA from './ComponentA'

export default {
  components: {
    ComponentA,
  },
  // ...
}

而特別常用的局部組件應(yīng)該做全局化處理, 參考官方文檔

3.5.2 全局注冊(cè)的源碼

src/core/global-api/assets.js

export function initAssetRegisters (Vue: GlobalAPI) {
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        //... 組件名校驗(yàn) √
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

核心this.options[type + 's'][id] = this.options._base.extend(definition)
即擴(kuò)展了 this.$options.components.cc ( cc 只是一個(gè)組件名 )

提問: 為什么這樣就可以用了呢?在模板中遇到<cc></cc>會(huì)如何解析呢?
答: 原來在_createElement的時(shí)候會(huì)對(duì) tagName 進(jìn)行判斷, 如果是原生 tagName 就創(chuàng)建原生DOM 對(duì)應(yīng)的 vnode , 否則執(zhí)行vnode = createComponent(...)

過程細(xì)節(jié):

  • resolveAssets 拼了老命地嘗試去查找組件名, 先找分割線,不行就駝峰, 再不行就首字母大寫試試, 還不行就去prototype 中找(這里面有 KeepAlive,Transition,TransitionGroup), 實(shí)在找不到就返回 undefined( 進(jìn)而在下面判斷 Array.isArray(vnode)的時(shí)候進(jìn)入分支createEmptyVNode()
  • createComponent 之前的注冊(cè)相當(dāng)于只是記錄了 component 的信息, 到這一步才是真的創(chuàng)建, 在這一步里, 會(huì)new VNode , 然后處理好它的 data, elm, tag等等, 同時(shí)還對(duì) slot 做了處理

提問: vm.$options.components 什么時(shí)候拿到的呢?
答: 在合并配置時(shí), mergeField 時(shí), 就把 Vue['components'] 和傳入?yún)?shù) merge 在一起賦值給 vm.$options 了

3.5.4 局部注冊(cè)的源碼

過程同全局注冊(cè)類似, mergeOption掛到 vm.$options.components , 這樣 resolveAssets 就可以拿到了.
要注意此時(shí)并沒有注冊(cè)到 Vue.components 對(duì)象上

3.5.5 異步組件

還沒看,暫時(shí)略過

4 深入響應(yīng)式原理

4.0 什么時(shí)候收集依賴和清空依賴

收集依賴:
執(zhí)行數(shù)據(jù)的 getter 時(shí)會(huì)收集依賴, 一般為[初次渲染 DOM, 執(zhí)行計(jì)算屬性的 getter] 等情況, 前者將 RederWatcher 添加入數(shù)據(jù)的__ob__deps[]中. 后者不僅將用戶 computed watcher 添加入數(shù)據(jù)的deps[]中, 還通過computed watcher.depend()來將 RenderWatcher 添加入數(shù)據(jù)的deps[]

清空依賴 watcher.cleanupDeps()

  • RenderWatcher 執(zhí)行完自己的 getter(內(nèi)部執(zhí)行_update(_render()), 返回銷毀函數(shù)), 會(huì)執(zhí)行一次清空依賴
  • 計(jì)算屬性在執(zhí)行完 getter 以及popTarget()后, 會(huì)執(zhí)行一次清空依賴.
    • 清空前可能是name屬性依賴[ useless, firstName, lastName ], 此時(shí)存放在name watchernewDepsnewDepIds中, 同時(shí)三個(gè) data 的__ob__.deps : []中也存儲(chǔ)了name watcher
    • 清空的過程就是查找 watcher.dep[](oldDep)中有但是newDep[]中沒有的對(duì)象, 即可以想象為

name 對(duì) firstName 說: "以前我依賴你, 但我重新審視了一下自身, 我現(xiàn)在已經(jīng)不依賴你了, 我們斷絕關(guān)系吧!"

/**
 * Clean up for dependency collection.
 */
Watcher.prototype.cleanupDeps = function cleanupDeps () {
    var this$1 = this;

  var i = this.deps.length;
  while (i--) {
    var dep = this$1.deps[i];
    if (!this$1.newDepIds.has(dep.id)) {
      dep.removeSub(this$1);
    }
  }
  var tmp = this.depIds;
  this.depIds = this.newDepIds;
  this.newDepIds = tmp;
  this.newDepIds.clear();
  tmp = this.deps;
  this.deps = this.newDeps;
  this.newDeps = tmp;
  this.newDeps.length = 0;
};

4.1 Vue.set 為什么不允許設(shè)置根 data ?

例如 Vue.set(app,'msg','value') , 這樣app.msg是拿不到app.$data.msg的, 缺少了一層代理(在defineReactive函數(shù)中沒有為它增加到$data的代理). 而如果是Vue.set(app.msg,'msg2','value'), 則是可以通過代理拿到app.$data.msg.msg2

簡(jiǎn)單來說, Vue.set(app,'msg','value')實(shí)際上就是執(zhí)行defineReactive(app,'msg','value'), 而這個(gè)函數(shù)內(nèi)部是沒有寫proxy

如果重新調(diào)整代碼結(jié)構(gòu), 把 proxy 放入 defineReactive 函數(shù)中中執(zhí)行, 那就 ok . 不過干脆期待 vue3.0的 es6 proxy 代理比較好, 比 Object.defineProperty 的功能更強(qiáng)大.

4.2 用戶手動(dòng)添加 watcher 導(dǎo)致 update loop 時(shí)的循環(huán)流程

和 watcher 相關(guān)的全局變量有has = { } , waiting = false, flushing = false, index //queue
watcher的 queue 加入順序, 實(shí)例流程解析如下

var app = new Vue({
  data : { msg : 1, count: 0 },
  watch : { msg(){ this.count++<100 && this.msg = Math.random() }
})
1. 首次渲染后, 第一次改變 msg 的數(shù)值時(shí), watcher 的隊(duì)列內(nèi)容如下
queue = [ { id:1, expression:"msg"}, { id:2, expression:" ... vm._update(vm._render())" } ]
has = { 1: true , 2: true }
2. 接著, 若此時(shí) waiting 為 false, 則置 waiting 為 true, 置 flushing 為 true,注冊(cè)nextTick(flushSchedulerQueue)
3. 執(zhí)行flushSchedulerQueue時(shí), 首先取出 queue[0], 設(shè)置 has[1] = null , 執(zhí)行該 watcher 的 .run 函數(shù)時(shí), 
發(fā)現(xiàn)處理好新舊 value 后, 在調(diào)用 callback(cb)的過程中, 碰到了用戶代碼的 this.msg = Math.random() 語句,
則再次觸發(fā)一輪新的 代理 setter 過程
4. 在新的一輪代理 setter 過程中, 訂閱者仍然是["msg", "... vm._update(vm._render())"]兩人, 此時(shí)
has = [ 1: null, 2: true ], 所以前者可以以插隊(duì)形式(正好插在{id:2}的前面)加入 queue, 后者的插隊(duì)被拒絕(因?yàn)?has[2] 為 true).

循環(huán)若干次后, queue 的狀態(tài)會(huì)變?yōu)?
queue = [
 { id:1, expression : "msg" },
 { id:1, expression : "msg" },
 { id:1, expression : "msg" },
...
 { id:2, expression : "... vm._update(vm._render())" }, 
]

5.最終, 達(dá)到100次后(若超過100次則會(huì)拋出"infinite update loop"異常), 不再向 queue 中添加新的內(nèi)容, 
index 終于可以如愿執(zhí)行至{ id:2 }的 watcher,  DOM 被更新

總結(jié): 可以看到, 得益于nextTick 的異步機(jī)制, msg 這個(gè)數(shù)據(jù)執(zhí)行了100次(數(shù)量取決于用戶代碼)setter后才刷新一次 DOM, 性能表現(xiàn)很好.

4.3 this.$nextTick( ) 異步更新

Vue 內(nèi)部檢測(cè)到數(shù)據(jù)變化后會(huì)將 watcher 添加入 queue, 而 DOM 刷新是放在了nextTick(flushSchedulerQueue)中, 也就是說 DOM 的更新是個(gè)異步過程, 同時(shí)用戶自定義 watch 函數(shù)也是異步的, 作為驗(yàn)證, 可以測(cè)試如下代碼, watch 內(nèi)的函數(shù)只執(zhí)行了一次

var app = new Vue({
  // ...
  data : { msg : 1 },
  methods : { 
    change(){ 
        [2,3,4,5].forEach(x => this.msg = x) 
    }
  },
  watch: { 
    msg() { 
      console.log(arguments)  //該函數(shù)只會(huì)觸發(fā)一次
    } 
  }
}

如果要獲取修改后的 DOM, 可以調(diào)用 this.$nextTickVue.nextTick, 二者完全一致

  • this.$nextTick(fn) 會(huì)將 fn 加入 callbacks 隊(duì)列
  • this.$nextTick()會(huì)生成一個(gè)狀態(tài)為resolvedPromise 對(duì)象, 并將該存儲(chǔ)于函數(shù)閉包中的_resolve加入 callbacks 隊(duì)列
  • 若閉包中的變量pending為 false, nextTick 函數(shù)會(huì)執(zhí)行macroTimerFunc()microTimerFunc()來異步執(zhí)行 flushCallbacks.
    • macroTimerFunc實(shí)質(zhì)上等于messageChannel觸發(fā)onmessage事件,該事件的回調(diào)是flushCallbacks
    • microTimerFunc實(shí)質(zhì)上等于()=>Promise.resolve().then(flushCallbacks)
var app = new Vue({
  // ...
  data : { msg : 1 },
  methods : { 
    async change(){ 
        this.msg = 2
        console.log('sync:', this.$refs.msg.innerText) // sync: 1
        /* 下面三種寫法效果一致, 都會(huì)輸出 2 */ 
        this.$nextTick(()=>{
              console.log('nextTick:', this.$refs.msg.innerText)
        })
        this.$nextTick().then(()=>{
              console.log('nextTick with promise:', this.$refs.msg.innerText)
        })
        await this.$nextTick()
        console.log('sync:', this.$refs.msg.innerText)
    }
  },

有所區(qū)別的是, 下面的代碼會(huì)輸出1

  async change(){ 
        this.msg = 2
        Promise.resolve().then(()=>console.log('promise:', this.$refs.msg.innerText)) //1
    }

因?yàn)樵摵瘮?shù)里的 Promise 是在這一輪 event-loop 的末尾執(zhí)行的, 而 nextTick 的回調(diào)是在下一輪event-loop 的開頭執(zhí)行的

關(guān)于這點(diǎn), 單步調(diào)試可發(fā)現(xiàn) nextTick 中在macromicro二選一時(shí)選擇了macro

更新: vue-2.6中作者權(quán)衡利弊后又把 nextTick 全部改為了 microTask, 參見2.6 Internal Change: Reverting nextTick to Always Use Microtask

4.4 Vue 如何實(shí)現(xiàn) computed 計(jì)算屬性 ?

示例代碼:

var app= new Vue({
 //...
 computed:{
            name(){
                return this.useless > 0? this.firstName+ ', ' +this.lastName : 'please click change'
            }
  },
})

4.4.1 計(jì)算屬性注冊(cè):

  1. initState函數(shù)中發(fā)現(xiàn)$options中有 computed 屬性, 則調(diào)用 initComputed 函數(shù)
  2. 在 vm 上掛載vm._computedWatchers屬性,初始化為{ }, 該屬性是為計(jì)算屬性專屬, 如果用戶 options 中沒有計(jì)算屬性, 則它不會(huì)出現(xiàn)
  3. 遍歷computed , 例如本例中只會(huì)遍歷一次name
  • 獲取computed[name]對(duì)應(yīng)的 getter:
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
  • 新建一個(gè) watcher
    watchers[name] = new Watcher(vm, getter, noop, { lazy :true });
    注意該 watcher 的lazy屬性和dirty屬性都為 true, 這里做了一個(gè)緩存機(jī)制
  • Object.defineProperty(vm, key /* name */, sharedPropertyDefinition);

其中sharedPropertyDefinition的 getter 是由 createComputedGetter 函數(shù)來生成的, 放到下面取值的過程講. setter 我們暫時(shí)忽略

4.4.2 計(jì)算屬性的 getter

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}

當(dāng)渲染 DOM 需要用到name時(shí), 檢測(cè)dirty標(biāo)志, 判斷該計(jì)算屬性是否被修改過,

  • 若無, 則返回之前的緩存值watcher.value
  • 若有, 則置dirty為 false , 執(zhí)行watcher.evaluate()獲取新的值
    • 在該函數(shù)中, 會(huì)調(diào)用this.get(), 而此處會(huì)判斷數(shù)值是否變動(dòng)來決定下一步操作, 例如'firstName' + 'lastName' === 'firstNam'+'elastName', 此時(shí)兩個(gè)響應(yīng)式屬性的變動(dòng)會(huì)將 name 的 watcher 添加到隊(duì)列中并執(zhí)行, 但 name的 watcher 執(zhí)行了自己的this.get()后發(fā)現(xiàn)自己沒變化, 就不需要把渲染 watcher 再添加到隊(duì)列了.
  • 在渲染 watcher 的上下文環(huán)境中要做依賴收集//若是控制臺(tái)無聊輸出 vm.name 則不需要
    接下來看看this.firstName的變動(dòng)是如何讓namedirty變化的fase => true

4.4.3 data 變動(dòng)引起 computed 值變動(dòng)的過程

  1. 首先, this.useless會(huì)變?yōu)?code>true, 會(huì) dep.notify( ) namewatcher 和DOMwatcher
  • update namewatcher 的過程就是設(shè)置它的this.dirty為 true
  • 將 DOM 渲染 watcher 放入隊(duì)列
    提問: 為什么 useless 會(huì)同時(shí)擁有兩個(gè) watcher?為什么不是 useless 通知 name 更新, name 再通知 dom 更新?
    答: 因?yàn)榇a中有這樣一部分, 計(jì)算屬性取值時(shí)watcher.evaluate()后, 又執(zhí)行了watcher.depend(), 該方法中會(huì)執(zhí)行this.deps[i].depend(), 于是就把 dom 渲染 watcher 也給 useless 和 firstName 等每人依賴了一份. 相對(duì)的, 在數(shù)據(jù)變動(dòng)而 notify 它的 watcher 更新時(shí), 不會(huì)把這兩個(gè) watcher 都放入隊(duì)列, 而是只把計(jì)算書行的 dirty 設(shè)置為 true, 把 dom渲染 watcher 放入隊(duì)列.
  1. nextTick 清空 callBacks 隊(duì)列 => 清空 flushSchedulerQueue 隊(duì)列
    在這個(gè)過程中, dom 渲染 watcher.run()時(shí), 會(huì)重新收集依賴.

4.4.4 如果 data 變了但是 computed 不變會(huì)怎么辦?

關(guān)于這件事的詳細(xì)討論可以參考

  1. github 博客 深入淺出 - vue變化偵測(cè)原理
  2. vue-2.5.17-beta0中引入的PR 尤大寫的 PR 事實(shí)上該 beta 版本未被合入2.5.17正式版中

考慮如下的代碼

示例代碼1

computed:{
  name() { return this.a + this.b }
},
methods:{
  change() { this.a++; this.b-- }
}

示例代碼2

computed:{
  name() { return this.a > 0 ? this.b : 0 }
},
methods:{
  change() { this.a++ }
}

2.5.17~2.6.10的版本中

會(huì)在同步代碼執(zhí)行完畢后, 判斷新舊 vnode 相同的部分來達(dá)到不重繪dom 的目的, 但是生成新的 vnode 時(shí)還是用了不少時(shí)間.

2.5.17-beta.0的版本中

作者嘗試使用watcher.getAndInvoke函數(shù)來實(shí)現(xiàn)計(jì)算屬性不變則不重繪的目的, 結(jié)果:

  • 對(duì)示例代碼2表現(xiàn)效果非常好
  • 但對(duì)示例代碼1會(huì)發(fā)現(xiàn)事與愿違, 由于event-loop的關(guān)系, 上述代碼反而會(huì)讓name()發(fā)現(xiàn)自己被改變了2次, 進(jìn)而觸發(fā)兩次創(chuàng)建新 vnode, 進(jìn)而觸發(fā)2次重繪 dom.

那么理想的解決辦法是什么呢?

理想的執(zhí)行順序?yàn)?
change()改變 data(同步) => name()求值(異步) => 根據(jù)需要重繪 dom

目前在2.5.17版本中, 重繪 dom的異步是在 macrotask(messageChannel)中實(shí)現(xiàn)的
而在^2.6.10版本中, 重繪 dom的異步全部使用 microTask(Promise)

那么, 如果想要讓 computed 的求值異步任務(wù)放在重繪 DOM 之前, 就要構(gòu)造一個(gè)優(yōu)先級(jí)比 Promise 更高的 microtask. 我很期待 Vue 3.0 給我們帶來的改變!

5. 編譯

關(guān)于簡(jiǎn)單的 HTMLParser 請(qǐng)移步我的另一篇文章 HTMLParser 的實(shí)現(xiàn)和使用, 下面主要紀(jì)錄 Vue 的start end等鉤子函數(shù)中做了什么.

5.1 AST Node 的分類

  • type:1 普通 tag , 例如{ type:1, tag:'div ,attrs}
  • type:2 模板語法字符串, 例如 { type:2, text:'{{msg}}', expression:'_s(msg)', tokens }
  • type:3 純文本字符串, 例如{ type:3, text:'hello world' }
  • type:3 注釋字符串, { type:3, text, isComment:true }

5.2 鉤子函數(shù)

5.2.1 comment

function comment (text: string) {
      currentParent.children.push({
        type: 3,
        text,
        isComment: true
      })//純文本注釋
    }

5.2.2 chars

function chars (text: string) {
      const children = currentParent.children
      //對(duì)于一般</ul>閉合前的若干空格, text.trim()會(huì)變成長(zhǎng)度為0的"",此時(shí)一般保留1個(gè)空格
      //我也不知道為什么,明明下面 end()中又把這個(gè)空格 pop 掉了
      text = text.trim() ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
                         // only preserve whitespace if its not right after a starting tag
                         : preserveWhitespace && children.length ? ' ' : ''
      if (text) {
        let res
        if (text !== ' ' && (res = parseText(text, delimiters))) {
          children.push({
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          })//模板字符串,由 "{{msg}}" 轉(zhuǎn)為 "_s(msg)"
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          children.push({
            type: 3,
            text
          })//純文本節(jié)點(diǎn)
        }
      }
    }

5.2.3 start

function start(tag, attrs, unary) {
    /* @type{ type:1, tag, parent, children, attrsList, attrsMap } */
    let element  = createASTElement(tag, attrs, currentParent)

    // apply pre-transforms 如果 tag 為 input 的話處理 v-model 相關(guān)
    for (let i = 0; i < preTransforms.length; i++) {
        element = preTransforms[i](element, options) || element
    }
    if (!element.processed) {
        processFor(element)     /* 處理 v-for, 例如對(duì) v-for="(item,index) in data"處理為
                                   Object.assign(element,{ alias:"item", for:"data", iterator1:"index" }) */
        processIf(element)      /* 處理 v-if, 例如對(duì) v-if="isShow" 處理為 element.if="isShow",
                                   同時(shí)設(shè)置 elment.ifConditions = [{ exp:"isShow", block:element }] */
                                /* 另外,遇到attrs 含有 v-else 節(jié)點(diǎn)時(shí),標(biāo)記 { else:true }, 然后在下面 processIfConditions 處理*/

        processOnce(element)    //處理 v-once
        processElement(element, options)/* 處理 ref slot component, 
                                         * transform[0] : 處理 staticClass, classBinding
                                         * transform[1] : 處理 staticStyle, styleBinding 
                                         */
    }

    // tree management
    if (!root) {
        root = element
    } 
    if (currentParent) {
        if (element.elseif || element.else) {
            processIfConditions(element, currentParent) /* 對(duì) v-else 節(jié)點(diǎn)vel,在當(dāng)前父親下尋找前面的 v-if 節(jié)點(diǎn)vif,并設(shè)置
                                                           vif.ifConditions.push({ 
                                                               exp:vel.elseif, 
                                                               block:vel 
                                                           }) */
        } else if (element.slotScope) { // scoped slot
            currentParent.plain = false
            const name = element.slotTarget || '"default"'
                ; (currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
        } else {
            currentParent.children.push(element)
            element.parent = currentParent
        }
    }
    if (!unary) {
        currentParent = element
        stack.push(element)
    } else {
        closeElement(element)
    }
}

5.2.4 end

function end() {
    //去除尾部空白字符,例如
    /* <ul>
          <li></li>
       </ul> //此時(shí) ul 會(huì)有2個(gè) child,一個(gè)是 li,一個(gè)是 li 后面的空格,所以要去除空格
    */
    var element = stack[stack.length - 1];
    var lastNode = element.children[element.children.length - 1];
    if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
        element.children.pop();
    }
    // pop stack
    stack.length -= 1;
    currentParent = stack[stack.length - 1];
}

5.3 optimize 優(yōu)化

  • optimize 的目標(biāo)是通過標(biāo)記靜態(tài)根的方式, 優(yōu)化重新渲染過程中對(duì)靜態(tài)節(jié)點(diǎn)的處理邏輯
  • optimize 的過程就是深度遍歷這個(gè) AST 樹,先標(biāo)記靜態(tài)節(jié)點(diǎn), 在標(biāo)記靜態(tài)根
  • 靜態(tài)節(jié)點(diǎn): 例如 <p>123</p>, 即子節(jié)點(diǎn)都要是靜態(tài)節(jié)點(diǎn), 且自己能通過isStatic
  • 靜態(tài)根: node.type必須為1 且node.static==1node.children必須有>=1個(gè)非純文本孩子, 稱之為靜態(tài)根
    • <div><p>111</p></div>是靜態(tài)根
    • <ul><li>1</li><li>2</li><li>3</li>是靜態(tài)根
    • <li>1</li>不是靜態(tài)根, 雖然它是靜態(tài)節(jié)點(diǎn)

單個(gè)靜態(tài)節(jié)點(diǎn)的判定:

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

5.4 codegen 代碼生成

5.4.1 codegen 的輸入和輸出

下面的示例代碼

<ul :class="bindCls" class="list" v-if="isShow">
    <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

會(huì)被編譯成如下渲染函數(shù)

function anonymous(
) {
  with(this){
  return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return _c('li', {
          on: {
            "click": function($event) {
              clickItem(index)
            }
          }
        },
        [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()
 }
}

可以在渲染函數(shù)-模板編譯測(cè)試一下

其中用到的_c這些下劃線函數(shù)可以在src/core/instance/render-helpers/index.js中找到

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual /* Check if two values are loosely equal - that is,
                         if they are plain objects, do they have the same shape? */
  target._i = looseIndexOf // 判斷是否相等時(shí)使用上面的函數(shù)的數(shù)組 indexOf 
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}

5.4.2 我自己嘗試編寫的簡(jiǎn)單的 codegen 邏輯

其中 js 字符串排版使用了beautify.js
其中輸入的 ast 是optimize 優(yōu)化過的 ast 結(jié)構(gòu)


function generate(node) {
    var res = "function anonymous(){with(this){return "
    res += gen(node);
    return res + "}}"

    function gen(el) {
        if (el.type == 1) {
            debugger
            if (el.for && !el.forProcessed) {
                el.forProcessed = true
                return genFor(el)

            } else if (el.if && !el.ifProcessed) {
                el.ifProcessed = true
                return genIf(el)
            } else {
                var data = el.plain ? undefined : JSON.stringify(el.attrsMap)
                var children = ""
                if (el.children.length === 1 && el.children[0].for) {
                    children = gen(el.children[0])
                }
                else {
                    children = '[' + el.children.map(x => gen(x)).join(',') + ']'
                }
                code = `_c('${el.tag}'${data ? ("," + data) : ''}${children ? ("," + children) : ''})`;
                return code
            }
        } else if (el.type == 2) {
            return `_v(${el.expression})`
        } else if (el.type == 3 && el.text.trim()) {
            return `_v(${el.text})`
        } else {
            return ''
        }
        function genIf(el) {
            return (function genIfConditions(conditions) {
                var leftCondition = conditions.shift()
                if (leftCondition && leftCondition.exp) {
                    return '(' + leftCondition.exp + ')?' + gen(leftCondition.block) + ':' + genIfConditions(conditions)
                }
                else {
                    return "_e()"
                }
            })(el.ifConditions.slice())
        }
        function genFor(el) {
            return `_l((${el.for}),function(${el.alias},${el.iterator1}){ return ${gen(el, false)}})`
        }
    }
}

整體思路比較簡(jiǎn)單, 對(duì)于文本節(jié)點(diǎn)使用_v,對(duì)于iffor做了特殊的處理,下面看一下對(duì)同一段 DOM 的測(cè)試結(jié)果:

//測(cè)試
js_beautify(generate(ast),{ indent_size: 2, space_in_empty_paren: true })
//測(cè)試結(jié)果
function anonymous() {
  with(this) {
    return (isShow) ? _c('ul', {
      ":class": "bindCls",
      "class": "list",
      "v-if": "isShow"
    }, _l((data), function(item, index) {
      return _c('li', {
        "v-for": "(item,index) in data",
        "@click": "clickItem(index)"
      }, [_v(_s(item) + ":" + _s(index))])
    })) : _e()
  }
}

可以看到, 我寫的簡(jiǎn)單 generate 和 vue 的, 對(duì)同一段簡(jiǎn)單 DOM 生成的 render 函數(shù)基本一致, 所以原理基本搞清楚了, 但有些細(xì)微差別:

  1. 我沒處理{ on: { "click": function($event) { clickItem(index) } } 這種結(jié)構(gòu)

  2. 我沒處理node.staticRoot這些靜態(tài)根節(jié)點(diǎn)

  3. 我沒處理組件

  4. 對(duì)于v-forv-if 我的處理和 vue 一致, 其中包括:

    • v-for且 children 數(shù)組長(zhǎng)度為1時(shí), 不生成[ gen(el) ] 而是直接生成 gen(el), 即將此種孩子提升了一級(jí). 實(shí)際上_c是能接收_l產(chǎn)生的結(jié)果的
    • v-if中良好的處理了v-if v-elseif v-elseif v-else這種多級(jí)結(jié)構(gòu)

5.4.3 Vue對(duì) staticRoot 節(jié)點(diǎn)的處理

Vue 會(huì)將這類 node 渲染為一個(gè)新的function anonymous(){ with(this) //... }, 并 push 入staticRenderFns中, 然后它的 codegen 就是返回該函數(shù)的序號(hào), 例如_m(0).
此外, Vue 還以簡(jiǎn)單的 cached[template] 的形式對(duì)模板生成的 render 函數(shù)和staticRenderFns 進(jìn)行了緩存

6 擴(kuò)展

6.1 Vue 和 React 事件綁定的this 對(duì)比

="handler" ="handler()" ="()=>handler()"
Vue 編譯為with(this){ .... handler() }
成功綁定回調(diào)和this
編譯為with(this){ .... function($event){ handler() }
成功綁定回調(diào)和 this
with(this){ .... 'click':()=>handler() } 在Render時(shí)綁定this到箭頭函數(shù)
React 能觸發(fā)事件,但是直接執(zhí)行handler()未綁定 this 在 Render 時(shí)會(huì)觸發(fā)一次 handler(), 然后將 handler()的返回值傳入addEventListener, 可能為 undefined 而導(dǎo)致事件沒有回調(diào) 成功綁定回調(diào), 在 Render 時(shí)綁定 this 到箭頭函數(shù)

6.2 為什么@click="alert(1)" 或者 @click="console.log(1)"會(huì)報(bào)錯(cuò)

這個(gè)問題出現(xiàn)在 vue2.5.17-2.6.10 的非 production 版本上, 如果使用 production 版本則問題消失. 檢查源碼可以發(fā)現(xiàn)在開發(fā)版的 vue 生命周期中有一個(gè)initProxy函數(shù), 為 vm 掛載了vm._renderProxy屬性, 此時(shí), 在執(zhí)行 render 函數(shù)訪問其中屬性時(shí), 會(huì)優(yōu)先訪問代理屬性. 即, 訪問 console.log(1), 會(huì)優(yōu)先訪問 this.console

vue 的 render 函數(shù)大概長(zhǎng)這樣:

function anonymous() {
    with (this) {
        return _c('div', [_c('p', {
            on: {
                "click": function($event) {
                    return console.log(1)
                }
            }
        })])
    }
}

本來, 在 with(this) 的函數(shù)中, 訪問 this.console 如果是 undefined, 會(huì)再次訪問上級(jí)作用域來尋找 console 值. 但是 開發(fā)版的 vue 做了代理,

  const hasHandler = {
    has (target, key) {
      const has = key in target
      const isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
      if (!has && !isAllowed) {
        if (key in target.$data) warnReservedPrefix(target, key)
        else warnNonPresent(target, key)
      }
      return has || !isAllowed
    }
  }

這個(gè)代理導(dǎo)致系統(tǒng)在判斷 vm.console 是否存在時(shí), has值為false, isAllowedfalse , 因此系統(tǒng)覺得 vm.console 存在, 就不去訪問 window 了, 于是出錯(cuò)

總之, 還是不推薦在@click=""里直接寫alert console window這些全局作用域里有的東西, 只寫 vm 的作用域里有的東西比較好. 或者把alert console window轉(zhuǎn)移至methods中去

6.3 語法糖 v-model

對(duì)普通元素的 v-model 有下面的等價(jià)關(guān)系

<input v-model="message">

<input v-bind:value="message"
       v-on:input="if($event.target.composing) return; message=$event.target.value">

對(duì)組件的 v-model, 語法糖等價(jià)關(guān)系變?yōu)榱?/p>

//子組件 
let Child = {
  template: '<div>'
  + '<input :value="value" @input="updateValue" placeholder="edit me">' +
  '</div>',
  props: ['value'],
  methods: {
    updateValue(e) {
      this.$emit('input', e.target.value)
    }
  }
}
// 父組件 v-model 寫法 
let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child v-model="message"></child>' +
  '<p>Message is: {{ message }}</p>' +
  '</div>',
  data() {
    return {
      message: ''
    }
  },
  components: {
    Child
  }
})
//父組件語法糖寫法
let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child :value="message" @input="message=arguments[0]"></child>' +
  '<p>Message is: {{ message }}</p>' +
  '</div>',
  data() {
    return {
      message: ''
    }
  },
  components: {
    Child
  }
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末揖盘,一起剝皮案震驚了整個(gè)濱河市眉厨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兽狭,老刑警劉巖憾股,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異箕慧,居然都是意外死亡服球,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門颠焦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斩熊,“玉大人,你說我怎么就攤上這事伐庭》矍” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵圾另,是天一觀的道長(zhǎng)霸株。 經(jīng)常有香客問我,道長(zhǎng)盯捌,這世上最難降的妖魔是什么淳衙? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮饺著,結(jié)果婚禮上箫攀,老公的妹妹穿的比我還像新娘。我一直安慰自己幼衰,他們只是感情好靴跛,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渡嚣,像睡著了一般梢睛。 火紅的嫁衣襯著肌膚如雪肥印。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天绝葡,我揣著相機(jī)與錄音深碱,去河邊找鬼。 笑死藏畅,一個(gè)胖子當(dāng)著我的面吹牛敷硅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播愉阎,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼绞蹦,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了榜旦?” 一聲冷哼從身側(cè)響起幽七,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎溅呢,沒想到半個(gè)月后澡屡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咐旧,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年挪蹭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片休偶。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖辜羊,靈堂內(nèi)的尸體忽然破棺而出踏兜,到底是詐尸還是另有隱情,我是刑警寧澤八秃,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布碱妆,位于F島的核電站,受9級(jí)特大地震影響昔驱,放射性物質(zhì)發(fā)生泄漏疹尾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一骤肛、第九天 我趴在偏房一處隱蔽的房頂上張望纳本。 院中可真熱鬧,春花似錦腋颠、人聲如沸繁成。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)巾腕。三九已至面睛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尊搬,已是汗流浹背叁鉴。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留佛寿,地道東北人幌墓。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像狗准,于是被迫代替她去往敵國(guó)和親克锣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • #1 Commit ID: 871ed9126639c9128c18bb2f19e6afd42c0c5ad9 La...
    DogRod閱讀 211評(píng)論 0 0
  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom閱讀 2,693評(píng)論 0 3
  • # 傳智播客vue 學(xué)習(xí)## 1. 什么是 Vue.js* Vue 開發(fā)手機(jī) APP 需要借助于 Weex* Vu...
    再見天才閱讀 3,525評(píng)論 0 6
  • 氣候?qū)χ性醭c游牧民族關(guān)系的影響——以漢朝為中心腔长。 氣候與國(guó)家有著密不可分的關(guān)系袭祟,從一副氣象圖上可以得知:氣候較...
    荊瑤閱讀 304評(píng)論 0 1
  • 今天是很有意義的一天,下午到客戶家收住院資料理賠捞附,晚上區(qū)上給到的個(gè)人榮譽(yù)晚宴巾乳,邀約了部分家人來見證和捧場(chǎng),很幸福鸟召,...
    卓彤的美好時(shí)光閱讀 55評(píng)論 0 0