博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vue源码解析(1)--------双向绑定数据(未完)
阅读量:3967 次
发布时间:2019-05-24

本文共 9520 字,大约阅读时间需要 31 分钟。

vue源码解析

双向绑定数据

1.原理

时时监听数据变化, 一旦数据发生变化就更新界面

在vue模板的写法

{

{ name }}

当你在input 标签内进行输入时候,随后p标签的内容也会随之改变.

2.如何实现

  • 通过原生JS的defineProperty 方法去帮助vue实现实时监听数据变化的

那么下面就是介绍此方法:

2.1defineProperty

注:以下介绍一些常见的属性,更多属性去官网上看

  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

  • 语法Object.defineProperty(obj, prop, descriptor)

    • 参数解析:

      • 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 )
  • 对结构体objectdefineProperty 添加对应的描述符
  • 从简单的结构体到复杂的结构体
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实现数据双向绑定

vue实例

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实例可以分析出

  1. 要想使用Vue必须先创建Vue的实例, 创建Vue的实例通过new来创建, 所以说明Vue是一个类

  2. Vue就会根据指定的区域$el和数据$data, 去编译渲染这个区域

  3. el 可以使 id 名,也可以是 dom 元素。

  4. vue 实例会将传递的控制区域和数据都绑定到创建出来的实例对象上也就是 dom 与 data 分别绑定到$el 和$data 上

image-20210223164222541

创建nue实例

上面是对实例进行分析,接下来我们可以开始写简单的属于自己的实例

步骤:

  • 先创建类似于vue的类nue
  • 保存创建时候的$el 和$data
  • 根据$el 和$data 去进行渲染页面

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 标签中)

接下来我们看如何渲染页面

  1. 获取此时绑定的区域,将元素放在内存中-----------------------(为什么要放在内存中?为什么不可以直接遍历元素)
  2. 需要找到哪里使用了v-model ,
  3. 找到之后,我们还要去找那个标签有这样{
    {}}
    的符号
  4. 将数据渲染到对应的标签

1.获取元素放入内存

我们开始第一步:

先介绍准备知识

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,在此基础上增加多个元素,然后再放在浏览器中渲染)

2.寻找指令和模板

我们开始第二步甚至第三步(一起进行):

  • 我们既然把控制区域的元素都存在文档碎片中,那么需要取出来,列为一个数组
  • 此时我们需要判断取出来的是一个元素还是一个文本?
    • 如果是一个元素, 我们需要判断有没有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); } }}

说明:

当你去把从文档碎片获取的元素打印出来时候你可以发现其实空格也是元素之一(当作文本)

image-20210303213312449

如果你这样写<div id="app"><input type="text" v-model="name"><p>{

{name}}</p></div>

看下面输出:

image-20210303214054029

是不是得到了上面的结论!!

如果你没有上诉的结论,你肯定觉得文本就是元素之间的你写的那些文本,而这样恰恰相反.因为你写完了你会得到这样的结果

image-20210303215603461

就只看到了vue的指令,没有vue的文本,这个时候你就需要像元素里面再进行一次寻找(不断递归)

此时你就看到了你想要看到的结果

image-20210303215820730

3.渲染指令

首先我们需要考虑的是指令,不仅仅我们才讨论到的v-model 还有v-html v-text ,所以鉴于指令较多,我们可以在前面定义一个类,对对应的指令进行相关的操作

let CompilerUtil = {
model: function (node, value, vm) {
}, html: function (node, value, vm) {
}, text: function (node, value, vm) {
}}

此时,我们在编译指令的地方进行处理

  1. 获取指令的有效值 例如v-model 中的model
  2. 确定传递的参数
    1. 此时的元素节点
    2. 指令的值
    3. 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 进行嵌套

image-20210305172253339

其实此时的input 是不会显示time 中的属性的

image-20210305172340349

为什么会出现undefined 这个情况呢?

image-20210313155210896

你的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;

htmltext就是一样的走法

转载地址:http://gxdki.baihongyu.com/

你可能感兴趣的文章
按键驱动--platform设备的例子
查看>>
mini2440按键驱动及详细解释(转)
查看>>
mini2440按键驱动及详细解释(转)
查看>>
在中断上下文使用disable_irq()的…
查看>>
在中断上下文使用disable_irq()的…
查看>>
内核定时器
查看>>
内核定时器
查看>>
中断与内核定时器
查看>>
中断与内核定时器
查看>>
source&nbsp;insight的疑问
查看>>
source&nbsp;insight的疑问
查看>>
Linux输入子系统&nbsp;input_dev&nbsp;概述
查看>>
Linux输入子系统&nbsp;input_dev&nbsp;概述
查看>>
A&nbsp;new&nbsp;I/O&nbsp;memory&nbsp;access&nbsp;mechanis…
查看>>
A&nbsp;new&nbsp;I/O&nbsp;memory&nbsp;access&nbsp;mechanis…
查看>>
s3c2410时钟信号:FCLK、HCL…
查看>>
s3c2410时钟信号:FCLK、HCL…
查看>>
自旋锁与信号量(转载)
查看>>
自旋锁与信号量(转载)
查看>>
主函数main中变量(int&nbsp;argc…
查看>>