补充
事件
改一下例子
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
指令是通过修改元素的display
CSS属性让其显示或者隐藏
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) { |