2020-12-30

浅谈vue原理(四)

  经过了前面这么久,我们已经弄好了数据劫持,数据代理,还通过了发布订阅模式实现了data中数据变化,页面上也会跟着变化;

  现在还差点东西,就是当页面上的数据变化的时候,data中的数据也能跟着变化,进而使得页面的数据都刷新成最新数据,这就是view->model这条线;

1.html准备

  首先我们需要准备一个input标签

<body> <div id="app"> <h1>呵呵:{{message}}</h1> <input type="text" v-model="message"> </div> <script src="./mvvm.js"></script> <script> // 自定义的myVue实例 let myVue = new MyVue({  el: '#app',  data: {  message: "hello world"  } }) </script></body>

 

2.数据双向绑定的实现

  之前说过,html标签中每一个{{}}占位符都是对应着一个Watcher,而且{{}}占位符处于文本节点中,我们在初始化的时候使用node.nodeType === 3 && reg.test(nodeText)条件,使用正则就能匹配文本节点中的{{}}占位符;

  现在在初始化的过程,添加新逻辑:首先需要找到input标签,我们可以使用node.nodeType === 1先找到元素节点,再找有v-model属性的节点,然后取出其中的属性值,这个属性就对应着data中的数据;

  初始化完成之后,有两种可能:

  (1)当我们手动的在input框中改变值,data中对应的属性值也应该发生变化,这个就添加监听事件去处理

node.addEventListener("input", e => {   // 获取input输入框中的值   let inputValue = e.target.value;   //input中修改的值同步到到data中去,这里又会触发该属性的set方法,set方法中又会触发发布订阅模式,将所有的Watcher都调用一遍   vm[exp] = inputValue;  })

 

(2)当手动的修改data中的数据,set方法中会触发所有的Watcher的回调函数,这里的input标签中的值也应该变化;

  而之前我们只是对{{xxx}}占位符的文本节点创建了Watcher,这种占位符节点的值肯定会刷新,但是input是元素节点,不会刷新!所以需要将这种带有v-model的元素节点也创建Watcher:

 

  这段逻辑完整的代码下图所示,初始化的过程中首先找到v-model所在标签的属性值,将data中对应的属性值覆盖input框中内容!而且还需要创建一个Watcher(Watcher注册逻辑之前说过)以及给input添加事件监听;

  事件监听用于初始化完成之后,用户手动修改input框中的值,触发data中的数据发生变化,进一步触发set方法的notify方法,调用所有的Watcher的回调函数,刷新页面数据;

 

全部的js代码:

function MyVue (options = {}) { //第一步:首先就是将实例化的对象给拿到,得到data对象 this.$options = options; this._data = this.$options.data; //第二步:数据劫持,将data对象中每一个属性都设置get/set方法 observe(this._data); //第三步:数据代理,这里就是将_data的对象属性放到myVue实例中一份,实际的数据还是_data中的 for (let key in this._data) { //这里的this代表当前myVue实例对象 Object.defineProperty(this, key, {  enumerable: true,  get () {  return this._data[key];  },  set (newVal) {  this._data[key] = newVal;  } }) } //第四步:compile模板,需要将el属性和当前myVue实例 compile(options.el, this)}function compile (el, vm) { return new Compile(el, vm);}function Compile (el, vm) { //将el代表的那个dom节点挂载到myVue实例中 vm.$el = document.querySelector(el); //创建虚拟节点容器树 let fragment = document.createDocumentFragment(); //将el下所有的dom节点都放到容器树中,注意appendChild方法,这里是将将dom节点移动到容器树中啊,不是死循环! while (child = vm.$el.firstChild) { // console.log('count:' + vm.$el.childElementCount); fragment.appendChild(child) }; //遍历虚拟节点中的所有节点,将真实数据填充覆盖这种占位符{{}} replace(fragment, vm); //将虚拟节点树中内容渲染到页面中 vm.$el.appendChild(fragment);}function replace (n, vm) { //遍历容器树中所有的节点,解析出{{}}里面的内容,然后将数据覆盖到节点中去 Array.from(n.childNodes).forEach(node => { console.log('nodeType:' + node.nodeType); let nodeText = node.textContent; let reg = /\{\{(.*)\}\}/; // 节点类型常用的有元素节点,属性节点和文本节点,值分别是1,2,3 //一定要弄清楚这三种节点,比如<p id="123">hello</p>,这个p标签整个的就是元素节点,nodeType==1 //id="123"可以看作是属性节点,nodeType==2 //hello 表示文本节点,nodeType==3 //因为占位符{{}}只在文本节点中,所以需要判断是否等于3 if (node.nodeType === 3 && reg.test(nodeText)) {  // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2。。。  let arr = RegExp.$1.split(".");  let val = vm;  // 这个for循环就是取出这样的值:myVue[name][user]  arr.forEach(i => {  val = val[i];  })  // 创建Watcher,最主要的是传入的这个回调函数,会覆盖node节点中的占位符{{xxx}}  new Watcher(vm, RegExp.$1, function (newVal) {  node.textContent = nodeText.replace(reg, newVal);  })  // 把值覆盖到虚拟节点的占位符{{}}这里  node.textContent = nodeText.replace(reg, val); } //这里遍历到元素的节点,例如:<p id="xx">aaa</p></p>,<input type="text v-model=" username"> //然后需要取到其中的vue相关的指令,例如v-model="xxx",一般是以v-开头的 if (node.nodeType === 1) {  // console.log(node);  let nodeAttributes = node.attributes;  Array.from(nodeAttributes).forEach(attr => {  let name = attr.name;  let exp = attr.value;  // 找到v-model指令,data中的数据填充到input框中  if (name.indexOf("v-model") == 0) {   node.value = vm[exp];  }  // data中的数据变化,input中的数据也要跟着变化  new Watcher(vm, exp, function (newVal) {   node.value = newVal;  })  node.addEventListener("input", e => {   // 获取input输入框中的值   let inputValue = e.target.value;   //input中修改的值同步到到data中去,这里又会触发该属性的set方法,set方法中又会触发发布订阅模式,将所有的Watcher都调用一遍   vm[exp] = inputValue;  })  }) } // 第一个遍历的节点是<div id="app">这一行后面的换行,nodeType等于3,但是没有占位符{{}},所以会进入到这里进行递归调用内部 //的每一个节点,直到找到文本节点而且占位符{{}} if (node.childNodes) {  replace(node, vm); } })}//数据劫持操作function observe (data) { // 如果data不是对象,就结束,不然递归调用会栈溢出的 if (typeof data !== 'object') return; return new Observe(data);}function Observe (data) { let dep = new Dep(); // 遍历data所有属性 for (let key in data) { let val = data[key]; //初始化的时候, data中就有复杂对象的时候,例如data: { message:{a:{b:1}}} ,就需要递归的遍历这个对象中每一个属性都添加get和set方法 observe(val); Object.defineProperty(data, key, {  enumerable: true,  get () {  // 订阅  Dep.target && dep.addSub(Dep.target);  return val;  },  set (newVal) {  if (val === newVal) return;  val = newVal;  //当后续可能因为业务逻辑使得_data.message = {name: "小王"},设置对象类型的属性值,就需要递归的给对象中{name: "小王"}的每个属性也添加get和set方法  //否则name是没有get/set方法的  observe(val);  dep.notify();  } }) }}// ===============================发布订阅===============================// 可以看做是公众号端function Dep () { // 存放每个用户的容器 this.subs = [];}//对外提供的api之一,供用户订阅Dep.prototype.addSub = function (sub) { this.subs.push(sub);}// 对外提供的api之二,遍历每个用户,给每个用法发信息Dep.prototype.notify = function () { this.subs.forEach(sub => { sub.update(); });}// 用户端function Watcher (vm, exp, fn) { // 这个可以看作是用户的标志;注意,这个fn一般是一个回调函数 this.vm = vm; this.exp = exp; this.fn = fn; Dep.target = this; let val = vm; let arr = exp.split("."); arr.forEach(item => { val = val[item]; }) Dep.target = null;}// 用户端提供的对外api,让公众号端使用Watcher.prototype.update = function () { let val = this.vm; let arr = this.exp.split("."); arr.forEach(item => { val = val[item]; }) this.fn(val);}

View Code








原文转载:http://www.shaoqun.com/a/504541.html

跨境电商:https://www.ikjzd.com/

oklink:https://www.ikjzd.com/w/1362

csa:https://www.ikjzd.com/w/904


经过了前面这么久,我们已经弄好了数据劫持,数据代理,还通过了发布订阅模式实现了data中数据变化,页面上也会跟着变化;  现在还差点东西,就是当页面上的数据变化的时候,data中的数据也能跟着变化,进而使得页面的数据都刷新成最新数据,这就是view->model这条线;1.html准备  首先我们需要准备一个input标签<body><divid="app&quo
抢注商标:抢注商标
贝贝网:贝贝网
成都极地海洋世界开门营业和关门时间 :成都极地海洋世界开门营业和关门时间
深圳盐田区有什么免费旅游景点?:深圳盐田区有什么免费旅游景点?
世园会接待游客55万人次_成为假期国内旅游新亮点 :世园会接待游客55万人次_成为假期国内旅游新亮点

No comments:

Post a Comment