问题和知识点集合
1. 问题:为什么要一个数据一个dep
这会造成,这个组件里面的所有的dep收集的都是同一个渲染watcher,既然都是一个watcher,那创建那么多dep干甚?
答:平时用户会写很多watcher,computed,当数据变化之后,调用的是用户写的回调函数,只有每个数据都对应一个dep,才能精准收集每个数据对应的回调函数。如此细粒度的dep是为了用户watcher准备的,不是给渲染watcher用的。
2. 再问依赖收集什么时候被收集到,和编译环节有关系吗
答:和编译环节没有关系,编译环节生成render函数。是在执行render函数生成虚拟dom阶段,会访问到具体的值。拿到了这些值,才知道vnode长什么样。
注意:和diff算法一点关系都没有,因为render函数执行的时候,根本还没有进入到diff环节
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
例子:
html
<div id="demo">
<div>{{currentBranch}}</div>
</div>
render函数
(function anonymous() {
with(this){
return _c(
'div',
{
attrs:{
"id":"demo"
}
},
[
_c(
'div',
[
_v(
_s(currentBranch)
)
]
)
]
)
}
})
在执行_s(currentBranch)语句过程中,在这个_s函数执行之前,先会访问所传参数currentBranch,此时就触发了get函数。不是一定要函数里面用到这个参数,才会访问,只要传了,就等于访问了一次this.currentBranch。
构建组件实例 =》
new Watcher =》
第一次patch =》
批量创建_update(_render()) =》
_s(currentBranch) =》 // 在即将执行之前,会先访问参数,然后再执行函数
访问值触发get =》
dep开始做依赖收集
3. 为什么vue中的render函数用with语句
1. 参考尤雨溪自己的回答
总结:用with可以执行作用域,使得生成的render函数的代码量大量减少
2. with语句用法
var a = new Function(`
with(this){
console.log(location.href)
}
`)
a()
//file:///D:/MyDocument/%E6%A1%8C%E9%9D%A2/project/%E9%BB%84%E6%AF%85vue%E6%BA%90%E7%A0%81/vue-dev/examples/commits/index.html
剩余问题点
1. slot原理,重要
2. keep-alive原理,重要
模板是如何变成渲染函数render的,三个步骤
1. 解析:模板转化为对象AST(抽象语法树)
AST:用对象的形式描述我们将要生成的js代码
2. 优化:标记静态节点,diff时候直接跳过,节约性能
3. 生成:代码生成,转化ast为代码的字符串。
将来怎么转化为真正函数呢?new Function('代码字符串'),new一个Function,里面传入字符串就行了
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1. 解析:模板转化为对象AST
const ast = parse(template.trim(), options)
// 2. 优化:标记静态节点
if (options.optimize !== false) {
optimize(ast, options)
}
// 代码生成
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
例子
html
<div id="demo">
<div>{{currentBranch}}</div>
</div>
render函数
根元素一个div,有属性id,子元素共一个,div,子元素有一个孙子元素,一个文本节点。
执行这个render函数,则执行里面的with语句,则有执行with里面的return里面的函数,这些是具体的生成虚拟dom的函数,最终return出去的,是一份完整的虚拟dom,就是一份jjs对象。
这个返回的render函数是已经new Function过之后的,已经是一个真正的函数,不是字符串,字符串是with语句的那段。
(function anonymous() {
with(this){
return _c(
'div',
{
attrs:{
"id":"demo"
}
},
[
_c(
'div',
[
_v(
_s(currentBranch)
)
]
)
]
)
}
})
1. 解析,parse
文件地址:src\compiler\parser\index.js
compileToFunctions =》
createCompiler =》
createCompilerCreator =》 解析,优化,代码生成都在这个函数
parse =》
parseHTML:得到模板字符串,
用正则解析:
解析HTML的方法类似于一个栈的方式,对应开始标签和结束标签,不断地入栈出栈。
只要遇到开始标签,就执行start函数,遇到结束就是end,遇到文本就chars,遇到注释就是comment。整个过程是一个递归过程。
每次遇到一个新的开始标签,就新创建一个ast对象
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
start (tag, attrs, unary) {
//
},
end () {
//
},
chars (text: string) {
//
},
comment (text: string) {
//
}
})
return root
}
关键指令解析(重点看)
文件地址:src\compiler\parser\index.js
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element)
processIf(element)
processOnce(element)
// element-scope stuff
processElement(element, options)
}
2. 优化,optimize
主要就是标记静态节点,标记静态根节点。被标记静态节点的vnode会带有static:true,staticRoot:true这两个属性
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
3. 生成代码,generate
重点看几个重要语句都生成啥样的render函数,v-if,v-for,v-once等
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
return code
}
}
真正生成代码字符串的函数:genElement,以下是几个重要的生成代码的点。
1. v-if
通过代码可以看出,最终生成了一个三元表达式的字符串。
核心代码
if (condition.exp) {
return `(${condition.exp})?${
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
return `${genTernaryExp(condition.block)}`
}
模板
<div v-if="test === 1">333</div>
最终生成的代码字符串,是一个三元表达式
(test === 1)?_c('div',[_v("333")]):_e()
2. v-for
从一下源码和生成的代码可以看出,最终生成了_l函数,在render执行的时候,就会在with语句里面执行this._l最终生成vnode。在这个_l函数里面,还藏了很多别的函数,比如生成div就要有_c函数,主要是看for循环里面要做什么
核心源码
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
模板
<div v-for="(item, index) in arr" :key="item.key">{{item.value}}</div>
arr: [
{
key: 11,
value: '11'
},
{
key: 22,
value: '22'
}
],
生成的代码字符串
_l(
(arr),
function(item,index){return _c('div',{key:item.key},[_v(_s(item.value))])}
)
3. v-once
由例子可以看出,由于只渲染一次,在没有别的指令情况下,会执行生成静态元素的函数genStatic。
核心代码
// v-once
function genOnce (el, state) {
el.onceProcessed = true;
if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.staticInFor) {
var key = '';
var parent = el.parent;
while (parent) {
if (parent.for) {
key = parent.key;
break
}
parent = parent.parent;
}
if (!key) {
state.warn(
"v-once can only be used inside v-for that is keyed. ",
el.rawAttrsMap['v-once']
);
return genElement(el, state)
}
return ("_o(" + (genElement(el, state)) + "," + (state.onceId++) + "," + key + ")")
} else {
return genStatic(el, state)
}
}
模板
<div v-once>{{num1}}</div>
生成代码字符串
_m(0)
4. 静态节点
静态节点调用的函数是genStatic,必须是两层嵌套起步才会被判定为静态节点,这个做应该是为了平衡内存和性能之间的取舍。
核心代码
function genStatic (el, state) {
el.staticProcessed = true;
// Some elements (templates) need to behave differently inside of a v-pre
// node. All pre nodes are static roots, so we can use this as a location to
// wrap a state change and reset it upon exiting the pre node.
var originalPreState = state.pre;
if (el.pre) {
state.pre = el.pre;
}
state.staticRenderFns.push(("with(this){return " + (genElement(el, state)) + "}"));
state.pre = originalPreState;
return ("_m(" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
}
模板
<div>
<div>645465645</div>
</div>
生成代码字符串
_m(1)
事件的处理
在编译生成代码字符串阶段,事件是被当做el.属性处理,在genData函数内,此函数处理了很多属性,如原生事件,v-model事件,其他各种属性等
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:{${genProps(el.attrs)}},`
}
// DOM props
if (el.props) {
data += `domProps:{${genProps(el.props)}},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
1. 原生事件
原生事件最终编译得到的代码字符串如下
<div @click=a>{{num1}}</div>
{on:{"click":a}}
在mount阶段,执行render函数后,变成vnode的结果,在data里面有on对象
在patch阶段,vnode中的on对象,最终转化为真正的事件挂载到相应的dom上
步骤:
vm._update(vm._render(), hydrating); =》
patch =》
createElm =》
invodeCreateHooks =》
updateDOMListeners
invodeCreateHooks是在组件创建的过程中,创建元素的过程中执行的,如果当前元素有属性就会执行。不仅仅是包括有事件。
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
updateDOMListeners函数最终把事件挂载到dom上
function add (event, fn) {
target.$on(event, fn);
}
// 添加原生事件
function add (
event: string,
handler: Function,
once: boolean,
capture: boolean,
passive: boolean
) {
handler = withMacroTask(handler)
if (once) handler = createOnceHandler(handler, event, capture)
target.addEventListener(
event,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
2. 自定义事件
步骤
patch =》
createComponent(创建组件) =》
hook.init(属性处理)=》
createComponentInstanceForVnode(创建组件实例) =》
init =》
initEvent =》
updateComponentListeners
事件的派发和监听者都是组件实例,自定义组件中一定伴随着原生事件的监听和处理
3. 双向绑定事件
编译阶段,编译后的结果:
1)有一个on监听事件,默认监听的是input事件,可以修改。
2)有一个domProps事件,这是添加属性事件,会给dom元素的value属性,值是inputValue的值。
<input type="text" v-model="inputValue">
{
directives:[{name:"model",rawName:"v-model",value:(inputValue),expression:"inputValue"}],
attrs:{"type":"text"},
domProps:{"value":(inputValue)},
on:{"input":function($event){if($event.target.composing)return;inputValue=$event.target.value}}
}
3)为什么还要有一个指令directives,是干什么用的。
在编译阶段用的,src\platforms\web\compiler\directives\model.js
编译阶段,针对不同的元素,如input,selectcheckbox,radio,textarea会生成不同的代码,这个指令就是在这个时候用到的
生成vnode后的结果
最后转化在真实dom身上,就是有一个原生input事件,还有inputValue值被依赖收集了,变成了响应式。
4. 组件双向绑定事件v-model
编译后代码字符串
patch过程中的处理,在createComponent函数内
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
初始化阶段,对节点赋值以及事件监听
对节点赋值:src\platforms\web\runtime\modules\dom-props.js
事件监听:src\platforms\web\runtime\modules\events.js
额外的model指令:src\platforms\web\compiler\directives\model.js
5. 事件,彩蛋(@hook:name)
在Vue当中,hooks可以作为一种event,在Vue源码当中,称之为hookEvent。
<el-table @hook:created="handleTableCreated" />
应用场景举例:有一个来自第三方的复杂表格组件,表格进行数据更新的时候渲染时间需要一秒,由于渲染时间较长,为了更好的用户体验,我们希望在表格进行更新的时候显示一个loading动画,修改源码这个方法不优雅,于是可以用hookEvent。
原理:
在源码中,如果有hookEvent,则会额外派发一个事件出去,事件名称写死是hook:开头的。
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook] // 这是一个数组
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}