# vue-study-writeCode **Repository Path**: yangaoyu/vue-study-writeCode ## Basic Information - **Project Name**: vue-study-writeCode - **Description**: 学习手写vue重要部分和插件 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-03-01 - **Last Updated**: 2021-06-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue源码解析 ## 双向绑定数据 ### 1.原理 时时监听数据变化, 一旦数据发生变化就更新界面 在vue模板的写法 ```html

{{ name }}

``` 当你在`input` 标签内进行输入时候,随后`p`标签的内容也会随之改变. ### 2.如何实现 * 通过原生JS的`defineProperty` 方法去帮助vue实现实时监听数据变化的 那么下面就是介绍此方法: #### 2.1`defineProperty` 注:==以下介绍一些常见的属性,更多属性去官网上看== [官网介绍](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) * `Object.defineProperty()` 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。 * 语法`Object.defineProperty(obj, prop, descriptor)` * 参数解析: * *obj: 需要操作的对象* * *prop: 需要操作的属性* * *descriptor: 属性描述符* (简单说就是对属性赋予操作能力) * `descriptor` 的值 * `value: 'yyy'` ----> 该属性对应的值。**默认为 [`undefined`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/undefined)** * `writable: true` ------> 当且仅当该属性的 `writable` 键值为 `true` 时,属性的值,也就是上面的 `value`,才能被[`赋值运算符`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Assignment_Operators)改变。**默认为 `false`** * `configurable` 当且仅当该属性的 `configurable` 键值为 `true` 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 **默认为** **`false`**。 * `enumerable` 当且仅当该属性的 `enumerable` 键值为 `true` 时,该属性才会出现在对象的枚举属性中。 **默认为 `false`**。 * `get` 属性的 getter 函数,该函数的返回值会被用作属性的值。**默认为 [`undefined`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/undefined)**。 * `set` 属性的 setter 函数,该函数的返回值会被用作属性的值。**默认为 [`undefined`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/undefined)**。 * | | configurable | enumerable | **value** | writable | **get** | set | | -------------- | ------------ | ---------- | ---------- | ---------- | ---------- | ---------- | | **数据描述符** | **可以** | **可以** | **可以** | **可以** | **不可以** | **不可以** | | **存取描述符** | **可以** | **可以** | **不可以** | **不可以** | **可以** | **可以** | 注意:*如果设置了get/set方法, 那么就不能通过value直接赋值, 也不能编写writable:true* 换句话说==数据描述符和存取描述符不能同时存在== 以上都是准备知识, ## 代码解析 为了方便管理代码,Vue 使用了一个 Observe 类专门来处理对象的监听,在初始化类时将需要监听的对象传入即可。 * 我们就先构造一个`Observer` 对象 * 利用`constructor ` 对所创建的`Observer` 进行初始化(都添加`defineProperty` ) * 对结构体`object` 的`defineProperty` 添加对应的描述符 * 从简单的结构体到复杂的结构体 ```JavaScript 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实例 ```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实例可以分析出 1. 要想使用Vue必须先创建Vue的实例, 创建Vue的实例通过new来创建, 所以说明Vue是一个类 2. Vue就会根据指定的区域$el和数据\$data, 去编译渲染这个区域 3. el 可以使 id 名,也可以是 dom 元素。 4. vue 实例会将传递的控制区域和数据都绑定到创建出来的实例对象上也就是 dom 与 data 分别绑定到\$el 和\$data 上 ![image-20210223164222541](https://i.loli.net/2021/02/23/chTPHMEjvkxrQl2.png) ### 创建nue实例 上面是对实例进行分析,接下来我们可以开始写简单的属于自己的实例 步骤: * 先创建类似于vue的类`nue` * 保存创建时候的\$el 和\$data * 根据\$el 和\$data 去进行渲染页面 `nue.js` ```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`,文档片段**接口,一个没有父对象的最小文档对象[文档](https://developer.mozilla.org/zh-CN/docs/Web/API/DocumentFragment) 我们会用到一些方法: * `Document.createDocumentFragment()` ---->创建一个新的空白的文档片段( [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/DOM/DocumentFragment))。 * `appendChild` 将元素添加到文档碎片中 * `firstChild` 获取此时第一个元素 ```js 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` 都是一个道理只是多了一段代码罢了) * 如果是一个文本, 我们需要判断有没有**`{{}}`**的内容 ```js 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](https://i.loli.net/2021/03/03/XoV9H1dGIYn8g4E.png) 如果你这样写`

{{name}}

` 看下面输出: ![image-20210303214054029](https://cdn.jsdelivr.net/gh/baici1/image-host/img/20210303214054.png) 是不是得到了上面的结论!! ### 3.渲染指令 [参考文件1](https://www.jianshu.com/p/5078188d9d24) [参考文件2](https://blog.csdn.net/weixin_47886687/article/details/113173300)