当我们进行数据更新的时候,vue虚拟dom不会销毁这个组件(如果说删除某个数据,会从后往前销毁组件,前面的总是最后销毁),而是进行更新(根据数据改变),如果指令有update钩子会运行这个钩子函数,但是对于元素在bind中绑定的事件,在update中没有处理的话,他不会消失(依然引用初始化时形成的闭包中的数据),所以当我们更改数据再次点击元素后,看到的数据还是原数据。
源码分析
函数执行顺序:createElm/initComponent/patchVnode --> invokeCreateHooks (cbs.create) --> updateDirectives --> _update
在createElm方法和initComponent方法和更新节点patchVnode时会调用invokeCreateHooks方法,它会去遍历cbs.create中钩子函数进行执行,cbs.create中的钩子函数如下图所示共8个。我们所需要看的就是updateDirectives这个函数,这个函数会继续调用_update函数,vue中的指令操作就都在这个_update函数中了。
下面我们就来详细看下这个_update函数。
function _update(oldVnode, vnode) { //判断旧节点是不是空节点,是的话表示新建/初始化组件 var isCreate = oldVnode === emptyNode; //判断新节点是不是空节点,是的话表示销毁组件 var isDestroy = vnode === emptyNode; //获取旧节点上的所有自定义指令 var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); //获取新节点上的所有自定义指令 var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); //保存inserted钩子函数 var dirsWithInsert = []; //保存componentUpdated钩子函数 var dirsWithPostpatch = []; var key, oldDir, dir; //这里先说下callHook$1函数的作用 //callHook$1有五个参数,第一个参数是指令对象,第二个参数是钩子函数名称,第三个参数新节点, //第四个参数是旧节点,第五个参数是是否为注销组件,默认为undefined,只在组件注销时使用 //在这个函数里,会根据我们传递的钩子函数名称,运行我们自定义组件时,所声明的钩子函数, //遍历所有新节点上的自定义指令 for(key in newDirs) { oldDir = oldDirs[key]; dir = newDirs[key]; //如果旧节点中没有对应的指令,一般都是初始化的时候运行 if(!oldDir) { //对该节点执行指令的bind钩子函数 callHook$1(dir, 'bind', vnode, oldVnode); //dir.def是我们所定义的指令的五个钩子函数的集合 //如果我们的指令中存在inserted钩子函数 if(dir.def && dir.def.inserted) { //把该指令存入dirsWithInsert中 dirsWithInsert.push(dir); } } else { //如果旧节点中有对应的指令,一般都是组件更新的时候运行 //那么这里进行更新操作,运行update钩子(如果有的话) //将旧值保存下来,供其他地方使用(仅在 update 和 componentUpdated 钩子中可用) dir.oldValue = oldDir.value; //对该节点执行指令的update钩子函数 callHook$1(dir, 'update', vnode, oldVnode); //dir.def是我们所定义的指令的五个钩子函数的集合 //如果我们的指令中存在componentUpdated钩子函数 if(dir.def && dir.def.componentUpdated) { //把该指令存入dirsWithPostpatch中 dirsWithPostpatch.push(dir); } } } //我们先来简单讲下mergeVNodeHook的作用 //mergeVNodeHook有三个参数,第一个参数是vnode节点,第二个参数是key值,第三个参数是回函数 //mergeVNodeHook会先用一个函数wrappedHook重新封装回调,在这个函数里运行回调函数 //如果该节点没有这个key属性,会新增一个key属性,值为一个数组,数组中包含上面说的函数wrappedHook //如果该节点有这个key属性,会把函数wrappedHook追加到数组中 //如果dirsWithInsert的长度不为0,也就是在初始化的时候,且至少有一个指令中有inserted钩子函数 if(dirsWithInsert.length) { //封装回调函数 var callInsert = function() { //遍历所有指令的inserted钩子 for(var i = 0; i < dirsWithInsert.length; i++) { //对节点执行指令的inserted钩子函数 callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode); } }; if(isCreate) { //如果是新建/初始化组件,使用mergeVNodeHook绑定insert属性,等待后面调用。 mergeVNodeHook(vnode, 'insert', callInsert); } else { //如果是更新组件,直接调用函数,遍历inserted钩子 callInsert(); } } //如果dirsWithPostpatch的长度不为0,也就是在组件更新的时候,且至少有一个指令中有componentUpdated钩子函数 if(dirsWithPostpatch.length) { //使用mergeVNodeHook绑定postpatch属性,等待后面子组建全部更新完成调用。 mergeVNodeHook(vnode, 'postpatch', function() { for(var i = 0; i < dirsWithPostpatch.length; i++) { //对节点执行指令的componentUpdated钩子函数 callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode); } }); } //如果不是新建/初始化组件,也就是说是更新组件 if(!isCreate) { //遍历旧节点中的指令 for(key in oldDirs) { //如果新节点中没有这个指令(旧节点中有,新节点没有) if(!newDirs[key]) { //从旧节点中解绑,isDestroy表示组件是不是注销了 //对旧节点执行指令的unbind钩子函数 callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy); } } } }
callHook$1函数