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



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

以下是我到目前为止尝试过的:

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>{{keyword}}{{keyword1}}</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>{{keyword}}{{keyword1}}</span></p>
</div>
</div>

第一种情况:应该是因为相关的VNode已经被<span><strong></strong></span>替换了,所以不会用数据属性正确更新。

第二种情况:它按预期工作。该解决方案:key添加以强制装载组件,因此当触发更新时,它将使用模板和最新数据属性进行渲染,然后装载。

但我更喜欢强制挂载指令钩子而不是在组件上绑定:key,或者根据模板和最新的数据属性获取更新的 Dom($el(。 因此,其他任何想要使用此指令的人都不需要对:key进行大小写。

非常感谢任何。

我不确定这是最佳实践,因为有关于修改vnode的警告,但这在您的示例中适用于动态添加键

vnode.key = vnode.elm.innerText

奇怪的是,我注意到第一个指令响应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>{{keyword}}{{keyword1}}</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>{{keyword}}{{keyword1}}</span></p>
</div>
</div>

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

检查 Vue Github 生命周期源代码,所以第一个元素可以是一个将被挂载的 Dom 对象。

所以我按照步骤使用指令钩子的第三个参数(绑定、组件更新、更新等(来生成新的 Dom 元素,然后将其复制到指令钩子的第一个参数。

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

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

PS:正如 Vue 指令访问其实例所说,在指令的钩子中,使用vnode.context来访问实例。

编辑:循环test下的所有子项,然后附加到el,简单地复制到el.innerHTMLtest.innerHTML会导致一些问题,例如按钮不起作用。

然后在我的实际项目中测试此指令,例如<div v-keyword-highlight>very complicated template</div>,到目前为止它工作正常。

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>{{keyword}}{{keyword1}}</span></p>
<button @click="changeData()">Click me</button>
</div>
</div>

最新更新