注: 路人讀者請(qǐng)移步 => Huang Yi 老師的Vue源碼閱讀點(diǎn)這里, 我寫這一篇筆記更傾向于以自問自答的形式增加一些自己的理解 , 內(nèi)容包含面試題范圍但超出更多.
自己提出的問題自己解決:
-
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)
fromimport { 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'
而初始值是在initMixin
的initData
方法中通過
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ā)序列化的有如下幾種情況:
-
functional component
函數(shù)式組件返回的是一個(gè)數(shù)組而不是一個(gè)根節(jié)點(diǎn)時(shí), 會(huì)調(diào)用simpleNormalizeChildren
, 通過Array.prototype.concat
方法把整個(gè) children 數(shù)組打平膘盖,讓它的深度只有一層 -
render 函數(shù)
是用戶手寫時(shí),當(dāng) children 由基礎(chǔ)類型組成時(shí),Vue會(huì)調(diào)用normalizeChildren
中的createTextVNode
創(chuàng)建一個(gè)文本節(jié)點(diǎn)的 VNode衔憨; - 當(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),如下圖)
} 至此 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 是一個(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
}
})
- 模塊系統(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 watcher
的newDeps
和newDepIds
中, 同時(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.$nextTick
或Vue.nextTick
, 二者完全一致
-
this.$nextTick(fn)
會(huì)將 fn 加入 callbacks 隊(duì)列 -
this.$nextTick()
會(huì)生成一個(gè)狀態(tài)為resolved
的Promise 對(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 中在
macro
和micro
二選一時(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è):
- 在
initState
函數(shù)中發(fā)現(xiàn)$options
中有 computed 屬性, 則調(diào)用 initComputed 函數(shù) - 在 vm 上掛載
vm._computedWatchers
屬性,初始化為{ }
, 該屬性是為計(jì)算屬性專屬, 如果用戶options
中沒有計(jì)算屬性, 則它不會(huì)出現(xiàn) - 遍歷
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ì)列了.
- 在該函數(shù)中, 會(huì)調(diào)用
- 在渲染 watcher 的上下文環(huán)境中要做依賴收集
//若是控制臺(tái)無聊輸出 vm.name 則不需要
接下來看看this.firstName
的變動(dòng)是如何讓name
的dirty
變化的fase => true
4.4.3 data 變動(dòng)引起 computed 值變動(dòng)的過程
- 首先,
this.useless
會(huì)變?yōu)?code>true, 會(huì) dep.notify( )name
watcher 和DOM
watcher
- update
name
watcher 的過程就是設(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ì)列.
- nextTick 清空 callBacks 隊(duì)列 => 清空 flushSchedulerQueue 隊(duì)列
在這個(gè)過程中, dom 渲染 watcher.run()時(shí), 會(huì)重新收集依賴.
4.4.4 如果 data 變了但是 computed 不變會(huì)怎么辦?
關(guān)于這件事的詳細(xì)討論可以參考
- github 博客 深入淺出 - vue變化偵測(cè)原理
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==1
且node.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ì)于if
和for
做了特殊的處理,下面看一下對(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ì)微差別:
我沒處理
{ on: { "click": function($event) { clickItem(index) } }
這種結(jié)構(gòu)我沒處理
node.staticRoot
這些靜態(tài)根節(jié)點(diǎn)我沒處理組件
-
對(duì)于
v-for
和v-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
, isAllowed
為false
, 因此系統(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
}
})