注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Object.defineProperty也能监听数组变化?

首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。 在 Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.define...
继续阅读 »


首先,解答一下标题:Object.defineProperty 不能监听原生数组的变化。如需监听数组,要将数组转成对象。




Vue2 时是使用了 Object.defineProperty 监听数据变化,但我查了下 文档,发现 Object.defineProperty 是用来监听对象指定属性的变化。没有看到可以监听个数组变化的。


Vue2 有的确能监听到数组某些方法改变了数组的值。本文的目标就是解开这个结。






基础用法


Object.defineProperty() 文档


关于 Object.defineProperty() 的用法,可以看官方文档。


基础部分本文只做简单的讲解。




语法


Object.defineProperty(obj, prop, descriptor)

参数


  • obj 要定义属性的对象。
  • prop 要定义或修改的属性的名称或 Symbol
  • descriptor 要定义或修改的属性描述符。

const data = {}
let name = '雷猴'

Object.defineProperty(data, 'name', {
get() {
console.log('get')
return name
},
set(newVal) {
console.log('set')
name = newVal
}
})

console.log(data.name)
data.name = '鲨鱼辣椒'

console.log(data.name)
console.log(name)

上面的代码会输出


get
雷猴
set
鲨鱼辣椒
鲨鱼辣椒



上面的意思是,如果你需要访问 data.name ,那就返回 name 的值。


如果你想设置 data.name ,那就会将你传进来的值放到变量 name 里。


此时再访问 data.name 或者 name ,都会返回新赋予的值。




还有另一个基础用法:“冻结”指定属性


const data = {}

Object.defineProperty(data, 'name', {
value: '雷猴',
writable: false
})

data.name = '鲨鱼辣椒'
delete data.name
console.log(data.name)

这个例子,把 data.name 冻结住了,不管你要修改还是要删除都不生效了,一旦访问 data.name 都一律返回 雷猴


以上就是 Object.defineProperty 的基础用法。






深度监听


上面的例子是监听基础的对象。但如果对象里还包含对象,这种情况就可以使用递归的方式。


递归需要创建一个方法,然后判断是否需要重复调用自身。


// 触发更新视图
function updateView() {
console.log('视图更新')
}

// 重新定义属性,监听起来(核心)
function defineReactive(target, key, value) {

// 深度监听
observer(value)

// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue != value) {
// 深度监听
observer(newValue)

// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue

// 触发视图更新
updateView()
}
}
})
}

// 深度监听
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}

// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}

// 准备数据
const data = {
name: '雷猴'
}

// 开始监听
observer(data)

// 测试1
data.name = {
lastName: '鲨鱼辣椒'
}

// 测试2
data.name.lastName = '蟑螂恶霸'

上面这个例子会输出2次“视图更新”。




我创建了一个 updateView 方法,该方法模拟更新 DOM (类似 Vue的操作),但我这里简化成只是输出 “视图更新” 。因为这不是本文的重点。




测试1 会触发一次 “视图更新” ;测试2 也会触发一次。


因为在 Object.definePropertyset 里面我有调用了一次 observer(newValue)observer 会判断传入的值是不是对象,如果是对象就再次调用 defineReactive 方法。


这样可以模拟一个递归的状态。




以上就是 深度监听 的原理,其实就是递归。


但递归有个不好的地方,就是如果对象层次很深,需要计算的量就很大,因为需要一次计算到底。






监听数组


数组没有 key ,只有 下标。所以如果需要监听数组的内容变化,就需要将数组转换成对象,并且还要模拟数组的方法。


大概的思路和编码流程顺序如下:


  1. 判断要监听的数据是否为数组
  2. 是数组的情况,就将数组模拟成一个对象
  3. 将数组的方法名绑定到新创建的对象中
  4. 将对应数组原型的方法赋给自定义方法



代码如下所示


// 触发更新视图
function updateView() {
console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原形指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);

['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName = 收起阅读 »

JS 将伪数组转换成数组

在 JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。 本文将详细讲解 什么是伪数组,以及分别在 ES5 和 ES6 中将伪数组转换成真正的数组 。 什么是伪数组? 伪数组的主要特征:它是一个对象,并且该对象有 le...
继续阅读 »




JS 中,伪数组 是非常常见的,它也叫 类数组。伪数组可能会给 JS 初学者带来一点困扰。


本文将详细讲解 什么是伪数组,以及分别在 ES5ES6 中将伪数组转换成真正的数组






什么是伪数组?


伪数组的主要特征:它是一个对象,并且该对象有 length 属性


比如


let arrayLike = {
"0": "a",
"1": "b",
"2": "c",
"length": 3
}

像上面的 arrayLike 对象,有 length 属性,key 也是有序序列。可以遍历,也可以查询长度。但却不能调用数组的方法。比如 push、pop 等方法。


ES6 之前,还有一个常见的伪数组:arguments


arguments 看上去也很像一个数组,但它没有数组的方法。


比如 arguments.push(1) ,这样做一定会报错。




除了 arguments 之外,NodeList 对象表示节点的集合也是伪数组,比如通过 document.querySelectorAll 获取的节点集合等。






转换


将伪数组转换成真正的数组的方法不止一个,我们先从 ES5 讲起。




ES5 的做法


在 ES6 问世之前,开发者通常需要用以下的方法把伪数组转换成数组。




方法1


// 通过 makeArray 方法,把数组转成伪数组
function makeArray(arrayLike) {
let result = [];
for (let i = 0, len = arrayLike.length; i < len; i++) {
result.push(arrayLike[i]);
}
return result;
}

function doSomething () {
let args = makeArray(arguments);
console.log(args);
}

doSomething(1, 2, 3);

// 输出: [1, 2, 3]

这个方法虽然有效,但要多写很多代码。




方法2


function doSomething () {
let args = Array.prototype.slice.call(arguments);
console.log(args);
}
doSomething(1, 2, 3);

// 输出: [1, 2, 3]

这个方法的功能和 方法1 是一样的,虽然代码量减少了,但不能很直观的让其他开发者觉得这是在转换。




ES6的做法


直到 ES6 提供了 Array.from 方法完美解决以上问题。


function doSomething () {
let args = Array.from(arguments);
console.log(args);
}

doSomething('一', '二', '三');

// 输出: ['一', '二', '三']

Array.from 的主要作用就是把伪数组和可遍历对象转换成数组的。




说“主要作用”的原因是因为 Array.from 还提供了2个参数可传。这样可以延伸很多种小玩法。


Array.from 的第二个参数是一个函数,类似 map遍历 方法。用来遍历的。


Array.from 的第三个参数接受一个 this 对象,用来改变 this 指向。




第三个参数的用法(不常用)


let helper = {
diff: 1,
add (value) {
return value + this.diff; // 注意这里有个 this
}
};

function translate () {
return Array.from(arguments, helper.add, helper);
}

let numbers = translate(1, 2, 3);

console.log(numbers); // 2, 3, 4



Array.from 其他玩法


创建长度为5的数组,且初始化数组每个元素都是1


let array = Array.from({length: 5}, () => 1)
console.log(array)

// 输出: [1, 1, 1, 1, 1]

第二个参数的作用和 map遍历 差不多的,所以 map遍历 有什么玩法,这里也可以做相同的功能。就不多赘述了。




把字符串转换成数组


let msg = 'hello';
let msgArr = Array.from(msg);
console.log(msgArr);

// 输出: ["h", "e", "l", "l", "o"]

如果传一个真正的数组给 Array.from 会返回一个一模一样的数组。

 
收起阅读 »

我写了一个将 excel 文件转化成 本地json文件的插件

插件介绍 excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。 适用场景: 项目国际化,配置多语言 使用方法 1. 安装excel-2b-json npm install excel-2b-json 2. 引入使用...
继续阅读 »


插件介绍


excel-2b-json 插件用于将 google excel 文件转化成 本地json文件。


适用场景: 项目国际化,配置多语言


使用方法


1. 安装excel-2b-json


npm install excel-2b-json

2. 引入使用


const excelToJson = require('excel-2b-json');
// path 生成的json文件目录

excelToJson('https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0', path)


转化得到



下面是插件的实现



源码已放到github:github.com/Sunny-lucki…



一、涉及的算法


1. 26字母转换成数字,26进制,a为1,aa为27,ab为28


  function colToInt(col) {
const letters = ['', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
col = col.trim().split('')
let n = 0

for (let i = 0; i < col.length; i++) {
n *= 26
n += letters.indexOf(col[i])
}

return n
}

2. 生成几行几列的二维空数组


function getEmpty2DArr(rows, cols) {
let arrs = new Array(rows);
for (var i = 0; i < arrs.length; i++) {
arrs[i] = new Array(cols).fill(''); //每行有cols列
}
return arrs;
}


3. 清除二维数组中空的数组


[
[1,2,3],
['','',''],
[7,8,9]
]

转化为
[
[1,4,7],
[3,6,9]
]

  clearEmptyArrItem(matrix) {
return matrix.filter(function (val) {
return val.some(function (val1) {
return val1.replace(/\s/g, '') !== ''
})
})
}


4. 矩阵的翻转


[
[1,2,3],
[4,5,6],
[7,8,9]
]

转化为
[
[1,4,7],
[2,5,8],
[3,6,9]
]

算法实现


  /**
*
* @param {array*2} matrix 一个二维数组,返回旋转后的二维数组。
*/

rotateExcelDate(matrix) {
if (!matrix[0]) return []
var results = [],
result = [],
i,
j,
lens,
len
for (i = 0, lens = matrix[0].length; i < lens; i++) {
result = []
for (j = 0, len = matrix.length; j < len; j++) {
result[j] = matrix[j][i]
}
results.push(result)
}
return results
}

二、插件的实现


1. 下载google Excel文档到本地


我们先看看google Excel文档的url的组成


https://docs.google.com/spreadsheets/d/文档ID/edit#哈希值

例如下面这条,你可以尝试打开,下面这条链接是可以打开的。


https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0


下载google文档的步骤非常简单,只要获取原始的链接,然后拼接成下面的url,向这个Url发起请求,然后以流的方式写入生成文件就可以了。


https://docs.google.com/spreadsheets/d/ + "文档ID" + '/export?format=xlsx&id=' + id + '&' + hash

因此实现下载的方法非常简单,可以直接看代码


downLoadExcel.js



const fs = require('fs')
const request = require('superagent')
const rmobj = require('./remove')

/**
* 下载google excel 文档到本地
* @param {*} url // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/edit#gid=0
* @returns
*/

function downLoadExcel(url) {

// 记录当前下载文件的目录,方便删除
rmobj.push({
path: __dirname,
ext: 'xlsx'
})
return new Promise((resolve, reject) => {
var down1 = url.split('/')
var down2 = down1.pop() // edit#gid=0
var url2 = down1.join('/') // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
var id = down1.pop() // 12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM
var hash = down2.split('#').pop() // gid=0
var downurl = url2 + '/export?format=xlsx&id=' + id + '&' + hash // https://docs.google.com/spreadsheets/d/12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM/export?format=xlsx&id=12q3leiNxdmI_ZLWFj4LP_EA5PeJpLF18vViuyiSOuvM&gid=0
var loadedpath = __dirname + '/' + id + '.xlsx'
const stream = fs.createWriteStream(loadedpath)
const req = request.get(downurl)
req.pipe(stream).on('finish', function () {
resolve(loadedpath)
// 已经成功下载下来了,接下来将本地excel转化成json的工作就交给Excel对象来完成
})
})

}

module.exports = downLoadExcel

入口文件可以这样写


async function excelToJson(excelPathName, outputPath) {
if (Util.checkAddress(excelPathName) === 'google') {
// 1.判断是谷歌excel文档,需要交给Google对象去处理,主要是下载线上的,生成本地excel文件
const filePath = await downLoadExcel(excelPathName)

// 2.解析本地excel成二维数组
const data = await parseXlsx(filePath)

// 3.生成json文件
generateJsonFile(data, outputPath)
}

}
module.exports = excelToJson


之所以写if判断,是为了后面扩展,也许就不止是解析google文档了,或许也要解析腾讯等其他文档呢


第一步已经实现了,接下来就看第二步怎么实现


2. 解析本地excel成二维数组


解析本地excel文件,获取excel的sheet信息和strings信息


excel 文件其实本质上是多份xml文件的压缩文件。



xml是存储数据的,而html是显示数据的



而在这里我们只需要获取两份xml 文件,一份是strings,就是excel里的内容,一份是sheet,概括整个excel文件的信息。


async function parseXlsx(path) {

// 1. 解析本地excel文件,获取excel的sheet信息和content信息
const files = await extractFiles(path);

// 2. 根据strings和sheet解析成二维数组
const data = await extractData(files)

// 3. 处理二维数组的内容,
const fixData = handleData(data)
return fixData;
}

所以第一步我们看看怎么获取excel的sheet信息和strings信息


function extractFiles(path) {

// excel的本质是多份xml组成的压缩文件,这里我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml
const files = {
strings: {}, // strings内容
sheet: {},
'xl/sharedStrings.xml': 'strings',
'xl/worksheets/sheet1.xml': 'sheet'
}

const stream = path instanceof Stream ? path : fs.createReadStream(path)

return new Promise((resolve, reject) => {
const filePromises = [] // 由于一份excel文档,会被解析成好多分xml文档,但是我们只需要两份xml文档,分别是(xl/sharedStrings.xml和xl/worksheets/sheet1.xml),所以用数组接受

stream
.pipe(unzip.Parse())
.on('error', reject)
.on('close', () => {
Promise.all(filePromises).then(() => {
return resolve(files)
})
})
.on('entry', entry => {

// 每解析某个xml文件都会进来这里,但是我们只需要xl/sharedStrings.xml和xl/worksheets/sheet1.xml,并将内容保存在strings和sheet中
const file = files[entry.path]
if (file) {
let contents = ''
let chunks = []
let totalLength = 0
filePromises.push(
new Promise(resolve => {
entry
.on('data', chunk => {
chunks.push(chunk)
totalLength += chunk.length
})
.on('end', () => {
contents = Buffer.concat(chunks, totalLength).toString()
files[file].contents = contents
if (/�/g.test(contents)) {
throw TypeError('本次转化出现乱码�')
} else {
resolve()
}
})
})
)
} else {
entry.autodrain()
}
})
})
}

可以断点看看entry.path,你就会看到分别进来了好几次,然后我们会分别看到我们想要的那两个文件



两份xml文件解析之后就会到close方法里了,这时就可以看到strings和sheet都有内容了,而且内容都是xml



我们分别看看strings和sheet的内容


stream
.pipe(unzip.Parse())
.on('error', reject)
.on('close', () => {
Promise.all(filePromises).then(() => {
console.log(files.strings.contents);
console.log(files.sheet.contents);
return resolve(files)
})
})


格式化一下


strings



sheet


可以发现strings的内容非常简单,现在我们借助xmldom将内容解析为节点对象,然后用xpath插件来获取内容


xpath的用法:github.com/goto100/xpa…


  const XMLDOM = require('xmldom')
const xpath = require('xpath')
const ns = { a: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' }
const select = xpath.useNamespaces(ns)

const valuesDoc = new XMLDOM.DOMParser().parseFromString(
files.strings.contents
)

// 把所有每个格子的内容都放进了values数组里。
values = select('//a:si', valuesDoc).map(string =>

select('.//a:t', string)
.map(t => t.textContent)
.join('')
)


'//a:si' 是xpath语法,//表示选择当前节点下的所有子孙节点,a是schemas.openxmlformats.org/spreadsheet…




可以看到,xpath的用法很简单,就是找到si节点下的子节点t的内容,然后放进数组里



最终生成的values数组是[ 'lang', 'cn','en', 'lang001','我是阳光', 'i am sunny','lang002', '前端阳光','FE Sunny', 'lang003','带带我', 'ddw']


现在我们要获取sheet的内容了,我们先分析一下xml结构



可以看到sheetData节点其实就是记录strings的内容的信息的,strings的内容是我们真正输入的,而sheet则是类似一种批注。


我们分析看看


row就是表示表格中的行,c则表示的是列,属性t="s"表示的是当前这个格子有内容,r="A1"表示的是在第一行中的A列



而节点v则表示该格子是该表格的第几个有值的格子,不信?我们可以试试看




可以看到这打印出来的xml内容,strings中已经没有了那两个值,而sheet中的那两个格子的c节点的t属性没了,而且v节点也没有了。


现在我们可以知道,string只保存有值的格子里的值,而sheet则是一个网格,不管格子有没有值都会记录,有值的会有个序号存在v节点中。


现在就要收集c节点


  const na = {
textContent: ''
}

class CellCoords {
constructor(cell) {
cell = cell.split(/([0-9]+)/)
this.row = parseInt(cell[1])
this.column = colToInt(cell[0])
}
}

class Cell {
constructor(cellNode) {
const r = cellNode.getAttribute('r')
const type = cellNode.getAttribute('t') || ''
const value = (select('a:v', cellNode, 1) || na).textContent
const coords = new CellCoords(r)

this.column = coords.column // 该格子所在列数
this.row = coords.row // 该格子所在行数
this.value = value // 该格子的顺序
this.type = type // 该格子是否为空
}
}

const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
node => new Cell(node)
)

每个c节点用cell对象来表示


可以看到cell节点有四个属性。


你现在知道它为什么要保存顺序了吗?


因为这样才可以直接从strings生成的values数组中拿出对应顺序的值填充到网格中。


接下来要获取总共有多少列数和行数。这就需要获取最大最小行数列数,然后求差得到


// 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

function calculateDimensions(cells) {
const comparator = (a, b) => a - b
const allRows = cells.map(cell => cell.row).sort(comparator)
const allCols = cells.map(cell => cell.column).sort(comparator)
const minRow = allRows[0]
const maxRow = allRows[allRows.length - 1]
const minCol = allCols[0]
const maxCol = allCols[allCols.length - 1]

return [{ row: minRow, column: minCol }, { row: maxRow, column: maxCol }]
}

接下来就根据列数和行数造空二维数组,然后再根据cells和values填充内容


  // 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

// 生成二维空数组
data = getEmpty2DArr(rows, cols)

// 填充二维空数组
for (const cell of cells) {
let value = cell.value

// s表示该格子有内容
if (cell.type == 's') {
value = values[parseInt(value)]
}

// 填充该格子
if (data[cell.row - d[0].row]) {
data[cell.row - d[0].row][cell.column - d[0].column] = value
}
}
return data

我们看看最终生成的data,可以发现,excel的网格已经被二维数组模拟出来了



所以我们看看extractData的完整实现


function extractData(files) {
let sheet
let values
let data = []

try {
sheet = new XMLDOM.DOMParser().parseFromString(files.sheet.contents)
const valuesDoc = new XMLDOM.DOMParser().parseFromString(
files.strings.contents
)

// 把所有每个格子的内容都放进了values数组里。
values = select('//a:si', valuesDoc).map(string =>
select('.//a:t', string)
.map(t => t.textContent)
.join('')
)

console.log(values);
} catch (parseError) {
return []
}



const na = {
textContent: ''
}

class CellCoords {
constructor(cell) {
cell = cell.split(/([0-9]+)/)
this.row = parseInt(cell[1])
this.column = colToInt(cell[0])
}
}

class Cell {
constructor(cellNode) {
const r = cellNode.getAttribute('r')
const type = cellNode.getAttribute('t') || ''
const value = (select('a:v', cellNode, 1) || na).textContent
const coords = new CellCoords(r)

this.column = coords.column // 该格子所在列数
this.row = coords.row // 该格子所在行数
this.value = value // 该格子的顺序
this.type = type // 该格子是否为空
}
}

const cells = select('/a:worksheet/a:sheetData/a:row/a:c', sheet).map(
node => new Cell(node)
)

// 计算该表格的最大最小列数行数
d = calculateDimensions(cells)

const cols = d[1].column - d[0].column + 1
const rows = d[1].row - d[0].row + 1

// 生成二维空数组
data = getEmpty2DArr(rows, cols)

// 填充二维空数组
for (const cell of cells) {
let value = cell.value

// s表示该格子有内容
if (cell.type == 's') {
value = values[parseInt(value)]
}

// 填充该格子
if (data[cell.row - d[0].row]) {
data[cell.row - d[0].row][cell.column - d[0].column] = value
}
}
return data
}


接下来就是要去除空行和空列,并将二维数组翻转成我们需要的格式


function handleData(data) {
if (data) {
data = clearEmptyArrItem(data)
data = rotateExcelDate(data)
data = clearEmptyArrItem(data)
}
return data
}


可以看到,现在数组的第一项子数组则是key列表了。


接下来就可以根据key来生成对应的json文件了。


3. 生成json数据


这一步非常简单


function generateJsonFile(excelDatas, outputPath) {

// 获得转化成json格式
const jsons = convertProcess(excelDatas)

// 生成写入文件
writeFile(jsons, outputPath)
}

首先就是获取json数据


先获取data数组的第一项数组,第一项数组是key,然后生成每种语言的json对象


  /**
*
* @param {array*2} data
* 返回处理完后的多语言数组,每一项都是一个json对象。
*/

function convertProcess(data) {
var keys_arr = [],
data_arr = [],
result_arr = [],
i,
j,
data_arr_len,
col_data_json,
col_data_arr,
data_arr_col_len
// 表格合并处理,这是json属性列。
keys_arr = data[0]
// 第一例是json描述,后续是语言包
data_arr = data.slice(1)

for (i = 0, data_arr_len = data_arr.length; i < data_arr_len; i++) {
// 取出第一个列语言包
col_data_arr = data_arr[i]
// 该列对应的临时对象
col_data_json = {}
for (
j = 0, data_arr_col_len = col_data_arr.length;
j < data_arr_col_len;
j++
) {

col_data_json[keys_arr[j]] = col_data_arr[j]
}
result_arr.push(col_data_json)
}

return result_arr
}


我们可以看看生成的result_arr



可见已经成功生成每一种语言的json对象了。


接下来只需要生成json文件就可以了,注意把之前生成的excel文件删除


  //得到的数据写入文件
function writeFile(datas, outputPath) {
for (let i = 0, len = datas.length; i < len; i++) {
fs.writeFileSync(outputPath +
(datas[i].filename || datas[i].lang) +
'.json',
JSON.stringify(datas[i], null, 4)
)
}
rmobj.flush();
}

到此,一个稍微完美的插件就此完成了。 撒花撒花!!!!

收起阅读 »

V8系列第二篇:从执行上下文的角度看JavaScript到底是怎么运行的

1.前言 先来说一说V8引擎和浏览器 V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空...
继续阅读 »


1.前言


先来说一说V8引擎和浏览器


V8引擎要想运行起来,就必须依附于浏览器,或者依附于Node.js宿主环境。因此V8引擎是被浏览器或者Node.js启动的。比如在Chrome浏览器中,你打开一个网址后,渲染进程便会初始化V8引擎,同时在V8中会初始化堆空间和栈空间,而栈空间就是来管理执行上下文的。而执行上下文就可以说是我们平常写的JavaScript代码的运行环境。


好了简单理解一下,那接下来本篇就重点来学习一下V8引擎中的执行上下文


2.执行上下文概述



首先从宏观的角度来说: JavaScript代码要想能够被执行,就必须先被V8引擎编译,编译完成之后才会进入到执行阶段,总结为六个字:先编译再执行



在V8引擎编译的过程中,同时会生成执行上下文。最开始执行代码的时候通常会生成全局执行上下文、执行一个函数时会生成该函数的执行上下文、当执行一个代码块时也会生成代码块的可执行上下文。所以一段代码可以说成是先编译再执行,那么整个过程就是无数个先编译再执行构成的(通常编译发生在执行代码前的几微秒,甚至更短的时间)。


我们再来理解一下上面说到的执行上下文, 在JavaScript 高级程序设计(第四版)中大概是这样描述的:



执行上下文的概念在JavaScript中是非常重要的。变量或者函数的执行上下文决定了它们可以访问哪些数据,以及他们拥有哪些行为(可以执行哪些方法吧)。每个执行上下文都有一个关联的变量对象,而这个执行上下文中定义的所有变量和函数都存在于这个对象上。这个对象我们在代码中是无法访问的。



执行上下文可以说是执行一段JavaScript代码的运行环境,可以算作是一个抽象的概念。


简单的理解一下概念(下文如果再需要的时候你可以返回顶部再次理解查看)后,我们就来看看JavaScript是怎么将一个变量和函数运行起来的。


3.准备测试代码


这里为了更直观的查看代码的运行效果,我特意新建了一个xxxx.html文件,文件所有代码如下所示:
这里突然发现html文件中只有script标签和js代码也是可以执行的,不清楚以前是不是也是可以,还是说JavaScript引擎在后期做了优化处理。


<script>
a_Function()
var a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>


特别强调一个点,我上面声明变量使用的var关键字



运行后的执行结果


image.png


4.调试var声明的变量


相信通过运行结果,你心中应该有了自己的代码执行过程了。我们接着往下操作,在第2行代码(下文截图中的位置)打个调试断点,如下图所示


image.png


此时代码已经准备开始要执行a_Function函数了。脑补一下,我们就以此为分割点(按正常来说这肯定是不合理的,因为代码已经开始执行了,不过你可以暂且这样尝试去理解一下),就是运行到第2行代码之前的时间段或者状态,我们就称它为编译阶段,这之后代码就开始运行了,我们称它为执行阶段


1、通过截图可以发现,作用域下的全局 已经有了一个a_Function函数,以及一个a_variable变量其值为 undefined,这里可以看到许许多多的其他变量、函数,这其实就是全局window对象。


2、使用过JavaScript的人都清楚,JavaScript是按照顺序执行代码的,但是通过截图去看,好像又不太对劲,所以执行前的编译阶段,JavaScript引擎还是处理了不少事情的,它做了什么事情呢?


V8引擎编译这段代码的时候,同时会生成一个全局执行上下文,在截图的第二行代码发现是一个函数,便会在代码中查找到该函数的定义,并将该函数体放到全局执行上下文词法环境中。该函数体里的代码还未执行,所以不会去编译,继续第三行代码,发现是var声明的一个变量,便会将该变量放到全局执行上文变量环境中,同时给该变量赋值为undefined。


具体如下模拟代码


function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
var a_variable = undefined

这段代码主要在编译代码阶段做了变量提升,会将var声明的变量存放到变量环境中(let和const声明的变量存放到词法环境中),而函数的声明会被存放到词法环境中。
词法环境变量环境是存在于执行上下文的,变量的默认值会被设置为undefined,函数的执行体会被带到词法环境
然后还会生成可执行代码,其实编译生成的是字节码,下面的代码算是模拟代码:


a_Function()
a_variable = 'aehyok'
console.log(a_variable)

  • 执行阶段
    接下来开始按照顺序执行上面生成的可执行代码,其实在执行阶段已经变成了机器码

a_Function()
a_variable = 'aehyok'
console.log(a_variable)

第一行模拟代码:先调用a_Function,此时会开始生成该函数的函数执行上下文, 执行a_Function中的代码,函数a_Function执行了 undefined,因为此时的a_variable还没给予赋值操作


第二行模拟代码:对a_variable变量进行赋值字符串"aehyok",此时变量环境中的a_variable值变为"aehyok"


第三行模拟代码:打印已经赋值为aehyok的变量。


5.调试let声明的变量


5.1主要是将上面的测试代码中:声明变量的关键字var改为let


<script>
a_Function()
let a_variable = 'aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行代码以后发现直接报错了,报错内容如下图所示


image.png


5.2打断点调试代码


image.png
代码断点打到如截图中第2行位置,可以看到let声明的变量,存在于单独的Script作用域中,并且赋值为undefined。


5.3分析5.1和5.2的代码


  • 通过varlet两种方式代码运行比对情况来看,let声明变量的方式不存在变量提升的情况。
  • 通过3.2截图可以发现,let声明变量的方式,在作用域中的已经创建,并赋值为undefined,但通过查阅资料发现:


let声明的变量,主要是因为V8虚拟机做了限制,虽然a_variable已经在内存中并且赋值为undefined,但是当你在let a_variable 之前访问a_variable时,根据ECMAScript定义,虚拟机会阻止的访问!也可以说成是形成了暂时性的死区,这是语法规定出来的。所以就会报错。



6.调试let声明的变量继续执行


主要添加了一个let声明的变量,以及为其进行了赋值操作,代码如下所示


<script>
a_Function()
var a_variable = 'a_aehyok'
let aa_variable = 'aa_aehyok'
console.log(a_variable)
function a_Function() {
console.log('函数a_Function执行了', a_variable);
}
</script>

执行后情况截图如下


image.png


可以发现通过var声明的变量和let(也可以使用const)声明的变量被储存在了不同的位置,之前上面说过通过var声明的变量被存放到了变量环境中了。那么现在我再告诉你,通过let(也可以是const)声明的变量被存放到了词法环境中了。


  • var声明的变量存放在变量环境
  • let和const声明的变量存放在词法环境
  • 函数的声明存放在词法环境
  • 变量环境词法环境都存在于执行上下文

7.总结三种执行上下文


在上面的一小段代码中,我们已经使用过了两种执行上下文,全局执行上下文函数执行上下文


  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。


var声明的变量会在全局window对象上,而let和const声明的变量是不会在全局window对象上的。而全局函数时会在全局window对象上。




  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。



  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。



8.总结



  • 1、通过这篇简单的文章,我想我自己理清楚了,原来JavaScript代码是先编译再执行的。



  • 2、然后代码在编译的时候就生成了执行上下文,也就是代码运行的环境。



  • 3、var声明的变量存在变量提升,并且在编译阶段存放到了变量环境中,变量环境其实也是一个词法环境



  • 4、通过变量提升发现,代码会先生成执行上下文,然后再生成可执行的代码



  • 5、const和let声明的变量不存在变量提升,并且再编译阶段被存放到了词法环境中。



  • 6、所有var定义的全局变量和全局定义的函数,都会在window对象上。



  • 7、所有let和const定义的全局变量不会定义在全局上下文中,但是在作用域链的解析效果上是一样的(跟var定义的)。

 
收起阅读 »

V8开篇:V8是如何运行JavaScript(let a = 1)代码的?

我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言: 编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需...
继续阅读 »


我们知道,机器是不能直接理解我们平常工作或者自己学习的代码的。所以,在执行程序之前,需要将代码翻译成机器能读懂的机器语言。按语言的执行流程,可以把计算机语言划分为编译型语言和解释型语言:



编译型语言:在代码运行前编译器直接将对应的代码转换成机器码,运行时不需要再重新翻译,直接可以使用编译后的结果。




解释型语言:需要将代码转换成机器码,和编译型语言的区别在于运行时需要转换。解释型语言的执行速度要慢于编译型语言,因为解释型语言每次执行都需要把源码转换一次才能执行。



Java 和 C++ 等语言都是编译型语言,而 JavaScript 是解释性语言,它整体的执行速度会略慢于编译型的语言。V8 是众多JavaScript引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。


1.运行的整体过程


未命名文件 (4).png


2.英译汉翻译的过程


比如我们看到了google V8官网的一篇英文文章 v8.dev/blog/faster…,在阅读的过程中,可以就是要对每一个单词进行解析翻译成中文,然后多个单词进行语法的解析,再通过对整句话进行整个语句进行解析,那么这句话就翻译结束了。


下面我们就举例一句英文的翻译过程:I am a programmer。


  • 1、首先对输入的字符串I am a programmer。进行拆分便会拆分成 I am a programmer


相当于词法分析




  • 2、I 是一个主语, am 是一个谓语, a是一个形容词, programmer是个名词, 标点符号。



  • 3、I的意思, am的意思, a一个的意思, programmer程序员的意思, 句号的意思。




2和3一起相当于语法分析



  • 4、对3中的语法分析进行拼接处理:我是一个程序员。当然这是非常简单的一个英译汉,一篇文章的话,就会复杂一些了。


相当于语义分析



3.V8运行的整个过程


3.1.准备一段JavaScript源代码


let a = 10

3.2.词法分析:


一段源代码,就是一段字符串。编译器识别源代码的第一步就是要进行分词,将源代码拆解成一个个的token。所谓的token,就是不可再分的单个字符或者字符串。


3.3.token


通过 esprima.org/demo/parse.… 可以查看生成的tokens,也就是上面那段源代码生成的所有token。


Token类别: 关键字、标识符、字面量、操作符、数据类型(String、Numeric)等


image.png


3.4.语法分析


将上一步生成的 token 数据,根据语法规则转为 AST。通过astexplorer.net 可以查看生成AST抽象语法树。


3.5.AST


生成的AST如下图所示,生成过程就是先分词(词法分析),再解析(语法分析)


image.png
当然你也可以查看生成的AST的JSON结构


{
"type": "Program",
"start": 0,
"end": 9,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 9,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}

同样我在本地下载了v8,直接用v8来查看AST


v8-debug  --print-ast hello.js

image.png


3.6.解释器


解释器会将AST生成字节码,生成字节码的过程也就是对AST抽象语法树进行遍历循环,并进行语义分析


3.7.字节码


在最开始的V8引擎中是没有字节码,是直接将AST转换生成为机器码。这种架构存在的问题就是内存消耗特别大,尤其是在移动设备上,编译出来的机器码占了整个chorme浏览器的三分之一,这样为代码运行时留下的内存就更小了。
于是后来在V8中加入了Ignition 解释器,引入字节码,主要就是为了减少内存消耗。
本地可以使用V8命令行查看生成的字节码


v8-debug  --print-bytecode hello.js

image.png


3.8.热点代码


首先判断字节码是否为热点代码。通常第一次执行的字节码,Ignition 解释器会逐条解释执行。在执行的过程中,如果发现是热点代码,比如for 循环中的代码被执行了多次,这种就称之为热点代码。那么后台的TurboFan就会把该段热点代码编译为高效的机器码,然后再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了, 这样就大大提升了代码的执行效率。


3.9.编译器


TurboFan编译器也可以说是JIT的即时编译器,也可以说是优化编译器。



Ignition 解释器: 可以将AST生成字节码,还可以解释执行字节码。



4、总结


  • 了解V8整个的运行机制
  • 学习JavaScript到底是怎么运行的
  • 对日后编写JavaScript代码有非常多的好处
  • 看完学习了,能提升我们的技术水平
  • 对于日后遇到问题,能够从底层去思考问题出在那里,更快速的定位和解决问题
  • 真的非常熟悉了,可以自己开发一门新的语言
 
收起阅读 »

一盏茶的功夫,拿捏作用域&作用域链

前言 我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢? 一、作...
继续阅读 »


前言


我们需要先知道的是引擎,引擎的工作简单粗暴,就是负责javascript从头到尾代码的执行。引擎的一个好朋友是编译器,主要负责代码的分析和编译等;引擎的另一个好朋友就是今天的主角--作用域。那么作用域用来干什么呢?作用域链跟作用域又有什么关系呢?


一、作用域(scope)


作用域的定义:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。


1、作用域的分类

  1. 全局作用域

var name="global";
function foo(){
console.log(name);
}
foo();//global

这里函数foo()内部并没有声明name变量,但是依然打印了name的值,说明函数内部可以访问到全局作用域,读取name变量。再来一个例子:


hobby='music';
function foo(){
hobby='book';
console.log(hobby);
}
foo();//book

这里全局作用域和函数foo()内部都没有声明hobby这个变量,为什么不会报错呢?这是因为hobby='music';写在了全局作用域,就算没有var,let,const的声明,也会被挂在window对象上,所以函数foo()不仅可以读取,还可以修改值。也就是说hobby='music';等价于window.hobby='music';


  1. 函数体作用域

函数体的作用域是通过隐藏内部实现的。换句话说,就是我们常说的,内层作用域可以访问外层作用域,但是外层作用域不能访问内层。原因,说到作用域链的时候就迎刃而解了。


function foo(){
var age=19;
console.log(age);
}
console.log(age);//ReferenceError:age is not defined

很明显,全局作用域下并没有age变量,但是函数foo()内部有,但是外部访问不到,自然而然就会报错了,而函数foo()没有调用,也就不会执行。


  1. 块级作用域

块级作用域更是见怪不怪,像我们接触的let作用域,代码块{},for循环用let时的作用域,if,while,switch等等。然而,更深刻理解块级作用域的前提是,我们需要先认识认识这几个名词:


--标识符:能在作用域生效的变量。函数的参数,变量,函数名。需要格外注意的是:函数体内部的标识符外部访问不到


--函数声明:function 函数名(){}


--函数表达式: var 函数名=function(){}


--自执行函数: (function 函数名(){})();自执行函数前面的语句必须有分号,通常用于隐藏作用域。


接下来我们就用一个例子,一口气展示完吧


function foo(sex){
console.log(sex);
}
var f=function(){
console.log('hello');
}
var height=180;
(
function fn(){
console.log(height);
}
)();
foo('female');
//依次打印:
//180
//female
//hello

分析一下:标识符:foo,sex,height,fn;函数声明:function foo(sex){};函数表达式:var f=function(){};自执行函数:(function fn(){})();需要注意,自执行函数fn()前面的var height=180;语句,分号不能抛弃。否则,你可以试一下。


二、预编译


说好只是作用域和作用域链的,但是考虑到理解作用域链的必要性,这里还是先聊聊预编译吧。先讨论预编译在不同环境发生的情况下,是如何进行预编译的。


  1. 发生在代码执行之前

(1)声明提升


console.log(b);
var b=123;//undefined

这里打印undefined,这不是报错,与Refference:b is not defined不同。这是代码执行之前,预编译的结果,等同于以下代码:


var b;//声明提升
console.log(b);//undefined
b=123;

(2)函数声明整体提升


test();//hello123  调用函数前并没有声明,但是任然打印,是因为函数声明整体提升了
function test(){
var a=123;
console.log('hello'+a);
}

2.发生在函数执行之前


理解这个只需要掌握四部曲


(1)创建一个AO(Activation Object)


(2)找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined


(3)将实参和形参统一


(4)在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
那么接下来就放大招了:


var global='window';
function foo(name,sex){
console.log(name);
function name(){};
console.log(name);
var nums=123;
function nums(){};
console.log(nums);
var fn=function(){};
console.log(fn);
}
foo('html');

这里的结果是什么呢?分析如下:


//从上到下
//1、创建一个AO(Activation Object)
AO:{
//2、找形参和变量声明,然后将形参和变量声明作为AO的属性名,属性值为undefined
name:undefined,
sex:undefined,
nums=undefined,
fn:undefined,
//3、将实参和形参统一
name:html,
sex:undefined,
nums=123,
fn:function(){},
//4、在函数体内找函数声明,将函数名作为AO对象的属性名,属性值予函数体
name:function(){},
sex:undefined,
fn:function(){},
nums:123//这里不仅存在nums变量声明,也存在nums函数声明,但是取前者的值

以上步骤得到的值,会按照后面步骤得到的值覆盖前面步骤得到的值
}
//依次打印
//[Function: name]
//[Function: name]
//123
//[Function: fn]

3.发生在全局(内层作用域可以访问外层作用域)


同发生在函数执行前一样,发生在全局的预编译也有自己的三部曲:


(1)创建GO(Global Object)对象
(2)找全局变量声明,将变量声明作为GO的属性名,属性值为undefined
(3)在全局找函数声明,将函数名作为GO对象的属性名,属性值赋予函数体
举个栗子:


var global='window';
function foo(a){
console.log(a);
console.log(global);
var b;
}
var fn=function(){};
console.log(fn);
foo(123);
console.log(b);

这个例子比较简单,一样的步骤和思路,就不在赘述分析了,相信你已经会了。打印结果依次是:


[Function: fn]
123
window
ReferenceError: b is not defined

好啦,进入正轨,我们接着说作用域链。


三、作用域链


作用域链就可以帮我们找到,为什么内层可以访问到外层,而外层访问不到内层?但是同样的,在认识作用域链之前,我们需要见识见识一些更加晦涩抽象的名词。


  1. 执行期上下文:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。
  2. 查找变量:从作用域链的顶端依次往下查找。

3. [[scope]]:作用域属性,也称为隐式属性,仅支持引擎自己访问。函数作用域,是不可访问的,其中存储了运行期上下文的结合。


我们先看一眼函数的自带属性:


function test(){//函数被创建的那一刻,就携带name,prototype属性
console.log(123);
}
console.log(test.name);//test
console.log(test.prototype);//{} 原型
// console.log(test[[scope]]);访问不到,作用域属性,也称为隐式属性

// test() --->AO:{}执行完毕会回收
// test() --->AO:{}执行完毕会回收

接下来看看作用域链怎么实现的:


var global='window';
function foo(){
function fn(){
var fn=222;
}
var foo=111;
console.log(foo);
}
foo();

分析:


GO:{
foo:function(){}
}
fooAO:{
foo:111,
fn:function(){}
}
fnAO:{
fn:222
}
// foo定义时 foo.[[scope]]---->0:GO{}
// foo执行时 foo.[[scope]]---->0:AO{} 1:GO{} 后访问的在前面
//fn定义时 fn.[[scope]]---->0:fnAO{} 1:fooAO{} 2:GO{}
fnAO:fn的AO对象;fooAO:foo的AO对象

 


综上而言:作用域链就是[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。



收起阅读 »

Dart(三)—方法定义、箭头函数、函数相互调用、匿名、自执行方法及闭包

方法定义 dart自定义方法的基本格式: 返回类型 方法名称(参数1,参数2,...){ 方法体 return 返回值 / 或无返回值; } 定义方法的的几个例子: void printInfo(){ print('我是一个自定义方法');...
继续阅读 »

方法定义



dart自定义方法的基本格式:

返回类型 方法名称(参数1,参数2,...){
方法体
return 返回值 / 或无返回值;
}

定义方法的的几个例子:


void printInfo(){
print('我是一个自定义方法');
}

int getNum(){
var count = 123;
return count;
}

String printUserInfo(){

return 'this is str';
}

List getList(){

return ['111','2222','333'];
}

Dart没有public 、private等关键字,_ 下横向直接代表 private


方法的作用域


void main(){

void outFun(){
innerFun(){

print('aaa');
}
innerFun();
}

// innerFun(); 错误写法

outFun(); //调用方法
}

方法传参


一般定义:


String getUserInfo(String username, int age) {
//形参
return "姓名:$username -> 年龄:$age";
}

print(printUserInfo('小明', 23)); //实参

Dart中可以定义一个带可选参数的方法 ,可选参数需要指定类型默认值:


void main() {
String printUserInfo(String username, [int age = 0]) { //age格式表示可选
//形参
if (age != 0) {
return "姓名:$username -> 年龄:$age";
}
return "姓名:$username -> 年龄不详";
}

print(printUserInfo('小明', 28)); //实参
//可选就可以不传了
print(printUserInfo('李四'));
}

定义一个带默认参数的方法:


String getUserInfo(String username,[String sex='男',int age=0]){  //形参
if(age!=0){
return "姓名:$username -> 性别:$sex -> 年龄:$age";
}
return "姓名:$username -> 性别:$sex -> 年龄不详";
}
print(getUserInfo('张三'));
print(getUserInfo('李四','男'));
print(getUserInfo('李梅梅','女',25));

定义一个命名参数的方法,定义命名参数需要指定类型默认值:


命名参数的好处是在使用时可以不用按顺序赋值,看下面代码:


String getUserInfo(String username, {int age = 0, String sex = '男'}) {//形参
if (age != 0) {
return "姓名:$username -> 性别:$sex -> 年龄:$age";
}
return "姓名:$username -> 性别:$sex -> 年龄保密";
}
print(getUserInfo('张三',sex: '男',age: 20));

定义一个把方法当做参数的方法:


其实就是方法可以当做参数来用,这点和Kotlin也是一样的:


//方法1 随便打印一下
fun1() {
print('fun1');
}

//方法2 参数是一个方法
fun2(fun) {
fun();
}

//调用fun2这个方法 把fun1这个方法当做参数传入
fun2(fun1());

箭头函数和函数的相互调用


箭头函数


在之前的学习中,我们知道可以使用forEach来遍历List,其一般格式如下:


List list = ['a', 'b', 'c'];
list.forEach((value) {
print(value);
});

而箭头函数就是可以简写这种格式:


list.forEach((value) => print(value));

箭头后面指向的就是方法的返回值,这里要注意的是:



箭头函数内只能写一条语句,并且语句后面没有分号(;)



对于之前map转换的例子也可以使用箭头方法来简化一下:


List list = [1, 3, 6, 8, 9];
var newList = list.map((value) {
if (value > 3) {
return value * 2;
}
return value;
});

这里就是修改List里面的数据,让数组中大于3的值乘以2。那用箭头函数简化后可以写成:


var newList = list.map((value) => value > 3 ? value*2 : value);

一句代码完成,非常有意思。


函数的相互调用


  // 定义一个方法来判断一个数是否是偶数  
bool isEvenNumber(int n) {
if (n % 2 == 0) {
return true;
}
return false;
}
// 定义一个方法打印1-n以内的所有偶数
prinEvenNumber(int n) {
for (var i = 1; i <= n; i++) {
if (isEvenNumber(i)) {
print(i);
}
}
}
prinEvenNumber(10);

匿名方法、自执行方法及方法的递归


匿名方法


var printNum = (){
print(12);
};
printNum();

这里很明显跟Kotlin中的特性基本是一样的。带参数的匿名方法:


var printNum = (int n) {
print(n + 2);
};

printNum(3);

自执行方法


自执行方法顾名思义就是不需要调用,会自动去执行的,这是因为自执行函数的定义和调用合为了一体。当我们创建了一个匿名函数,并执行了它,由于外部无法引用的它的内部变量,所以在执行完就会很快被释放,而且这种做法不会污染到全局对象。看如下代码:


((int n) {
print("这是一个自执行方法 + $n");
})(666);
}

方法的递归


方法的递归无非就是在条件满足的条件下继续在方法内调用自己本身,看以下代码:


var sum = 0;
void fn(int n) {
sum += n;
if (n == 0) {
return;
}
fn(n - 1);
}
fn(100);
print(sum);

实现的是1加到100。


闭包


闭包是一个前端的概念,客户端开发早期使用Java可以说是不支持闭包,或是不完整的闭包,但Kotlin是可以支持闭包的操作。


闭包的意思就是函数嵌套函数, 内部函数会调用外部函数的变量或参数, 变量或参数不会被系统回收(不会释放内存)。所以闭包解决的两个问题是:



  • 变量常驻内存

  • 变量不污染全局


闭包的一般写法是:



  • 函数嵌套函数,并return 里面的函数,这样就形成了闭包


闭包的写法:


Function func() {
var a = 1; /*不会污染全局 常驻内存*/
return () {
a++;
print(a);
};
}

这里return匿名方法后,a的值就可以常驻内存了:


var mFun = func();
mFun();
mFun();
mFun();

打印:2、3、4。


作者:柒叁
链接:https://juejin.cn/post/7116205745367941150
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Dart(二)—循环表达式、List、Set及Map的常用属性和方法

Dart的循环表达式 for循环 for (int i = 1; i<=100; i++) { print(i); } 也可以写成: for (var i = 1; i<=10; i++) { print(i); } 对于List...
继续阅读 »

Dart的循环表达式


for循环


for (int i = 1; i<=100; i++) {   
print(i);
}

也可以写成:


for (var i = 1; i<=10; i++) {
print(i);
}

对于List的遍历我们可以这样做:


var list = <String>["张三","李四","王五"];
for (var element in list) {
print(element);
}

对于Map的迭代我们也可以使用for循环语句:


var person={
"name":"小明",
"age":28,
"work":["程序员","Android开发"]
};

person.forEach((key, value) {
print(value);
});

while语句


while有两种语句格式:


while(表达式/循环条件){    

}

do{
语句/循环体

}while(表达式/循环条件);


注意:



  • 1、最后的分号不要忘记

  • 2、循环条件中使用的变量需要经过初始化

  • 3、循环体中,应有结束循环的条件,否则会造成死循环。



看下面代码:


int i = 1;
while (i <= 10) {
print(i);
i++;
}

do...while()最大的区别就是不管条件成立与否都会至少执行一次:


var i = 2;
do{
print('执行代码');
}while(i < 2);

break和continue语句


break语句功能:



  • switch语句中使流程跳出switch结构。

  • 在循环语句中使流程跳出当前循环,遇到break循环终止,后面代码也不会执行


需要强调的是:



  • 如果在循环中已经执行了break语句,就不会执行循环体中位于break后的语句。

  • 在多层循环中,一个break语句只能向外跳出一层


break可以用在switch case中 也可以用在for循环和while循环中。


continue语句的功能:


只能在循环语句中使用,使本次循环结束,即跳过循环体中下面尚未执行的语句,接着进行下次的是否执行循环的判断。


continue可以用在for循环以及while循环中,但是不建议用在while循环中,不小心容易死循环。


break使用:


//如果 i等于4的话跳出循环
for(var i=1;i<=10;i++){
if(i==4){
break; /*跳出循环体*/
}
print(i);
}

//break语句只能向外跳出一层
for(var i = 0;i < 5;i++){
for(var j = 0;j< 3;j++){
if(j == 1){
break;
}
}
}

while循环跳出:


//while循环 break跳出循环

var i = 1;

while(i< =10){
if(i == 4){
break;
}
print(i);
i++;
}

continue使用:


//如果i等于4的话跳过

for(var i=1;i<=5;i++){
if(i == 2){
continue; //跳过当前循环体 然后循环还会继续执行
}
print(i);
}

List常用属性和方法


常用属性:



  • length 长度

  • reversed 翻转

  • isEmpty 是否为空

  • isNotEmpty 是否不为空


常用方法:



  • add 增加

  • addAll 拼接数组

  • indexOf 查找 传入具体值

  • remove 删除 传入具体值

  • removeAt 删除 传入索引值

  • fillRange 修改

  • insert(index,value) 指定位置插入

  • insertAll(index,list) 指定位置插入List

  • toList() 其他类型转换成List

  • join() List转换成字符串

  • split() 字符串转化成List

  • forEach

  • map

  • where

  • any


一些常用属性和方法使用举例:


var list=['张三','李四','王五',"小明"];
print(list.length);
print(list.isEmpty);
print(list.isNotEmpty);
print(list.reversed); //对列表倒序排序

print(list.indexOf('李四')); //indexOf查找数据 查找不到返回-1 查找到返回索引值

list.remove('王五');

list.removeAt(2);

list.fillRange(1, 2,'a'); //修改 1是开始的位置 2二是结束的位置

print(list);

list.insert(1,'a');

print(list);

list.insertAll(1, ['a','b']); //插入多个

Set


Set的最主要的功能就是去除数组重复内容,它是没有顺序且不能重复的集合,所以不能通过索引去获取值。


var s = new Set();
s.add('A');
s.add('B');
s.add('B');

print(s); //{A, B}

add相同内容时候无法添加进去的。


Set可以通过add方法添加一个List,并清除值相同的元素:


var list = ['香蕉','苹果','西瓜','香蕉','苹果','香蕉','苹果'];
var s = new Set();
s.addAll(list);
print(s);
print(s.toList());

Map常用属性和方法


Map是无序的键值对,它的常用属性主要有以下:


常用属性:



  • keys 获取所有的key值

  • values 获取所有的value值

  • isEmpty 是否为空

  • isNotEmpty 是否不为空


常用方法:



  • remove(key) 删除指定key的数据

  • addAll({...}) 合并映射 给映射内增加属性

  • containsValue 查看映射内的值 返回true/false

  • forEach

  • map

  • where

  • any

  • every


map转换:


List list = [1, 3, 4];
//map转换,根据返回值返回新的元素列表
var newList = list.map((value) {
return value * 2;
});
print(newList.toList());

where:获取符合条件的元素:


List list = [1,3,4,5,7,8,9];

var newList = list.where((value){
return value > 5;
});
print(newList.toList());

any:是否有符合条件的元素


List list = [1, 3, 4, 5, 7, 8, 9];
//只要集合里面有满足条件的就返回true
var isContain = list.any((value) {
return value > 5;
});
print(isContain);

every:需要每一个都满足条件


List myList=[1,3,4,5,7,8,9];
//每一个都满足条件返回true 否则返回false
var flag = myList.every((value){

return value > 5;
});
print(flag);

Set使用forEach遍历:


var s=new Set();

s.addAll([11,22,33]);

s.forEach((value) => print(value));

Map使用forEach遍历:


Map person={
"name":"张三",
"age":28
};

person.forEach((key,value){
print("$key -> $value");
});

作者:柒叁
链接:https://juejin.cn/post/7116159285054144526/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Dart(一)—变量、常量、基本类型、运算符、条件判断以及类型转换

前言 Dart语言跟Kotlin都是一种强大的脚本语言,它的很多语法跟Kotlin是很相似的。比如Dart也是可以不预先定义变量类型 ,自动会类型推倒,它修饰一般变量的关键字也是var,所以如果我们熟悉Kotlin,Dart也会很容易上手。 Dart变量和常量...
继续阅读 »

前言


Dart语言跟Kotlin都是一种强大的脚本语言,它的很多语法跟Kotlin是很相似的。比如Dart也是可以不预先定义变量类型 ,自动会类型推倒,它修饰一般变量的关键字也是var,所以如果我们熟悉KotlinDart也会很容易上手。


Dart变量和常量


变量


如前言所说,DartKotlin一样是强大的脚本类语言,可以不预先定义变量类型 ,自动会类型推倒,Dart中定义变量可以通过var关键字可以通过类型来申明变量:


var str = 'dart';

String str2 = 'this is dart';

int count = 123;


注意: var 后就不要写类型 , 写了类型 不要var 两者都写 var a int = 5; 报错



常量:final 和 const修饰符



  • const修饰的值不变 要在定义变量的时候就得赋值;

  • final可以开始不赋值 只能赋一次,而final不仅有const的编译时常量的特性,最重要的它是运行时常量,并且final是惰性初始化,即在运行时第一次使用前才初始化。


final name = 'Max';
final String sex = '男';

const bar = 1000000;
const double atm = 1.01325 * bar;

如果我们使用了阿里的代码规范插件,其实他会提示我们最好用const代替final


Dart的命名规则



  • 变量名称必须由数字、字母、下划线和美元符($)组成。

  • 注意:标识符开头不能是数字

  • 标识符不能是保留字和关键字。

  • 变量的名字是区分大小写的如: age和Age是不同的变量。在实际的运用中,也建议,不要用一个单词大小写区分两个变量。

  • 标识符(变量名称)一定要见名思意 :变量名称建议用名词,方法名称建议用动词


Dart的入口方法


Dart 入口方法main有两种定义


//表示main方法没有返回值
void main(){
print('dart');
}

main(){

print('dart');
}

Dart基本类型


数据类型


Dart中常用的数据类型有以下的类型:


Numbers(数值):
int
double
Strings(字符串)
String
Booleans(布尔)
bool
List(数组)
在Dart中,数组是列表对象,所以大多数人只是称它们为列表
Maps(字典)
通常来说,Map 是一个键值对相关的对象。 键和值可以是任何类型的对象。每个键只出现一次,而一个值则可以出现多次

数值类型: int double


int整型:


  int a=123;
a=45;

double既可以是整型,也可是浮点型:


double b=23.5;

b=24;

字符串类型


字符串定义:


var str1='this is str1';

String str2='this is str2';

字符串拼接:


print("$str1 $str2");

print(str1 + str2);

布尔类型


定义方式:


bool flag1=true;

var flag2=true;

判断条件上和Kotlin使用无异。


List(数组/集合)


不指定类型定义List


var list1 = ["张三",20,true];

print(list1);
print(list1[2]);

这就有点颠覆我们以往的观念了,一个list里面还可以有不同的类型。


指定类型定义List


var list2 = <String>["张三","李四"];

print(list2);

通过[]来定义Lsit


通过[]创建的集合的容量可以变化:


var list = [];

list.add("小明");
list.add(24);
list.add(true);

print(list);

也可以指定List中的元素类型:


List<String> list = [];

又或者是:


List<String> list = List.empty(growable: true);

growable 为 false 是为 固定长度列表,为 true 是为 长度可变列表


通过List.filled创建的集合长度是固定:


var list1 = List.filled(2, "");

var list2 = List<String>.filled(2, "");

Map


Map的定义:


直接赋值方式:


var person={
"name":"小明",
"age":28,
"work":["程序员","Android开发"]
};

print(person["name"]);

print(person["age"]);

print(person["work"]);


通过Map分别赋值:


var map =new Map();

map["name"]="小明";
map["age"]=26;
map["work"]=["程序员","Android开发"];
print(map);

is 关键词来判断类型


var str = 123;

if(str is String){
print('是string类型');
}else if(str is int){
print('int');
}else{
print('其他类型');
}

运算符


算术运算符


使用和符号上和Kotlin中的基本无异:


int a=13;
int b=5;

print(a+b); //加
print(a-b); //减
print(a*b); //乘
print(a/b); //除
print(a%b); //其余
print(a~/b); //取整

关系运算符


关系运算符主要有:


==    !=   >    <    >=    <=

使用:


int a=5;
int b=3;

print(a==b); //判断是否相等
print(a!=b); //判断是否不等
print(a>b); //判断是否大于
print(a<b); //判断是否小于
print(a>=b); //判断是否大于等于
print(a<=b); //判断是否小于等于

逻辑运算符


! 取反:


bool flag=false;
print(!flag); //取反

&&并且:全部为true的话值为true 否则值为false:


bool a=true;
bool b=true;

print(a && b);

||或者:全为false的话值为false 否则值为true:


bool a=false;
bool b=false;

print(a || b);

赋值运算符


基础赋值运算符 =、??= ++ --


int c=a+b;   //从右向左

b??=23;  表示如果b为空的话把 23赋值给b

++ --


// ++  --   表示自增 自减 1
//在赋值运算里面 如果++ -- 写在前面 这时候先运算 再赋值,如果++ --写在后面 先赋值后运行运算

var a = 10;
var b = a--;

print(a); //9
print(b); //10

// var a=10;

// a++; //a=a+1;

// print(a);

复合赋值运算符 +=、-= 、*= 、 /= 、%= 、~/=


+=


var a=12;
a+=12; //a = a+12
print(a);

-=


a-=6; // a = a-6

*=


a*=3;  //a=a*3;

/=


需要返回double类型


double a=12;
a/=12;

%=


double a=12;
a %= 12;

~/=


返回的是int整型


int a1 = 3;
int a2 = 2;

int a = a1 ~/= a2;

a = 1.


条件表达式


**if else **


 bool flag=true;

if(flag){
print('true');
}else{
print('false');
}

switch case


var sex = "女";
switch (sex) {
case "男":
print('性别是男');
break;
case "女":
print('性别是女');
break;
default:
print('传入参数错误');
break;
}

三目运算符


bool flag = false;
String str = flag?'我是true':'我是false';
print(str);

??运算符


var a;
var b= a ?? 10;

print(b); // a为空,则赋值为10

// var a=22;
// var b= a ?? 10;
//
// print(b); // 20

类型转换


Number与String类型之间的转换



  • Number类型转换成String类型toString()

  • String类型转成Number类型int.parse()


StringNumber


String str = '123';

var myNum = int.parse(str);

print(myNum is int);

// String str='123.1';

// var myNum=double.parse(str);

// print(myNum is double);


String:


var myNum=12;

var str=myNum.toString();

print(str is String);

其他类型转换成Boolean类型


isEmpty:判断字符串是否为空


var str = '';
if (str.isEmpty) {
print('str空');
} else {
print('str不为空');
}

isNaN:判断值是否为非数字


var myNum = 0 / 0;

if (myNum.isNaN) {
print('NaN');
}

作者:柒叁
链接:https://juejin.cn/post/7116024077273432094
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter 桌面端实践之识别外接媒体设备

最近我们希望Flutter技术在桌面端的应用能有所突破,所以笔者跨进了本不熟悉的桌面端应用领域。今天给大家分享下我们是如何让Flutter如何识别外接媒体设备,并且实现视频流渲染和拍照;从官方插件外界纹理到platformView实践,都尝试了一遍,最后选择了...
继续阅读 »

最近我们希望Flutter技术在桌面端的应用能有所突破,所以笔者跨进了本不熟悉的桌面端应用领域。今天给大家分享下我们是如何让Flutter如何识别外接媒体设备,并且实现视频流渲染和拍照;从官方插件外界纹理platformView实践,都尝试了一遍,最后选择了webRtc,整个预研过程一波三折,学到了很多知识!



需求背景


需求是在win10和Android9的设备上支持外接摄像头,能够进行实时拍摄,做一个类似相机的应用。

从技术流程上来分析,我们需要识别出相机设备,拿到媒体流信息然后做渲染(渲染机制一般通过外接纹理Texture去实现),最后捕获帧进行拍照/录制。Flutter中,任何对象渲染后自然能拿到RanderObject,只要有RanderObject这个真实的渲染对象,我们就能进行照片的存储。

以上流程,理论上库已经帮我们做好,但是桌面端的生态,往往没那么简单~~~


一、官方Plugin


Android端使用camera,windows使用camera_windows。官方的库对于内置相机的支持做的很不错,直接引用后在手机和普通电脑上效果都很好;但是两个库都是明确不支持外接设备,见issus-1issus-2,优先级分别是P4、P5,显然官方认为这些问题优先级不高。
而纵观整个Flutter生态对USB外设的支持,并没有一个官方的库,pub上的基本也是参差不齐,大多只支持单一平台。


实现原理



  • Android端的camera插件,使用原生Camera2 Api,通过TextureRegistry创建纹理,然后Flutter用Texture进行绘制。



  1. 创建相机实例,返回textureId


// camera_android-0.9.8+3\lib\src\android_camera.dart
@override
Future<int> createCamera(
CameraDescription cameraDescription,
ResolutionPreset? resolutionPreset, {
bool enableAudio = false,
}) async {
try {
final Map<String, dynamic>? reply = await _channel
.invokeMapMethod<String, dynamic>('create', <String, dynamic>{
'cameraName': cameraDescription.name,
'resolutionPreset': resolutionPreset != null
? _serializeResolutionPreset(resolutionPreset)
: null,
'enableAudio': enableAudio,
});

return reply!['cameraId']! as int;
} on PlatformException catch (e) {
throw CameraException(e.code, e.message);
}
}


  1. 预览控件返回Flutter Texture Widget,与原生返回的纹理id形成绑定,从而接收纹理信息然后绘制


// camera_android-0.9.8+3\lib\src\android_camera.dart
@override
Widget buildPreview(int cameraId) {
return Texture(textureId: cameraId);
}


  1. Android端通过TextureRegistry创建createSurfaceTexture,把textureId返回到Dart层。


// camera_android-0.9.8+3\android\src\main\java\io\flutter\plugins\camera\MethodCallHandlerImpl.java
private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException {
String cameraName = call.argument("cameraName");
String preset = call.argument("resolutionPreset");
boolean enableAudio = call.argument("enableAudio");

TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture =
textureRegistry.createSurfaceTexture();
DartMessenger dartMessenger =
new DartMessenger(
messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper()));
CameraProperties cameraProperties =
new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity));
ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset);

camera =
new Camera(
activity,
flutterSurfaceTexture,
new CameraFeatureFactoryImpl(),
dartMessenger,
cameraProperties,
resolutionPreset,
enableAudio);

Map<String, Object> reply = new HashMap<>();
reply.put("cameraId", flutterSurfaceTexture.id());
result.success(reply);
}

值得一提的是Flutter3.0后,官方的原生绘制方式已经抛弃了VirtualDisplay,拥抱TextureLayer,性能上已经优化了不少,让Flutter的音视频渲染能力提升了不少。 但问题就是在instantiateCamera之前,官方在Camera2的实现上,没有对外界设备进行处理,从而搜索不到对应的外接相机。



  • Windows端的实现完全一样,都是通过Texture做渲染,原因也是获取相机列表的时候没有做外接设备的实现,这里不在赘述。


解决方案


基于多端的camera接口做处理,把外设设备的逻辑加上,应该就可以了。 在Texture纹理这块官方的实现是没有问题的。
当然这个思路我目前只停留在理论层面,并未真正去实现,原因如下:



  1. 两个库都是设计原生知识,我们维护成本会很大;

  2. 官方的库维护的很频繁,后面更多优化还得看官方,很有可能哪个版本就得全部推翻重新来一遍。


二、PlatformView


明确一个观点,这个方案不可落地。预研这个方案的原因是我们本身已经有原生的代码封装,基于CameraX的Android实现,我只需要在Plugin上注册下视图即可,具体实现代码如下:



  1. 注册视图


override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val key: String = "camera"

channel = MethodChannel(flutterPluginBinding.binaryMessenger, "camera_plugin")
channel.setMethodCallHandler(this)
CameraInfoManager.getCameraInfoList().forEach {
Log.d(TAG, "onAttachedToEngine: $it")
}

// 注册视图
flutterPluginBinding.platformViewRegistry.registerViewFactory(
key,
CameraFactory(flutterPluginBinding.binaryMessenger)
)
}


  1. 视图工厂


class CameraFactory(private val messenger: BinaryMessenger) :
PlatformViewFactory(StandardMessageCodec.INSTANCE) {

override fun create(context: Context?, id: Int, args: Any?): PlatformView {
return CameraPlatformView(context!!)
}

}


  1. 引入CameraX视图


class CameraPreView(context: Context, attrs: AttributeSet?) :
LinearLayout(context, attrs), LifecycleOwner {

private var camera: PreviewView

private val mLifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)

init {
val view: View = LayoutInflater.from(context).inflate(R.layout.layout_camera_preview, this)
camera = view.findViewById(R.id.camera_preview_view)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)

// 这里是封装好的CameraX预览视图
CameraXPreview
.bindLifecycle(this)
.setPreviewView(camera)
.setCameraId(0)
.startPreview(context)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}

override fun getLifecycle(): Lifecycle = mLifecycleRegistry
}

问题显而易见,Flutter引擎启动白屏300ms,视图同步产生延时,内存平均新增20M+,而且视图生命周期没法同步,都是致命问题。

基于上面的实践,Windows上我们没有再做尝试了,Fail。


三、webRtc


上面两种方案都以失败告终后,大佬提到了webRtc,从基础协议出发,往往能解决核心问题。于是flutter_webrtc上场,WebRTC提供音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:windows,linux,mac,android,已经被纳入被纳入W3C推荐标准。webRtc开发文档



  • 引用flutter_webrtc这个库,其渲染原理依旧是外接纹理,使用方法查看官方的example实例即可;

  • 重点在实现拍照功能,拍照无非就是进行帧捕获,Android已经实现:


  final videoTrack = localStream!
.getVideoTracks()
.firstWhere((track) => track.kind == 'video');
final frame = await videoTrack.captureFrame();

// 使用image.memory即可渲染
frameList = frame.asUint8List();


  • 而windows端很遗憾,还没有实现拍照功能,见issus;于是我想到了曲线救国,通过截取屏幕来保存图像由于是使用Texture渲染,通常的RenderRepaintBoundary+GlobalKey是没办法拿到RanderObject的!
    幸好插件提供了截取屏幕的方式,也算完成曲线救国了。


try {
var sources = await desktopCapturer.getSources(types: [SourceType.Window]);
DesktopCapturerSource capture =
sources.firstWhere((element) => element.name == 'my_camera');

// 使用image.memory即可渲染
frameList = capture.thumbnail;
return;
} catch (e) {
print(e.toString());
}

写在最后


到此,坎坷的外接相机预研之路告一段落。但是性能比起原生,真的差了一截,这让我们意识到,在官方不支持外接设备之前,针对此类需求,还是少用Flutter来实现。

Flutter桌面应用虽然发布了Stable版本,但说句实话生态确实比移动端差了不少,这意味着我们需要共同建设这个生态,但是趋势起来了,我们也愿意社区共建!

另外插个题外话,关于上面windows截取屏幕的需求,其实是有issus未关闭的,7月1号下午刚参与了issue的讨论,傍晚作者就拉了pull request,并且更了一版,解了燃眉之急啊!!!

怎么说呢,开源万岁,Respect!


image.png


image.png


作者:Karl_wei
链接:https://juejin.cn/post/7115674087682375717
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS block与__block、weak、__weak、__strong

iOS

首先需要知道:

block,本质是OC对象,对象的内容,是代码块。
封装了函数调用以及函数调用环境。

block也有自己的isa指针,依据block的类别不同,分别指向
__NSGlobalBlock __ ( _NSConcreteGlobalBlock )
__NSStackBlock __ ( _NSConcreteStackBlock )
__NSMallocBlock __ ( _NSConcreteMallocBlock )
需要注意是,ARC下只存在__NSGlobalBlock和__NSMallocBlock。
通常作为参数时,才可能是栈区block,但是由于ARC的copy作用,会将栈区block拷贝到堆上。
通常不管作为属性、参数、局部变量的block,都是__NSGlobalBlock,即使block内部出现了常量、静态变量、全局变量,也是__NSGlobalBlock,
除非block内部出现其他变量,auto变量或者对象属性变量等,就是__NSMallocBlock

为什么block要被拷贝到堆区,变成__NSMallocBlock,可以看如下链接解释:Ios开发-block为什么要用copy修饰

对于基础数据类型,是值传递,修改变量的值,修改的是a所指向的内存空间的值,不会改变a指向的地址。

对于指针(对象)数据类型,修改变量的值,是修改指针变量所指向的对象内存空间的地址,不会改变指针变量本身的地址
简单来说,基础数据类型,只需要考虑值的地址,而指针类型,则需要考虑有指针变量的地址和指针变量指向的对象的地址

以变量a为例

1、基础数据类型,都是指值的地址

1.1无__block修饰,

a=12,地址为A
block内部,a地址变B,不能修改a的值
block外部,a的地址依旧是A,可以修改a的值,与block内部的a互不影响
内外a的地址不一致

1.2有__block修饰

a=12,地址为A
block内部,地址变为B,可以修改a的值,修改后a的地址依旧是B
block外部,地址保持为B,可以修改a的值,修改后a的地址依旧是B

2、指针数据类型

2.1无__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,不能修改a指向的对象地址
block外部,a指针变量的地址为A,指向的对象地址为B,可以修改a指向的对象地址,
block外部修改后,
外部a指针变量的地址依旧是A,指向的对象地址变为D
内部a指针变量的地址依旧是C,指向的对象地址依旧是B

2.1有__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block外部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block内外,或者另一个block中,无论哪里修改,a指针变量地址都保持为C,指向的对象地址保持为修改后的一致

block内修改变量的实质(有__block修饰):

block内部能够修改的值,必须都是存放在堆区的。
1、基础数据类型,__block修饰后,调用block时,会在堆区开辟新的值的存储空间,
指针数据类型,__block修饰后,调用block时,会在堆区开辟新的指针变量地址的存储空间

2、并且无论是基础数据类型还是指针类型,block内和使用block之后,变量的地址所有地址(包括基础数据类型的值的地址,指针类型的指针变量地址,指针指向的对象的地址),都是保持一致的
当然,只有block进行了真实的调用,才会在调用后发生这些地址的变化

另外需要注意的是,如果对一个已存在的对象(变量a),进行__block声明另一个变量b去指向它,
a的指针变量地址为A,b的指针变量会是B,而不是A,
原因很简单,不管有没__block修饰,不同变量名指向即使指向同一个对象,他们的指针变量地址都是不同的。

__weak,__strong

两者本身也都会增加引用计数。
区别在于,__strong声明,会在作用域区间范围增加引用计数1,超过其作用域然后引用计数-1
而__weak声明的变量,只会在其使用的时候(这里使用的时候,指的是一句代码里最终并行使用的次数),临时生成一个__strong引用,引用+次数,一旦使用使用完毕,马上-次数,而不是超出其作用域再-次数

    NSObject *obj = [NSObject new];
NSLog(@"声明时obj:%p, %@, 引用计数:%ld",&obj, obj, CFGetRetainCount((__bridge CFTypeRef)(obj)));
__weak NSObject *weakObj = obj;
NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));
NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

声明时obj:0x16daa3968, , 引用计数:1
声明时weakObj:0x16daa3960, ,, , 引用计数:5
声明后weakObj引用计数:2

这个5,是因为obj本来计数是1,

    NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句代码打印5,是因为除去&weakObj(&这个不是使用weakObj指向的对象,而只是取weakObj的指针变量地址,所以不会引起计数+1),另外还使用了4次weakObj,导致引用计数+4

   NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句打印2,说明上一句使用完毕后,weakObj引用增加的次数会马上清楚,重新变回1,而这句使用了一次weakObj,加上obj的一次引用,就是2了

__weak 与 weak

通常,__weak是单独为某个对象,添加一条弱引用变量的。
weak则是property属性里修饰符。

LGTestBlockObj *testObj = [LGTestBlockObj new];
self.prpertyObj = testObj;
__weak LGTestBlockObj *weakTestObj = testObj;
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), self.prpertyObj,self.prpertyObj,self.prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(self.prpertyObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), _prpertyObj,_prpertyObj,_prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"prpertyObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"weakTestObj:%p, %@,%@, %@, 引用计数:%ld",&weakTestObj, weakTestObj,weakTestObj,weakTestObj, CFGetRetainCount((__bridge CFTypeRef)(weakTestObj)));

prpertyObj:0x1088017b0, ,, , 引用计数:2
prpertyObj:, 引用计数:2
testObj:, 引用计数:2
weakTestObj:0x16b387958, ,, , 引用计数:6

待补充...

Block常见疑问收录

1、block循环引用

通常,block作为属性,并且block内部直接引用了self,就会出现循环引用,这时就需要__weak来打破循环。

2、__weak为什么能打破循环引用?

一个变量一旦被__weak声明后,这个变量本身就是一个弱引用,只有在使用的那行代码里,才会临时增加引用结束,一旦那句代码执行完毕,引用计数马上-1,所以看起来的效果是,不会增加引用计数,block中也就不会真正持有这个变量了

3、为什么有时候又需要使用__strong来修饰__weak声明的变量?

在block中使用__weak声明的变量,由于block没有对该变量的强引用,block执行的过程中,一旦对象被销毁,该变量就是nil了,会导致block无法继续正常向后执行。
使用__strong,会使得block作用区间,保存一份对该对象的强引用,引用计数+1,一旦block执行完毕,__strong变量就会销毁,引用计数-1
比如block中,代码执行分7步,在执行第二步时,weak变量销毁了,而第五步要用到weak变量。
而在block第一步,可先判断weak变量是否存在,如果存在,加一个__strong引用,这样block执行过程中,就始终存在对weak变量的强引用了,直到block执行完毕

4、看以下代码,obj对象最后打印的引用计数是多少,为什么?

    NSObject *obj = [NSObject new];
void (^testBlock)(void) = ^{
NSLog(@"%@",obj);
};
NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

最后的打印的是3
作为一个局部变量的block,由于引用了外部变量(非静态、常量、全局),定义的时候其实是栈区block,但由于ARC机制,使其拷贝到堆上,变成堆block,所以整个函数执行的过程中,实际上该block,存在两份,一个栈区,一个堆区,这就是使得obj引用计数+2了,加上创建obj的引用,就是3了

5、为什么栈区block要copy到堆上

block:我们称代码块,他类似一个方法。而每一个方法都是在被调用的时候从硬盘到内存,然后去执行,执行完就消失,所以,方法的内存不需要我们管理,也就是说,方法是在内存的栈区。所以,block不像OC中的类对象(在堆区),他也是在栈区的。如果我们使用block作为一个对象的属性,我们会使用关键字copy修饰他,因为他在栈区,我们没办法控制他的消亡,当我们用copy修饰的时候,系统会把该 block的实现拷贝一份到堆区,这样我们对应的属性,就拥有的该block的所有权。就可以保证block代码块不会提前消亡。

转载自:https://cloud.tencent.com/developer/article/1894410

iOS皮肤适配

iOS
1、皮肤颜色资源和图片路径配置皮肤配置文件light.json 配置示例dark.json 配置示例2、设置全局的colorKey 来对应颜色路径 imageKey 来对应图片路径,利于维护颜色key配置图片key配置1、获取皮肤资源协议方法2、皮肤使用3、...
继续阅读 »
皮肤配置文件创建

1、皮肤颜色资源和图片路径配置


皮肤配置文件

如图所示,创建 light.json 和 dark.json ( light 和 dark 配置路径key 一样,对应的value 不同)

light.json 配置示例

{
"statusBarStyle": "black",
"colors":{
"mainFunction":"#E92424",
"gradientStockUp":[
"#0FFFFFFF",
"#0FE92424"
]
},
"images": {
"selfStock_info_icon": "appres/skinImage/light/selfStock_info_icon.png",
"selfStock_money_icon": "appres/skinImage/light/selfStock_money_icon.png",
}

// appres/skinImage/light/selfStock_info_icon.png 对应的图片文件夹路径
}

dark.json 配置示例

{
"statusBarStyle": "red",
"colors":{
"mainFunction":"#BC935C",
"gradientStockUp":[
"#26171717",
"#26E92424"
]
},
"images": {
"selfStock_info_icon": "appres/skinImage/dark/selfStock_info_icon.png",
"selfStock_money_icon": "appres/skinImage/dark/selfStock_money_icon.png",
}
}

2、设置全局的colorKey 来对应颜色路径 imageKey 来对应图片路径,利于维护


颜色key配置


图片key配置

皮肤使用

1、获取皮肤资源协议方法

// 获取皮肤资源协议方法
- (HJThemeDataModel *)getThemeModelWithName:(NSString *)name {
NSString *path = [NSString stringWithFormat:@"appres/theme/%@",name];
NSDictionary *colorDic = [self getDictFromJsonName:path][@"colors"];
NSDictionary *imageDic = [self getDictFromJsonName:path][@"images"];
HJThemeDataModel *model = [HJThemeDataModel new];
model.colorDic = colorDic;
model.imageDic = imageDic;
return model;
}

/// 设置默认主题(使用皮肤,至少有一个默认皮肤)
- (HJThemeDataModel *)getDefaultThemeModel {
return [self getThemeModelWithName:@"light"];
}

2、皮肤使用

// 导入头文件
#import "HJThemeManager.h"

// 设置当前皮肤 或切换 皮肤为 @"light"
[[HJThemeManager sharedInstance] switchThemeWithName:@"light"];

// 设置当前view 的背景色
//1、适配皮肤
self.view.themeBackgroundColor = backgroundColorKey;
//2、不适配皮肤,必须带#号
self.view.themeBackgroundColor = @“#333333;
//3、适配皮肤,随皮肤变化
self.view.themeBackgroundColor = [HJThemeManager getThemeColor:backgroundColorKey];
//4、指定皮肤,不会随皮肤变化
self.view.themeBackgroundColor = [HJThemeManager getThemeColor:backgroundColorKey themeName:@"light"];

/**
* [HJThemeManager getThemeColor:backgroundColorKey];
* 实质上是
theme://
"backgroundColorKey"?
*
* [HJThemeManager getThemeColor:backgroundColorKey themeName:@"light"];
* 实质上是
theme://"backgroundColorKey"?themeName=light
*/

//所以可以直接写URL 例如:
self.view.themeBackgroundColor = theme://backgroundColorKey?themeName=light;

// 设置当前imageView 的image
//1、适配皮肤
imageView.themeImage = imageKey;
//2、适配皮肤,随皮肤变化
imageView.themeImage = [HJThemeManager getThemeImage:imageKey];
//3、指定皮肤,不会随皮肤变化
imageView.themeImage = [HJThemeManager getThemeImage:imageKey themeName:@"light"];

/**
* [HJThemeManager getThemeImage:imageKey];
* 实质上是
theme://
"imageKey"?
*
* [HJThemeManager getThemeImage:imageKey themeName:@"light"];
* 实质上是
theme://"imageKey"?themeName=light
*/


//完整写法,指定皮肤
imageView.themeImage = theme://"imageKey"?themeName=light;

// 兼容不适配皮肤写法
// imageNamed 加载图片
imageView.themeImage = bundle://"imageKey";
// sdwebimage 解析 http/https 加载图片
imageView.themeImage = http://imagePath;
// 使用serverManager getimage 的协议方法获取图片
imageView.themeImage = imagePath;

3、皮肤的实现原理
1、创建一个NSObject分类(category),然后关联一个字典属性(themes),用于进行缓存UI控件调用的颜色方法和参数或者是图片方法和参数。再关联属性的时候添加一个通知监听,用于切换皮肤时,发送通知,然后再次调用缓存的方法和参数,进行颜色和图片的更换。

2、创建UI控件的分类(category),然后每个分类都有themes字典,然后设置新的方法来设置颜色或图片。在该方法内,需要做的处理有:

颜色举例说明:themeBackgroundColor = colorKey

a、在 themeBackgroundColor 的set方法中,判断是否是皮肤设置,皮肤的设置都是带有 theme:// 的字符串。这个(theme://)字符串是约定的。
b、皮肤适配模式,即带有 theme:// 字符串,就会用 themes 字典保存 系统的方法setBackgroundColor: 方法和参数colorKeythemeName,当切换皮肤时,再次调用 setBackgroundColor: 方法和参数colorKeythemeName
c、@"#333333", 直接是色值方法的 不需要 themes 字典保存,只需要直接调用系统方法 setBackgroundColor:[UIColor colorFromHexString:@"#333333"];

图片举例说明:imageView.themeImage = imageKey

a、在 themeImage 的set方法中,判断是否是皮肤设置,皮肤的设置都是带有 theme:// 的字符串。这个(theme://)字符串是约定的。
b、皮肤适配模式,即带有 theme:// 字符串,就会用 themes 字典保存 系统的方法setImage: 方法和参数imageKeythemeName,当切换皮肤时,再次调用 setImage: 方法和参数imageKeythemeName
c、bundle://, 直接是调用系统方法setImage:[UIImage imageNamed:@"imageNamed"] 进行赋值,不需要进行 themes 字典保存处理;
d、http:// 或 https:// , 采用SD框架加载图片,不需要进行 themes 字典保存处理;

3、主要的UI控件的分类

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>

@interface UIView (CMSThemeView)

/// 设置皮肤文件名称 默认为空值,取当前皮肤
/// 可以设置指定皮肤 例如: @"Dark" / @"Light" ;
/// defaultThemeKey 为默认皮肤
/// 如何设置 Color 或 Image 有 themeName ,优先使用 themeName
指定皮肤
@property (nonatomic, copy) NSString *themeStyle;
@property (nonatomic, copy) NSString *themeBackgroundColor;
@property (nonatomic, copy) NSString *themeTintColor;
/// 根据路径获取color 并缓存方法和参数 ()
- (void)setThemeColorWithIvarName:(NSString *)ivarName colorPath:(NSString *)path;
@end

@interface UILabel (ThemeLabel)
@property (nonatomic, copy) NSString *themeTextColor;
@property (nonatomic, copy) NSString *themeHighlightedTextColor;
@property (nonatomic, copy) NSString *themeShadowColor;
/// 主要是颜色
@property (nonatomic, strong) NSAttributedString *themeAttributedText;
@end

@interface UITextField (ThemeTextField)
@property (nonatomic, copy) NSString *themeTextColor;
@end

@interface UIImageView (CMSThemeImageView)
@property (nonatomic, copy) NSString *themeImage;

// 带有 UIImageRenderingMode 的处理,image 修改渲染色的,即tintColor
- (void)themeSetImageKey:(NSString *)imageKey
renderingMode:(UIImageRenderingMode)mode;

@end

@interface UIButton (ThemeButton)

- (void)themeSetImage:(NSString *)path forState:(UIControlState)state;
- (void)themeSetImage:(NSString *)path forState:(UIControlState)state renderingMode:(UIImageRenderingMode)mode;
- (void)themeSetBackgroundImage:(NSString *)path forState:(UIControlState)state;
- (void)themeSetTitleColor:(NSString *)path forState:(UIControlState)state;
@end

@interface UITableView (ThemeTableView)
@property (nonatomic, copy) NSString *themeSeparatorColor;
@end

@interface CALayer (ThemeLayer)
/// 设置皮肤文件名称 默认为空值,取当前皮肤 eg: @"Dark" / @"Light" ; defaultThemeKey 为默认皮肤
@property (nonatomic, copy) NSString *themeStyle;
@property (nonatomic, copy) NSString *themeBackgroundColor;
@property (nonatomic, copy) NSString *themeBorderColor;
@property (nonatomic, copy) NSString *themeShadowColor;
/// 根据路径获取cgcolor 并缓存方法和参数 ()
- (void)setThemeCGColorWithIvarName:(NSString *)ivarName colorPath:(NSString *)path;
@end

以上是简单列举了几个,其他UIKIt 控件一样分类处理即可

皮肤颜色流程图



皮肤颜色流程图

皮肤图片流程图


皮肤图片流程图

存在的缺陷

1、不能全局统一处理,需要一处一处的设置,比较麻烦。
2、目前还不支持网络下载皮肤功能,需要其他位置处理下载解压过程。
3、XIB的使用还需要其他的处理,这个比较重要

转载自:https://cloud.tencent.com/developer/article/1894412

收起阅读 »

Python爬虫 | 一条高效的学习路径

数据是创造和决策的原材料,高质量的数据都价值不菲。而利用爬虫,我们可以获取大量的价值数据,经分析可以发挥巨大的价值,比如:豆瓣、知乎:爬取优质答案,筛选出各话题下热门内容,探索用户的舆论导向。淘宝、京东:抓取商品、评论及销量数据,对各种商品及用户的消费场景进行...
继续阅读 »

数据是创造和决策的原材料,高质量的数据都价值不菲。而利用爬虫,我们可以获取大量的价值数据,经分析可以发挥巨大的价值,比如:

豆瓣、知乎:爬取优质答案,筛选出各话题下热门内容,探索用户的舆论导向。

淘宝、京东:抓取商品、评论及销量数据,对各种商品及用户的消费场景进行分析。

搜房、链家:抓取房产买卖及租售信息,分析房价变化趋势、做不同区域的房价分析。

拉勾、智联:爬取各类职位信息,分析各行业人才需求情况及薪资水平。

雪球网:抓取雪球高回报用户的行为,对股票市场进行分析和预测。

爬虫是入门Python最好的方式,没有之一。Python有很多应用的方向,比如后台开发、web开发、科学计算等等,但爬虫对于初学者而言更友好,原理简单,几行代码就能实现基本的爬虫,学习的过程更加平滑,你能体会更大的成就感。

掌握基本的爬虫后,你再去学习Python数据分析、web开发甚至机器学习,都会更得心应手。因为这个过程中,Python基本语法、库的使用,以及如何查找文档你都非常熟悉了。

对于小白来说,爬虫可能是一件非常复杂、技术门槛很高的事情。比如有的人则认为先要掌握网页的知识,遂开始 HTMLCSS,结果入了前端的坑,瘁……

但掌握正确的方法,在短时间内做到能够爬取主流网站的数据,其实非常容易实现,但建议你从一开始就要有一个具体的目标。

在目标的驱动下,你的学习才会更加精准和高效。那些所有你认为必须的前置知识,都是可以在完成目标的过程中学到的。这里给你一条平滑的、零基础快速入门的学习路径。

- ❶ -

学习 Python 包并实现基本的爬虫过程

大部分爬虫都是按“发送请求——获得页面——解析页面——抽取并储存内容”这样的流程来进行,这其实也是模拟了我们使用浏览器获取网页信息的过程。

Python中爬虫相关的包很多:urllib、requests、bs4、scrapy、pyspider 等,建议从requests+Xpath 开始,requests 负责连接网站,返回网页,Xpath 用于解析网页,便于抽取数据。

如果你用过 BeautifulSoup,会发现 Xpath 要省事不少,一层一层检查元素代码的工作,全都省略了。这样下来基本套路都差不多,一般的静态网站根本不在话下,豆瓣、糗事百科、腾讯新闻等基本上都可以上手了

-❷-

掌握各种技巧,应对特殊网站的反爬措施

当然,爬虫过程中也会经历一些绝望啊,比如被网站封IP、比如各种奇怪的验证码、userAgent访问限制、各种动态加载等等。

遇到这些反爬虫的手段,当然还需要一些高级的技巧来应对,常规的比如访问频率控制、使用代理IP池、抓包、验证码的OCR处理等等

往往网站在高效开发和反爬虫之间会偏向前者,这也为爬虫提供了空间,掌握这些应对反爬虫的技巧,绝大部分的网站已经难不到你了。

-❸-

学习 scrapy,搭建工程化的爬虫

掌握前面的技术一般量级的数据和代码基本没有问题了,但是在遇到非常复杂的情况,可能仍然会力不从心,这个时候,强大的 scrapy 框架就非常有用了。

scrapy 是一个功能非常强大的爬虫框架,它不仅能便捷地构建request,还有强大的 selector 能够方便地解析 response,然而它最让人惊喜的还是它超高的性能,让你可以将爬虫工程化、模块化。

学会 scrapy,你可以自己去搭建一些爬虫框架,你就基本具备爬虫工程师的思维了。

- ❹ -

学习数据库基础,应对大规模数据存储

爬回来的数据量小的时候,你可以用文档的形式来存储,一旦数据量大了,这就有点行不通了。所以掌握一种数据库是必须的,学习目前比较主流的 MongoDB 就OK。

MongoDB 可以方便你去存储一些非结构化的数据,比如各种评论的文本,图片的链接等等。你也可以利用PyMongo,更方便地在Python中操作MongoDB。

因为这里要用到的数据库知识其实非常简单,主要是数据如何入库、如何进行提取,在需要的时候再学习就行。

-❺-

分布式爬虫,实现大规模并发采集

爬取基本数据已经不是问题了,你的瓶颈会集中到爬取海量数据的效率。这个时候,相信你会很自然地接触到一个很厉害的名字:分布式爬虫

分布式这个东西,听起来很恐怖,但其实就是利用多线程的原理让多个爬虫同时工作,需要你掌握 Scrapy + MongoDB + Redis 这三种工具

Scrapy 前面我们说过了,用于做基本的页面爬取,MongoDB 用于存储爬取的数据,Redis 则用来存储要爬取的网页队列,也就是任务队列。

所以有些东西看起来很吓人,但其实分解开来,也不过如此。当你能够写分布式的爬虫的时候,那么你可以去尝试打造一些基本的爬虫架构了,实现一些更加自动化的数据获取。

你看,这一条学习路径下来,你已然可以成为老司机了,非常的顺畅。所以在一开始的时候,尽量不要系统地去啃一些东西,找一个实际的项目(开始可以从豆瓣、小猪这种简单的入手),直接开始就好

在这里有一套非常系统的爬虫课程,除了为你提供一条清晰的学习路径,我们甄选了最实用的学习资源以及庞大的主流爬虫案例库。短时间的学习,你就能够很好地掌握 Python 爬虫,获取你想得到的数据,同时具备数据分析、机器学习的Python基础。

如果你希望在短时间内学会Python爬虫,少走弯路

- 高效的学习路径 -

一上来就讲理论、语法、编程语言是非常不合理的,我们会直接从具体的案例入手,通过实际的操作,学习具体的知识点。我们为你规划了一条系统的学习路径,让你不再面对零散的知识点。

说点具体的,比如我们会直接用 lxml+Xpath取代 BeautifulSoup 来进行网页解析,减少你不必要的检查网页元素的操作,多种工具都能完成的,我们会给你最简单的方法,这些看似细节,但可能是很多人都会踩的坑。

《Python爬虫:入门+进阶》大纲

第一章:Python 爬虫入门

1、什么是爬虫

网址构成和翻页机制

网页源码结构及网页请求过程

爬虫的应用及基本原理

2、初识Python爬虫

Python爬虫环境搭建

创建第一个爬虫:爬取百度首页

爬虫三步骤:获取数据、解析数据、保存数据

3、使用Requests爬取豆瓣短评

Requests的安装和基本用法

用Requests爬取豆瓣短评信息

一定要知道的爬虫协议

4、使用Xpath解析豆瓣短评

解析神器Xpath的安装及介绍

Xpath的使用:浏览器复制和手写

实战:用Xpath解析豆瓣短评信息

5、使用pandas保存豆瓣短评数据

pandas的基本用法介绍

pandas文件保存、数据处理

实战:使用pandas保存豆瓣短评数据

6、浏览器抓包及headers设置(案例一:爬取知乎)

爬虫的一般思路:抓取、解析、存储

浏览器抓包获取Ajax加载的数据

设置headers突破反爬虫限制

实战:爬取知乎用户数据

7、数据入库之MongoDB(案例二:爬取拉勾)

MongoDB及RoboMongo的安装和使用

设置等待时间和修改信息头

实战:爬取拉勾职位数据

将数据存储在MongoDB中

补充实战:爬取微博移动端数据

8、Selenium爬取动态网页(案例三:爬取淘宝)

动态网页爬取神器Selenium搭建与使用

分析淘宝商品页面动态信息

实战:用Selenium爬取淘宝网页信息

第二章:Python爬虫之Scrapy框架

1、爬虫工程化及Scrapy框架初窥

html、css、js、数据库、http协议、前后台联动

爬虫进阶的工作流程

Scrapy组件:引擎、调度器、下载中间件、项目管道等

常用的爬虫工具:各种数据库、抓包工具等

2、Scrapy安装及基本使用

Scrapy安装

Scrapy的基本方法和属性

开始第一个Scrapy项目

3、Scrapy选择器的用法

常用选择器:css、xpath、re、pyquery

css的使用方法

xpath的使用方法

re的使用方法

pyquery的使用方法

4、Scrapy的项目管道

Item Pipeline的介绍和作用

Item Pipeline的主要函数

实战举例:将数据写入文件

实战举例:在管道里过滤数据

5、Scrapy的中间件

下载中间件和蜘蛛中间件

下载中间件的三大函数

系统默认提供的中间件

6、Scrapy的Request和Response详解

Request对象基础参数和高级参数

Request对象方法

Response对象参数和方法

Response对象方法的综合利用详解

第三章:Python爬虫进阶操作

1、网络进阶之谷歌浏览器抓包分析

http请求详细分析

网络面板结构

过滤请求的关键字方法

复制、保存和清除网络信息

查看资源发起者和依赖关系

2、数据入库之去重与数据库

数据去重

数据入库MongoDB

第四章:分布式爬虫及实训项目

1、大规模并发采集——分布式爬虫的编写

分布式爬虫介绍

Scrapy分布式爬取原理

Scrapy-Redis的使用

Scrapy分布式部署详解


转载自: https://cloud.tencent.com/developer/article/1895399

收起阅读 »

什么是爬虫?怎么样玩爬虫

看到上面的那只蜘蛛没?别误会,今天要教你如何玩上面的蜘蛛。我们正式从0到1轻松学会Python爬虫.......知识碎片化学习难度学习特点爬虫的概念网络爬虫(又被称为网页蜘蛛、网页机器人)就是模拟客户端(主要是指浏览器)发送请求,接收请求响应,按照一定规则、自...
继续阅读 »

Python爬虫入门:什么是爬虫?

看到上面的那只蜘蛛没?别误会,今天要教你如何玩上面的蜘蛛。我们正式从0到1轻松学会Python爬虫.......

爬虫特点概要

  • 知识碎片化

爬虫方向的知识是十分碎片化的,因为我们写爬虫的时候会面对各种各样的网站,每个网站实现的技术都是相似的,但是大多数时候还是有差别的,这就要求我们对不同的网站使用不同的技术手段。爬虫并不像在学习web的时候要实现某一功能只要按照一定的套路就能做出来。

  • 学习难度

爬虫的入门相对而言还是要比web简单,但是在后期,爬虫的难度要大于web。难点在于爬虫工程师与运维人员进行对抗,可能你写一个网站的爬虫,结果该网站的运维人员加了反爬的措施,那么作为爬虫工程师就要解决这个反爬。

  • 学习特点

学习爬虫并不像学习web,学习web有一个完整的项目可以练手,因为爬虫的特点,也导致学习爬虫是以某网站为对象的,可以理解为一个技术点一个案例。

爬虫的概念

模拟浏览器,发送请求,获取响应

网络爬虫(又被称为网页蜘蛛、网页机器人)就是模拟客户端(主要是指浏览器)发送请求,接收请求响应,按照一定规则、自动抓取互联网信息的程序。

  • 原则上,只要是浏览器能做的事情,爬虫都能做
  • 爬虫也只能获取浏览器所展示出来的数据

在浏览器中输入百度网址,打开开发者工具,点击network,点击刷新,即可进行抓包。


了解爬虫概念

爬虫的作用

爬虫在互联网中的作用

  • 数据采集
  • 软件测试
  • 网站投票
  • 网络安全

爬虫的分类

根据被爬网闸的数量不同,可以分为:

  • 通用爬虫,如搜索引擎
  • 聚焦爬虫,如12306抢票,或者专门抓取某一网站的某一类数据

根据是否以获取数据为目的,可以分为:

  • 功能性爬虫,给你喜欢的明星,投票点赞
  • 数据增量式爬虫,比如招聘信息

根据URL地址和对应页面内容是否改变,数据增量爬虫可以分为:

  • 基于URL地址变化,内容变化的增量式爬虫
  • URL地址不变,内容变化的数据增量式爬虫

爬虫分类


了解爬虫分类


爬虫流程

1、获取一个URL

2、向URL发送请求,并获取响应(http协议)

3、如果从响应中提取URL,则继续发送请求获取响应

4、如果从响应中获取数据,则数据进行保存


掌握爬虫流程


http以及https的概念和区别

在爬虫流程的第二步,向URL发送请求,那么就要依赖于HTTP/HTTPS协议。

HTTPS比HTTP更安全,但是性能更低

  • HTTP:超文本传输协议,默认端口为80
    。超文本:是指超过文本,不限于文本,可以传输图片、视频、音频等数据
    。传输协议:是指使用公共约定的固定格式来传递转换成字符串的超文本内容
  • HTTPS:HTTP+SSL(安全套接字),即带有安全套接字层的超文本传输协议,默认端口443
    。SSL对传输内容(超文本,也就是请求头和响应体)进行加密
  • 可以打开一个浏览器访问URL,右键检查,点击network,选择一个URL,查看HTTP协议的形式。

掌握http及https的概念和默认端口


爬虫特别注意的请求头

请求头与响应头

http请求形式如上图所示,爬虫要特别关注以下几个请求头字段

  • Content-Type
  • Host
  • Connection
  • Upgrade-Insecure-Requests(升级为https请求)
  • User-Agent(用户代理)
  • Referer
  • Cookie(保持用户状态)
  • Authorization(认证信息)


例如,使用浏览器访问百度进行抓包

当我点击view source的时候,就会出现另外一种格式的请求头,这个是原始的版本,如果没有点击view source的请求头格式是经过浏览器优化的。

爬虫特别注意的响应头

  • set-cookie

cookie是基于服务端生成的,在客户端头信息中,在第一次把请求发送到服务端,服务端生成cookie,存放到客户端,下次发送请求时会带上cookie。

常见的响应状态码

  • 200:成功
  • 302:跳转,新的URL在响应中的Location头中给出
  • 303:浏览器对于post响应进行重定向至新的URL
  • 307:浏览器对于get响应进行重定向至新的URL
  • 403:资源不可用,服务器理解客户端的请求,但拒绝处理它(没有权限)
  • 404:找不到页面
  • 500:服务器内部错误
  • 503:服务器由于维护或者负载过重未能应答。在响应中可能会携带Retry-After响应头,有可能是因为爬虫频繁访问URL,使服务器忽视爬虫的请求,最终返回503状态码

所有的状态码都不可信,一切要以抓包得到的响应中获取的数据为准

network中抓包得到的源码才是判断依据。element中的源码是渲染之后的源码,不能作为判断标准。


了解常见的响应状态码


http请求的过程

1、浏览器在拿到域名对应的IP之后,先向地址栏中的URL发起请求,并获取响应。

2、在返回响应内容(HTML)中,会带有CSS、JS、图片等URL地址,以及Ajax代码,浏览器按照响应内容中的顺序依次发送其他请求,并获取响应。

3、浏览器每获取一个响应就对展示出的结果进行添加(加载),JS、CSS等内容会修改页面内容,JS也可以重新发送请求,获取响应。

4、从获取第一个响应并在浏览器中展示,直到最终获取全部响应,并在展示结果中添加内容或修改,这个过程叫做浏览器的渲染

注意

在爬虫中,爬虫只会请求URL地址,对应的拿到URL地址对应的响应(该响应可以是HTML、CSS 、JS或是是图片、视频等等)。

浏览器渲染出来的页面和爬虫请求抓取的页面很多时候是不一样的,原因是爬虫不具有渲染功能。

  • 浏览器最终展示的结果是由多次请求响应共同渲染的结果
  • 爬虫只对一个URL地址发起请求并得到响应

理解浏览器展示的结果可以是多次请求响应共同渲染的结果,而爬虫是一次请求对应一个响应。

转载自: https://cloud.tencent.com/developer/article/1895395

收起阅读 »

实时监控股市公告的Python爬虫

精力有限的我们,如何更加有效率地监控信息? 很多时候特别是交易时,我们需要想办法监控一些信息,比如股市的公告。如果现有的软件没有办法实现我们的需求,那么就要靠我们自己动手,才能丰衣足食。你在交易看盘时,如果有一个小窗口,平时默默的不声不响,但是如果有公告发布...
继续阅读 »

精力有限的我们,如何更加有效率地监控信息?

很多时候特别是交易时,我们需要想办法监控一些信息,比如股市的公告。如果现有的软件没有办法实现我们的需求,那么就要靠我们自己动手,才能丰衣足食。

你在交易看盘时,如果有一个小窗口,平时默默的不声不响,但是如果有公告发布,就会显示公告的信息:这是什么公告,然后给我们公告的链接。这样,既不会像弹窗那样用信息轰炸我们,又能够定制我们自己想要的内容,做到想看就看,想不看就不看,那就很方便了。

爬虫抓取的是东方财富上的上市公司公告,上市公司公告有些会在盘中公布。实时监控的原理,其实就是程序代替人工,定期地去刷新网页,然后用刷新前后得到的数据进行比对,如果一样,那么等待下一个周期继续刷新,如果不一样,那么就把增量信息提取出来,供我们查阅。

利用python爬虫实时监控公告信息四部曲

第一步,导入随机请求头和需要的包

我们使用json来解析获取的信息,使用什么方法解析数据取决于我们请求数据的返回形式,这里使用json最方便,我们就导入json包。

第二步,获取初始的公告数据

我们发现,每一个公告都有一个独有的文章号码:art_code,因此我们以这个号码作为新旧比较的基准,如果新页面的头一个公告的art_code和已有的一致,那么就进入下一个刷新周期,如果不一致,那么说明页面已经更新过了,我们提取最新的报告,同时更新这个art_code,用于下一次比对。

原始url的获取。获取之后,通过json解析其中的内容,得到art_code,覆盖写入在tmp.txt文件中,用于比对。

读取了tmp.txt文件中的art_code,跟页面解析的art_code比对。

第三步,获取公告标题和文章链接

通过json我们基本上已经能够解析出大部分的数据内容。

通过观察网站的公告链接的特点,我们发现主要的差别就是在art_code,因此通过网址链接的拼接,我们就能够得到公告的pdf链接。

第四步,运行我们的程序

程序运行的结果会打印到窗口当中,每当有新的公告发布,程序上就会出现一串新的信息。

总结

自此,我们通过程序把我们要的信息打印到了程序的运行窗口,同时,我们的程序也可以根据我们需求进行强化和扩充。首先,这些信息也可以非常方便的通过接口发送到邮箱、钉钉等平台,起到实时提醒的作用,其次,我们也可以从不同的地方抓取信息,完成所需信息的自定义整合,这些将在我们后续的文章中提到。

收起阅读 »

我对 React 实现原理的理解

React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:vdom为什么 react 和 vue...
继续阅读 »

React 是前端开发每天都用的前端框架,自然要深入掌握它的原理。我用 React 也挺久了,这篇文章就来总结一下我对 react 原理的理解。

react 和 vue 都是基于 vdom 的前端框架,我们先聊下 vdom:

vdom

为什么 react 和 vue 都要基于 vdom 呢?直接操作真实 dom 不行么?

考虑下这样的场景:

渲染就是用 dom api 对真实 dom 做增删改,如果已经渲染了一个 dom,后来要更新,那就要遍历它所有的属性,重新设置,比如 id、clasName、onclick 等。

而 dom 的属性是很多的:


有很多属性根本用不到,但在更新时却要跟着重新设置一遍。

能不能只对比我们关心的属性呢?

把这些单独摘出来用 JS 对象表示不就行了?

这就是为什么要有 vdom,是它的第一个好处。

而且有了 vdom 之后,就没有和 dom 强绑定了,可以渲染到别的平台,比如 native、canvas 等等。

这是 vdom 的第二个好处。

我们知道了 vdom 就是用 JS 对象表示最终渲染的 dom 的,比如:

{
   type'div',
   props: {
       id'aaa',
       className: ['bbb''ccc'],
       onClickfunction() {}
  },
   children: []
}

然后用渲染器把它渲染出来。

但是要让开发去写这样的 vdom 么?

那肯定不行,这样太麻烦了,大家熟悉的是 html 那种方式,所以我们要引入编译的手段。

dsl 的编译

dsl 是 domain specific language,领域特定语言的意思,html、css 都是 web 领域的 dsl。

直接写 vdom 太麻烦了,所以前端框架都会设计一套 dsl,然后编译成 render function,执行后产生 vdom。

vue 和 react 都是这样:


这套 dsl 怎么设计呢?

前端领域大家熟悉的描述 dom 的方式是 html,最好的方式自然是也设计成那样。

所以 vue 的 template,react 的 jsx 就都是这么设计的。

vue 的 template compiler 是自己实现的,而 react 的 jsx 的编译器是 babel 实现的,是两个团队合作的结果。

比如我们可以这样写:


编译成 render function 后再执行就是我们需要的 vdom。

接下来渲染器把它渲染出来就行了。

那渲染器怎么渲染 vdom 的呢?

渲染 vdom

渲染 vdom 也就是通过 dom api 增删改 dom。

比如一个 div,那就要 document.createElement 创建元素,然后 setAttribute 设置属性,addEventListener 设置事件监听器。

如果是文本,那就要 document.createTextNode 来创建。

所以说根据 vdom 类型的不同,写个 if else,分别做不同的处理就行了。

没错,不管 vue 还是 react,渲染器里这段 if else 是少不了的:

switch (vdom.tag) {
 case HostComponent:
   // 创建或更新 dom
 case HostText:
   // 创建或更新 dom
 case FunctionComponent
   // 创建或更新 dom
 case ClassComponent
   // 创建或更新 dom
}

react 里是通过 tag 来区分 vdom 类型的,比如 HostComponent 就是元素,HostText 就是文本,FunctionComponent、ClassComponent 就分别是函数组件和类组件。

那么问题来了,组件怎么渲染呢?

这就涉及到组件的原理了:

组件

我们的目标是通过 vdom 描述界面,在 react 里会使用 jsx。

这样的 jsx 有的时候是基于 state 来动态生成的。如何把 state 和 jsx 关联起来呢?

封装成 function、class 或者 option 对象的形式。然后在渲染的时候执行它们拿到 vdom 就行了。

这就是组件的实现原理:

switch (vdom.tag) {
 case FunctionComponent
      const childVdom = vdom.type(props);
      
      render(childVdom);
      //...
 case ClassComponent
    const instance = new vdom.type(props);
    const childVdom = instance.render();
    
    render(childVdom);
    //...
}

如果是函数组件,那就传入 props 执行它,拿到 vdom 之后再递归渲染。

如果是 class 组件,那就创建它的实例对象,调用 render 方法拿到 vdom,然后递归渲染。

所以,大家猜到 vue 的 option 对象的组件描述方式怎么渲染了么?

{
   data: {},
   props: {}
   render(h) {
       return h('div', {}, '');
  }
}

没错,就是执行下 render 方法就行:

const childVdom = option.render();

render(childVdom);

大家可能平时会写单文件组件 sfc 的形式,那个会有专门的编译器,把 template 编译成 render function,然后挂到 option 对象的 render 方法上:


所以组件本质上只是对产生 vdom 的逻辑的封装,函数的形式、option 对象的形式、class 的形式都可以。

就像 vue3 也有了函数组件一样,组件的形式并不重要。

基于 vdom 的前端框架渲染流程都差不多,vue 和 react 很多方面是一样的。但是管理状态的方式不一样,vue 有响应式,而 react 则是 setState 的 api 的方式。

真说起来,vue 和 react 最大的区别就是状态管理方式的区别,因为这个区别导致了后面架构演变方向的不同。

状态管理

react 是通过 setState 的 api 触发状态更新的,更新以后就重新渲染整个 vdom。

而 vue 是通过对状态做代理,get 的时候收集以来,然后修改状态的时候就可以触发对应组件的 render 了。

有的同学可能会问,为什么 react 不直接渲染对应组件呢?

想象一下这个场景:

父组件把它的 setState 函数传递给子组件,子组件调用了它。

这时候更新是子组件触发的,但是要渲染的就只有那个组件么?

明显不是,还有它的父组件。

同理,某个组件更新实际上可能触发任意位置的其他组件更新的。

所以必须重新渲染整个 vdom 才行。

那 vue 为啥可以做到精准的更新变化的组件呢?

因为响应式的代理呀,不管是子组件、父组件、还是其他位置的组件,只要用到了对应的状态,那就会被作为依赖收集起来,状态变化的时候就可以触发它们的 render,不管是组件是在哪里的。

这就是为什么 react 需要重新渲染整个 vdom,而 vue 不用。

这个问题也导致了后来两者架构上逐渐有了差异。

react 架构的演变

react15 的时候,和 vue 的渲染流程还是很像的,都是递归渲染 vdom,增删改 dom 就行。

但是因为状态管理方式的差异逐渐导致了架构的差异。

react 的 setState 会渲染整个 vdom,而一个应用的所有 vdom 可能是很庞大的,计算量就可能很大。

浏览器里 js 计算时间太长是会阻塞渲染的,会占用每一帧的动画、重绘重排的时间,这样动画就会卡顿。

作为一个有追求的前端框架,动画卡顿肯定是不行的。但是因为 setState 的方式只能渲染整个 vdom,所以计算量大是不可避免的。

那能不能把计算量拆分一下,每一帧计算一部分,不要阻塞动画的渲染呢?

顺着这个思路,react 就改造为了 fiber 架构。

fiber 架构

优化的目标是打断计算,分多次进行,但现在递归的渲染是不能打断的,有两个方面的原因导致的:

  • 渲染的时候直接就操作了 dom 了,这时候打断了,那已经更新到 dom 的那部分怎么办?

  • 现在是直接渲染的 vdom,而 vdom 里只有 children 的信息,如果打断了,怎么找到它的父节点呢?

第一个问题的解决还是容易想到的:

渲染的时候不要直接更新到 dom 了,只找到变化的部分,打个增删改的标记,创建好 dom,等全部计算完了一次性更新到 dom 就好了。

所以 react 把渲染流程分为了两部分: render 和 commit。

render 阶段会找到 vdom 中变化的部分,创建 dom,打上增删改的标记,这个叫做 reconcile,调和。

reconcile 是可以打断的,由 schedule 调度。

之后全部计算完了,就一次性更新到 dom,叫做 commit。

这样,react 就把之前的和 vue 很像的递归渲染,改造成了 render(reconcile + schdule) + commit 两个阶段的渲染。

从此以后,react 和 vue 架构上的差异才大了起来。

第二个问题,如何打断以后还能找到父节点、其他兄弟节点呢?

现有的 vdom 是不行的,需要再记录下 parent、silbing 的信息。所以 react 创造了 fiber 的数据结构。


除了 children 信息外,额外多了 sibling、return,分别记录着兄弟节点、父节点的信息。

这个数据结构也叫做 fiber。(fiber 既是一种数据结构,也代表 render + commit 的渲染流程)

react 会先把 vdom 转换成 fiber,再去进行 reconcile,这样就是可打断的了。

为什么这样就可以打断了呢?

因为现在不再是递归,而是循环了:

function workLoop() {
 while (wip) {
   performUnitOfWork();
}

 if (!wip && wipRoot) {
   commitRoot();
}
}

react 里有一个 workLoop 循环,每次循环做一个 fiber 的 reconcile,当前处理的 fiber 会放在 workInProgress 这个全局变量上。

当循环完了,也就是 wip 为空了,那就执行 commit 阶段,把 reconcile 的结果更新到 dom。

每个 fiber 的 reconcile 是根据类型来做的不同处理。当处理完了当前 fiber 节点,就把 wip 指向 sibling、return 来切到下个 fiber 节点。:

function performUnitOfWork() {
 const { tag } = wip;

 switch (tag) {
   case HostComponent:
     updateHostComponent(wip);
     break;

   case FunctionComponent:
     updateFunctionComponent(wip);
     break;

   case ClassComponent:
     updateClassComponent(wip);
     break;
   case Fragment:
     updateFragmentComponent(wip);
     break;
   case HostText:
     updateHostTextComponent(wip);
     break;
   default:
     break;
}

 if (wip.child) {
   wip = wip.child;
   return;
}

 let next = wip;

 while (next) {
   if (next.sibling) {
     wip = next.sibling;
     return;
  }
   next = next.return;
}

 wip = null;
}

函数组件和 class 组件的 reconcile 和之前讲的一样,就是调用 render 拿到 vdom,然后继续处理渲染出的 vdom:

function updateClassComponent(wip) {
 const { typeprops } = wip;
 const instance = new type(props);
 const children = instance.render();

 reconcileChildren(wipchildren);
}

function updateFunctionComponent(wip) {
 renderWithHooks(wip);

 const { typeprops } = wip;

 const children = type(props);
 reconcileChildren(wipchildren);
}

循环执行 reconcile,那每次处理之前判断一下是不是有更高优先级的任务,就能实现打断了。

所以我们在每次处理 fiber 节点的 reconcile 之前,都先调用下 shouldYield 方法:

function workLoop() {
while (wip && shouldYield()) {
performUnitOfWork();
}

if (!wip && wipRoot) {
commitRoot();
}
}

shouldYiled 方法就是判断待处理的任务队列有没有优先级更高的任务,有的话就先处理那边的 fiber,这边的先暂停一下。

这就是 fiber 架构的 reconcile 可以打断的原理。通过 fiber 的数据结构,加上循环处理前每次判断下是否打断来实现的。

聊完了 render 阶段(reconcile + schedule),接下来就进入 commit 阶段了。

前面说过,为了变为可打断的,reconcile 阶段并不会真正操作 dom,只会创建 dom 然后打个 effectTag 的增删改标记。

commit 阶段就根据标记来更新 dom 就可以了。

但是 commit 阶段要再遍历一次 fiber 来查找有 effectTag 的节点,更新 dom 么?

这样当然没问题,但没必要。完全可以在 reconcile 的时候把有 effectTag 的节点收集到一个队列里,然后 commit 阶段直接遍历这个队列就行了。

这个队列叫做 effectList。

react 会在 commit 阶段遍历 effectList,根据 effectTag 来增删改 dom。

dom 创建前后就是 useEffect、useLayoutEffect 还有一些函数组件的生命周期函数执行的时候。

useEffect 被设计成了在 dom 操作前异步调用,useLayoutEffect 是在 dom 操作后同步调用。

为什么这样呢?

因为都要操作 dom 了,这时候如果来了个 effect 同步执行,计算量很大,那不是把 fiber 架构带来的优势有毁了么?

所以 effect 是异步的,不会阻塞渲染。

而 useLayoutEffect,顾名思义是想在这个阶段拿到一些布局信息的,dom 操作完以后就可以了,而且都渲染完了,自然也就可以同步调用了。

实际上 react 把 commit 阶段也分成了 3 个小阶段。

before mutation、mutation、layout。

mutation 就是遍历 effectList 来更新 dom 的。

它的之前就是 before mutation,会异步调度 useEffect 的回调函数。

它之后就是 layout 阶段了,因为这个阶段已经可以拿到布局信息了,会同步调用 useLayoutEffect 的回调函数。而且这个阶段可以拿到新的 dom 节点,还会更新下 ref。

至此,我们对 react 的新架构,render、commit 两大阶段都干了什么就理清了。

总结

react 和 vue 都是基于 vdom 的前端框架,之所以用 vdom 是因为可以精准的对比关心的属性,而且还可以跨平台渲染。

但是开发不会直接写 vdom,而是通过 jsx 这种接近 html 语法的 DSL,编译产生 render function,执行后产生 vdom。

vdom 的渲染就是根据不同的类型来用不同的 dom api 来操作 dom。

渲染组件的时候,如果是函数组件,就执行它拿到 vdom。class 组件就创建实例然后调用 render 方法拿到 vdom。vue 的那种 option 对象的话,就调用 render 方法拿到 vdom。

组件本质上就是对一段 vdom 产生逻辑的封装,函数、class、option 对象甚至其他形式都可以。

react 和 vue 最大的区别在状态管理方式上,vue 是通过响应式,react 是通过 setState 的 api。我觉得这个是最大的区别,因为它导致了后面 react 架构的变更。

react 的 setState 的方式,导致它并不知道哪些组件变了,需要渲染整个 vdom 才行。但是这样计算量又会比较大,会阻塞渲染,导致动画卡顿。

所以 react 后来改造成了 fiber 架构,目标是可打断的计算。

为了这个目标,不能变对比变更新 dom 了,所以把渲染分为了 render 和 commit 两个阶段,render 阶段通过 schedule 调度来进行 reconcile,也就是找到变化的部分,创建 dom,打上增删改的 tag,等全部计算完之后,commit 阶段一次性更新到 dom。

打断之后要找到父节点、兄弟节点,所以 vdom 也被改造成了 fiber 的数据结构,有了 parent、sibling 的信息。

所以 fiber 既指这种链表的数据结构,又指这个 render、commit 的流程。

reconcile 阶段每次处理一个 fiber 节点,处理前会判断下 shouldYield,如果有更高优先级的任务,那就先执行别的。

commit 阶段不用再次遍历 fiber 树,为了优化,react 把有 effectTag 的 fiber 都放到了 effectList 队列中,遍历更新即可。

在dom 操作前,会异步调用 useEffect 的回调函数,异步是因为不能阻塞渲染。

在 dom 操作之后,会同步调用 useLayoutEffect 的回调函数,并且更新 ref。

所以,commit 阶段又分成了 before mutation、mutation、layout 这三个小阶段,就对应上面说的那三部分。

我觉得理解了 vdom、jsx、组件本质、fiber、render(reconcile + schedule) + commit(before mutation、mutation、layout)的渲染流程,就算是对 react 原理有一个比较深的理解了。


作者:zxg_神说要有光
来源:juejin.cn/post/7117051812540055588

收起阅读 »

Flutter中的异步

同步与异步程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用...
继续阅读 »

同步与异步

程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。

从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。

同步I/O 与 异步I/O的区别


为什么使用异步

用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:

  • I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用

  • 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步

  • 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应

计算机中异步的实现方式就是任务调度,也就是进程的切换

任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。

计算机系统分为用户空间内核空间,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程


在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程

每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换

Future

Future<T> 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error,使用代码实例:

Future<int> future = getFuture();
future.then((value) => handleValue(value))
    .catchError((error) => handleError(error))
.whenComplete(func);

future可以是三种状态:未完成的返回结果值返回异常

当一个返回future对象被调用时,会发生两件事:

  • 将函数操作入队列等待执行结果并返回一个未完成的Future对象

  • 函数操作完成时,Future对象变为完成并携带一个值或一个错误

首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue中事件,最后执行事件队列Event Queue中的事件,示例:

void main(){
 Future(() => print(10));
Future.microtask(() => print(9));
 print("main");
}
/// 打印结果为:
/// main
/// 9
/// 10

基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function

factory Future(FutureOr<T> computation()) {
   _Future<T> result = new _Future<T>();
   Timer.run(() {
     try {
       result._complete(computation());
    } catch (e, s) {
       _completeWithErrorCallback(result, e, s);
    }
  });
   return result;
}

Function有多种写法:

//简单操作,单步
Future(() => print(5));
//稍复杂,匿名函数
Future((){
 print(6);
});
//更多操作,方法名
Future(printSeven);

printSeven(){
 print(7);
}
 

Future.microtask

此工程方法创建的事件将发送到微任务队列Microtask Queue,具有相比事件队列Event Queue优先执行的特点

factory Future.microtask(FutureOr<T> computation()) {
   _Future<T> result = new _Future<T>();
//
   scheduleMicrotask(() {
     try {
       result._complete(computation());
    } catch (e, s) {
       _completeWithErrorCallback(result, e, s);
    }
  });
   return result;
}

Future.sync

返回一个立即执行传入参数的Future,可理解为同步调用

factory Future.sync(FutureOr<T> computation()) {
   try {
     var result = computation();
     if (result is Future<T>) {
       return result;
    } else {
       // TODO(40014): Remove cast when type promotion works.
       return new _Future<T>.value(result as dynamic);
    }
  } catch (error, stackTrace) {
     /// ...
  }
}
Future.microtask(() => print(9));
 Future(() => print(10));
 Future.sync(() => print(11));

/// 打印结果: 11、9、10

Future.value

创建一个将来包含value的future

factory Future.value([FutureOr<T>? value]) {
   return new _Future<T>.immediate(value == null ? value as T : value);
}

参数FutureOr含义为T value 和 Future value 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:

    Future.value(12).then((value) => print(value));
 Future.value(Future<int>((){
   return 13;
}));

这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前

Future.error

创建一个执行结果为error的future

factory Future.error(Object error, [StackTrace? stackTrace]) {
/// ...
return new _Future<T>.immediateError(error, stackTrace);
}

_Future.immediateError(var error, StackTrace stackTrace)
: _zone = Zone._current {
_asyncCompleteError(error, stackTrace);
}
Future.error(new Exception("err msg"))
.then((value) => print("err value: $value"))
.catchError((e) => print(e));

/// 执行结果为:Exception: err msg

Future.delayed

创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future

factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
/// ...
new Timer(duration, () {
if (computation == null) {
result._complete(null as T);
} else {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
}
});
return result;
}

Future.wait

等待多个Future并收集返回结果

static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
{bool eagerError = false, void cleanUp(T successValue)?}) {
/// ...
}

FutureBuilder结合使用:

child: FutureBuilder(
future: Future.wait([
firstFuture(),
secondFuture()
]),
builder: (context,snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
}
final first = snapshot.data[0];
final second = snapshot.data[1];
return Text("data $first $second");
},
),

Future.any

返回futures集合中第一个返回结果的值

static Future<T> any<T>(Iterable<Future<T>> futures) {
var completer = new Completer<T>.sync();
void onValue(T value) {
if (!completer.isCompleted) completer.complete(value);
}
void onError(Object error, StackTrace stack) {
if (!completer.isCompleted) completer.completeError(error, stack);
}
for (var future in futures) {
future.then(onValue, onError: onError);
}
return completer.future;
}

对上述例子来说,Future.any snapshot.data 将返回firstFuturesecondFuture中第一个返回结果的值

Future.forEach

为传入的每一个元素,顺序执行一个action

static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {
var iterator = elements.iterator;
return doWhile(() {
if (!iterator.moveNext()) return false;
var result = action(iterator.current);
if (result is Future) return result.then(_kTrue);
return true;
});
}

这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:

Future.forEach(["one","two","three"], (element) {
print(element);
});

Future.doWhile

执行一个操作直到返回false

Future.doWhile((){
for(var i=0;i<5;i++){
print("i => $i");
if(i >= 3){
return false;
}
}
return true;
});
/// 结果打印到 3

以上为Future中常用构造函数和方法

在Widget中使用Future

Flutter提供了配合Future显示的组件FutureBuilder,使用也很简单,伪代码如下:

child: FutureBuilder(
future: getFuture(),
builder: (context, snapshot){
if(!snapshot.hasData){
return CircularProgressIndicator();
} else if(snapshot.hasError){
return _ErrorWidget("Error: ${snapshot.error}");
} else {
return _ContentWidget("Result: ${snapshot.data}")
}
}
)

Async-await

使用

这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。

两条基本原则:

  • 定义一个异步方法,必须在方法体前声明 async

  • await关键字必须在async方法中使用

首先,在要执行耗时操作的方法体前增加async:

void main() async { ··· }

然后,根据方法的返回类型添加Future修饰

Future<void> main() async { ··· }

现在就可以使用await关键字来等待这个future执行完毕

print(await createOrderMessage());

例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:

var list = getCategoryList();
list.then((value) => value[0].getCategorySubList(value[0].id))
.then((subCategoryList){
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}).catchError((e) => (){
print(e);
});

现在来看下使用async/await,事情变得简单了多少

Future<void> main() async {
await getCourses().catchError((e){
print(e);
});
}
Future<void> getCourses() async {
var list = await getCategoryList();
var subCategoryList = await list[0].getCategorySubList(list[0].id);
var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
print(courseList);
}

可以看到这样更加直观

缺陷

async/await 非常方便,但是还是有一些缺点需要注意

因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。

这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求

Future<String> getBannerList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "banner list";
});
}

Future<String> getHomeTabList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "tab list";
});
}

Future<String> getHomeMsgList() async {
return await Future.delayed(Duration(seconds: 3),(){
return "msg list";
});
}

使用await编写很可能会写成这样,打印执行操作的时间

Future<void> main2() async {
var startTime = DateTime.now().second;
await getBannerList();
await getHomeTabList();
await getHomeMsgList();
var endTime = DateTime.now().second;
print(endTime - startTime); // 9
}

在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:

Future<void> main() async {
var startTime = DateTime.now().second;
var bannerList = getBannerList();
var homeTabList = getHomeTabList();
var homeMsgList = getHomeMsgList();

await bannerList;
await homeTabList;
await homeMsgList;
var endTime = DateTime.now().second;
print(endTime - startTime); // 3
}

将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。

原理

线程模型

当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程GPU线程IO线程。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做"Platform thread"


UI线程是所有的Dard代码运行的地方,例如framework和你的应用,除非你启动自己的isolates,否则Dart将永远不会运行在其他线程。平台线程是所有依赖插件的代码运行的地方。该线程也是native frameworks为其他任务提供服务的地方,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个Platform thread为其提供服务。跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟Android/iOS UI相关的操作都必须在主线程进行相类似。

Isolates是Dart中概念,本意是隔离,它的实现功能和thread类似,但是他们之间的实现又有着本质的区别,Isolote是独立的工作者,它们之间不共享内存,而是通过channel传递消息。Dart是单线程执行代码,Isolate提供了Dart应用可以更好的利用多核硬件的解决方案。

事件循环

单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)当Flutter项目程序触发如点击事件IO事件网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。

microtask queue中事件优先于event queue执行,当有任务发送到microtask队列时,会在当前event执行完成后,阻塞当前event queue转而去执行microtask queue中的事件,这样为Dart提供了任务插队的解决方案。

event queue的阻塞意味着app无法进行UI绘制,响应鼠标和I/O等事件,所以要谨慎使用,如下为流程图:


这两个任务队列中的任务切换在某些方面就相当于是协程调度机制

协程

协程是一种协作式的任务调度机制,区别于操作系统的抢占式任务调度机制,它是用户态下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs


对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些


协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用yield完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程

多线程执行任务模型如图:


线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。

协程是协同多任务的,这意味着协程提供并发性但不提供并行性,执行流模型图如下:


协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多

但是线程并不是一无是处,抢占式线程调度器事实上提供了准实时的体验。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……

总结

  • 同步与异步

  • Future提供了Flutter中异步代码链式编写方式

  • async-wait提供了异步代码的同步书写方式

  • Future的常用方法和FutureBuilder编写UI

  • Flutter中线程模型,四个线程

  • 单线程语言的事件驱动模型

  • 进程间切换和协程对比

参考

dart.cn/tutorials/l…

dart.cn/codelabs/as…

medium.com/dartlang/da…

juejin.cn/post/684490…

developer.mozilla.org/en-US/docs/…

http://www.zhihu.com/question/19…

http://www.zhihu.com/question/50…

en.wikipedia.org/wiki/Asynch…

eli.thegreenplace.net/2018/measur…


作者:QiShare
来源:juejin.cn/post/6987637272375984165

收起阅读 »

计算机会是下一个土木吗?

最近互联网裁员,有网友热议:2022年大规模裁员后,计算机专业会不会成为下一个土木?今年是我进入这个行业的第 10 年,算是抓住了这个行业的红利期,不用依靠家里也可以在一线城市买房、成家立业。但反观现在,“被毕业、逃离互联网、躺平算了...”却成了这个行业的主...
继续阅读 »

最近互联网裁员,有网友热议:2022年大规模裁员后,计算机专业会不会成为下一个土木?

今年是我进入这个行业的第 10 年,算是抓住了这个行业的红利期,不用依靠家里也可以在一线城市买房、成家立业。

但反观现在,“被毕业、逃离互联网、躺平算了...”却成了这个行业的主旋律,身边陆续有一些同事润到国企和外企去了,放低了对工资的预期,转而追求稳定和平衡。

互联网行业真的不行了吗?未来计算机专业会怎么样?真的会变成“土木专业”吗?

有个匿名网友写了这样一个回答,我挺认同的,想跟大家分享一下。


我学了10年计算机,现在还在找工作,我爸干了一辈子土木现在也在找工作。我觉得计算机不会成为下一个土木。

至于是不是49年入国军,我觉得楼主的眼光可以看长远一点,就是你这辈子到底想从事什么职业,或者干个什么事情。这篇相当于回忆录,供大家参考。

我2012年高考完了那年选专业还十分纠结,我记得那年最好的专业是金融,其次是建筑、土木、医生这些。生物、化学这些更次。

但这些分都还比计算机高点或者差不多,我爸就说让我自己想,学什么都可以。

我爸倒是觉得干土木也不错,但是我高考太差了,国内最好的那几个土木专业都报不进去。

一个18岁的无知少年,对专业选择能有什么想法呢。当时可以填5个专业,我前面几个都是填的金融管理这些热门专业,我最后填的计算机。

为什么填计算机呢,因为我喜欢打游戏,我觉得学计算机的应该可以去编游戏,那我也挺喜欢的。最后,金融那么火,所以前面的都没录上,调剂到计算机。

一、中国互联网的黄金十年

从11,12年到最近这一两年,我想称之为中国互联网的黄金十年。

如果进入社会就恰好在这黄金十年的开端,你可能根本意识不到有些精彩和癫狂只是短暂的。

我上本科那会儿,你以为大家是想着去哪工作吗?不是的,我们很多同学都在想着怎么创业,有技术追求的都在想着怎么造轮子,或者出国读博士搞学术,只有躺平的人才想着本科毕业找工作。

从大一开始就会有VC到学校劝人创业。那个年代滴滴和快滴还没有合并,美团也没有外卖,知乎过百赞都算非常热门的回答,短视频和直播更是连网速基础都还不具备。

你能做出一个app 雏形,哪怕之后怎么赚钱都不知道,你都能拿到投资。所以有同学真的就拿着种子轮出去创业了。

更有专业课老师原话:“你们这些搞计算机的,就是年轻的时候要想着几年赚个几百万几千万,把这辈子钱全赚够。” 可见当时的空气有多浮躁。

工作很难找吗?你能想象有公司给大二的学生全职吗,有些同学课不上,出去工作,只来考试和交作业。

我算是学的很渣的,因为我上大学一直在搞乱七八糟的东西,平均分不到80。好在我游戏打的多且好(多款游戏全区前10),面试反转链表都写的磕磕巴巴的,毕业依然进了某游戏大厂。

我们本科毕业那年单说就业的人,工作最好的去了google,微软,微信总部,阿里核心电商业务,去腾讯当产品经理,或者知名PE,VC。

对!你没有看错,互联网全是热钱,搞投资的也需要招懂点技术的啊。技术学的不好怎么办呢,转产品经理呀,或者游戏策划,再不济运维测试。

反正最后怎么都可以找到个工作。那时候工资最低的是去四大行总部做开发,但人家通常朝九晚五,有编制啊。

毕业我去游戏公司工作了1-2年,感觉做的都是换皮游戏,和我理想中的游戏开发差距太大了,就去美国继续读书了。

那时候工作一年就有猎头打你电话让你跳槽了。这有可能是我人生中的一个错误决定。出国之后发现工作越来越难找,中国和美国都难,面试越来越难。

头一年大家可能一般考个medium难度的题目,好家伙,第二年可能就直接变全考hard。好不容易找到工作,结果疫情一来offer直接取消了。

本来国内公司也面了一下,给了offer但是一看工资,和之前猎头说的工资多不了多少。合着回国的话硕士两年相当于白读。于是不服,转了博士继续读。

就算到了博士工作还是不好找,因为适合博士的岗位更少。面试机器学习要问,论文要聊,之前的实习经验会问,最后还要考leetcode hard,拜托我只是面个实习好吗。要知道8-6年前面试都只有easy,转专业刷50道题进美国大厂的人大有人在。

我还认识一个年纪比我大很多的博士,他说10年左右那会儿,你要是学校招聘会去了Amazon的摊位,交了简历,那就可以当场拿到一个offer。看来美国也有就业市场十分疯狂的年代。

归根结底,还是因为移动互联网的红利消失。没有那么多业务扩张,不会每年扩招。工资依然高,但进入的门槛也越来越高。有逐渐精英化的趋势。

也不奇怪,早几年不是就已经说中国互联网已经进入了下半场,对我们这些打工人来说意思就是变的更卷了呗。

二、聊一聊土木

接下来说一说土木。我爸工作那会儿是80年代末,大学毕业分配去了铁路某局。那时候土木也不算最热门的。

80年代末,90年代初最热的是下海经商。那会儿好多人从体制内出来经商。

我爸当时比较怂,我爷爷奶奶都是农民,家里没有本钱和关系。另一方面铁路系统工作也是个香饽饽,看病铁路医院,学校铁路小学,吃饭单位食堂,基本上不花钱。

我爸说小时候饿怕了,他觉得改革开放中国接下来几十年会修很多房子,很多路桥,所以他学土木完全是为了保证以后能长期吃上饭。

他这话倒是没说错,从90年代初到20年,中国一直在大修基建和住房。直到最近房地产房子才真的卖不太动,高铁和高速也修的足够多了。但我爸并没有干30年,他40多岁就退休了,在单位挂职。

中国土木最火的年代应该是2000年后到2015年左右,尤其是08年的四万亿,那时候很多包工头,建材行业的老板真的是在地上捡钱。但整个黄金年代差不多也就10年出头。

还是以我爸为例,我上小学那年,我爸嫌弃铁路系统给钱少,跳到上海一个建筑企业管工程,3年之后居然在上海全款买房了。

论买房速度,当年土木赚钱可能比现在互联网还多点。然后又干了几年在二线城市也买了两套房子,和一个商铺,我爸就辞职不干了。

因为他根本不喜欢土木,他喜欢炒股和种菜。结果呢炒股这么多年了也没赚几个钱。反倒是土木行情不好,挂靠那公司不给他挂靠了,但是他又还没有到领退休金的年纪,还要找个公司交社保。

所以他又开始找工作,可能也是因为太闲了。但是根本没有公司要他,现在年轻人都找不到工作,谁会要一个快60的老头呢?

但是土木就业市场也有癫狂的年代呀,我爸的原话是2010年左右,“什么阿猫阿狗都能来工作”,你只要是学土木,专科都可以。

现在土木凉了,简单来说是国内建设搞的差不多了。美国也有基建浪潮,但是那一波过后,就不再需要那么多人了,现在美国土木工程师也不算高收入群体。

所以我算是睥见了两个行业的黄金的年代。现代社会发展变化极快,一个行业要是能赚钱可以在3年之内迅速内卷。10年可能一整个周期就过了。

三、计算机到底怎么样?

所以你问要不要转计算机?我想说你喜欢编程吗?你喜欢用你的技术去解决问题吗?而不仅仅是因为赚钱,因为可能最好的日子也就那几年,但你需要工作40年(按法定退休年龄65)。

大多数人都不能洞见未来,当初12年我说自己录了计算机专业,很多长辈还说你那个估计和培训班出来的工资差不多。

谁又能知道,12年微信只用了几个月就成了国民APP,大家瞬间就进入了移动互联网时代呢?谁又能知道几年之后浪潮就已经过了呢?

所以现在这个时间节点我认为计算机只适合喜欢的人。

计算机我认为还是很好就业的,只是前几年找工作太容易。转专业,学两天java/python也没什么实际项目经历就可以找到高薪工作。

世界怎么可能一直如此美好?但是你要是有个正儿八经的计算机学位,上学期间认真做了些项目或者有实习,找工作应该不难。前提是不要往头部大厂算法岗位去卷。

至于裁员,你放心,你真要有技术,绝不可能裁员到你头上。

我原来做游戏,一个组十几个人。一个主程,一个引擎程序员(看着40岁以上,头发都半白了),带着我们这些刚毕业1-2年新人做开发。

整组平时有问题都是找他们两个。我相信就算裁员,也不会裁他们两个。就算公司倒闭,猎头第二天就会打来电话。

裁的会是光写简单业务逻辑,既不能做架构,也没有在某一块有足够技术深度的人。因为可替代性太高了。

实际上就算在美国,FB几年之内不能升资深工程师就会被裁,Amazon好像每年固定开除绩效末尾6%,有些公司更高到10%。

为什么这些公司敢这么做?因为这些人可替代性太高了,招个毕业生培训一下就和他们干一样的活。

四、计算机依然是最好的专业之一

但是学计算机,做软件工程师依然是接下来几十年最好的工作之一。

虽然上一个黄金时代已经过去,但是白银时代也香啊。说不定下一轮技术革命来到(元宇宙,通用AI,脑机接口,生物信息等等,太多了,都可以产生新的红利)大家又都进入了黄金时代。

现在这个时间节点卷cs的性价比依然较高。能和cs比工资的基本上也就投行,药厂,半导体,律师,医生可能都还差了一点。

药厂律师医生这些哪个不要博士毕业?进美国法学院医学院比其他专业都难,中国医学法律高考分也不低吧。半导体行业总体收入还是比软件工程师少点吧。而且人家也很难啊,做实验要扛几十斤的示波器,焊板子一坐就是一天,人家也要编程修bug。

至于投行,那必须要是名校毕业,各种社会活动,本科期间多个实习,或者家里有关系本来就不缺钱,甚至就我观察还要长得帅或者比较漂亮。

至于cs,你只要卷出一个本科/硕士学位,和面试刷题这两关。职业生涯初期拿的更多。

医生律师大后期会比较厉害,但是搞计算机也一样啊,走技术路线不说卷到60岁,卷到45+岁没问题的。主任工程师,资深科学家收入不比主任医师差。

再者,工作多年后,收入很大一部分是投资性收入。这些个体差异更大,而不是行业差异。

最后说回土木,土木怎么就不好了呢?去非洲一年也好几十万呢。人除了为了事业(钱)而奋斗,更重要的难道不是为了理想吗?

来源:知乎

收起阅读 »

“𠈌”计划2月优秀环友表彰及3月获选标准

环信“𠈌”计划(https://www.imgeek.org/article/825360062),以传递人人为我,我为人人的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。每月结束后由社区综合评选出当月积极帮助他人或参与环信社区建设的优秀开发者,优...
继续阅读 »

环信“𠈌”计划https://www.imgeek.org/article/825360062,以传递人人为我,我为人人的开发者互助精神为目标,将程序员自由开放和共享精神发扬光大。每月结束后由社区综合评选出当月积极帮助他人或参与环信社区建设的优秀开发者,优秀环友墙上留名并奖励环信大礼包,下面让我们康康2月的优秀环友们吧~

上帝之眼
王二蛋和他的张大花
孤狼☞小九

山雾sys

待补充。。。

恭喜以上开发者,请查收并回复站内私信,领取相应礼包。


“𠈌”计划3月礼包发放标准:
1、社区/社群活跃用户,积极参与社区活动 --2-5人
2、IMGeek发文章2篇原创或5篇转载 不限人数
3、反馈IM SDK bug并技术确认 不限人数

3月徽章👇


欢迎环信资深用户加入开发者交流群

加好友备注“社区”



往期回顾:

4月优秀环友:https://www.imgeek.org/article/825360198

5月优秀环友:https://www.imgeek.org/question/472968

2023年1月优秀环友

上帝之眼
王二蛋和他的张大花
孤狼☞小九
新新人类
一诗一画一菩提
林鹏
nikewei
willpein
LDFeng
emojiiii
yoyobiubiu
shiyl
yfliu
Fire_raiN

环信“𠈌”计划11月优秀环友如下:

上帝之眼
王二蛋和他的张大花
emojiiii
雷厉风行•琛
加长挡泥板
JERRY_694
孤狼☞小九
bilierha
HW1
fushixin

环信“𠈌”计划10月优秀环友如下:

xugj
马师傅
九漏鱼
暖光
hongg
冯小姐
conanma
Hc.
tjss
上帝之眼
月兑さん
山雾
Tsj
redme
孤狼☞小九

环信“𠈌”计划9月优秀环友如下:

山雾
孤狼☞小九
阿城627236
大锤子
微信用户_657
hongg
恋雨
mg13209643545
容颜难忘
bilierha
殷离恨
诺一啦
努力奋斗
lsp1007
马师傅
我要打中单
九漏鱼
微信用户_119
张尚斌
行走的商人
加菲猫
xugj
little28
Ju独一 -
上帝之眼
暗夜公爵
Empty_353
查南
诺一
ByteDance
万众一心摆烂到底
不开心就大概
依旧
微信用户_297
wilpein
微信用户_43
安和桥
魂断兴哥

环信“𠈌”计划8月优秀环友如下:

容颜难忘

山雾

孤狼☞小九

上帝之眼

阿城627236

柳天明

xugj

美国队长

Jiayun

Hc.

查南哥

查南

李全喜

yangjian

conanma

马师傅
王二蛋和他的张大花
雨淋湿了天空


环信“𠈌”计划7月优秀环友如下:

内容组:conanma、王二蛋和他的张大花、上帝之眼、环Sir、思密达、Hc.、little28、马师傅、sweetloser、柳天明
社群组:孤狼☞小九


环信“𠈌”计划6月优秀环友如下:

内容组:conanma、环Sir、上帝之眼、王二蛋和他的张大花、马师傅、Hc.、栈狮的咆哮

社群组:孤狼☞小九
收起阅读 »

Android组件化思路引文

前言本文并不是具体的实现文,主要讨论组件化的思路,分模块 和 模块通信。并实践一个简单的路由组件,完成模块间页面跳转和通信这两个功能,意图在于帮助刚接触组件化的同学提供一个简单不复杂的思路,对组件化有感性认识快速入门具体的实践文,可看这篇:Android 组件...
继续阅读 »

前言

本文并不是具体的实现文,主要讨论组件化的思路,分模块 和 模块通信。并实践一个简单的路由组件,完成模块间页面跳转和通信这两个功能,意图在于帮助刚接触组件化的同学提供一个简单不复杂的思路,对组件化有感性认识快速入门

具体的实践文,可看这篇:Android 组件化最佳实践 - 掘金 (juejin.cn)

什么是组件化

组件化本质上是一种组织代码的方式,只不过它的粒度更大,以module为单位。在未使用组件化之前,所有的代码都放在app模块中,在app模块内部通过分包划分业务代码和功能代码

如下图所示,

根据业务划分三个包:

  1. find 发现
  2. home 首页
  3. shop 商城

根据功能划分两个包:

  1. http 网络请求
  2. utils 工具类

Untitled.png 上述是未使用组件化情况,所有代码都在一个模块中编写,这样做并没有什么问题,但是当项目代码越来越多的 或者 有多人参数到项目中就有很大的问题了,比如:

  1. 代码都在写在一个模块中,不论怎么细致的分包,都免不了一个包下出现10多个类甚至更多的情况
  2. 分包的形式几乎对代码没有约束
  3. 开发人员多了,代码都写在一个模块中,每一位开发都拥有对文件读写的权利,容易出现代码覆盖冲突问题

总之组件化是为了应对代码多,人多 或者代码和人都多的情况而使用的一种组织代码的方式,一个模块中的代码分散到多个模块中。由于代码不在一个模块中,会出现到A模块无法引用到B模块中的类,引出通信问题。

所以组件化面对的主要问题主要有两个:

  1. 分模块
  2. 模块间通信

分模块

模块依据什么划分呢? 四个大字:单一职责 。老实说,写代码的时候 能够时刻牢记 单一职责,就能写出很不错的代码了。

拆分巨型单模块 与 拆分巨型单一类 的思想都是一致的。 其实它们出现的原因也一致,把不同职责的代码都放到一个类/模块中。所以拆分代码可以理解为代码归类

代码大致可以分为业务代码 和 功能代码,比如:

  1. 首页属于业务,网络请求属于功能
  2. 商城属于业务,数据库属于功能

所以当你的项目计划进行模块化的时候,只需要根据项目实际情况划分即可,没有什么硬性规定。

拆分代码有两个好处:

  1. 高复用性
    1. 体现功能模块上,比如:网络请求,轮播图,播放器,支付,分享等功能,任何一个业务都能可能会使用。实现为一个单独的模块,哪里使用哪里引入。
  2. 代码隔离
    1. 体现在业务模块,比如:A模块实现商城,B模块实现文章论坛,两者绝大部分代码没有任何关联,独立存在。
    2. 假设有一天项目不做文章论坛了,业务直接砍掉。那么删除B模块即可,A模块不受任何影响
    3. 但A,B模块都有可能有到分享功能,所以分享作为功能模块出现,不包含任何业务,只提供分享功能。

经过划分模块的代码结构如图

Untitled 1.png

三个业务模块:

  1. module_find 发现
  2. module_home 首页
  3. module_shop 商城

两个功能模块:

  1. library_network 网络请求
  2. library_utils 工具类

总之代码模块的拆分是单一职责的体现,大概可以分为业务模块和功能模块两种,功能模块的粒度更小可复用性更高,比如:轮播图,播放器任何位置都可能使用。

业务模块的粒度更大,可以引用多个功能模块解决问题,大多数代码都是依据业务逻辑编写,与功能模块相比 除非是同一个公司有相同业务否则复用性没那么高,

通信分析

上面主要讲了拆分模块的思路,现在聊聊模块间通信。特意设计模块间通信方案,主要是用于业务模块间通信。

业务模块和功能模块之间是单向通信,业务模块直接引用功能模块,调用功能模块暴露的方法即可。

但业务模块不同,业务模块之间存在互相通信的情况,核心情况有两种:

  1. 页面跳转
    1. A模块跳转B模块的页面,B模块跳转A模块的页面
  2. 数据通信
    1. A模块获取B模块的数据,比如调用B模块的网络请求。
    2. 可能会有点疑问,直接在A模块写要调用的接口不就好了,为什么要费劲巴拉的进行模块间通信,可以是可以。组件化就是为了隔离,解耦,复用。如果A模块直接实现了要用的网络请求,还要组件化干嘛呢,出现类似情况都这么干,项目内就会出现很多重复代码,除了图方便 没有别好处

单一模块开发时所有的类都能直接访问,上述的问题简直不是问题,从MainActivity 跳转到 TestActivity ,可以直接获取TestActivity的class对象完成跳转

val intent = Intent(this@MainActivity,TestActivity::class.java)
startActivity(intent)

但是分开多模块就是问题了,MainActivity 和 TestActivity 分别在A,B两个模块中,两个业务模块之间没有直接引用代码隔离,所以不能直接调用到想使用的类。

这种情况就需要一个中间人,帮助A,B模块通信。

(需求简单,实现简单)中间人好像邮局,两人住在同一个村甚至对门,想要唠嗑,送点东西,因为距离近走着就去了。如果两人相隔千里不能见面,想要唠嗑需要写信,标记地址交给邮局,让邮局转发。

(需求复杂,实现复杂)信件好保存一般不会损坏,运送比较方便。如果想要快点到,加钱用更快的运送工具。 如果想要送一块家乡的红烧肉,为了保鲜原汁原味,可能要加更多的钱用飞机+各种保险措施送过去

模块间通信也是类似,A,B模块通过中间人,也就是路由组件通信。页面跳转是最简单的通信需求实现简单,如果想要访问数据,获取对象应用等更复杂的需求,可能需要更加复杂的设计和其他技术手段才实现目标。

但总之A,B模块代码隔离之后不会无缘无故就实现了通信,一定会存在路由角色帮助A,B模块通信。区别在于路由是否强大,支持多少功能。

Untitled 2.png

粗糙的路由实现

页面跳转

实现路由组件最基本的功能页面跳转,讨论具体技术方案之前,先理清思路。

Android原生跳转页面只有一种办法 startActivity(intent(context,class)) ,调用startActivity方法有三要素

  1. context 提供的 startActivity方法
  2. 构造intent 需要 context
  3. 构造intent 需要 目标类的class对象

世面上所有的路由组件封装跳转页面功能,就算他封装出花来,也是基于AndroidSDK,无法脱离原生提供的方法。

所以我们现在需要想办法调用完整的startActivity(intent(context,class))

关键点在于,由于代码隔离,我们无法直接获取目标activity的class,直白点说无法 直接**“.”**出class。那么怎么可以在代码隔离的情况下拿到目标类的class呢

有个小技巧,先要说明一个事,模块A,模块B仅仅在编码的时候处于代码隔离的状态,但是打包之后它们还是一个应用,代码在一个虚拟机中。所以可以使用 Class.forName(包名+类名) 运行时获取class对象,完成跳转

val clazz = Class.forName("com.xxx.TestActivity")
val intent = Intent(this,clazz);
startActivity(intent)

这种方式可以帮助我们实现页面跳转的逻辑,但是非常粗糙,总不能需要模块间页面跳转,就硬编码包名+类名 获取class,太麻烦了,太容易出错了,代码散落在程序各处。

但是这种粗糙的方式也为我们提供了一点思想火花

如果我们能通过一种方式收集到 有模块间跳转需求的页面class对象 或者 包名+类名,在需要跳转的时候取出不就可以了么。

大概步骤:

  1. 创建路由组件
  2. 模块向路由注册页面信息
  3. 从路由取出页面信息实现跳转

创建路由组件,只有一个Route类

object Route {

private val routeMap = ArrayMap<String, Class<*>>()

fun register(path: String, clazz: Class<*>) {
routeMap[path] = clazz
}

fun navigation(context: Context, path: String) {
val clazz = routeMap[path]
val intent = Intent(context, clazz)
context.startActivity(intent)
}

}

其他组件在初始化时注册路由

Route.register("home/HomeActivity", HomeActivity::class.java)

模块间跳转页面

class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val button: Button = findViewById(R.id.button)
button.setOnClickListener {
Route.navigation(this, "home/HomeActivity")
}
}
}

把握住核心思想快速实现简单的模块间页面跳转还是非常简单的,在来回顾一下

  1. 代码隔离后 实现页面跳转最关键的问题是,无法直接获取目标类的Class引用
  2. 项目只是在编码期隔离,打包之后仍然是在一个虚拟机内,可以通过 Class.forName(包名+类名) 获取引用
  3. key-value的形式存储 需要模块间跳转类的Class信息,在需要的时候取出

看没什么用的效果图

QQ图片20220603100940.gif

上述代码肯定是可用的,但是实际运行并不是仅仅引入一个路由组件就可以了,还有很多项目配置细节,可以参考 开头推荐的文章

模块间通信

接口下沉方案,在Route组件中定义通讯接口,使所有模块都可以引用,具体实现只在某个业务模块中,在初始化时注册实现类,运行时通过反射动态创建实现类对象。

添加模块通信后,Route组件有两种逻辑要处理,页面跳转和模块通信。 保存的Class可能是Activity 或 某个接口实现类,业务操作也不同。

为了区分两种不同的业务,对Route组件进行一点小改造,新增 RouteEntity 保存数据,RouteType 路由类型用于区分,如下:

object Route {

private val routeMap = ArrayMap<String, RouteEntity>()

/**
* 注册信息
*/
fun register(route: RouteEntity) {
routeMap[route.path] = route
}

/**
* 页面导航
*/
fun navigation(context: Context, path: String) {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
val intent = Intent(context, routeEntity.clazz)
context.startActivity(intent)
}

/**
* 获取通信实例
*/
fun getService(path: String): Any {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
return routeEntity.clazz.newInstance()
}

}

/**
* 保存路由信息
* @param path 路径 用于查找class
* @param type 类型 区分 页面跳转 和 通信
* @param clazz 类信息
*/
data class RouteEntity(val path: String,@RouteType val type:Int,val clazz: Class<*>)

/**
* 路由类型
*/
@IntDef(RouteType.ACTIVITY, RouteType.SERVICE)
annotation class RouteType() {
companion object {
const val ACTIVITY = 0
const val SERVICE = 1
}
}

使用如下:

//在 Route组件中 定义接口

interface IShopService {
fun getPrice(): Int
}

//业务模块中实现接口
class ShopServiceImpl :IShopService {
override fun getPrice(): Int {
return 12
}
}

//模块初始化时注册
override fun create(context: Context) {
Route.register(RouteEntity("shop/ShopActivity",RouteType.ACTIVITY,ShopActivity::class.java))
Route.register(RouteEntity("shop/ShopService",RouteType.SERVICE,ShopServiceImpl::class.java))
}

//其他模块中使用

class HomeActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity_home)
val btnGoShop = findViewById<Button>(R.id.btn_go_shop)
val btnGetPrice = findViewById<Button>(R.id.btn_get_price)
btnGoShop.setOnClickListener {
//跳转页面
Route.navigation(this, "shop/ShopActivity")
}
btnGetPrice.setOnClickListener {
//模块通信
val shopService: IShopService = Route.getService("shop/ShopService") as IShopService
Toast.makeText(this, "价格:${shopService.getPrice()}", Toast.LENGTH_SHORT).show()
}
}
}

几点路由优化思路

  1. 路由信息 每次都要手动注册 很麻烦
    1. 利用编译时注解结合APT技术优化
    2. 自定义注解,跳转的页面和通信类添加注解
    3. 定义注解处理器,在编译时读取注解
    4. 根据注解携带的信息 处理业务逻辑 生成java类 完成组件注册功能
  2. 路由组件 所有路由信息在初始化的时候一次性加载到内存中,需要优化
    1. 分组保存,懒加载信息
    2. 根据路径 把路由信息分组保存,
      1. RootManager 保存 内部持有map 保存所有group 信息
      2. Group 内部持有 List 保存所有 节点信息
    3. 当用到某一group时,
      1. 通过反射实例化Group 加载当前Group下的节点信息到内存中
  3. 每次获取对象时,都是通过反射创建新对象,消耗内存
    1. 新增缓存机制,只在第一次创建新对象
    2. 可以使用 LruCache 缓存

上述路由组件的例子是非常简单的,难点在于从零开始,没有任何借鉴的情况下搞出这个”简单的”路由组件,反正我是没有这个创造能力 哈哈。

如果想要搞一个成熟完美的路由组件还是非常难的,但是最初肯定都是从基础功能开始一点一点迭代。除非是大佬,不然不推荐自定义路由组件


作者:图个喜庆
链接:https://juejin.cn/post/7105576036720443405
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

FlutterWeb浏览器刷新后无法回退的解决方案

一、问题 在Flutter开发的网页运行时,浏览器刷新网页后,虽然会显示刷新前的页面(前提是用静态路由跳转),但这时调用Navigator.pop方法是回不到上一页的,包括点击浏览器的回退按钮也是无效的(地址栏中的url会变,页面不会变)。 二、原因 当浏览器...
继续阅读 »

一、问题


在Flutter开发的网页运行时,浏览器刷新网页后,虽然会显示刷新前的页面(前提是用静态路由跳转),但这时调用Navigator.pop方法是回不到上一页的,包括点击浏览器的回退按钮也是无效的(地址栏中的url会变,页面不会变)。


二、原因


当浏览器刷新时,Flutter引擎会重新启动,并加载当前页面,也就是说,刷新后的Flutter内存中所有静态变量都被初始化,页面栈内之前的页面记录都未保留,只有当前的页面。就像是浏览网页时,把其中一页的网址拷出来,在新的标签页再次打开。


三、解决方案


1. 思路


知道什么原因引起的,就针对性解决。页面栈记录丢失,那么就代码中自己维护一套备用栈,监听页面路由,每次进入新页面时,记录当前页面的URL,当退出时,删除记录的URL,在浏览器刷新栈记录失效时,帮助回退到上一页。


2.方案优缺点


优点: 可实现回退效果无异常,调用Navigator.pop方法或点击浏览器回退按钮都支持;


缺点: Navigator.pushName().then的回调无法生效,因为是重新生成的上一页,所以并不会调用回调;回退后的页面中的临时数据都会消失,比如输入框内的内容,成员变量等;跳转必须用静态路由的方式,并且传参要用Uri包裹,不能用构造函数传参。


四、实现


1. Web本地存储工具—localStorage


localStorage是在html包下window中的一个存储对象,以keyvalue的形式进行存储


// 导包
import 'dart:html' as html;

// 使用方式
html.window.localStorage["key"] = "value"

对存储工具的封装这里就不写到文章里了,根据实现业务情况去封装,方便调用就行。


2. 栈记录工具类RouterHistory


这是一个栈记录工具,主要作用是注册监听,添加删除记录等。


/// DB()为封装好的本地数据库
class RouterHistory {
/// 监听浏览器刷新前的回调
static Function(html.Event event)? _beforeUnload;

/// 监听浏览器回退时的回调
static Function(html.Event event)? _popState;

/// 目前页面是否被刷新过
static bool isRefresh = false;

/// 初始化与注册监听
static void register() {
// 刷新时回调
_beforeUnload = (event) {
// 本地记录,标记成"已刷新"
DB(DBKey.isRefresh).value = true;
// 移除刷新前的实例的监听
html.window.removeEventListener('beforeunload', _beforeUnload);
html.window.removeEventListener('popstate', _popState);
};
// 浏览器回退按钮回调
_popState = (event) {
// 页面被刷新,触发备用回调
if (isRefresh) {
_back(R.currentContext); //R.currentContext 为当前页面的Context
}
};
// 添加监听
html.window.addEventListener('beforeunload', _beforeUnload);
html.window.addEventListener('popstate', _popState);

// 从本地数据库中取出"刷新"标记
isRefresh = DB(DBKey.isRefresh).get(false);

// 如果未被刷新,清除上次备用栈中的历史记录
if (!isRefresh) {
clean();
}

// 还原本地库中的刷新标记
DB(DBKey.isRefresh).value = false;
}


static bool checkBack(currentContext) {
// 是否能正常 pop
if (Navigator.canPop(currentContext)) {
return true;
}

// 不能则启用备用栈
_back(currentContext);
return false;
}

// 返回
static void _back(currentContext) {
List history = get();
if (history.length > 1) {
history.removeLast();
set(history);
//跳转至上一页并关闭当前页
Navigator.of(currentContext).popAndPushNamed(history.last);
}
}

// 添加记录
static add(String? path) {
if (path ` null) return;
List history = get();
if (history.contains(path)) return;
history.add(path);
set(history);
}

// 删除记录
static remove(String? path) {
if (path ` null) return;
List history = get();
history.remove(path);
set(history);
}

// 设置备用栈数据
static String set(List<dynamic> history) => DB(DBKey.history).value = json.encode(history);

// 取出备用栈数据
static get() => json.decode(DB(DBKey.history).get('[]'));

// 清除备用栈
static clean() => DB(DBKey.history).value = '[]';
}

3. 监听Flutter路由


自定义类并实现NavigatorObserver,并将实现类放在MaterialApp中的navigatorObservers参数中。


// 实现类
class HistoryObs extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 存路由信息
RouterHistory.add(route.settings.name);
}

@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 删路由信息
RouterHistory.remove(route.settings.name);
}
}


// 设置监听
MaterialApp(
......
navigatorObservers: [ HistoryObs() ],
......
)

4. 路由方法封装


跳转方法必须为静态路由,以保证参数和路径都能在url中,才可实现回退效果


 /// 替换 Navigator.pop ,
static pop() {
// 检测是否能正常返回,不能则返回FALSE
if (RouterHistory.checkBack(currentContext)) {
Navigator.pop(currentContext);
}
}

/// 静态路由跳转
static Future toName(String pageName, {Map<String, dynamic>? params}) {
// 封装路径以及参数
var uri = Uri(scheme: RoutePath.scheme, host: pageName, queryParameters: params ?? {});
return Navigator.of(currentContext).pushNamed(uri.toString());
}


5. 初始化位置


放在MaterialApp外层的build中,或initState中即可。


  @override
void initState() {
super.initState();
RouterHistory.register();
}

@override
Widget build(BuildContext context) {
// 或 RouterHistory.register();
return MaterialApp(
navigatorObservers: [MiddleWare()],
);
}


以上就是该方案的关键代码


五、最后


该方案只是能解决问题,但不是最好的解决方案。有更好的解决方案欢迎留言~


Flutter官方的Navigator 2.0 虽然能实现回退,本质上也是跳转了新页面,并造成栈内记录混乱,不能像真正的web一样,感兴趣的同学可以自行了解下Navigator 2.0



Navigator2.0在浏览器回退按钮的处理上又与Navigator1.0不同,点击回退按钮时Navigator2.0并不是执行pop操作,而是执行setNewRoutePath操作,本质上应该是从浏览器的history中获取上一个页面的url,然后重新加载。这样确实解决了刷新后回退的问题,因为刷新后浏览器的history并未丢失,但是也导致了文章中我们提到的flutter中的页面栈混乱的问题。


作者:苏啵曼
链接:https://juejin.cn/post/7114848130004156446
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android - setVisibility() 失效,竟然是因为内存泄露

一、前情概要 目前,我在开发的一个 Android 项目需要各个功能做到线上动态化,其中,App 启动时显示的 Loading 模块,会优先检测加载远程的 Loading 模块,加载失败时,会使用 App 本身默认的 Loading 视图,为此,我编写了一个 ...
继续阅读 »

一、前情概要


目前,我在开发的一个 Android 项目需要各个功能做到线上动态化,其中,App 启动时显示的 Loading 模块,会优先检测加载远程的 Loading 模块,加载失败时,会使用 App 本身默认的 Loading 视图,为此,我编写了一个 LoadingLoader 工具类:


/**
* Loading 加载器
*
* @author GitLqr
* @since 2022/7/2
*/
object LoadingLoader {

private var isInited = false // 防止多次初始化
private lateinit var onLoadFail: () -> Unit // 远程loading加载失败时的回调
private lateinit var onLoadComplete: () -> Unit // 加载完成后回调(无论成功失败)

fun init(onLoadFail: () -> Unit = {}, onLoadComplete: () -> Unit = {}): LoadingLoader {
if (!isInited) {
this.onLoadFail = onLoadFail
this.onLoadComplete = onLoadComplete
isInited = true
} else {
log("you have inited, this time is not valid")
}
return this
}

fun go() {
if (isInited) {
loadRemoteLoading(callback = { isSuccess ->
if (!isSuccess) onLoadFail()
onLoadComplete()
})
} else {
log("you must invoke init() firstly")
}
}

private fun loadRemoteLoading(callback: (boolean: Boolean) -> Unit) {
// 模拟远程 Loading 模块加载失败
Handler(Looper.getMainLooper()).postDelayed({
callback(false)
}, 1000)
}

private fun log(msg: String) {
Log.e("LoadingUpdater", msg)
}
}

LoadingLoader 工具类使用 Kotlin 的单例模式,init() 方法接收 2 个回调参数,go() 方法触发加载远程 Loading 模块,并根据加载结果执行回调,其中 isInited 用于防止该工具类被初始化多次。然后,在 App 的主入口 LoadingActivity 中使用 LoadingLoader,当加载远程 Loading 模块失败时,将原本隐藏的默认 Loading 视图显示出来;当加载 Loading 模块完成后(无论成功失败),模拟初始化数据并跳转主界面,关闭 LoadingActivity:


/**
* App 启动时的 Loading 界面
*
* @author GitLqr
* @since 2022/7/2
*/
class LoadingActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_loading)
// Loading 模块加载器
LoadingLoader.init(onLoadFail, onLoadComplete).go()
}

override fun onDestroy() {
super.onDestroy()
Log.e("GitLqr", "onDestroy")
}

private val onLoadFail: () -> Unit = {
// 显示默认 loading 界面
findViewById<View>(R.id.cl_def_loading).setVisibility(View.VISIBLE)
}

private val onLoadComplete: () -> Unit = {
// 模拟初始化数据,1秒后跳转主界面
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// 注意:此处意图使用的 flag,会将 LoadingActivity 界面关闭,触发 onDestroy()
startActivity(intent)
}, 1000)
}
}

LoadingActivity 的 xml 布局代码如下,默认的 Loading 布局初始状态不可见,即 visibility="gone"


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_def_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f00"
android:visibility="gone"
tools:visibility="visible">

<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="很好看的默认loading界面"
android:textColor="@color/white"
android:textSize="60dp" />

<ProgressBar
android:id="@+id/pb_loading"
android:layout_width="0dp"
android:layout_height="0dp"
android:indeterminateDrawable="@drawable/anim_loading"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75"
app:layout_constraintWidth_percent="0.064" />

</androidx.constraintlayout.widget.ConstraintLayout>

</FrameLayout>

以上代码比较简单,现在来看下演示效果:



这里会发现一个问题,因为是以清空栈的方式启动 MainActivity,所以第二次启动时,理论上应该会跟第一次启动时界面显示效果完全一致,即每次启动都会显示默认的 Loading 视图,但是实际情况并没有,而控制台的日志也证实了 LoadingActivity 的 onDestroy() 有被触发:



二、摸索过程


1、代码执行了吗?


难道第二次启动 App 时,LoadingActivity.onLoadFail 没有触发吗?加上日志验证一下:


class LoadingActivity : AppCompatActivity() {
...
private val onLoadFail: () -> Unit = {
// 显示默认 loading 界面
val defLoading = findViewById<View>(R.id.cl_def_loading)
defLoading.setVisibility(View.VISIBLE)
Log.e("GitLqr", "defLoading.setVisibility --> ${defLoading.visibility}")
}
}

重新打包再执行一遍上面的演示操作,日志输出如下:



说明 2 次启动都是有触发 LoadingActivity.onLoadFail 的,并且结果都是 0 ,即 View.VISIBLE。



此时有点怀疑人生,于是网上找了一圈 setVisibility() 失效 的原因,基本上都是同一个内容(都特么抄来抄去的),说是做动画导致的,可是我这里并没有做动画,所以与网上说的情况不相符。



2、视图不显示的直接原因是什么?


既然,代码有输出日志,那说明 setVisibility(View.VISIBLE) 这行代码肯定执行过了,而界面上不显示,直接原因是什么?是因为默认 Loading 视图的 visibility 依旧为 View.GONE?又或者是因为其他因素导致 View 的尺寸出现了问题?这时,可以使用 AndroidStudio 的 Layout Inspector 工具,可以直观的分析界面的布局情况,为了方便 Layout Inspector 工具获取 LoadingActivity 的布局信息,需要将 LoadingActivity.onLoadComplete 中跳转主界面的代码注释掉,其他保持不变:


class LoadingActivity : AppCompatActivity() {
...
private val onLoadComplete: () -> Unit = {
// 模拟初始化数据,1秒后跳转主界面
Handler(Looper.getMainLooper()).postDelayed({
// val intent = Intent(this, MainActivity::class.java)
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// // 注意:此处意图的 flag,会将 LoadingActivity 界面关闭,触发 onDestroy()
// startActivity(intent)
}, 1000)
}
}

然后重复上述演示操作,第一次启动,显示出默认 Loading,手动按返回键退出 App,再第二次启动,不显示默认 Loading:



控制台日志信息也如期输出,第二次启动确实执行了 setVisibility(View.VISIBLE)



这时,使用 Layout Inspector(菜单栏 -> Tools -> Layout Inspector),获取到 LoadingActivity 的布局信息:



这里可以断定,就是默认 Loading 视图的 visibility 依旧为 View.GONE 的情况。



注:因为 View.GONE 不占据屏幕空间,所以宽高都为 0,是正常的。



3、操作的视图是同一个吗?


现在回顾一下上述的 2 个线索,首先,代码中确定执行了 setVisibility(View.VISIBLE),并且日志里也显示了该视图的显示状态为 0,即 View.VISIBLE:



其次,使用 Layout Inspector 看到的的视图状态却为 View.GONE:



所以,真相只有一个,日志输出的视图 和 Layout Inspector 看到的的视图,肯定不是同一个!!为了验证这一点,代码再做如下调整,分别在 onCreate() 和 onLoadFail 中打印默认 Loading 视图信息:


class LoadingActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_loading)

val defLoading = findViewById<View>(R.id.cl_def_loading)
Log.e("GitLqr", "onCreate ---> view is ${defLoading}")

// Loading 模块加载器
LoadingLoader.init(onLoadFail, onLoadComplete).go()
}

private val onLoadFail: () -> Unit = {
// 显示默认 loading 界面
val defLoading = findViewById<View>(R.id.cl_def_loading)
defLoading.setVisibility(View.VISIBLE)
Log.e("GitLqr", "defLoading.setVisibility --> ${defLoading.visibility}, view is ${defLoading}")
}
}

再如上述演示操作一遍,日志输出如下:



可以看到第二次启动时,LoadingActivity.onLoadFail 中操作的视图,还是第一次启动时的那个视图,该视图是通过 findViewById 获取到的,说明 LoadingActivity.onLoadFail 中引用的 Activity 是第一次启动时的 LoadingActivity,也就是说 LoadingActivity 发生内存泄露了。此时才焕然大悟,Kotlin 中的 Lambda 表达式(像 onLoadFail、onLoadComplete 这种),对应到 Java 中就是匿名内部类,通过 Kotlin Bytecode 再反编译成 java 代码可以验证这点:


public final class LoadingActivity extends AppCompatActivity {
private final Function0 onLoadFail = (Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}

public final void invoke() {
View defLoading = LoadingActivity.this.findViewById(1000000);
defLoading.setVisibility(0);
StringBuilder var10001 = (new StringBuilder()).append("defLoading.setVisibility --> ");
Intrinsics.checkExpressionValueIsNotNull(defLoading, "defLoading");
Log.e("GitLqr", var10001.append(defLoading.getVisibility()).append(", view is ").append(defLoading).toString());
}
});
private final Function0 onLoadComplete;

protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(1300004);
View defLoading = this.findViewById(1000000);
Log.e("GitLqr", "onCreate ---> view is " + defLoading);
LoadingLoader.INSTANCE.init(this.onLoadFail, this.onLoadComplete).go();
}

protected void onDestroy() {
super.onDestroy();
Log.e("GitLqr", "onDestroy");
}

public LoadingActivity() {
this.onLoadComplete = (Function0)null.INSTANCE;
}
}

我们知道,Java 中,匿名内部类会持有外部类的引用,即匿名内部类实例 onLoadFail 持有 LoadingActivity 实例,而 onLoadFail 又会通过 LoadingLoader.init() 方法传递给 LoadingLoader 这个单例对象,所以间接导致 LoadingLoader 持有了 LoadingActivity,因为单例生命周期与整个 App 进程相同,所以只要 App 进程不死,内存中就只有一分 LoadingLoader 实例,又因为是强引用,所以 GC 无法回收掉第一次初始化时传递给 LoadingLoader 的 LoadingActivity 实例,所以,无论重启多少次,onLoadFail 中永远都是拿着第一次启动时的 LoadingActivity 来执行 findViewById,拿到的 Loading 视图自然也不会是当前最新 LoadingActivity 的 Loading 视图。


三、解决方案


既然知道是因为 LoadingActivity 内存泄露导致的,那么解决方案也简单,就是在 LoadingLoader 完成它的使命之后,及时释放掉对 LoadingActivity 的引用即可,又因为 LoadingActivity 实际上并不是被 LoadingLoader 直接引用,而是被其内部变量 onLoadFail 直接引用的,那么在 LoadingLoader 中只需要将 onLoadFail 的引用切断就行了:


object LoadingLoader {
private var isInited = false // 防止多次初始化
private lateinit var onLoadFail: () -> Unit // 远程loading加载失败时的回调
private lateinit var onLoadComplete: () -> Unit // 加载完成后回调

fun go() {
if (isInited) {
loadRemoteLoading(callback = { isSuccess ->
if (!isSuccess) onLoadFail()
onLoadComplete()
destroy() // 使命完成,释放资源
})
} else {
log("you must invoke init() firstly")
}
}

fun destroy() {
this.onLoadFail = {}
this.onLoadComplete = {}
this.isInited = false
}
}

至此,因内存泄露导致 setVisibility() 失效的问题就解决掉了,要坚信,在代码的世界里,没有魔法~


作者:GitLqr
链接:https://juejin.cn/post/7115781013170552840
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一文读懂Kotlin的数据流

一、Android分层架构 不管是早期的MVC、MVP,还是最新的MVVM和MVI架构,这些框架一直解决的都是一个数据流的问题。一个良好的数据流框架,每一层的职责是单一的。例如,我们可以在表现层(Presentation Layer)的基础上添加一个领域层(D...
继续阅读 »

一、Android分层架构


不管是早期的MVC、MVP,还是最新的MVVM和MVI架构,这些框架一直解决的都是一个数据流的问题。一个良好的数据流框架,每一层的职责是单一的。例如,我们可以在表现层(Presentation Layer)的基础上添加一个领域层(Domain Layer) 来保存业务逻辑,使用数据层(Data Layer)对上层屏蔽数据来源(数据可能来自远程服务,可能是本地数据库)。


在Android中,一个典型的Android分层架构图如下:


image.png


其中,我们需要重点看下Presenter 和 ViewModel, Presenter 和 ViewModel向 View 提供数据的机制是不同的。



  • Presenter: Presenter通过持有 View 的引用并直接调用操作 View,以此向 View 提供和更新数据。

  • ViewModel:ViewModel 通过将可观察的数据暴露给观察者来向 View 提供和更新数据。


目前,官方提供的可观察的数据组件有LiveData、StateFlow和SharedFlow。可能大家对LiveData比较熟悉,配合ViewModel可以很方便的实现数据流的流转。不过,LiveData也有很多常见的缺陷,并且使用场景也比较固定,如果网上出现了KotlinFlow 替代 LiveData的声音。那么 Flow 真的会替代 LiveData吗?Flow 真的适合你的项目吗?看完下面的分析后,你定会有所收获。


二、ViewModel + LiveData


ViewModel的作用是将视图和逻辑进行分离,Activity或者Fragment只负责UI显示部分,网络请求或者数据库操作则有ViewModel负责。ViewModel旨在以注重生命周期的方式存储和管理界面相关的数据,让数据可在发生屏幕旋转等配置更改后继续留存。并且ViewModel不持有View层的实例,通过LiveData与Activity或者Fragment通讯,不需要担心潜在的内存泄漏问题。


而LiveData 则是一种可观察的数据存储器类,与常规的可观察类不同,LiveData 具有生命周期感知能力,它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保LiveData当数据源发生变化的时候,通知它的观察者更新UI界面。同时它只会通知处于Active状态的观察者更新界面,如果某个观察者的状态处于Paused或Destroyed时那么它将不会收到通知,所以不用担心内存泄漏问题。


下面是官方发布的架构组件库的生命周期的说明:


image.png


2.1 LiveData 特性


通过前面的介绍可以知道,LiveData 是 Android Jetpack Lifecycle 组件中的内容,具有生命周期感知能力。一句话概括就是:LiveData 是可感知生命周期的,可观察的,数据持有者。特点如下:



  • 观察者的回调永远发生在主线程

  • 仅持有单个且最新的数据

  • 自动取消订阅

  • 提供「可读可写」和「仅可读」两个版本收缩权限

  • 配合 DataBinding 实现「双向绑定」


观察者的回调永远发生在主线程


因为LiveData 是被用来更新 UI的,因此 Observer 接口的 onChanged() 方法必须在主线程回调。


public interface Observer<T> {
void onChanged(T t);
}

背后的道理也很简单,LiveData 的 setValue() 发生在主线程(非主线程调用会抛异常),而如果调用postValue()方法,则它的内部会切换到主线程调用 setValue()。


protected void postValue(T value) {
boolean postTask;
synchronized (mDataLock) {
postTask = mPendingData == NOT_SET;
mPendingData = value;
}
if (!postTask) {
return;
}
ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable);
}

可以看到,postValue()方法的内部调用了postToMainThread()实现线程的切换,之后遍历所有观察者的 onChanged() 方法。


仅持有单个且最新数据


作为数据持有者,LiveData仅持有【单个且最新】的数据。单个且最新,意味着 LiveData 每次只能持有一个数据,如果有新数据则会覆盖上一个。并且,由于LiveData具备生命周期感知能力,所以观察者只会在活跃状态下(STARTED 到 RESUMED)才会接收到 LiveData 最新的数据,在非活跃状态下则不会收到。


自动取消订阅


可感知生命周期的重要优势就是可以自动取消订阅,这意味着开发者无需手动编写那些取消订阅的模板代码,降低了内存泄漏的可能性。背后的实现逻辑是在生命周期处于 DESTROYED 时,移除观察者。


@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState();
if (currentState == DESTROYED) {
removeObserver(mObserver);
return;
}
... //省略其他代码
}

提供「可读可写」和「仅可读」两种方式


LiveData 提供了setValue() 和 postValue()两种方式来操作实体数据,而为了细化权限,LiveData又提供了mutable(MutableLiveData) 和 immutable(LiveData) 两个类,前者「可读可写」,后者则「仅可读」。


image.png


配合 DataBinding 实现「双向绑定」


LiveData 配合 DataBinding 可以实现更新数据自动驱动UI变化,如果使用「双向绑定」还能实现 UI 变化影响数据的变化功能。


2.2 LiveData的缺陷


正如前面说的,LiveData有自己的使用场景,只有满足使用场景才会最大限度的发挥它的功能,而下面这些则是在设计时将自带的一些缺陷:



  • value 可以是 nullable 的

  • 在 fragment 订阅时需要传入正确的 lifecycleOwner

  • 当 LiveData 持有的数据是「事件」时,可能会遇到「粘性事件」

  • LiveData 是不防抖的

  • LiveData 的 transformation 需要工作在主线程


value 可以是 nullable 的


由于LiveData的getValue() 是可空的,所以在使用时应该注意判空,否则容易出现空指针的报错。


@Nullable
public T getValue() {
Object data = mData;
if (data != NOT_SET) {
return (T) data;
}
return null;
}

传入正确的 lifecycleOwner


Fragment 调用 LiveData的observe() 方法时传入 this 和 viewLifecycleOwner 的含义是不一样的。因为Fragment与Fragment中的View的生命周期并不一致,有时候我们需要的让observer感知Fragment中的View的生命周期而非Fragment。


粘性事件


粘性事件的定义是,发射的事件如果早于注册,那么注册之后依然可以接收到的事件,这一现象称为粘性事件。解决办法是:将事件作为状态的一部分,在事件被消费后,不再通知观察者。推荐两种解决方式:



  • KunMinX/UnPeek-LiveData

  • 使用kotlin 扩展函数和 typealias 封装解决「粘性」事件的 LiveData


默认不防抖


当setValue()/postValue() 传入相同的值且多次调用时,观察者的 onChanged() 也会被多次调用。不过,严格来讲,这也不算一个问题,我们只需要在调用 setValue()/postValue() 前判断一下 vlaue 与之前是否相同即可。


transformation 工作在主线程


有些时候,我们需要对从Repository 层得到的数据进行处理。例如,从数据库获得 User列表,我们需要根据 id 获取某个 User, 那么就需要用到MediatorLiveData 和 Transformatoins 来实现。



  • Transformations.map

  • Transformations.switchMap


并且,map 和 switchMap 内部均是使用 MediatorLiveData的addSource() 方法实现的,而该方法会在主线程调用,使用不当会有性能问题。


@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}

2.3 LiveData 小结


LiveData 是一种可观察的数据存储器类,与常规的可观察类不同,LiveData 具有生命周期感知能力,它遵循其他应用组件(如 Activity、Fragment 或 Service)的生命周期。这种感知能力可确保LiveData当数据源发生变化的时候,通知它的观察者更新UI界面。同时它只会通知处于Active状态的观察者更新界面,如果某个观察者的状态处于Paused或Destroyed时那么它将不会收到通知,所以不用担心内存泄漏问题。


同时,LiveData 专注单一功能,因此它的一些方法使用上是有局限性的,并且需要配合 ViewModel 使用才能显示其价值。


三、Flow


3.1 简介


Flow是Google官方提供的一套基于kotlin协程的响应式编程模型,它与RxJava的使用类似,但相比之下Flow使用起来更简单,另外Flow作用在协程内,可以与协程的生命周期绑定,当协程取消时,Flow也会被取消,避免了内存泄漏风险。


协程是轻量级的线程,本质上协程、线程都是服务于并发场景下,其中协程是协作式任务,线程是抢占式任务。默认协程用来处理实时性不高的数据,请求到结果后整个协程就结束了。比如,有下面一个例子:


image.png


其中,红框中需要展示的内容实时性不高,而需要交互的,比如转发和点赞属于实时性很高的数据需要定时刷新。对于实时性不高的场景,直接使用 Kotlin 的协程处理即可,比如。


suspend fun loadData(): Data

uiScope.launch {
val data = loadData()
updateUI(data)
}

而对于实时性要求较高的场景,上面的方式就不起作用了,此时需要用到Kotlin提供的Flow数据流。


fun dataStream(): Flow<Data>uiScope.launch { 
dataStream().collect { data ->
updateUI(data)
}
}

3.2 基本概念


Kotlin的数据流主要由三个成员组成,分别是生产者、消费者和中介。
生产者:生成添加到数据流中的数据,可以配合得协程使用,使用异步方式生成数据。
中介(可选):可以修改发送到数据流的值,或修正数据流本身。
消费者:使用方则使用数据流中的值。


其中,中介可以对数据流中的数据进行更改,甚至可以更改数据流本身,他们的架构示意图如下。


image.png


在Kotlin中,Flow 是一种冷流,不过有一种特殊的Flow( StateFlow/SharedFlow) 是热流。什么是冷流,他和热流又有什么关系呢?


冷流:只有订阅者订阅时,才开始执行发射数据流的代码。并且冷流和订阅者只能是一对一的关系,当有多个不同的订阅者时,消息是重新完整发送的。也就是说对冷流而言,有多个订阅者的时候,他们各自的事件是独立的。
热流:无论有没有订阅者订阅,事件始终都会发生。当 热流有多个订阅者时,热流与订阅者们的关系是一对多的关系,可以与多个订阅者共享信息。


3.3 StateFlow


前面说过,冷流和订阅者只能是一对一的关系,当我们要实现一个流多个订阅者的场景时,就需要使用热流了。


StateFlow 是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。可以通过其 value 属性读取当前状态值,如需更新状态并将其发送到数据流,那么就需要使用MutableStateFlow。


3.3.1 基本使用


在Android 中,StateFlow 非常适合需要让可变状态保持可观察的类。由于StateFlow并不是系统API,所以使用前需要添加依赖:


dependencies {
... //省略其他

implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
}

接着,我们需要创建一个ViewModel,比如:


class StateFlowViewModel: ViewModel() {
val data = MutableStateFlow<Int>(0)
fun add(v: View) {
data.value++
}
fun del(v: View) {
data.value--
}
}

可以看到,我们使用MutableStateFlow包裹需要操作的数据,并添加了add()和del()两个方法。然后,我们再编写一段测试代码实现数据的修改,并自动刷新数据。


class StateFlowActivity : AppCompatActivity() {
private val viewModel by viewModels<StateFlowViewModel>()
private val mBinding : ActivityStateFlowBinding by lazy {
ActivityStateFlowBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
initFlow()
}
private fun initFlow() {
mBinding.apply {
btnAdd.setOnClickListener {
viewModel.add(it)
}
btnDel.setOnClickListener {
viewModel.del(it)
}
}
}

}

上面代码中涉及到的布局代码如下:


<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="stateFlowViewModel"
type="com.xzh.demo.flow.StateFlowViewModel" />
</data>

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="200dp"
android:layout_marginTop="30dp"
android:text="@{String.valueOf(stateFlowViewModel.data)}"
android:textSize="24sp" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:contentDescription="start"
android:src="@android:drawable/ic_input_add" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_del"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:contentDescription="cancel"
android:src="@android:drawable/ic_menu_close_clear_cancel" />
</FrameLayout>
</layout>

上面代码中,我们使用了DataBing写法,因此不需要再手动的绑定数据和刷新数据。


3.4 SharedFlow


3.4.1 SharedFlow基本概念


SharedFlow提供了SharedFlow 与 MutableSharedFlow两个版本,平时使用较多的是MutableSharedFlow。它们的区别是,SharedFlow可以保留历史数据,MutableSharedFlow 没有起始值,发送数据时需要调用 emit()/tryEmit() 方法。


首先,我们来看看SharedFlow的构造函数:


public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

可以看到,MutableSharedFlow需要三个参数:



  • replay:表示当新的订阅者Collect时,发送几个已经发送过的数据给它,默认为0,即默认新订阅者不会获取以前的数据

  • extraBufferCapacity:表示减去replay,MutableSharedFlow还缓存多少数据,默认为0

  • onBufferOverflow:表示缓存策略,即缓冲区满了之后Flow如何处理,默认为挂起。除此之外,还支持DROP_OLDEST 和DROP_LATEST 。


 //ViewModel
val sharedFlow=MutableSharedFlow<String>()
viewModelScope.launch{
sharedFlow.emit("Hello")
sharedFlow.emit("SharedFlow")
}

//Activity
lifecycleScope.launch{
viewMode.sharedFlow.collect {
print(it)
}
}

3.4.2 基本使用


SharedFlow并不是系统API,所以使用前需要添加依赖:


dependencies {
... //省略其他

implementation "androidx.activity:activity-ktx:1.3.1"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
}

接下来,我们创建一个SharedFlow,由于需要一对多的进行通知,所以我们MutableSharedFlow,然后重写postEvent()方法,代码如下:


object LocalEventBus  {
private val events= MutableSharedFlow< Event>()
suspend fun postEvent(event: Event){
events.emit(event)
}
}
data class Event(val timestamp:Long)

接下来,我们再创建一个ViewModel,里面添加startRefresh()和cancelRefresh()两个方法,如下。


class SharedViewModel: ViewModel() {
private lateinit var job: Job

fun startRefresh(){
job=viewModelScope.launch (Dispatchers.IO){
while (true){
LocalEventBus.postEvent(Event(System.currentTimeMillis()))
}
}
}

fun cancelRefresh(){
job.cancel()
}
}

前面说过,一个典型的Flow是由三部分构成的。所以,此处我们先新建一个用于数据消费的Fragment,代码如下:


class FlowFragment: Fragment() {
private val mBinding : FragmentFlowBinding by lazy {
FragmentFlowBinding.inflate(layoutInflater)
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return mBinding.root
}
override fun onStart() {
super.onStart()
lifecycleScope.launchWhenCreated {
LocalEventBus.events.collect {
mBinding.tvShow.text=" ${it.timestamp}"
}
}
}
}

FlowFragment的主要作用就是接收LocalEventBus的数据,并显示到视图上。接下来,我们还需要创建一个数据的生产者,为了简单,我们只在生产者页面中开启协程,代码如下:


class FlowActivity : AppCompatActivity() {
private val viewModel by viewModels<SharedViewModel>()
private val mBinding : ActivityFlowBinding by lazy {
ActivityFlowBinding.inflate(layoutInflater)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
initFlow()
}

private fun initFlow() {
mBinding.apply {
btnStart.setOnClickListener {
viewModel.startRefresh()
}
btnStop.setOnClickListener {
viewModel.cancelRefresh()
}
}
}
}

其中,FlowActivity代码中涉及的布局如下:


<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<data>
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".fragment.SharedFragment">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<fragment
android:name="com.xzh.demo.FlowFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_marginStart="10dp"
android:layout_marginBottom="10dp"
android:src="@android:drawable/ic_input_add"
android:contentDescription="start" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="cancel" />
</FrameLayout>
</layout>

最后,当我们运行上面的代码时,就会在FlowFragment的页面上显示当前的时间戳,并且页面的数据会自动进行刷新。


3.5 冷流转热流


前文说过,Kotlin的Flow是一种冷流,而StateFlow/SharedFlow则属于热流。那么有人会问:怎么将冷流转化为热流呢?答案就是kotlin提供的shareIn()和stateIn()两个方法。


首先,来看一下StateFlow的shareIn的定义:


public fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T
): StateFlow<T>

shareIn方法将流转换为SharedFlow,需要三个参数,我们重点看一下started参数,表示流启动的条件,支持三种:



  • SharingStarted.Eagerly:无论当前有没有订阅者,流都会启动,订阅者只能接收到replay个缓冲区的值。

  • SharingStarted.Lazily:当有第一个订阅者时,流才会开始,后面的订阅者只能接收到replay个缓冲区的值,当没有订阅者时流还是活跃的。

  • SharingStarted.WhileSubscribed:只有满足特定的条件时才会启动。


接下来,我们在看一下SharedFlow的shareIn的定义:


public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>

此处,我们重点看下replay参数,该参数表示转换为SharedFlow之后,当有新的订阅者的时候发送缓存中值的个数。


3.6 StateFlow与SharedFlow对比


从前文的介绍可以知道,StateFlow与SharedFlow都是热流,都是为了满足流的多个订阅者的使用场景的,一时间让人有些傻傻分不清,那StateFlow与SharedFlow究竟有什么区别呢?总结起来,大概有以下几点:



  • SharedFlow配置更为灵活,支持配置replay、缓冲区大小等,StateFlow是SharedFlow的特殊化版本,replay固定为1,缓冲区大小默认为0。

  • StateFlow与LiveData类似,支持通过myFlow.value获取当前状态,如果有这个需求,必须使用StateFlow。

  • SharedFlow支持发出和收集重复值,而StateFlow当value重复时,不会回调collect给新的订阅者,StateFlow只会重播当前最新值,SharedFlow可配置重播元素个数(默认为0,即不重播)。


从上面的描述可以看出,StateFlow为我们做了一些默认的配置,而SharedFlow泽添加了一些默认约束。总的来说,SharedFlow相比StateFlow更灵活。


四、总结


目前,官方提供的可观察的数据组件有LiveData、StateFlow和SharedFlow。LiveData是Android早期的数据流组件,具有生命周期感知能力,需要配合ViewModel才能实现它的价值。不过,LiveData也有很多使用场景缺陷,常见的有粘性事件、不支持防抖等。


于是,Kotlin在1.4.0版本,陆续推出了StateFlow与SharedFlow两个组件,StateFlow与SharedFlow都是热流,都是为了满足流的多个订阅者的使用场景,不过它们也有微妙的区别,具体参考前面内容的说明。


作者:xiangzhihong
链接:https://juejin.cn/post/7116704131141615629
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter JSON 解析最佳实践

这篇文章其实早该写了,之前的业余时间一直花在开源项目或其它文章上了。 JSON 解析对于 Flutter 新人来讲是个绕不开的话题,大家都在吐槽 Flutter 没有反射,导致 JSON 解析无法像 Android 那样方便,其实是不必要的,因为可以做到一样方...
继续阅读 »

这篇文章其实早该写了,之前的业余时间一直花在开源项目或其它文章上了。


JSON 解析对于 Flutter 新人来讲是个绕不开的话题,大家都在吐槽 Flutter 没有反射,导致 JSON 解析无法像 Android 那样方便,其实是不必要的,因为可以做到一样方便。


网上讲 JSON 解析的文章很多,大家自行去学习即可,本篇文章直接给出我创造出的、我认为的最佳方案,如有雷同,纯属巧合:


使用 JsonToDart 插件自动生成 Bean 类,再使用 dynamic 关键字的能力,自动将 JSON 字符串代表的数据填充到 Bean 类中


我们以如下 JSON 文本为例:


{
"nickName": "hackware",
"realName": "陈方兵",
"age": 29,
"sex": "男"
}

这是个 Person 对象的描述,我们先使用 JsonToDart 插件将其转换成 Bean 类,这样我们就无需手写解析代码了:


Snipaste_2022-07-02_07-31-58.png


Snipaste_2022-07-02_07-33-45.png


生成的 Bean 类代码如下:


class Person {
Person({
this.nickName,
this.realName,
this.age,
this.sex,
});

Person.fromJson(dynamic json) {
nickName = json['nickName'];
realName = json['realName'];
age = json['age'];
sex = json['sex'];
}

String? nickName;
String? realName;
int? age;
String? sex;

Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['nickName'] = nickName;
map['realName'] = realName;
map['age'] = age;
map['sex'] = sex;
return map;
}
}

这段代码最核心的是 fromJson 这个构造函数,由于 Flutter 中没有反射,我们无法动态的调用 fromJson 方法。但我们可以先构造一个空的 Person 对象,再使用 dynamic 关键字调用它,但需要对 fromJson 做一下更改,将它从构造函数改为普通函数,如下:


Person fromJson(dynamic json) {
nickName = json['nickName'];
realName = json['realName'];
age = json['age'];
sex = json['sex'];
return this;
}

或是:


void fromJson(dynamic json) {
nickName = json['nickName'];
realName = json['realName'];
age = json['age'];
sex = json['sex'];
}

真正的解析代码如下:


String jsonData = ''; // 从网络加载的 JSON 文本
Person person = Person().fromJson(jsonDecode(jsonData));

我想说明的是:使用反射自动创建对象和手动创建对象后再自动为该对象填充数据是一样方便的。因此即便没有反射,我们也能对网络请求做很好的封装。


目前由于我用的是自创的 PVState 架构模式,它是 MVC 的改进版,它也是个轻量级的状态管理方案。只有不到 120 行代码。它分为 PState 和 VState,这里的 State 指的是 StatefulWidget 的 State。前者封装业务逻辑,后者描述 UI,UI 和业务逻辑可以完全隔离。我把网络请求的基础能力封装到了 BasePState 中,如下:


void sendRequest<BEAN>({
required Future<Response<String>> call,
required BEAN bean,
OnStartCallback? startCallback,
OnSuccessCallback<BEAN>? successCallback,
OnFailCallback<BEAN>? failCallback,
}) async {
startCallback?.call();
bool? success;
Object? exception;
try {
Response<String> resp = await call;
dynamic result = (bean as dynamic).fromJson(jsonDecode(resp.data!));
success = result.success;
} catch (e) {
debugPrint('$e');
exception = e;
} finally {
try {
if (success == true) {
successCallback?.call(bean);
} else {
failCallback?.call(bean, exception);
}
} catch (e) {
exception = e;
failCallback?.call(bean, exception);
}
}
}

真正发起请求的代码如下:


sendRequest(
call: dio.get(
'https://xxx',
),
bean: RealtimeAlarmListBean(),
startCallback: () {
setState(() {
loadingRealtimeAlarm = true;
});
},
successCallback: (RealtimeAlarmListBean bean) {
setState(() {
realtimeAlarmListBean = bean;
loadingRealtimeAlarm = false;
});
},
failCallback: (_, __) {
setState(() {
loadingRealtimeAlarm = false;
showToast('请求失败');
});
},
);

可见我在外部构造好了空的 Bean 对象传进去,当请求回来后会把数据填充进去,最后在 successCallback 再把非空的 Bean 对象回传回来。整个过程我没有手动对 JSON 做解析。是不是挺方便的呢?


我比较喜欢这种网络模块的封装模式,当然你也可以使用 async、await 做“同步”的封装,萝卜青菜各有所爱吧。


这是目前我看到的最好的 JSON 解析方法,如果你有更好的方法,欢迎在评论区交流哦!


作者:hackware
链接:https://juejin.cn/post/7115564008047902727
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套

这次的 Flutter 小技巧是 ListView 和 PageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListView 和 PageView 的三种嵌套模式带大家收获一些不一样的小技巧。 正常嵌套 最常...
继续阅读 »

这次的 Flutter 小技巧是 ListViewPageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListViewPageView 的三种嵌套模式带大家收获一些不一样的小技巧。


正常嵌套


最常见的嵌套应该就是横向 PageView 加纵向 ListView 的组合,一般情况下这个组合不会有什么问题,除非你硬是要斜着滑


最近刚好遇到好几个人同时在问:“斜滑 ListView 容易切换到 PageView 滑动” 的问题,如下 GIF 所示,当用户在滑动 ListView 时,滑动角度带上倾斜之后,可能就会导致滑动的是 PageView 而不是 ListView


xiehuadong


虽然从我个人体验上并不觉得这是个问题,但是如果产品硬是要你修改,难道要自己重写 PageView 的手势响应吗?


我们简单看一下,不管是 PageView 还是 ListView 它们的滑动效果都来自于 Scrollable ,而 Scrollable 内部针对不同方向的响应,是通过 RawGestureDetector 完成:



  • VerticalDragGestureRecognizer 处理垂直方向的手势

  • HorizontalDragGestureRecognizer 处理水平方向的手势


所以简单看它们响应的判断逻辑,可以看到一个很有趣的方法 computeHitSlop根据 pointer 的类型确定当然命中需要的最小像素,触摸默认是 kTouchSlop (18.0)


image-20220613103745974


看到这你有没有灵光一闪:如果我们把 PageView 的 touchSlop 修改了,是不是就可以调整它响应的灵敏度? 恰好在 computeHitSlop 方法里,它可以通过 DeviceGestureSettings 来配置,而 DeviceGestureSettings 来自于 MediaQuery ,所以如下代码所示:


body: MediaQuery(
///调高 touchSlop 到 50 ,这样 pageview 滑动可能有点点影响,
///但是大概率处理了斜着滑动触发的问题
data: MediaQuery.of(context).copyWith(
gestureSettings: DeviceGestureSettings(
touchSlop: 50,
)),
child: PageView(
scrollDirection: Axis.horizontal,
pageSnapping: true,
children: [
HandlerListView(),
HandlerListView(),
],
),
),

小技巧一:通过嵌套一个 MediaQuery ,然后调整 gestureSettingstouchSlop 从而修改 PageView 的灵明度 ,另外不要忘记,还需要把 ListViewtouchSlop 切换会默认 的 kTouchSlop


class HandlerListView extends StatefulWidget {
@override
_MyListViewState createState() => _MyListViewState();
}
class _MyListViewState extends State<HandlerListView> {
@override
Widget build(BuildContext context) {
return MediaQuery(
///这里 touchSlop 需要调回默认
data: MediaQuery.of(context).copyWith(
gestureSettings: DeviceGestureSettings(
touchSlop: kTouchSlop,
)),
child: ListView.separated(
itemCount: 15,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
separatorBuilder: (context, index) {
return const Divider(
thickness: 3,
);
},
),
);
}
}

最后我们看一下效果,如下 GIF 所示,现在就算你斜着滑动,也很触发 PageView 的水平滑动,只有横向移动时才会触发 PageView 的手势,当然, 如果要说这个粗暴的写法有什么问题的话,大概就是降低了 PageView 响应的灵敏度


xiehuabudong


同方向 PageView 嵌套 ListView


介绍完常规使用,接着来点不一样的,在垂直切换的 PageView 里嵌套垂直滚动的 ListView , 你第一感觉是不是觉得不靠谱,为什么会有这样的场景?



对于产品来说,他们不会考虑你如何实现的问题,他们只会拍着脑袋说淘宝可以,为什么你不行,所以如果是你,你会怎么做?



而关于这个需求,社区目前讨论的结果是:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理



如果对实现逻辑分析没兴趣,可以直接看本小节末尾的 源码链接



看到自己管理先不要慌,虽然要自己实现 PageViewListView 的手势分发,但是其实并不需要重写 PageViewListView ,我们可以复用它们的 Darg 响应逻辑,如下代码所示:



  • 通过 NeverScrollableScrollPhysics 禁止了 PageViewListView 的滚动效果

  • 通过顶部 RawGestureDetector VerticalDragGestureRecognizer 自己管理手势事件

  • 配置 PageControllerScrollController 用于获取状态


body: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
})
},
behavior: HitTestBehavior.opaque,
child: PageView(
controller: _pageController,
scrollDirection: Axis.vertical,
///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
children: [
ListView.builder(
controller: _listScrollController,
///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(title: Text('List Item $index'));
},
itemCount: 30,
),
Container(
color: Colors.green,
child: Center(
child: Text(
'Page View',
style: TextStyle(fontSize: 50),
),
),
)
],
),
),

接着我们看 _handleDragStart 实现,如下代码所示,在产生手势 details 时,我们主要判断:



  • 通过 ScrollController 判断 ListView 是否可见

  • 判断触摸位置是否在 ListIView 范围内

  • 根据状态判断通过哪个 Controller 去生产 Drag 对象,用于响应后续的滑动事件



void _handleDragStart(DragStartDetails details) {
///先判断 Listview 是否可见或者可以调用
///一般不可见时 hasClients false ,因为 PageView 也没有 keepAlive
if (_listScrollController?.hasClients == true &&
_listScrollController?.position.context.storageContext != null) {
///获取 ListView 的 renderBox
final RenderBox? renderBox = _listScrollController
?.position.context.storageContext
.findRenderObject() as RenderBox;

///判断触摸的位置是否在 ListView 内
///不在范围内一般是因为 ListView 已经滑动上去了,坐标位置和触摸位置不一致
if (renderBox?.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.contains(details.globalPosition) ==
true) {
_activeScrollController = _listScrollController;
_drag = _activeScrollController?.position.drag(details, _disposeDrag);
return;
}
}

///这时候就可以认为是 PageView 需要滑动
_activeScrollController = _pageController;
_drag = _pageController?.position.drag(details, _disposeDrag);
}

前面我们主要在触摸开始时,判断需要响应的对象时 ListView 还是 PageView ,然后通过 _activeScrollController 保存当然响应对象,并且通过 Controller 生成用于响应手势信息的 Drag 对象。



简单说:滑动事件发生时,默认会建立一个 Drag 用于处理后续的滑动事件,Drag 会对原始事件进行加工之后再给到 ScrollPosition 去触发后续滑动效果。



接着在 _handleDragUpdate 方法里,主要是判断响应是不是需要切换到 PageView :



  • 如果不需要就继续用前面得到的 _drag?.update(details)响应 ListView 滚动

  • 如果需要就通过 _pageController 切换新的 _drag 对象用于响应


void _handleDragUpdate(DragUpdateDetails details) {
if (_activeScrollController == _listScrollController &&

///手指向上移动,也就是快要显示出底部 PageView
details.primaryDelta! < 0 &&

///到了底部,切换到 PageView
_activeScrollController?.position.pixels ==
_activeScrollController?.position.maxScrollExtent) {
///切换相应的控制器
_activeScrollController = _pageController;
_drag?.cancel();

///参考 Scrollable 里
///因为是切换控制器,也就是要更新 Drag
///拖拽流程要切换到 PageView 里,所以需要 DragStartDetails
///所以需要把 DragUpdateDetails 变成 DragStartDetails
///提取出 PageView 里的 Drag 相应 details
_drag = _pageController?.position.drag(
DragStartDetails(
globalPosition: details.globalPosition,
localPosition: details.localPosition),
_disposeDrag);
}
_drag?.update(details);
}


这里有个小知识点:如上代码所示,我们可以简单通过 details.primaryDelta 判断滑动方向和移动的是否是主轴



最后如下 GIF 所示,可以看到 PageView 嵌套 ListView 同方向滑动可以正常运行了,但是目前还有个两个小问题,从图示可以看到:



  • 在切换之后 ListView 的位置没有保存下来

  • 产品要求去除 ListView 的边缘溢出效果


7777777777777


所以我们需要对 ListView 做一个 KeepAlive ,然后用简单的方法去除 Android 边缘滑动的 Material 效果:



  • 通过 with AutomaticKeepAliveClientMixinListView 在切换之后也保持滑动位置

  • 通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Scrollable 的边缘 Material 效果


child: PageView(
controller: _pageController,
scrollDirection: Axis.vertical,
///去掉 Android 上默认的边缘拖拽效果
scrollBehavior:
ScrollConfiguration.of(context).copyWith(overscroll: false),


///对 PageView 里的 ListView 做 KeepAlive 记住位置
class KeepAliveListView extends StatefulWidget {
final ScrollController? listScrollController;
final int itemCount;

KeepAliveListView({
required this.listScrollController,
required this.itemCount,
});

@override
KeepAliveListViewState createState() => KeepAliveListViewState();
}

class KeepAliveListViewState extends State<KeepAliveListView>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return ListView.builder(
controller: widget.listScrollController,

///屏蔽默认的滑动响应
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(title: Text('List Item $index'));
},
itemCount: widget.itemCount,
);
}

@override
bool get wantKeepAlive => true;
}

所以这里我们有解锁了另外一个小技巧:通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Android 滑动到边缘的 Material 2效果,为什么说 Material2, 因为 Material3 上变了,具体可见: Flutter 3 下的 ThemeExtensions 和 Material3


000000000



本小节源码可见: github.com/CarGuo/gsy_…



同方向 ListView 嵌套 PageView


那还有没有更非常规的?答案是肯定的,毕竟产品的小脑袋,怎么会想不到在垂直滑动的 ListView 里嵌套垂直切换的 PageView 这种需求。


有了前面的思路,其实实现这个逻辑也是异曲同工:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理,不同的就是手势方法分发的差异。


RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
})
},
behavior: HitTestBehavior.opaque,
child: ListView.builder(
///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _listScrollController,
itemCount: 5,
itemBuilder: (context, index) {
if (index == 0) {
return Container(
height: 300,
child: KeepAlivePageView(
pageController: _pageController,
itemCount: itemCount,
),
);
}
return Container(
height: 300,
color: Colors.greenAccent,
child: Center(
child: Text(
"Item $index",
style: TextStyle(fontSize: 40, color: Colors.blue),
),
));
}),
)

同样是在 _handleDragStart 方法里,这里首先需要判断:



  • ListView 如果已经滑动过,就不响应顶部 PageView 的事件

  • 如果此时 ListView 处于顶部未滑动,判断手势位置是否在 PageView 里,如果是响应 PageView 的事件


  void _handleDragStart(DragStartDetails details) {
///只要不是顶部,就不响应 PageView 的滑动
///所以这个判断只支持垂直 PageView 在 ListView 的顶部
if (_listScrollController.offset > 0) {
_activeScrollController = _listScrollController;
_drag = _listScrollController.position.drag(details, _disposeDrag);
return;
}

///此时处于 ListView 的顶部
if (_pageController.hasClients) {
///获取 PageView
final RenderBox renderBox =
_pageController.position.context.storageContext.findRenderObject()
as RenderBox;

///判断触摸范围是不是在 PageView
final isDragPageView = renderBox.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.contains(details.globalPosition);

///如果在 PageView 里就切换到 PageView
if (isDragPageView) {
_activeScrollController = _pageController;
_drag = _activeScrollController.position.drag(details, _disposeDrag);
return;
}
}

///不在 PageView 里就继续响应 ListView
_activeScrollController = _listScrollController;
_drag = _listScrollController.position.drag(details, _disposeDrag);
}

接着在 _handleDragUpdate 方法里,判断如果 PageView 已经滑动到最后一页,也将滑动事件切换到 ListView


void _handleDragUpdate(DragUpdateDetails details) {
var scrollDirection = _activeScrollController.position.userScrollDirection;

///判断此时响应的如果还是 _pageController,是不是到了最后一页
if (_activeScrollController == _pageController &&
scrollDirection == ScrollDirection.reverse &&

///是不是到最后一页了,到最后一页就切换回 pageController
(_pageController.page != null &&
_pageController.page! >= (itemCount - 1))) {
///切换回 ListView
_activeScrollController = _listScrollController;
_drag?.cancel();
_drag = _listScrollController.position.drag(
DragStartDetails(
globalPosition: details.globalPosition,
localPosition: details.localPosition),
_disposeDrag);
}
_drag?.update(details);
}

当然,同样还有 KeepAlive 和去除列表 Material 边缘效果,最后运行效果如下 GIF 所示。


22222222222



本小节源码可见:github.com/CarGuo/gsy_…



最后再补充一个小技巧:如果你需要 Flutter 打印手势竞技的过程,可以配置 debugPrintGestureArenaDiagnostics = true;来让 Flutter 输出手势竞技的处理过程


import 'package:flutter/gestures.dart';
void main() {
debugPrintGestureArenaDiagnostics = true;
runApp(MyApp());
}

image-20220613115808538


最后


最后总结一下,本篇介绍了如何通过 Darg 解决各种因为嵌套而导致的手势冲突,相信大家也知道了如何利用 ControllerDarg 来快速自定义一些滑动需求,例如 ListView 联动 ListView 的差量滑动效果:


///listView 联动 listView
class ListViewLinkListView extends StatefulWidget {
@override
_ListViewLinkListViewState createState() => _ListViewLinkListViewState();
}

class _ListViewLinkListViewState extends State<ListViewLinkListView> {
ScrollController _primaryScrollController = ScrollController();
ScrollController _subScrollController = ScrollController();

Drag? _primaryDrag;
Drag? _subDrag;

@override
void initState() {
super.initState();
}

@override
void dispose() {
_primaryScrollController.dispose();
_subScrollController.dispose();
super.dispose();
}

void _handleDragStart(DragStartDetails details) {
_primaryDrag =
_primaryScrollController.position.drag(details, _disposePrimaryDrag);
_subDrag = _subScrollController.position.drag(details, _disposeSubDrag);
}

void _handleDragUpdate(DragUpdateDetails details) {
_primaryDrag?.update(details);

///除以10实现差量效果
_subDrag?.update(DragUpdateDetails(
sourceTimeStamp: details.sourceTimeStamp,
delta: details.delta / 30,
primaryDelta: (details.primaryDelta ?? 0) / 30,
globalPosition: details.globalPosition,
localPosition: details.localPosition));
}

void _handleDragEnd(DragEndDetails details) {
_primaryDrag?.end(details);
_subDrag?.end(details);
}

void _handleDragCancel() {
_primaryDrag?.cancel();
_subDrag?.cancel();
}

void _disposePrimaryDrag() {
_primaryDrag = null;
}

void _disposeSubDrag() {
_subDrag = null;
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListViewLinkListView"),
),
body: RawGestureDetector(
gestures: <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel;
})
},
behavior: HitTestBehavior.opaque,
child: ScrollConfiguration(
///去掉 Android 上默认的边缘拖拽效果
behavior:
ScrollConfiguration.of(context).copyWith(overscroll: false),
child: Row(
children: [
new Expanded(
child: ListView.builder(

///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _primaryScrollController,
itemCount: 55,
itemBuilder: (context, index) {
return Container(
height: 300,
color: Colors.greenAccent,
child: Center(
child: Text(
"Item $index",
style: TextStyle(
fontSize: 40, color: Colors.blue),
),
));
})),
new SizedBox(
width: 5,
),
new Expanded(
child: ListView.builder(

///屏蔽默认的滑动响应
physics: NeverScrollableScrollPhysics(),
controller: _subScrollController,
itemCount: 55,
itemBuilder: (context, index) {
return Container(
height: 300,
color: Colors.deepOrange,
child: Center(
child: Text(
"Item $index",
style:
TextStyle(fontSize: 40, color: Colors.white),
),
),
);
}),
),
],
),
),
));
}
}

44444444444444


作者:恋猫de小郭
链接:https://juejin.cn/post/7116267156655833102
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

环信IM重大更新:新SDK+新场景+质量洞察+内容审核+出海

即时通讯IM是人与人沟通的基础服务,随着线上场景的进一步丰富,用户对于IM的能力要求日益提升。IM本质是一项服务,用户对于体验质量的要求异常严苛,掌握终端用户质量体验的变化和趋势,能够快速发现问题及根因,成为开发者核心关注的问题。近日,环信IM发布重大更新,包...
继续阅读 »

即时通讯IM是人与人沟通的基础服务,随着线上场景的进一步丰富,用户对于IM的能力要求日益提升。IM本质是一项服务,用户对于体验质量的要求异常严苛,掌握终端用户质量体验的变化和趋势,能够快速发现问题及根因,成为开发者核心关注的问题。近日,环信IM发布重大更新,包括:新SDK+新场景+水晶球+内容审核+出海等重磅特性。

WechatIMG14814.png

环信IM在已经推出的实时热点数据、消息投递查询、用户连接状态查询的基础上,

1、增加水晶球质量洞察能力对终端用户体验数据进行统计分析,帮助开发者实时掌控用户体验;

2、IM中传递的是用户产生的信息,对于所有终端用户而言,有一个安全、干净的聊天环境至关重要,环信IM提供了多种维度的内容审核管理能力,帮助开发者有效地对内容进行管理;

3、IM作为基础的沟通服务,在聊天的基础上衍生出了多种沟通的方式随着元宇宙等新场景的爆发,多人沟通的Discord社区模型受到了游戏玩家的青睐,环信IM提供了一组新的特性对新场景进行支持;

4、2022出海依旧是互联网的一个风口,随着越来越多的企业扬帆出海,环信IM提供的全球加速网络、安全合规、翻译都将助力出海客户快速构建符合当地用户使用习惯的应用,环信是IM行业首家通过全球最严苛安全GDPR认证的厂商。同时,环信IM提供业界最全的SDK矩阵,支持业界最全的小程序生态;

业界最全SDK矩阵提升开发体验

随着新技术、新平台的发展,环信IM响应开发者的需求,提供更多的SDK支持。在跨平台的支持上,提供Flutter、React Native、Unity、Uni-App。同时增加了原生Windows SDK的支持。 

环信IM支持Android、iOS、macOS、Windows、Linux、Web、Flutter、Unity、Electron、React Native、Uni-App、ApiCloud等12大SDK。

同时支持业界最全的小程序生态,包括:微信/QQ小程序、支付宝小程序、字节跳动小程序、快手小程序、百度小程序、360小程序等。

水晶球-质量洞察

实现从质量数据主动监控、异常问题实时告警、消息投递调查分析到历史数据回溯洞察是 IM质量监控功能的闭环。本期增加的质量数据主动监控为开发者提供了全景的用户IM使用质量数据。

终端数据:提供全球终端用户的登录、消息收发、好友管理、用户管理、群组管理、聊天室管理监控数据

image.png

Server API数据:提供全球服务端token获取、用户操作、文件操作、发送消息、群组管理、聊天室管理、用户属性操作的监控数据。

image.png

多维度内容审核能力

互联网不是法外之地!聊天内容的治理一直是即时通讯的核心能力,伴随着个人隐私保护法的实施,为用户提供一个安全、干净的聊天环境愈发重要。环信IM提供多维度多层次的内容审核能力,帮助开发者应对内容治理的挑战。

丰富的用户管理手段

作为开发者在应用中拥有最高权限,可以对用户进行APP级别的封禁、强制下线、删除等操作;在群组和聊天室中,可以对用户进行踢出、拉黑、禁言等操作。

消息举报:终端用户对自己接收的不良内容进行举报,开发者在console中对于发送者和消息进行处理。

image.png基于AI模型的文本、图片、语音、视频消息的审核能力:提供多种违规模型的不同消息类型的审核服务

image.png

社区多人沟通新场景

随着国外Discord爆红出圈,对于多人沟通的社区场景,环信IM增加了消息表情回复、子区等特性。

消息表情回复:用户可以在单聊和群聊中对消息添加、删除表情。表情可以直观地表达情绪,使用表情回复增加用户互动,提升用户使用体验。同时在群组中,利用表情回复可以发起投票,根据不同表情的追加数量来确认投票。

image.png消息子区:子区是群组成员的子集,是支持多人沟通的即时通讯系统,提供子区创建删除、成员管理等能力。

image.png

愈加成熟的全球服务

作为互联网底层通讯服务提供商,环信IM服务的海外客户已经遍布全球各地,随着海外服务经验的积累,环信IM的全球化服务也愈加成熟。环信是IM行业首家通过全球最严苛安全GDPR认证的厂商,帮助出海开发者免除安全后顾之忧。基于环信全球加速网络SD-GMN服务,全球端到端消息发送平均时延低于100ms,保证跨国跨区域沟通的用户体验。

环信5大数据中心覆盖全球200多个国家和地区;集团自建上万台服务器,部署全球300多个补充加速节点,实现低延迟;FPA加速与AWS加速智能切换,确保通信质量和高可用能力;On-Demand就近接入节点,全球加速网络SD-GMN服务,实测北美数据:30-40毫秒、欧洲:20-30毫秒、东南亚/日韩:30-40毫秒、北非:45毫秒、澳洲:50毫秒、中东:70毫秒、南美和南非:90毫秒;

image.png

image.pngimage.png

- SD-GMN实测数据展示 -

翻译功能:文本消息支持翻译功能,包含按需翻译和自动翻译。

1.按需翻译:收到消息时,接收方将消息内容翻译成目标语言。

2.自动翻译:用户发送消息时,SDK 根据设置的目标语言自动翻译消息内容,然后将消息原文和译文一并发送给消息接收方

image.png

结语

除了环信IM的大招,环信PUSH、环信MQTT也火力全开,环信正经历从即时通讯云到全球消息云的生态演化:1、IM平台向下深化和泛化。从Person to Person IM 发展为更通用的消息云平台,应用和应用之间,设备和云之间消息等,以及MQTT物联网消息新机会。2、IM平台向上层场景拓展。包括新场景产品,全渠道通知(IM、企业微信、小程序、短信等)。3、消息领域拓展。包括互联网消息外增加电信类消息,以及5G消息、短信、闪验等。环信全球消息云生态将覆盖包括:chat 聊天、notification 通知、push推送、营销、iot、短信验证码等丰富场景。

未来已来,环信已经准备好了!

即刻免费体验环信IM新特性:https://console.easemob.com/user/register

收起阅读 »

使用 C# 开发 node.js 插件

项目需求最近在开发一个 electron 程序,其中有用到和硬件通讯部分;硬件厂商给的是 .dll 链接库做通讯桥接, 第一版本使用 C 写的 Node.js 扩展 😁;由于有异步任务的关系,实现使用了 N-API 提供的多线程做异步任务调度, 虽然功能实...
继续阅读 »

项目需求

最近在开发一个 electron 程序,其中有用到和硬件通讯部分;硬件厂商给的是 .dll 链接库做通讯桥接,
第一版本使用 C 写的 Node.js 扩展 😁;由于有异步任务的关系,实现使用了 N-API 提供的多线程做异步任务调度,
虽然功能实现了,但是也有些值得思考的点。

  • 纯 C 编程效率低,木有 trycatch 的语言调试难度也大 (磕磕绊绊的)
  • 编写好的 .node 扩展文件,放在 electron 主进程中运行会有一定的隐患稍有差错会导致软件闪退 (后来用子进程隔离运行)
  • 基于 N-API 方式去编写 Node.js 插件会显得有所束缚,木有那种随心所欲写 C 的那种“顺畅”;尤其是多线程部分

综上考虑,加上通讯功能又是调用 .dll 文件,索性转战 C#,对于 windows 来说再合适不过了;但是问题是 C# 咋编译到 Node.js 中?
答案是“编译不了”。
插件实现的功能只是收到命令后调用 .dll 去操作硬件,再时时能把结果返回即可。
基于这个需求我们用 C# 去调用 .dll 文件,然后再解决派发命令、实时获取结果的通讯问题就OK了,剩下的就都是好处啦

  • C# 编写难度低于 C,又是 windows 亲儿子,基于 .NET Framework 编译后的程序仅 19KB (C实现同样功能编出来的.node文件 565KB)
  • 基于 C# 的插件独立于 Node.js 运行环境,程序出了问题不会影响 electron 应用
  • 木有任何的编程束缚,~亲想咋写就咋写

通讯问题

说这个之前我们还忽略了一个问题,这个 C# 的程序(.exe文件)如果启动?
既然是一个程序(.exe文件),我们双击即可执行;既然双击即可执行,我们就可以用 child_process 模块提供的
spawn 去拉起程序(代替鼠标双击);

好!程序已经启动了,那么该到了如果通讯的环节了。
spawn 的执行就是开启了一个单独的进程,通讯问题也就是进程通讯问题。之前如果你用过 spawn 启动过 Node.js 程序(.js文件),那么你肯定知道通讯使用 send 方法即可;这个是 Node.js 内置的方式

我们启动的进程是 C# 程序,通讯问题只能我们自己来解决了;进程通讯的方式有好多这里不展开。对于前端(web)攻城狮来讲,我们最熟悉的莫过于 http 通讯方式了;就用它!

  • C# 程序端启动开启一个 http 服务等待 Node.js 端发送请求过来;根据参数决定要干啥
  • spawn 启动的应用(进程),会返回一个 ChildProcessWithoutNullStreams (这个我也不能很明确的理解);能够接收到标准的 stdio 输入/输出
    那我们就利用这点使用 ChildProcessWithoutNullStreams.stdout.on('data', chunk => console.log(chunk.toString())) 的方式就可以收到 C# 通过 stdioConsole.WriteLine() 发过来的数据;
    哇!好方便~
  • 可能有人会想到用双工的 web socket 实现通讯,很棒!实现方式确实有很多种,这里用 Console.WriteLine() 通过标准的 stdio 方式实现,算不算是一个开发成本不高的讨巧做法呢!

大致流程


  • 如果觉得这篇文章有难度,可以看简单版的哦 Node.js 利用 stdio 标准输入/输出实现与 C# 程序通讯

开发环境

  • C# 代码部分使用 Visual Studio 2017
  • test.js 代码部分使用 VsCode
using System; 
using System.Collections.Generic;
using System.Linq; using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text.RegularExpressions;
namespace NodeAddons {
class Program {
static TcpListener listener;
static int port = 8899;
static string now = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
static void Main(string[] args){
listener = new TcpListener(IPAddress.Any, port);
listener.Start();
// 启用服务器线程
new Thread(new ThreadStart(StartServer)).Start();
Console.WriteLine("Http server run at {0}.", port);
}
// Http 服务器
static void StartServer() {
while(true) {
// 这里会阻塞线程,直到接受到一个请求
Socket socket = listener.AcceptSocket();
// 将请求单独开一个线程处理;while(true)会回到等待下一个请求状态,周而复始
new Thread(new ParameterizedThreadStart(HandleRequest)).Start(socket);
} } // 处理一个请求 static void HandleRequest(object args) { Socket socket = (Socket)args; byte[] receive = new byte[1024]; socket.Receive(receive, receive.Length, SocketFlags.None); string httpRawTxt = Encoding.ASCII.GetString(receive); // 通过 stdio(Console.WriteLine) 实现与 node.js 通讯 // ## 开头、结尾,方便区分这个条输出是给 node.js 通讯用的 Console.WriteLine("##" + httpRawTxt + "##"); SendToBrowser(ref socket, now); } // 发送数据 static void SendToBrowser(ref Socket socket, string body) { string header = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "Content-Length: " + body.Length + "\r\n" + "Access-Control-Allow-Origin:*\r\n" // 支持跨域 + "\r\n"; // 响应头与响应体分界 byte[] data = Encoding.ASCII.GetBytes(header + body); if (socket.Connected) { int res = socket.Send(data, data.Length, SocketFlags.None); if (res == -1){ Console.WriteLine("Socket Error cannot Send Packet.");}else{ Console.WriteLine(">> [{0}]", now); } socket.Close();}}}}

Node.js 部分

const http = require('http'); 
const cp = require('child_process');
const path = require('path');
// const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons.exe'));
const handel = cp.spawn(path.join(__dirname, 'dist/NodeAddons_WithConsole.exe')); handel.stdout.on('data', chunk => { const str = chunk.toString();
// 约定 ##数据## 的字符串为通讯数据
let res = str.match(/##([\S\s]*)##/g);
if (!Array.isArray(res)) return;
res = res[0].match(/(?<=(\?))(.*)(?=(\sHTTP\/1.1))/);
if (!Array.isArray(res)) return;
console.log('[stdout queryString]', res[0]); });
function query(param, cb) {
http.get(`http://127.0.0.1:8899/?${(new URLSearchParams(param)).toString()}`, res => { res.on('data', chunk => { cb(chunk.toString()); });
}); }
query({ name: 'anan', age: 29, time: Date.now() }, httpRawTxt => { console.log('[http response]', httpRawTxt); });
// 监听 Ctrl + c process.on('SIGINT', () => { handel.kill(); process.exit(0); });

测试一下

  • 当然程序不会自己停下来哈,毕竟子进程的 http 服务一直在运行!
    $ node test.js [stdout queryString] name=anan&age=29&time=1595134635733 [http response] 2020-07-19 12:57:15


本文转载自 https://www.jianshu.com/p/9ac4f9ef9625

收起阅读 »

分享4个Linux中Node.js的进程管理器

Node.js进程管理器是一个有用的工具,可以确保Node.js进程或脚本连续(永久)运行,并使其能够在系统引导时自动启动。它允许您监视正在运行的服务,它有助于执行常见的系统管理任务(例如重新启动失败,停止,重新加载配置而无需停机,修改环境变量/设置,显示性能...
继续阅读 »

Node.js进程管理器是一个有用的工具,可以确保Node.js进程或脚本连续(永久)运行,并使其能够在系统引导时自动启动。

它允许您监视正在运行的服务,它有助于执行常见的系统管理任务(例如重新启动失败,停止,重新加载配置而无需停机,修改环境变量/设置,显示性能指标等等)。它还支持应用程序日志记录,群集和负载平衡,以及许多其他有用的流程管理功能。

另请参阅:2019年为开发人员提供的14个最佳NodeJS框架

包管理器尤其适用于在生产环境中部署Node.js应用程序。 在本文中,我们将回顾Linux系统中Node.js应用程序管理的四个进程管理器。

1. PM2

PM2是一个开源,高级,功能丰富,跨平台和最流行的Node.js生产级流程管理器,内置负载均衡器。它允许您列出,监视和处理所有已启动的Nodejs进程,并支持群集模式。


安装PM2以在Linux中运行Nodejs应用程序

它支持应用程序监视:提供一种监视应用程序资源(内存和CPU)使用情况的简单方法。它支持您的流程管理工作流,允许您通过流程文件配置和调整每个应用程序的行为(支持的格式包括Javascript,JSON和YAML)。

应用程序日志始终是生产环境中的关键,在这方面,PM2允许您轻松管理应用程序的日志。它提供了分别处理和显示日志的不同方式和格式。您可以实时显示日志,刷新日志,并在需要时重新加载日志。

重要的是,PM2支持启动脚本,您可以将其配置为在预期或意外的计算机重新启动时自动启动进程。它还支持在当前目录或其子目录中修改文件时自动重新启动应用程序。

此外,PM2还带有一个模块系统,允许用户为Nodejs进程管理创建自定义模块。例如,您可以为日志轮换模块或负载平衡创建模块等等。

最后但同样重要的是,如果您使用Docker容器,PM2允许容器集成,并提供允许您以编程方式使用它的API系统。

2. StrongLoop PM

StrongLoop PM也是一个开源的高级生产过程管理器,用于Node.js应用程序,内置负载平衡,就像PM2一样,它可以通过命令行或图形界面使用。


用于Nodejs的StrongLoop PM进程管理器

它支持应用程序监视(查看性能指标,如事件循环时间、CPU和内存消耗)、多主机部署、集群模式、零停机应用程序重启和升级、故障时自动进程重启以及日志聚合和管理。

此外,它附带Docker支持,允许您将性能指标导出到与状态兼容的服务器,并在第三方控制台(如DataDog、石墨、Splunk以及Syslog和原始日志文件)中查看。

3. Forever

Forever是一个开源,简单且可配置的命令行界面工具,可以连续(Forever)运行给定的脚本。它适用于运行Node.js应用程序和脚本的较小部署。您可以通过两种方式永久使用:通过命令行或将其嵌入代码中。


Forever运行脚本

它允许您管理(启动,列出,停止,停止所有,重新启动,重新启动所有等等。)Node.js进程,它支持监视文件更改,调试模式,应用程序日志,终止进程和退出信号自定义等等。此外,它还支持多种使用选项,您可以直接从命令行传递或将它们传递到JSON文件中。

4. Systemd - 服务和系统管理器

在Linux中,Systemd是一个守护程序,用于管理系统资源,例如进程和文件系统的其他组件。 systemd管理的任何资源都称为一个单元。有不同类型的单元,包括服务,设备,插座,安装,目标和许多其他单元。

Systemd通过称为单元文件的配置文件管理单元。因此,为了像任何其他系统服务一样管理Node.js服务器,您需要为它创建一个单元文件,在这种情况下它将是一个服务文件。

为Node.js服务器创建服务文件后,可以启动它,启用它以在系统引导时自动启动,检查其状态,重新启动(停止并再次启动它)或重新加载其配置,甚至像任何其他系统服务一样停止它。

摘要

Node.js包管理器是在生产环境中部署项目的有用工具。它使应用程序永远存在,并简化了如何控制它。在本文中,我们回顾了Node.js的四个包管理器。如果您有任何疑问或问题,请使用下面的反馈表与我们联系。

本文转载自: https://www.jianshu.com/p/ee49e600dd16
收起阅读 »

Node.js编写组件的几种方式

本文主要备忘为Node.js编写组件的三种实现:纯js实现、v8 API实现(同步&异步)、借助swig框架实现。简介(1)v8 API方式为官方提供的原生方法,功能强大而完善,缺点是需要熟悉v8 API,编写起来比较麻烦,是js强相关的,不容易支持其...
继续阅读 »

Node.js编写组件的几种方式

本文主要备忘为Node.js编写组件的三种实现:纯js实现、v8 API实现(同步&异步)、借助swig框架实现。

关键字:Node.js、C++、v8、swig、异步、回调。

简介

首先介绍使用v8 API跟使用swig框架的不同:

(1)v8 API方式为官方提供的原生方法,功能强大而完善,缺点是需要熟悉v8 API,编写起来比较麻烦,是js强相关的,不容易支持其它脚本语言。

(2)swig为第三方支持,一个强大的组件开发工具,支持为python、lua、js等多种常见脚本语言生成C++组件包装代码,swig使用者只需要编写C++代码和swig配置文件即可开发各种脚本语言的C++组件,不需要了解各种脚本语言的组件开发框架,缺点是不支持javascript的回调,文档和demo代码不完善,使用者不多。

二、纯JS实现Node.js组件

(1)到helloworld目录下执行npm init 初始化package.json,各种选项先不管,默认即可,更多package.json信息参见:https://docs.npmjs.com/files/package.json

(2)组件的实现index.js,例如:

module.exports.Hello = function(name) {
console.log('Hello ' + name);
}

(3)在外层目录执行:npm install ./helloworld,helloworld于是安装到了node_modules目录中。

(4)编写组件使用代码:

var m = require('helloworld');
m.Hello('zhangsan'); //输出: Hello zhangsan

三、 使用v8 API实现JS组件——同步模式

(1)编写binding.gyp, eg:

    { "target_name": "hello", "sources": [ "hello.cpp" ]
}
]
}

关于binding.gyp的更多信息参见:https://github.com/nodejs/node-gyp

(2)编写组件的实现hello.cpp,eg:

#include <node.h>

namespace cpphello { using v8::FunctionCallbackInfo; using v8::Isolate; using v8::Local; using v8::Object; using v8::String; using v8::Value; void Foo(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "Hello World"));
} void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "foo", Foo);
}

NODE_MODULE(cpphello, Init)
}

(3)编译组件

node-gyp configure
node-gyp build

./build/Release/目录下会生成hello.node模块。

(4)编写测试js代码

const m = require('./build/Release/hello')
console.log(m.foo()); //输出 Hello World

(5)增加package.json 用于安装 eg:

{ "name": "hello", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "node test.js" }, "author": "", "license": "ISC" }

(5)安装组件到node_modules

进入到组件目录的上级目录,执行:npm install ./helloc //注:helloc为组件目录

会在当前目录下的node_modules目录下安装hello模块,测试代码这样子写:

var m = require('hello');
console.log(m.foo());

四、 使用v8 API实现JS组件——异步模式

上面三的demo描述的是同步组件,foo()是一个同步函数,也就是foo()函数的调用者需要等待foo()函数执行完才能往下走,当foo()函数是一个有IO耗时操作的函数时,异步的foo()函数可以减少阻塞等待,提高整体性能。

异步组件的实现只需要关注libuv的uv_queue_work API,组件实现时,除了主体代码hello.cpp和组件使用者代码,其它部分都与上面三的demo一致。

hello.cpp:

/* * Node.js cpp Addons demo: async call and call back.
* gcc 4.8.2
* author:cswuyg
* Date:2016.02.22
* */
#include <iostream> #include <node.h> #include <uv.h> #include <sstream> #include <unistd.h> #include <pthread.h>

namespace cpphello { using v8::FunctionCallbackInfo; using v8::Function; using v8::Isolate; using v8::Local; using v8::Object; using v8::Value; using v8::Exception; using v8::Persistent; using v8::HandleScope; using v8::Integer; using v8::String; // async task
struct MyTask{
uv_work_t work; int a{0}; int b{0}; int output{0};
unsigned long long work_tid{0};
unsigned long long main_tid{0};
Persistent<Function> callback;
}; // async function
void query_async(uv_work_t* work) {
MyTask* task = (MyTask*)work->data;
task->output = task->a + task->b;
task->work_tid = pthread_self();
usleep(1000 * 1000 * 1); // 1 second
} // async complete callback
void query_finish(uv_work_t* work, int status) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope handle_scope(isolate);
MyTask* task = (MyTask*)work->data; const unsigned int argc = 3;
std::stringstream stream;
stream << task->main_tid;
std::string main_tid_s{stream.str()};
stream.str("");
stream << task->work_tid;
std::string work_tid_s{stream.str()};

Local<Value> argv[argc] = {
Integer::New(isolate, task->output),
String::NewFromUtf8(isolate, main_tid_s.c_str()),
String::NewFromUtf8(isolate, work_tid_s.c_str())
};
Local<Function>::New(isolate, task->callback)->Call(isolate->GetCurrentContext()->Global(), argc, argv);
task->callback.Reset(); delete task;
} // async main
void async_foo(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope handle_scope(isolate); if (args.Length() != 3) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "arguments num : 3"))); return;
} if (!args[0]->IsNumber() || !args[1]->IsNumber() || !args[2]->IsFunction()) {
isolate->ThrowException(Exception::TypeError(String::NewFromUtf8(isolate, "arguments error"))); return;
}
MyTask* my_task = new MyTask;
my_task->a = args[0]->ToInteger()->Value();
my_task->b = args[1]->ToInteger()->Value();
my_task->callback.Reset(isolate, Local<Function>::Cast(args[2]));
my_task->work.data = my_task;
my_task->main_tid = pthread_self();
uv_loop_t *loop = uv_default_loop();
uv_queue_work(loop, &my_task->work, query_async, query_finish);
} void Init(Local<Object> exports) {
NODE_SET_METHOD(exports, "foo", async_foo);
}

NODE_MODULE(cpphello, Init)
}

异步的思路很简单,实现一个工作函数、一个完成函数、一个承载数据跨线程传输的结构体,调用uv_queue_work即可。难点是对v8 数据结构、API的熟悉。

test.js

// test helloUV module
'use strict';
const m = require('helloUV')

m.foo(1, 2, (a, b, c)=>{
console.log('finish job:' + a);
console.log('main thread:' + b);
console.log('work thread:' + c);
}); /* output:
finish job:3
main thread:139660941432640
work thread:139660876334848 */

五、swig-javascript 实现Node.js组件

利用swig框架编写Node.js组件

(1)编写好组件的实现:.h和.cpp **

eg:

namespace a { class A{ public: int add(int a, int y);
}; int add(int x, int y);
}

(2)编写.i,用于生成swig的包装cpp文件*

eg:

/* File : IExport.i */

%module my_mod

%include "typemaps.i"

%include "std_string.i"

%include "std_vector.i"

%{

#include "export.h"

%}

%apply int *OUTPUT { int *result, int* xx};

%apply std::string *OUTPUT { std::string* result, std::string* yy };

%apply std::string &OUTPUT { std::string& result };

%include "export.h"

namespace std {

%template(vectori) vector<int>;

%template(vectorstr) vector<std::string>;

};

上面的%apply表示代码中的 int* result、int* xx、std::string* result、std::string* yy、std::string& result是输出描述,这是typemap,是一种替换。

C++导出函数返回值一般定义为void,函数参数中的指针参数,如果是返回值的(通过*.i文件中的OUTPUT指定),swig都会把他们处理为JS函数的返回值,如果有多个指针,则JS函数的返回值是list。

%template(vectori) vector 则表示为JS定义了一个类型vectori,这一般是C++函数用到vector 作为参数或者返回值,在编写js代码时,需要用到它。

swig支持的更多的stl类型参见:https://github.com/swig/swig/tree/master/Lib/javascript/v8

(3)编写binding.gyp,用于使用node-gyp编译

**(4)生成warpper cpp文件 **生成时注意v8版本信息,eg:swig -javascript -node -c++ -DV8_VERSION=0x040599 example.i

(5)编译&测试

难点在于stl类型、自定义类型的使用,这方面官方文档太少。

六、其它

在使用v8 API实现Node.js组件时,可以发现跟实现Lua组件的相似之处,Lua有状态机,Node有Isolate。

Node实现对象导出时,需要实现一个构造函数,并为它增加“成员函数”,最后把构造函数导出为类名。Lua实现对象导出时,也需要实现一个创建对象的工厂函数,也需要把“成员函数”们加到table中。最后把工厂函数导出。

Node的js脚本有new关键字,Lua没有,所以Lua对外只提供对象工厂用于创建对象,而Node可以提供对象工厂或者类封装。


转载自 https://cloud.tencent.com/developer/article/1929669

收起阅读 »

从零打造node.js版scf客户端

node.js是一个划时代的技术,它在原有的Web前端和后端技术的基础上总结并提炼出了许多新的概念和方法,堪称是十多年来Web开发经验的集大成者。转转公司在使用node.js方面,一起走在前沿。8月16日,转转公司的FE王澍老师,在镜泊湖会议室进行了一场主题为...
继续阅读 »

node.js是一个划时代的技术,它在原有的Web前端和后端技术的基础上总结并提炼出了许多新的概念和方法,堪称是十多年来Web开发经验的集大成者。转转公司在使用node.js方面,一起走在前沿。8月16日,转转公司的FE王澍老师,在镜泊湖会议室进行了一场主题为《nodejs全栈之路》的讲座。优秀的语言、平台、工具只有在优秀的程序员的手中才能显现出它的威力。一直听说转转公司在走精英化发展战略,所以学习下转转对node.js的使用方式,就显得很有必要。
对于大多数人使用node.js上的直观感受,就是模块、工具很齐全,要什么有什么。简单request一下模块,就可以开始写javasript代码了。然而出自58同城的转转,同样存在大量服务,使用着58自有的rpc框架scf。scf无论从设计还是实际效果,都算得上业内领先。只不过在跨平台的基础建设上,略显不足。从反编译的源码中,可以找到支持的平台有.net、java、c、php。非java平台的scf版本更新,也有些滞后。之前还听说肖指导管理的应用服务部,以“兼职”的方式开发过c++版客户端。而且也得到umcwrite等服务的实际运用。所以node.js解决好调用scf服务,是真正广泛应用的前提。这也正是我最关心的问题。
王澍老自己的演讲过程并没有介绍scf调用的解决方案,但在提问环节中,进行了解答。我能记住的内容是,目前的采用的方案是使用node-java模块,启动一个jvm进程,最终还是在node.js的项目中编写的java代码,性能尚可接受,但使用中内存占用很大;王澍老师也在尝试自己使用c++开发模块来弃用node-java。
这确实很让我很失望,我所理解的node.js应该是与性能有关的部分,几乎全部是c++编写的。之前肖指导要求发布公共服务,改写成使用scf提供的异步方式执行,借那次机会,我也阅读了一部分反编译的scf源码。感觉如果只是解决node.js调用scf的问题,不应该是个很难的事情。像管理平台、先知等外围功能,可以后期一点点加入。正巧我一直在质疑自己是不是基础差的问题,干脆写一个node.js版的scf客户端,来试试自己的水准。
结合自己之前对node.js的零散知识(其实现在也很零散)。对这次实践提出如下的一些设计要点:
1、序列化版本使用scfv3,虽然难度应该是最大的,但应该能在较长的时间内避免升级序列化版本的琐事。
2、使用管理平台读取配置,禁用scf.config类似的本地配置。想想之前许多部门,推进禁用线上服务直连的过程,就觉得很有必要(管理平台也用线下环境,线下调试根本不是阻碍)。
3、客户端支持全类型,之前偶尔听说了c++版客户端不支持枚举类型,使得有些服务只能调整接口。
4、c++使用libuv库,具备跨平台开发、调试能力。c++版客户端听说只支持linux平台。
5、只提供异步接口,这是当然的,不然node.js就别想用了。

现有的c++客户端,在3、4、5上与我的设想不符合,所以我决定亲自编写。
先是搜了本介绍libuv的pdf——《An Introduction to libuv》,看了几天,对libuv的使用方式有所了解,用上的只有tcp相关接口。(比起java,node.js的资料还是少,介绍的也少有深入的,像这样的底层类库,资料就更少了)
在58作为rd,如果不是做ios,是少有配macbook的员工的。所以我本次是在windows上编写的。不得不说node.js就是霸道,自动安装时,默认全部安装最新的版本。这样在windows平台上编译c++时,就要求visual studio不能低于2015。
网上搜索c++开发node.js模块,基本总是能找到那个addon的示例。可能是由于v8引擎的接口也有过变化,addon的示例使用的类型、接口也存在几种,终于还是试出了自己可以编译过的了。
首先在addon的基础上,写个运用libuv连接tcp的逻辑,一旦试通了,就可以一点点抄写反编译的scf客户端源码了。

在开发过程中,我的设计也进行了一些修改:
1、反序列化逻辑,通过tcp连接,交由一个java程序来执行(基于netty开发)。由于反序列化时,scf的二进制数据是没有足够的类型信息的。大体上,当读取到一个typeid时,如果本地没有对应的类型信息,完全不知道下一个字节是做什么用的。(我其实只希望得到一个类似多叉树的嵌套格式,也做不到。)如果非要使用c++来执行反序列化,也并非不可能。需要将scf反序列化用到的类型信息,整理成一种新的数据格式,存放于c++程序的内存中。为此需要开发一个输出类型配置数据的java离线工具,node.js模块需要开发:读取这个类型配置文件到内存,再将scf反序列化的逻辑使用c++抄一遍。综上来看,使用一个java的反序列化辅助进程,可以在性能几乎无损的情况下,极大的减少了开发量,同时避免了许多反序列化过程中的bug。这不正是一个极简的微服务嘛。
2、javascript入参对象中,需要自带scf序列化相关的类型信息,这样就能在全类型的支持scf对象了。当然我也设想过,有没有机会将序列化,也交由java辅助进程。那样就需要设计一个java对象在javascript中的表示形式,由java辅助进程,先转换为java对象,再序列化。再加上两次额外tcp传输。在没有减少工作量的情况下,浪费了不少性能。当然如果十分拒绝c++开发的话,倒是能因此少写些c++代码。

后续可以做的一些事情:
1、完善的重连、超时处理;
2、管理平台配置热更新;
3、管理平台数据上报;
4、先知;
5、加密、压缩(似乎和node.js的非计算密集场景有些冲突,而且公司的scf配置默认都是关闭这两个的。scf良好的用了这么些年,不开启这两个功能的功劳应该也不小)

当然已开发的内容中,也一定满是bug。等有人用了,我再考虑改bug的事。生产环境下的试错机会,才能让程序真正成长。

收起阅读 »

React-Native与原生模块间的几种通信方式

原理string-NSStringnumber - int/NSInteger/float/double/NSNumberboolean - BOOL/NSNumberarray - NSArrayobject - NSDictionary(NSString型...
继续阅读 »

每种语言都有自己的设计理念、语法、运行环境,这也导致了不同语言间相互交流通信时必须要有中介来翻译,如JAVA与C/C++通过JNI来交流、OC与C/C++需要在.mm文件混编、而JAVA/OC与Lua通信时需要通过C/C++语言来做中介。那么在React-Native中JSX是如何与底层模块进行通信的呢?这里主要以iOS系统来做说明。

原理

通信本质上是信息的交流,具体到计算机语言则是数据的流动。应用中数据在React-Native与原生模块间的流动与共享,完成了与用户的交互,达成了应用的目标。React-Native与OC间通信的数据只能是下面的几种类型(前为JS类型,后为OC类型):

  • string-NSString
  • number - int/NSInteger/float/double/NSNumber
  • boolean - BOOL/NSNumber
  • array - NSArray
  • object - NSDictionary(NSString型key, value可以为这里的其它类型)
  • func - RCTResponseSenderBlock

其它类型的数据需要通过一定的规则转换成这几种类型后(一般都会转换成JSON串)再通信.

React-Native本质是通过JavaScriptCore.framework实现JS代码与OC代码间的互动。因此下面说的几种方式在本质原理上都是相同的,不同的地方只是在于实现形式与方法的差别。

函数调用

在将原生模块封装并提供给React-Native使用时,可以通过RCT_EXPORT_METHOD()宏向React-Native侧定义其可以调用的接口函数,完成两模块间的通信。

//定义了startVPN接口,React-Native将VPN的具体参数通过该接口传入到原生模块,开启指定的VPN
RCT_EXPORT_METHOD(startVPN:(NSDictionary*)config)
{
LSShadowSocksDataMode* mode = [[LSShadowSocksDataMode alloc] initWithDictionary:config];
[self.manager startVPN:mode];
}

除了传入数据外,通过可以通过这种方式从原生侧获取数据。最容易想到的是通过返回值获取,可惜的是RCT_EXPORT_METHOD宏不支持返回值,不过其提供了另外一种实现返回值的方式:

RCT_EXPORT_METHOD(isOpen:(RCTResponseSenderBlock)callback)
{
BOOL open = [self.manager status];
callback(@[[NSNull null], @[@(open)]]);
}

通过回调函数的形式实现返回值的效果,达到了数据交换的目的。

属性共享

这种方式主要针对于UI控件来说的。React-Native中最基础的UI类型是RCTRootView,该类有一个初始化方法initWithBridge:moduleName:initialProperties:,第三个参数initialProperties表示的是UI控件的初始属性值,类型为NSDictionary,其最终会被同步到由第二个参数定义的React-Native类的props中,即完成了两个模块间的数据交流。

NSArray *imageList = @[@"http://foo.com/bar1.png",
@"http://foo.com/bar2.png"];

NSDictionary *props = @{@"images" : imageList};

RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ImageBrowserApp"
initialProperties:props];
import React, { Component } from 'react';
import {
AppRegistry,
View,
Image,
} from 'react-native';

class ImageBrowserApp extends Component {
renderImage(imgURI) {
return (
<Image source={{uri: imgURI}} />
);
}
render() {
return (
<View>
{this.props.images.map(this.renderImage)}
</View>
);
}
}

AppRegistry.registerComponent('ImageBrowserApp', () => ImageBrowserApp);

初始化接口只能在UI组件建立时使用,如果需要在UI组件的生命周期内通信呢,RCTRootView提供了appProperties这样一种机制:

NSArray *imageList = @[@"http://foo.com/bar3.png",
@"http://foo.com/bar4.png"];
rootView.appProperties = @{@"images" : imageList};

通知

OC中使用NSNotificationCenter向整个应用发送通知,所有对该通知感兴趣的对象都会获得该通知并执行相应的动作。React-Native中也提供有类似的机制:RCTEventEmitter。原生模块继承该类后,就可以向React-Native侧发送通知,而React-Native就能够接收到该通知,并处理一并传送过来的数据了。

-(void)vpnStatusChanged:(NSNotification*)notification
{
NEVPNStatus status = [self.manager status];
NSString* value = nil;
switch (status) {
case NEVPNStatusReasserting:
value = @"重新连接中";
break;
case NEVPNStatusConnecting:
value = @"连接中";
break;
case NEVPNStatusConnected:
value = @"已连接";
break;
case NEVPNStatusDisconnecting:
value = @"断开连接中";
break;
case NEVPNStatusDisconnected:
case NEVPNStatusInvalid:
value = @"末连接";
break;
default:
break;
}
if(value){
[self sendEventWithName:@"VpnStatus" body:@{@"status":value}];
}
}

这里将V**的状态通过通知发送到React-Native侧,由React-Native将V**的状态显示的UI界面上。


转载自 https://cloud.tencent.com/developer/article/1930848

收起阅读 »

为抵制 7-Zip,列出 “三宗罪” ?网友:“第3个才是重点吧?”

谈及电脑必装软件有哪些时,压缩软件绝对算一个。由于各人需求不同,其选择的压缩软件也不尽相同,如 WinRAR、360 压缩、7-Zip、BandiZip、快压等,其中完全免费且开源的 7-Zip 就深受许多用户青睐。作为一款开源压缩软件,7-Zip 发布于 1...
继续阅读 »
谈及电脑必装软件有哪些时,压缩软件绝对算一个。由于各人需求不同,其选择的压缩软件也不尽相同,如 WinRAR、360 压缩、7-Zip、BandiZip、快压等,其中完全免费且开源的 7-Zip 就深受许多用户青睐。
作为一款开源压缩软件,7-Zip 发布于 1999 年,大多数源代码都基于 GNU LGPL 许可协议下发布,使用了 LZMA 与 LZMA2 算法使其拥有极高的压缩比,小巧的体积也是一大优势。

(图片来自7-Zip中文官网)
然而,近日一位名为 Paul 的开发者却发表了一篇呼吁抵制 7-Zip 的文章,其标题给出的理由是:“有限”的开源 & 安全问题


Paul 给 7-Zip 定下“三宗罪”

然而,在整体看过这篇文章后便可发现,Paul 给 7-Zip 定下的是“三宗罪”。
  • 第一宗罪:“有限”的开源

正如开头所说,大多 7-Zip 的源码均基于 GNU LGPL 许可协议发布,其开源属性理应毋庸置疑。
而 Paul 认为 7-Zip 开源“有限”的点在于:7-Zip 的代码没有托管在 Github、Gitlab 或其他任何公共代码托管平台上,只能在其官方 Sourceforge 页面的 src.7z 中找到,而且“没有历史、没有提交者、没有名字、没有文档,只是一个存档”。
关于这个唯一托管了 7-Zip 源码的 Sourceforge 平台,Paul 直言其声誉不好:“Sourceforge 曾被指控在 Windows.exe 文件和自解压文件中包含间谍软件和恶意软件。”
而对于“没有历史、没有提交者、没有名字、没有文档”这点,Paul 也揣测道,这可能是因为 7-Zip 的作者不希望开发者通过源码构建应用,有了提交历史将更容易跟踪任何更改和恢复任何错误的部分,同时也更容易运输一些“隐藏的黑暗元素”,如隐藏的遥测或后门。
  • 第二宗罪:存在安全问题

在 Paul 看来,7-Zip 不仅过去存在许多漏洞,此前曝出的提权漏洞 CVE-2022-29072 至今也仍未修复,明显存在安全隐患。Paul 还引用了 2012 年作者回应用户建议的言论:“现在没有时间做这些事情,也许以后我会看看。”
除此之外,Paul 还指出 7-Zip 的安装程序似乎从未设置签名——“签名可验证供应商并防止坏人安装软件”。
  • 第三宗罪:软件作者是俄罗斯开发者

抵制 7-Zip 的第三个理由 Paul 没有在标题中体现:7-Zip 是由俄罗斯开发者 Igor Pavlov 所开发的,“当前局势下为了声援乌克兰,最好不要使用俄罗斯软件”。
在举出以上“三宗罪”后,Paul 最后还推荐了一些 7-Zip 的替代品,如 PeaZip、NanaZip,还有与 7-Zip 相当的 Zstd(Zstandard)等。

网友:“一些阴谋论罢了”

Paul 这番抵制 7-Zip 的言论在 reddit 论坛上引起了不少讨论,但从评论情况来看,Paul 的目的没有达成:大多数人都认为 Paul 的理由站不住脚,并抨击 Paul 的“阴谋论”。
  
讨论帖中,点赞数最高的是一位名为 qvop 网友的评论:
即便 7-Zip 源码没有在 Github、Gitlab 等平台上托管,那又怎样?它仍然是开源的,没有任何规定要求开源就一定要在某些特定平台上托管代码,我看是 Paul 自己的认知有问题。
实际上,7-Zip 在 Sourceforge 上的源码是有一些(相对较少的)文档的,包括变更日志和关于如何编译程序及其一些内部工作的描述。而且,如果开发者只想单独开发、不想寻求贡献,那么这些不必需的东西开源也没用
Paul 认为 7-Zip 作者故意不让开发者通过源码构建应用的说法也几乎是“阴谋论”,因为目前没有任何证据支持这一说法,相反 7-Zip 方面有超过 20 年的开发和维护记录。
此外,因为 7-Zip 作者的国籍而放弃使用开源软件更是愚蠢至极,尤其目前没有任何迹象表明其作者有何相关冲突立场。
总而言之,对我来说,这篇文章就是一个大杂烩,其中还掺杂着一些权利和阴谋论。
除此之外,许多网友也对 Paul 发表的这篇博文予以讽刺:“一句话总结就是,发帖者不喜欢 7-Zip的作者名字”、“不为这篇文章辩护,作者就是个白痴”、“我会继续使用它的,谢谢,我没有看到任何停止使用它的理由”。
“开源无国界”一直是开源界所呼吁的口号,然而在当前国际形势下,这句口号似乎已有些站不住脚了:GitHub 封禁俄罗斯开发者账户、起家于俄罗斯的 NGINX 开源项目宣布禁俄……这些事件本就令众多开源爱好者对“开源”的本质提出质疑,Paul 呼吁抵制 7-Zip 的第三个理由更是令许多人无法理解:“难道说以后我们在选择使用开源软件时要考虑作者国籍吗?这真的很奇怪。”
那么,你对于 Paul 的言论有何看法?你平时常用的压缩软件又是什么呢?
参考链接:

整理 | 郑丽媛、出品 | CSDN(ID:CSDNnews)

收起阅读 »

清除 useEffect 副作用

web
在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。import { useState, useEffect } f...
继续阅读 »

在 React 组件中,我们会在 useEffect() 中执行方法,并返回一个函数用于清除它带来的副作用影响。以下是我们业务中的一个场景,该自定义 Hooks 用于每隔 2s 调用接口更新数据。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   const id = setInterval(async () => {
     const data = await fetchData();
     setList(list => list.concat(data));
  }, 2000);
   return () => clearInterval(id);
}, [fetchData]);

 return list;
}

🐚 问题

该方法的问题在于没有考虑到 fetchData() 方法的执行时间,如果它的执行时间超过 2s 的话,那就会造成轮询任务的堆积。而且后续也有需求把这个定时时间动态化,由服务端下发间隔时间,降低服务端压力。

所以这里我们可以考虑使用 setTimeout 来替换 setInterval。由于每次都是上一次请求完成之后再设置延迟时间,确保了他们不会堆积。以下是修改后的代码。

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   async function getList() {
     const data = await fetchData();
     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => clearTimeout(id);
}, [fetchData]);

 return list;
}

不过改成 setTimeout 之后会引来新的问题。由于下一次的 setTimeout 执行需要等待 fetchData() 完成之后才会执行。如果在 fetchData() 还没有结束的时候我们就卸载组件的话,此时 clearTimeout() 只能无意义的清除当前执行时的回调,fetchData() 后调用 getList() 创建的新的延迟回调还是会继续执行。

在线示例:CodeSandbox


可以看到在点击按钮隐藏组件之后,接口请求次数还是在继续增加着。那么要如何解决这个问题?以下提供了几种解决方案。

🌟如何解决

🐋 Promise Effect

该问题的原因是 Promise 执行过程中,无法取消后续还没有定义的 setTimeout() 导致的。所以最开始想到的就是我们不应该直接对 timeoutID 进行记录,而是应该向上记录整个逻辑的 Promise 对象。当 Promise 执行完成之后我们再清除 timeout,保证我们每次都能确切的清除掉任务。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let getListPromise;
   async function getList() {
     const data = await fetchData();
     setList((list) => list.concat(data));
     return setTimeout(() => {
       getListPromise = getList();
    }, 2000);
  }

   getListPromise = getList();
   return () => {
     getListPromise.then((id) => clearTimeout(id));
  };
}, [fetchData]);
 return list;
}

🐳 AbortController

上面的方案能比较好的解决问题,但是在组件卸载的时候 Promise 任务还在执行,会造成资源的浪费。其实我们换个思路想一下,Promise 异步请求对于组件来说应该也是副作用,也是需要”清除“的。只要清除了 Promise 任务,后续的流程自然不会执行,就不会有这个问题了。

清除 Promise 目前可以利用 AbortController 来实现,我们通过在卸载回调中执行 controller.abort() 方法,最终让代码走到 Reject 逻辑中,阻止了后续的代码执行。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

function fetchDataWithAbort({ fetchData, signal }) {
 if (signal.aborted) {
   return Promise.reject("aborted");
}
 return new Promise((resolve, reject) => {
   fetchData().then(resolve, reject);
   signal.addEventListener("aborted", () => {
     reject("aborted");
  });
});
}
function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   const controller = new AbortController();
   async function getList() {
     try {
       const data = await fetchDataWithAbort({ fetchData, signal: controller.signal });
       setList(list => list.concat(data));
       id = setTimeout(getList, 2000);
    } catch(e) {
       console.error(e);
    }
  }
   getList();
   return () => {
     clearTimeout(id);
     controller.abort();
  };
}, [fetchData]);

 return list;
}

🐬 状态标记

上面一种方案,我们的本质是让异步请求抛错,中断了后续代码的执行。那是不是我设置一个标记变量,标记是非卸载状态才执行后续的逻辑也可以呢?所以该方案应运而生。

定义了一个 unmounted 变量,如果在卸载回调中标记其为 true。在异步任务后判断如果 unmounted === true 的话就不走后续的逻辑来实现类似的效果。

在线示例:CodeSandbox

import { useState, useEffect } from 'react';

export function useFetchDataInterval(fetchData) {
 const [list, setList] = useState([]);
 useEffect(() => {
   let id;
   let unmounted;
   async function getList() {
     const data = await fetchData();
     if(unmounted) {
       return;
    }

     setList(list => list.concat(data));
     id = setTimeout(getList, 2000);
  }
   getList();
   return () => {
     unmounted = true;
     clearTimeout(id);
  }
}, [fetchData]);

 return list;
}

🎃 后记

问题的本质是一个长时间的异步任务在过程中的时候组件卸载后如何清除后续的副作用。

这个其实不仅仅局限在本文的 Case 中,我们大家平常经常写的在 useEffect 中请求接口,返回后更新 State 的逻辑也会存在类似的问题。

只是由于在一个已卸载组件中 setState 并没有什么效果,在用户层面无感知。而且 React 会帮助我们识别该场景,如果已卸载组件再做 setState 操作的话,会有 Warning 提示。


再加上一般异步请求都比较快,所以大家也不会注意到这个问题。

所以大家还有什么其他的解决方法解决这个问题吗?欢迎评论留言~


作者:lizheming
链接:juejin.cn/post/7057897311187238919

收起阅读 »

如何用一个插件解决 Serverless 灰度发布难题?

web
Serverless 灰度发布什么是 Serverless ?Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serv...
继续阅读 »

Serverless 灰度发布

什么是 Serverless ?

Serverless 顾名思义就是无服务器,它是一种“来了就用,功能齐全,用完即走”的全新计算提供方式,用户无需预制或管理服务器即可运行代码,只需将代码从部署在服务器上,转换到部署到各厂商 Serverless 平台上;同时享受 Serverless 按需付费,高弹性,低运维成本,事件驱动,降本提效等优势。

什么是 Serverless 灰度发布?

灰度发布又称为金丝雀发布( Canary Deployment )。过去,矿工们下矿井前,会先放一只金丝雀到井内,如果金丝雀在矿井内没有因缺氧、气体中毒而死亡后,矿工们才会下井工作,可以说金丝雀保护了工人们的生命。

与此类似,在软件开发过程中,也有一只金丝雀,也就是灰度发布(Gray release):开发者会先将新开发的功能对部分用户开放,当新功能在这部分用户中能够平稳运行并且反馈正面后,才会把新功能开放给所有用户。金丝雀发布就是从不发布,然后逐渐过渡到正式发布的一个过程。

那么对于部署在 Serverless 平台上的函数应该怎么进行灰度发布呢?

下文将以阿里云函数计算 FC 为例,为各位展开介绍。

灰度发布有一个流程,两种方式。

一个流程

Serverless 灰度发布是通过配置别名来实现的,别名可以配置灰度版本和主版本的流量比例,在调用函数时使用配置好的别名即可将流量按比例发送到相应版本。


配置灰度发布的流程如下:

  1. Service 中发布一个新版本。

  2. 创建或更新别名,配置别名关联新版本和稳定版本,新版本即为灰度版本。

  3. 将触发器 ( Trigger ) 关联到别名。

  4. 将自定义域名 ( Custom Domain ) 关联到别名。

  5. 在调用函数中使用别名,流量会按配置比例发送到新版本和稳定版本。

传统做法的两种方式

1、阿里云控制台 web 界面:

a.发布版本


b.创建别名


c.关联触发器


d.关联自定义域名


2、使用 Serverless Devs cli

a.发布版本

s cli fc version publish --region cn-hangzhou --service-name fc-deploy-service --description "test publish version"

b.创建别名并设置灰度

s cli fc alias publish --region cn-hangzhou --service-name fc-deploy-service --alias-name pre --version-id 1 --gversion 3 --weight 20

c.关联触发器

需要到控制台配置

d.关联自定义域名

需要到控制台配置可以看到,使用控制台或 Serverless Devs 进行灰度发布流程中的每一步,都需要用户亲自操作。并且由于配置繁多,极易出错。除了这些弊端以外,客户困扰的另一个问题是使用灰度发布策略非常不方便。

常见的灰度发布策略有 5 种:

    1. CanaryStep: 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

    2. LinearStep:分批发布,每批固定流量,间隔指定时间后再开始下一个批次。

    3. CanaryPlans:自定义灰度批次,每批次设定灰度流量和间隔时间,间隔指定时间后按照设定的流量进行灰度。

    4. CanaryWeight:手动灰度,直接对灰度版本设置对应的权重。

    5. FullWeight: 全量发布,全量发布到某一版本。

这些灰度策略中,前三项都需要配置间隔时间,而用户在控制台或者使用 Serverless Devs 工具去配置灰度都没有办法通过自动程序来配置间隔时间,不得不通过闹钟等方式提醒用户手动进行下一步灰度流程,这个体验是非常不友好的。下面我们介绍个能够帮您一键灰度发布函数的插件:FC-Canary 。

基于 Serverless Devs 插件 FC-Canary 的灰度发布

为了应对以上问题,基于 Serverless Devs 的插件 FC-Canary 应运而生,该插件可以帮助您通过 Serverless-Devs 工具和 FC 组件实现函数的灰度发布能力,有效解决灰度发布时参数配置繁杂、需要开发人员亲自操作以及可用策略少等问题。


(内容配置及注意事项-部分截图)

详细流程请见:github.com/devsapp/fc-…

FC-Canary 的优势

1、FC-Canary 支持超简配置

用户最短只需在 s.yaml 中增加 5 行代码代码即可开启灰度发布功能。


2、FC-Canary 配置指引简单清晰:


3、FC-Canary 支持多种灰度策略

  • 灰度发布,先灰度指定流量,间隔指定时间后再灰度剩余流量。

此时流量变化为:20%流量到新版本,10 分钟后 100%流量到新版本


  • 手动灰度,指定时直接将灰度版本设置对应的权重。

此时为 10%流量到新版本,90%到稳定版本


  • 自定义灰度,以数组的方式配置灰度变化。

此时流量变化为:10%到新版本 -> (5 分钟后) 30% 流量到新版本 -> (10 分钟后) 100% 流量到新版本


  • 分批发布,不断累加灰度比例直到 100%流量到新版本。

流量变化:40%到新版本 -> (10 分钟后) 80%流量到新版本 -> (再 10 分钟后) 100%流量到新版本


  • 全量发布,100%流量发到新版本


FC-Canary 插件支持上述 5 种灰度策略,用户选择所需策略并进行简单配置,即可体验一键灰度发布。

4、FC-Canary 灰度阶段提示清晰


插件对每一个里程碑都会以 log 的方式展现出来,给开发者足够的信心。

5、FC-Canary 支持钉钉群组机器人提醒


配置钉钉机器人即可在群中收到相关提醒,例如:


FC-Canary 最佳实践

使用 FC-Canary 插件灰度发布 nodejs 12 的函数。

代码仓库:

github.com/devsapp/fc-…

初始化配置

  • 代码配置


  • yaml 配置


我们采用 canaryWeight 的灰度策略:灰度发布后,50%的流量到新版本,50%的流量到旧版本。

进行第一次发布

  1. 执行发布

在 terminal 中输入: s deploy --use-local

  1. 查看结果

命令行输出的 log 中可以看到:


由于是第一次发布,项目中不存在历史版本,所以即使配置了灰度发布策略 ,FC-Canary 插件也会进行全量发布,即流量都发送到版本 1。

修改代码,第二次发布

  1. 在第二次发布前,我们修改一下代码,让代码抛出错误。


  1. 执行发布

在terminal中输入: s deploy --use-local

  1. 结果

命令行输出 log 中可以看到:


第二次发布,应用了灰度发布策略,即 50%流量发送到版本 1, 50%的流量发送到版本 2。

测试

获取 log 中输出的 domain,访问 domain 100 次后查看控制台监控大盘。


可以看到调用了函数 100 次,错误的函数有 49 次,正确的函数有 100 - 49 = 51 次,正确和错误的函数都约占总调用数的 50%。

分析:函数版本 1 为正确函数,函数版本 2 为错误函数,我们的灰度配置为流量 50% 到版本 1,50% 到版本 2,所以调用过程中,错误函数和正确函数应该各占 50%,图中结果符合我们的假设,这证明我们的灰度策略是成功的。

总结

我们可以发现相比使用控制台进行灰度发布,使用 FC-Canary 插件免去了用户手动创建版本、发布别名、关联触发器和管理自定义域名的麻烦,使用起来非常方便。

引申阅读

Serverless Devs 组件和插件的关系

  • 组件是什么?

根据 Serverless Devs Model v0.0.1 中说明, 组件 Component: 是由 Package developer 开发并发布的符合 Serverless Package Model 规范的一段代码,通常这段代码会在应用中被引用,并在 Serverless Devs 开发者工具中被加载,并按照预定的规则进行执行某些动作。例如,将用户的代码部署到 Serverless 平台;将 Serverless 应用进行构建和打包;对 Serverless 应用进行调试等。

举个例子:

如果想要使用 Serverless Devs 管理阿里云函数计算的函数计算资源,则需要在 yaml 配置文件中声明阿里云 FC 组件,之后便可以使用阿里云 FC 组件的能力。

FC 组件可以提供管理阿里云函数计算资源的能力,包括:管理服务、函数、版本、别名 等功能。组件地址:github.com/devsapp/fc

  • 插件是什么?

插件作为组件的补充,提供组件的原子性功能。

举个例子:

  1. 使用 FC 组件 deploy 的功能部署函数,可以在部署结束后采用 FC-Canary 插件对部署的函数进行灰度发布。

  2. 使用 FC 组件 deploy 的功能部署函数,可以在部署开始前采用 layer-fc 插件来降低部署过程中上传代码的耗时:即 layer-fc 可以让函数直接使用公共依赖库(远程)中的依赖,从而在部署时不再需要上传这些远程存在的依赖。

  • 组件和插件的关系?


  • 在 Serverless Devs Model 中,组件是占据核心地位,插件是辅助地位,也就是说,插件的目的是提升组件能力,提供给组件一些可选的原子性功能。

  • Serverless Devs 管理组件和插件的生命周期,如果是 pre 插件,则会让其在组件执行前执行,反之,post 插件则会在组件后完成一些收尾工作。

  • 一个组件可以同时使用多个插件, 其中组件插件的执行顺序是:

    1. 插件按照 yaml 顺序执行, 前一个插件的执行结果为后一个插件的入参

    2. 最后一个 pre 插件的输出作为组件的入参

    3. 组件的输出作为第一个 post 插件的入参

相关概念

  • FC 函数 (Function) 是系统调度和运行的单位,由函数代码和函数配置构成。FC 函数必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作,请参见 管理函数。函数计算支持事件函数和 HTTP 函数两种函数类型,关于二者的区别,请参见 函数类型。

  • 服务 (Service) 可以和微服务对标 ( 有版本和别名 ),多个函数可以共同组成服务单元。创建函数前必须先创建服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。

  • 触发器 (Trigger) 的作用是触发函数执行的。函数计算提供了一种事件驱动的计算模型。函数的执行可以通过函数计算控制台或 SDK 触发,也可以由其他一些事件源来触发。您可以在指定函数中创建触发器,该触发器描述了一组规则,当某个事件满足这些规则,事件源就会触发关联的函数。

  • 自定义域名(Custom Domain) 是函数计算提供为 Web 应用绑定域名的能力。

  • 版本 (Version) 是服务的快照,包括服务的配置、服务内的函数代码及函数配置,不包括触发器,当发布版本时,函数计算会为服务生成快照,并自动分配一个版本号与其关联,以供后续使用。

  • 别名 (Alias) 结合版本,帮助函数计算实现软件开发生命周期中的持续集成和发布。

最后,欢迎大家一起来贡献更多的开源插件!

参考链接:

Serverless Devs:

github.com/Serverless-…

FC 组件地址:

github.com/devsapp/fc

FC-Canary 插件具体信息及其使用请参考:

github.com/devsapp/fc-…

FC 函数管理:

help.aliyun.com/document_de…

FC 函数类型:

help.aliyun.com/document_de…


作者:长淇
来源:https://juejin.cn/post/7116556273662820382

收起阅读 »

Java VS .NET:Java与.NET的特点对比

为什么要写Java跟.NET对比?二、项目构建工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。平台工具ken.io的解释JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDKJavaJDKJRE(Java项目运行环境)...
继续阅读 »

一、前言

为什么要写Java跟.NET对比?

.NET出生之后就带着Java的影子。从模仿到创新,.NET平台也越来越成熟。他们不同的支持者也经常因为孰弱孰强的问题争论不休。但是本文并不是为了一分高下。而是针对Java平台跟.NET平台做一些对比。主要围绕项目构建、Web框架、项目部署展开讨论。相信经过这些讨论可以让Java/.NET工程师对Java平台、.NET平台有更好的了解。

二、项目构建

项目构建工具

工欲善其事必先利其器。开发环境配置+工具使用当然要先讲了。

1、表面上的工具

平台工具ken.io的解释

.NETVisual Studio微软官方IDE,它具备了开发.NET应用程序的几乎所有工具

JavaIdea/EclipseIDE,负责管理项目以及代码的运行调试等,依赖于JDK

JavaMaven负责管理项目模板、打包(jar包等),依赖于JDK

JavaJDKJRE(Java项目运行环境),Java工具(编译器等)

.NET工程师要开展工作,安装Visual Studio(后面简称:VS)就可以进行开发了。但是Java开发,只安装IDE是不行的,就算某些IDE会自动安装JDK,甚至是Maven,但是这些还是需要自己配置,不然还可能会踩坑。从开发环境的配置来说,.NET工程师操作上确实简单一些,一直下一步,等待安装完成即可。Java工程师就先要了解下工具,以及各个工具的职责。然后逐一配置。

从这个点上来说,Java的入门曲线会稍陡一些,但是Java工程师也会比.NET工程师更早关注到项目构建的重要环节。

2、实际上的工具

职责.NET平台Java平台ken.io的解释

项目管理VSIDEA/Eclips.NET只有微软官方IDE,Java没有官方的IDE,没有VS好用,但是有多个选择

项目模板VS+MSBuildIDE+Maven.NET项目的模板是VS自带的,是直接符合MSBuild(编译器)标准的,项目由sln+csproj文件组织,Java平台编译器的标准是公开的,目前主流项目都是基于Maven模板来创建,项目由pom.xml文件组织。

编译&调试VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织,但是编译器并不是认识pom.xml,所以编译需要Maven的参与

Package管理NuGetMavenNuget是微软官方开源的VS插件,Maven是Apache下的开源项目。ken.io觉得Maven更灵活、强大。NuGet容易上手。

打包/发布VS+MSBuild+SDKIDE+Maven+SDK.NET平台的编译器是独立的,Java平台的编译器是集成在JDK中,Maven模板的项目是由pom.xml文件组织的,但是编译器并不是认识pom.xml,所以打包需要Maven的参与。IDE主要是提供图形化界面替代命令操作

从项目管理上说。VS这个IDE更好用一些,项目模板上,.NET项目模板由于有Visual Studio的存在,可以说简单易用而且丰富,Java平台的Maven模板灵活。

其实大部分差异都是编译器跟模板带来的差异。.NET平台的编译器是独立的,编译器MSBuild有一套标准, 而且Visual Studio提供了丰富好用的项目模板。

Java平台的编译器的编译配置是xml文档,由于Java官方没有项目模板,IDE只负责帮你组织项目,但是并没有模板,你可以将任意目录指定为SourceRoot(代码根目录),ResourceRoot(资源文件根目录:比如配置文件)也可以任意指定,编译的时候,IDE会将你的项目代码,以及编译器所需要的编译描述/配置xml文档告诉编译器该如何编译你的项目。确实非常灵活,但是也增加了项目管理的成本。包的管理也非常麻烦,还好有Maven结束了这个混沌的Java世界。

编码特点

—.NETJava

类的组织namespace:命名空间,name跟目录无关Package:name跟目录名一致

类.cs文件:类名跟文件名无关.java文件,类名跟文件名无关,但一个类文件只能定义一个public类

编译产出.dll,.exe文件.jar,.war文件

三、框架

.NET的Web框架基本上都是微软官方的,官方的框架也最为流行,而Java平台,除了官方提供的Servlet API(相当于.NET的System.Web)其他的基本都由Spring大家族统治了。本次我们主要对比目前Web开发最常用的MVC框架以及持久层框架

功能.NETJavaken.io的说明

Web核心ASP.NETServlet—

Web框架ASP.NET MVCSpring MVCASP.NET MVC是微软官方框架,Srping MVC框架隶属于Spring大家族,依赖于Spring

视图引擎RazorThymeleaf/FreeMarkerRazor是微软官方的视图引擎,非常好用,Spring MVC并没有视图引擎,但是有Thymeleaf,FreeMarker。ken.io更喜欢Razor的风格

持久层Entity FrameworkMyBatisEF是微软官方的持久层框架,易上手、开发效率高、但侵入性强。MyBatis配置灵活,无侵入性。各有利弊。

.NET平台的框架由于都是微软官方的,比较好组织,上手容易。Java平台的框架,灵活可配置。这也是Java平台一贯的风格。但是ken.io不得不吐槽的是,Spring MVC作为一个MVC框架,竟然没有自己的视图引擎,那MVC种的View去哪了?

可能是因为Java作为Web后端的主力平台,确实很少关注视图层,但是Spring MVC没有View层引擎,还是感觉不合适。Thymeleaf跟FreeMarker,ken.io更推荐FreeMarker。因为ken.io更喜欢FreeMaker的语法。可能是用惯了Razor的缘故。

四、项目部署

对于项目部署。.NET平台貌似没得选,只能选Windows+IIS,虽然有Mono,但毕竟不是支持所有的类库。而Java平台既可以选择Windows+Tomcat,也可以选择Linux+Tomcat。但是通常会选择Linux+Tomcat毕竟成本低。

职责.NETJava

操作系统Windows ServerWindows Server、Linux Server

Web服务器IISTomcat(Tomcat是目前最主流的,也有其他的Servlet容易例如:JBoss)

不过Java平台的特性,Java项目的部署会比.NET项目部署偏麻烦一些。

IIS图形化界面一直下一步,再调整下应用程序池的版本就行了。而Tomcat不论是在Windows,还是在Linux,都通过修改配置文件完成站点配置


转载自:https://cloud.tencent.com/developer/article/1926747

收起阅读 »

ASP.NET MVC 与 ASP.NET Web Form 的介绍与区别

是微软提供的以MVC模式为基础的ASP.NET Web应用程序开发框架。Model:领域模型 处理应用程序数据逻辑部分,获取数据,处理数据Controller:控制器 通过Model 读取处理数据,通过View 将结果返回。在 ASP.NET 框架下的一种基于...
继续阅读 »

1 ASP.NET MVC

是微软提供的以MVC模式为基础的ASP.NET Web应用程序开发框架。

MVC 模式分别为:

Model:领域模型 处理应用程序数据逻辑部分,获取数据,处理数据

View:视图 用于处理实际返回给用户的页面

Controller:控制器 通过Model 读取处理数据,通过View 将结果返回。

2 ASP.NET Webform

在 ASP.NET 框架下的一种基于事件模型的开发模式,有开发速度快,容易上手等特点。

3 两者的区别和各自优缺点

ASP.NET 作为微软的Web程序开发框架,MVC与Webform 是不同时期的开发模式,

在ASP.NET 运行处理原理 基本一致.

Webform 优点:可以基于事件模型开发,类似Winform中,所有请求使用ViewState和页面生命周期来维持控件状态,同时控件的开发,加快了开发速度,整体Webform的内部封装比较高。

Webform 缺点: 正是由于封装程度高,Webform非常难扩展,开发人员便利了解内部运行原理,不容易被测试。同时控件的ViewState 增加了网站服务器的传输量,一定程度上影响程序的效率。

MVC 优点 :易于扩展,易于单元测试,易于测试驱动开发。MVC中的一个路由的存在,可以做一些链接伪静态的处理。

总结: MVC 不是取代了Webform,两者适用于不同的开发环境下,都是简单三层中的表示层的开发框架,都是ASP.NET 框架下的开发模式。

1 页面处理流程:
MCV的页面处理流程依旧在ASP.NET原有上有扩展,MVC通过特定的IHttpModule和IHttpHandler 来处理请求,与Webform不同的,Webform中每个aspx页面都会有是一个IHttphandler实例。MVC中 Controller都比是IHttpHandler的子类实例,Action是在MvcHandler中通过MVC的工厂反射执行的,MvcHandler可以自定义。

2 上下文 请求对象: Context Session Request Response Cookie 基本一致

3 配置文件基本一致,但不通用

4 部分服务器控件并不是不可以在MVC中使用

5 在ASP.NET MVC中,包括Membership,healthMonitoring,httpModule,trace在内的内置和自定义的组件模块仍然是继续可用。

附图 :MVC 原理图和介绍

123123123.jpg

1 客户端发出请求给IIS(mvc中为集成模式),执行HttpRunTime的ProcessRequest方法

2 创建了一批MvcApplication对象,存放在应用程序池中,执行第一个MvcApplication对象实例中的 Application的Application_Start()方法、

RouteConfig.RegisterRountes(RouteTable.Routes)-->向路由规则集合注册一条默认的路由规则

3 调用Application对象实例的ProcessRequest方法 ,传入上下文对象HttpContext,开始执行19个管道事件

4 第七个管道事件:

UrlRouting过滤器:

1 获取当前Reuqest对象中的RawUrl:此时 /Home/Index

2 去扫描当前路由规则集合中的所,从上而下开始匹配,匹配成功了--{controller}/{action}/{id}这条路由规则,MVC底层就会根据路由规则解析出控制器名称

HomeController action:Index

3 调用DefaultControllerFactory反射创建控制器类的对象实例,存入RemapHandler中

4 将 控制器和action名称以字符串的形式存入RouteData中

5 第八个管道事件

1 判断当前RemapHandler是否为null, 不为null直接跳过执行后面的管道事件

2null继续创建页面类对象

6 第十一,十二个管道事件

1 获取上下文的RemapHandler中的控制器类的对象实例

2从RoutData中取出当前请求action名称

3 以反射的方式动态执行action方法

4 action返回类型分为:

4.1 如果是一个视图类型:调用具体的视图(.cshtml)编译成页面类,在调用页面类的Excute()方法,将所有的代码执行后写入到Response中

4.2如果是一个非视图类型,直接将结果写入到Response中即可
收起阅读 »

NodeJS 入门了解

1 NodeJS 是什么NodeJS 是 javascript 的一种运行环境,是对 Google V8 引擎进行的封装。是一个服务器端的 javascript 解释器;NodeJS 使用事件驱动,非阻塞 I/O 模型;什么是非阻塞 I/O 模型:阻塞:I/O...
继续阅读 »

1 NodeJS 是什么

  • NodeJS 是 javascript 的一种运行环境,是对 Google V8 引擎进行的封装。是一个服务器端的 javascript 解释器;
  • NodeJS 使用事件驱动,非阻塞 I/O 模型;

什么是非阻塞 I/O 模型:

  • 阻塞:I/O 时进程休眠等待 I/O 完成后再进行下一步;
  • 非阻塞 I/O :I/O 时函数立即返回,进程不等待 I/O 完成;

什么是事件驱动:
I/O 等异步操作结束后的通知。

2 NodeJS 和 npm 的关系

包含关系,NodeJS 中含有 npm,比如说你安装好 NodeJS,你打开 cmd 输入 npm -v 会发现出 npm 的版本号,说明 npm 已经安装好。

引用大神的总结:
其实 npm 是 NodeJS 的包管理器(package manager)。我们在 NodeJS 上开发时,会用到很多别人已经写好的 javascript 代码,如果每当我们需要别人的代码时,都根据名字搜索一下,下载源码,解压,再使用,会非常麻烦。

于是就出现了包管理器 npm。大家把自己写好的源码上传到 npm 官网上,如果要用某个或某些个,直接通过 npm 安装就可以了,不用管那个源码在哪里。并且如果我们要使用模块 A,而模块 A 又依赖模块 B,模块 B 又依赖模块 C 和 D,此时 npm 会根据依赖关系,把所有依赖的包都下载下来并且管理起来。试想如果这些工作全靠我们自己去完成会多么麻烦!

3 NodeJS 的安装

直接网上下载安装就可以了。环境配置,其实就是在 path,加入 NodeJS 的安装目录,这样就可以在控制台使用 NodeJS 的命令。验证,可以在控制台输入:node -vnpm -v


4 初始化 npm 环境

首先保证有 node 和 npm 环境,运行 node -vnpm -v 查看

进入项目目录,运行 npm init 按照步骤填写最终生成 package.json 文件,所有使用 npm 做依赖管理的项目,根目录下都会有一个这个文件,该文件描述了项目的基本信息以及一些第三方依赖项(插件)。详细的使用说明可查阅官网文档

5 安装插件

已知我们将使用 webpack 作为构建工具,那么就需要安装相应插件,运行 npm install webpack webpack-dev-server --save-dev 来安装两个插件。

又已知我们将使用 React ,也需要安装相应插件,运行 npm i react react-dom --save 来安装两个插件。其中 iinstall 的简写形式。

安装完成之后,查看 package.json 可看到多了 devDependenciesdependencies 两项,根目录也多了一个 node_modules 文件夹。

6 --save--save-dev 的区别

npm i 时使用 --save--save-dev,可分别将依赖(插件)记录到 package.json 中的 dependenciesdevDependencies 下面。

dependencies 下记录的是项目在运行时必须依赖的插件,常见的例如 reactjquery 等,即及时项目打包好了、上线了,这些也是需要用的,否则程序无法正常执行。

devDependencies 下记录的是项目在开发过程中使用的插件,例如这里我们开发过程中需要使用 webpack 打包,而我在工作中使用 fis3 打包,但是一旦项目打包发布、上线了之后,webpackfis3 就都没有用了,可卸磨杀驴。

延伸一下,我们的项目有 package.json,其他我们用的项目如 webpack 也有 package.json,见 ./node_modules/webpack/package.json,其中也有 devDependenciesdependencies。当我们使用 npm i webpack 时,./node_modules/webpack/package.json 中的dependencies 会被 npm 安装上,而 devDependencies 也没必要安装。

参考:http://www.imooc.com/article/14499

7 CommonJS

CommonJS 是 node 的模块管理规范

  • 每个文件都是一个模块,有自己的作用域;
  • 在模块内部 module 变量代表模块本身;
  • module.exports 属性代表模块对外接口;

require 规则

  • / 表示绝对路径,./ 表示相对路径;
  • 支持 js、json、node 扩展名,不写依次尝试;
  • 不写路径则认为是 build-in 模块或者各级 node_modules 内的第三方模块

require 特性

  • module 被加载的时候执行,加载后缓存;
  • 一旦出现某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出

8 global

node 全局对象 global,相当于 web 的 window 对象。

  • CommonJS
  • Buffer、process、console
  • timer
收起阅读 »

nodeJS操纵数据库

下载nodeJS,安装另外一种安装我们node的方式 使用nvm这个软件来安装 node version manger,如果你想同时安装多个node版本 教程:http://www.jianshu.com/p/07c3456e875a2、使用上面装好的n...
继续阅读 »

Node.exe的安装

下载nodeJS,安装

检测是否安装成功 node -v

另外一种安装我们node的方式
使用nvm这个软件来安装
node version manger,如果你想同时安装多个node版本
教程:http://www.jianshu.com/p/07c3456e875a

步骤:
1、安装nvm这个软件: https://github.com/coreybutler/nvm-windows/releases

2、使用上面装好的nvm软件,安装我们需要的node版本了
指令:
nvm install 具体的版本号就行了
nvm uninstall 具体的版本号
nvm list 查看当前安装了哪些版本
nvm use 具体版本号,切换到某个版本

建议:
安装一个高一点的稳定的版本即可,因为软件都是向下兼容

系统环境变量及其作用

系统环境变量

每个系统都会提供一种叫做环境变量的东西,用来简化我们去
访问某一个应用程序可执行文件(.exe)的操作

我们配置了环境变量能做到什么事呢?
在我们终端的任何一个目录下,都可以访问,配置在系统
环境变量里面的可执行文件

如何将一个软件的可执行文件配置在我们的系统环境变量中?
步骤:
1、拷贝一个可执行文件所在的目录,比如:
node.exe所在的目录 C:\Program Files\nodejs

2、系统 > 高级系统设置 > 高级 > 环境变量 >
系统变量 > Path > 填写上你的目录

注意事项:
如果更改了系统的环境变量,就必须把终端重新启动

启动node.exe执行js代码

启动(相当于启动Apache服务器)

1、在我们的node的安装目录下,去双击我们node.exe

2、在终端输入 node即可 node.exe

退出我们的node.exe

1、在终端中输入.exit
2、连续按住两次 CTRL + C

怎么去执行js代码

1、直接在我们启动的node.exe中写代码(在开启的REPL环境中写代码执行)
缺点:
书写不方便,阅读起来也不方便
因为在我们的cmd中写的代码,是放在内存中的,
一旦我们退出了node.exe,原先写的代码都没有了

2、把我们写好的代码放在一个单独的js文件中去执行

在终端中输入 node.exe +执行的文件名称

注意:
1、我们js代码不是在终端中运行的,只是借助终端
去启动我们node.exe,并且最终将结果展现在终端里面而已

2、在运行时候,首先你的终端的目录得切换到你要
执行的文件的目录下面去,然后使用node 文件名称执行即可
我们nodejs的代码是在一个叫做REPL环境中,执行的

REPL

JS的执行

执行js在浏览器端,我是是要依靠浏览器(js的解析引擎)

在服务器端 nodejs开启的REPL环境

官网的解释:
参考:http://shouce.qdfuns.com/nodejs/repl.html

REPL就是当通过node.exe启动之后开辟的一块内存空间,
在这块内容空间里面就可以解释执行我们的js代码

例如:
在终端中输入了 node abc.js 做的事情就是,将abc.js中
写好的js的逻辑代码扔在启动好的node的内容空间中去运行,
我们把启动好的node的这块内存空间称之为REPL环境

模块化思想

为什么前端需要有模块化

1、解决全局变量名污染的问题
2、把相同功能的代码放在一个模块(一个js文件中)方便后期维护
3、便于复用

NodeJS中如何体现模块化

1、Node本身是基于CommonJS规范,
参考:http://javascript.ruanyifeng.com/nodejs/module.html#toc0

2、Node作者在设计这门语言的时候,就严格按照CommonJS
的规范,将它的API设计成模块化了,比如它将开启Web服务这
个功能所有代码都放入一个http模块中

3、Node本质来说就是将相同功能的代码放入到一个.js文件中管理

常用NodeJS中的模块

模块              作用
http 开启一个Web服务,给浏览器提供服务
url 给浏览器发送请求用,还可以传递参数(GET)
querystring 处理浏览器通过GET/POST发送过来的参数
path 查找文件的路径
fs 在服务器端读取文件用的

上面五大核心模块加上其它一些第三方的模块,就可以完成基本的数据库操作了

nodeJS核心模块及其操作

http

使用http模块开启web服务
步骤:
//1、导入我们需要的核心模块(NodeJS提供的模块我们称之为核心模块)
var http = require('http');

//2、利用获取到的核心模块的对象,创建一个server对象
var server = http.createServer();

//3、利用server对象监听浏览器的请求,并且处理(请求-处理-响应)
server.on('request',function(req,res){
res.end("welcome");
});

//4、开启web服务开始监听
server.listen(8080,'127.0.0.1',function(){
console.log('开启服务器成功');
});

url

1、导入url这个核心模块

2、调用url.parse(url字符串,true),如果是true的话代表把我们
的username=zhangsan&pwd=123 字符串解析成js对象

 // 使用url模块获取url中的一些相关信息
const url = require('url')
var testURL = http://127.0.0.1:8899/login?username=zhangsan&pwd=123
console.log(url.parse(testURL,true))//{username:zhangsan,pwd:123}

QueryString

作用:
将GET/POST传递过来的参数,进行解析
GET : ?username=zhangsan&pwd=123
POST : username=zhangsan&pwd=123

使用:
const querystring = require('querystring')

const paramsObj = querystring.parse(键值对的字符串)

GET&POST

相同点:
都是HTTP协议的方法
都能传递参数给服务器

不同点:
1、传参的方式不一样
GET 放在路径后面 ?开始,后面键值对
POST 放在请求体 键值对的方式

2、传参的限制不一样
GET 2048B
POST 2M

3、GET有缓存,POST没有

4、GET传参不安全,POST相对安全

建议:
如果只是单纯的获取数据,就用GET,因为GET有缓存效率高

如果是要向服务器提交数据,就用POST

fs&path

path

作用:获取路径

path.join(__dirname,'你要读取的文件夹下面的文件名称即可')

__dirname全局属性,代表当前文件所在的文件夹路径

path.join会自动判断文件的路径,并且给他加上`/`

fs

作用:读取服务器硬盘上面的某一个文件(操作文件)

fs.readFile : 异步读取服务器硬盘上面的某一个文件
fs:node去读取服务器硬盘中的文件(操作文件)

path:获取文件的路径

上面两个基本上配合起来用

自定义模块

CommonJS规范认为,一个.js文件就可以看成一个模块,如果我们想把模块中定义的变量,方法,对象给外面的js使用,就必须使用CommonJS提供module将我们需要给外面用的东西,导出去

注意点

在commonjs中导入模块用 require
在commonjs中在模块中导出 使用module.exports
如果是自定义模块,在导入自定义模块的时候,得把路径写完整
require导入的东西,就是别的文件modulu.exports导出的东西

Express 框架

基本概念

它是对HTTP封装,用来简化我们网络功能那一块

官网:http://www.expressjs.com.cn/ 官方解释:
基于 Node.js 平台,快速、开放、极简的 web 开发框架。

重点

1、如何去接收GET/POST传递过来的参数
2、如何通过Express进行分门别类的处理路由
3、静态资源的处理

使用

1、Hello World 案例

步骤:
1、导入包
2、创建一个app
3、请求处理响应
4、开启web服务,开始监听

2、获取GET/POST参数
GET参数:登录 http://127.0.0.1:3000/login?username=zhangsan&pwd=123

可以直接在我们的req.query中就可以获取了

POST参数:因为express没有直接提供获取POST参数的方法,需要借助一个第三方包 body-parser
参考: https://www.npmjs.com/package/body-parser

步骤:
1、npm install body-parser --save
2、导包
3、实现某些方法

最后通过req.body即可以获取到post提交过来的参数

路由处理

前端路由:
作用:当触发了某个超链接之后,根据路由的配置,决定
跳转到哪个页面,最终将这个页面呈现出来

后台的路由
作用:就是用来分门别类的出路用户发送过来的请求

    http://127.0.0.1:3000/login
http://127.0.0.1:3000/register

http://127.0.0.1:3000/getGoodsList
http://127.0.0.1:3000/getGoodsInfo

jd购物
男士:(专门创建一个man.js文件来实现男士区域商品的请求)
http://www.jd.com/man/xz
http://www.jd.com/man/ld
http://www.jd.com/man/px

女士:(专门创建一个girl.js文件来实现女士区域商品的请求)
http://www.jd.com/girl/xs
http://www.jd.com/girl/bag
http://www.jd.com/girl/kh

express中代码实现?

步骤:
1、先要创建一个单独的路由(js文件),来处理某一类
请求下面的所有用户请求,并且需要导出去
1.1 导入包 express
1.2 创建一个路由对象
const manRouter = express.Router()
1.3 在具体的路由js中处理属于我们该文件的路由
manRouter.get(xxx)
manRouter.post(xxx)
1.4 将上面创建的路由对象导出去,在入口文件中使用

2、在入口文件中,导入我们的路由文件,并且使用就可以了

//导入路由文件
const manRouter = require(path.join(__dirname,"man/manRouter.js"))
//在入口文件中使用
app.use('/man',manRouter)
```

## Express中静态资源的处理
Express希望对我们后台静态资源处理,达到简单的目的,
然后只希望我们程序员写一句话就能搞定

步骤:
1、在我们入口文件中设置静态资源的根目录
注意点:一定要在路由处理之前设置

app.use(express.static(path.join(__dirname,'statics')))
```

2、在我们的页面中,按照我们Express的规则来请求后台
静态资源数据
写link的href,script的src写的时候,除开静态资源根
路径之外,按照他在服务器上面的路径规则写

mongodb数据库

数据库

保存数据的仓库,数据库本质也是一个文件,只是说和普通的
文件不太一样,他有自己的存储规则,让我们保存数据和查询
数据更加方便

存储文件的介质

localStorage 文本文件
大型数据或是海量数据的时候必须要用到数据库

数据库的分类

客户端:
iOS/Android/前端
iOS/Android SQLite 在iOS/Android存储App的数据

服务端:
关系型数据库
部门---员工
mysql
sqlserver
oracle

非关系型数据库
JSON对象的形式来存储

MongoDB : 简单,你会js、JSON就能操作 Redis Memcached

数据库的作用

1、保存应用程序产生的数据(用户注册数据,用户的个人信息等等)
2、当应用程序需要数据的时候,提供给应用程序去展示

安装mongodb服务端

步骤:
1、安装mongodb服务端软件
2、设置mongodb的环境变量,重启终端验证 mongo -version
3、建立一个文件夹,用来存储mongodb数据库产生的数
据(建议放在C盘根目录 mongodb_datas)
4、启动
mongod --dbpath c:/mongodb_datas

启动服务端有几种方式

1、方式一,直接在cmd中输入 mongod --dbpath c:/mongodb_datas
32位: mongod --dbpath c:/mongodb_datas --journal --storageEngine=mmapv1

2、方式二,可以把mongod --dbpath c:/mongodb_datas做成一个批处理文件
32位: mongod --dbpath c:/mongodb_datas --journal --storageEngine=mmapv1

使用robomongo这个小机器人来操作我们的数据库中的数据

步骤:
1、连接到我们mongodb数据库服务端,并且连接成功之
后,服务端会给我们返回一个操作数据库的db对象

2、拿着上一步返回的db对象,对mongodb数据库中的数据进行操作了

连接成功之后,我们要来操作数据的话
1、创建一个数据库 (相当于在excel中创建空白工作簿)
2、创建集合 (相当于在excel创建工作表单)
数据的一个集合,把相关联的数据放在一个集合中
3、确立表头,插入数据、删除数据、修改数据、查询数据

MongoDB数据库中的概念

数据库 : 一个App中对应一个数据库

集合:相当于Excel中表单,一堆数据的集合,相关联的数据,
会放在一个集合中

文档:相当于excel中的每一行数据

一个数据中可以有多个集合(学生集合、食品集合)
一个集合可以有多条文档(多条数据)

在NodeJS中使用mongodb这个第三方包来操作我们mongodb数据库中的数据

参考: https://www.npmjs.com/package/mongodb

前提准备:
1、使用npm i mongodb --save来安装

正式集成:
1、导入包
2、拿到我们mongoClient对象
3、使用mongoClient连接到mongodb的服务端,返回操作数据库的db对象
4、通过db对象,拿到数据集合

db.collection('集合的名称')
5、调用集合的增,删,改,查的方法,来操作数据库中的数据
收起阅读 »

nodejs中的fs模块

对于文件处理的四个操作 增删改查 简称 curd(create-update-read-del)需要使用到的模块叫File System 简称fs 是nodejs 自带的一个库const fs=require('fs');1、使用 fs.mkdir...
继续阅读 »

对于文件处理的四个操作 增删改查 简称 curd(create-update-read-del)

需要使用到的模块叫File System 简称fs 是nodejs 自带的一个库

const fs=require('fs');

1、使用 fs.mkdir 创建目录css


2、fs.readdir 读取当前目录下的文件node02

同步读取 异步读取 同步读取时候 用try catch 处理报错 异步 直接用回调函数中的参数处理。


3、fs.rename 重命名html 下的index为base


4、 fs.unlink 删除文件t.txt


收起阅读 »

这可能是掘金讲「原型链」,讲的最好最通俗易懂的了,附练习题!

前言 大家好,我是林三心,相信大家都听过前端的三座大山:闭包,原型链,作用域,这三个其实都只是算基础。而我一直觉得基础是进阶的前提,所以不能因为是基础就忽视他们。今天我就以我的方式讲讲原型链吧,希望大家能牢固地掌握原型链知识 很多文章一上来就扔这个图,但是我不...
继续阅读 »


前言


大家好,我是林三心,相信大家都听过前端的三座大山:闭包,原型链,作用域,这三个其实都只是算基础。而我一直觉得基础是进阶的前提,所以不能因为是基础就忽视他们。今天我就以我的方式讲讲原型链吧,希望大家能牢固地掌握原型链知识


很多文章一上来就扔这个图,但是我不喜欢这样,我觉得这样对基础不好的同学很不好,我喜欢带领大家去从零实现这个图,在实现的过程中,不断地掌握原型链的所有知识!!!来吧!!!跟着我从零实现吧!!!跟着我驯服原型链吧!!!


截屏2021-09-13 下午9.58.41.png


prototype和__proto__


是啥


这两个东西到底是啥呢?


  • prototype: 显式原型
  • __ proto__: 隐式原型

有什么关系


那么这两个都叫原型,那他们两到底啥关系呢?


一般,构造函数的prototype和其实例的__proto__是指向同一个地方的,这个地方就叫做原型对象


那什么是构造函数呢?俗话说就是,可以用来new的函数就叫构造函数,箭头函数不能用来当做构造函数哦


function Person(name, age) { // 这个就是构造函数
this.name = name
this.age = age
}

const person1 = new Person('小明', 20) // 这个是Person构造函数的实例
const person2 = new Person('小红', 30) // 这个也是Person构造函数的实例

构造函数的prototype和其实例的__proto__是指向同一个地方的,咱们可以来验证一下


function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function() {
console.log(this.name)
}
console.log(Person.prototype) // { sayName: [Function] }

const person1 = new Person('小明', 20)
console.log(person1.__proto__) // { sayName: [Function] }

const person2 = new Person('小红', 30)
console.log(person2.__proto__) // { sayName: [Function] }

console.log(Person.prototype === person1.__proto__) // true
console.log(Person.prototype === person2.__proto__) // true

截屏2021-09-12 下午9.23.35.png


函数


咱们上面提到了构造函数,其实他说到底也是个函数,其实咱们平时定义函数,无非有以下几种


function fn1(name, age) {
console.log(`我是${name}, 我今年${age}岁`)
}
fn1('林三心', 10) // 我是林三心, 我今年10岁

const fn2 = function(name, age){
console.log(`我是${name}, 我今年${age}岁`)
}
fn2('林三心', 10) // 我是林三心, 我今年10岁

const arrowFn = (name, age) => {
console.log(`我是${name}, 我今年${age}岁`)
}
arrowFn('林三心', 10) // 我是林三心, 我今年10岁

其实这几种的本质都是一样的(只考虑函数的声明),都可以使用new Function来声明,是的没错Function也是一个构造函数。上面的写法等同于下面的写法


const fn1 = new Function('name', 'age', 'console.log(`我是${name}, 我今年${age}岁`)')
fn1('林三心', 10) // 我是林三心, 我今年10岁

const fn2 = new Function('name', 'age', 'console.log(`我是${name}, 我今年${age}岁`)')
fn2('林三心', 10) // 我是林三心, 我今年10岁

const arrowFn = new Function('name', 'age', 'console.log(`我是${name}, 我今年${age}岁`)')
arrowFn('林三心', 10) // 我是林三心, 我今年10岁

截屏2021-09-12 下午9.17.42.png


我们之前说过,构造函数prototype和其实例__proto__是指向同一个地方的,这里的fn1,fn2,arrowFn其实也都是Function构造函数的实例,那我们来验证一下吧


function fn1(name, age) {
console.log(`我是${name}, 我今年${age}岁`)
}

const fn2 = function(name, age){
console.log(`我是${name}, 我今年${age}岁`)
}

const arrowFn = (name, age) => {
console.log(`我是${name}, 我今年${age}岁`)
}

console.log(Function.prototype === fn1.__proto__) // true
console.log(Function.prototype === fn2.__proto__) // true
console.log(Function.prototype === arrowFn.__proto__) // true

截屏2021-09-12 下午9.29.00.png


对象


咱们平常开发中,创建一个对象,通常会用以下几种方法。


  • 构造函数创建对象,他创建出来的对象都是此Function构造函数的实例,所以这里不讨论它
  • 字面量创建对象
  • new Object创建对象
  • Object.create创建对象,创建出来的是一个空原型的对象,这里不讨论它

// 第一种:构造函数创建对象
function Person(name, age) {
this.name = name
this.age = age
}
const person1 = new Person('林三心', 10)
console.log(person1) // Person { name: '林三心', age: 10 }

// 第二种:字面量创建对象
const person2 = {name: '林三心', age: 10}
console.log(person2) // { name: '林三心', age: 10 }

// 第三种:new Object创建对象
const person3 = new Object()
person3.name = '林三心'
person3.age = 10
console.log(person3) // { name: '林三心', age: 10 }

// 第四种:Object.create创建对象
const person4 = Object.create({})
person4.name = '林三心'
person4.age = 10
console.log(person4) // { name: '林三心', age: 10 }

咱们来看看字面量创建对象new Object创建对象两种方式,其实字面量创建对象的本质就是new Object创建对象


// 字面量创建对象
const person2 = {name: '林三心', age: 10}
console.log(person2) // { name: '林三心', age: 10 }

本质是

// new Object创建对象
const person2 = new Object()
person2.name = '林三心'
person2.age = 10
console.log(person2) // { name: '林三心', age: 10 }

截屏2021-09-12 下午9.52.47.png


我们之前说过,构造函数prototype和其实例__proto__是指向同一个地方的,这里的person2,person3其实也都是Object构造函数的实例,那我们来验证一下吧


const person2 = {name: '林三心', age: 10}

const person3 = new Object()
person3.name = '林三心'
person3.age = 10

console.log(Object.prototype === person2.__proto__) // true
console.log(Object.prototype === person3.__proto__) // true

截屏2021-09-12 下午9.58.31.png


Function和Object


上面咱们常说


  • 函数Function构造函数的实例
  • 对象Object构造函数的实例

Function构造函数Object构造函数他们两个又是谁的实例呢?


  • function Object()其实也是个函数,所以他是Function构造函数的实例
  • function Function()其实也是个函数,所以他也是Function构造函数的实例,没错,他是他自己本身的实例

咱们可以试验一下就知道了


console.log(Function.prototype === Object.__proto__) // true
console.log(Function.prototype === Function.__proto__) // true

截屏2021-09-12 下午10.12.40.png


constructor


constructor和prototype是成对的,你指向我,我指向你。举个例子,如果你是我老婆,那我肯定是你的老公。


function fn() {}

console.log(fn.prototype) // {constructor: fn}
console.log(fn.prototype.constructor === fn) // true

截屏2021-09-12 下午10.35.40.png


原型链


Person.prototype 和 Function.prototype


讨论原型链之前,咱们先来聊聊这两个东西


  • Person.prototype,它是构造函数Person的原型对象
  • Function.prototype,他是构造函数Function的原型对象

都说了原型对象,原型对象,可以知道其实这两个本质都是对象


那既然是对象,本质肯定都是通过new Object()来创建的。既然是通过new Object()创建的,那就说明Person.prototype 和 Function.prototype都是构造函数Object的实例。也就说明了Person.prototype 和 Function.prototype他们两的__proto__都指向Object.prototype


咱们可以验证一下


function Person(){}

console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Function.prototype.__proto__ === Object.prototype) // true

截屏2021-09-12 下午10.46.41.png


什么是原型链?


什么是原型链呢?其实俗话说就是:__proto__的路径就叫原型链


截屏2021-09-12 下午10.55.48.png


原型链终点


上面咱们看到,三条原型链结尾都是Object.prototype,那是不是说明了Object.prototype就是原型链的终点呢?其实不是的,Object.prototype其实也有__proto__,指向null,那才是原型链的终点


至此,整个原型示意图就画完啦!!!


截屏2021-09-13 下午9.56.10.png


原型继承


说到原型,就不得不说补充一下原型继承这个知识点了,原型继承就是,实例可以使用构造函数上的prototype中的方法


function Person(name) { // 构造函数
this.name = name
}
Person.prototype.sayName = function() { // 往原型对象添加方法
console.log(this.name)
}


const person = new Person('林三心') // 实例
// 使用构造函数的prototype中的方法
person.sayName() // 林三心

截屏2021-09-12 下午11.10.41.png


instanceof


使用方法


A instanceof B

作用:判断B的prototype是否在A的原型链上


例子


function Person(name) { // 构造函数
this.name = name
}

const person = new Person('林三心') // 实例

console.log(Person instanceof Function) // true
console.log(Person instanceof Object) // true
console.log(person instanceof Person) // true
console.log(person instanceof Object) // true

练习题


练习题只为了大家能巩固本文章的知识


第一题


var F = function() {};

Object.prototype.a = function() {
console.log('a');
};

Function.prototype.b = function() {
console.log('b');
}

var f = new F();

f.a();
f.b();

F.a();
F.b();

答案


f.a(); // a
f.b(); // f.b is not a function

F.a(); // a
F.b(); // b

第二题


var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
}
var c = new A();

console.log(b.n);
console.log(b.m);

console.log(c.n);
console.log(c.m);

答案


console.log(b.n); // 1
console.log(b.m); // undefined

console.log(c.n); // 2
console.log(c.m); // 3

第三题


var foo = {},
F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log(foo.a);
console.log(foo.b);

console.log(F.a);
console.log(F.b);

答案


console.log(foo.a); // value a
console.log(foo.b); // undefined

console.log(F.a); // value a
console.log(F.b); // value b

第四题


function A() {}
function B(a) {
this.a = a;
}
function C(a) {
if (a) {
this.a = a;
}
}
A.prototype.a = 1;
B.prototype.a = 1;
C.prototype.a = 1;

console.log(new A().a);
console.log(new B().a);
console.log(new C(2).a);

答案


console.log(new A().a); // 1
console.log(new B().a); // undefined
console.log(new C(2).a); // 2

第五题


console.log(123['toString'].length + 123)

答案:123是数字,数字本质是new Number(),数字本身没有toString方法,则沿着__proto__function Number()prototype上找,找到toString方法,toString方法的length是1,1 + 123 = 124,至于为什么length是1,可以看95%的人都回答不上来的问题:函数的length是多少?


console.log(123['toString'].length + 123) // 124

结语



如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下

收起阅读 »

7张图,20分钟就能搞定的async/await原理!为什么要拖那么久?

前言 大家好,我是林三心,以最通俗的话,讲最难的知识点是我写文章的宗旨 之前我发过一篇手写Promise原理,最通俗易懂的版本!!!,带大家基本了解了Promise内部的实现原理,而提到Promise,就不得不提一个东西,那就是async/await,asyn...
继续阅读 »


前言


大家好,我是林三心,以最通俗的话,讲最难的知识点是我写文章的宗旨


之前我发过一篇手写Promise原理,最通俗易懂的版本!!!,带大家基本了解了Promise内部的实现原理,而提到Promise,就不得不提一个东西,那就是async/awaitasync/await是一个很重要的语法糖,他的作用是用同步的方式,执行异步操作。那么今天我就带大家一起实现一下async/await吧!!!


async/await用法


其实你要实现一个东西之前,最好是先搞清楚这两样东西


  • 这个东西有什么用?
  • 这个东西是怎么用的?

有什么用?


async/await的用处就是:用同步方式,执行异步操作,怎么说呢?举个例子


比如我现在有一个需求:先请求完接口1,再去请求接口2,我们通常会这么做


function request(num) { // 模拟接口请求
return new Promise(resolve => {
setTimeout(() => {
resolve(num * 2)
}, 1000)
})
}

request(1).then(res1 => {
console.log(res1) // 1秒后 输出 2

request(2).then(res2 => {
console.log(res2) // 2秒后 输出 4
})
})

或者我现在又有一个需求:先请求完接口1,再拿接口1返回的数据,去当做接口2的请求参数,那我们也可以这么做


request(5).then(res1 => {
console.log(res1) // 1秒后 输出 10

request(res1).then(res2 => {
console.log(res2) // 2秒后 输出 20
})
})

其实这么做是没问题的,但是如果嵌套的多了,不免有点不雅观,这个时候就可以用async/await来解决了


async function fn () {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2) // 2秒后输出 20
}
fn()

是怎么用?


还是用刚刚的例子


需求一:


async function fn () {
await request(1)
await request(2)
// 2秒后执行完
}
fn()

需求二:


async function fn () {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2) // 2秒后输出 20
}
fn()

截屏2021-09-11 下午9.57.58.png


其实就类似于生活中的排队,咱们生活中排队买东西,肯定是要上一个人买完,才轮到下一个人。而上面也一样,在async函数中,await规定了异步操作只能一个一个排队执行,从而达到用同步方式,执行异步操作的效果,这里注意了:await只能在async函数中使用,不然会报错哦


刚刚上面的例子await后面都是跟着异步操作Promise,那如果不接Promise会怎么样呢?


function request(num) { // 去掉Promise
setTimeout(() => {
console.log(num * 2)
}, 1000)
}

async function fn() {
await request(1) // 2
await request(2) // 4
// 1秒后执行完 同时输出
}
fn()

可以看出,如果await后面接的不是Promise的话,有可能其实是达不到排队的效果的


说完await,咱们聊聊async吧,async是一个位于function之前的前缀,只有async函数中,才能使用await。那async执行完是返回一个什么东西呢?


async function fn () {}
console.log(fn) // [AsyncFunction: fn]
console.log(fn()) // Promise {<fulfilled>: undefined}

可以看出,async函数执行完会自动返回一个状态为fulfilled的Promise,也就是成功状态,但是值却是undefined,那要怎么才能使值不是undefined呢?很简单,函数有return返回值就行了


async function fn (num) {
return num
}
console.log(fn) // [AsyncFunction: fn]
console.log(fn(10)) // Promise {<fulfilled>: 10}
fn(10).then(res => console.log(res)) // 10

可以看出,此时就有值了,并且还能使用then方法进行输出


总结


总结一下async/await的知识点


  • await只能在async函数中使用,不然会报错
  • async函数返回的是一个Promise对象,有无值看有无return值
  • await后面最好是接Promise,虽然接其他值也能达到排队效果
  • async/await作用是用同步方式,执行异步操作

什么是语法糖?


前面说了,async/await是一种语法糖,诶!好多同学就会问,啥是语法糖呢?我个人理解就是,语法糖就是一个东西,这个东西你就算不用他,你用其他手段也能达到这个东西同样的效果,但是可能就没有这个东西这么方便了。


  • 举个生活中的例子吧:你走路也能走到北京,但是你坐飞机会更快到北京。
  • 举个代码中的例子吧:ES6的class也是语法糖,因为其实用普通function也能实现同样效果

回归正题,async/await是一种语法糖,那就说明用其他方式其实也可以实现他的效果,我们今天就是讲一讲怎么去实现async/await,用到的是ES6里的迭代函数——generator函数


generator函数


基本用法


generator函数跟普通函数在写法上的区别就是,多了一个星号*,并且只有在generator函数中才能使用yield,什么是yield呢,他相当于generator函数执行的中途暂停点,比如下方有3个暂停点。而怎么才能暂停后继续走呢?那就得使用到next方法next方法执行后会返回一个对象,对象中有value 和 done两个属性


  • value:暂停点后面接的值,也就是yield后面接的值
  • done:是否generator函数已走完,没走完为false,走完为true

function* gen() {
yield 1
yield 2
yield 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: undefined, done: true }

可以看到最后一个是undefined,这取决于你generator函数有无返回值


function* gen() {
yield 1
yield 2
yield 3
return 4
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: false }
console.log(g.next()) // { value: 4, done: true }

截屏2021-09-11 下午9.46.17.png


yield后面接函数


yield后面接函数的话,到了对应暂停点yield,会马上执行此函数,并且该函数的执行返回值,会被当做此暂停点对象的value


function fn(num) {
console.log(num)
return num
}
function* gen() {
yield fn(1)
yield fn(2)
return 3
}
const g = gen()
console.log(g.next())
// 1
// { value: 1, done: false }
console.log(g.next())
// 2
// { value: 2, done: false }
console.log(g.next())
// { value: 3, done: true }

yield后面接Promise


前面说了,函数执行返回值会当做暂停点对象的value值,那么下面例子就可以理解了,前两个的value都是pending状态的Promise对象


function fn(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num)
}, 1000)
})
}
function* gen() {
yield fn(1)
yield fn(2)
return 3
}
const g = gen()
console.log(g.next()) // { value: Promise { <pending> }, done: false }
console.log(g.next()) // { value: Promise { <pending> }, done: false }
console.log(g.next()) // { value: 3, done: true }

截屏2021-09-11 下午10.51.38.png


其实我们想要的结果是,两个Promise的结果1 和 2,那怎么做呢?很简单,使用Promise的then方法就行了


const g = gen()
const next1 = g.next()
next1.value.then(res1 => {
console.log(next1) // 1秒后输出 { value: Promise { 1 }, done: false }
console.log(res1) // 1秒后输出 1

const next2 = g.next()
next2.value.then(res2 => {
console.log(next2) // 2秒后输出 { value: Promise { 2 }, done: false }
console.log(res2) // 2秒后输出 2
console.log(g.next()) // 2秒后输出 { value: 3, done: true }
})
})

截屏2021-09-11 下午10.38.37.png


next函数传参


generator函数可以用next方法来传参,并且可以通过yield来接收这个参数,注意两点


  • 第一次next传参是没用的,只有从第二次开始next传参才有用
  • next传值时,要记住顺序是,先右边yield,后左边接收参数

function* gen() {
const num1 = yield 1
console.log(num1)
const num2 = yield 2
console.log(num2)
return 3
}
const g = gen()
console.log(g.next()) // { value: 1, done: false }
console.log(g.next(11111))
// 11111
// { value: 2, done: false }
console.log(g.next(22222))
// 22222
// { value: 3, done: true }

截屏2021-09-11 下午10.53.02.png


Promise+next传参


前面讲了


  • yield后面接Promise
  • next函数传参

那这两个组合起来会是什么样呢?


function fn(nums) {
return new Promise(resolve => {
setTimeout(() => {
resolve(nums * 2)
}, 1000)
})
}
function* gen() {
const num1 = yield fn(1)
const num2 = yield fn(num1)
const num3 = yield fn(num2)
return num3
}
const g = gen()
const next1 = g.next()
next1.value.then(res1 => {
console.log(next1) // 1秒后同时输出 { value: Promise { 2 }, done: false }
console.log(res1) // 1秒后同时输出 2

const next2 = g.next(res1) // 传入上次的res1
next2.value.then(res2 => {
console.log(next2) // 2秒后同时输出 { value: Promise { 4 }, done: false }
console.log(res2) // 2秒后同时输出 4

const next3 = g.next(res2) // 传入上次的res2
next3.value.then(res3 => {
console.log(next3) // 3秒后同时输出 { value: Promise { 8 }, done: false }
console.log(res3) // 3秒后同时输出 8

// 传入上次的res3
console.log(g.next(res3)) // 3秒后同时输出 { value: 8, done: true }
})
})
})

截屏2021-09-11 下午11.05.44.png


实现async/await


其实上方的generator函数Promise+next传参,就很像async/await了,区别在于


  • gen函数执行返回值不是Promise,asyncFn执行返回值是Promise
  • gen函数需要执行相应的操作,才能等同于asyncFn的排队效果
  • gen函数执行的操作是不完善的,因为并不确定有几个yield,不确定会嵌套几次

截屏2021-09-11 下午11.53.41.png


那我们怎么办呢?我们可以封装一个高阶函数。什么是高阶函数呢?高阶函数的特点是:参数是函数,返回值也可以是函数。下方的highorderFn就是一个高阶函数


function highorderFn(函数) {
// 一系列处理

return 函数
}

我们可以封装一个高阶函数,接收一个generator函数,并经过一系列处理,返回一个具有async函数功能的函数


function generatorToAsync(generatorFn) {
// 经过一系列处理

return 具有async函数功能的函数
}

返回值Promise


之前我们说到,async函数的执行返回值是一个Promise,那我们要怎么实现相同的结果呢


function* gen() {

}

const asyncFn = generatorToAsync(gen)

console.log(asyncFn()) // 期望这里输出 Promise

其实很简单,generatorToAsync函数里做一下处理就行了


function* gen() {

}
function generatorToAsync (generatorFn) {
return function () {
return new Promise((resolve, reject) => {

})
}
}

const asyncFn = generatorToAsync(gen)

console.log(asyncFn()) // Promise

加入一系列操作


咱们把之前的处理代码,加入generatorToAsync函数


function fn(nums) {
return new Promise(resolve => {
setTimeout(() => {
resolve(nums * 2)
}, 1000)
})
}
function* gen() {
const num1 = yield fn(1)
const num2 = yield fn(num1)
const num3 = yield fn(num2)
return num3
}
function generatorToAsync(generatorFn) {
return function () {
return new Promise((resolve, reject) => {
const g = generatorFn()
const next1 = g.next()
next1.value.then(res1 => {

const next2 = g.next(res1) // 传入上次的res1
next2.value.then(res2 => {

const next3 = g.next(res2) // 传入上次的res2
next3.value.then(res3 => {

// 传入上次的res3
resolve(g.next(res3).value)
})
})
})
})
}
}

const asyncFn = generatorToAsync(gen)

asyncFn().then(res => console.log(res)) // 3秒后输出 8

可以发现,咱们其实已经实现了以下的async/await的结果了


async function asyncFn() {
const num1 = await fn(1)
const num2 = await fn(num1)
const num3 = await fn(num2)
return num3
}
asyncFn().then(res => console.log(res)) // 3秒后输出 8

完善代码


上面的代码其实都是死代码,因为一个async函数中可能有2个await,3个await,5个await
,其实await的个数是不确定的。同样类比,generator函数中,也可能有2个yield,3个yield,5个yield,所以咱们得把代码写成活的才行


function generatorToAsync(generatorFn) {
return function() {
const gen = generatorFn.apply(this, arguments) // gen有可能传参

// 返回一个Promise
return new Promise((resolve, reject) => {

function go(key, arg) {
let res
try {
res = gen[key](arg) // 这里有可能会执行返回reject状态的Promise
} catch (error) {
return reject(error) // 报错的话会走catch,直接reject
}

// 解构获得value和done
const { value, done } = res
if (done) {
// 如果done为true,说明走完了,进行resolve(value)
return resolve(value)
} else {
// 如果done为false,说明没走完,还得继续走

// value有可能是:常量,Promise,Promise有可能是成功或者失败
return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))
}
}

go("next") // 第一次执行
})
}
}

const asyncFn = generatorToAsync(gen)

asyncFn().then(res => console.log(res))

这样的话,无论是多少个yield都会排队执行了,咱们把代码写成活的了


示例


async/await版本


async function asyncFn() {
const num1 = await fn(1)
console.log(num1) // 2
const num2 = await fn(num1)
console.log(num2) // 4
const num3 = await fn(num2)
console.log(num3) // 8
return num3
}
const asyncRes = asyncFn()
console.log(asyncRes) // Promise
asyncRes.then(res => console.log(res)) // 8

使用generatorToAsync函数的版本


function* gen() {
const num1 = yield fn(1)
console.log(num1) // 2
const num2 = yield fn(num1)
console.log(num2) // 4
const num3 = yield fn(num2)
console.log(num3) // 8
return num3
}

const genToAsync = generatorToAsync(gen)
const asyncRes = genToAsync()
console.log(asyncRes) // Promise
asyncRes.then(res => console.log(res)) // 8

结语


如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。


如果你想一起学习前端或者摸鱼,那你可以加我,加入我的摸鱼学习群,点击这里 ---> 摸鱼沸点


如果你是有其他目的的,别加我,我不想跟你交朋友,我只想简简单单学习前端,不想搞一些有的没的!!

 
收起阅读 »

知其然,而知其所以然,JS 对象创建与继承【汇总梳理】

  这些文章是: 蓦然回首,“工厂、构造、原型”设计模式,正在灯火阑珊处JS精粹,原型链继承和构造函数继承的 “毛病”“工厂、构造、原型” 设计模式与 JS 继承JS 高级程序设计 4:class 继承的重点JS class 并不只是简单的语法糖! ...
继续阅读 »
 

这些文章是:



本篇作为汇总篇,来一探究竟!!冲冲冲


image.png


对象创建


不难发现,每一篇都离不开工厂、构造、原型这 3 种设计模式中的至少其一!


让人不禁想问:JS 为什么非要用到这种 3 种设计模式了呢??


正本溯源,先从对象创建讲起:


我们本来习惯这样声明对象(不用任何设计模式)


let car= {
price:100,
color:"white",
run:()=>{console.log("run fast")}
}

当有两个或多个这样的对象需要声明时,是不可能一直复制写下去的:


let car1 = {
price:100,
color:"white",
run:()=>{console.log("run fast")}
}

let car2 = {
price:200,
color:"balck",
run:()=>{console.log("run slow")}
}

let car3 = {
price:300,
color:"red",
run:()=>{console.log("broken")}
}

这样写:


  1. 写起来麻烦,重复的代码量大;
  2. 不利于修改,比如当 car 对象要增删改一个属性,需要多处进行增删改;

工厂函数


肯定是要封装啦,第一个反应,可以 借助函数 来帮助我们批量创建对象~


于是乎:


function makeCar(price,color,performance){
let obj = {}
obj.price = price
obj.color= color
obj.run = ()=>{console.log(performance)}
return obj
}

let car1= makeCar("100","white","run fast")
let car2= makeCar("200","black","run slow")
let car3= makeCar("300","red","broken")

这就是工厂设计模式在 JS 创建对象时应用的由来~


到这里,对于【对象创建】来说,应该够用了吧?是,在不考虑扩展的情况下,基本够用了。


但这个时候来个新需求,需要创建 car4、car5、car6 对象,它们要在原有基础上再新增一个 brand 属性,会怎么写?


第一反应,直接修改 makeCar


function makeCar(price,color,performance,brand){
let obj = {}
obj.price = price
obj.color= color
obj.run = ()=>{console.log(performance)}
obj.brand = brand
return obj
}

let car4= makeCar("400","white","run fast","benz")
let car5= makeCar("500","black","run slow","audi")
let car6= makeCar("600","red","broken","tsl")

这样写,不行,会影响原有的 car1、car2、car3 对象;


那再重新写一个 makeCarChild 工厂函数行不行?


function makeCarChild (price,color,performance,brand){
let obj = {}
obj.price = price
obj.color= color
obj.run = ()=>{console.log(performance)}
obj.brand = brand
return obj
}

let car4= makeCarChild("400","white","run fast","benz")
let car5= makeCarChild("500","black","run slow","audi")
let car6= makeCarChild("600","red","broken","tsl")

行是行,就是太麻烦,全量复制之前的属性,建立 N 个相像的工厂,显得太蠢了。。。


image.png


构造函数


于是乎,在工厂设计模式上,发展出了:构造函数设计模式,来解决以上复用(也就是继承)的问题。


function MakeCar(price,color,performance){
this.price = price
this.color= color
this.run = ()=>{console.log(performance)}
}

function MakeCarChild(brand,...args){
MakeCar.call(this,...args)
this.brand = brand
}

let car4= new MakeCarChild("benz","400","white","run fast")
let car5= new MakeCarChild("audi","500","black","run slow")
let car6= new MakeCarChild("tsl","600","red","broken")

构造函数区别于工厂函数:


  • 函数名首字母通常大写;
  • 创建对象的时候要用到 new 关键字(new 的过程这里不再赘述了,之前文章有);
  • 函数没有 return,而是通过 this 绑定来实现寻找属性的;

到此为止,工厂函数的复用也解决了。


构造+原型


新的问题在于,我们不能通过查找原型链从 MakeCarChild 找到 MakeCar


car4.__proto__===MakeCarChild.prototype // true

MakeCarChild.prototype.__proto__ === MakeCar.prototype // false
MakeCarChild.__proto__ === MakeCar.prototype // false

无论在原型链上怎么找,都无法从 MakeCarChild 找到 MakeCar


这就意味着:子类不能继承父类原型上的属性



这里提个思考问题:为什么“要从原型链查找到”很重要?为什么“子类要继承父类原型上的属性”?就靠 this 绑定来找不行吗?



image.png


于是乎,构造函数设计模式 + 原型设计模式 的 【组合继承】应运而生


function MakeCar(price,color,performance){
this.price = price
this.color= color
this.run = ()=>{console.log(performance)}
}

function MakeCarChild(brand,...args){
MakeCar.call(this,...args)
this.brand = brand
}

MakeCarChild.prototype = new MakeCar() // 原型继承父类的构造器

MakeCarChild.prototype.constructor = MakeCarChild // 重置 constructor

let car4= new MakeCarChild("benz","400","white","run fast")

现在再找原型,就找的到啦:


car4.__proto__ === MakeCarChild.prototype // true
MakeCarChild.prototype.__proto__ === MakeCar.prototype // true

其实,能到这里,就已经很很优秀了,该有的都有了,写法也不算是很复杂。


工厂+构造+原型


但,总有人在追求极致。


image.png


上述的组合继承,父类构造函数被调用了两次,一次是 call 的过程,一次是原型继承 new 的过程,如果每次实例化,都重复调用,肯定是不可取的,怎样避免?


工厂 + 构造 + 原型 = 寄生组合继承 应运而生


核心是,通过工厂函数新建一个中间商 F( ),复制了一份父类的原型对象,再赋给子类的原型;


function object(o) { // 工厂函数
function F() {}
F.prototype = o;
return new F(); // new 一个空的函数,所占内存很小
}

function inherit(child, parent) { // 原型继承
var prototype = object(parent.prototype)
prototype.constructor = child
child.prototype = prototype
}


function MakeCar(price,color,performance){
this.price = price
this.color= color
this.run = ()=>{console.log(performance)}
}

function MakeCarChild(brand,...args){ // 构造函数
MakeCar.call(this,...args)
this.brand = brand
}

inherit(MakeCarChild,MakeCar)

let car4= new MakeCarChild("benz","400","white","run fast")

car4.__proto__ === MakeCarChild.prototype // true

MakeCarChild.prototype.__proto__ === MakeCar.prototype // true

ES6 class


再到后来,ES6 的 class 作为寄生组合继承的语法糖:


class MakeCar {
constructor(price,color,performance){
this.price = price
this.color= color
this.performance=performance
}
run(){
console.log(console.log(this.performance))
}
}

class MakeCarChild extends MakeCar{
constructor(brand,...args){
super(brand,...args);
this.brand= brand;
}
}

let car4= new MakeCarChild("benz","400","white","run fast")

car4.__proto__ === MakeCarChild.prototype // true

MakeCarChild.prototype.__proto__ === MakeCar.prototype // true

有兴趣的工友,可以看下 ES6 解析成 ES5 的代码:原型与原型链 - ES6 Class的底层实现原理 #22


对象与函数


最后本瓜想再谈谈关于 JS 对象和函数的关系:


image.png


即使是这样声明一个对象,let obj = {} ,它一样是由构造函数 Object 构造而来的:


let obj = {} 

obj.__proto__ === Object.prototype // true


在 JS 中,万物皆对象,对象都是有函数构造而来,函数本身也是对象。



对应代码中的意思:


  1. 所有的构造函数的隐式原型都等于 Function 的显示原型,函数都是由 Function 构造而来,Object 构造函数也不例外;
  2. 所有构造函数的显示原型的隐式原型,都等于 Object 的显示原型,Function 也不例外;

// 1.
Object.__proto__ === Function.prototype // true

// 2.
Function.prototype.__proto__ === Object.prototype // true

这个设计真的就一个大无语,大纠结,大麻烦。。。


image.png


只能先按之前提过的歪理解记着先:Function 就是上帝,上帝创造了万物;Object 就是万物。万物由上帝创造(对象由函数构造而来),上帝本身也属于一种物质(函数本身却也是对象);


对于本篇来说,继承,其实都是父子构造函数在继承,然后再由构造函数实例化对象,以此来实现对象的继承。


到底是谁在继承?函数?对象?都是吧~~




小结


本篇由创建对象说起,讲了工厂函数,它可以做一层最基本的封装;


再到,对工厂的拓展,演进为构造函数;


再基于原型特点,构造+原型,得出组合继承;


再追求极致,讲到寄生组合;


再讲到简化书写的 Es6 class ;


以及最后对对象与函数的思考。


就先到这吧~~

 
收起阅读 »

程序员版本的八荣八耻~

大家好,最近整理了一个关于程序员日常开发版本的八荣八耻,还挺有意思的。给大家分享一下,哈哈~以接口兼容为荣,怎么理解呢?我们还要以接口裸奔为耻。为了保证接口报文的安全性,拒绝接口报文裸奔。因此,我们可以使用https协议,还建议对接口加签验签处理,数据加密等。...
继续阅读 »

前言

大家好,最近整理了一个关于程序员日常开发版本的八荣八耻,还挺有意思的。给大家分享一下,哈哈~

1. 以接口兼容为荣,以接口裸奔为耻

接口兼容为荣,怎么理解呢?

很多bug都是因为修改了对外旧接口,但是却不做兼容导致的。关键这个问题多数是比较严重的,可能直接导致系统发版失败的。新手程序员很容易犯这个错误。所以我们修改老接口的时候,一般要做好兼容


如果你的需求是在原来接口上修改,尤其这个接口是对外提供服务的话,一定要考虑接口兼容。举个例子吧,比如dubbo接口,原本是只接收A,B参数,现在你加了一个参数C,就可以考虑这样处理:

//老接口
void oldService(A,B){
//兼容新接口,传个null代替C
newService(A,B,null);
}

//新接口,暂时不能删掉老接口,需要做兼容。
void newService(A,B,C){
...
}

我们还要以接口裸奔为耻。为了保证接口报文的安全性,拒绝接口报文裸奔。因此,我们可以使用https协议,还建议对接口加签验签处理,数据加密等。

接口签名很简单,就是把接口请求相关信息(请求报文,包括请求时间戳、版本号、appid等),客户端私钥加签,然后服务端用公钥验签,验证通过才认为是合法的、没有被中间人篡改过的请求。

2. 以规范日志为荣,以乱打日志为耻

我们的业务逻辑代码需要日志保驾护航。比如:你实现转账业务,转个几百万,然后转失败了,接着客户投诉,然后你还没有打印到日志,想想那种水深火热的困境下,你却毫无办法。。。

因此大家要打好日志,比如日志级别使用恰当,日志格式,在哪些地方打日志,参数打印哪个等等。不能乱打日志,要以规范日志为荣,乱打日志为耻。


3. 以代码自测为荣,以过度自信为耻

修改完代码,要自测一下,这个是每个程序必备的素养,即使你只是修改了一个变量或者一个字段。

要杜绝过度自信,尤其不要抱有这种侥幸心理:我只是改了一个变量或者我只改了一行配置的代码,不用自测了,怎么可能有问题


因此,我们要以代码自测为荣,以过度自信为耻

4. 以参数校验为荣,以运行异常为耻

数校验是每个程序员必备的基本素养。你的方法处理,必须先校验参数。比如入参是否允许为空,入参长度是否符合你的预期长度。因此,我们要以参数校验为荣

比如你的数据库表字段设置为varchar(16),对方传了一个32位的字符串过来,如果你不校验参数,插入数据库直接异常了。


我们要以运行时异常为耻

比如你没有做好一些非空校验,数组边界校验等等,导致的空指针异常、数组边界异常,尤其这些运行时异常还发生在生产环境的话,在有经验的程序员看来,这些错误行为会显得特别低级。

所以,我们要以参数校验为荣,以运行异常为耻

5. 以设计模式为荣,以代码重复为耻

日常工作中,我们要以设计模式为荣。

比如策略模式、工厂模式、模板方法模式、观察者模式、单例模式、责任链模式等等,都是很常用的。在恰当的业务场景,我们还是把设计模式用上吧。设计模式可以让我们的代码更优雅、更具有扩展性。但是不要过度设计哈,不要硬套设计模式。

我们还要以重复代码为耻。重复代码,我相信每个程序员都讨厌的,尤其有时候你的开发工具还会给你提示出来。我们可以抽取公共方法,抽取公用变量、扩展继承类等方式去消除重复代码。


6. 以优化代码为荣,以复制粘贴为耻

日常开发中,很多程序员在实现某个功能时,如果看到老代码有类似的功能,他们很喜欢复制粘贴过来。这样很容易产生重复代码,所以我们要以复制粘贴为耻。一般建议加自己的思考,怎么优化这部分代码,怎么抽取公用方法,用什么设计模式等等。


个人觉得,优化代码的过程,可以让自己取得更大的进步。因此我们要以优化代码为荣,以复制粘贴为耻。

7. 以定义常量为荣,以魔法数字为耻

大家平时工作中,是不是经常看到魔法数字。魔法数字(Magic Number)是指拥有特殊意义,却又不能明确表现出这种意义的数字。程序里面存在魔法数字,易读性很差,且非常难以维护。

如下:

if(type==1){
  System.out.println("关注公众号:捡田螺的小男孩");
}else if(type==2){
  System.out.println("关注公众号:程序员田螺");
}else{
  System.out.println("关注其他公众号");
}

代码中的1、2就表示魔法数字,我们可以用常量取代魔法数,或者定义枚举去代替魔法数字哈。

8. 以总结思考为荣,以混水摸鱼为耻。

我们要以总结思考为荣。

比如你看完田螺哥的文章,可以总结思考一下,或者做做笔记,或者放到收藏夹,茶余饭后再看看。再比如你日常工作中,看到一段不错的代码,也可以思考一下亮点在哪里,如果是你自己来写的话,怎么写出更好的代码。反正就是要多总结,多思考,多复习,温故而知新嘛。

我们要以混水摸鱼为耻。比如工作中,一些小伙伴喜欢混水摸鱼,当一天和尚敲一天钟,代码多是复制粘贴,做完需求就摸鱼。实际上这个不可取的。


我们要在工作中成长,通过认真工作,使自己会得更多,将来换工作也可以拿到更高的薪水,对吧,加油吧,小伙子,以总结思考为荣,以混水摸鱼为耻

来源:捡田螺的小男孩 ,作者捡田螺的小男孩

收起阅读 »

生成二维码或条形码JavaScript脚本库

web
二维码或条形码在日常生活中现在应用已经非常普遍了,文章分享生成条形码和二维码的JavaScript库。条形码条形码是日常生活中比较常见的,主要用于商品。通俗的理解就是一串字符串的集合(含字母、数字及其它ASCII字符的集合应用),用来常用来标识一个货品的唯一性...
继续阅读 »


二维码或条形码在日常生活中现在应用已经非常普遍了,文章分享生成条形码和二维码的JavaScript库。

条形码

条形码是日常生活中比较常见的,主要用于商品。通俗的理解就是一串字符串的集合(含字母、数字及其它ASCII字符的集合应用),用来常用来标识一个货品的唯一性,当然还有更多更深入与广泛的应用,像超市的商品、衣服、微信、支付宝、小程序等到处都有条形码的广泛应用;

安装依赖:

npm install jsbarcode --save-dev

在 HTML 页面上加入以下代码:

<svg id="barcode"
jsbarcode-value="123456789012"
jsbarcode-format="code128"></svg>

接下来看下 JavaScript 代码,如下:

import jsbarcode from 'jsbarcode';
const createBarcode = (value, elemTarget) => {
  jsbarcode(elemTarget, "value");
};
createBarcode("#barcode", "devpoint");

运行成功的效果如下:


二维码

相比条形码,二维码的使用场景也越来也多,支付码、场所码、小程序等等。二维码的长相经常是在一个正方形的框中填充各种点点或无规则小图形块而构成的图形,这种称之为二维码,他与一维码最大的区别就是存储容量大很多,而且保密性好。二维码本质上表现给大家的就是一个静态图片,其实是包含特字加密算法的图形,里面存储的是一串字符串(即字母、数字、ASCII码等),这说明二维码不仅存储量大,而且存储的内容很广泛,数字、字母、汉字等都可以被存储。

安装依赖:

npm install qrcode --save-dev

HTML:

<canvas id="qrcode"></canvas>

JavaScript:

import QRCode from "qrcode";
const createQrcode = (value, elemTarget) => {
  QRCode.toCanvas(document.querySelector(elemTarget), value);
};
createQrcode("#qrcode", "devpoint");

效果如下:


来源:juejin.cn/post/7116156434605146126

收起阅读 »

Flutter 小技巧之 ListView 和 PageView 的各种花式嵌套

这次的 Flutter 小技巧是 ListView 和 PageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListView 和 PageView 的三种嵌套模式带大家收获一些不一样的小技巧。正常嵌套最常见的...
继续阅读 »

这次的 Flutter 小技巧是 ListViewPageView 的花式嵌套,不同 Scrollable 的嵌套冲突问题相信大家不会陌生,今天就通过 ListViewPageView 的三种嵌套模式带大家收获一些不一样的小技巧。

正常嵌套

最常见的嵌套应该就是横向 PageView 加纵向 ListView 的组合,一般情况下这个组合不会有什么问题,除非你硬是要斜着滑

最近刚好遇到好几个人同时在问:“斜滑 ListView 容易切换到 PageView 滑动” 的问题,如下 GIF 所示,当用户在滑动 ListView 时,滑动角度带上倾斜之后,可能就会导致滑动的是 PageView 而不是 ListView


虽然从我个人体验上并不觉得这是个问题,但是如果产品硬是要你修改,难道要自己重写 PageView 的手势响应吗?

我们简单看一下,不管是 PageView 还是 ListView 它们的滑动效果都来自于 Scrollable ,而 Scrollable 内部针对不同方向的响应,是通过 RawGestureDetector 完成:

  • VerticalDragGestureRecognizer 处理垂直方向的手势

  • HorizontalDragGestureRecognizer 处理水平方向的手势

所以简单看它们响应的判断逻辑,可以看到一个很有趣的方法 computeHitSlop根据 pointer 的类型确定当然命中需要的最小像素,触摸默认是 kTouchSlop (18.0)

image-20220613103745974

看到这你有没有灵光一闪:如果我们把 PageView 的 touchSlop 修改了,是不是就可以调整它响应的灵敏度? 恰好在 computeHitSlop 方法里,它可以通过 DeviceGestureSettings 来配置,而 DeviceGestureSettings 来自于 MediaQuery ,所以如下代码所示:

body: MediaQuery(
 ///调高 touchSlop 到 50 ,这样 pageview 滑动可能有点点影响,
 ///但是大概率处理了斜着滑动触发的问题
 data: MediaQuery.of(context).copyWith(
     gestureSettings: DeviceGestureSettings(
   touchSlop: 50,
)),
 child: PageView(
   scrollDirection: Axis.horizontal,
   pageSnapping: true,
   children: [
     HandlerListView(),
     HandlerListView(),
  ],
),
),

小技巧一:通过嵌套一个 MediaQuery ,然后调整 gestureSettingstouchSlop 从而修改 PageView 的灵明度 ,另外不要忘记,还需要把 ListViewtouchSlop 切换会默认 的 kTouchSlop

class HandlerListView extends StatefulWidget {
 @override
 _MyListViewState createState() => _MyListViewState();
}
class _MyListViewState extends State<HandlerListView> {
 @override
 Widget build(BuildContext context) {
   return MediaQuery(
     ///这里 touchSlop 需要调回默认
     data: MediaQuery.of(context).copyWith(
         gestureSettings: DeviceGestureSettings(
       touchSlop: kTouchSlop,
    )),
     child: ListView.separated(
       itemCount: 15,
       itemBuilder: (context, index) {
         return ListTile(
           title: Text('Item $index'),
        );
      },
       separatorBuilder: (context, index) {
         return const Divider(
           thickness: 3,
        );
      },
    ),
  );
}
}

最后我们看一下效果,如下 GIF 所示,现在就算你斜着滑动,也很触发 PageView 的水平滑动,只有横向移动时才会触发 PageView 的手势,当然, 如果要说这个粗暴的写法有什么问题的话,大概就是降低了 PageView 响应的灵敏度

xiehuabudong

同方向 PageView 嵌套 ListView

介绍完常规使用,接着来点不一样的,在垂直切换的 PageView 里嵌套垂直滚动的 ListView , 你第一感觉是不是觉得不靠谱,为什么会有这样的场景?

对于产品来说,他们不会考虑你如何实现的问题,他们只会拍着脑袋说淘宝可以,为什么你不行,所以如果是你,你会怎么做?

而关于这个需求,社区目前讨论的结果是:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理

如果对实现逻辑分析没兴趣,可以直接看本小节末尾的 源码链接

看到自己管理先不要慌,虽然要自己实现 PageViewListView 的手势分发,但是其实并不需要重写 PageViewListView ,我们可以复用它们的 Darg 响应逻辑,如下代码所示:

  • 通过 NeverScrollableScrollPhysics 禁止了 PageViewListView 的滚动效果

  • 通过顶部 RawGestureDetectorVerticalDragGestureRecognizer 自己管理手势事件

  • 配置 PageControllerScrollController 用于获取状态

body: RawGestureDetector(
 gestures: <Type, GestureRecognizerFactory>{
   VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
           VerticalDragGestureRecognizer>(
      () => VerticalDragGestureRecognizer(),
      (VerticalDragGestureRecognizer instance) {
     instance
      ..onStart = _handleDragStart
      ..onUpdate = _handleDragUpdate
      ..onEnd = _handleDragEnd
      ..onCancel = _handleDragCancel;
  })
},
 behavior: HitTestBehavior.opaque,
 child: PageView(
   controller: _pageController,
   scrollDirection: Axis.vertical,
   ///屏蔽默认的滑动响应
   physics: const NeverScrollableScrollPhysics(),
   children: [
     ListView.builder(
       controller: _listScrollController,
       ///屏蔽默认的滑动响应
       physics: const NeverScrollableScrollPhysics(),
       itemBuilder: (context, index) {
         return ListTile(title: Text('List Item $index'));
      },
       itemCount: 30,
    ),
     Container(
       color: Colors.green,
       child: Center(
         child: Text(
           'Page View',
           style: TextStyle(fontSize: 50),
        ),
      ),
    )
  ],
),
),

接着我们看 _handleDragStart 实现,如下代码所示,在产生手势 details 时,我们主要判断:

  • 通过 ScrollController 判断 ListView 是否可见

  • 判断触摸位置是否在 ListIView 范围内

  • 根据状态判断通过哪个 Controller 去生产 Drag 对象,用于响应后续的滑动事件

  void _handleDragStart(DragStartDetails details) {

   if (_listScrollController?.hasClients == true &&
       _listScrollController?.position.context.storageContext != null) {
     ///获取 ListView 的 renderBox
     final RenderBox? renderBox = _listScrollController
         ?.position.context.storageContext
        .findRenderObject() as RenderBox;

     if (renderBox?.paintBounds
            .shift(renderBox.localToGlobal(Offset.zero))
            .contains(details.globalPosition) ==
         true) {
       _activeScrollController = _listScrollController;
       _drag = _activeScrollController?.position.drag(details, _disposeDrag);
       return;
    }
  }

   ///这时候就可以认为是 PageView 需要滑动
   _activeScrollController = _pageController;
   _drag = _pageController?.position.drag(details, _disposeDrag);
}

前面我们主要在触摸开始时,判断需要响应的对象时ListView 还是 PageView ,然后通过 _activeScrollController 保存当然响应对象,并且通过 Controller 生成用于响应手势信息的 Drag 对象。

简单说:滑动事件发生时,默认会建立一个 Drag 用于处理后续的滑动事件,Drag 会对原始事件进行加工之后再给到 ScrollPosition 去触发后续滑动效果。

接着在 _handleDragUpdate 方法里,主要是判断响应是不是需要切换到 PageView:

  • 如果不需要就继续用前面得到的 _drag?.update(details)响应 ListView 滚动

  • 如果需要就通过 _pageController 切换新的 _drag 对象用于响应

void _handleDragUpdate(DragUpdateDetails details) {
 if (_activeScrollController == _listScrollController &&

     ///手指向上移动,也就是快要显示出底部 PageView
     details.primaryDelta! < 0 &&

     ///到了底部,切换到 PageView
     _activeScrollController?.position.pixels ==
         _activeScrollController?.position.maxScrollExtent) {
   ///切换相应的控制器
   _activeScrollController = _pageController;
   _drag?.cancel();

   ///参考 Scrollable 里
   ///因为是切换控制器,也就是要更新 Drag
   ///拖拽流程要切换到 PageView 里,所以需要 DragStartDetails
   ///所以需要把 DragUpdateDetails 变成 DragStartDetails
   ///提取出 PageView 里的 Drag 相应 details
   _drag = _pageController?.position.drag(
       DragStartDetails(
           globalPosition: details.globalPosition,
           localPosition: details.localPosition),
       _disposeDrag);
}
 _drag?.update(details);
}

这里有个小知识点:如上代码所示,我们可以简单通过 details.primaryDelta 判断滑动方向和移动的是否是主轴

最后如下 GIF 所示,可以看到 PageView 嵌套 ListView 同方向滑动可以正常运行了,但是目前还有个两个小问题,从图示可以看到:

  • 在切换之后 ListView 的位置没有保存下来

  • 产品要求去除 ListView 的边缘溢出效果

7777777777777

所以我们需要对 ListView 做一个 KeepAlive ,然后用简单的方法去除 Android 边缘滑动的 Material 效果:

  • 通过 with AutomaticKeepAliveClientMixinListView 在切换之后也保持滑动位置

  • 通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Scrollable 的边缘 Material 效果

child: PageView(
 controller: _pageController,
 scrollDirection: Axis.vertical,
 ///去掉 Android 上默认的边缘拖拽效果
 scrollBehavior:
     ScrollConfiguration.of(context).copyWith(overscroll: false),


///对 PageView 里的 ListView 做 KeepAlive 记住位置
class KeepAliveListView extends StatefulWidget {
 final ScrollController? listScrollController;
 final int itemCount;

 KeepAliveListView({
   required this.listScrollController,
   required this.itemCount,
});

 @override
 KeepAliveListViewState createState() => KeepAliveListViewState();
}

class KeepAliveListViewState extends State<KeepAliveListView>
   with AutomaticKeepAliveClientMixin {
 @override
 Widget build(BuildContext context) {
   super.build(context);
   return ListView.builder(
     controller: widget.listScrollController,

     ///屏蔽默认的滑动响应
     physics: const NeverScrollableScrollPhysics(),
     itemBuilder: (context, index) {
       return ListTile(title: Text('List Item $index'));
    },
     itemCount: widget.itemCount,
  );
}

 @override
 bool get wantKeepAlive => true;
}

所以这里我们有解锁了另外一个小技巧:通过 ScrollConfiguration.of(context).copyWith(overscroll: false) 快速去除 Android 滑动到边缘的 Material 2效果,为什么说 Material2, 因为 Material3 上变了,具体可见: Flutter 3 下的 ThemeExtensions 和 Material3

000000000

本小节源码可见: github.com/CarGuo/gsy_…

同方向 ListView 嵌套 PageView

那还有没有更非常规的?答案是肯定的,毕竟产品的小脑袋,怎么会想不到在垂直滑动的 ListView 里嵌套垂直切换的 PageView 这种需求。

有了前面的思路,其实实现这个逻辑也是异曲同工:PageViewListView 的滑动禁用,然后通过 RawGestureDetector 自己管理,不同的就是手势方法分发的差异。

RawGestureDetector(
         gestures: <Type, GestureRecognizerFactory>{
           VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<
                   VerticalDragGestureRecognizer>(
              () => VerticalDragGestureRecognizer(),
              (VerticalDragGestureRecognizer instance) {
             instance
              ..onStart = _handleDragStart
              ..onUpdate = _handleDragUpdate
              ..onEnd = _handleDragEnd
              ..onCancel = _handleDragCancel;
          })
        },
         behavior: HitTestBehavior.opaque,
         child: ListView.builder(
               ///屏蔽默认的滑动响应
               physics: NeverScrollableScrollPhysics(),
               controller: _listScrollController,
               itemCount: 5,
               itemBuilder: (context, index) {
                 if (index == 0) {
                   return Container(
                     height: 300,
                     child: KeepAlivePageView(
                       pageController: _pageController,
                       itemCount: itemCount,
                    ),
                  );
                }
                 return Container(
                     height: 300,
                     color: Colors.greenAccent,
                     child: Center(
                       child: Text(
                         "Item $index",
                         style: TextStyle(fontSize: 40, color: Colors.blue),
                      ),
                    ));
              }),
      )

同样是在 _handleDragStart 方法里,这里首先需要判断:

  • ListView 如果已经滑动过,就不响应顶部 PageView 的事件

  • 如果此时 ListView 处于顶部未滑动,判断手势位置是否在 PageView 里,如果是响应 PageView 的事件

  void _handleDragStart(DragStartDetails details) {
   if (_listScrollController.offset > 0) {
     _activeScrollController = _listScrollController;
     _drag = _listScrollController.position.drag(details, _disposeDrag);
     return;
  }
   if (_pageController.hasClients) {
     ///获取 PageView
     final RenderBox renderBox =
         _pageController.position.context.storageContext.findRenderObject()
             as RenderBox;

     ///判断触摸范围是不是在 PageView
     final isDragPageView = renderBox.paintBounds
        .shift(renderBox.localToGlobal(Offset.zero))
        .contains(details.globalPosition);

     ///如果在 PageView 里就切换到 PageView
     if (isDragPageView) {
       _activeScrollController = _pageController;
       _drag = _activeScrollController.position.drag(details, _disposeDrag);
       return;
    }
  }

   ///不在 PageView 里就继续响应 ListView
   _activeScrollController = _listScrollController;
   _drag = _listScrollController.position.drag(details, _disposeDrag);
}

接着在 _handleDragUpdate 方法里,判断如果 PageView 已经滑动到最后一页,也将滑动事件切换到 ListView


当然,同样还有 KeepAlive 和去除列表 Material 边缘效果,最后运行效果如下 GIF 所示。

22222222222

本小节源码可见:github.com/CarGuo/gsy_…

最后再补充一个小技巧:如果你需要 Flutter 打印手势竞技的过程,可以配置 debugPrintGestureArenaDiagnostics = true;来让 Flutter 输出手势竞技的处理过程

import 'package:flutter/gestures.dart';
void main() {
 debugPrintGestureArenaDiagnostics = true;
 runApp(MyApp());
}

image-20220613115808538

最后

最后总结一下,本篇介绍了如何通过 Darg 解决各种因为嵌套而导致的手势冲突,相信大家也知道了如何利用 ControllerDarg 来快速自定义一些滑动需求,例如 ListView 联动 ListView 的差量滑动效果:




44444444444444


作者:恋猫de小郭
来源:juejin.cn/post/7116267156655833102

收起阅读 »

跟我学企业级flutter项目:简化框架demo参考

前言最近很多人在问我,没有一个不错的demo,不会如何做单工程模式,如何封装网络请求,如何去做网络持久化。那么今天我将demo分享出来。现阶段还无法把我构建的flutter快速开发框架开源出来。暂时用简化demo来展示。 相关文章: 跟我学企业级fl...
继续阅读 »

前言

最近很多人在问我,没有一个不错的demo,不会如何做单工程模式,如何封装网络请求,如何去做网络持久化。那么今天我将demo分享出来。现阶段还无法把我构建的flutter快速开发框架开源出来。暂时用简化demo来展示。 相关文章: 跟我学企业级flutter项目:flutter模块化,单工程架构模式构思与实践

跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层

跟我学企业级flutter项目:dio网络框架增加公共请求参数&header

demo地址:

github.com/smartbackme…

为了大家更清楚的使用,我将对目录结构进行说明:

目录结构

在这里插入图片描述

以模块一来说明

在这里插入图片描述 模块一启动配置:

class MyConfiger extends ICommentConfiger{

@override
Widget getRouter(RouteSettings settings) {
var router = RouterPage.getRouter(settings);
if(router!=null){
return router;
}
return const NullRouter();
}

}
void main() {
Application.init(AppInit(MyConfiger()));
runApp(const MyApp());
}

公共模块说明

在这里插入图片描述

主工程启动说明

import 'package:commonmodule/commonmodule.dart';
import 'package:commonmodule/config.dart';
import 'package:flutter/material.dart';
import 'package:commonmodule/router_name.dart' as common;
import 'package:kg_density/kg_density.dart';
import 'package:myflutter/page/home.dart';
import 'package:onemodule/router_page.dart' as onemodule;
import 'package:twomodule/router_page.dart' as twomodule;

// 路由分配管理
class MyCommentConfiger extends ICommentConfiger{
@override
Widget getRouter(RouteSettings settings) {
if(settings.name == common.RouterName.home){
return const HomePage();
}
var teachertRouter = onemodule.RouterPage.getRouter(settings);
if(teachertRouter!=null){
return teachertRouter;
}
var clientRouter = twomodule.RouterPage.getRouter(settings);
if(clientRouter!=null){
return clientRouter;
}
return const NullRouter();

}


}

//启动初始化
void main() async {
MyFlutterBinding.ensureInitialized();
KgDensity.initKgDensity(designWidth : 375);
await SpSotre.instance.init();
ULogManager.init();
Application.init(AppInit(MyCommentConfiger()));
runApp(const MyApp());
}

//WidgetsFlutterBinding 配置
class MyFlutterBinding extends WidgetsFlutterBinding with KgFlutterBinding {

static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null) MyFlutterBinding();
return WidgetsBinding.instance!;
}
}


作者:王二蛋与他的张大花
链接:https://juejin.cn/post/7115236177136844808/
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

React Native ART

Android自带ART,不用导入。iOS要使用需要使用xcode打开react native 的ios目录,

1、使用xcode中打开react-native中的ios项目,选中‘Libraries’目录 ——> 右键选择‘Add Files to 项目名称’ ——> 'node_modules/react-native/Libraries/ART/ART.xcodeproj' 添加;


2、选中项目根目录 ——> 点击’Build Phases‘ ——> 点击‘Link Binary With Libraries’ ——> 点击左下方‘+’ ——> 选中‘libART.a’添加。


感谢奋斗的orange 提供,转载原文http://blog.csdn.net/u010940770/article/details/71126700

如果要使用svg作为渲染,使用react-native-art-svg

以下是个人记录:

1. svg的设计要使用局中描边;

2. 画扇形

import React from 'react'

import {

View,

ART

} from 'react-native'

const {Surface} = ART;

import Wedge from './Wedge'

export default class Fan extends React.Component{

render(){

return(

outerRadius={50}

startAngle={0}

endAngle={60}

originX={50}

originY={50}

fill="blue"/>

)

}

}



解析Python爬虫赚钱方式

Python爬虫怎么挣钱?解析Python爬虫赚钱方式,想过自己学到的专业技能赚钱,首先需要你能够数量掌握Python爬虫技术,专业能力强才能解决开发过程中出现的问题,Python爬虫可以通过Python爬虫外包项目、整合信息数据做产品、独立的自媒体三种方式挣钱。

Python爬虫怎么挣钱?

  一、Python爬虫外包项目

  网络爬虫最通常的的挣钱方式通过外包网站,做中小规模的爬虫项目,向甲方提供数据抓取,数据结构化,数据清洗等服务。新入行的程序员大多都会先尝试这个方向,直接靠技术手段挣钱,这是我们技术人最擅长的方式,因项目竞价的人太多,外包接单网站上的爬虫项目被砍到了白菜价也是常有的事。

  二、整合信息数据做产品

  利用Python爬虫简单说就是抓取分散的信息,整合后用网站或微信或APP呈现出来,以通过网盟广告,电商佣金,直接售卖电商产品或知识付费来变现。

三、最典型的就是找爬虫外包活儿

网络爬虫最通常的的挣钱方式通过外包网站,做中小规模的爬虫项目,向甲方提供数据抓取,数据结构化,数据清洗等服务。新入行的程序员大多都会先尝试这个方向,直接靠技术手段挣钱,这是我们技术人最擅长的方式,因项目竞价的人太多,外包接单网站上的爬虫项目被砍到了白菜价也是常有的事。

接着又去琢磨了其他的挣钱方法

四、爬数据做网站

那会儿开始接触运营,了解到一些做流量,做网盟挣钱的一些方法。挺佩服做运营的热,觉得鬼点子挺多的(褒义),总是会想到一些做流量的方法,但是他们就是需要靠技术去帮忙实现,去帮忙抓数据,那会我就在思考我懂做网站,抓数据都没问题,只要我能融汇运营技巧,就可以靠个人来挣钱钱了,于是就学习了一些SEO,和做社群的运营方法。

开始抓数据,来做网站挣钱,每个月有小几千块钱,虽然挣得不多,但做成之后不需要怎么维护,也算是有被动收入了。当然如果你技术学的还不够好,暂时就不要做了,可以先去小编的专栏简介的学习小天地,里面很多新教程项目多练习

五、去股市里浪一下【股市有风险,谨慎入市】

年龄越来越大了,有点余钱了就想投资一下,就去研究了下美股,买了一阵美股,挣了点钱,就想挣得更多,就在想有没有方法通过IT技术手段来辅助一下,那时喜欢买shopitify (类似国内的有赞)这类高成长,财报季股价波动大的股票。因为他是依附于facebook这类社交网站的,就是那些facebook上的网红可以用shopitify开店,来给他们的粉丝卖商品。

所以shopitify有个特点就是在社交媒体上的讨论量和相关话题度能反应一些这家公司这个季度的销售近况,这会影响它这个季度的财报,所以就想方设法就facebook上抓数据,来跟往期,历史上的热度来对比,看当季的财报是否OK,就用这种方法来辅助我买卖(是辅助,不是完全依靠)。

当初战绩还是可以,收益基本2-3倍于本金,心里挺喜滋滋的,后面由于我的风险控制意识不够,大亏了2次,亏到吐血。所以印证了那句话,股市有风险,谨慎入市。

六、在校大学生

最好是数学或计算机相关专业,编程能力还可以的话,稍微看一下爬虫知识,主要涉及一门语言的爬虫库、html解析、内容存储等,复杂的还需要了解URL排重、模拟登录、验证码识别、多线程、代理、移动端抓取等。由于在校学生的工程经验比较少,建议找一些少量数据抓取的项目,而不要去接一些监控类的项目、或大规模抓取的项目。慢慢来,步子不要迈太大。

七、在职人员

如果你本身就是爬虫工程师,挣钱很简单。如果你不是,也不要紧。只要是做IT的,稍微学习一下爬虫应该不难。

在职人员的优势是熟悉项目开发流程,工程经验丰富,能对一个任务的难度、时间、花费进行合理评估。可以尝试去找一些大规模抓取任务、监控任务、移动端模拟登录并抓取任务等,收益想对可观一些。

八、独立的自媒体号

  做公众号、自媒体、独立博客,学Python写爬虫的人越来越多,很多是非计算机科班出身。所以把用Python写爬虫的需求增大了,工作上的实践经验多一点,可以多写一些教程和学习经验总结。

以上就是关于Python爬虫赚钱的方式介绍,掌握专业技能除本职工作外还可以兼职接单哦。

  掌握python爬虫、Web前端、人工智能与机器学习、自动化开发、金融分析、网络编程等技能,零基础python找到工作也就不难了的哦。

本文转自: https://cloud.tencent.com/developer/article/1895384

两个textinput 切换不用点两下

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。如有侵权,请联系 cloudcommunity@tencent.com 删除。

核心代码

//添加手势监听

componentWillMount(){ 

this._panResponder = PanResponder.create({

onStartShouldSetPanResponder: () => true,

onPanResponderRelease: (evt,gs)=>{

console.log(gs);

{/*处理事件*/}

if (gs.y0<46+38 && gs.y0>46) {

this.refs.textInputs.focus()

};

}

})

}


将手势监听给一个组件

{...this._panResponder.panHandlers}

将组建和事件写出来

ref='textInputs'

onFocus={() => {this.refs.textInputs.focus()}}

即可

🌰

/**

 * Sample React Native App

 * https://github.com/facebook/react-native

 * @flow

 */

import React, { Component } from 'react'; 

import {

AppRegistry,

StyleSheet,

View,

ScrollView,

PanResponder,

TextInput,

Text

} from 'react-native';

export default class button extends Component {

constructor(props) {

//加载父类方法,不可省略

super(props);

//设置初始的状态

this.state = {

top:0,

left:0,

};

}

componentWillMount(){

this._panResponder = PanResponder.create({

onStartShouldSetPanResponder: () => true,

onPanResponderRelease: (evt,gs)=>{

console.log(gs);

if (gs.y0<46+38 && gs.y0>46) {

this.refs.textInputs.focus()

};

}

})

}

render(){

return (



{...this._panResponder.panHandlers}

keyboardShouldPersistTaps={false}>



联系方式







style={styles.telTextInput}

autoCapitalize = "none"

autoCorrect={false}

multiline = {true}

keyboardType = "default"

ref='textInputs'

placeholder = "请输入手机号或邮箱"

placeholderTextColor = "#999"

onFocus={() => {this.refs.textInputs.focus()}}

>





style={styles.telTextInput}

autoCapitalize = "none"

autoCorrect={false}

multiline = {true}

keyboardType = "default"

ref='textInput'

placeholder = "请输入手机号或邮箱"

placeholderTextColor = "#999"

onFocus={() => {this.refs.textInput.focus()}}

>







);

}

}

const styles = StyleSheet.create({

container: {

flex:1,

flexDirection: 'column',

//marginTop:64,

backgroundColor:'white'

},

line3:{

height:46,

paddingHorizontal:15,

paddingVertical:15,

borderBottomColor:'#E0E0E0',

borderBottomWidth:1

},

fdcontext:{

color:'#aaa',

fontSize:14

},

line5:{

flexDirection: 'column',

flex:1,

height: 38*2,

borderBottomColor:'#E0E0E0',

borderBottomWidth:1,

},

telTextInput:{

height:37,

fontSize: 12,

color:'#aaa',

paddingHorizontal:15,

paddingVertical:6,

}

});

AppRegistry.registerComponent('button', () => button);


原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

iOS block与__block、weak、__weak、__strong

iOS

首先需要知道:

block,本质是OC对象,对象的内容,是代码块。
封装了函数调用以及函数调用环境。

block也有自己的isa指针,依据block的类别不同,分别指向
__NSGlobalBlock __ ( _NSConcreteGlobalBlock )
__NSStackBlock __ ( _NSConcreteStackBlock )
__NSMallocBlock __ ( _NSConcreteMallocBlock )
需要注意是,ARC下只存在__NSGlobalBlock和__NSMallocBlock。
通常作为参数时,才可能是栈区block,但是由于ARC的copy作用,会将栈区block拷贝到堆上。
通常不管作为属性、参数、局部变量的block,都是__NSGlobalBlock,即使block内部出现了常量、静态变量、全局变量,也是__NSGlobalBlock,
除非block内部出现其他变量,auto变量或者对象属性变量等,就是__NSMallocBlock

为什么block要被拷贝到堆区,变成__NSMallocBlock,可以看如下链接解释:Ios开发-block为什么要用copy修饰

对于基础数据类型,是值传递,修改变量的值,修改的是a所指向的内存空间的值,不会改变a指向的地址。

对于指针(对象)数据类型,修改变量的值,是修改指针变量所指向的对象内存空间的地址,不会改变指针变量本身的地址
简单来说,基础数据类型,只需要考虑值的地址,而指针类型,则需要考虑有指针变量的地址和指针变量指向的对象的地址

以变量a为例

1、基础数据类型,都是指值的地址

1.1无__block修饰,

a=12,地址为A
block内部,a地址变B,不能修改a的值
block外部,a的地址依旧是A,可以修改a的值,与block内部的a互不影响
内外a的地址不一致

1.2有__block修饰

a=12,地址为A
block内部,地址变为B,可以修改a的值,修改后a的地址依旧是B
block外部,地址保持为B,可以修改a的值,修改后a的地址依旧是B

2、指针数据类型

2.1无__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,不能修改a指向的对象地址
block外部,a指针变量的地址为A,指向的对象地址为B,可以修改a指向的对象地址,
block外部修改后,
外部a指针变量的地址依旧是A,指向的对象地址变为D
内部a指针变量的地址依旧是C,指向的对象地址依旧是B

2.1有__block修饰

a=[NSObject new],a指针变量的地址为A,指向的对象地址为B
block内部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block外部,a指针变量的地址为C,指向的对象地址为B,能修改a指向的对象地址
block内外,或者另一个block中,无论哪里修改,a指针变量地址都保持为C,指向的对象地址保持为修改后的一致

block内修改变量的实质(有__block修饰):

block内部能够修改的值,必须都是存放在堆区的。
1、基础数据类型,__block修饰后,调用block时,会在堆区开辟新的值的存储空间,
指针数据类型,__block修饰后,调用block时,会在堆区开辟新的指针变量地址的存储空间

2、并且无论是基础数据类型还是指针类型,block内和使用block之后,变量的地址所有地址(包括基础数据类型的值的地址,指针类型的指针变量地址,指针指向的对象的地址),都是保持一致的
当然,只有block进行了真实的调用,才会在调用后发生这些地址的变化

另外需要注意的是,如果对一个已存在的对象(变量a),进行__block声明另一个变量b去指向它,
a的指针变量地址为A,b的指针变量会是B,而不是A,
原因很简单,不管有没__block修饰,不同变量名指向即使指向同一个对象,他们的指针变量地址都是不同的。

__weak,__strong

两者本身也都会增加引用计数。
区别在于,__strong声明,会在作用域区间范围增加引用计数1,超过其作用域然后引用计数-1
而__weak声明的变量,只会在其使用的时候(这里使用的时候,指的是一句代码里最终并行使用的次数),临时生成一个__strong引用,引用+次数,一旦使用使用完毕,马上-次数,而不是超出其作用域再-次数

    NSObject *obj = [NSObject new];
NSLog(@"声明时obj:%p, %@, 引用计数:%ld",&obj, obj, CFGetRetainCount((__bridge CFTypeRef)(obj)));
__weak NSObject *weakObj = obj;
NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));
NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

声明时obj:0x16daa3968, , 引用计数:1
声明时weakObj:0x16daa3960, ,, , 引用计数:5
声明后weakObj引用计数:2

这个5,是因为obj本来计数是1,

    NSLog(@"声明时weakObj:%p, %@,%@, %@, 引用计数:%ld",&weakObj, weakObj,weakObj,weakObj, CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句代码打印5,是因为除去&weakObj(&这个不是使用weakObj指向的对象,而只是取weakObj的指针变量地址,所以不会引起计数+1),另外还使用了4次weakObj,导致引用计数+4

   NSLog(@"声明后weakObj引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(weakObj)));

这句打印2,说明上一句使用完毕后,weakObj引用增加的次数会马上清楚,重新变回1,而这句使用了一次weakObj,加上obj的一次引用,就是2了

__weak 与 weak

通常,__weak是单独为某个对象,添加一条弱引用变量的。
weak则是property属性里修饰符。

LGTestBlockObj *testObj = [LGTestBlockObj new];
self.prpertyObj = testObj;
__weak LGTestBlockObj *weakTestObj = testObj;
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), self.prpertyObj,self.prpertyObj,self.prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(self.prpertyObj)));
NSLog(@"prpertyObj:%p, %@,%@, %@, 引用计数:%ld",&(_prpertyObj), _prpertyObj,_prpertyObj,_prpertyObj, CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"prpertyObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(_prpertyObj)));
NSLog(@"testObj:, 引用计数:%ld", CFGetRetainCount((__bridge CFTypeRef)(testObj)));
NSLog(@"weakTestObj:%p, %@,%@, %@, 引用计数:%ld",&weakTestObj, weakTestObj,weakTestObj,weakTestObj, CFGetRetainCount((__bridge CFTypeRef)(weakTestObj)));

prpertyObj:0x1088017b0, ,, , 引用计数:2
prpertyObj:, 引用计数:2
testObj:, 引用计数:2
weakTestObj:0x16b387958, ,, , 引用计数:6

待补充...

Block常见疑问收录

1、block循环引用

通常,block作为属性,并且block内部直接引用了self,就会出现循环引用,这时就需要__weak来打破循环。

2、__weak为什么能打破循环引用?

一个变量一旦被__weak声明后,这个变量本身就是一个弱引用,只有在使用的那行代码里,才会临时增加引用结束,一旦那句代码执行完毕,引用计数马上-1,所以看起来的效果是,不会增加引用计数,block中也就不会真正持有这个变量了

3、为什么有时候又需要使用__strong来修饰__weak声明的变量?

在block中使用__weak声明的变量,由于block没有对该变量的强引用,block执行的过程中,一旦对象被销毁,该变量就是nil了,会导致block无法继续正常向后执行。
使用__strong,会使得block作用区间,保存一份对该对象的强引用,引用计数+1,一旦block执行完毕,__strong变量就会销毁,引用计数-1
比如block中,代码执行分7步,在执行第二步时,weak变量销毁了,而第五步要用到weak变量。
而在block第一步,可先判断weak变量是否存在,如果存在,加一个__strong引用,这样block执行过程中,就始终存在对weak变量的强引用了,直到block执行完毕

4、看以下代码,obj对象最后打印的引用计数是多少,为什么?

    NSObject *obj = [NSObject new];
void (^testBlock)(void) = ^{
NSLog(@"%@",obj);
};
NSLog(@"引用计数:%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

最后的打印的是3
作为一个局部变量的block,由于引用了外部变量(非静态、常量、全局),定义的时候其实是栈区block,但由于ARC机制,使其拷贝到堆上,变成堆block,所以整个函数执行的过程中,实际上该block,存在两份,一个栈区,一个堆区,这就是使得obj引用计数+2了,加上创建obj的引用,就是3了

5、为什么栈区block要copy到堆上

block:我们称代码块,他类似一个方法。而每一个方法都是在被调用的时候从硬盘到内存,然后去执行,执行完就消失,所以,方法的内存不需要我们管理,也就是说,方法是在内存的栈区。所以,block不像OC中的类对象(在堆区),他也是在栈区的。如果我们使用block作为一个对象的属性,我们会使用关键字copy修饰他,因为他在栈区,我们没办法控制他的消亡,当我们用copy修饰的时候,系统会把该 block的实现拷贝一份到堆区,这样我们对应的属性,就拥有的该block的所有权。就可以保证block代码块不会提前消亡。