响应式java递归怎么写_模仿vue自己动手写响应式框架(五)终章 - v-for指令实现

概述

在上一篇中,我们实现了vue对象的构建,并且已经初步实现了变量的绑定和事件绑定,现在我们就剩下一个问题需要解决,就是v-for指令的实现,这也是本系列中最难的部分。

难点

实现v-for有以下几个难点表达式解析,v-for有两种语法item in items和(item,index) in items,第二种可以获取到序号,程序需要解析这两种语法

编译v-for内的元素,虽然已经有了compile函数,但是v-for循环内的上下文和vue并不一致,什么意思呢,compile里面绑定的值和变量是vue,vue是全局的,但v-for内绑定的变量是循环内的,每次都不一样

编译

在compile中,如果遇到v-for会先将v-for内的节点全部生成好,再作为子节点append到父节点上,因此第一步就是判断是否包含v-for指令function isLoop(element) {

return element.attributes && element.attributes['v-for'];

}

compile函数递归编译子节点从for (let i = 0; i < node.childNodes.length; ++i) {

element.appendChild(compile(node.childNodes[i]));

}

修改为for (let i = 0; i < node.childNodes.length; ++i) {

let child = node.childNodes[i];

if (isLoop(child)) {

let ns = compileLoop(child, element);

for (let j = 0; j < ns.length; ++j) {

element.appendChild(ns[j]);

}

} else {

element.appendChild(compile(child));

}

}

compileLoop会对v-for节点进行编译,并且返回节点数组,父节点对返回的节点进行append。

解析

编译的第一步就是解析,需要解析三部分的内容循环的数组变量

循环过程中变量名

循环过程中元素下标let vfor = element.attributes['v-for'].value;

let itemName;

let indexName;

let varName;

let loopExp1 = /\(([^,]+),([^\)]+)\)\s+in\s+(.*)/g;

let loopExp2 = /(\w+)\s+in\s+(.*)/g;

let m;

if (m = loopExp1.exec(vfor)) {

itemName = m[1];

indexName = m[2]

varName = m[3];

} else if (m = loopExp2.exec(vfor)) {

itemName = m[1];

varName = m[2];

}

直接用正则进行解析,loopExp1和loopExp2分别对应两种语法,varName:数组名,itemName:循环变量名,indexName:循环下标

元素生成

解析完成后就可以开始生成元素var directive = {

origin: element.cloneNode(true),

attr: 'v-for',

exp: {

varName: varName,

indexName: indexName,

itemName: itemName

}

}

element.attributes.removeNamedItem('v-for');

let arrays = vue[varName];

let elements = [];

for (let i = 0; i < arrays.length; ++i) {

vue[itemName] = arrays[i];

vue[indexName] = i;

elements.push(compile(element.cloneNode(true), false));

}

if (!loopElement[varName]) {

let loop = {};

loop.elements = elements;

loop.parent = parent;

loopElement[varName] = loop;

}定义了一个变量directive,把v-for一些语法也做了保存,下次可以直接用,无需再次解析

因为是用clone生成,因此需要移除掉v-for标签,不然会进入死循环

递归调用compile生成新元素,在每一次循环都将当前变量和下标放到vue中,保证了编译的时候程序可以找到变量for (let i = 0; i < arrays.length; ++i) {

vue[itemName] = arrays[i];

vue[indexName] = i;

elements.push(compile(element.cloneNode(true), false));

}将结果保存到loopElement中,保存的目的是,当绑定的数组发生变化时,需要删除当前相关节点重新生成新的节点

指令directive.change = function (name, value) {

let ele = loopElement[name];

for (let i = 0; i < ele.elements.length; ++i) {

ele.elements[i].remove();

}

let newEles = [];

let arrays = vue[this.exp.varName];

for (let i = 0; i < arrays.length; ++i) {

vue[this.exp.itemName] = arrays[i];

vue[this.exp.indexName] = i;

let node = compile(this.origin.cloneNode(true));

newEles.push(node);

}

loopElement[name].elements = newEles;

for (let j = 0; j < newEles.length; ++j) {

ele.parent.appendChild(newEles[j]);

}

}

addSubscriber(varName, directive);先对当前元素进行移除

和上面的逻辑一样,生成新的元素

通过之前保存的parent进行append

addSubscriber创建订阅者将指令注册到订阅者中

完整的compileLoop代码如下function compileLoop(element, parent) {

let vfor = element.attributes['v-for'].value;

let itemName;

let indexName;

let varName;

let loopExp1 = /\(([^,]+),([^\)]+)\)\s+in\s+(.*)/g;

let loopExp2 = /(\w+)\s+in\s+(.*)/g;

let m;

if (m = loopExp1.exec(vfor)) {

itemName = m[1];

indexName = m[2]

varName = m[3];

} else if (m = loopExp2.exec(vfor)) {

itemName = m[1];

varName = m[2];

}

var directive = {

origin: element.cloneNode(true),

attr: 'v-for',

exp: {

varName: varName,

indexName: indexName,

itemName: itemName

}

}

element.attributes.removeNamedItem('v-for');

let arrays = vue[varName];

let elements = [];

for (let i = 0; i < arrays.length; ++i) {

vue[itemName] = arrays[i];

vue[indexName] = i;

elements.push(compile(element.cloneNode(true), false));

}

if (!loopElement[varName]) {

let loop = {};

loop.elements = elements;

loop.parent = parent;

loopElement[varName] = loop;

}

directive.change = function (name, value) {

let ele = loopElement[name];

for (let i = 0; i < ele.elements.length; ++i) {

ele.elements[i].remove();

}

let newEles = [];

let arrays = vue[this.exp.varName];

for (let i = 0; i < arrays.length; ++i) {

vue[this.exp.itemName] = arrays[i];

vue[this.exp.indexName] = i;

let node = compile(this.origin.cloneNode(true));

newEles.push(node);

}

loopElement[name].elements = newEles;

for (let j = 0; j < newEles.length; ++j) {

ele.parent.appendChild(newEles[j]);

}

}

addSubscriber(varName, directive);

return elements;

}

事件响应

在上一篇中我们的事件响应是这么写的function addEvent(element, event, method) {

element.addEventListener(event, function(e) {

let params = [];

let paramNames = method.params;

if (paramNames) {

for (let i = 0; i < paramNames.length; ++i) {

params.push(vue[paramNames[i]]);

}

}

vue[method.name].apply(vue, params);

})

}

这么写对于循环有个问题,因为每次循环都会重置下标和循环变量,下标和循环变量都是保存在vue对象中的,所以当事件触发时,params.push(vue[paramNames[i]]);这行代码是取不到值的因为上下文已经发生变化。解决这个问题的办法就是闭包,通过闭包保存当时环境信息,不至于运行时丢失,只需将获取数据移到外面就行。function addEvent(element, event, method) {

let params = [];

let paramNames = method.params;

if (paramNames) {

for (let i = 0; i < paramNames.length; ++i) {

params.push(vue[paramNames[i]]);

}

}

element.addEventListener(event, function (e) {

vue[method.name].apply(vue, params);

})

}

到这里就可以实现v-for指令,但之前的一些遗留还未修复,我们在dom解析这篇中提到目前对于文本节点值发生变化只是简单的文本替换,如下:if (node.nodeType == 3) {

directive.change = function(name, value) {

this.node.textContent = this.origin.replace("\{\{" + name + "\}\}", value);

}

}

如果有多个变量或者类似todo.text这种多级变量结果就会出错,这里写了一个专门用来解析表达的函数if (node.nodeType == 3) {

directive.change = function (name, value) {

this.node.textContent = evaluteExpression(this.origin);

}

}evaluteExpressionfunction evaluteExpression(text) {

let vars = parseVariable(text);

for (let i = 0; i < vars.length; ++i) {

let value = getVariableValue(vars[i]);

text = text.replace("\{\{" + vars[i] + "\}\}", value);

}

return text;

}先对变量进行解析

循环获取变量值,通过调用getVariableValue

循环替换

getVariableValuefunction getVariableValue(name) {

let value;

if (name.indexOf(".")) {

let ss = name.split(".");

value = vue[ss[0]];

if (value) {

for (let i = 1; i < ss.length; ++i) {

value = value[ss[i]];

if (value == undefined) {

break;

}

}

}

} else {

value = vue[name];

}

if (value == undefined || value == null) {

value = "";

}

return value;

}类似item.text的多级变量进行循环获取值

如果未定义设置为空字符串

效果

以下是实现的效果图,也可以点击这里进行查看cv6930981

完整js代码点击这里查看

参考

点击以下链接,查看该系列其他文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答你的问题。首先,我们需要了解Vue3的响应式系统是如何工作的。Vue3使用了一个名为`Reactive`的函数来实现响应式。 `Reactive`函数的作用是将一个普通的JavaScript对象转换成响应式的对象。当响应式对象的属性被修改时,所有依赖该属性的地方都会自动更新。 下面是`Reactive`函数的实现: ```javascript function Reactive(obj) { const handlers = { get(target, prop, receiver) { const value = Reflect.get(target, prop, receiver); track(target, prop); return isObject(value) ? Reactive(value) : value; }, set(target, prop, value, receiver) { const oldValue = Reflect.get(target, prop, receiver); let result = true; if (oldValue !== value) { result = Reflect.set(target, prop, value, receiver); trigger(target, prop); } return result; }, deleteProperty(target, prop) { const result = Reflect.deleteProperty(target, prop); trigger(target, prop); return result; } }; return new Proxy(obj, handlers); } ``` `Reactive`函数接受一个普通的JavaScript对象作为参数,返回一个响应式的对象。在实现中,我们使用了ES6的Proxy对象来实现响应式。 在`get`处理器中,我们使用了`track`函数来收集依赖。`track`函数的作用是将当前正在执行的计算函数添加到依赖列表中。 在`set`处理器中,我们首先获取旧值,然后判断新值是否与旧值相同。如果不同,我们使用`trigger`函数来触发更新。`trigger`函数的作用是遍历依赖列表,执行所有计算函数。 在`deleteProperty`处理器中,我们使用`trigger`函数来触发更新,因为删除属性也可能导致依赖更新。 在以上代码中,我们还使用了`isObject`函数来判断一个值是否为对象。该函数的实现如下: ```javascript function isObject(value) { return typeof value === 'object' && value !== null; } ``` 这个函数非常简单,它只是判断一个值是否为对象。如果是对象,我们就递归调用`Reactive`函数来将该对象转换成响应式。 总之,这就是Vue3的响应式系统的实现原理。通过`Reactive`函数和Proxy对象,我们可以将一个普通的JavaScript对象转换成响应式的对象,并实现自动更新。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值