记录一些vue与原生js的思考

DocumentFragment: DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到 DOM 中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用 DocumentFragment 处理节点,速度和性能远远优于直接操作 DOM。Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。

property 和 attribute 的区别
property: 先天的,好比人有手、眼睛、器官。有类型的,例如Boolean,Number,String等。可以像跟对象属性赋值一样进行修改操作。

1
2
<input type="radio" checked value="1" name="xiaolin">
document.querySelectorAll('input')[0].type = 'text'

attribute: 后天的,好比人有名字、学历。只能为String类型

1
2
3
document.querySelectorAll('input')[0].getAttribute('name')
document.querySelectorAll('input')[0].setAttribute('name',newval)
document.querySelectorAll('input')[0].hasAttribute('name')

DOM节点

DOM节点共有7种类型:

  • Document 整个文档树的顶层节点 但不是根节点
  • DocumentType doctype标签
  • Element 网页的其他各种标签
  • Attribute 标签的属性
  • Text 标签与标签之间的文本
  • Comment 注释
  • DocumentFragment文档的片段
1
2
3
4
5
6
7
8
9
10
11
12
13
document.getElementById('app').childNodes 
//所有子节点,元素节点、文本节点(包括回车)、注释节点
//nodeType分别为1、3、8,nodeName为标签的大写、'#TEXT'、'#COMMENT'
Node.firstChild
Node.lastChild
Node.nextSibling
Node.previousSibling //以上属性均为childNodes基础上的属性
Node.parentNode //有三种:element节点、document节点和documentfragment节点
document.parentNode //null
Node.removeChild(oldnode) //返回oldnode,依然在内存中,可以操作
Node.replaceChild(newnode, oldnode) //返回oldnode

document.getElementById('app').children //所有元素子节点

访问器属性:Object.defineProperty,顾名思义,就是为对象定义属性。 定义:直接在一个对象上定义一个新的属性,或者是修改(覆盖)已存在的属性。最终这个方法会返回该对象。
普通用法

1
2
3
4
5
6
7
Object.defineProperty(obj,key,{
value : val,
writable: false, //设置该属性是否可写
configuration: false//设置该属性是否可操作
enumerable:false //设置该属性是否可枚举
//默认均为true,可写可操作可枚举
})

关于属性的可枚举性,做一个扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObject = {name: 'xiaolin'} //设一个私有属性
Object.prototype.age= 22 //设一个继承属性

for (var key in myObject) {
alert(key);
//'name' 'age'
}

for(var key in myObject) {
if(myObject.hasOwnProperty(key)){
alert(key);// 'name'
}
}

Object.keys(myObject) // 'name'

对象的原生内置属性都是不可枚举的

  • for…in循环:只遍历对象自身的和继承的可枚举的属性
  • Object.keys( ):返回对象自身的所有可枚举的属性的键名
  • JSON.stringify( ):只串行化对象自身的可枚举的属性
  • Object.assign( ): 只合并自身的属性
  • Object.getOwnPropertyNames( ) 返回所有自身属性名(包括不可枚举的属性)
    可以用下面另两个方法来查看某个属性是否可以枚举:
1
2
Object.getOwnPropertyDescriptor(obj, key)  //返回obj对象key属性的描述对象
Object.propertyIsEnumerable(key) //判断是否可以枚举

特殊用法

1
2
3
4
5
6
7
8
9
10
11
Object.defineProperty(obj,key,{
get: function(){
//只要访问obj.key 就会触发set方法
},
set: function(newval){
//只要 obj.key = newval 就会触发set方法,newval作为参数参入
},
writable: true,
configuration: true
enumerable:true
})

极简双向绑定原理剖析
1、 实现Observer
将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter。实现一个消息订阅器,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法
2、 实现Compile
compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3、 实现Watcher
Watcher订阅者是Observer和Compile之间通信的桥梁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<!DOCTYPE html>
<head>
<title>Vue</title>
</head>
<style>
#app { text-align: center }
</style>
<body>
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">增加</button>
</form>
<h3 v-bind="number"></h3>
<form>
<input type="text" v-model="count">
<button type="button" v-click="decrement">减小</button>
</form>
<h3 v-bind="count"></h3>
</div>
</body>
<script>
window.onload = function() {
var app = new Vue({
el:'#app',
data: {
number: 0,
count: 0,
other: {
name: '123',
age: 22
}
},
methods: {
increment: function() {
this.number ++
},
decrement: function() {
this.count --
}
}
})
}
function Vue(options) {
this._init(options)
}
Vue.prototype._init = function (options) {
this.$options = options
this.$el = document.querySelector(options.el)
this.$data = options.data
this.$methods = options.methods
this._binding = {}
//这个对象装着将data数据通过_observe函数递归打平为一层后
//所有属性名及其对应的订阅者数组
this._observe(this.$data)
//劫持所有data里面的属性,使之属性值都有set和get方法,
//并在set之后通知其所有订阅者触发更新函数
this._complie(this.$el)
//递归编译DOM,同时遇到绑定了指令的属性,就在_binding对应属性里添加订阅者
}
Vue.prototype._observe = function (obj) {
var _this = this
Object.keys(obj).forEach(function (key) {

if (obj.hasOwnProperty(key)) {
_this._binding[key] = {
_directives: [] //给每个属性值都设置一个订阅者列表
}
var value = obj[key]
if (typeof value === 'object') {
_this._observe(value) //若为对象则递归遍历
}
var binding = _this._binding[key]
Object.defineProperty(_this.$data, key, {
enumerable: true, //设为可枚举
configurable: true, //设为可操作
get: function () {
return value
},
set: function (newVal) {
// console.log(`${key}更新${newVal}`)
if (value !== newVal) {
value = newVal
binding._directives.forEach(function (item) {
item.update()
//属性值更新后,遍历订阅者触发其更新函数,实现Model-->View
})
}
}
})
}
})
}

Vue.prototype._complie = function (element) {
var _this = this
var nodes = element.children
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]

if (node.children.length) {
this._complie(node)
//若遇到元素节点,则进行递归编译
}

if (node.hasAttribute('v-click')) {
node.onclick = (function () {
var attrVal = nodes[i].getAttribute('v-click')
//这里获取v-click绑定的函数名
return _this.$methods[attrVal].bind(_this.$data)
//让函数执行this指向data,这样就达到了函数中this指向组件的效果
})()
}

if (node.hasAttribute('v-model')) {//绑定了v-model的属性为双向绑定
node.addEventListener('input', (function(key) {
var attrVal = node.getAttribute('v-model')
//获取v-model绑定的那个属性,监听input事件,并添加订阅者
_this._binding[attrVal]._directives.push(new Watcher(
node,
_this,
attrVal,
'value'
))

return function() {
_this.$data[attrVal] = nodes[key].value
}
})(i))
}

if (node.hasAttribute('v-bind')) {
var attrVal = node.getAttribute('v-bind')
//这里不同的指令对应着不同的订阅内容
_this._binding[attrVal]._directives.push(new Watcher(
node,
_this,
attrVal,
'innerHTML'
))
}
}
}
function Watcher( el, vm, exp, attr) {
this.el = el //指令对应的DOM元素
this.vm = vm //指令所属Vue实例
this.exp = exp //指令对应的值,本例如"number"
this.attr = attr //绑定的属性值,本例为"innerHTML"
this.update()
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.exp]
//更新函数,把model层数据赋给视图层
}
</script>

上述例子只是对双向绑定原理的基本解释,远远不是真正的实现方法,比如数据不是打平为一层,模板的编译也不是遍历节点来编译。template会被编译成AST语法树,AST会经过generate得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点

  • parse 过程,将 template 利用正则转化成 AST 抽象语法树。
  • optimize 过程,标记静态节点,后 diff 过程跳过静态节点,提升性能。
  • generate 过程,生成 render 字符串

Vue SSR基本原理

主要通过vue-server-renderer将Vue组件输出成HTML,过程:

  • 客户端 entry-client 主要作用挂载到 DOM 上,服务端 entry-server 除了创建和返回实例,还进行路由匹配与数据预获取
  • webpack打包客户端为client-bundle,打包服务端为server-bundle
  • 服务器接收请求,根据 url 来加载相应组件,然后生成 html 发送给客户端
  • 客户端激活, Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM,为确保混合成功,客户端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到 stroe 里,这样,在客户端挂载到 DOM 之前,可以直接从 store 里取数据。首屏的动态数据通过 window.INITIAL_STATE 发送到客户端