Open youngwind opened 7 years ago
相信大家都用过vue非常好用的v-if功能,那么它是如何实现的呢?回顾一下之前我们已经实现的动态数据绑定 #87 ,我们动态绑定的是一个普通文本节点和一个数据之间的关系。当数据发生改变时,修改文本节点值。 但是,我们现在要做的是,当数据发生改变时,渲染插入某个节点或者把某个节点从DOM中移除,而且这个节点不是普通的文本节点。 所以,我们不能照搬之前的那一套,需要做一些改动。
考虑下面的例子 这个例子是较为简单的例子,因为b-if里面不包含user.name这样的变量,我称它为不带变量的条件渲染
// html <div id="app"> <p>姓名:{{user.name}}</p> <p>年龄:{{user.age}}</p> <div b-if="show" id="#sub_app"> <h1>如果show为真,我们就显示</h1> <h1>如果show为假,我们就不显示</h1> </div> </div>
const app = new Bue({ el: '#app', data: { show: true, user: { name: 'youngwind', age: 24 } } });
问题是:如何做到,当show为true时,渲染整个div。当show为false,不渲染整个div。
首先,b-if指令对应的div结构内部可能是一个很复杂的DOM结构(比如上面的例子,b-if指令内部就包含两个h1标签),所以,我们更应该把"b-if"对应的DOM结构看成是一个新的vue实例,而非一个普通的Directive。我们将要实现的是:一个vue实例嵌套另一个vue实例,父实例是#app,子实例是#sub_app。 如何做到呢?我们从修改渲染节点函数入手:
/** * 渲染节点 * @param node {Element} * @private */ exports._compileElement = function (node) { let hasAttributes = node.hasAttributes(); // 添加了这个判断,如果包含b-if指令,那么就做特殊处理,不走原先的DOM遍历了 if (hasAttributes && this._checkPriorityDirs(node)) { return; } if (node.hasChildNodes()) { Array.from(node.childNodes).forEach(this._compileNode, this); } };
代码写到这儿,我们就可以看到_directive数组中就多了一个show的Directive了,如下图所示。
// 这里定义了一些特殊的指令,如v-if,碰到他们就做特殊处理 const priorityDirs = [ 'if' ]; /** * 检查node节点是否包含某些如 "v-if" 这样的高优先级指令 * 如果包含,那么就不用走原先的DOM遍历了, 直接走指令绑定 * @param node {Element} * @private */ exports._checkPriorityDirs = function (node) { priorityDirs.forEach((dir) => { let value = _.attr(node, dir); // 获取b-if指令的值,此为"show" if (value) { // _bindDirective是我们在动态绑定的时候就做好的, 如果不明白这一块,请往前面的文章翻 this._bindDirective(dir, value, node); return true; } }); };
然后,接下来是重点。 对于一个受指令控制的DOM节点,如例子中的b-if,它其实至少有两个生命周期:一个是初始化,第一次解析DOM的时候,我们称之为bind;另一个是当数据变化时,DOM节点会更新,我们称之为update。 回想一下,我们之前构造Directive的时候其实就已经隐含这样的思想,如下面代码所示(这是之前就有的代码)
Directive.prototype._bind = function () { if (!this.expression) return; // 这里执行初始化 this.bind && this.bind(); this._watcher = new Watcher( this.vm, this.expression, this._update, // 回调函数,目前是唯一的,就是更新DOM this // 上下文 ); // 这里执行更新 this.update(this._watcher.value); };
所以,得出的结论是:我们需要为b-if指令也定义这样的bind和update方法,分别完成初始化和更新的动作。 所以就有了下面的代码:
// if.js /** * 此函数在初次解析v-if节点的时候执行 * 作用是用一个注释节点占据原先的v-if节点位置 * (其实就差不多相当于:对于文本节点,就用一个空的文本节点代替他一样。 */ exports.bind = function () { let el = this.el; // 这个注释节点就是用来占位的,好让我们记住原先的b-if指令DOM结构在哪儿 this.ref = document.createComment(`${config.prefix}-if`); _.after(this.ref, el); _.remove(el); this.inserted = false; }; /** * 当v-if指令依赖的数据发生变化时触发此更新函数 * @param value {Boolean} true/false 表示显示还是不显示该节点 */ exports.update = function (value) { if (value) { // 挂载子实例 if (!this.inserted) { if (!this.childBM) { this.build(); } this.childBM.$before(this.ref); // 这里其实就是将子实例插入DOM this.inserted = true; } } else { // 卸载子实例 if (this.inserted) { this.childBM.$remove(); // 这里其实就是将子实例移出DOM this.inserted = false; } } }; /** * 这个build比较吊 * 因为对于一个 "v-if" 结构来说, 远比一个普通的文本节点要复杂。 * 所以对弈v-if节点不能当成普通的节点来处理, 它更像是一个子的vue实例 * 所以我们将整个v-if节点当成是另外一个vue实例, 然后实例化它 */ exports.build = function () { this.childBM = new _.Bue({ el: this.el // 这个this.el就是#sub_app }); };
实现效果如下,这个版本的代码在这儿。
然而,我们可以发现上面的做法存在一个很重大的bug。 考虑如下情况:
<div id="app"> <p>姓名:{{user.name}}</p> <p>年龄:{{user.age}}</p> <div b-if="show" id="sub_app"> <h1>如果show为真,我们就显示{{user.name}}</h1> <h1>如果show为真,我们就显示{{user.age}}</h1> </div> </div>
实际效果却是如下图所示(直接报错):
问题:对于b-if条件渲染,为什么加入了user.name和user.age之后,程序就报错呢? 这显然是不合理的,因为我们知道:必须做到条件渲染里面也可以渲染变量,我们看看如何解决这个问题。
为什么上面的程序会出现这样的bug呢? 通过debug代码我们发现了核心原因:因为实例化子实例#sub_app的时候我们压根没给它传data数据,所以子实例本身并没有自己的数据,所以根本拿不到user,更别说是user.name了。 但是按照我们正常的想法,即便这是一个条件渲染,也应该能够访问父实例所有的变量才对啊! so,这就引出了一个重要的概念:作用域 在我们探索v-if指令之前,一直都只有一个vue实例,它有自己的数据,所以不存在作用域的问题。但是,当一个实例嵌套另外一个实例的时候,子实例的的作用域又是什么呢? 其实这是一个非常宽泛的问题,包括后期我们想实现组件化的时候,这个问题肯定是绕不过去的。 但是,目前组件化作用域这个问题对于我来说太难了。假如我们现在把问题简单化一些,只考虑实现v-if呢? 我们发现一个非常便利的地方:v-if的子实例的作用域完全等价于父实例的作用域。所以,我们通过下面的代码,将父实例的作用域传递到子实例。
// init.js if (this.$parent) { this.$data = options.parent.$data; } else { this.$data = options.data || {}; }
解决了作用域的问题,那么在子实例中就可以访问父实例的数据了。(喜大普奔~~) 但是,我们还有一个大问题:设想以下情景:当修改父实例的数据user.name时,父实例的observer能监听到,然后就会触发父实例的_updateBindingAt,然后就会将一系列watcher放到bathcer队列中去,最后父实例中的DOM元素就得到了更新。但是子实例中的user.name没有跟着更新啊!!为毛?因为子实例自己的observer为空啊!! 所以我们需要将父实例中的observer对象也一并传过来!!
if (this.$parent) { this.observer = this.$parent.observer; } else { this.observer = Observer.create(data); }
至此,我们就实现了带变量的条件渲染了。具体的效果如下图所示,这个版本的代码在这里
在写这个v-if条件渲染的时候,我参考的vue版本是这个。然而,在这个版本中,其实作者只实现了不带变量的情况,并没有实现带变量的情况。对于带变量的实现方法,是我自己想的,所以显得非常简单粗暴。 做到这儿,我能感觉到,之后作用域问题将会是一个非常核心的问题,需要好好思考思考。
时间:2016/9/22 内容:当我去实现v-repeat列表渲染的时候,发现本篇采用的直接传递$data和observer的方法无法解决列表渲染的作用域问题,所以用原型链的方式重写了这一部分,可以参考下一篇中的具体解释。
只需要if.js中
exports.build = function () { this.childBM = new _.Bue({ el: this.el, data: this.vm.$data//添加,只需要添加这句,就可以解决 }); };
前言
相信大家都用过vue非常好用的v-if功能,那么它是如何实现的呢?回顾一下之前我们已经实现的动态数据绑定 #87 ,我们动态绑定的是一个普通文本节点和一个数据之间的关系。当数据发生改变时,修改文本节点值。 但是,我们现在要做的是,当数据发生改变时,渲染插入某个节点或者把某个节点从DOM中移除,而且这个节点不是普通的文本节点。 所以,我们不能照搬之前的那一套,需要做一些改动。
问题具象化
考虑下面的例子 这个例子是较为简单的例子,因为b-if里面不包含user.name这样的变量,我称它为不带变量的条件渲染
问题是:如何做到,当show为true时,渲染整个div。当show为false,不渲染整个div。
不带变量的条件渲染
首先,b-if指令对应的div结构内部可能是一个很复杂的DOM结构(比如上面的例子,b-if指令内部就包含两个h1标签),所以,我们更应该把"b-if"对应的DOM结构看成是一个新的vue实例,而非一个普通的Directive。我们将要实现的是:一个vue实例嵌套另一个vue实例,父实例是#app,子实例是#sub_app。 如何做到呢?我们从修改渲染节点函数入手:
代码写到这儿,我们就可以看到_directive数组中就多了一个show的Directive了,如下图所示。
然后,接下来是重点。 对于一个受指令控制的DOM节点,如例子中的b-if,它其实至少有两个生命周期:一个是初始化,第一次解析DOM的时候,我们称之为bind;另一个是当数据变化时,DOM节点会更新,我们称之为update。 回想一下,我们之前构造Directive的时候其实就已经隐含这样的思想,如下面代码所示(这是之前就有的代码)
所以,得出的结论是:我们需要为b-if指令也定义这样的bind和update方法,分别完成初始化和更新的动作。 所以就有了下面的代码:
实现效果如下,这个版本的代码在这儿。
Bug
然而,我们可以发现上面的做法存在一个很重大的bug。 考虑如下情况:
实际效果却是如下图所示(直接报错):
问题:对于b-if条件渲染,为什么加入了user.name和user.age之后,程序就报错呢? 这显然是不合理的,因为我们知道:必须做到条件渲染里面也可以渲染变量,我们看看如何解决这个问题。
带变量的条件渲染
为什么上面的程序会出现这样的bug呢? 通过debug代码我们发现了核心原因:因为实例化子实例#sub_app的时候我们压根没给它传data数据,所以子实例本身并没有自己的数据,所以根本拿不到user,更别说是user.name了。 但是按照我们正常的想法,即便这是一个条件渲染,也应该能够访问父实例所有的变量才对啊! so,这就引出了一个重要的概念:作用域 在我们探索v-if指令之前,一直都只有一个vue实例,它有自己的数据,所以不存在作用域的问题。但是,当一个实例嵌套另外一个实例的时候,子实例的的作用域又是什么呢? 其实这是一个非常宽泛的问题,包括后期我们想实现组件化的时候,这个问题肯定是绕不过去的。 但是,目前组件化作用域这个问题对于我来说太难了。假如我们现在把问题简单化一些,只考虑实现v-if呢? 我们发现一个非常便利的地方:v-if的子实例的作用域完全等价于父实例的作用域。所以,我们通过下面的代码,将父实例的作用域传递到子实例。
解决了作用域的问题,那么在子实例中就可以访问父实例的数据了。(喜大普奔~~) 但是,我们还有一个大问题:设想以下情景:当修改父实例的数据user.name时,父实例的observer能监听到,然后就会触发父实例的_updateBindingAt,然后就会将一系列watcher放到bathcer队列中去,最后父实例中的DOM元素就得到了更新。但是子实例中的user.name没有跟着更新啊!!为毛?因为子实例自己的observer为空啊!! 所以我们需要将父实例中的observer对象也一并传过来!!
至此,我们就实现了带变量的条件渲染了。具体的效果如下图所示,这个版本的代码在这里
后话
在写这个v-if条件渲染的时候,我参考的vue版本是这个。然而,在这个版本中,其实作者只实现了不带变量的情况,并没有实现带变量的情况。对于带变量的实现方法,是我自己想的,所以显得非常简单粗暴。 做到这儿,我能感觉到,之后作用域问题将会是一个非常核心的问题,需要好好思考思考。
更新
时间:2016/9/22 内容:当我去实现v-repeat列表渲染的时候,发现本篇采用的直接传递$data和observer的方法无法解决列表渲染的作用域问题,所以用原型链的方式重写了这一部分,可以参考下一篇中的具体解释。