vue源码分析—数据代理,模板解析,数据绑定

前言

本篇文章 分析 vue 作为一个 MVVM 框架的基本实现原理 :数据代理 ;模板解析 ;数据绑定

不直接看 vue.js 的源码 ,剖析 github 上某基友仿 vue 实现的 mvvm 库  地址:https://github.com/DMQ/mvvm
 

 

必备的知识点

在开始分析vue源码之前,必须要知道的以下知识点

[].slice.call(lis): 将伪数组转换为真数组

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<ul id="fragment_test">
  <li>test1</li>
  <li>test2</li>
  <li>test3</li>
</ul>

<script type="text/javascript">
  const lis = document.getElementsByTagName('li') // lis是伪数组(是一个特别的对象, 具有length和数值下标属性)

  // 判断lis是否是对象,是否是真数组
  console.log(lis instanceof Object, lis instanceof Array)

  // 数组的slice()截取数组中指定部分的元素, 生成一个新的数组  [1, 3, 5, 7, 9], slice(0, 3)
  // slice2(),数组的slice()方法实现方式
  Array.prototype.slice2 = function (start, end) {
    start = start || 0
    end = start || this.length
    const arr = []
    for (var i = start; i < end; i++) {
      arr.push(this[i])
    }
    return arr
  }

  const lis2 = Array.prototype.slice.call(lis)  // lis.slice()
  console.log(lis2 instanceof Object, lis2 instanceof Array)
  // lis2.forEach() //lis2已经是真数组,可以使用forEach进行遍历
</script>
</body>
</html>

或者使用ES6中的语法

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<ul id="fragment_test">
  <li>test1</li>
  <li>test2</li>
  <li>test3</li>
</ul>

<script type="text/javascript">
  const lis = document.getElementsByTagName('li') // lis是伪数组(是一个特别的对象, 具有length和数值下标属性)

  // 判断lis是否是对象,是否是真数组
  console.log(lis instanceof Object, lis instanceof Array) // true ,false

  const lis2 = Array.from(lis)  // lis.slice()
  console.log(lis2 instanceof Object, lis2 instanceof Array) // true , true
  // lis2.forEach() //lis2已经是真数组,可以使用forEach进行遍历
</script>
</body>
</html>

node.nodeType: 得到节点类型

节点类型:document(整个html文档),Element(某个标签元素),attr(标签元素的属性),text(标签元素的文本内容)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="test">test</div>
<script type="text/javascript">
  const elementNode = document.getElementById('test')
  const attrNode = elementNode.getAttributeNode('id')
  const textNode = elementNode.firstChild
  console.log(elementNode.nodeType, attrNode.nodeType, textNode.nodeType)
</script>
</body>
</html>

Object.defineProperty(obj, propertyName, {}): 给对象添加/修改属性(指定描述符)

PS:IE8以下是不支持这个语法的,vue不支持IE8的根本原因也是这个语法不支持

configurable: true/false(默认)  是否可以重新define
enumerable: true/false(默认) 是否可以枚举(for..in / keys())
value: 指定初始值
writable: true/false(默认) value是否可以修改存取(访问)描述符
get: 函数, 用来得到当前属性值
set: 函数, 用来监视当前属性值的变化

  Object.defineProperty(obj, 'fullName', {
    configurable: false, //是否可以重新define
    enumerable: true, // 是否可以枚举(for..in / keys())
    value: 'A-B', // 指定初始值
    writable: false // value是否可以修改
  })
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<script type="text/javascript">
  const obj = {
    firstName: 'A',
    lastName: 'B'
  }
  // 给obj这个对象添加fullname属性,obj.fullName = 'A-B'
  Object.defineProperty(obj, 'fullName', {
    // 整个是属性描述符

    // 数据描述符

    // 访问描述符
    // 当读取对象此属性值时自动调用, 将函数返回的值作为属性值, this为obj
    get () {
      return this.firstName + "-" + this.lastName
    },
    // 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this为obj
    set (value) {
      const names = value.split('-')
      this.firstName = names[0]
      this.lastName = names[1]
    }
  })

  console.log(obj.fullName) // A-B
  obj.fullName = 'C-D'
  console.log(obj.firstName, obj.lastName) // C D

  Object.defineProperty(obj, 'fullName2', {
    configurable: false, //是否可以重新define
    enumerable: true, // 是否可以枚举(for..in / keys())
    value: 'A-B', // 指定初始值
    writable: false // value是否可以修改
  })
  console.log(obj.fullName2)  // A-B
  obj.fullName2 = 'E-F'
  console.log(obj.fullName2) // A-B
</script>
</body>
</html>

Object.keys(obj): 得到对象自身可枚举的属性名的数组

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<script type="text/javascript">
  const obj = {
    firstName: 'A',
    lastName: 'B'
  }
  // 给obj这个对象添加fullname属性,obj.fullName = 'A-B'
  Object.defineProperty(obj, 'fullName', {
    // 整个是属性描述符

    // 数据描述符

    // 访问描述符
    // 当读取对象此属性值时自动调用, 将函数返回的值作为属性值, this为obj
    get () {
      return this.firstName + "-" + this.lastName
    },
    // 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this为obj
    set (value) {
      const names = value.split('-')
      this.firstName = names[0]
      this.lastName = names[1]
    }
  })

  console.log(obj.fullName) // A-B
  obj.fullName = 'C-D'
  console.log(obj.firstName, obj.lastName) // C D

  Object.defineProperty(obj, 'fullName2', {
    configurable: false, //是否可以重新define
    enumerable: true, // 是否可以枚举(for..in / keys())
    value: 'A-B', // 指定初始值
    writable: false // value是否可以修改
  })
  console.log(obj.fullName2)  // A-B
  obj.fullName2 = 'E-F'
  console.log(obj.fullName2) // A-B

  const names = Object.keys(obj) // 其中fullName是不可以枚举的
  console.log(names)
</script>
</body>
</html>

DocumentFragment: 文档碎片(高效批量更新多个节点)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<ul id="fragment_test">
  <li>test1</li>
  <li>test2</li>
  <li>test3</li>
</ul>
<script type="text/javascript">
  // document: 对应显示的页面, 包含n个elment  一旦更新document内部的某个元素界面更新(做法是遍历每个li,然后依次替换)
  // documentFragment: 内存中保存n个element的容器对象(不与界面关联), 如果更新framgnet中的某个element, 界面不变
  /*
  <ul id="fragment_test">
    <li>test1</li>
    <li>test2</li>
    <li>test3</li>
  </ul>
   */
  const ul = document.getElementById('fragment_test')
  // 1. 创建fragment
  const fragment = document.createDocumentFragment()
  // 2. 取出ul中所有子节点取出保存到fragment
  let child
  while(child = ul.firstChild) { // 一个节点只能有一个父亲
    fragment.appendChild(child)  // 先将child从ul中移除, 添加为fragment子节点
  }

  // 3. 更新fragment中所有li的文本(先将伪数组变成真数组)
  Array.prototype.slice.call(fragment.childNodes).forEach(node => {
    if (node.nodeType===1) { // 元素节点 <li>
      node.textContent = 'fragment'
    }
  })

  // 4. 将fragment插入ul
  ul.appendChild(fragment)

</script>
</body>
</html>

obj.hasOwnProperty(prop): 判断prop是否是obj自身的属性(原型链上的是不属于自身的属性)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<script type="text/javascript">
  const obj = {
    firstName: 'A',
    lastName: 'B'
  }
  // 给obj这个对象添加fullname属性,obj.fullName = 'A-B'
  Object.defineProperty(obj, 'fullName', {
    get () {
      return this.firstName + "-" + this.lastName
    },
    // 当修改了对象的当前属性值时自动调用, 监视当前属性值的变化, 修改相关的属性, this为obj
    set (value) {
      const names = value.split('-')
      this.firstName = names[0]
      this.lastName = names[1]
    }
  })

  console.log(obj.hasOwnProperty('fullName'), obj.hasOwnProperty('toString'))  // true false
</script>
</body>
</html>

 

 

Vue 源码分析—数据代理

数据代理: 通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)

vue 数据代理: 通过 vm 对象来代理 data 对象中所有属性的操作

好处: 更方便的操作 data 中的数据

基本实现流程

  • 通过 Object.defineProperty()给 vm 添加与 data 对象的属性对应的属性描述符
  • 所有添加的属性都包含 getter/setter
  • getter/setter 内部去操作 data 中对应的属性数据
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>02_数据代理_vue</title>
</head>
<body>

<div id="test"></div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  const vm = new Vue({
    el: "#test",
    data: {
      name: '张三'
    }
  })
  console.log(vm)
  console.log(vm.name)  // 读取的是data中的name,  vm代理对data的读操作
  vm.name = '李四' // 数据保存到data中的name上, vm代理对data的写操作
  console.log(vm.name, vm._data.name)
</script>
</body>
</html>

接下来我们来分析数据代理的过程是怎么样的

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>数据代理</title>
</head>
<body>
<!--
1. vue数据代理: data对象的所有属性的操作(读/写)由vm对象来代理操作
2. 好处: 通过vm对象就可以方便的操作data中的数据
3. 实现:
  1). 通过Object.defineProperty(vm, key, {})给vm添加与data对象的属性对应的属性
  2). 所有添加的属性都包含get/set方法
  3). 在get/set方法中去操作data中对应的属性
-->
<script type="text/javascript" src="js/mvvm/compile.js"></script>
<script type="text/javascript" src="js/mvvm/mvvm.js"></script>
<script type="text/javascript" src="js/mvvm/observer.js"></script>
<script type="text/javascript" src="js/mvvm/watcher.js"></script>
<script type="text/javascript">
  const vm = new MVVM({
    el: "#test",
    data: {
      name: '张三2'
    }
  })
  console.log(vm.name)  // 读取的是data中的name,  vm代理对data的读操作
  vm.name = '李四2' // 数据保存到data中的name上, vm代理对data的写操作
  console.log(vm.name, vm._data.name)
</script>
</body>
</html>

下面代码是模拟vue源码中的vue构造函数的代码片段,这片代码就是数据代理的整个流程

// 相关于Vue的构造函数

function MVVM(options) {
  // 将选项对象保存到vm
  this.$options = options;
  // 将data对象保存到vm和data变量中
  var data = this._data = this.$options.data;
  //将vm保存在me变量中
  var me = this;
  // 遍历data中所有属性
  Object.keys(data).forEach(function (key) { // 属性名: name
    // 对指定属性实现代理
    me._proxy(key);
  });

  // 对data进行监视
  observe(data, this);

  // 创建一个用来编译模板的compile对象
  this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
  $watch: function (key, cb, options) {
    new Watcher(this, key, cb);
  },

  // 对指定属性实现代理
  _proxy: function (key) {
    // 保存vm
    var me = this;
    // 给vm添加指定属性名的属性(使用属性描述)
    Object.defineProperty(me, key, {
      configurable: false, // 不能再重新定义
      enumerable: true, // 可以枚举
      // 当通过vm.name读取属性值时自动调用
      get: function proxyGetter() {
        // 读取data中对应属性值返回(实现代理读操作)
        return me._data[key];
      },
      // 当通过vm.name = 'xxx'时自动调用
      set: function proxySetter(newVal) {
        // 将最新的值保存到data中对应的属性上(实现代理写操作)
        me._data[key] = newVal;
      }
    });
  }
};

 

 

Vue 源码分析—模板解析

模板解析的基本流程

将 el 的所有子节点取出, 添加到一个新建的文档 fragment 对象中

对 fragment 中的所有层次子节点递归进行编译解析处理

  • 对大括号表达式文本节点进行解析
  • 对元素节点的指令属性进行解析 :事件指令解析 ,一般指令解析 

将解析后的 fragment 添加到 el 中显示

第一步:在MVVM构造函数中创建compile实例对象,将挂在的el元素和vm对象传递给compile构造函数

第二步:在compile构造函数中判断el存在不存在后,取出el下所有的子节点,存放到fragment对象中

第三步:编译fragment中所有层次的子节点

  • 第一步:将fragment传递给compileElement()方法,compileElement()方法内得到所有子节点继续遍历,判断是那种类型的节点
  • 第二步:如果是元素节点(如果是,走编译节点的指令属性的方法)还是大括号表达式的文本节点(走编译大括号表达式的的方法)

第四步:将fragment添加到el中

模板解析(1): 大括号表达式解析

根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1 name

从 data 中取出表达式对应的属性值

将属性值设置为文本节点的 textContent

第一步:调用compileText()方法,然后再调用compileUtil对象下的text方法,然后再去调用compileUtil对象下的bind方法(其实就是解析v-text这个指令)

compileText: function (node, exp) {
  // 调用编译工具对象解析
  compileUtil.text(node, this.$vm, exp);
},
// 解析: v-text/{{}}
text: function (node, vm, exp) {
  this.bind(node, vm, exp, 'text');
},

第二步:拼接方法名,赋给变量updaterFn,然后执行这个updaterFn方法(其实就是执行了updater对象下的textUpdater方法)并且将_getVMVal方法返回的数据传给这个方法

// 得到表达式对应的value
_getVMVal: function (vm, exp) {
  var val = vm._data;
  exp = exp.split('.');
  exp.forEach(function (k) {
    val = val[k];
  });
  return val;
},
// 真正用于解析指令的方法
bind: function (node, vm, exp, dir) {
  /*实现初始化显示*/
  // 根据指令名(text)得到对应的更新节点函数
  var updaterFn = updater[dir + 'Updater'];
  // 如果存在调用来更新节点
  updaterFn && updaterFn(node, this._getVMVal(vm, exp));

  // 创建表达式对应的watcher对象
  new Watcher(vm, exp, function (value, oldValue) {/*更新界面*/
    // 当对应的属性值发生了变化时, 自动调用, 更新对应的节点
    updaterFn && updaterFn(node, value, oldValue);
  });
},

第三步:更新这个文本节点的内容

// 更新节点的textContent
textUpdater: function (node, value) {
  node.textContent = typeof value == 'undefined' ? '' : value;
},

第四步:

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值