本文共 9520 字,大约阅读时间需要 31 分钟。
时时监听数据变化, 一旦数据发生变化就更新界面
在vue模板的写法
{
{ name }}
当你在input
标签内进行输入时候,随后p
标签的内容也会随之改变.
defineProperty
方法去帮助vue实现实时监听数据变化的那么下面就是介绍此方法:
defineProperty
注:以下介绍一些常见的属性,更多属性去官网上看
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
语法Object.defineProperty(obj, prop, descriptor)
参数解析:
descriptor
的值
value: 'yyy'
----> 该属性对应的值。默认为
writable: true
------> 当且仅当该属性的 writable
键值为 true
时,属性的值,也就是上面的 value
,才能被改变。默认为 false
configurable
当且仅当该属性的 configurable
键值为 true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
false
。 enumerable
当且仅当该属性的 enumerable
键值为 true
时,该属性才会出现在对象的枚举属性中。
false
。 get
属性的 getter 函数,该函数的返回值会被用作属性的值。默认为 。
set
属性的 setter 函数,该函数的返回值会被用作属性的值。默认为 。
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
注意:如果设置了get/set方法, 那么就不能通过value直接赋值, 也不能编写writable:true 换句话说数据描述符和存取描述符不能同时存在
以上都是准备知识,
为了方便管理代码,Vue 使用了一个 Observe 类专门来处理对象的监听,在初始化类时将需要监听的对象传入即可。
Observer
对象constructor
对所创建的Observer
进行初始化(都添加defineProperty
)object
的defineProperty
添加对应的描述符class Observer{//第一步 // 只要将需要监听的那个对象传递给Observer这个类 // 这个类就可以快速的给传入的对象的所有属性都添加get/set方法 constructor(data){//第二步 this.observer(data); } observer(obj){ if(obj && typeof obj === 'object'){ // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法 for(let key in obj){ this.defineRecative(obj, key, obj[key]) } } } // obj: 需要操作的对象 // attr: 需要新增get/set方法的属性 // value: 需要新增get/set方法属性的取值 defineRecative(obj, attr, value){//第三步 // 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法 this.observer(value);//第四步 Object.defineProperty(obj, attr, { get(){ return value; }, set:(newValue)=>{ if(value !== newValue){ // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法 this.observer(newValue); value = newValue; console.log('监听到数据的变化, 需要去更新UI'); } } }) } } new Observer(obj);
以上就是利用原生js实现数据双向绑定
let vue = new Nue({ // 3.告诉Vue的实例对象, 将来需要控制界面上的哪个区域 el: '#app', // el: document.querySelector('#app'), // 4.告诉Vue的实例对象, 被控制区域的数据是什么 data: { name: "yyy", age: 33 } }); console.log(vue.$el); console.log(vue.$data);
回到vue实例可以分析出
要想使用Vue必须先创建Vue的实例, 创建Vue的实例通过new来创建, 所以说明Vue是一个类
Vue就会根据指定的区域$el和数据$data, 去编译渲染这个区域
el 可以使 id 名,也可以是 dom 元素。
vue 实例会将传递的控制区域和数据都绑定到创建出来的实例对象上也就是 dom 与 data 分别绑定到$el 和$data 上
上面是对实例进行分析,接下来我们可以开始写简单的属于自己的实例
步骤:
nue
nue.js
class Nue { constructor(options){ // 1.保存创建时候传递过来的数据 if(this.isElement(options.el)){ this.$el = options.el; }else{ this.$el = document.querySelector(options.el); } this.$data = options.data; // 2.根据指定的区域和数据去编译渲染界面 if(this.$el){ new Compiler(this)//这一块可能一开始看到不太懂什么意思? ---> 主要是用于数据更新后,重新渲染页面 console.log('this: ', this); } } // 判断是否是一个元素 isElement(node){ return node.nodeType === 1; }}class Compiler { constructor(vm){ this.vm = vm; console.log('this.vm: ', this.vm); }}
至于index.html
中的new vue
换成 new nue
即可
此时你也经写出了一个简单的vue实例,但是也有问题就是没有渲染页面,(没有将name
渲染到他的p
标签中)
接下来我们看如何渲染页面
v-model
,{ {}}
的符号我们开始第一步:
先介绍准备知识
DocumentFragment
,文档片段接口,一个没有父对象的最小文档对象
我们会用到一些方法:
Document.createDocumentFragment()
---->创建一个新的空白的文档片段( )。appendChild
将元素添加到文档碎片中firstChild
获取此时第一个元素class Compiler { constructor(vm){ this.vm = vm; // 1.将网页上的元素放到内存中 let fragment = this.node2fragment(this.vm.$el); console.log(fragment); // 2.利用指定的数据编译内存中的元素 // 3.将编译好的内容重新渲染会网页上 } node2fragment(app){ // 1.创建一个空的文档碎片对象 let fragment = document.createDocumentFragment(); // 2.编译循环取到每一个元素 let node = app.firstChild; while (node){ // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失 fragment.appendChild(node); node = app.firstChild; } // 3.返回存储了所有元素的文档碎片对象 return fragment; }}
回答上面一个问题:
如果你使用遍历循环去找具有
v-model
的元素和{ {name}}
的元素,找到了然后去渲染对应的值name
,但是每次你的值name
进行改变时候,那么就会重新渲染那一个区域,多次改变就会多次重新渲染,就会导致浏览器性能降低,而我们将控制区域存在内存中,在内存中找到,在内存中将数据替换好,之后再去渲染,那么此时只需要渲染一次.其实有这样一个案例,你需要利用appendchild去添加子元素,此时你需要添加10000000个,如果你仅仅不断的用appendchild,会导致我加一个元素,我需要将全部重新渲染一遍,其实是不需要的,你可以利用string去将那么多的子元素连在一起,只需要用一次appendchild
其实这个想法和之前学的在dom树中增加元素是一个道理.(利用
string
,在此基础上增加多个元素,然后再放在浏览器中渲染)
我们开始第二步甚至第三步(一起进行):
v-model
属性(眼睛也不要只盯着v-model
例如v-text
v-html
都是一个道理只是多了一段代码罢了){ {}}
**的内容class Compiler { constructor(vm){ this.vm = vm; // 1.将网页上的元素放到内存中 let fragment = this.node2fragment(this.vm.$el); // 2.利用指定的数据编译内存中的元素 this.buildTemplate(fragment); // 3.将编译好的内容重新渲染会网页上 } node2fragment(app){ // 1.创建一个空的文档碎片对象 let fragment = document.createDocumentFragment(); // 2.编译循环取到每一个元素 let node = app.firstChild; while (node){ // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失 fragment.appendChild(node); node = app.firstChild; } // 3.返回存储了所有元素的文档碎片对象 return fragment; } buildTemplate(fragment){ let nodeList = [...fragment.childNodes]; nodeList.forEach(node=>{ // 需要判断当前遍历到的节点是一个元素还是一个文本 // 如果是一个元素, 我们需要判断有没有v-model属性 // 如果是一个文本, 我们需要判断有没有{ {}}的内容 if(this.vm.isElement(node)){ // 是一个元素 this.buildElement(node); console.log('node: ', node); // 处理子元素(处理后代) this.buildTemplate(node); }else{ // 不是一个元素 this.buildText(node); console.log('node1: ', node.textContent); } }) } buildElement(node){ let attrs = [...node.attributes]; attrs.forEach(attr => { let { name, value} = attr; if(name.startsWith('v-')){ console.log('是Vue的指令, 需要我们处理', name); } }) } buildText(node){ let content = node.textContent; let reg = /\{\{.+?\}\}/gi; if(reg.test(content)){ console.log('是{ {}}的文本, 需要我们处理', content); } }}
说明:
当你去把从文档碎片获取的元素打印出来时候你可以发现其实空格也是元素之一(当作文本)
如果你这样写<div id="app"><input type="text" v-model="name"><p>{ {name}}</p></div>
看下面输出:
是不是得到了上面的结论!!
如果你没有上诉的结论,你肯定觉得文本就是元素之间的你写的那些文本,而这样恰恰相反.因为你写完了你会得到这样的结果
就只看到了vue的指令,没有vue的文本,这个时候你就需要像元素里面再进行一次寻找(不断递归)
此时你就看到了你想要看到的结果
首先我们需要考虑的是指令,不仅仅我们才讨论到的v-model
还有v-html
v-text
,所以鉴于指令较多,我们可以在前面定义一个类,对对应的指令进行相关的操作
let CompilerUtil = { model: function (node, value, vm) { }, html: function (node, value, vm) { }, text: function (node, value, vm) { }}
此时,我们在编译指令的地方进行处理
v-model
中的model
data
值—>此时的this
buildElement(node){ let attrs = [...node.attributes]; attrs.forEach(attr => { let { name, value} = attr; // v-model="name" / name:v-model / value:name if(name.startsWith('v-')){ // v-model / v-html / v-text / v-xxx let [_, directive] = name.split('-'); // v-model -> [v, model] CompilerUtil[directive](node, value, this.vm); } }) }
调用函数处理好之后,我们可以写之前定义的类中的函数.
可能到这里,会有一点疑惑,就是为什么要编译指令.我拿
v-model
举例,编译指令,是将v-model
的值渲染到你写的input
上,然而对于v-html
也是如此
在这里就一段代码的事情
node.value = vm.$data[value];
将指令的值渲染到对应的元素上
此时,依旧看不到页面,因为元素全部在内存中,以上的操作都是对内存中的元素进行操作的,所以当你编译好元素后,需要将编译好的内容重新放到html
上,所以在编译网页后加一段代码
this.vm.$el.appendChild(fragment);
将内容重新加入到html
上
你可能觉得写完了,但是其实存在一个问题,先看看问题所在!!
你定义的data肯定不只是name:"yyy"
这样子的,肯定也会用json
进行嵌套
其实此时的input
是不会显示time
中的属性的
为什么会出现undefined
这个情况呢?
你的
v-model
写法与上图无异,这就导致buildElement
这个函数中获取到的v-model
的值不再是name
这样简单的了,获取的是time.h
,所以你又怎么可能通过vm.$data[time.h]
找到呢,并且这样写法也是错的.
既然出现这个问题,那么我们应该怎么解决呢?
其实也不难,就先获取time的值然后在获取time.h的值,可能描述不是很清晰.
data[time]
->data[time].h
这不就拿到了time.h
值了吗!!
一些视频可能会推荐你去使用reduce
去实现这个.
但是在此我发表一下自己的浅显的看法:
当初学习
reduce
时候,我就觉得这个函数很傻,用reduce基本上那些可以用foreach实现,或者for感觉不知道哪一个场景使用他更好,而且对我来说代码很不清晰.可能我不是大佬吧!!!所以一般别人用reduce我就会用foreach去进行替换
reduce实现
getValue(vm, value) { //time.h --> [time, h] let x = value.split('.').reduce((data, currentKey) => { // 第一次执行: data=$data, currentKey=time // 第二次执行: data=time, currentKey=h console.log('data[currentKey]: ', data[currentKey]); return data[currentKey]; }, vm.$data); console.log('x: ', x); return x; }
foreach实现
data = vm.$data; let data2; value.split('.').forEach((ele, item) => { data2 = data[ele]; console.log('data2: ', data2); data = data[ele]; }); return data2;
那么对应的model
的函数可能就不能那么写的了
let val = this.getValue(vm, value);node.value = val;
html
和text
就是一样的走法
转载地址:http://gxdki.baihongyu.com/