1.请简述Vue首次渲染过程
1.实例创建完成后,调用$mount()方法
完整版中会先调用src/platforms/web/entry-runtime-with-compiler.js中重写的$mount()(即进行模板编译),其中:
- 先判断
options中是否有render,如果没有传递render,调用compileToFunctions(),生成render()函数 - 然后设置
options.render = render
然后调用原来的$mount()(在src/platform/web/runtime/index.js中定义),其中会调用mountComponent(this, el, hydrating)
2.mountComponent(this, el, hydrating)
- 触发
beforeMount:callHook(vm, ‘beforeMount’)` - 定义函数
updateComponent = () => { vm._update(vm._render(), hydrating) }vm._render()生成虚拟DOM(vm._render()定义在src/core/instance/render.js中)vm._update()将虚拟DOM转换成真是DOM(vm._update()定义在src/core/instance/lifecycle.js中)
- 创建
Watcher实例,把updateComponent传递进去,updateComponent是在Watcher中通过Watcher的get()实例方法执行的 - 触发
mounted;返回实例return vm
3.watcher.get()
首次渲染创建的是渲染Watcher,创建完Watcher实例后会调用一个get()方法,get()中会调用updateComponent(),updateComponent()中会调用vm._update(VNode. hydrating),而其中的VNode是调用vm._render()创建VNode
vm._render()的执行过程:
- 获取创建实例时存放在
options中的render函数:const { render, _parentVnode } = vm.$options - 调用
render.call(vm.renderProxy, vm.$createElement)(这个render是创建实例new Vue()时传入的render(),或者是编译template生成的render()),最后返回VNode
然后执行vm._update(VNode, hydrating),其中:
- 调用
vm.__patch__(vm.$el, vnode)挂载真实DOM - 将
vm.__patch__的返回值记录在vm.$el中
2.请简述 Vue 响应式原理
Vue使用观察者模式来对其数据进行响应式处理,过程如下:
创建观察者
在创建
Vue实例时,调用的this._init(options)中会执行initInjections(vm)、initState(vm),这两个方法中分别会对inject的成员、本实例的props和data进行响应式处理initInjections(vm)中会遍历inject的成员,通过defineReactive(vm, key, result[key])将每个成员转换成响应式属性(即劫持getter/setter)initState(vm)中调用initProps(vm, opts.props),其中编辑props属性,通过defineReactive(props, key, value)将属性转换成getter、setter,然后存入props(也是vm._props)中initState(vm)中调用initData(vm, opts.props),其中调用observe(data, true /* asRootData */)对data进行响应式处理defineReactive(obj, key, val, customSetter?, shallow?)- 会将传入的
key换成响应式属性,即其劫持getter/setter(Object.defineProperty( obj, key, { ..., get(){...}, set(){...} } )) - 为每个属性
key生成一个Dep对象dep(const dep = new Dep());dep会在getter中收集依赖(即相应属性的Watcher对象),在setter中调用dep.notify()派发更新; - 在需要递归观察子对象时,会调用
observe(val)(let childOb = !shallow && observe(val)),若val是个对象,则会为这个对象创建一个Observer对象,并返回。
- 会将传入的
observe (value, asRootData?)- 判断
value是否是对象,不是对象就返回 - 是对象,则这个对象可称之为 观测对象 ,然后为这个对象创建一个
Observer对象 :ob = new Observer(value),并返回ob(在defineReactive中,这个返回的ob会在getter中收集依赖相应的依赖) Observer构造函数中,会新建一个Dep对象dep,这里的dep是为传入的 观测对象(进行响应式处理的对象)收集依赖(Watcher);与defineReactive中的dep不一样- 将
Observer实例挂载到 观测对象 的__ob__属性:def(value, '__ob__', this) - 若
value不是数组,则执行this.walk(value)方法,遍历value中的每一个属性,然后调用defineReactive(obj, keys[i]) - 若
value是数组Vue并没有对数组对象的索引调用defineReactive来生成getter/setter,而是重写了原生数组中会更改原数组的方法,调用这些新方法后,数组对象对应的dep对象会调用 dep.notify 方法来驱动视图的更新- 重写的数组方法:
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' - 重写数组方法后,调用
this.observeArray(value),作用是当数组中的元素存在对象时,为数组中的每一个对象创建一个observer实例
- 判断
至此,创建观察者结束
依赖的收集
- 在进行挂载时( 调用
$mount()),会执行mountComponent方法,其中会创建一个渲染Watcher对象 - 渲染
Watcher对象构造函数的最后会执行get()方法,get()中会先执行pushTarget(this),pushTarget中则会将 Dep.target 设置为该watcher(Dep.target = target) - 然后调用
this.getter.call(vm, vm),即执行了vm._update(vm._render(), hydrating),而vm._render()中执行render.call(vm._renderProxy, vm.$createElement)以生成vnode,这个生成VNode的过程中,会触发 相应的响应式数据的getter,然后其中的dep.depend()则会收集当前实例watcher
当生成完 VNode 后,就完成了响应式数据的的依赖收集
通知的发送
但修改某个响应式数据时,会触发该数据的 setter
- 如果新值是对象,且需要递归观察子对象时执行
childOb = !shallow && observe(newVal),将新增也进行响应式处理 - 调用
dep.notify()派发更新,notify会调用每个订阅者(watcher)的update方法实现更新 watcher的update中使用queueWatcher()判断watcher是否被处理,若没有,则把watcher添加进queue队列中,并调用flushSchedulerQueue()flushSchedulerQueue()中先触发beforeUpdate钩子函数,然后调用watcher.run()watcher.run()中会调用get()方法,get中执行getter,而getter就是传入的updateComponent方法,updateComponent中执行vm._update(vm._render(), hydrating),如此就完成了视图的更新- 然后
flushSchedulerQueue()后续代码中 还原更新步骤的初始状态、触发actived钩子函数、触发updated钩子函数
3.请简述虚拟 DOM 中 Key 的作用和好处
Key 是用来优化 Diff 算法的。Diff算法核心在于同层次节点比较,Key 就是用于在比较同层次新、旧节点时,判断其是否相同。
Key 一般用于生成一列同类型节点时使用,这种情况下,当修改这些同类型节点的某个内容、变更位置、删除、添加等时,此时界面需要更新视图,Vue 会调用 patch 方法通过对比新、旧节点的变化来更新视图。其从根节点开始若新、旧 VNode 相同,则调用 patchVnode
patchVnode 中若新节点没有文本,且新节点和旧节点都有有子节点,则需对子节点进行 Diff 操作,即调用 updateChildren,Key 就在 updateChildren 起了大作用
updateChildren 中会遍历对比上步中的新、旧节点的子节点,并按 Diff 算法通过 sameVnode 来判断要对比的节点是否相同
- 若这里的子节点未设置
Key,则此时的每个新、旧子节点在执行sameVnode时会判定相同,然后再次执行一次patchVnode来对比这些子节点的子节点 - 若设置了
Key,当执行sameVnode- 若
Key不同sameVnode返回false,然后执行后续判断; - 若
Key相同sameVnode返回true,然后再执行 patchVnode 来对比这些子节点的子节点
- 若
即,使用了 Key 后,可以优化新、旧节点的对比判断,减少了遍历子节点的层次,少使用很多次 patchVnode
4.请简述 Vue 中模板编译的过程
Vue 模板编译入口文件执行过程
在完整版 Vue 中,src/platforms/web/entry-runtime-with-compiler.js 里先保留 Vue 实例的 mount方法,然后重写该mount方法,然后重写该mount 方法,这个重写的方法就是完整版 Vue 中的模板编译器,其中在 $options 上挂载了模板编译后生成的 render 函数。
$options 上的 render 函数是由 compileToFunctions(template, options, vm) 这个函数生成,即将 template 转换成了 render 函数。所以这里就是完成了首次加载时对模板的编译。
这里梳理下生成 render 函数的相关函数的调用过程
第一步
调用 compileToFunctions(template, options, vm)
// src/platforms/web/entry-runtime-with-compiler.js
// 把 template 转换成 render 函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
第二步
compileToFunctions 是由 src/platforms/web/compiler/index.js 里的 createCompiler(baseOptions) 生成的。baseOptions 里是一些关于指令、模块、HTML标签相关的方法,这里不予关心。
所以 第一步 中 compileToFunctions 是这里的 createCompiler 返回的函数。
// src/platforms/web/compiler/index.js
import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
第三步
createCompiler 来自于 src/compiler/index.js,其中调用了 createCompilerCreator(function baseCompile (template, options)) 方法
所以 第二步 中的 createCompiler 来自于这里的 createCompilerCreator 返回的函数,createCompilerCreator 中传入 函数 baseCompile 作为参数
那么 第一步 中的 compileToFunctions 就是这里的 createCompilerCreator 返回的函数执行(即执行 createCompiler(baseOptions))后返回的函数
// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 把模板转换成 ast 抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化抽象语法树
optimize(ast, options)
}
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options)
return {
ast,
// 渲染函数
render: code.render,
// 静态渲染函数,生成静态 VNode 树
staticRenderFns: code.staticRenderFns
}
})
第四步
createCompilerCreator 来自于 src/compiler/create-compiler.js
// src/compiler/create-compiler.js
export function createCompilerCreator (baseCompile: Function): Function {
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions
): CompiledResult {
const finalOptions = Object.create(baseOptions)
....
const compiled = baseCompile(template.trim(), finalOptions)
...
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
可以看出这个函数返回了 createCompiler(baseOptions) 函数,则往上推可知 第二步 中的
// src/platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)
其实就是执行的这里的 function createCompiler (baseOptions: CompilerOptions){...},这个 createCompiler 函数里返回了方法 compile、compileToFunctions
方法 compile 中执行了传入的函数参数 baseCompile,这个 baseCompile 是 第三步 中传入的,其返回值为 ast、render、staticRenderFns
而方法 compileToFunctions 正是 第一步 中调用的 compileToFunctions(template, options, vm),其来自于 createCompileToFunctionFn(compile)
第五步
createCompileToFunctionFn 来自于 src/compiler/to-function.js
// src/compiler/to-function.js
export function createCompileToFunctionFn (compile: Function): Function {
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
...
// 1. 读取缓存中的 CompiledFunctionResult 对象,如果有直接返回
const key = options.delimiters
? String(options.delimiters) + template
: template
if (cache[key]) {
return cache[key]
}
// 2. 把模板编译为编译对象(render, staticRenderFns),字符串形式的js代码
const compiled = compile(template, options)
// 3. 把字符串形式的js代码转换成js方法
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// 4. 缓存并返回res对象(render, staticRenderFns方法)
return (cache[key] = res)
}
}
createCompileToFunctionFn 返回函数 compileToFunctions,即 第四步 中 createCompiler 函数返回的 compileToFunctions,所以是 第一步 中调用的 compileToFunctions 就是在执行这里的 compileToFunctions。
总结过程
- 执行
src/platforms/web/entry-runtime-with-compiler.js中的compileToFunctions(template, options, vm) - 执行
src/compiler/to-function.js中的compileToFunctions,compileToFunctions中调用compile(template, options) - 执行
src/compiler/create-compiler.js中的compile,compile中调用baseCompile(template.trim(), finalOptions) - 执行
src/compiler/index.js传入createCompilerCreator中的函数参数 baseCompile(template, options),返回ast、render、staticRenderFns src/compiler/create-compiler.js中const compiled = { ast、render、staticRenderFns },返回compiledsrc/compiler/to-function.js中返回res,即返回render, staticRenderFns方法src/platforms/web/entry-runtime-with-compiler.js获取render、staticRenderFns