传送门vue技术揭秘:https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/new-vue.html
一、主要步骤
1.初始化
- vue初始化init的过程包含生命周期、事件、props、methods、data、computed与watch等的初始化
其中最主要的两个步骤是watch的初始化和data属性的observer过程,两个过程是实现响应式和依赖收集
2.编译 - 编译是将template转变为render function 的过程,包括:解析/优化/生成三个步骤
解析:template->AST(抽象语法树)
优化:标记AST中的静态(static)节点
生成:AST->render function
3.render function 执行 - render function 执行后生成虚拟节点树(VNode DOM Tree)
4.渲染展现页面
二、依赖收集过程
整体的流程图中render function 执行开始的绿色箭头指向的流程为依赖收集过程
1.render function 执行中会依此调用使用到的data.attr的get方法
2.get方法调用Dep.add将Vue对象中的watch加入到attr.Dep数组里
3.整个页面渲染完毕后,所有需要使用attr的组件Vue对象的watch都收集到attr.Dep,attr.Dep内容即为template与data的依赖关系(attr是随便起的一个组件名)
三、响应式原理
整体流程图中attr.set()执行开始的红色箭头指向的流程为响应式原理
1.对data.attr赋值即调用attr.set方法
2.attr.set会调用Dep.notify(),notify方法是依次执行attr.Dep数组中watch对象的update方法
3.update()是重新渲染视图的过程,中间生成的Vnode DOM Tree,供patch使用 (利用diff算法)
四、update中的patch
patch,是将update产生的New Vnode节点与上一次渲染的Old Vnode进行对比的过程,最终只有对比后的差异节点才会被更新到视图上,从而达到提高update性能的目的
原文链接:https://www.cnblogs.com/zs-note/p/8675755.html
五、 new Vue 的操作
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue) //如果函数function Vue 被new了当前的this就是Vue类型
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
process在node中有全局变量表示的是当前的node进程。process.env包含着关于系统环境的信息。但是process.env中不存在NODE_ENV这个东西,是用户自定义的变量,在webpack中判断生产环境和开发环境的。
_init中主要做了合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化data、props、computed、watcher等等
页面使用
export default {
data(){
return {
name:'zmq'
}
},
mouted(){
console.log(this.name) //为什么data中得可以直接this得到,因为被挂载到了vm上,vue实例上。主要实现得方法是proxy(代理)
//proxy(target,sourceKey,key)
}
}
target 传递得是vm sourcKey传递得是_data,key就是name
获取得时候就是sharePropertyDefinition.get=function proxyGetter(){
return this [sourceKey][key]//this [_data][name]
}
总结:vue初始化逻辑,把不同功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然。
六、挂载的时候的操作
首先对el做了限制不能挂载在html和body上。如果没有定义render方法,则会把el或者template字符串转换成render方法;然后是调用vm.render方法生成虚拟node,然后实例化watcher,回调updateComponent方法,更新dom。。当挂载完成的时候会vm.isMounted=true 做标记,当前vm.$node(父虚拟node)为null。代表挂载完成
核心方法:vm.render 和vm.update
vm.update在修改数据得时候也会再次被调用
七、virtualDom和diff(vue实现)
虚拟dom就是真实dom的一层抽象,用属性描述真实dom的各个特性
例子:
<template>
<div id='dd'>
<p><span></span></p>
<p>abc</p>
<p>123</p>
</div>
</template>
var virtual=
{
dom:'div',
props:{
id:dd
},
children:[
{
dom:'p',
children:[
dom:'span',
children:[]
]
},
{
dom:'p',
children:[
]
},
{
dom:'p',
children:[
]
}
]
}
可以想象,最简单粗暴的方法就是将整个dom结构用innerHtml修改到页面上,但是这样进行重绘整个视图层是相当消耗性能的。每次只更新被修改的部分是不是性能高一些。所以vue.js将dom抽象成一个以javascript对象为节点的虚拟dom树,以vnode节点模拟真实dom,可以对这颗抽象树进行创建节点、删除节点一级修改节点等操作,在这个过程中都不需要操作真实dom,只需要操作javascript对象差异修改,这样大大提升了性能。修改以后在经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样减少了很多不需要的dom操作,大大提高了性能。
vue使用这种虚拟dom,对真实dom的一层抽象,不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至node平台也可以对这样一颗dom树进行创建删除修改等操作。这也是为前后端同构提供了可能。
vue修改视图
vue是通过双向绑定来修改视图,当某个数据被修改的时候,set方法会让闭包中的dep调用notify通知所有订阅者watcher,watcher通过get方法执行
vm._update(vm._render(),hydrating)
diff算法
diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式
两张图代表旧的和新的vnode进行patch(修补)的过程,他们只是在同层级的vnode之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的vnode节点),然后修改变化的视图,所以十分高校
_patch
当oldvnode和vnode在samevnode的时候才会进行patchvnode,也就是新旧节点判定为同一节点的时候才会进行patchvnode这个过程,否则就是创建新的dom,移除旧的dom
return function patch(oldVnode,vnode,hydrating,removeOnly,parentElm,refElm){
//vnode不存在的时候则直接调用销毁钩子(没有新节点,就把旧节点销毁掉)
if(isUndef(vnode)){
if(isDef(oldVnode)) invokeDestoryHook(oldVnode)
return
}
let isInitialPatch=false
const insertedVnodeQueue=[]
if(isUndef(oldVnode)){
//如果没有旧节点的时候就是没有root节点,就需要创建一个新的节点
isInitialPatch=true
createElm(vnode,insertedVnodeQueue,parentElm,refElm)
}else{
//查看当前是否又nodeType,也就子节点
const isRealElement=isDef(oldVnode.nodeType)
//当没有子节点并且是相同节点得时候,就直接修改现有的节点
if(!isRealElement&&sameVnode(oldVnode,vnodes)){
//当没有
patchVnode(oldVnode,vnode,insertedVnodeQueue,removeOnly)
}else{
//如果当前又子节点
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
/*当旧的VNode是服务端渲染的元素,hydrating记为true*/
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
/*需要合并到真实DOM上*/
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
/*调用insert钩子*/
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
/*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/
oldVnode = emptyNodeAt(oldVnode)
}
}
/*取代现有元素*/
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
if (isDef(vnode.parent)) {
// component root element replaced.
// update parent placeholder node element, recursively
/*组件根节点被替换,遍历更新父节点element*/
let ancestor = vnode.parent
while (ancestor) {
ancestor.elm = vnode.elm
ancestor = ancestor.parent
}
if (isPatchable(vnode)) {
/*调用create回调*/
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode.parent)
}
}
}
if (isDef(parentElm)) {
/*移除老节点*/
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
/*Github:https://github.com/answershuto*/
/*调用destroy钩子*/
invokeDestroyHook(oldVnode)
}
}
/*调用insert钩子*/
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
samenode
/**判断两个vnode节点是否是同一个节点,需要满足一下条件,key相同,
tag相同(当前节点的标签名)
isComment(是否为注释节点)相同
是否data都有定义(当前节点对应的对象,包含了具体的一些数据信息)是一个vnodeData类型,可以参考vnodeData类型中的数据信息)
参当标签<input>的时候,type必须相同
**/
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
/**
判断当标签是input的时候,type是否相同.
**/
八、render、createElement、patch
render可以通过手写,也可以通过编译生成
render最终执行得createElement方法返回得vnode
vue.js利用createElement方法创建Vnode
createElement实际上是对_createElement方法得封装,让传入得参数更加灵活,在处理这些参数得时候调用真正得创建vnode得函数_createElement
创建得时候会检测data,不能是响应式得
_createElement方法5个参数,
context 表示Vnode得上下文环境,他是component类型
tag表示标签,它可以是一个字符串,也可以是compenent,这个时候就会创建一个组件。
data表示Vnode得数据,他是一个VNodeData类型,可以在flow/vnode.js中找到他的定义,
children表示当前VNode得子节点,他是任意类型得,他接下来需要被规范为标准得VNode数组;
noramalizationType表示子节点规范得类型,类型不同规范得方法也就不一样,它主要是参考render函数是编译生成还是用户手写得
createElement函数得流程略微多,需要看的2个重点流程:children得规范化以及VNode得创建
children得规范化
用于Virtual DOM实际上是一个树状结构,每一个vnode可能会有若干个子节点,这些子节点应该也是vnode类型。_createElement接收得第四个children是任意类型,因此我们需要把它门规范成vnode类型。
这里会根据normalizationType得不同,调用了noramalizeChildren(children)和simpleNoramalizeChildren(children)方法
simpleNormalizeChildren方法调用场景是render是编译生成得。理论上编译生成得children都已经是vnode类型得,这里有一个例外就是,functional compenent 组件返回得是一个数组而不是一个根节点。所以会通过Array.prototype.concat方法把整个children数组打平,让它得深度只有一层。
normalizeChildren方法得调用场景有2种,一个 场景是render函数是用户手写得,当children只有一个节点得时候,vue.js从接口层面允许用户把children写成基础类型用来创建单个简单得文本节点,这种情况会调用createTextNode创建一个文本节点得vnode;另一个场景是编译slot、v-for得时候会产生嵌套数组得情况,会调用noramlizeArrayChildren方法。
noramlizeArrayChildren接收2个参数,children表示要规范得子节点,nestedIndex表示嵌套得索引,因为单个child可能一个数组类型。normalizeArrayChildren主要得逻辑就是遍历children,获得单个节点c,然后对c得类型判断,如果是一个数组类型,则递归调用normalizeArrayChildren;如果是基础类型,则通过createTextVNode方法转换成VNode类型,否则就已经是Vnode类型了。如果children是一个泪飙并且列表还存在嵌套得情况,则根据nestedIndex去更新他的key。这里需要注意一点,在遍历得过程中,对这3种情况都做了如下处理:如果存在两个连续得text节点,会把他们合并一个text节点。
经过children得规范化,children变成了一个类型为Vnode得Array
VNode创建
createElement创建VNode得时候会对tag做判断,
如果是string类型,则接着判断如果是内置得一些节点,则直接创建一个普通VNode,如果是为已注册得组件名,则通过createComponent创建一个组件类型得Vnode,否则创建一个位置标签VNode。
如果tag是一个Component类型,则直接调用createComponent创建一个组件类型得VNode节点。
总结
createElement 创建 VNode 的过程,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree。
归根节点是一个:const elm=docuemnt.createElement(tagName)
普通的patch和组件patch
普通的patch是对vnode(虚拟属性)的判断 ,新增子节点、删除子节点、新增节点,移除当前节点的操作。组件中的patch是发现组件,就去createComponent->子组件初始化-》子组件render->子组件patch
patch
组件中patch
patch的整体流程:createComponent->子组件初始化-》子组件render->子组件patch
activeInstance为当前激活的vm实例;vm.$vnode为组件的占位vnode;vm._vnode为组件的渲染vnode
嵌套组件的插入顺序是先子后父(递归)
九、update
update得有两次:1.首次渲染得时候 2. 当我们去改变数据(更新)得时候
_update得核心就是调用vm.patch方法。这个方法在不同得平台中定义是不一样得,例如web和weex
在web中,是否在服务端渲染会对这个方法产生影响。因为在服务端渲染中,没有真实dom环境,所以不需要把VNode最终转换成Dom,因此是一个空函数,而在浏览器端渲染中,它指向了patch方法
patch方法是createPatchFunction方法得返回值,这里传入一个对象,包含nodeOps参数和modules参数。其中,nodeOps封装了一系列Dom操作得方法,modules定义了一些模块得钩子函数得实现。
值得学习得地方:
因为patch是跟平台相关得得,在web和weex环境,他们把虚拟dom映射到“平台Dom”得方法是不相同得,并且对Dom包括得属性模块创建和更新也尽不相同。因此每个平台都是各自得nodeOps和modules。而不同平台得主要逻辑是相同得。所以他们把公共得托管在core中。
差异化部分只需要通过参数来区别,这里用到了一个函数柯里化得技巧,通过createPatchFunction把差异化参数提前固定化,这样不用每次调用patch得时候都传递nodeOps和modules了。
整个过程就是new Vue 然后初始化init 事件,钩子函数,data。watcher/等等,然后就是挂载,编译成render函数生成vnode,在通过patch,insert插入dom元素变成真实dom。
十、createComponent
构建子类构造函数、安装组件钩子函数、实例化VNode,然后最后通过patch把VNode转换成真正的Dom节点。
组件的创建最终执行的是insert(parentElm,vnode.elm,refElm),插入顺序是先子后父,因为里面有一个递归,如果碰到组件就去创建,初始化,渲染。
十一、合并配置
不同的key,合并配置不同
钩子函数的合并配置策略
父组件和子组件中的钩子函数都是一样的。如果有子组件,就将父子组件相同钩子函数合并在一起,最后返回的是一个数组
三元运算符:看当前有没有子元素,如果没有就返回父元素,如果有子元素就把父元素和子元素合并,如果没有父元素,就查看当前子元素是不是一个数组,如果不是就变成数组,如果是就直接返回子元素数组
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
})
例如:都定义了created()
vm.$options = {
parent: Vue /*父Vue实例*/,
propsData: undefined,
_componentTag: undefined,
_parentVnode: VNode /*父VNode实例*/,
_renderChildren:undefined,
__proto__: {
components: { },
directives: { },
filters: { },
_base: function Vue(options) {
//...
},
_Ctor: {},
created: [
function created() {
console.log('parent created')
}, function created() {
console.log('child created')
}
],
mounted: [
function mounted() {
console.log('child mounted')
}
],
data() {
return {
msg: 'Hello Vue'
}
},
template: '<div>{{msg}}</div>'
}
}
合并策略:
他们的合并策略都是 mergeHook 函数。这个函数的实现也非常有意思,用了一个多层 3 元运算符,逻辑就是如果不存在 childVal ,就返回 parentVal;否则再判断是否存在 parentVal,如果存在就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。所以回到 mergeOptions 函数,一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
十二、组件注册
全局注册:
要注册一个全局组件,可以使用vue.component(tabName,options)
Vue.component('my-component',{
//选项
})
命名规范取值的判断:
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
逻辑分析:先通过const assets=options[type]拿到assets,然后在尝试assets[id],这里有一个顺序,先直接使用id拿,如果不存在,则把id变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母在变成大写的形式再拿,如果仍然拿不到就报错。这样就说明了我们在使用Vue.compoent(id,definition)全局注册组件的时候,id可以是连字符、驼峰或首字母大写的形式
局部注册
import HelloWorld from './components/HelloWord';
export default{
components:{
HelloWorld
}
}
全局组件注册是扩展到Vue.options下,所以在所有组件创建的过程中,都会从全局的Vue.options.componets扩展到当前组件的vm.$options.components下,这就是全局注册的组件能被任意使用的原因。
异步组件(工厂函数)
普通函数异步组件
里面又有个方法是
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update()
}
}
就是调用渲染watcher得update方法,让渲染watcher对应得回调函数执行,也就是触发了组件得重新渲染。之所以这么做是因为vue通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化得,所以通过执行$forceUpdate可以强制组件重新渲染一次。
异步组件得3种实现方式
普通函数异步组件(工厂)
export function once (fn: Function): Function {
let called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
once逻辑非常简单,传入一个函数,并返回一个新函数,它非常巧妙得利用闭包和一个标志保证了它包装得函数只会执行一次。(技巧值得学习)
Promise异步组件
配合得是webpack +import 得语法
高级异步组件
高级异步组件非常得巧妙,它可以通过简单得配置实现了loading、resolve、reject、timeout 4种状态
const AsyncComp = () => ({
// 需要加载的组件。应当是一个 Promise
component: import('./MyComp.vue'),
// 加载中应当渲染的组件
loading: LoadingComp,
// 出错时渲染的组件
error: ErrorComp,
// 渲染加载中组件前的等待时间。默认:200ms。
delay: 200,
// 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
timeout: 3000
})
Vue.component('async-example', AsyncComp)
优点:
- 异步组件可以减少包得大小,一个性能优化得技巧
- 按需加载组件
深入响应式原理
前端有两个工作:
- 一把数据渲染到页面
- 另一个就是处理用户交互
响应式对象
响应式对象核心:Object.defineProperty给数据添加了getter和setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑,getter做的事情就是依赖收集,setter做的事情就是派发更新。
依赖收集
依赖收集的目的是为了当这些响应式数据发生变化,触发他们的setter的时候,能知道应该通知哪些订阅者去做相应的逻辑处理,我们把这个过程叫派发更新,watcher和Dep是一个非常经典的观察者设计模式