Vue 自定义指令使用更新后的 Dom(或 $el)

Posted

技术标签:

【中文标题】Vue 自定义指令使用更新后的 Dom(或 $el)【英文标题】:Vue custom directive uses the updated Dom (or $el) 【发布时间】:2018-11-08 17:26:20 【问题描述】:

我想为 Dom 树中的所有 TextNode 设计一个自定义指令来将 'cx' 替换为 <strong>cx</strong>

以下是我迄今为止尝试过的:

Vue.config.productionTip = false

function removeKeywords(el, keyword)
  if(!keyword) return
  let n = null
  let founds = []
  walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false)
  while(n=walk.nextNode()) 
    if(n.textContent.trim().length < 1) continue
    founds.push(n)
  
  let result = []
  founds.forEach((item) => 
    if( new RegExp('cx', 'ig').test(item.textContent) ) 
      let kNode = document.createElement('span')
      kNode.innerhtml = item.textContent.replace(new RegExp('(.*?)(cx)(.*?)', 'ig'), '$1<strong>$2</strong>$3')
      item.parentNode.insertBefore(kNode, item)
      item.parentNode.removeChild(item)
    
  )


let myDirective = 
myDirective.install = function install(Vue) 
  let timeoutIDs = 
  Vue.directive('keyword-highlight', 
    bind: function bind(el, binding, vnode) 
      clearTimeout(timeoutIDs[binding.value.id])
      if(!binding.value) return
      timeoutIDs[binding.value.id] = setTimeout(() => 
        removeKeywords(el, binding.value.keyword)
      , 500)
    ,
    componentUpdated: function componentUpdated(el, binding, vnode) 
      clearTimeout(timeoutIDs[binding.value.id])
      timeoutIDs[binding.value.id] = setTimeout(() => 
        removeKeywords(el, binding.value.keyword)
      , 500)
    
  );
;
Vue.use(myDirective)
app = new Vue(
  el: "#app",
  data: 
    keyword: 'abc',
    keyword1: 'xyz'
  ,
  methods: 
  
)
.header 
  background-color:red;


strong 
  background-color:yellow
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<div id="app">
  <input v-model="keyword">
  <input v-model="keyword1">
  <h1>Test Case 1: try to change 2nd input to <span class="header">anything</span></h1>
  <div v-keyword-highlight="keyword:keyword, id:1">
    <p>Test1<span>Test2</span>Test3<span>keywordkeyword1</span></p>
  </div>
  <h1>Test Case 2 which is working</h1>
  <div :key="keyword+keyword1" v-keyword-highlight="keyword:keyword, id:2">
    <p>Test1<span>Test2</span>Test3<span>keywordkeyword1</span></p>
  </div>
</div>

第一种情况:应该是相关VNode已经被&lt;span&gt;&lt;strong&gt;&lt;/strong&gt;&lt;/span&gt;替换了,所以不会正确更新数据属性。

第二种情况:它按预期工作。解决方案是添加:key强制挂载组件,所以当触发更新时,会渲染模板和最新数据属性,然后挂载。

但我更喜欢在指令钩子中强制挂载,而不是在组件上绑定:key,或者根据模板和最新的数据属性获取更新的 Dom($el)。所以任何想要使用这个指令的人都不需要关注:key

非常感谢。

【问题讨论】:

【参考方案1】:

我不确定这是不是最佳做法,因为有警告不要修改 vnode,但这适用于您的示例以动态添加密钥

vnode.key = vnode.elm.innerText

我注意到 first 指令响应componentUpdated 但第二个没有,即使第二个内部元素更新它们的值但第一个没有 - 这与什么相反你会期待的。

请注意,发生更改是因为第二个实例在输入更改时再次调用bind,而不是因为componentUpdated 中的代码。

console.clear()
Vue.config.productionTip = false

function removeKeywords(el, keyword)
  console.log(el, keyword)
  if(!keyword) return
  let n = null
  let founds = []
  walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false)
  while(n=walk.nextNode()) 
    if(n.textContent.trim().length < 1) continue
    founds.push(n)
  
  let result = []
  founds.forEach((item) => 
    if( new RegExp('cx', 'ig').test(item.textContent) ) 
      let kNode = document.createElement('span')
      kNode.innerHTML = item.textContent.replace(new RegExp('(.*?)(cx)(.*?)', 'ig'), '$1<strong>$2</strong>$3')
      item.parentNode.insertBefore(kNode, item)
      item.parentNode.removeChild(item)
    
  )


let myDirective = 
myDirective.install = function install(Vue) 
  let timeoutIDs = 
  Vue.directive('keyword-highlight', 
    bind: function bind(el, binding, vnode) 
      console.log('bind', binding.value.id)
      clearTimeout(timeoutIDs[binding.value.id])
      if(!binding.value) return
      vnode.key = vnode.elm.innerText
      timeoutIDs[binding.value.id] = setTimeout(() => 
        removeKeywords(el, binding.value.keyword)
      , 500)
    ,
    componentUpdated: function componentUpdated(el, binding, vnode) 
      //clearTimeout(timeoutIDs[binding.value.id])
      //timeoutIDs[binding.value.id] = setTimeout(() => 
        //removeKeywords(el, binding.value.keyword)
      //, 500)
    
  );
;
Vue.use(myDirective)
app = new Vue(
  el: "#app",
  data: 
    keyword: 'abc',
    keyword1: 'xyz'
  ,
  methods: 
  
)
.header 
  background-color:red;


strong 
  background-color:yellow
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<div id="app">
  <input v-model="keyword">
  <input v-model="keyword1">
  <h1>Test Case 1: try to change 2nd input to <span class="header">anything</span></h1>
  <div v-keyword-highlight="keyword:keyword, id:1">
    <p>Test1<span>Test2</span>Test3<span>keywordkeyword1</span></p>
  </div>
  <h1>Test Case 2 which is working</h1>
  <div :key="keyword+keyword1" v-keyword-highlight.keyword1="keyword:keyword, id:2">
    <p>Test1<span>Test2</span>Test3<span>keywordkeyword1</span></p>
  </div>
</div>

【讨论】:

vnode & oldVnode 是只读的,那么不知道你为什么用vnode.key=innerText。第二个例子永远不会触发componentUpdate,因为每次更新都会重新挂载,所以只会触发hook=bind。 实际上,它只是说您应该将它们视为只读(因此我在帖子顶部的警告)。但是,您可以看到第一个示例现在可以正常工作,因为它有一个键集。 你提到了重新绑定,但不清楚的是为什么 当没有键componentUpdated 钩子在更改为关键字之一,但视图未更新。【参考方案2】:

我发现 Vue 使用 Vue.patch 来比较新旧节点,然后生成 Dom 元素。

检查Vue Github Lifecycle source code,所以第一个元素可以是一个将被挂载的Dom对象。

所以我按照步骤使用the directive hooks 的第三个参数(bind、componentUpdated、update 等)生成新的 Dom 元素,然后将其复制到指令钩子的第一个参数中。

最后下面的演示似乎可以工作了:没有强制重新挂载,只能重新编译 VNodes。

PS:我使用deepClone方法克隆vnode,因为在函数__patch__(oldNode, newNode, hydrating)内部,它会修改newNode

PS:正如Vue directive access its instance所说,在指令的钩子里面,使用vnode.context来访问实例。

编辑:循环test下的所有children,然后追加到el,简单的复制test.innerHTMLel.innerHTML会导致一些问题,比如按钮不起作用。

然后在我的实际项目中测试这个指令,比如&lt;div v-keyword-highlight&gt;very complicated template&lt;/div&gt;,到目前为止它工作正常。

function deepClone (vnodes, createElement) 
  let clonedProperties = ['text', 'isComment', 'componentOptions', 'elm', 'context', 'ns', 'isStatic', 'key']
  function cloneVNode (vnode) 
    let clonedChildren = vnode.children && vnode.children.map(cloneVNode)
    let cloned = createElement(vnode.tag, vnode.data, clonedChildren)
    clonedProperties.forEach(function (item) 
      cloned[item] = vnode[item]
    )
    return cloned
  
  return vnodes.map(cloneVNode)


function addStylesForKeywords(el, keyword)
  if(!keyword) return
  let n = null
  let founds = []
  walk=document.createTreeWalker(el,NodeFilter.SHOW_TEXT,null,false)
  while(n=walk.nextNode()) 
    if(n.textContent.trim().length < 1) continue
    founds.push(n)
  
  let result = []
  founds.forEach((item) => 
    if( new RegExp('cx', 'ig').test(item.textContent) ) 
      let kNode = document.createElement('span')
      kNode.innerHTML = item.textContent.replace(new RegExp('(.*?)(cx)(.*?)', 'ig'), '$1<strong>$2</strong>$3')
      item.parentNode.insertBefore(kNode, item)
      item.parentNode.removeChild(item)
    
  )


let myDirective = 
myDirective.install = function install(Vue) 
  let timeoutIDs = 
  let temp = Vue.extend(
  template: '<p>firstName lastName aka alias</p>'
  )
  let fakeVue = new temp()
  Vue.directive('keyword-highlight', 
    bind: function bind(el, binding, vnode) 
      clearTimeout(timeoutIDs[binding.value.id])
      if(!binding.value) return
      timeoutIDs[binding.value.id] = setTimeout(() => 
        addStylesForKeywords(el, binding.value.keyword)
      , 500)
    ,
    componentUpdated: function componentUpdated(el, binding, vnode) 
      let fakeELement = document.createElement('div')
      //vnode is readonly, but method=__patch__(orgNode, newNode) will load new dom into the second parameter=newNode.$el, so uses the cloned one instead
      let clonedNewNode = deepClone([vnode], vnode.context.$createElement)[0]
      let test = clonedNewNode.context.__patch__(fakeELement, clonedNewNode)

      while (el.firstChild) 
          el.removeChild(el.firstChild);
      
      test.childNodes.forEach((item) => 
        el.appendChild(item)
      )
      clearTimeout(timeoutIDs[binding.value.id])
      timeoutIDs[binding.value.id] = setTimeout(() => 
        addStylesForKeywords(el, binding.value.keyword)
      , 500)
    
  );
;
Vue.use(myDirective)
Vue.config.productionTip = false
app = new Vue(
  el: "#app",
  data: 
    keyword: 'abc',
    keyword1: 'xyz'
  ,
  methods: 
    changeData: function () 
      this.keyword += 'c'
      this.keyword1 = 'x' + this.keyword1
      console.log('test')
    
  
)
.header 
  background-color:red;


strong 
  background-color:yellow
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<script src="https://unpkg.com/lodash"></script>
<div id="app">
  <input v-model="keyword">
  <input v-model="keyword1">
  <h4>Test Case 3 <span class="header"></span></h4>
  <div v-keyword-highlight="keyword:keyword, id:1">
    <p>Test1<span>Test2</span>Test3<span>keywordkeyword1</span></p>
    <button @click="changeData()">Click me</button>
  </div>
</div>

【讨论】:

以上是关于Vue 自定义指令使用更新后的 Dom(或 $el)的主要内容,如果未能解决你的问题,请参考以下文章

Vue2自定义指令改变DOM值后未刷新data中绑定属性的值

Vue.directive 自定义指令

Vue自定义指令

Vue2.0 - 自定义指令 vue-directive

vue怎么触发元素的原生事件

vue - 自定义指令