前端技术原理

reactNative 原理

说明:没有Andorid,ios,c++底层分析功力无法完全通透

React Native桥接器初探link link

  • React Native 渲染

    在 React 框架中,JSX 源码通过 React 框架最终渲染到了浏览器的真实 DOM 中,而在 React Native 框架中,JSX 源码通过 React Native 框架编译后,通过对应平台的 Bridge 实现了与原生框架的通信。如果我们在程序中调用了 React Native 提供的 API,那么 React Native 框架就通过 Bridge 调用原生框架中的方法。

JavaScriptCore是一个C++ vm。使用Apple提供的JavaScriptCore框架,你可以在Objective-C或者基于C的程序中执行Javascript代码,也可以向JavaScript环境中插入一些自定义的对象。JavaScriptCore从iOS 7.0之后可以直接使用。

因为 React Native 的底层为 React 框架,所以如果是 UI 层的变更,那么就映射为虚拟 DOM 后进行 diff 算法,diff 算法计算出变动后的 JSON 映射文件,最终由 Native 层将此 JSON 文件映射渲染到原生 App 的页面元素上,最终实现了在项目中只需要控制 state 以及 props 的变更来引起 iOS 与 Android 平台的 UI 变更。

编写的 React Native代码最终会打包生成一个 main.bundle.js 文件供 App 加载,此文件可以在 App 设备本地,也可以存放于服务器上供 App 下载更新,热更新就会涉及到 main.bundle.js 位置的设置问题。

  • 优点:

1.复用了 React 的思想,有利于前端开发者涉足移动端
2.能够利用 JavaScript 动态更新的特性,快速迭代
3.相比于原生平台,开发速度更快,相比于 Hybrid 框架,性能更好

  • 缺点
  1. 开发者依然要为 iOS 和 Android 两个平台提供两套不同的代。有组件是区分平台的,即使是共用组件,也会有平台独享的函数。
  2. 不能做到完全屏蔽 iOS 端或 Android端,前端开发者必须对原生平台有所了解。
  3. 由于 Objective-C 与 JavaScript 之间的切换存在固定的时间开销,所以性能必定不及原生。(比如目前的官方版本无法做到 UItableview(ListView) 的视图重用,因为滑动过程中,视图重用需要在异步线程中执行,速度太慢。这也就导致随着 Cell 数量的增加,占用的内存也线性增加。)
  • React Native 交互原理总结

Objective-C 有 JavaScript Core 框架用来执行 JavaScript 代码。
JavaScript 通过配置表生成类,方法,参数三个元素,放入消息队列,Objective-C获取之后,
就可以唯一确定要调用的是哪个Objective-C函数,然后调用

babel 原理,抽象语法树如何产生?

  1. 将整个代码字符串分割成 语法单元 数组, 最小语法单元包含。
  • 空白:JS中连续的空格、换行、缩进等
  • 注释:行注释或块注释
  • 字符串
  • 数字 16、10、8进制以及科学表达法等数字表达语法
  • 标识符,没有被引号括起来的的连续字符,包含字母,_,数字,常量 true,false,if , return, function
  • 运算符 +、-、*、/、>、<
  • 括号 ( ), 大扩招,冒号,分好,点,等等

拆分过程就是 一个字符一个字符地遍历,然后分情况讨论,整个实现方法就是顺序遍历和大量的条件判断。

1
2
3
4
5
6
if (1 > 0) {
alert("if \"1 > 0\"");
}

'if' ' ' '(' '1' ' ' '>' ' ' ')' ' ' '{'
'\n ' 'alert' '(' '"if \"1 > 0\""' ')' ';' '\n' '}'

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
function tokenizeCode(code){
const tokens = [];
for(let i=0;i<code.length;i++){
//从0开始,一个字符一个字符读取
let currentChar = code.charAt(i);
if(currentChar === ';'){
tokens.push({
type:'sep',
value:';'
})
continue;
}
if(currentChar === '('|| currentChar===')'){
tokens.push({
type:'parens',
value:currentChar
})
continue;
}
if(currentChar ==='}' || currentChar==='{'){
tokens.push({
type:'brace',
value:currentChar
});
continue;
}
if(currentChar ==='>' || currentChar==='<'){
tokens.push({
type:'operator',
value:currentChar
});
continue;
}
if(currentChar === '"' || currentChar ==='\''){
const token = {
type:'string',
value:currentChar
};
tokens.push(token);
const closer = currentChar;
let escaped = false;
//嵌套循环遍历,寻找字符串结尾
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
token.value += currentChar;
if(escaped) {
escaped = false;
}else if( currentChar === '\\'){
escaped = true;
}else if( currentChar === closer) {
break;
}
}
continue;
}
if(/[0-9]/.test(currentChar)){
const token = {
type:'number',
value:currentChar
};
token.push(token);
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
if(/[0-9\.]/.test(currentChar)){
token.value += currentChar;
}else{
i--;
break;
}
}
continue;
}
if(/[a-zA-Z\$\_]/.test(currentChar)){
const token = {
type:'identifer',
value:currentChar,
};
tokens.push(token)
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
if(/[a-zA-Z0-9\$\_]/.test(currentChar)){
token.value +=currentChar;
}else{
i--;
break;
}
}
continue;
}
if(/\s/.test(currentChar)){
const token = {
type:'whitespace',
value:currentChar,
};
token.push(token);
for(i++;i<code.length;i++){
currentChar = code.charAt(i);
if(/\s]/.test(currentChar)){
token.value += currentChar;
}else{
i--;
break;
}
}
continue;
}
throw new Error('Unexpected '+currentChar);
}
return tokens;
}
  1. 语义分析:在分析结果的基础上分析 语法单元之间的关系

在编程语言的解析中有两个很相似但是又有区别的重要概念:

  • 表达式:每个表达式都会产生一个值,它可以放在任何需要一个值的地方
1
2
3
var a = (5 + 6) / 2; //表达式:(5 + 6) / 2
var b = (function(){ return 25;})(); //表达式: (function(){ return 25;})()
foo(a*b); //表达式:a*b
  • 语句: 是由“;(分号)”分隔的句子或命令。如果在表达式后面加上一个“;”分隔符,这就被称为“表达式语句”。它表明“只有表达式,而没有其他语法元素的语句”。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var num = 9; //声明、赋值语句
    vloop: //标签语句
    { //其实这里大括号可以不需要的,在这里我只想向大家展示一下这种代码块结构而已
    for(var i=1; i<10; i++) { //循环语句
    if(i==num){ //条件语句
    break vloop;
    }else{
    num = num - 1;
    }
    }
    }
    console.log(num); //表达式语句,输出:5
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/*
* 如何语法分析
*
* stash()函数暂存当前的i,nextToken()读取一下个符号 token[i]
*
* 1, 判断if语句:
* 如果curToken.type为标识符,为if, nextToken()紧接着读取下一个token,
* 如果token类型不是空格,或者不是 '(', 抛出错误
* 后续statement.test = nextExpression() 的一个表达式是if的判断条件,递归找出if内的表达式
* nextToken()
* 如果后续token类型不是空格或者不是')',抛出错误
* statement.consequent = nextStatement() (无限递归)找出下一个语句是if成立时执行的语句
*
* 如果下一个符号是 else 就说明还存在 if 不成立时的逻辑
* if (curToken === 'identifier' && curToken.value === 'else') {
statement.alternative = nextStatement();
} else {
statement.alternative = null;
}
commit(); 上一个暂存点销毁,不再需要了。
return statement


2 判断如果是curToken.type === 'brace' && curToken.value ==="{"
* 认为是 { block代码块
* statement = { type:'BlockStatement',body:[],}
* statsh(); nextToken();
* if (curToken.type === 'brace' && curToken.value === '}') { commit(); //表示代码块结束}
* rewind(); //回到原来位置
statement.body.push(nextStatement()); //递归,寻找block代码块中的语句or表达式
* */

function parse (tokens) {
let i = -1;
let curToken;


function nextStatement(){
stash();
nextToken();

if(curToken.type === 'identifier' && curToken.value ==='if'){
//解析 if 语句
const statement = {
type:'IfStatement',
}

nextToken();
if (curToken.type !== 'parens' || curToken.value !== '(') {
throw new Error('Expected ( after if');
}

// 后续的一个表达式是 if 的判断条件
statement.test = nextExpression();
// 判断条件之后必须是 )
nextToken();
if(curToken.type!=='parens' || curToken.value!==')'){
throw new Error('Expected ) after if test expression');
}
statement.consequent = nextStatement();
if(curToken === 'identifier' && curToken.value ==='else'){
statement.alternative = nextStatement();
}else{
statement.alternative = null;
}
commit();
return statement;
}
if(curToken.type ==='brace' && curToken.value ==='{'){
// 以 { 开头表示是个代码块,我们暂不考虑JSON语法的存在
const statement = {
type: 'BlockStatement',
body: [],
};
while (i<tokens.length){
// 检查下一个符号是不是 }
stash();
nextToken();
if(curToken.type==='brace'&&curToken.value ==="}"){
commit();
break;
}
rewind();
statement.body.push(nextStatement());
}
commit();
return statement;
}

rewind();
const statement = {
type:'ExpressionStatement',
expression:nextExpression(),
};
if(statement.expression){
nextToken();
if (curToken.type !== 'EOF' && curToken.type !== 'sep') {
throw new Error('Missing ; at end of expression');
}
return statement;
}
}


// 读取下一个表达式
function nextExpression () {
nextToken();
if (curToken.type === 'identifier') {
const identifier = {
type: 'Identifier',
name: curToken.value,
};
stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === '(') {
// 如果一个标识符后面紧跟着 ( ,说明是个函数调用表达式
const expr = {
type: 'CallExpression',
caller: identifier,
arguments: [],
};
stash();
nextToken();
if (curToken.type === 'parens' && curToken.value === ')') {
// 如果下一个符合直接就是 ) ,说明没有参数
commit();
} else {
// 读取函数调用参数
rewind();
while (i < tokens.length) {
// 将下一个表达式加到arguments当中
expr.arguments.push(nextExpression());
nextToken();
// 遇到 ) 结束
if (curToken.type === 'parens' && curToken.value === ')') {
break;
}
// 参数间必须以 , 相间隔
if (curToken.type !== 'comma' && curToken.value !== ',') {
throw new Error('Expected , between arguments');
}
}
}
commit();
return expr;
}
rewind();
return identifier;
}


if (curToken.type === 'number' || curToken.type === 'string') {
// 数字或字符串,说明此处是个常量表达式
const literal = {
type: 'Literal',
value: eval(curToken.value),
};
// 但如果下一个符号是运算符,那么这就是个双元运算表达式
// 此处暂不考虑多个运算衔接,或者有变量存在
stash();
nextToken();
if (curToken.type === 'operator') {
commit();
return {
type: 'BinaryExpression',
left: literal,
right: nextExpression(),
};
}
rewind();
return literal;
}

if (curToken.type !== 'EOF') {
throw new Error('Unexpected token ' + curToken.value);
}
}

function nextToken(){
do{
i++;
curToken = tokens[i] || { type:'EOF' }
}while (curToken.type==='whitespace');
}

const stashStack = [];

function stash(cb){
stashStack.push(i);
}

function rewind(){
i = stashStack.pop();
curToken = tokens[i];
}

function commit(){
stashStack.pop()
}
const ast = {
type:'Program',
body:[]
}
while (i<tokens.length){
const statement = nextStatement();
if(!statement){ break; }
ast.body.push(statement);
}
return ast;
}

webpack 原理

实现一个简单的webpack link

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
156
157
158
159
160
161
162
let mainFile = parse(config.entry, true) //babylon.parse 返回入口js文件的AST抽象语法树,依赖
return {
fileName,
dependence: getDependence(ast),
code: getTranslateCode(ast),
};

let queue = getQueue(mainFile) //分析递归主文件依赖,返回文件依赖的字面量树。

console.log('queue',queue);
return bundle(queue) ;//打包所有js到一个文件。

=====

const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const {transformFromAst} = require('babel-core’);
const loaderUtils = require('loader-utils')

let config = {}


//fs.readFileSync获取源代码,在转换成ast之前执行loader
function getSource(modulePath){
try{
let rules = this.config.module.rules
const content = fs.readFileSync(modulePath, 'utf-8') //读取源文件
for(let i = 0;i<rules.length;i++){
let {test, use } = rules[i]
let len = use.length -1
if(test.test(modulePath)){
//递归处理所有loader
function loopLoader() {
let loader = require(use[len--])
let content = loader(content);
if(len>=0){
loopLoader()
}
}
loopLoader()
}
}
return content
}catch (e) {
throw new Error(`获取数据错误 : ${modulePath}`)
}
}

/**
* 获取文件,解析成ast语法
* @param filename
* @returns {*}
*/
function getAst (source) {
const content = source
return babylon.parse(content, {
sourceType: 'module',
});
}


function getDependence (ast) {
let dependencies = []
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
},
})
console.log('dependencies',dependencies);
return dependencies
}

/**
* 编译
* @param ast
* @returns {*}
*/
function getTranslateCode(ast) {
const {code} = transformFromAst(ast, null, {
presets: ['env']
});
console.log('code',code);
return code
}


/**
* 生成完整的文件依赖关系映射
* @param fileName
* @param entry
* @returns {{fileName: *, dependence, code: *}}
*/
function parse(fileName, entry) {
let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileName
let dirName = entry ? '' : path.dirname(config.entry)
let absolutePath = path.join(dirName, filePath)
let source =this.getSource(absolutePath)
const ast = getAst(source)

return {
fileName,
dependence: getDependence(ast),
code: getTranslateCode(ast),
};
}


/**
* 获取深度队列依赖关系
* @param main
* @returns {*[]}
*/
function getQueue(main) {
let queue = [main]
for (let asset of queue) {
asset.dependence.forEach(function (dep) {
let child = parse(dep)
queue.push(child)
})
}
return queue
}


function bundle(queue) {
let modules = ''
queue.forEach(function (mod) {
modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`
})

const result = `
(function(modules) {
function require(fileName) {
const fn = modules[fileName];
const module = { exports : {} };
fn(require, module, module.exports);
return module.exports;
}
require('${config.entry}');
})({${modules}})
`;
// We simply return the result, hurray! :)
return result;
}


//option tinypack.config.js
function bundleFile(option) {
console.log(option)
config = option
let mainFile = parse(config.entry, true) //返回入口js文件的AST抽象语法树,依赖
let queue = getQueue(mainFile) //递归主文件依赖,返回文件依赖的字面量树。
console.log('queue',queue);
return bundle(queue) ;//打包所有js到一个文件。
}

module.exports = bundleFile


`

DIFF 算法实现 原理

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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import {StateEnums,isString, move } from ‘./util’
import Element from ‘./element’

export default function diff(oldDomTree, newDomTree){
let patches = { } //记录差异
dfs(oldDomTree, newDomTree, 0, patches)
return patches
}
//树递归
//①,比较新旧节点,
function dfs(oldNode, newNode, index, patches){
let curPatches = [ ] //用于保存子树更改
// 需要判断三种情况
/*1.没有新的节点,什么都不做
2.新的节点 tagName和 key(可能没有) 和旧的相同,开始比较属性是否相同,如果不同则入栈属性改变状态,
(diffProps 比较属性。
* 遍历旧节点的属性key,如果旧节点属性key 新节点没有这个key,入栈属prop key (说明删除了)
* 遍历新节点尚属性key,若旧节点存在key,比较新旧vlue值,oldprops[key] !== 新节点props[key], 入栈 新节点key,新节点 value(说明添加了key)
若不存在oldProps[key], 入栈新 {prop:key,value:newProps}
)
开始遍历子树, diffChildren(oldNode.children, newNode.children, index, patches){
let { changes, list } = listDiff ( oldChild, newChild, index, patches )
//判断列表差异算法实现 Vitual Dom核心算法
/*
* 迭代oldList,找到oldlist的节点key,声明一个临时栈oldlistTemp,声明change栈
如果oldList节点存在的key newList不存在,说明oldList此节点key被删除,入栈null到oldKeyTemp里
如果都存在key相当说明 子树上存在相同节点,只用对比两个节点是否需要运动, 入栈key到oldKeyTemp里
* 迭代oldKeyTemp, 发现有null,入栈change.push({type:StateEnums.Remove, index:i})
* 迭代newlist,找到节点key,
如果存在oldKeyTemp[key],说明这个节点从旧到新没有发生操作,
但需要判断节点树层级,如果index!=i 入栈 changes.push({ type:StateEnums.Move, from:index,to:i }),移动节点
如果不存在oldkeyTemp.indexOf(key) === -1, 说明这个节点是个新节点,
入栈 changes.push({ type:StateEnums.Insert, node:item, index:i}) oldKeyTemp添加此key
listDiff方法返回 { changes, oldListTemp }

diffChildren中拿到change,list, 合并更改到patches里。 再次比较新旧节点,如果新节点存在字子节点,递归dfs重复上面过程
}
dfs返回所有节点的替换,删除,移动信息。


3.新的节点的tagName和 ‘key’ 和旧的不同,就替换所有树,不需遍历旧节点子元素,因为整个节点都被替换掉了,入栈节点替换状态

最后调用patch(root, diffs), 应用更新,由于更新用的都是无关dom操作的api,完成替换。
StateEnums.ChangeProps/remove/move/insert


*/
if(!newNode){ }
else if(newNode.tag===oldNode.tag && newNode.key === oldNode.key){
//判断属性是否变更
let props = diffProps(oldNode.props, newNode.props)
if(props.length) curPatches.push({ type:StateEnums.ChangeProps, props })
//遍历子树
diffChildren(oldNode.children, newNode.children, index, patches )
}else{
//节点不同,需要替换
curPatches.push({ type:StateEnums.Replace, node:newNode })
}
if(curPatches.length){
if(patches[index]){
patches[index] = patches[index].concat[curPatches]
}else{
patches[index] = curPatches
}
}
}

//判断属性的更改
/*
1 遍历旧节点的属性key,如果旧节点属性key 新节点没有这个key,入栈属prop key (说明删除了)
2 遍历新节点尚属性key,若旧节点存在key,比较新旧vlue值,oldprops[key] !== 新节点props[key], 入栈 新节点key,新节点 value(说明添加了key)
若不存在oldProps[key], 入栈新 {prop:key,value:newProps}
*/
function diffProps (oldProps, newProps){
let change = []
for(const key in oldProps) {
if(oldProps.hasOwnProperty(key) &&!newProps[key]){
change.push({ prop:key })
}
}
for(const key in newProps){
if(newProps.hasOwnProperty(key)){
const prop = newProps[key]
if(oldProps[key] && oldProps[key]!==newProps[key]){
change.push({ prop:key, value:newProps[key] })
}
}else if(!oldProps[key]){
change.push({ prop:key, value:newProps })
}
}
return change
}

//遍历子元素打标识,判断两个列表差异
/*
listDiff 拿到
*/

function diffChildren (oldChild,newChild,index,patches){
let { changes, list } = listDiff(oldChild, newChild, index, patches)
if(changes.length){
if(patches[index]){ //合并更改
patches[index] = patches[index].concat(changes)
}else{
patches[index] = changes
}
}

let last = null
oldChild &&
oldChild.forEach((item,i)=>{ //递归看是否仍然有子节点,然后进行dfs操作。
let child = item && item.children
if(child){
index = last && last.children? index+last.children.length+1:index+1
let keyIndex = list.indexOf(item.key)
let node = newChild[keyIndex]
// 只遍历新旧中都存在的节点,其他新增或者删除的没必要遍历
if(node){
dfs(item, node, index, patches)
}
}
else
index+=1

last = item
})
}

//判断列表差异算法实现 Vitual Dom核心算法
/* 1 迭代oldList,找到oldlist的节点key,声明一个临时栈oldlistTemp,声明change栈
如果oldList节点存在的key newList不存在,说明oldList此节点key被删除,入栈null到oldKeyTemp里
如果都存在key相当说明 子树上存在相同节点,只用对比两个节点是否需要运动, 入栈key到oldKeyTemp里

2 迭代oldKeyTemp, 发现有null,入栈change.push({type:StateEnums.Remove, index:i})

3 迭代newlist,找到节点key,
如果存在oldKeyTemp[key],说明这个节点从旧到新没有发生操作,
但需要判断节点树层级,如果index!=i 入栈 changes.push({ type:StateEnums.Move, from:index,to:i }),移动节点
如果不存在oldkeyTemp.indexOf(key) === -1, 说明这个节点是个新节点,
入栈 changes.push({ type:StateEnums.Insert, node:item, index:i}) oldKeyTemp添加此key

listDiff方法返回 { changes, oldListTemp }

*/
function listDiff (oldList, newList, index, patches){
let oldKeys = getKeys(oldList)
let newKeys = getKeys(newList)
let change = [ ]

let oldkeyTemp = [ ]
oldList &&
oldList.forEach(item=>{
let key = item.key
if(isString(item)) { key = item }
let index = newKeys.indexOf(key)
if( index === -1){
oldkeyTemp.push(null) //newList不存在key, 说明oldList此节点k被删除,入栈null
}
else oldkeyTemp.push(key) //把newlist, oldlist 都存在的key保留在数组里。
})
//遍历变更后的数组,因为删除数组元素是会更改索引的,所有从后往前删可以保证索引不变,
//找到null节点,入栈删除操作
let length = oldkeyTemp.length
for(let i = length-1; i>0;i--) {
if(!oldkeyTemp[i]){
oldkeyTemp.splice(i,1); //把空节点删掉。
changes.push({ type:StateEnums.Remove, index:i })
}
}
//遍历新的list, 判断是否有节点新增或移动, 同时也对 list 做节点新增和移动节点操作
newList &&
newList.forEach((item,i)=>{
let key = item.key
if(isString(item)) { key = item }
//寻找旧的children中是否含有当前节点
let index = oldkeyTemp.indexOf(key)
//如果没有找到新节点,说明此key是一个新的节点,需要插入
if(index === -1 || key== null){
changes.push({
type:StateEnums.Insert,
node:item,
index:i
})
oldListTemp.splice(i,0,key)
} else {
//找到了,需要判断是否需要移动
if(index!==i){
changes.push({
type:StateEnums.Move,
from:index,
to:i
})
move(list, index, i)
}
}
})
return { changes, oldListTemp }
}

function getKeys (list){
let keys = [ ]
let text
list && list.forEach(item=>{
let key
if(isString(item)) { key = [item] } //如果item本身为文本,将文本作为单数组 key
else if (item instanceof Element) { key = item.key }
keys.push(key)
})
return keys
}


//渲染差异 深度遍历树,将需要的变更操作取出来。 局部更新DOM
let index = 0

export default function patch(node,patchs){
let changes = patchs[index]
let childNodes = node && node.childNodes

if(!childNodes) index += 1
if(changes && changes.length && patchs[index]) { changeDom (node, changes) }
let last = null
if(childNodes && childNode.length){
childNodes.forEach((item,i) => {
index = last && last.children ? index +last.children.length+1:index+1
patch(item, patchs);
last = item
})
}
}

function changeDom ( node, changes, noChild) {
changes &&
changes.forEach(change =>{
let {type} = change
swtich(type) {
case StateEnums.ChangeProps:
let { props } = change
props.forEach( item => {
if(item.value) { node.setAttribute(item.prop, item.value) }
else { node.removeAttribute(item.prop) }
})
break;
case StateEnums.Remove:
node.childNodes[change.index].remove();
break;
case StateEnums.Insert:
let dom
if(isString(change.node)){
dom = document.createTextNode(change.node)
}else if( change.node instanceof Element){
dom = change.node.create()
}
node.insertBefore( dom, nodeChildNodes[change.index])
break;
case StateEnums.Move:
let fromNode = node.childNodes[change.from]
let toNode = node.childNodes[change.to]
let cloneFromNode = fromNode.cloneNode(true)
let cloneToNode = toNode.cloneNode(true)
node.replaceChild(cloneFromNode,toNode)
node.replaceChild(cloneToNode,fromNode)
break;
default:
break;
}

})

}


let test4 = new Element('div', { class: 'my-div' }, ['test4’],’key4')
let test5 = new Element('ul', { class: 'my-div' }, ['test5’],’key5')

let test1 = new Element('div', { class: 'my-div' }, [test4], ‘key1')
let test2 = new Element('div', { id: '11' }, [test5, test4], ‘key2')

let root = test1.render()

let patchs = diff(test1, test2)
console.log(patchs)

setTimeout(() => {
console.log('开始更新')
patch(root, patchs)
console.log('结束更新')
}, 1000)

diff的不足与待优化的地方


看图的 D,此时D不移动,但它的index是最大的,导致更新lastIndex=3,从而使得其他元素A,B,C的index<lastIndex,导致A,B,C都要去移动。
理想情况是只移动D,不移动A,B,C。因此,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响React的渲染性能。

viturl Dom实现

Virtual Dom实现

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
//创建节点  
//实现逻辑:
1 创建一个el空节点,遍历props,添加为el属性,如果存在key属性,添加key属性到le上
2 判断child参数是否存在,迭代child,判断child的每项是虚拟El节点则递归创建节点方法,不是El节点肯定为Text节点,
创建Text文本节点。 最后统一插入到el空节点下, 递归完成返回el节点

<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>
var ul = el('ul', {id: ‘list’,class:’list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
],'key')

export default class Element {
/**
* @param {String} tag 'div'
* @param {Object} props { class: 'item' }
* @param {Array} children [ Element1, 'text']
* @param {String} key option
*/
constructor(tag, props, children, key){
this.tag = tag
this.props = props
if(Array.isArray(children)){
this.children = children
}else if(isString(children)){
this.key = children
this.children = null
}
if(key) this.key = key
}
render(){
let root = this._createElement(
this.tag,
this.props,
this.children,
this.key
)
document.body.appendChild(root)
return root
}
create(){
return this._createElement(this.tag,this.props,this.children,this.key)
}
//创建节点
//实现逻辑:
1 创建一个el空节点,遍历props,添加为el属性,如果存在key属性,添加key属性到le上
2 判断child参数是否存在,迭代child,判断child的每项是虚拟El节点则递归创建节点方法,不是El节点肯定为Text节点,
创建Text文本节点。 最后统一插入到el空节点下, 递归完成返回el节点
_createElement(tag,props,child,key){
let el = document.createElement(el);
for(let key in props){
value = props[key];
el.setAttribute(key,value)
}
if(key){
el.setAttribute(‘key’,key)
}
if(child){
child.foreach(elem=>{
if(elem instanceOf Element){
let els = _createElement(elem.tag,elem.props,elem.child,elem.key)
}else{
let els = document.createTextNode(textNode)
}
el.appendChild(els)
})
}
return el;
}
}

Vue-router路由原理

目前在浏览器环境中这一功能的实现主要有两种方式:

利用URL中的hash(“#”) HashHistory

特点:

  • hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面
  • 可以为hash的改变添加监听事件: window.addEventListener(“hashchange”, funcRef, false)
  • 每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录

HashHistory中的push()方法:

1
2
3
4
5
6
7
8
9
10
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}

function pushHash (path) {
window.location.hash = path
}

transitionTo()方法是父类中定义的是用来处理路由变化中的基础逻辑的,push()方法最主要的是对window的hash进行了直接赋值HashHistory.replace()

* replace()方法与push()方法不同之处在于,它并不是将新路由添加到浏览器访问历史的栈顶,而是替换掉当前的路由:
1
2
3
4
5
6
7
8
9
10
11
12
13
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}

function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}

利用History interface在 HTML5中新增的方法 HTML5History

pushState(), replaceState()使得我们可以对浏览器历史记录栈进行修改:

两者优劣

调用history.pushState()相比于直接修改hash主要有以下优势:

  • pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL
  • pushState设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发记录添加到栈中
  • pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串
  • pushState可额外设置title属性供后续使用

history模式的一个问题

比如用户直接在地址栏中输入并回车,浏览器重启重新加载应用等。
hash模式仅改变hash部分的内容,而hash部分是不会包含在HTTP请求中的:

http://oursite.com/#/user/id // 如重新请求只会发送http://oursite.com/

故在hash模式下遇到根据URL请求页面的情况不会有问题。而history模式则会将URL修改得就和正常请求后端的URL一样

http://oursite.com/user/id

解决:
在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果是用 Node.js 作后台,可以使用服务端的路由来匹配 URL,当没有匹配到路由的时候返回 404,从而实现 fallback。
react使用一个叫做connect-history-api-fallback 中间件解决

小程序的实现原理是什么?

小程序架构 http://www.bubuko.com/infodetail-1906089.html

generator 原理

Generator 是 ES6中新增的语法,和 Promise 一样,都可以用来异步编程

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
// 使用 * 表示这是一个 Generator 函数
// 内部可以通过 yield 暂停代码
// 通过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
从以上代码可以发现,加上 *的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数可以继续执行被暂停的代码。以下是 Generator 函数的简单实现
// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};

return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 可以发现通过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 并且表明下次需要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}