补充
事件
改一下例子
main.js
1 | import Vue from 'vue' |
App.vue
1 | <template> |
从var code = ast ? genElement(ast, state) : '_c("div")';开始,先看生成的ast,进入genElement方法

1 | function generate ( |
进入genElement
1 | function genElement (el, state) { |
进入genChildren
1 | function genChildren ( |
这里遍历子节点数组,进入gen方法
1 | function genNode (node, state) { |
又进到genElement

这次走到了genData$2
1 | function genData$2 (el, state) { |
1)声明data初始值为{
2)这两个逻辑中,为data拼接事件
1 | if (el.events) { |
1 | "{on:{"select":selectHandler},nativeOn:{"click":function($event){return clickHandler($event)}}," |
3)最后去掉尾巴的逗号
所以最后得到的render方法是这样的
1 | "with(this){return _c('div',[_c('App',{on:{"select":selectHandler},nativeOn:{"click":function($event){return clickHandler($event)}}})],1)}" |
看一下生成vnode调用render方法的时候,发现App是个component,不是普通的HTML标签,所以创建组件vnode,下面是创建组件vnode的过程:


变量lsteners保存了data.on,并将data.on指向data.nativeOn,然后为组件安装声明周期钩子函数

之后创建组件vnode,这就是生成的组件vnode

进入update方法,首先是创建div占位符,然后遍历children,创建子节点,就看一下创建组件App的过程,在createComponent的时候,会调用子组件的init钩子方法

最后调用child.$mount挂载子组件,在挂载子组件过程中,会先生成子组件vnode,之后再挂载。
挂载之前会为元素添加事件了

进入invokeCreateHooks方法
1 | function invokeCreateHooks (vnode, insertedVnodeQueue) { |

进入updateDOMListeners方法
1 | function updateDOMListeners (oldVnode, vnode) { |

进入updateListeners方法
1 | function updateListeners ( |

进入add方法
1 | function add$1 ( |
在这里为元素添加事件
点击一下触发点击事件

走进original.apply(this,arguments)
1 | function invoker () { |
走进invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
1 | function invokeWithErrorHandling ( |
走进handler.apply(context, args)

触发App组件的click事件,不过触发之前,先回从_vm上取clickHandler,所以走到了get

这里补充一下initMethods方法吧,在初始化调用initState的时候会走进initMethods方法
1 | function initMethods (vm, methods) { |
这个方法,其实就是在vm上添加属性方法(bind(methods[key],vm)返回是个绑定了this方法)
1 | function nativeBind (fn, ctx) { |
继续说触发了App组件click方法

先打印出Button clicked,xxxxxx,之后调用this.$emit
1 | Vue.prototype.$emit = function (event) { |
获取vm._events[event],然后遍历它,调用每一项的方法,那么vm._events在什么时候加的呢?在initEvents(vm);的时候
1 | function initEvents (vm) { |
接着就是调用selectHandler方法

然后冒泡,触发

双向绑定原理
改一下例子
main.js
1 | import Vue from 'vue' |
从var code = generate(ast, options);开始
1 | var createCompiler = createCompilerCreator(function baseCompile ( |
一直往下走,这是调用栈

当el是input的时候

走进genData$2

1 | function genData$2 (el, state) { |
走进var dirs = genDirectives(el, state);
1 | function genDirectives (el, state) { |
在这里遍历了el.directives数组,获取指令对应的gen,走进!!gen(el, dir, state.warn);
1 | function model ( |
走进genDefaultModel(el, value, modifiers);
1 | function genDefaultModel ( |
1 | addProp(el, 'value', ("(" + value + ")")); |
其中的这两个方法,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件

然后上面var dirs = genDirectives(el, state);返回的dirs就是这样的

最终获得的render方法是这样的
1 | "with(this){return _c('div',[_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}}),_v("\n "+_s(message)+"\n ")])}" |
往后走到调用render方法的地方

顺便补充一下这个vm._renderProxy,其实就是new Proxy(vm,handlers)返回的实例
1 | initProxy = function initProxy (vm) { |
这个是input生成的vnode

走进去update方法看下patch阶段做了什么,进入createElm方法里的createChildren方法(这里是创建的是div标签的children)


又会走到createElm,这里是input标签的了

进入invokeCreateHooks方法
1 | function invokeCreateHooks (vnode, insertedVnodeQueue) { |

updateDOMListeners方法
1 | function updateDOMListeners (oldVnode, vnode) { |
走进updateListeners
1 | function updateListeners ( |
走进add(event.name, cur, event.capture, event.passive, event.params);
1 | function add$1 ( |
在这里添加绑定事件

触发一下绑定input事件


就是调用
1 | function($event){if($event.target.composing)return;message=$event.target.value} |
然后又触发setter

后面就不写了
总的来说就是
1)在模板解析,生成render方法的时候,为input的el先添加了events属性,之后又判断有events属性的话,生成的data做字符串拼接(最后拼接到render方法里的字符串)

2)在调用render方法生成vnode以后,在update的patch阶段,为DOM元素增加事件
数组
为什么不能直接通过索引,或者通过length属性修改值,在响应式原理有写。
其实Object.defineProperty本身有一定的监控到数组下标变化的能力,因为性能问题,所以没有使用到。
Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
1 | Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象。 |
nextTick
这个方法接收到参数后,把参数包在匿名函数中push进callbacks数组,然后把一个方法加到微任务队列或者宏任务队列,加进去的方法就是遍历callbacks调用匿名函数。
Vue 组件中 data 为什么必须是函数
先看错误提示在哪
在子组件做mergeOptions的时候

如果childVal不是function类型就会提示

因为:
在组件创建实例做参数合并的时候,看一下data,合并参数后返回的是这个方法,其中的childVal是new Vue(options),中的options中data的引用地址

然后在initData的时候,获取data

然后我去修改data的值

然后看下new Vue(options)

也就是说,在组件创建实例的时候,data如果是对象的话,每个组件的实例修改data的值,会有影响,所以使用方法可以让每个实例的data独立
自定义事件通知
emm…,意思其实就是子组件通知父组件啦
main.js
1 | import Vue from 'vue' |
App.vue
1 | <template> |
写个大概吧。
1)父组件初始化的时候,initState的时候,childEmitHandler强绑定this后挂载到vm实例上,在update阶段的时候为元素添加事件

2)子组件初始化的时候,initEvents的时候,添加自定义事件进vm._events对象


然后initState的时候clickHandler绑定this后挂载在实例上,在update阶段的时候为元素添加事件。
3)触发子组件点击事件,调用$emit方法通知调用自定义事件,遍历vm._events[event],调用每一项的方法调用


并且得知,事件是可以绑定多个方法的
总结结
- 子组件创建实例的时候,传入了options里包含父vnode,父vnode里包含了自定义事件。子组件init进入initEvent的时候挂载到了子组件实例的_events上
- $emit方法调用的时候,从vm._events上取得回调,并调用
vue中 key 值的作用
写两个例子,分别带key和不带key的情况
不带key
1 | import Vue from 'vue' |
1)触发点击事件
2)更新vnode
3)触发DOM更新
当没有key的时候(key是undefined),sameVnode(oldStartVnode, newStartVnode)方法的返回值为true,选择复用之前的DOM结构,也就是DOM元素不变,去改变它的children(当前这个元素是第一个li元素)

当前元素的children的值更新了

之后再改变另外一个li元素的children

带key
1 | import Vue from 'vue' |
1)触发点击事件
2)更新vnode
3)触发DOM更新
这时候因为有key,所以sameVnode(oldStartVnode, newStartVnode)方法返回值为false

所以走进了另外一个处理分支,sameVnode(oldStartVnode, newEndVnode)返回为true,把现有元素通过insertBefore移动到前面

总结:
带key:根据key的变化,来重新排列元素顺序,更新DOM结构。
不带key:没有 key 的值会采取一种“就地更新策略”,更新现有的DOM节点和它的children,(有可能会做很多创建、删除节点之类的,相对来说效率更低一点)。
为什么key不建议使用index
写两个例子
key为index
main.js
1 | import Vue from 'vue' |
1)触发点击事件
2)更新vnode
3)
第一个
li无变化,没有重新渲染第二个
li被重新渲染
第三个
li被重新渲染
第四个
li被移除
key为id
main.js
1 | import Vue from 'vue' |
1)触发点击事件
2)更新vnode
3)
第一个
li无变化,没有重新渲染第四个
li的更新内容
第三个
li更新内容
移除第二个
li
总结:
key为index:当移除数组的某一项,某项开始到之后的所有节点都会被重新渲染
key为id:尽可能复用相同的id的节点(因为插值表达式写了index,才做了渲染),只移除需要移除的那一项。
v-show和v-if指令的共同点和不同点
v-show指令是通过修改元素的displayCSS属性让其显示或者隐藏
v-if指令是直接销毁和重建DOM达到让元素显示和隐藏的效果(注意:v-if 可以实现组件的重新渲染)
\$route与\$router
Vue.use(VueRouter)后,在执行install方法的时候,除了安装了两个生命周期函数和全局组件,还为vm.$router和vm.$route添加了get

在new VueRouter({routes})的时候,创建路由注册表,下面是创建的实例

它的__proto__属性:

在init初始化,执行callHook(vm, 'beforeCreate');,也就是调用install的时候添加的beforeCreate生命周期钩子函数,在这里做路由初始化
$router就是VueRouter的实例

监听路由变化

然后$route在这里赋值


总结:
$router:路由器对象,提供了系列跳转相关的方法。
$route:相当于当前正在跳转的路由对象。
插槽
main.js
1 | import Vue from 'vue' |
app.vue
1 | <template> |
在父组件模板解析生成render方法
1 | with(this){return _c('App',[_c('p',{attrs:{"slot":"content"},slot:"content"},[_v(_s(name))])])} |
因为App是个组件,所以开始创建组件,进入组件init,在initInternalComponent的时候,拿到App组件vnode里的子vnode,就是那个p标签的vnode

在initRender的时候为子组件vm添加$slots属性

然后进入子组件挂载阶段
1 | child.$mount(hydrating ? vnode.elm : undefined, hydrating); |
进入_render方法,vm.$scopedSlots等于这么个方法
1 | if (_parentVnode) { |

生成vnode
vnode = render.call(vm._renderProxy, vm.$createElement);
进入render方法
1 | var render = function() { |
看一下_vm._t方法
1 | function renderSlot ( |
返回的nodes就是这个,就是那个p标签的vnode

然后调用_c,就是_createElement方法,最后生成的vnode大概是这样的
1 | { |
后面就不写了
总结:
也就是一开始父组件里使用组件的地方,组件标签里的模板,变成了vnode被存了起来,在子组件_render的时候被拿出来插到插槽的位置。
计算属性与监听属性
计算属性:
1 | import Vue from 'vue' |
initState进入到initComputed的时候,遍历计算属性- 添加计算属性
watcher,push到vm._watchers,watcher的getter方法就是计算属性的函数 - 为
vm上的计算属性键名,添加属性描述符(getter、setter、configurable、enumerable)
- 添加计算属性
调用
vm._render(),生成vnode的过程中取
totalName的值,触发getter(这里就是触发totalName计算属性的方法调用,其中又触发firstName和lastName的getter,就把当前这个totalName的watcher收集了,另外还收集了渲染watcher)生成
vnode
触发点击事件,修改
firstName的值,触发派发更新的过程- 因为计算属性的
watcher是lazy,所以没能加到queue队列里 - 将渲染
watcher加入到queue队列 - 之后通过
nextTick添加异步任务,遍历queue执行里面watcher的run方法,也就是重新执行vm._update(vm._render(), hydrating);,生成vnode的时候,更新totalName的数据 - 重新渲染
- 因为计算属性的
监听属性:
1 | import Vue from 'vue' |
initState进入到initWatch的时候,遍历监听属性添加
user watcher,push到vm._watchers,user watcher的cb属性是watch属性的回调函数,getter是这个方法1
2
3
4
5
6
7function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}创建
user watcher实例的最后,调用this.get() -> this.getter.call(vm, vm),getter就是上面这个方法,obj[segments[i]]的时候触发firstName的getter,把这个user watcher做依赖收集,添加到了firstName的dep里判断是否有
immediate属性,有就立即执行cb
触发点击事件,改变
firstName的值,触发派发更新调用刚才依赖收集到的
user watcher的run方法,先获取了val,也获取了oldVal,然后调用user watcher的cb函数1
this.cb.call(this.vm, value, oldValue);
所以监听属性与计算属性的区别:
计算属性:在render生成vnode的时候做依赖收集,也是在依赖是属性发生变化的时候,才重新生成vnode,重新渲染。
监听属性:在创建user watcher实例的时候,就做了依赖收集,在watch的值改变的时候,会调用watch属性的回调(监听属性比较适合做一些复杂的逻辑)。
v-if与v-for不建议使用在同一个模板
改一下例子
main.js
1 | import Vue from 'vue' |
生成的render函数
1 | (function anonymous( |
优先调用
1 | _l((arr),function(item){return (!arr.length)?_c('p',[_v(_s(item))]):_e()}) |
_l也就是下面这个方法
1 | function renderList ( |
如果传入的val是数组,就会遍历数组,调用ret[i] = render(val[i], i);,而这里的遍历操作是无意义的
keep-alive
main.js
1 | import Vue from 'vue' |
首先声明了KeepAlive对象,之后作为builtInComponents对象的元素
1 | var KeepAlive = { |
在initGlobalAPI方法调用中extend(Vue.options.components, builtInComponents);,把keepAlive组件添加到Vue.options.components
1 | function initGlobalAPI (Vue) { |
首次渲染:
根据ast生成的render函数
1 | (function anonymous( |
在patch阶段
生成keep-alive组件的vnode的时候,render函数就是keep-alive组件的render函数

1 | render: function render () { |
在这里对组件vnode进行了缓存,之后有用到的时候会取出来
缓存渲染:
点击添加按钮,并且切换视图两次,触发派发更新

走进更新视图的patch,会走进prepatch方法
1 | prepatch: function prepatch (oldVnode, vnode) { |
然后走进updateChildComponent
1 | function updateChildComponent ( |
调用vm.$forceUpdate,为queue新增了一个wathcer

之后调用新增这个wathcer的run方法(最后就是vm._update(vm._render(), hydrating);),在生成vnode的时候,从cache中取出缓存的vnode

之后调用update更新视图,这次走的不是新增的流程,而是比较的
这次在创建子组件的时候
1 | function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { |
进入组建init方法的时候,逻辑是走的这里
进入prepatch方法,在updateChildComponent做了一些更新操作
1 | prepatch: function prepatch (oldVnode, vnode) { |
回到createComponent,在initComponent方法里,把组建实例的$el赋值给了vnode.elm
1 | vnode.elm = vnode.componentInstance.$el; |
之后调用
1 | insert(parentElm, vnode.elm, refElm); |
把缓存的dom结构插入到了插槽中

之后再移除旧的节点

看一下componentInstance在什么时候添加的

看一下createComponentInstanceForVnode
1 | function createComponentInstanceForVnode ( |
需要注意的就是组建在重新渲染的时候不会调用created和mounted钩子函数,会调用activated钩子函数
而首次渲染也会调用activated钩子函数
Vue.directive
改一下例子
1 | import Vue from 'vue' |

看一下updateDirectives方法,其实就是遍历directives,调用_update
1 | function updateDirectives (oldVnode, vnode) { |
看一下_update方法
1 | function _update (oldVnode, vnode) { |
在这里调用bind钩子,其实就是调用directive传入的函数

ref在什么时候添加到$refs上的
在patch阶段




1 | create: function create (_, vnode) { |
进入registerRef
1 | function registerRef (vnode, isRemoval) { |
这个方法获取了vm.$refs,并且传值给refs,之后为refs添加属性
组件style标签中scoped的作用
在创建占位符的时候,调用setScope的时候,为标签设置了一个属性
1 | function setScope (vnode) { |


