某勒个杰

某勒个杰 查看完整档案

西安编辑  |  填写毕业院校无业游民  |  PHP 编辑 www.upupjie.cn 编辑
编辑

To be a better man.

个人动态

某勒个杰 收藏了文章 · 2月2日

Vue 生命周期详解

Vue 生命周期详解

Vue 生命周期流程

最开始,用户使用 new Vue() 创建根 Vue 实例,或者 Vue 实例化子组件都会调用_init方法(我们将这两种实例都称为vm):

function Vue(options) {        //Vue 构造函数
    ...
    this._init(options)
}
...
const Sub = function (options) {  // 定义子组件构造函数
    this._init(options)
}

vm实例化时会调用原型方法this._init方法进行初始化:

Vue.prototype._init = function(options) {
    vm.$options = mergeOptions(  // 合并options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
    ...
    initLifecycle(vm) // 开始一系列的初始化
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')        //执行 beforeCreate 钩子
    initInjections(vm)
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')                    //执行 created 钩子
    ...
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}

beforeCreate

首先,将用户提供的options对象,父组件定义在子组件上的eventprops(子组件实例化时),vm原型方法,和Vue构造函数内置的选项合并成一个新的options对象,赋值给vm.$options
接下来,执行 3 个初始化方法:

  1. initLifecycle(vm): 主要作用是确认组件的父子关系和初始化某些实例属性。找到父组件实例赋值给vm.$parent,将自己push给父组件的$children
  2. initEvents(vm): 主要作用是将父组件使用v-on@注册的自定义事件添加到子组件的私有属性vm._events中;
  3. initRender(vm): 主要作用是初始化用来将render函数转为vnode的两个方法vm._cvm.$createElement。用户自定义的render函数的参数h就是vm.$createElement方法,它可以返回vnode。等以上操作全部完成,就会执行beforeCreate钩子函数,此时用户可以在函数中通过this访问到vm.$parentvm.$createElement等有限的属性和方法。

created

接下来会继续执行 3 个初始化方法:

  1. initInjections(vm): 初始化inject,使得vm可以访问到对应的依赖;
  2. initState(vm): 初始化会被使用到的状态,状态包括propsmethodsdatacomputedwatch五个选项。调用相应的init方法,使用vm.$options中提供的选项对这些状态进行初始化,其中initData方法会调用observe(data, true),实现对data中属性的监听,实际上是使用Object.defineProperty方法定义属性的gettersetter方法;
  3. initProvide(vm):初始化provide,使得vm可以为子组件提供依赖。

这 3 个初始化方法先初始化inject,然后初始化props/data状态,最后初始化provide,这样做的目的是可以在props/data中使用inject内所注入的内容。
等以上操作全部完成,就会执行created钩子函数,此时用户可以在函数中通过this访问到vm中的propsmethodsdatacomputedwatchinject等大部分属性和方法。

beforeMount

如果用户在创建根 Vue 实例时提供了el选项,那么在实例化时会直接调用vm.$mount方法开始挂载:

if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

如果未提供el选项,则需要用户手动调用vm.$mount方法开挂载。vm.$mount方法:

运行时版本:
Vue.prototype.$mount = function(el) { // 最初的定义
    return mountComponent(this, query(el));
}
完整版:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(el) {  // 拓展编译后的
    var options = this.$options;
    if(!options.render) {
        if(options.template) {
            ...                //一些判断
        } else if (el) {    //传入的 el 选项不为空
            options.template = getOuterHTML(el);
        }
        
        if (options.template) {
                options.render = compileToFunctions(template, ...).render    //将 template 编译成 render 函数
        }
    }
    ...
    return mount.call(this, query(el))    //即 Vue.prototype.$mount.call(this, query(el))
}

在完整版的vm.$mount方法中,如果用户未提供render函数,就会将template或者el.outerHTML编译成render函数。
然后会执行mountComponent函数:

export function mountComponent(vm, el) {
    vm.$el = el
    ...
    callHook(vm, 'beforeMount')
    ...
    const updateComponent = function () {
        vm._update(vm._render())    // 调用 render 函数生成 vnode,并挂载到 HTML中
    }
    ...
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, 'mounted');
    }
}

如果用户提供了el选项,则会获取用于挂载的真实节点,将此节点赋值给vm.$el属性。
等以上操作全部完成,就会执行beforeMount钩子函数,如果用户提供了el选项,此时在函数中可以通过this访问到vm.$el属性,此时它的值为el提供的真实节点。

mounted

mountComponent方法中,会执行vm._render方法获取vnode

Vue.prototype._render = function() {
    const vm = this
    const { render } = vm.$options

    const vnode = render.call(vm, vm.$createElement)
    
    return vnode
}

vm._render方法中会调用vm.$options.render函数,传入实参vm.$createElement(对应声明render函数时的形参h),得到返回结果vnode
在执行一个如下的render函数的过程中:

render(h) {
    return h(
        "div",    //标签名
        [                //子节点数组
            [
                [h("h1", "title h1")],    //子节点也是通过 h 函数生成 vnode 的
                [h('h2', "title h2")]
            ],
            [
                h(obj, [                //子组件传入 obj 而不是标签名
                    h("p", "paragraph")
                ])
            ]
        ]
    );
}

执行render函数的过程就是递归调用h函数的过程,h函数会根据子组件的options选项对象生成一个vnode,以便之后将它转化为真实节点。

不管是根节点挂载时首次渲染,还是在数据改变后更新页面,都会调用updateComponent方法。_render方法返回的vnode是一个树形结构的JavaScript对象,接下来在updateComponent中会调用_update将这棵虚拟DOM树转化为真实的DOM树:

const updateComponent = function () {
    vm._update(vm._render())    // 调用 render 函数生成 vnode,并挂载到 HTML中
}

vm._update方法会将vm.__patch__方法返回的真实Dom节点赋值给vm.$el

Vue.prototype._update = function(vnode) {
    ...
    if (!prevVnode) {
        // 首次渲染
        vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
        // 更新
        vm.$el = vm.__patch__(prevVnode, vnode);
    }
    ...
}

vm.__patch__方法传入的参数vm.$el是之前在mountComponent方法中赋值的真实Dom元素,是挂载对象。vm.__patch__会生成并插入真实Dom

Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules }) 

nodeOps是一些操作原生Dom的方法的集合,modulesclass/attrs/style等属性创建、更新、销毁时相应钩子方法的集合,而createPatchFunction函数返回了一个patch函数:

export function createPatchFunction(backend) {
    ...
    const { modules, nodeOps } = backend
    
    return function patch (oldVnode, vnode) {  // 接收新旧 vnode 的 `patch`函数
        ...
        //isDef 函数 : (v) => v !== undefined && v !== null
        const isRealElement = isDef(oldVnode.nodeType) // 是否是真实 Dom
        if(isRealElement) {  // 首次渲染传入的 vm.$el 是真实 Dom
            oldVnode = emptyNodeAt(oldVnode)  // 将 vm.$el 转为 VNode 格式
        }
        ...
    }
}

调用emptyNodeAt函数将传入的vm.$el转化为VNode格式。VNodeVue定义的虚拟节点类,vnodeVNode类的实例对象。

function emptyNodeAt(elm) {
    return new VNode(
        nodeOps.tagName(elm).toLowerCase(), // 对应tag属性
        {},  // 对应data
        [],   // 对应children
        undefined,  //对应text
        elm  // 真实dom赋值给了elm属性
    )
}
包装后的:
{
    tag: 'div',
    elm: '<div id="app"></div>' // 真实dom
}

然后继续创建真实Dom

export function createPatchFunction(backend) { 
    ...
    return function patch (oldVnode, vnode) {
        const insertedVnodeQueue = []        //用于缓存 insertedVnode
        ...
        const oldElm = oldVnode.elm  //包装后的真实 Dom <div id='app'></div>
        const parentElm = nodeOps.parentNode(oldElm)  // 首次父节点为<body></body>
        
        createElm(  // 创建真实 Dom
            vnode, // 传入的 vnode
            insertedVnodeQueue,  // 空数组
            parentElm,  // <body></body>
            nodeOps.nextSibling(oldElm)  // 下一个兄弟节点
        )
        
        return vnode.elm // 返回真实 Dom ,之后在 _update 中覆盖 vm.$el
    }
}

createElm方法根据节点类型生成真实Dom节点,并插入parentElm中。而createElm方法在创建元素节点的过程中,会调用createChildren方法创建子节点,而createChildren方法又会调用createElm方法生成子节点的真实Dom节点,形成了createElm方法的递归调用:

function createElm(vnode, insertedVnodeQueue, parentElm, ...) {
    ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {    //此时可忽略这一步
        return 
    }
    ...
    // 如果要创建的节点是元素节点
    vnode.elm = nodeOps.createElement(tag)  // 先创建一个空元素用于挂载子节点
    createChildren(vnode, children, insertedVnodeQueue)  // 调用 `createChildren` 方法创建子节点
    insert(parentElm, vnode.elm, refElm)  // 将真实元素 vnode.elm 插入父节点中
    ...
}

递归创建子节点,插入父节点,最终生成vm的真实Dom节点vnode.elm
等以上操作全部完成,就会执行mounted钩子函数,此时在函数中可以通过this访问到vm.$el属性,此时它为虚拟vnode转化而来的真实Dom

activated

如果我们研究的实例vm是一个组件实例,而且它被<keep-alive>组件包裹,那么它将额外具有两个钩子函数activateddeactivated。我们假设vm是根 Vue 实例root的一个后代组件。
root挂载时,会在它的patch方法中调用createElm方法生成真实Dom节点并插入<body>root的父节点)。
如果有子节点,会先调用createChildren方法,在createChildren中通过createElm方法生成每个子节点的真实Dom节点,再将子Dom节点插入rootDom节点中:

function createChildren(vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
        for (var i = 0; i < children.length; ++i) {
            createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);    // 实参 vnode.elm 传给 parentElm 形参
        }
    }
    ...
}

所以再次回到上面的createElm方法,此时它被用于创建子节点,如果子节点为组件,在createElm中会调用createComponent方法对子组件进行初始化,生成子组件实例(假设就是vm),初始化子组件调用的是 init 钩子(vnode 有 4 个 management hookinit, prepatch, insertdestroy,在 render 函数生成 vnode 时会加载到 vnode.data.hook 上)。

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    var i = vnode.data;
    if (isDef(i)) {
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            i(vnode, false /* hydrating */ );    // 暂停执行 createComponent,开始调用 vnode.data.hook.init 钩子进行初始化
        }
        if (isDef(vnode.componentInstance)) {
            // 等 init 钩子执行完再执行,此时 vm 已执行完 $mount 方法,所以在 initComponent 方法中将 vnode push 到 insertedVnodeQueue 中
            initComponent(vnode, insertedVnodeQueue);    
            insert(parentElm, vnode.elm, refElm);    // 将真实元素 vnode.elm 插入父节点中
            if (isTrue(isReactivated)) {
                reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
            }
            return true
        }
    }
}

init 钩子中调用 Sub构造函数实例化子组件:

init: function init(vnode, hydrating) {
    ...
    //调用 `Sub`构造函数实例化子组件,执行 `beforeCreate` 和 `created` 钩子
    var child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance);
    //调用 vm.$mount,执行 `beforeMount` 钩子,然后执行 updateComponent,重复上面的流程
    child.$mount(hydrating ? vnode.elm : undefined, hydrating);    
},

初始化完成后,会调用子组件实例vm$mount方法进行挂载,执行patch方法,在vmpatch方法中又会调用createElm方法生成真实Dom,这时子组件实例会难以避免地再次执行createComponent方法:

function createElm(vnode, insertedVnodeQueue, parentElm, refElm) { 
    ...
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {    // 如果子节点为组件,调用 createComponent 方法对子组件进行初始化;之后在子组件的 `patch` 方法中又会调用 `createElm` 方法
          return      
    }    
    //继续创建真实节点
    ...    
    vnode.elm = nodeOps.createElement(tag) 
    createChildren(vnode, children, insertedVnodeQueue);    //从这里开始暂停,在 createChildren 中 createElm 子节点
    insert(parentElm, vnode.elm, refElm);        //将真实元素 vnode.elm 插入父节点中
    ...
}

这个时候createComponent不会执行初始化操作,而是直接返回undefined,这样就可以继续创建真实节点,如果后代还有组件,又是一个循环……
所以,父子节点的创建、挂载钩子执行顺序为:
beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount

回到mounted生命周期的createPatchFunction方法,在它返回的patch方法中,私有变量insertedVnodeQueue用于存储这些插入的后代组件的vnode

function patch() {
    var insertedVnodeQueue = [];
    ...
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);    //调用 insert 钩子
    return vnode.elm    //真实 Dom 元素
}
...
//`patch`方法就是 _update 中的 __patch__ 方法,
//它返回真实 Dom 元素给根 Vue 实例的 $el,之后会在 mountComponent 中调用根 Vue 实例的 mounted 钩子(具体看前面 mountComponent 和 _update 方法)
root.$el = root.__patch__(...)    // _update 中
...
callHook(root, 'mounted');        // mountComponent 中

vmroot的后代,vm.$vnode也在root实例的patch方法的insertedVnodeQueue中。在invokeInsertHook函数中,会调用这些vnodeinsert钩子:

function invokeInsertHook(vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    if (isTrue(initial) && isDef(vnode.parent)) {    
        vnode.parent.data.pendingInsert = queue;    //缓存 insertedVnode
    } else {
        //只有最初的实例的 initial 为 false,所以会延迟到根 Vue 实例 patch 方法的末尾调用所有后代组件的 insert 钩子
        for (var i = 0; i < queue.length; ++i) {
            queue[i].data.hook.insert(queue[i]);    //调用缓存的 insertedVnode 的 insert 钩子
        }
    }
}

假如当前调用的是vm.$vnode.data.hook.insert方法:

insert: function insert(vnode) {    //传入 vm.$vnode
    var context = vnode.context;        //父组件实例
    var componentInstance = vnode.componentInstance;    //vnode 对应的组件实例 vm
    if (!componentInstance._isMounted) {
        componentInstance._isMounted = true;
        callHook(componentInstance, 'mounted');    //调用 vm 的 mounted 钩子函数(所以子组件的 mounted 钩子先于父组件被调用)
    }
    if (vnode.data.keepAlive) {        //true
        if (context._isMounted) {
            // 父组件更新中
            queueActivatedComponent(componentInstance);    // 父组件更新时,将 `vm` push 到 Vue 全局变量 activatedChildren 中,等待执行 `activated` 钩子函数
        } else {
            // 父组件挂载中
            activateChildComponent(componentInstance, true /* direct */ );    //调用 `vm` 的 `activated` 钩子函数
        }
    }
}

由此可知,Vue会按照root实例的patch方法的insertedVnodeQueuevnode的顺序执行mounted钩子。而在节点树中,越底端的组件越先创建好完好的真实Dom节点并插入父Dom节点中,其vnode也越先被pushinsertedVnodeQueue中,所以越先执行它的mounted钩子。
所以,完整的父子节点的创建、挂载钩子执行顺序为:
beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted

vm.$vnode.data.hook.insert方法中调用的activateChildComponent函数会调用vm及其后代组件的activated钩子函数:

function activateChildComponent(vm, direct) {
    ...
    if (vm._inactive || vm._inactive === null) {
        vm._inactive = false;
        for (var i = 0; i < vm.$children.length; i++) {
            activateChildComponent(vm.$children[i]);    //递归调用子组件的 activated 钩子
        }
        callHook(vm, 'activated');        //调用 vm 的 activated 钩子
    }
}

vm首次挂载,调用mounted钩子函数后,会马上调用activated钩子函数。
之后vmactivated钩子函数会在 keep-alive 组件激活时调用激活时被调用,具体调用时机是在flushSchedulerQueue函数执行完queue中所有的watchers后。

deactivated

vmdeactivated钩子函数会在 keep-alive 组件停用时被调用。
patch方法的最后,会删除旧节点:

function patch() {
    ...
    removeVnodes(parentElm, [oldVnode], 0, 0);        // 在 removeVnodes 中调用 invokeDestroyHook(oldVnode) 删除旧节点
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
    return vnode.elm
}

如果要删除的vnodedestroy钩子,则调用vnode.data.hook.destroy

function invokeDestroyHook(vnode) {
    var i, j;
    var data = vnode.data;
    if (isDef(data)) {
        if (isDef(i = data.hook) && isDef(i = i.destroy)) {
            i(vnode);        //调用 vnode.data.hook.destroy 钩子
        }
        ...
    }
}
destroy: function destroy(vnode) {
    var componentInstance = vnode.componentInstance;
    if (!componentInstance._isDestroyed) {
        if (!vnode.data.keepAlive) {                                
            componentInstance.$destroy();        //    调用 vm.$destroy()
        } else {
            deactivateChildComponent(componentInstance, true /* direct */ );    //调用子组件的 'deactivated' 钩子
        }
    }
}

调用`vmdeactivated钩子,递归调用子组件的deactivated 钩子:

function deactivateChildComponent() {
    ...
    for (var i = 0; i < vm.$children.length; i++) {
        deactivateChildComponent(vm.$children[i]);        //递归调用子组件的 'deactivated' 钩子
    }
    callHook(vm, 'deactivated');        //调用 'deactivated' 钩子
    ...
}

这些操作在父组件的patch方法中执行,父组件patch后,会调用mounted或者updated钩子。

beforeUpdate

每个组件实例都对应一个watcher实例,它是在mountComponent方法中,在调用mounted钩子之前实例化的:

export function mountComponent(vm, el) {
    ...
    callHook(vm, 'beforeMount')
    ...
    const updateComponent = function () {
        vm._update(vm._render(), hydrating);
    };
    new Watcher(vm, updateComponent, noop, {
        before: function before () {                    //在 run 之前执行
         if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate');            // beforeUpdate 钩子等待执行
            }
        }
    }, true /* isRenderWatcher */);
    ...
    callHook(vm, 'mounted');
}

如果是RenderWatchervm._watcher会用它赋值:

var Watcher = function Watcher (vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm;                    //关联组件
    if (isRenderWatcher) {
        vm._watcher = this;
    }
    vm._watchers.push(this);
    ...
    this.before = options.before;
    ...
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;        //即 vm._watcher.getter = updateComponent
    }
    this.value = this.lazy ? undefined : this.get();    //this.get 中会调用 this.getter,所以 new Watcher 就立即调用 updateComponent
}

watcher会在组件渲染的过程中把接触过的数据属性记录为依赖。之后当依赖的值发生改变,触发依赖的setter方法时,会通知watcher,从而使它关联的组件(vm)重新渲染。
一旦侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。
等当前事件循环结束,下一次事件循环开始,Vue会刷新队列并执行已去重的工作。Vue会尝试使用Promise.thenMutationObserversetImmediate发布的微任务来执行queue中的watcher

function flushSchedulerQueue () {
    queue.sort(function (a, b) { return a.id - b.id; });    //queue 是在 Vue 构造函数中的声明的变量
    ...
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        if (watcher.before) {
            watcher.before();        //执行 beforeUpdate 钩子函数
        }
        id = watcher.id;
        has[id] = null;
        watcher.run();    //执行 watcher
        ...
    }
    ...
    // call component updated and activated hooks
    callActivatedHooks(activatedChildren.slice());    //执行 activated 钩子函数
    callUpdatedHooks(queue.slice());        //执行 updated 钩子函数
}

刷新前根据 idqueue 中的 watcher 进行排序。这样可以确保:

  1. watcher排在子watcher前,组件从父级更新到子级。(因为父母总是在子级之前创建,所以id更小);
  2. 在一个组件中,用户声明的watchers总是在render watcher之前执行,因为user watchers更先创建;
  3. 如果在父组件的watcher运行期间,销毁了某个子组件,可以跳过该子组件的watcher

在执行watcher.run方法之前,会执行watcher.before方法,从而执行beforeUpdate钩子函数。

updated

在执行watcher.run方法时,会调用watcher.getter方法,而其中某个watcher(vm._watcher)关联的就是我们的vm,它的getter是可以更新vmupdateComponent方法:

Watcher.prototype.run = function run () {
        if (this.active) {
            var value = this.get();        //调用 watcher.get 方法
            ...
        }
        ...
}
Watcher.prototype.get = function get () {
        ...
        try {
            value = this.getter.call(vm, vm);    //调用 watcher.getter 方法
        }
        ...
}

调用updateComponent方法

updateComponent = function () {
    vm._update(vm._render(), hydrating);
};

vm._render方法会重新执行render函数生成vnode,然后vm._update方法会将vnode转化为真实Dom,挂载到HTML中,并覆盖vm.$el
等以上操作全部完成,在flushSchedulerQueue函数的最后会执行子组件的activated钩子函数和vmupdated钩子函数:

function flushSchedulerQueue () {
    ...
    callActivatedHooks(activatedChildren.slice());    //执行 activated 钩子函数
    callUpdatedHooks(queue.slice());        //执行 updated 钩子函数
}

function callUpdatedHooks (queue) {
    var i = queue.length;
    while (i--) {
        var watcher = queue[i];
        var vm = watcher.vm;
        if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'updated');    //执行 updated 钩子函数
        }
    }
}

updated钩子函数中通过this.$el访问到的vm.$el属性的值为更新后的真实Dom
beforeUpdateupdated钩子函数的执行顺序真好相反,因为在flushSchedulerQueue函数中是索引递增处理queue中的watcher的,所以执行beforeUpdate钩子函数的顺序和queuewatcher的顺序相同;而在callUpdatedHooks函数中是按索引递减的顺序执行_watcher关联实例的updated钩子的,和queue_watcher顺序相反。
再加上父watcher排在子watcher前,所以如果父、子组件在同一个事件循环中更新,那么生命周期钩子的执行顺序为:
beforeUpdate => 子beforeUpdate => 子updated => 父updated

beforeDestroy

调用vm.$destroy销毁vm实例:

Vue.prototype.$destroy = function() {
    var vm = this;
    if (vm._isBeingDestroyed) {
        return
    }
    callHook(vm, 'beforeDestroy');
    vm._isBeingDestroyed = true;
    
    // remove self from parent
    var parent = vm.$parent;
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
        remove(parent.$children, vm);
    }
    
    // teardown watchers
    if (vm._watcher) {
        vm._watcher.teardown();
    }
    var i = vm._watchers.length;
    while (i--) {
        vm._watchers[i].teardown();
    }
    
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
        vm._data.__ob__.vmCount--;
    }
    
    // call the last hook...
    vm._isDestroyed = true;
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null);
    // fire destroyed hook
    callHook(vm, 'destroyed');
    // turn off all instance listeners.
    vm.$off();
    // remove __vue__ reference
    if (vm.$el) {
        vm.$el.__vue__ = null;
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
        vm.$vnode.parent = null;
    }
};

在调用beforeDestroy钩子前未进行销毁操作,所以在这一步,实例仍然完全可用。

destroyed

vm.$destroy执行的操作有

  1. 删除vm.$parent.$children中的vm
  2. 销毁vm._watcher(渲染 watcher),销毁vm._watchers[i]中的所有watcher
  3. 删除数据 observer 中的引用;
  4. 调用destroyed钩子函数;
  5. ...

其中vm.__patch__(vm._vnode, null)可以销毁所有子实例。

Vue 生命周期流程图

Vue 生命周期流程图

Vue 父子组件生命周期钩子执行顺序

  1. 父子组件挂载过程:父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted
  2. 子组件被keep-alive组件包裹(忽视keep-alive组件),父子组件挂载过程:父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 子activated => 父mounted
  3. 只修改父组件或子组件的数据:beforeUpdate => updated
  4. 在同一事件循环中修改父子组件的数据(无论先后):父beforeUpdate => 子beforeUpdate => 子updated => 父updated
  5. 父组件将数据传给子组件的一个 prop,且它们分别是父、子组件的依赖,在修改父组件的数据时:父beforeUpdate => 子beforeUpdate => 子updated => 父updated
  6. 子组件的v-show指令绑定父组件的数据,在修改父组件的数据时:父beforeUpdate => 父updated,子组件保持mounted状态不变;
  7. 子组件的v-show指令绑定父组件的数据,子组件被keep-alive组件包裹,在修改父组件的数据时:父beforeUpdate => 父updated,子组件保持activated状态不变;
  8. 子组件的v-if指令绑定父组件的数据,在修改父组件的数据时:

    1. true => false: 父beforeUpdate => 子beforeDestroy => 子destroyed => 父updated
    2. false => true: 父beforeUpdate => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父updated
  9. 子组件的v-if指令绑定父组件的数据,子组件被keep-alive组件包裹,在修改父组件的数据时:

    1. true => false: 父beforeUpdate => 子deactivated => 父updated
    2. 首次 false => true: 父beforeUpdate => 子beforeCreate => 子created => 子beforeMount => 子mounted => 子activated => 父updated
    3. 再次 false => true: 父beforeUpdate => 子activated => 父updated
  10. 子组件的is属性绑定父组件的数据,父组件将子组件一切换为子组件二:
    beforeUpdate => 子二beforeCreate => 子二created => 子二beforeMount => 子二mounted => 父beforeUpdate => 子一beforeDestroy => 子一destroyed => 父updated => 父updated
  11. 子组件的is属性绑定父组件的数据,子组件被keep-alive组件包裹,父组件将子组件一切换为子组件二:

    1. 首次:父beforeUpdate => 父beforeUpdate => 子二beforeCreate => 子二created => 子二beforeMount => 子一deactivated => 子二mounted => 子二activated => 父updated => 父updated
    2. 再次:父beforeUpdate => 子一deactivated => 子二activated => 父updated

动态组件触发两次父beforeUpdateupdated的原因:
在第一次事件循环只触发了一次父组件的_watcher,在调用 render函数重新生成父组件vnode的过程中:

var render = function() {    //Vue 编译 template 而来的 render 函数
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c("img", {
        attrs: { alt: "Vue logo", src: require("./assets/logo.png") }
      }),
      _c("p", [_vm._v(_vm._s(_vm.message))]),
            //被 keep-alive 组件包裹的情况,在生成 keep-alive 组件的 vnode 时,第二次触发了父组件的`_watcher`
      _c("keep-alive", [_c(_vm.now, { tag: "component" })], 1)
            //不被 keep-alive 组件包裹的情况,在生成子二组件的`vnode`时,第二次触发了父组件的`_watcher`
            _c(_vm.now, { tag: "component" })
        ],
    1
  )
}

其实 keep-alive 组件情况更具体一点,也是在生成 keep-alive 组件的孩子,子二组件的vnode时触发的_watcher
然后这个watcher会被插到queue中当前wacther的后面(根据 wacther.id的大小插入正确的位置):

function queueWatcher(watcher) {
    var id = watcher.id;
    if (has[id] == null) {    //在 flushSchedulerQueue 中,执行 watcher.run 之前,已经令 has[id] = null;
        has[id] = true;        //所以同 id 的 wacther 可以被插入 queue 中
        if (!flushing) {
            queue.push(watcher);
        } else {
            // if already flushing, splice the watcher based on its id
            // if already past its id, it will be run next immediately.
            var i = queue.length - 1;
            while (i > index && queue[i].id > watcher.id) {
                i--;
            }
            queue.splice(i + 1, 0, watcher);
        }
    }
    ...
}

等当前watcher.run执行完,再执行它。

查看原文

某勒个杰 收藏了文章 · 2020-10-10

Go 加密解密算法总结

前言

加密解密在实际开发中应用比较广泛,常用加解密分为:“对称式”、“非对称式”和”数字签名“。

对称式:对称加密(也叫私钥加密)指加密和解密使用相同密钥的加密算法。具体算法主要有DES算法3DES算法,TDEA算法,Blowfish算法,RC5算法,IDEA算法。

非对称加密(公钥加密):指加密和解密使用不同密钥的加密算法,也称为公私钥加密。具体算法主要有RSAElgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。

数字签名:数字签名是非对称密钥加密技术数字摘要技术的应用。主要算法有md5、hmac、sha1等。

以下介绍golang语言主要的加密解密算法实现。

md5

MD5信息摘要算法是一种被广泛使用的密码散列函数,可以产生出一个128位(16进制,32个字符)的散列值(hash value),用于确保信息传输完整一致。

func GetMd5String(s string) string {
    h := md5.New()
    h.Write([]byte(s))
    return hex.EncodeToString(h.Sum(nil))
}

hmac

HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)的缩写,

它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。

和我们自定义的加salt算法不同,Hmac算法针对所有哈希算法都通用,无论是MD5还是SHA-1。采用Hmac替代我们自己的salt算法,可以使程序算法更标准化,也更安全。

示例

//key随意设置 data 要加密数据
func Hmac(key, data string) string {
    hash:= hmac.New(md5.New, []byte(key)) // 创建对应的md5哈希加密算法
    hash.Write([]byte(data))
    return hex.EncodeToString(hash.Sum([]byte("")))
}
func HmacSha256(key, data string) string {
    hash:= hmac.New(sha256.New, []byte(key)) //创建对应的sha256哈希加密算法
    hash.Write([]byte(data))
    return hex.EncodeToString(hash.Sum([]byte("")))
}

sha1

SHA-1可以生成一个被称为消息摘要的160(20字节)散列值,散列值通常的呈现形式为40个十六进制数。


func Sha1(data string) string {
    sha1 := sha1.New()
    sha1.Write([]byte(data))
    return hex.EncodeToString(sha1.Sum([]byte("")))
}

AES

密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES(Data Encryption Standard),已经被多方分析且广为全世界所使用。AES中常见的有三种解决方案,分别为AES-128、AES-192和AES-256。如果采用真正的128位加密技术甚至256位加密技术,蛮力攻击要取得成功需要耗费相当长的时间。

AES 有五种加密模式:

  • 电码本模式(Electronic Codebook Book (ECB))、
  • 密码分组链接模式(Cipher Block Chaining (CBC))、
  • 计算器模式(Counter (CTR))、
  • 密码反馈模式(Cipher FeedBack (CFB))
  • 输出反馈模式(Output FeedBack (OFB))

ECB模式

出于安全考虑,golang默认并不支持ECB模式。

package main

import (
    "crypto/aes"
    "fmt"
)

func AESEncrypt(src []byte, key []byte) (encrypted []byte) {
    cipher, _ := aes.NewCipher(generateKey(key))
    length := (len(src) + aes.BlockSize) / aes.BlockSize
    plain := make([]byte, length*aes.BlockSize)
    copy(plain, src)
    pad := byte(len(plain) - len(src))
    for i := len(src); i < len(plain); i++ {
        plain[i] = pad
    }
    encrypted = make([]byte, len(plain))
    // 分组分块加密
    for bs, be := 0, cipher.BlockSize(); bs <= len(src); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
        cipher.Encrypt(encrypted[bs:be], plain[bs:be])
    }

    return encrypted
}

func AESDecrypt(encrypted []byte, key []byte) (decrypted []byte) {
    cipher, _ := aes.NewCipher(generateKey(key))
    decrypted = make([]byte, len(encrypted))
    //
    for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
        cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
    }

    trim := 0
    if len(decrypted) > 0 {
        trim = len(decrypted) - int(decrypted[len(decrypted)-1])
    }

    return decrypted[:trim]
}

func generateKey(key []byte) (genKey []byte) {
    genKey = make([]byte, 16)
    copy(genKey, key)
    for i := 16; i < len(key); {
        for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
            genKey[j] ^= key[i]
        }
    }
    return genKey
}
func main()  {

    source:="hello world"
    fmt.Println("原字符:",source)
    //16byte密钥
    key:="1443flfsaWfdas"
    encryptCode:=AESEncrypt([]byte(source),[]byte(key))
    fmt.Println("密文:",string(encryptCode))

    decryptCode:=AESDecrypt(encryptCode,[]byte(key))

    fmt.Println("解密:",string(decryptCode))


}

CBC模式


package main
import(
    "bytes"
    "crypto/aes"
    "fmt"
    "crypto/cipher"
    "encoding/base64"
)
func main() {
    orig := "hello world"
    key := "0123456789012345"
    fmt.Println("原文:", orig)
    encryptCode := AesEncrypt(orig, key)
    fmt.Println("密文:" , encryptCode)
    decryptCode := AesDecrypt(encryptCode, key)
    fmt.Println("解密结果:", decryptCode)
}
func AesEncrypt(orig string, key string) string {
    // 转成字节数组
    origData := []byte(orig)
    k := []byte(key)
    // 分组秘钥
    // NewCipher该函数限制了输入k的长度必须为16, 24或者32
    block, _ := aes.NewCipher(k)
    // 获取秘钥块的长度
    blockSize := block.BlockSize()
    // 补全码
    origData = PKCS7Padding(origData, blockSize)
    // 加密模式
    blockMode := cipher.NewCBCEncrypter(block, k[:blockSize])
    // 创建数组
    cryted := make([]byte, len(origData))
    // 加密
    blockMode.CryptBlocks(cryted, origData)
    return base64.StdEncoding.EncodeToString(cryted)
}
func AesDecrypt(cryted string, key string) string {
    // 转成字节数组
    crytedByte, _ := base64.StdEncoding.DecodeString(cryted)
    k := []byte(key)
    // 分组秘钥
    block, _ := aes.NewCipher(k)
    // 获取秘钥块的长度
    blockSize := block.BlockSize()
    // 加密模式
    blockMode := cipher.NewCBCDecrypter(block, k[:blockSize])
    // 创建数组
    orig := make([]byte, len(crytedByte))
    // 解密
    blockMode.CryptBlocks(orig, crytedByte)
    // 去补全码
    orig = PKCS7UnPadding(orig)
    return string(orig)
}
//补码
//AES加密数据块分组长度必须为128bit(byte[16]),密钥长度可以是128bit(byte[16])、192bit(byte[24])、256bit(byte[32])中的任意一个。
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
    padding := blocksize - len(ciphertext)%blocksize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padtext...)
}
//去码
func PKCS7UnPadding(origData []byte) []byte {
    length := len(origData)
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}

CRT模式

package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "fmt"
)
//加密
func aesCtrCrypt(plainText []byte, key []byte) ([]byte, error) {

    //1. 创建cipher.Block接口
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    //2. 创建分组模式,在crypto/cipher包中
    iv := bytes.Repeat([]byte("1"), block.BlockSize())
    stream := cipher.NewCTR(block, iv)
    //3. 加密
    dst := make([]byte, len(plainText))
    stream.XORKeyStream(dst, plainText)

    return dst, nil
}


func main() {
    source:="hello world"
    fmt.Println("原字符:",source)

    key:="1443flfsaWfdasds"
    encryptCode,_:=aesCtrCrypt([]byte(source),[]byte(key))
    fmt.Println("密文:",string(encryptCode))

    decryptCode,_:=aesCtrCrypt(encryptCode,[]byte(key))

    fmt.Println("解密:",string(decryptCode))
}

CFB模式

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "io"
)
func AesEncryptCFB(origData []byte, key []byte) (encrypted []byte) {
    block, err := aes.NewCipher(key)
    if err != nil {
        //panic(err)
    }
    encrypted = make([]byte, aes.BlockSize+len(origData))
    iv := encrypted[:aes.BlockSize]
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        //panic(err)
    }
    stream := cipher.NewCFBEncrypter(block, iv)
    stream.XORKeyStream(encrypted[aes.BlockSize:], origData)
    return encrypted
}
func AesDecryptCFB(encrypted []byte, key []byte) (decrypted []byte) {
    block, _ := aes.NewCipher(key)
    if len(encrypted) < aes.BlockSize {
        panic("ciphertext too short")
    }
    iv := encrypted[:aes.BlockSize]
    encrypted = encrypted[aes.BlockSize:]

    stream := cipher.NewCFBDecrypter(block, iv)
    stream.XORKeyStream(encrypted, encrypted)
    return encrypted
}
func main() {
    source:="hello world"
    fmt.Println("原字符:",source)
    key:="ABCDEFGHIJKLMNO1"//16位
    encryptCode:=AesEncryptCFB([]byte(source),[]byte(key))
    fmt.Println("密文:",hex.EncodeToString(encryptCode))
    decryptCode:=AesDecryptCFB(encryptCode,[]byte(key))

    fmt.Println("解密:",string(decryptCode))
}

OFB模式

package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "io"
)
func aesEncryptOFB( data[]byte,key []byte) ([]byte, error) {
    data = PKCS7Padding(data, aes.BlockSize)
    block, _ := aes.NewCipher([]byte(key))
    out := make([]byte, aes.BlockSize + len(data))
    iv := out[:aes.BlockSize]
    if _, err := io.ReadFull(rand.Reader, iv); err != nil {
        return nil, err
    }

    stream := cipher.NewOFB(block, iv)
    stream.XORKeyStream(out[aes.BlockSize:], data)
    return out, nil
}

func aesDecryptOFB( data[]byte,key []byte) ([]byte, error) {
    block, _ := aes.NewCipher([]byte(key))
    iv  := data[:aes.BlockSize]
    data = data[aes.BlockSize:]
    if len(data) % aes.BlockSize != 0 {
        return nil, fmt.Errorf("data is not a multiple of the block size")
    }

    out := make([]byte, len(data))
    mode := cipher.NewOFB(block, iv)
    mode.XORKeyStream(out, data)

    out= PKCS7UnPadding(out)
    return out, nil
}
//补码
//AES加密数据块分组长度必须为128bit(byte[16]),密钥长度可以是128bit(byte[16])、192bit(byte[24])、256bit(byte[32])中的任意一个。
func PKCS7Padding(ciphertext []byte, blocksize int) []byte {
    padding := blocksize - len(ciphertext)%blocksize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padtext...)
}
//去码
func PKCS7UnPadding(origData []byte) []byte {
    length := len(origData)
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}
func main() {
    source:="hello world"
    fmt.Println("原字符:",source)
    key:="1111111111111111"//16位  32位均可
    encryptCode,_:=aesEncryptOFB([]byte(source),[]byte(key))
    fmt.Println("密文:",hex.EncodeToString(encryptCode))
    decryptCode,_:=aesDecryptOFB(encryptCode,[]byte(key))

    fmt.Println("解密:",string(decryptCode))
}

RSA加密

首先使用openssl生成公私钥

package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "errors"
    "fmt"
)

// 私钥生成
//openssl genrsa -out rsa_private_key.pem 1024
var privateKey = []byte(`
-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDcGsUIIAINHfRTdMmgGwLrjzfMNSrtgIf4EGsNaYwmC1GjF/bM
h0Mcm10oLhNrKNYCTTQVGGIxuc5heKd1gOzb7bdTnCDPPZ7oV7p1B9Pud+6zPaco
qDz2M24vHFWYY2FbIIJh8fHhKcfXNXOLovdVBE7Zy682X1+R1lRK8D+vmQIDAQAB
AoGAeWAZvz1HZExca5k/hpbeqV+0+VtobMgwMs96+U53BpO/VRzl8Cu3CpNyb7HY
64L9YQ+J5QgpPhqkgIO0dMu/0RIXsmhvr2gcxmKObcqT3JQ6S4rjHTln49I2sYTz
7JEH4TcplKjSjHyq5MhHfA+CV2/AB2BO6G8limu7SheXuvECQQDwOpZrZDeTOOBk
z1vercawd+J9ll/FZYttnrWYTI1sSF1sNfZ7dUXPyYPQFZ0LQ1bhZGmWBZ6a6wd9
R+PKlmJvAkEA6o32c/WEXxW2zeh18sOO4wqUiBYq3L3hFObhcsUAY8jfykQefW8q
yPuuL02jLIajFWd0itjvIrzWnVmoUuXydwJAXGLrvllIVkIlah+lATprkypH3Gyc
YFnxCTNkOzIVoXMjGp6WMFylgIfLPZdSUiaPnxby1FNM7987fh7Lp/m12QJAK9iL
2JNtwkSR3p305oOuAz0oFORn8MnB+KFMRaMT9pNHWk0vke0lB1sc7ZTKyvkEJW0o
eQgic9DvIYzwDUcU8wJAIkKROzuzLi9AvLnLUrSdI6998lmeYO9x7pwZPukz3era
zncjRK3pbVkv0KrKfczuJiRlZ7dUzVO0b6QJr8TRAA==
-----END RSA PRIVATE KEY-----
`)

// 公钥: 根据私钥生成
//openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
var publicKey = []byte(`
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcGsUIIAINHfRTdMmgGwLrjzfM
NSrtgIf4EGsNaYwmC1GjF/bMh0Mcm10oLhNrKNYCTTQVGGIxuc5heKd1gOzb7bdT
nCDPPZ7oV7p1B9Pud+6zPacoqDz2M24vHFWYY2FbIIJh8fHhKcfXNXOLovdVBE7Z
y682X1+R1lRK8D+vmQIDAQAB
-----END PUBLIC KEY-----
`)

// 加密
func RsaEncrypt(origData []byte) ([]byte, error) {
    //解密pem格式的公钥
    block, _ := pem.Decode(publicKey)
    if block == nil {
        return nil, errors.New("public key error")
    }
    // 解析公钥
    pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return nil, err
    }
    // 类型断言
    pub := pubInterface.(*rsa.PublicKey)
    //加密
    return rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
}

// 解密
func RsaDecrypt(ciphertext []byte) ([]byte, error) {
    //解密
    block, _ := pem.Decode(privateKey)
    if block == nil {
        return nil, errors.New("private key error!")
    }
    //解析PKCS1格式的私钥
    priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }
    // 解密
    return rsa.DecryptPKCS1v15(rand.Reader, priv, ciphertext)
}
func main() {
    data, _ := RsaEncrypt([]byte("hello world"))
    fmt.Println(base64.StdEncoding.EncodeToString(data))
    origData, _ := RsaDecrypt(data)
    fmt.Println(string(origData))
}

参考:

https://www.liaoxuefeng.com/w...

https://studygolang.com/artic...

https://segmentfault.com/a/11...

我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev...

links

  • 目录
  • 上一节:
  • 下一节:
查看原文

某勒个杰 赞了文章 · 2020-08-21

react-quill中图片上传由默认转成base64改成上传到服务器

使用react-quill富文本编辑器,里面处理图片是默认转成base64,提交到后台的时候文件太大,因此这里改写处理image的逻辑,改成上传到服务器。
具体代码如下:

配置1

import Quill from 'quill'
import ReactQuill from 'react-quill'
import 'react-quill/dist/quill.core.css'
import 'react-quill/dist/quill.snow.css'
import QuillEmoji from 'quill-emoji'
import 'quill-emoji/dist/quill-emoji.css'

Quill.register({
    'modules/emoji-toolbar': QuillEmoji.ToolbarEmoji,
    // 'modules/emoji-textarea': QuillEmoji.TextAreaEmoji,
    'modules/emoji-shortname': QuillEmoji.ShortNameEmoji
})

const toolbarContainer = [
    [{ 'size': ['small', false, 'large', 'huge'] }],  // custom dropdown
    [{ 'font': [] }],
    [{ 'header': 1 }, { 'header': 2 }],               // custom button values
    ['bold', 'italic', 'underline', 'strike'],        // toggled buttons
    [{ 'align': [] }],
    [{ 'indent': '-1' }, { 'indent': '+1' }],          // outdent/indent
    [{ 'direction': 'rtl' }],                         // text direction
    [{ 'script': 'sub' }, { 'script': 'super' }],      // superscript/subscript
    ['blockquote', 'code-block'],

    [{ 'list': 'ordered' }, { 'list': 'bullet' }],
    [{ 'color': [] }, { 'background': [] }],
    ['emoji', 'image', 'video', 'link'],

    ['clean']
]

配置2

<ReactQuill
    ref={ref => this.quillRef = ref}
    placeholder="填写活动详情~"
    theme="snow"
    value={this.state.detailTpl}
    onChange={this.handleChangeDetail}
    modules={{
        toolbar: {
            container: toolbarContainer,
            handlers: {
                image: this.imageHandler
            }
        },
        'emoji-toolbar': true,
        // 'emoji-textarea': true,
        'emoji-shortname': true,
    }}
/>

图片处理方法

imageHandler = () => {
    this.quillEditor = this.quillRef.getEditor()
    const input = document.createElement('input')
    input.setAttribute('type', 'file')
    input.setAttribute('accept', 'image/*')
    input.click()
    input.onchange = async () => {
        const file = input.files[0]
        const formData = new FormData()
        formData.append('quill-image', file)
        const res = await uploadFile(formData) 
        const range = this.quillEditor.getSelection()
        const link = res.data[0].url

        // this part the image is inserted
        // by 'image' option below, you just have to put src(link) of img here. 
        this.quillEditor.insertEmbed(range.index, 'image', link)
    }
}
查看原文

赞 4 收藏 2 评论 1

某勒个杰 收藏了文章 · 2020-01-07

Golang项目部署

文章来源:https://goframe.org/deploymen...

一、独立部署

使用GF开发的应用程序可以独立地部署到服务器上,设置为后台守护进程运行即可。这种模式常用在简单的API服务项目中。

服务器我们推荐使用*nix服务器系列(包括:Linux, MacOS, *BSD),以下使用Ubuntu系统为例,介绍如何部署使用GF框架开发的项目。

1. nohup

我们可以使用简单的nohup命令来运行应用程序,使其作为后台守护进程运行,即使远程连接的SSH断开也不会影响程序的执行。在流行的Linux发行版中往往都默认安装好了nohup命令工具。
命令如下:

nohup ./gf-app &

2. tmux

tmux是一款Linux下的终端复用工具,可以开启不同的终端窗口来将应用程序作为后台守护进程执行,即使远程连接的SSH断开也不会影响程序的执行。
ubuntu系统下直接使用sudo apt-get install tmux安装即可。使用以下步骤将应用程序后台运行。

  1. tmux new -s gf-app
  2. 在新终端窗口中执行./gf-app即可;
  3. 使用crt + B & D快捷键可以退出当前终端窗口;
  4. 使用tmux attach -t gf-app可进入到之前的终端窗口;

3. supervisor

supervisor是用Python开发的一套通用的进程管理程序,能将一个普通的命令行进程变为后台daemon,并监控进程状态,异常退出时能自动重启。官方网站:http://supervisord.org/
常见配置如下:

[program:gf-app]
user=root
command=/var/www/main
stdout_logfile=/var/log/gf-app-stdout.log
stderr_logfile=/var/log/gf-app-stderr.log
autostart=true
autorestart=true

使用步骤如下:

  1. 使用sudo service supervisor start启动supervisor服务;
  2. 创建应用配置文件/etc/supervisor/conf.d/gf-app.conf, 内容如上;
  3. 使用sudo supervisorctl进入supervisor管理终端;
  4. 使用reload重新读取配置文件并重启当前supoervisor管理的所有进程;
  5. 也可以使用update重新加载配置(默认不重启),随后使用start gf-app启动指定的应用程序;
  6. 随后可以使用status指令查看当前supervisor管理的进程状态;

二、代理部署

代理部署即前置一层第三方的WebServer服务器处理所有的请求,将部分请求(往往是动态处理请求)有选择性地转交给后端的Golang应用程序执行,后端部署的Golang应用程序可以配置有多个。这种模式常用在复杂的WebServer配置中,常见的场景例如:需要静态文件分离、需要配置多个域名及证书、需要自建负载均衡层,等等。

虽然Golang实现的WebServer也能够处理静态文件,但是相比较于专业性的WebServernginx/apache来说比较简单,性能也较弱。因此不推荐使用Golang WebServer作为前端服务直接处理静态文件请求。

Nginx

我们推荐使用Nginx作为反向代理的前端接入层,有两种配置方式实现动静态请求的拆分。

静态文件后缀

这种方式通过文件名后缀区分,将指定的静态文件转交给nginx处理,其他的请求转交给golang应用。
配置示例如下:

server {
    listen       80;
    server_name  goframe.org;

    access_log   /var/log/gf-app-access.log;
    error_log    /var/log/gf-app-error.log;

    location ~ .*\.(gif|jpg|jpeg|png|js|css|eot|ttf|woff|svg|otf)$ {
        access_log off;
        expires    1d;
        root       /var/www/gf-app/public;
        try_files  $uri @backend;
    }

    location / {
        try_files $uri @backend;
    }

    location @backend {
        proxy_pass                 http://127.0.0.1:8199;
        proxy_redirect             off;
        proxy_set_header           Host             $host;
        proxy_set_header           X-Real-IP        $remote_addr;
        proxy_set_header           X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

其中,8199为本地的golang应用WebServer监听端口。

例如,在该配置下,我们可以通过http://goframe.org/my.png访问到指定的静态文件。

静态文件目录

这种方式通过文件目录区分,将指定目录的访问请求转交给nginx处理,其他的请求转交给golang应用。
配置示例如下:

server {
    listen       80;
    server_name  goframe.org;

    access_log   /var/log/gf-app-access.log;
    error_log    /var/log/gf-app-error.log;

    location ^~ /public {
        access_log off;
        expires    1d;
        root       /var/www/gf-app;
        try_files  $uri @backend;
    }

    location / {
        try_files $uri @backend;
    }

    location @backend {
        proxy_pass                 http://127.0.0.1:8199;
        proxy_redirect             off;
        proxy_set_header           Host             $host;
        proxy_set_header           X-Real-IP        $remote_addr;
        proxy_set_header           X-Forwarded-For  $proxy_add_x_forwarded_for;
    }
}

其中,8199为本地的golang应用WebServer监听端口。

例如,在该配置下,我们可以通过http://goframe.org/piblic/my.png访问静态文件。

三、容器部署

容器部署即使用docker化部署golang应用程序,这是在云服务时代最流行的部署方式,也是最推荐的部署方式。

1. 编译程序

跨平台交叉编译是golang的特点之一,可以非常方便地编译出我们需要的目标服务器平台的版本,而且是静态编译,非常方便地解决了运行依赖问题。
使用以下方式静态编译Linux平台amd64架构的可执行文件:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o gf-app main.go

这样便编译出来一个gf-app的可执行文件。

2. 编译镜像

我们需要将该可执行文件gf-app编译生成docker镜像,以便于分发及部署。Golang的运行环境推荐使用alpine基础系统镜像,编译出的容器镜像约为20MB左右。

一个参考的Dockerfile文件如下( 可以参考gf-home项目的Dcokerfile: https://github.com/gogf/gf-home ):

FROM loads/alpine:3.8

LABEL maintainer="john@johng.cn"

###############################################################################
#                                INSTALLATION
###############################################################################

ADD ./gf-app /bin/main
RUN chmod +x /bin/main

###############################################################################
#                                   START
###############################################################################

CMD main

其中,我们的基础镜像使用了loads/alpine:3.8这个镜像,基础镜像的Dockerfile地址:https://github.com/johngcn/do... ,仓库地址:https://hub.docker.com/u/loads

随后使用 docker build gf-app . 命令编译生成名为gf-appdocker镜像。

3. 运行镜像

使用以下命令运行镜像:

docker run gf-app

4. 镜像分发

容器的分发可以使用docker官方的平台:https://hub.docker.com/ ,国内也可以考虑使用阿里云:https://www.aliyun.com/produc...

5. 容器编排

在企业级生产环境中,docker容器往往需要结合kubernetes或者docker swarm容器编排工具一起使用。
容器编排涉及到的内容比较多,感兴趣的同学可以参考以下资料:

查看原文

某勒个杰 赞了文章 · 2020-01-03

利用ELK分析Nginx日志生产实战(高清多图)

本文以api.mingongge.com.cn域名为测试对象进行统计,日志为crm.mingongge.com.cn和risk.mingongge.com.cn请求之和(此二者域名不具生产换环境统计意义),生产环境请根据具体需要统计的域名进行统计。

由于涉及生产线上服务器,故本文部分服务器IP做了打码处理。

一、服务介绍

1.1、ELK

ELK是三个开源软件的缩写,分别表示:Elasticsearch , Logstash, Kibana , 它们都是开源软件。新增了一个FileBeat,它是一个轻量级的日志收集处理工具(Agent),Filebeat占用资源少,适合于在各个服务器上搜集日志后传输给Logstash,官方也推荐此工具。

Elasticsearch是个开源分布式搜索引擎,提供搜集、分析、存储数据三大功能。它的特点有:分布式,零配置,自动发现,索引自动分片,索引副本机制,restful风格接口,多数据源,自动搜索负载等。

Logstash 主要是用来日志的搜集、分析、过滤日志的工具,支持大量的数据获取方式。一般工作方式为c/s架构,client端安装在需要收集日志的主机上,server端负责将收到的各节点日志进行过滤、修改等操作在一并发往elasticsearch上去。

Kibana 也是一个开源和免费的工具,Kibana可以为 Logstash 和ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助汇总、分析和搜索重要数据日志。

1.2、Nginx

Nginx("engine x") 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP代理服务器。 Nginx 是由 Igor Sysoev 为俄罗斯访问量第二的 Rambler.ru 站点开发的,第一个公开版本0.1.0发布于2004年10月4日。其将源代码以类BSD许可证的形式发布,因它的稳定性、丰富的功能集、示例配置文件和低系统资源的消耗而闻名。Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。由俄罗斯的程序设计师Igor Sysoev所开发,供俄国大型的入口网站及搜索引擎Rambler(俄文:Рамблер)使用。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:新浪、网易、腾讯等。

本文中前端使用了nginx的反向代理功能,并使用了nginx的HTTP功能。

1.3、Kafka

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 对于像Hadoop一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka的目的是通过Hadoop的并行加载机制来统一线上和离线的消息处理,也是为了通过集群来提供实时的消息。

二、架构要求

2.1、架构描述

使用filebeat收集nginx日志,输出到kafka;logstash从kafka中消费日志,通过grok进行数据格式化,输出到elasticsearch中,kibana从elasticsearch中获取日志,进行过滤出图.

2.2、系统版本

CentOS Linux release 7.2.1511 (Core)3.10.0-514.26.2.el7.x86_64

 2.3、软件版本

jdk1.8.0_144nginx-1.12.2filebeat-6.3.2awurstmeister/kafka(docker image)logstash-6.5.4elasticsearch-6.4.0kibana-6.4.0

三、linux系统环境配置与优化

#查看服务器硬件信息dmidecode|grep "Product Name"#查看CPU型号grep name /proc/cpuinfo#查看CPU个数grep "physical id" /proc/cpuinfo#查看内存大小grep MemTotal /proc/meminfo

 四、系统初始化

4.1、关闭防火墙

systemctl stop filewalld

4.2、关闭selinux

setenforce 0sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/selinux/config

4.3、添加普通账户

useradd elsearchecho "******"|passwd --stdin elsearch

4.4、配置yum源

cat /etc/yum.repos.d/CentOS-Base.repo[base]name=CentOS-$releaseverenabled=1failovermethod=prioritybaseurl=http://mirrors.cloud.aliyuncs.com/centos/$releasever/os/$basearch/gpgcheck=1gpgkey=http://mirrors.cloud.aliyuncs.com/centos/RPM-GPG-KEY-CentOS-7[updates]name=CentOS-$releaseverenabled=1failovermethod=prioritybaseurl=http://mirrors.cloud.aliyuncs.com/centos/$releasever/updates/$basearch/gpgcheck=1gpgkey=http://mirrors.cloud.aliyuncs.com/centos/RPM-GPG-KEY-CentOS-7[extras]name=CentOS-$releaseverenabled=1failovermethod=prioritybaseurl=http://mirrors.cloud.aliyuncs.com/centos/$releasever/extras/$basearch/gpgcheck=1gpgkey=http://mirrors.cloud.aliyuncs.com/centos/RPM-GPG-KEY-CentOS-7

4.5、清理开机自启动服务

for i in `chkconfig --list|grep 3:on |awk '{print $1}'`;do chkconfig$i off;donefor i in crond network rsyslog sshd;do chkconfig --level 3 $ion;donechkconfig --list|grep 3:on

4.6、服务器时间同步

echo '*/5 * * * * /usr/sbin/ntpdate time.windows.com > /dev/null2>&1' >>/var/spool/cron/root

4.7、加大文件描述符

echo '* - nofile 65535' >> /etc/security/limits.conftail -1 /etc/security/limits.conf#重新登陆后生效(无需重启)ulimit -n(重新登陆后查看)

 4.8、内核参数调优(可不操作)

\cp /etc/sysctl.conf /etc/sysctl.conf.bakcat>>/etc/sysctl.conf<<EOFnet.ipv4.tcp_timestamps = 0net.ipv4.tcp_synack_retries = 2net.ipv4.tcp_syn_retries = 2net.ipv4.tcp_mem = 94500000 915000000 927000000net.ipv4.tcp_max_orphans = 3276800net.core.wmem_default = 8388608net.core.rmem_default = 8388608net.core.rmem_max = 16777216net.core.wmem_max = 16777216net.ipv4.tcp_rmem=4096 87380 16777216net.ipv4.tcp_wmem=4096 65536 16777216net.core.netdev_max_backlog = 32768net.core.somaxconn = 32768net.ipv4.tcp_syncookies=1net.ipv4.tcp_tw_reuse = 1net.ipv4.tcp_tw_recycle = 1net.ipv4.tcp_fin_timeout=1net.ipv4.tcp_keepalive_time=1200net.ipv4.tcp_max_syn_backlog = 65536net.ipv4.ip_local_port_range = 1024 65535EOF/sbin/sysctl -p

五、部署开始

5.1、更改nginx日志输出格式

5.1.1、定义日志格式

cat /etc/nginx/nginx.conflog_format main '$remote_addr - $remote_user [$time_local]"$request" ''$status$body_bytes_sent "$http_referer" ''"$http_user_agent" "$http_x_forwarded_for"';

5.1.2、加载日志格式到对应域名配置中

cat /etc/nginx/conf.d/vhost/api.mingongge.com.cn.confserver {listen 80;server_name  newtest-msp-api.mingongge.com.cn;access_log   /var/log/nginx/api.mingongge.com.cn.log main;}

5.1.3、reload生效

nginx -s reload

5.1.4、清空原输出文件,并查看输出的日志格式

:> /var/log/nginx/api.mingongge.com.cn.logtailf /var/log/nginx/api.mingongger.com.cn.log1xx.2xx.72.175 - - [18/Mar/2019:13:51:17 +0800] "GET/user/fund/113 HTTP/1.1" 200 673 "-" "Mozilla/5.0 (WindowsNT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) sun/1.5.6 Chrome/69.0.3497.106Electron/4.0.3 Safari/537.36" "-"

5.2、配置kafka

测试环境使用docker起的kafka,kafka部署掠过,以下任选一种

5.2.1、方法一 创建kafka topic

./kafka-topics.sh --create --topic nginxlog --replication-factor 1--partitions 1 --zookeeper localhost:2181

5.2.2、方法二

auto.create.topics.enable=true

开启kafka自动创建topic配置

5.2.3、filebeat部署完成后确认kafka topic中有数据

./kafka-console-consumer.sh --bootstrap-server 192.168.0.53:9091--from-beginning --topic nginxlog

输出如下

{"@timestamp":"2019-03-14T07:16:50.140Z","@metadata":{"beat":"filebeat","type":"doc","version":"6.3.2","topic":"nginxlog"},"fields":{"log_topics":"nginxlog"},"beat":{"version":"6.3.2","name":"test-kafka-web","hostname":"test-kafka-web"},"host":{"name":"test-kafka-web"},"source":"/var/log/nginx/newtest-msp-api.mingongge.com.cn-80.log","offset":114942,"message":"116.226.72.175- - [14/Mar/2019:15:16:49 +0800] newtest-msp-api.mingongge.com.cn POST\"/upstream/page\" \"-\" 200 6314\"http://newtest-msp-crm.mingongge.com.cn/\" 200 192.168.0.49:60070.024 0.024 \"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36\"\"-\""}Processed a total of 7516 messages

测试环境中kafka地址为

192.168.0.53:9091

 5.3、配置filebeat收集nginx日志

5.3.1、安装filebeat

cd /opt/ && wget http://download.mingongge.com.cn/download/software/filebeat-6.3.2-x86_64.rpmyum localinstall filebeat-6.3.2-x86_64.rpm -y

5.3.2、编辑配置文件

cat /etc/filebeat/filebeat.ymlfilebeat.prospectors:- input_type: logenabled: truepaths:- /var/log/nginx/api.mingongge.com.cn.log#收集日志路径fields:log_topics: nginxlog #kafka中topic名称json.keys_under_root: truejson.overwrite_keys: trueoutput.kafka:enabled: truehosts:["192.168.0.53:9091"] #kafka地址topic:'%{[fields][log_topics]}' #kafka中topic名称partition.round_robin:reachable_only: falsecompression: gzipmax_message_bytes: 1000000required_acks: 1

5.3.3、启动filebeat& 开机启动

systemctl start filebeatsystemctl enable filebeat

5.4、配置logstash

5.4.1 编辑配置

cat /usr/local/logstash/config/nginx.confinput {kafka {type =>"nginxlog"topics =>["nginxlog"]bootstrap_servers=> ["192.168.0.53:9091"]group_id =>"nginxlog"auto_offset_reset=> latestcodec =>"json"}}filter {if [type] == "nginxlog"{grok {match => {"message" => "%{COMBINEDAPACHELOG}" }remove_field =>"message"}date {match => ["timestamp" , "dd/MMM/YYYY:HH:mm:ss Z" ]}geoip {source =>"clientip"target =>"geoip"database =>"/usr/local/logstash/config/GeoLite2-City.mmdb"add_field => ["[geoip][coordinates]", "%{[geoip][longitude]}" ] #添加字段coordinates,值为经度add_field => ["[geoip][coordinates]", "%{[geoip][latitude]}" ] #添加字段coordinates,值为纬度}mutate {convert => ["[geoip][coordinates]", "float"]}useragent {source =>"agent"target =>"userAgent"}}}output {if [type] == 'nginxlog' {elasticsearch {hosts =>["http://192.168.0.48:9200"]index =>"logstash-nginxlog-%{+YYYY.MM.dd}"}stdout {codec =>rubydebug}}}

5.4.2、使用配置文件启动logstash服务,观察输出

/usr/local/logstash/bin/logstash -f nginx.conf{"httpversion"=> "1.1","verb" =>"GET","auth"=> "-","@timestamp"=> 2019-03-18T06:41:27.000Z,"type"=> "nginxlog","json"=> {},"source"=> "/var/log/nginx/newtest-msp-api.mingongge.com.cn-80.log","fields" =>{"log_topics"=> "nginxlog"},"response"=> "200","offset"=> 957434,"host"=> {"name" =>"test-kafka-web"},"beat"=> {"hostname"=> "test-kafka-web","version"=> "6.3.2","name"=> "test-kafka-web"},"bytes"=> "673","request"=> "/user/fund/113","timestamp"=> "18/Mar/2019:14:41:27 +0800","referrer"=> "\"-\"","userAgent"=> {"os"=> "Windows","major" => "4","patch"=> "3","build"=> "","minor"=> "0","os_name"=> "Windows","device"=> "Other","name"=> "Electron"},"geoip"=> {"ip" => "1xx.2xx.72.175","country_name" => "China","coordinates" => [[0] 121.4012,[1] 31.0449],"region_name" => "Shanghai","location" => {"lat"=> 31.0449,"lon"=> 121.4012},"continent_code" => "AS","timezone" => "Asia/Shanghai","longitude" => 121.4012,"city_name" => "Shanghai","country_code2" => "CN","region_code" => "SH","latitude" => 31.0449,"country_code3" => "CN"},"@version"=> "1","clientip"=> "1xx.2xx.72.175","ident"=> "-","agent"=> "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko) sun/1.5.6 Chrome/69.0.3497.106 Electron/4.0.3Safari/537.36\""}

5.4.3、后台启动logstash

确认出现以上输出后,将logstash分离出当前shell,并放在后台运行

nohup /usr/local/logstash/bin/logstash -f nginx.conf &>/dev/null &

5.5、kibana配置

5.5.1、修改kibana配置

/usr/local/kibana-6.5.4-linux-x86_64/config/kibana.yml #增加高德地图tilemap.url:'http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=7&x={x}&y={y}&z={z}'

5.5.2、创建Index Pattern

5.5.3、IP访问TOP5

选择柱形图

添加X轴,以geoip.ip为order by字段

5.5.4 、PV

选择metric

默认统计总日志条数,即为PV数

 5.5.5、全球访问地图

选择map

Field选择geoip.location

选择添加高德地图

5.5.6、实时流量

选择线条图

5.5.7、操作系统

选择饼图

5.5.8、登陆次数

过滤login关键字,并做count统计

5.5.9、访问地区

5.5.10、Dashboard展示

  • IP访问Top5:每日客户端IP请求数最多的前五个(可分析出攻击者IP)
  • PV:每日页面访问量
  • 全球访问图:直观的展示用户来自哪个国家哪个地区
  • 实时流量:根据@timestamp字段来展示单位时间的请求数(可根据异常峰值判断是否遭遇攻击)
  • 操作系统:展示客户端所用设备所占比重
  • 登陆次数:通过过滤request中login的访问记录,粗略估算出进行过登陆的次数
  • 访问地区:展示访问量最多的国家或地区
  • 需展示其他指标,可进行自由发挥

查看原文

赞 58 收藏 39 评论 3

某勒个杰 赞了文章 · 2019-12-05

PHP面试:常见查找算法一篇说透

预警

在本篇文章中,将为各位老铁介绍不同的搜索算法以及它们的复杂度。因为力求通俗易懂,所以篇幅可能较长,大伙可以先Mark下来,每天抽时间看一点理解一点。本文配套的Github Repo,欢迎各位老铁star,会一直更新的。

开篇

和排序类似,搜索或者叫做查找,也是平时我们使用最多的算法之一。无论我们搜索数据库还是文件,实际上都在使用某种搜索算法来定位想要查找的数据。

线性查找

执行搜索的最常见的方法是将每个项目与我们正在寻找的数据进行比较,这就是线性搜索或顺序搜索。它是执行搜索的最基本的方式。如果列表中有n项。在最坏的情况下。我们必须搜索n个项目才能找到一个特定的项目。下面遍历一个数组来查找一个项目。

function linearSearch(array $arr, int $needle) {
    for ($i = 0, $count = count($arr); $i < $count; $i++) {
        if ($needle === $arr[$i]) {
            return true;
        }
    }

    return false;
}

线性查找的复杂度

best time complexityO(1)
worst time complexityO(n)
Average time complexityO(n)
Space time complexityO(1)

二分搜索

线性搜索的平均时间复杂度或最坏时间复杂度是O(n),这不会随着待搜索数组的顺序改变而改变。所以如果数组中的项按特定顺序排序,我们不必进行线性搜索。我们可以通过执行选择性搜索而可以获得更好的结果。最流行也是最著名的搜索算法是“二分搜索”。虽然有点像我们之前说的二叉搜索树,但我们不用构造二叉搜索树就可以使用这个算法。


function binarySearch(array $arr, int $needle) {
    $low = 0;
    $high = count($arr) - 1;

    while ($low <= $high) {
        $middle = (int)(($high + $low) / 2);

        if ($arr[$middle] < $needle) {
            $low = $middle + 1;
        } elseif ($arr[$middle] > $needle) {
            $high = $middle - 1;
        } else {
            return true;
        }
    }

    return false;
}

在二分搜索算法中,我们从数据的中间开始,检查中间的项是否比我们要寻找的项小或大,并决定走哪条路。这样,我们把列表分成两半,一半完全丢弃,像下面的图像一样。

clipboard.png

递归版本:

function binarySearchRecursion(array $arr, int $needle, int $low, int $high)
{
    if ($high < $low) return false;

    $middle = (int)(($high + $low) / 2);

    if ($arr[$middle] < $needle) {
        return binarySearchRecursion($arr, $needle, $middle + 1, $high);
    } elseif ($arr[$middle] > $needle) {
        return binarySearchRecursion($arr, $needle, $low, $middle - 1);
    } else {
        return true;
    }
}

二分搜索复杂度分析

对于每一次迭代,我们将数据划分为两半,丢弃一半,另一半用于搜索。在分别进行了1,2次和3次迭代之后,我们的列表长度逐渐减少到n/2,n/4,n/8...。因此,我们可以发现,k次迭代后,将只会留下n/2^k项。最后的结果就是 n/2^k = 1,然后我们两边分别取对数 得到 k = log(n),这就是二分搜索算法的最坏运行时间复杂度。

best time complexityO(1)
worst time complexityO(log n)
Average time complexityO(log n)
Space time complexityO(1)

重复二分查找

有这样一个场景,假如我们有一个含有重复数据的数组,如果我们想从数组中找到2的第一次出现的位置,使用之前的算法将会返回第5个元素。然而,从下面的图像中我们可以清楚地看到,正确的结果告诉我们它不是第5个元素,而是第2个元素。因此,上述二分搜索算法需要进行修改,将它修改成一个重复的搜索,搜索直到元素第一次出现的位置才停止。

clipboard.png

function repetitiveBinarySearch(array $data, int $needle)
{
    $low = 0;
    $high = count($data);
    $firstIndex = -1;

    while ($low <= $high) {
        $middle = ($low + $high) >> 1;

        if ($data[$middle] === $needle) {
            $firstIndex = $middle;
            $high = $middle - 1;
        } elseif ($data[$middle] > $needle) {
            $high = $middle - 1;
        } else {
            $low = $middle + 1;
        }
    }

    return $firstIndex;
}

首先我们检查mid所对应的值是否是我们正在寻找的值。 如果是,那么我们将中间索引指定为第一次出现的index,我们继续检查中间元素左侧的元素,看看有没有再次出现我们寻找的值。 然后继续迭代,直到$low > $high。 如果没有再次找到这个值,那么第一次出现的位置就是该项的第一个索引的值。 如果没有,像往常一样返回-1。我们运行一个测试来看代码是否正确:

public function testRepetitiveBinarySearch()
{
    $arr = [1,1,1,2,3,4,5,5,5,5,5,6,7,8,9,10];

    $firstIndex = repetitiveBinarySearch($arr, 6);

    $this->assertEquals(11, $firstIndex);
}

发现结果正确。

clipboard.png

到目前为止,我们可以得出结论,二分搜索肯定比线性搜索更快。但是,这一切的先决条件是数组已经排序。在未排序的数组中应用二分搜索会导致错误的结果。 那可能存在一种情况,就是对于某个数组,我们不确定它是否已排序。现在有一个问题就是,是否应该首先对数组进行排序然后应用二分查找算法吗?还是继续使用线性搜索算法?

小思考

对于一个包含n个项目的数组,并且它们没有排序。由于我们知道二分搜索更快,我们决定先对其进行排序,然后使用二分搜索。但是,我们清楚最好的排序算法,其最差的时间复杂度是O(nlogn),而对于二分搜索,最坏情况复杂度是O(logn)。所以,如果我们排序后应用二分搜索,复杂度将是O(nlogn)。

但是,我们也知道,对于任何线性或顺序搜索(排序或未排序),最差的时间复杂度是O(n),显然好于上述方案。

考虑另一种情况,即我们需要多次搜索给定数组。我们将k表示为我们想要搜索数组的次数。如果k为1,那么我们可以很容易地应用之前的线性搜索方法。如果k的值比数组的大小更小,暂且使用n表示数组的大小。如果k的值更接近或大于n,那么我们在应用线性方法时会遇到一些问题。假设k = n,线性搜索将具有O(n2)的复杂度。现在,如果我们进行排序然后再进行搜索,那么即使k更大,一次排序也只会花费O(nlogn)时间复。然后,每次搜索的复杂度是O(logn),n次搜索的复杂度是O(nlogn)。如果我们在这里采取最坏的运行情况,排序后然后搜索k次总的的复杂度是O(nlogn),显然这比顺序搜索更好。

我们可以得出结论,如果一些搜索操作的次数比数组的长度小,最好不要对数组进行排序,直接执行顺序搜索即可。但是,如果搜索操作的次数与数组的大小相比更大,那么最好先对数组进行排序,然后使用二分搜索。

二分搜索算法有很多不同的版本。我们不是每次都选择中间索引,我们可以通过计算作出决策来选择接下来要使用的索引。我们现在来看二分搜索算法的两种变形:插值搜索和指数搜索。

插值搜索

在二分搜索算法中,总是从数组的中间开始搜索过程。 如果一个数组是均匀分布的,并且我们正在寻找的数据可能接近数组的末尾,那么从中间搜索可能不是一个好选择。 在这种情况下,插值搜索可能非常有用。插值搜索是对二分搜索算法的改进,插值搜索可以基于搜索的值选择到达不同的位置。例如,如果我们正在搜索靠近数组开头的值,它将直接定位到到数组的第一部分而不是中间。使用公式计算位置,如下所示

clipboard.png

可以发现,我们将从通用的mid =(low * high)/2 转变为更复杂的等式。如果搜索的值更接近arr[high],则此公式将返回更高的索引,如果值更接近arr[low],则此公式将返回更低的索引。

function interpolationSearch(array $arr, int $needle)
{
    $low = 0;
    $high = count($arr) - 1;

    while ($arr[$low] != $arr[$high] && $needle >= $arr[$low] && $needle <= $arr[$high]) {
        $middle = intval($low + ($needle - $arr[$low]) * ($high - $low) / ($arr[$high] - $arr[$low]));

        if ($arr[$middle] < $needle) {
            $low = $middle + 1;
        } elseif ($arr[$middle] > $needle) {
            $high = $middle - 1;
        } else {
            return $middle;
        }
    }

    if ($needle == $arr[$low]) {
        return $low;
    } 
    
    return -1;
    
}

插值搜索需要更多的计算步骤,但是如果数据是均匀分布的,这个算法的平均复杂度是O(log(log n)),这比二分搜索的复杂度O(logn)要好得多。 此外,如果值的分布不均匀,我们必须要小心。 在这种情况下,插值搜索的性能可以需要重新评估。下面我们将探索另一种称为指数搜索的二分搜索变体。

指数搜索

在二分搜索中,我们在整个列表中搜索给定的数据。指数搜索通过决定搜索的下界和上界来改进二分搜索,这样我们就不会搜索整个列表。它减少了我们在搜索过程中比较元素的数量。指数搜索是在以下两个步骤中完成的:

1.我们通过查找第一个指数k来确定边界大小,其中值2^k的值大于搜索项。 现在,2^k和2^(k-1)分别成为上限和下限。
2.使用以上的边界来进行二分搜索。

下面我们来看下PHP实现的代码

function exponentialSearch(array $arr, int $needle): int
{
    $length = count($arr);
    if ($length == 0) return -1;

    $bound = 1;

    while ($bound < $length && $arr[$bound] < $needle) {
        $bound *= 2;
    }

    return binarySearchRecursion($arr, $needle, $bound >> 1, min($bound, $length));
}

我们把$needle出现的位置记位i,那么我们第一步花费的时间复杂度就是O(logi)。表示为了找到上边界,我们的while循环需要执行O(logi)次。因为下一步应用一个二分搜索,时间复杂度也是O(logi)。我们假设j是我们上一个while循环执行的次数,那么本次二分搜索我们需要搜索的范围就是2^j-1 至 2^j,而j=logi,即

clipboard.png

那我们的二分搜索时间复杂度需要对这个范围求log2,即

clipboard.png

那么整个指数搜索的时间复杂度就是2 O(logi),省略掉常数就是O(logi)。

best time complexityO(1)
worst time complexityO(log i)
Average time complexityO(log i)
Space time complexityO(1)

哈希查找

在搜索操作方面,哈希表可以是非常有效的数据结构。在哈希表中,每个数据都有一个与之关联的唯一索引。如果我们知道要查看哪个索引,我们就可以非常轻松地找到对应的值。通常,在其他编程语言中,我们必须使用单独的哈希函数来计算存储值的哈希索引。散列函数旨在为同一个值生成相同的索引,并避免冲突。

PHP底层C实现中数组本身就是一个哈希表,由于数组是动态的,不必担心数组溢出。我们可以将值存储在关联数组中,以便我们可以将值与键相关联。

function hashSearch(array $arr, int $needle)
{
    return isset($arr[$needle]) ? true : false;
}

树搜索

搜索分层数据的最佳方案之一是创建搜索树。在第理解和实现树中,我们了解了如何构建二叉搜索树并提高搜索效率,并且介绍了遍历树的不同方法。 现在,继续介绍两种最常用的搜索树的方法,通常称为广度优先搜索(BFS)和深度优先搜索(DFS)。

广度优先搜索(BFS)

在树结构中,根连接到其子节点,每个子节点还可以继续表示为树。 在广度优先搜索中,我们从节点(主要是根节点)开始,并且在访问其他邻居节点之前首先访问所有相邻节点。 换句话说,我们在使用BFS时必须逐级移动。

clipboard.png

使用BFS,会得到以下的序列。

clipboard.png

伪代码如下:

procedure BFS(Node root)
    Q := empty queue
    Q.enqueue(root)
    
    while(Q != empty) 
        u := Q.dequeue()
        for each node w that is childnode of u
            Q.enqueue(w)
        end for each
    end while
end procedure        

下面是PHP代码。

class TreeNode
{
    public $data = null;
    public $children = [];

    public function __construct(string $data = null)
    {
        $this->data = $data;
    }

    public function addChildren(TreeNode $treeNode)
    {
        $this->children[] = $treeNode;
    }
}

class Tree
{
    public $root = null;

    public function __construct(TreeNode $treeNode)
    {
        $this->root = $treeNode;
    }

    public function BFS(TreeNode $node): SplQueue
    {
        $queue = new SplQueue();
        $visited = new SplQueue();

        $queue->enqueue($node);

        while (!$queue->isEmpty()) {
            $current = $queue->dequeue();
            $visited->enqueue($current);

            foreach ($current->children as $children) {
                $queue->enqueue($children);
            }
        }

        return $visited;
    }
}

完整的例子和测试,你可以点击这里查看

如果想要查找节点是否存在,可以为当前节点值添加简单的条件判断即可。BFS最差的时间复杂度是O(|V| + |E|),其中V是顶点或节点的数量,E则是边或者节点之间的连接数,最坏的情况空间复杂度是O(|V|)。

图的BFS和上面的类似,但略有不同。 由于图是可以循环的(可以创建循环),需要确保我们不会重复访问同一节点以创建无限循环。 为了避免重新访问图节点,必须跟踪已经访问过的节点。可以使用队列,也可以使用图着色算法来解决。

深度优先搜索(DFS)

深度优先搜索(DFS)指的是从一个节点开始搜索,并从目标节点通过分支尽可能深地到达节点。 DFS与BFS不同,简单来说,就是DFS是深入挖掘而不是先扩散。DFS在到达分支末尾时然后向上回溯,并移动到下一个可用的相邻节点,直到搜索结束。还是上面的树

clipboard.png

这次我们会获得不通的遍历顺序:

clipboard.png

从根开始,然后访问第一个孩子,即3。然后,到达3的子节点,并反复执行此操作,直到我们到达分支的底部。在DFS中,我们将采用递归方法来实现。

procedure DFS(Node current)
    for each node v that is childnode of current
       DFS(v)
    end for each
end procedure
 public function DFS(TreeNode $node): SplQueue
{
    $this->visited->enqueue($node);

    if ($node->children) {
        foreach ($node->children as $child) {
            $this->DFS($child);
        }
    }

    return $this->visited;
}

如果需要使用迭代实现,必须记住使用栈而不是队列来跟踪要访问的下一个节点。下面使用迭代方法的实现

public function DFS(TreeNode $node): SplQueue
{
    $stack = new SplStack();
    $visited = new SplQueue();

    $stack->push($node);

    while (!$stack->isEmpty()) {
        $current = $stack->pop();
        $visited->enqueue($current);

        foreach ($current->children as $child) {
            $stack->push($child);
        }
    }

    return $visited;
}

这看起来与BFS算法非常相似。主要区别在于使用栈而不是队列来存储被访问节点。它会对结果产生影响。上面的代码将输出8 10 14 13 3 6 7 4 1。这与我们使用迭代的算法输出不同,但其实这个结果没有毛病。

因为使用栈来存储特定节点的子节点。对于值为8的根节点,第一个值是3的子节点首先入栈,然后,10入栈。由于10后来入栈,它遵循LIFO。所以,如果我们使用栈实现DFS,则输出总是从最后一个分支开始到第一个分支。可以在DFS代码中进行一些小调整来达到想要的效果。

public function DFS(TreeNode $node): SplQueue
{
    $stack = new SplStack();
    $visited = new SplQueue();

    $stack->push($node);

    while (!$stack->isEmpty()) {
        $current = $stack->pop();
        $visited->enqueue($current);

        $current->children = array_reverse($current->children);
        foreach ($current->children as $child) {
            $stack->push($child);
        }
    }

    return $visited;
}

由于栈遵循Last-in,First-out(LIFO),通过反转,可以确保先访问第一个节点,因为颠倒了顺序,栈实际上就作为队列在工作。要是我们搜索的是二叉树,就不需要任何反转,因为我们可以选择先将右孩子入栈,然后左子节点首先出栈。

DFS的时间复杂度类似于BFS。

查看原文

赞 51 收藏 41 评论 4

某勒个杰 收藏了文章 · 2019-12-05

PHP面试:常见查找算法一篇说透

预警

在本篇文章中,将为各位老铁介绍不同的搜索算法以及它们的复杂度。因为力求通俗易懂,所以篇幅可能较长,大伙可以先Mark下来,每天抽时间看一点理解一点。本文配套的Github Repo,欢迎各位老铁star,会一直更新的。

开篇

和排序类似,搜索或者叫做查找,也是平时我们使用最多的算法之一。无论我们搜索数据库还是文件,实际上都在使用某种搜索算法来定位想要查找的数据。

线性查找

执行搜索的最常见的方法是将每个项目与我们正在寻找的数据进行比较,这就是线性搜索或顺序搜索。它是执行搜索的最基本的方式。如果列表中有n项。在最坏的情况下。我们必须搜索n个项目才能找到一个特定的项目。下面遍历一个数组来查找一个项目。

function linearSearch(array $arr, int $needle) {
    for ($i = 0, $count = count($arr); $i < $count; $i++) {
        if ($needle === $arr[$i]) {
            return true;
        }
    }

    return false;
}

线性查找的复杂度

best time complexityO(1)
worst time complexityO(n)
Average time complexityO(n)
Space time complexityO(1)

二分搜索

线性搜索的平均时间复杂度或最坏时间复杂度是O(n),这不会随着待搜索数组的顺序改变而改变。所以如果数组中的项按特定顺序排序,我们不必进行线性搜索。我们可以通过执行选择性搜索而可以获得更好的结果。最流行也是最著名的搜索算法是“二分搜索”。虽然有点像我们之前说的二叉搜索树,但我们不用构造二叉搜索树就可以使用这个算法。


function binarySearch(array $arr, int $needle) {
    $low = 0;
    $high = count($arr) - 1;

    while ($low <= $high) {
        $middle = (int)(($high + $low) / 2);

        if ($arr[$middle] < $needle) {
            $low = $middle + 1;
        } elseif ($arr[$middle] > $needle) {
            $high = $middle - 1;
        } else {
            return true;
        }
    }

    return false;
}

在二分搜索算法中,我们从数据的中间开始,检查中间的项是否比我们要寻找的项小或大,并决定走哪条路。这样,我们把列表分成两半,一半完全丢弃,像下面的图像一样。

clipboard.png

递归版本:

function binarySearchRecursion(array $arr, int $needle, int $low, int $high)
{
    if ($high < $low) return false;

    $middle = (int)(($high + $low) / 2);

    if ($arr[$middle] < $needle) {
        return binarySearchRecursion($arr, $needle, $middle + 1, $high);
    } elseif ($arr[$middle] > $needle) {
        return binarySearchRecursion($arr, $needle, $low, $middle - 1);
    } else {
        return true;
    }
}

二分搜索复杂度分析

对于每一次迭代,我们将数据划分为两半,丢弃一半,另一半用于搜索。在分别进行了1,2次和3次迭代之后,我们的列表长度逐渐减少到n/2,n/4,n/8...。因此,我们可以发现,k次迭代后,将只会留下n/2^k项。最后的结果就是 n/2^k = 1,然后我们两边分别取对数 得到 k = log(n),这就是二分搜索算法的最坏运行时间复杂度。

best time complexityO(1)
worst time complexityO(log n)
Average time complexityO(log n)
Space time complexityO(1)

重复二分查找

有这样一个场景,假如我们有一个含有重复数据的数组,如果我们想从数组中找到2的第一次出现的位置,使用之前的算法将会返回第5个元素。然而,从下面的图像中我们可以清楚地看到,正确的结果告诉我们它不是第5个元素,而是第2个元素。因此,上述二分搜索算法需要进行修改,将它修改成一个重复的搜索,搜索直到元素第一次出现的位置才停止。

clipboard.png

function repetitiveBinarySearch(array $data, int $needle)
{
    $low = 0;
    $high = count($data);
    $firstIndex = -1;

    while ($low <= $high) {
        $middle = ($low + $high) >> 1;

        if ($data[$middle] === $needle) {
            $firstIndex = $middle;
            $high = $middle - 1;
        } elseif ($data[$middle] > $needle) {
            $high = $middle - 1;
        } else {
            $low = $middle + 1;
        }
    }

    return $firstIndex;
}

首先我们检查mid所对应的值是否是我们正在寻找的值。 如果是,那么我们将中间索引指定为第一次出现的index,我们继续检查中间元素左侧的元素,看看有没有再次出现我们寻找的值。 然后继续迭代,直到$low > $high。 如果没有再次找到这个值,那么第一次出现的位置就是该项的第一个索引的值。 如果没有,像往常一样返回-1。我们运行一个测试来看代码是否正确:

public function testRepetitiveBinarySearch()
{
    $arr = [1,1,1,2,3,4,5,5,5,5,5,6,7,8,9,10];

    $firstIndex = repetitiveBinarySearch($arr, 6);

    $this->assertEquals(11, $firstIndex);
}

发现结果正确。

clipboard.png

到目前为止,我们可以得出结论,二分搜索肯定比线性搜索更快。但是,这一切的先决条件是数组已经排序。在未排序的数组中应用二分搜索会导致错误的结果。 那可能存在一种情况,就是对于某个数组,我们不确定它是否已排序。现在有一个问题就是,是否应该首先对数组进行排序然后应用二分查找算法吗?还是继续使用线性搜索算法?

小思考

对于一个包含n个项目的数组,并且它们没有排序。由于我们知道二分搜索更快,我们决定先对其进行排序,然后使用二分搜索。但是,我们清楚最好的排序算法,其最差的时间复杂度是O(nlogn),而对于二分搜索,最坏情况复杂度是O(logn)。所以,如果我们排序后应用二分搜索,复杂度将是O(nlogn)。

但是,我们也知道,对于任何线性或顺序搜索(排序或未排序),最差的时间复杂度是O(n),显然好于上述方案。

考虑另一种情况,即我们需要多次搜索给定数组。我们将k表示为我们想要搜索数组的次数。如果k为1,那么我们可以很容易地应用之前的线性搜索方法。如果k的值比数组的大小更小,暂且使用n表示数组的大小。如果k的值更接近或大于n,那么我们在应用线性方法时会遇到一些问题。假设k = n,线性搜索将具有O(n2)的复杂度。现在,如果我们进行排序然后再进行搜索,那么即使k更大,一次排序也只会花费O(nlogn)时间复。然后,每次搜索的复杂度是O(logn),n次搜索的复杂度是O(nlogn)。如果我们在这里采取最坏的运行情况,排序后然后搜索k次总的的复杂度是O(nlogn),显然这比顺序搜索更好。

我们可以得出结论,如果一些搜索操作的次数比数组的长度小,最好不要对数组进行排序,直接执行顺序搜索即可。但是,如果搜索操作的次数与数组的大小相比更大,那么最好先对数组进行排序,然后使用二分搜索。

二分搜索算法有很多不同的版本。我们不是每次都选择中间索引,我们可以通过计算作出决策来选择接下来要使用的索引。我们现在来看二分搜索算法的两种变形:插值搜索和指数搜索。

插值搜索

在二分搜索算法中,总是从数组的中间开始搜索过程。 如果一个数组是均匀分布的,并且我们正在寻找的数据可能接近数组的末尾,那么从中间搜索可能不是一个好选择。 在这种情况下,插值搜索可能非常有用。插值搜索是对二分搜索算法的改进,插值搜索可以基于搜索的值选择到达不同的位置。例如,如果我们正在搜索靠近数组开头的值,它将直接定位到到数组的第一部分而不是中间。使用公式计算位置,如下所示

clipboard.png

可以发现,我们将从通用的mid =(low * high)/2 转变为更复杂的等式。如果搜索的值更接近arr[high],则此公式将返回更高的索引,如果值更接近arr[low],则此公式将返回更低的索引。

function interpolationSearch(array $arr, int $needle)
{
    $low = 0;
    $high = count($arr) - 1;

    while ($arr[$low] != $arr[$high] && $needle >= $arr[$low] && $needle <= $arr[$high]) {
        $middle = intval($low + ($needle - $arr[$low]) * ($high - $low) / ($arr[$high] - $arr[$low]));

        if ($arr[$middle] < $needle) {
            $low = $middle + 1;
        } elseif ($arr[$middle] > $needle) {
            $high = $middle - 1;
        } else {
            return $middle;
        }
    }

    if ($needle == $arr[$low]) {
        return $low;
    } 
    
    return -1;
    
}

插值搜索需要更多的计算步骤,但是如果数据是均匀分布的,这个算法的平均复杂度是O(log(log n)),这比二分搜索的复杂度O(logn)要好得多。 此外,如果值的分布不均匀,我们必须要小心。 在这种情况下,插值搜索的性能可以需要重新评估。下面我们将探索另一种称为指数搜索的二分搜索变体。

指数搜索

在二分搜索中,我们在整个列表中搜索给定的数据。指数搜索通过决定搜索的下界和上界来改进二分搜索,这样我们就不会搜索整个列表。它减少了我们在搜索过程中比较元素的数量。指数搜索是在以下两个步骤中完成的:

1.我们通过查找第一个指数k来确定边界大小,其中值2^k的值大于搜索项。 现在,2^k和2^(k-1)分别成为上限和下限。
2.使用以上的边界来进行二分搜索。

下面我们来看下PHP实现的代码

function exponentialSearch(array $arr, int $needle): int
{
    $length = count($arr);
    if ($length == 0) return -1;

    $bound = 1;

    while ($bound < $length && $arr[$bound] < $needle) {
        $bound *= 2;
    }

    return binarySearchRecursion($arr, $needle, $bound >> 1, min($bound, $length));
}

我们把$needle出现的位置记位i,那么我们第一步花费的时间复杂度就是O(logi)。表示为了找到上边界,我们的while循环需要执行O(logi)次。因为下一步应用一个二分搜索,时间复杂度也是O(logi)。我们假设j是我们上一个while循环执行的次数,那么本次二分搜索我们需要搜索的范围就是2^j-1 至 2^j,而j=logi,即

clipboard.png

那我们的二分搜索时间复杂度需要对这个范围求log2,即

clipboard.png

那么整个指数搜索的时间复杂度就是2 O(logi),省略掉常数就是O(logi)。

best time complexityO(1)
worst time complexityO(log i)
Average time complexityO(log i)
Space time complexityO(1)

哈希查找

在搜索操作方面,哈希表可以是非常有效的数据结构。在哈希表中,每个数据都有一个与之关联的唯一索引。如果我们知道要查看哪个索引,我们就可以非常轻松地找到对应的值。通常,在其他编程语言中,我们必须使用单独的哈希函数来计算存储值的哈希索引。散列函数旨在为同一个值生成相同的索引,并避免冲突。

PHP底层C实现中数组本身就是一个哈希表,由于数组是动态的,不必担心数组溢出。我们可以将值存储在关联数组中,以便我们可以将值与键相关联。

function hashSearch(array $arr, int $needle)
{
    return isset($arr[$needle]) ? true : false;
}

树搜索

搜索分层数据的最佳方案之一是创建搜索树。在第理解和实现树中,我们了解了如何构建二叉搜索树并提高搜索效率,并且介绍了遍历树的不同方法。 现在,继续介绍两种最常用的搜索树的方法,通常称为广度优先搜索(BFS)和深度优先搜索(DFS)。

广度优先搜索(BFS)

在树结构中,根连接到其子节点,每个子节点还可以继续表示为树。 在广度优先搜索中,我们从节点(主要是根节点)开始,并且在访问其他邻居节点之前首先访问所有相邻节点。 换句话说,我们在使用BFS时必须逐级移动。

clipboard.png

使用BFS,会得到以下的序列。

clipboard.png

伪代码如下:

procedure BFS(Node root)
    Q := empty queue
    Q.enqueue(root)
    
    while(Q != empty) 
        u := Q.dequeue()
        for each node w that is childnode of u
            Q.enqueue(w)
        end for each
    end while
end procedure        

下面是PHP代码。

class TreeNode
{
    public $data = null;
    public $children = [];

    public function __construct(string $data = null)
    {
        $this->data = $data;
    }

    public function addChildren(TreeNode $treeNode)
    {
        $this->children[] = $treeNode;
    }
}

class Tree
{
    public $root = null;

    public function __construct(TreeNode $treeNode)
    {
        $this->root = $treeNode;
    }

    public function BFS(TreeNode $node): SplQueue
    {
        $queue = new SplQueue();
        $visited = new SplQueue();

        $queue->enqueue($node);

        while (!$queue->isEmpty()) {
            $current = $queue->dequeue();
            $visited->enqueue($current);

            foreach ($current->children as $children) {
                $queue->enqueue($children);
            }
        }

        return $visited;
    }
}

完整的例子和测试,你可以点击这里查看

如果想要查找节点是否存在,可以为当前节点值添加简单的条件判断即可。BFS最差的时间复杂度是O(|V| + |E|),其中V是顶点或节点的数量,E则是边或者节点之间的连接数,最坏的情况空间复杂度是O(|V|)。

图的BFS和上面的类似,但略有不同。 由于图是可以循环的(可以创建循环),需要确保我们不会重复访问同一节点以创建无限循环。 为了避免重新访问图节点,必须跟踪已经访问过的节点。可以使用队列,也可以使用图着色算法来解决。

深度优先搜索(DFS)

深度优先搜索(DFS)指的是从一个节点开始搜索,并从目标节点通过分支尽可能深地到达节点。 DFS与BFS不同,简单来说,就是DFS是深入挖掘而不是先扩散。DFS在到达分支末尾时然后向上回溯,并移动到下一个可用的相邻节点,直到搜索结束。还是上面的树

clipboard.png

这次我们会获得不通的遍历顺序:

clipboard.png

从根开始,然后访问第一个孩子,即3。然后,到达3的子节点,并反复执行此操作,直到我们到达分支的底部。在DFS中,我们将采用递归方法来实现。

procedure DFS(Node current)
    for each node v that is childnode of current
       DFS(v)
    end for each
end procedure
 public function DFS(TreeNode $node): SplQueue
{
    $this->visited->enqueue($node);

    if ($node->children) {
        foreach ($node->children as $child) {
            $this->DFS($child);
        }
    }

    return $this->visited;
}

如果需要使用迭代实现,必须记住使用栈而不是队列来跟踪要访问的下一个节点。下面使用迭代方法的实现

public function DFS(TreeNode $node): SplQueue
{
    $stack = new SplStack();
    $visited = new SplQueue();

    $stack->push($node);

    while (!$stack->isEmpty()) {
        $current = $stack->pop();
        $visited->enqueue($current);

        foreach ($current->children as $child) {
            $stack->push($child);
        }
    }

    return $visited;
}

这看起来与BFS算法非常相似。主要区别在于使用栈而不是队列来存储被访问节点。它会对结果产生影响。上面的代码将输出8 10 14 13 3 6 7 4 1。这与我们使用迭代的算法输出不同,但其实这个结果没有毛病。

因为使用栈来存储特定节点的子节点。对于值为8的根节点,第一个值是3的子节点首先入栈,然后,10入栈。由于10后来入栈,它遵循LIFO。所以,如果我们使用栈实现DFS,则输出总是从最后一个分支开始到第一个分支。可以在DFS代码中进行一些小调整来达到想要的效果。

public function DFS(TreeNode $node): SplQueue
{
    $stack = new SplStack();
    $visited = new SplQueue();

    $stack->push($node);

    while (!$stack->isEmpty()) {
        $current = $stack->pop();
        $visited->enqueue($current);

        $current->children = array_reverse($current->children);
        foreach ($current->children as $child) {
            $stack->push($child);
        }
    }

    return $visited;
}

由于栈遵循Last-in,First-out(LIFO),通过反转,可以确保先访问第一个节点,因为颠倒了顺序,栈实际上就作为队列在工作。要是我们搜索的是二叉树,就不需要任何反转,因为我们可以选择先将右孩子入栈,然后左子节点首先出栈。

DFS的时间复杂度类似于BFS。

查看原文

某勒个杰 报名了系列讲座 · 2019-11-11

某勒个杰 赞了文章 · 2019-10-30

MySQL的又一神器-锁,MySQL面试必备

原文链接:blog.ouyangsihai.cn >> MySQL的又一神器-锁,MySQL面试必备

在看这篇文章之前,我们回顾一下前面的几篇关于MySQL的文章,应该对你读下面的文章有所帮助。

1 什么是锁

1.1 锁的概述

在生活中锁的例子多的不能再多了,从古老的简单的门锁,到密码锁,再到现在的指纹解锁,人脸识别锁,这都是锁的鲜明的例子,所以,我们理解锁应该是非常简单的。

再到MySQL中的锁,对于MySQL来说,锁是一个很重要的特性,数据库的锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性,这样才能保证在高并发的情况下,访问数据库的时候,数据不会出现问题。

1.2 锁的两个概念

在数据库中,lock和latch都可以称为锁,但是意义却不同。

Latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短。若持续的时间长,则应用的性能会非常差,在InnoDB引擎中,Latch又可以分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测的机制。

Lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行。并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同)。

2 InnoDB存储引擎中的锁

2.1 锁的粒度

在数据库中,锁的粒度的不同可以分为表锁、页锁、行锁,这些锁的粒度之间也是会发生升级的,锁升级的意思就是讲当前锁的粒度降低,数据库可以把一个表的1000个行锁升级为一个页锁,或者将页锁升级为表锁,下面分别介绍一下这三种锁的粒度(参考自博客:https://blog.csdn.net/baoling...)。

表锁

表级别的锁定是MySQL各存储引擎中最大颗粒度的锁定机制。该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。由于表级锁一次会将整个表锁定,所以可以很好的避免困扰我们的死锁问题。

当然,锁定颗粒度大所带来最大的负面影响就是出现锁定资源争用的概率也会最高,致使并大度大打折扣。

使用表级锁定的主要是MyISAM,MEMORY,CSV等一些非事务性存储引擎。

特点: 开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

页锁

页级锁定是MySQL中比较独特的一种锁定级别,在其他数据库管理软件中也并不是太常见。页级锁定的特点是锁定颗粒度介于行级锁定与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力也同样是介于上面二者之间。另外,页级锁定和行级锁定一样,会发生死锁。
在数据库实现资源锁定的过程中,随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量是越来越多的,实现算法也会越来越复杂。不过,随着锁定资源 颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也随之提升。
使用页级锁定的主要是BerkeleyDB存储引擎。

特点: 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

行锁

行级锁定最大的特点就是锁定对象的粒度很小,也是目前各大数据库管理软件所实现的锁定颗粒度最小的。由于锁定颗粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能。

虽然能够在并发处理能力上面有较大的优势,但是行级锁定也因此带来了不少弊端。由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。

特点: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

比较表锁我们可以发现,这两种锁的特点基本都是相反的,而从锁的角度来说,表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

MySQL 不同引擎支持的锁的粒度

2.2 锁的类型

InnoDB存储引擎中存在着不同类型的锁,下面一一介绍一下。

S or X (共享锁、排他锁)

数据的操作其实只有两种,也就是读和写,而数据库在实现锁时,也会对这两种操作使用不同的锁;InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和互斥锁(Exclusive Lock)

  • 共享锁(读锁)(S Lock),允许事务读一行数据。
  • 排他锁(写锁)(X Lock),允许事务删除或更新一行数据。

IS or IX (共享、排他)意向锁

为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB存储引擎支持一种额外的锁方式,就称为意向锁,意向锁在 InnoDB 中是表级锁,意向锁分为:

  • 意向共享锁:表达一个事务想要获取一张表中某几行的共享锁。
  • 意向排他锁:表达一个事务想要获取一张表中某几行的排他锁。

另外,这些锁之间的并不是一定可以共存的,有些锁之间是不兼容的,所谓兼容性就是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。

下面我们再看一下这两种锁的兼容性。

  • S or X (共享锁、排他锁)的兼容性

  • IS or IX (共享、排他)意向锁的兼容性

3 前面小结

这里用一个思维导图把前面的概念做一个小结。

4 一致性非锁定读和一致性锁定读

一致性锁定读(Locking Reads)

在一个事务中查询数据时,普通的SELECT语句不会对查询的数据进行加锁,其他事务仍可以对查询的数据执行更新和删除操作。因此,InnoDB提供了两种类型的锁定读来保证额外的安全性:

  • SELECT ... LOCK IN SHARE MODE
  • SELECT ... FOR UPDATE

SELECT ... LOCK IN SHARE MODE: 对读取的行添加S锁,其他事物可以对这些行添加S锁,若添加X锁,则会被阻塞。

SELECT ... FOR UPDATE: 会对查询的行及相关联的索引记录加X锁,其他事务请求的S锁或X锁都会被阻塞。 当事务提交或回滚后,通过这两个语句添加的锁都会被释放。 注意:只有在自动提交被禁用时,SELECT FOR UPDATE才可以锁定行,若开启自动提交,则匹配的行不会被锁定。

#### 一致性非锁定读

一致性非锁定读(consistent nonlocking read) 是指InnoDB存储引擎通过多版本控制(MVVC)读取当前数据库中行数据的方式。如果读取的行正在执行DELETE或UPDATE操作,这时读取操作不会因此去等待行上锁的释放。相反地,InnoDB会去读取行的一个快照。所以,非锁定读机制大大提高了数据库的并发性。

来自网络:侵权删

一致性非锁定读是InnoDB默认的读取方式,即读取不会占用和等待行上的锁。在事务隔离级别READ COMMITTEDREPEATABLE READ下,InnoDB使用一致性非锁定读。

然而,对于快照数据的定义却不同。在READ COMMITTED事务隔离级别下,一致性非锁定读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,则读取事务开始时的行数据版本

下面我们通过一个简单的例子来说明一下这两种方式的区别。

首先创建一张表;

插入一条数据;

insert into lock_test values(1);

查看隔离级别;

select @@tx_isolation;

下面分为两种事务进行操作。

REPEATABLE READ事务隔离级别下;

REPEATABLE READ事务隔离级别下,读取事务开始时的行数据,所以当会话B修改了数据之后,通过以前的查询,还是可以查询到数据的。

READ COMMITTED事务隔离级别下;

READ COMMITTED事务隔离级别下,读取该行版本最新的一个快照数据,所以,由于B会话修改了数据,并且提交了事务,所以,A读取不到数据了。

5 行锁的算法

InnoDB存储引擎有3种行锁的算法,其分别是:

  • Record Lock:单个行记录上的锁。
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。

Record Lock:总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。

Next-Key Lock:结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。举个例子10,20,30,那么该索引可能被Next-Key Locking的区间为:

除了Next-Key Locking,还有Previous-Key Locking技术,这种技术跟Next-Key Lock正好相反,锁定的区间是区间范围和前一个值。同样上述的值,使用Previous-Key Locking技术,那么可锁定的区间为:

不是所有索引都会加上Next-key Lock的,这里有一种特殊的情况,在查询的列是唯一索引(包含主键索引)的情况下,Next-key Lock会降级为Record Lock

接下来,我们来通过一个例子解释一下。

CREATE TABLE test (
    x INT,
    y INT,
    PRIMARY KEY(x),    // x是主键索引
    KEY(y)    // y是普通索引
);
INSERT INTO test select 3, 2;
INSERT INTO test select 5, 3;
INSERT INTO test select 7, 6;
INSERT INTO test select 10, 8;

我们现在会话A中执行如下语句;

SELECT * FROM test WHERE y = 3 FOR UPDATE

我们分析一下这时候的加锁情况。

  • 对于主键x

  • 辅助索引y

用户可以通过以下两种方式来显示的关闭Gap Lock:

  • 将事务的隔离级别设为 READ COMMITED。
  • 将参数innodb_locks_unsafe_for_binlog设置为1。

Gap Lock的作用:是为了阻止多个事务将记录插入到同一个范围内,设计它的目的是用来解决Phontom Problem(幻读问题)。在MySQL默认的隔离级别(Repeatable Read)下,InnoDB就是使用它来解决幻读问题。

幻读:是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL可能会返回之前不存在的行,也就是第一次执行和第二次执行期间有其他事务往里插入了新的行。

6 锁带来的问题

6.1 脏读

脏读: 在不同的事务下,当前事务可以读到另外事务未提交的数据。另外我们需要注意的是默认的MySQL隔离级别是REPEATABLE READ是不会发生脏读的,脏读发生的条件是需要事务的隔离级别为READ UNCOMMITTED,所以如果出现脏读,可能就是这种隔离级别导致的。

下面我们通过一个例子看一下。

从上面这个例子可以看出,当我们的事务的隔离级别为READ UNCOMMITTED的时候,在会话A还没有提交时,会话B就能够查询到会话A没有提交的数据。

6.2 不可重复读

不可重复读: 是指在一个事务内多次读取同一集合的数据,但是多次读到的数据是不一样的,这就违反了数据库事务的一致性的原则。但是,这跟脏读还是有区别的,脏读的数据是没有提交的,但是不可重复读的数据是已经提交的数据。

我们通过下面的例子来看一下这种问题的发生。

从上面的例子可以看出,在A的一次会话中,由于会话B插入了数据,导致两次查询的结果不一致,所以就出现了不可重复读的问题。

我们需要注意的是不可重复读读取的数据是已经提交的数据,事务的隔离级别为READ COMMITTED,这种问题我们是可以接受的。

如果我们需要避免不可重复读的问题的发生,那么我们可以使用Next-Key Lock算法(设置事务的隔离级别为READ REPEATABLE)来避免,在MySQL中,不可重复读问题就是Phantom Problem,也就是幻像问题

6.3 丢失更新

丢失更新:指的是一个事务的更新操作会被另外一个事务的更新操作所覆盖,从而导致数据的不一致。在当前数据库的任何隔离级别下都不会导致丢失更新问题,要出现这个问题,在多用户计算机系统环境下有可能出现这种问题。

如何避免丢失更新的问题呢,我们只需要让事务的操作变成串行化,不要并行执行就可以。

我们一般使用SELECT ... FOR UPDATE语句,给操作加上一个排他X锁。

6.4 小结

这里我们做一个小结,主要是在不同的事务的隔离级别下出现的问题的对照,这样就更加清晰了。

1、原创不易,老铁,文章需要你的 点赞 让更多的人看到,希望能够帮助到大家!

2、文章有不当之处,欢迎指正,如果喜欢微信阅读,你也可以关注我的微信公众号好好学java,公众号已有 6W 粉丝,回复:1024,获取公众号的大礼包,公众号长期发布 Java 优质系列文章,关注我们一定会让你收获很多!

查看原文

赞 103 收藏 80 评论 4

某勒个杰 收藏了文章 · 2019-07-11

用Flutter构建漂亮的UI界面 - 基础组件篇

图片描述

1. 前言

Flutter作为时下最流行的技术之一,凭借其出色的性能以及抹平多端的差异优势,早已引起大批技术爱好者的关注,甚至一些闲鱼美团腾讯等大公司均已开始使用。虽然目前其生态还没有完全成熟,但身靠背后的Google加持,其发展速度已经足够惊人,可以预见将来对Flutter开发人员的需求也会随之增长。

无论是为了现在的技术尝鲜还是将来的潮流趋势,都9102年了,作为一个前端开发者,似乎没有理由不去尝试它。正是带着这样的心理,笔者也开始学习Flutter,同时建了一个用于练习的仓库,后续所有代码都会托管在上面,欢迎star,一起学习。这是我写的Flutter系列文章:

今天分享的是Flutter中最常用到的一些基础组件,它们是构成UI界面的基础元素:容器绝对定位布局文本图片图标等。

2. 基础组件

2.1 Container(容器组件)

容器组件

Container组件是最常用的布局组件之一,可以认为它是web开发中的div,rn开发中的View。其往往可以用来控制大小、背景颜色、边框、阴影、内外边距和内容排列方式等。我们先来看下其构造函数:

Container({
  Key key,
  double width,
  double height,
  this.margin,
  this.padding,
  Color color,
  this.alignment,
  BoxConstraints constraints,
  Decoration decoration,
  this.foregroundDecoration,
  this.transform,
  this.child,
})

2.1.1 widthheightmarginpadding

这些属性的含义和我们已经熟知的并没有区别。唯一需要注意的是,marginpadding的赋值不是一个简单的数字,因为其有left, top, right, bottom四个方向的值需要设置。Flutter提供了EdgeInsets这个类,帮助我们方便地生成四个方向的值。通常情况下,我们可能会用到EdgeInsets的4种构造方法:

  • EdgeInsets.all(value): 用于设置4个方向一样的值;
  • EdgeInsets.only(left: val1, top: val2, right: val3, bottom: val4): 可以单独设置某个方向的值;
  • EdgeInsets.symmetric(horizontal: val1, vertical: val2): 用于设置水平/垂直方向上的值;
  • EdgeInsets.fromLTRB(left, top, right, bottom): 按照左上右下的顺序设置4个方向的值。

2.1.2 color

该属性的含义是背景颜色,等同于web/rn中的backgroundColor。需要注意的是Flutter中有一个专门表示颜色的Color类,而非我们常用的字符串。不过我们可以非常轻松地进行转换,举个栗子:

在web/rn中我们会用'#FF0000''red'来表示红色,而在Flutter中,我们可以用Color(0xFFFF0000)Colors.red来表示。

2.1.3 alignment

该属性是用来决定Container组件的子组件将以何种方式进行排列(PS:再也不用为怎么居中操心了)。其可选值通常会用到:

  • Alignment.topLeft: 左上
  • Alignment.topCenter: 上中
  • Alignment.topRight: 右上
  • Alignment.centerLeft: 左中
  • Alignment.center: 居中
  • Alignment.centerRight: 右中
  • Alignment.bottomLeft: 左下
  • Alignment.bottomCenter: 下中
  • Alignment.bottomRight: 右下

2.1.4 constraints

在web/rn中我们通常会用minWidth/maxWidth/minHeight/maxHeight等属性来限制容器的宽高。在Flutter中,你需要使用BoxConstraints(盒约束)来实现该功能。

// 容器的大小将被限制在[100*100 ~ 200*200]内
BoxConstraints(
  minWidth: 100,
  maxWidth: 200,
  minHeight: 100,
  maxHeight: 200,
)

2.1.5 decoration

该属性非常强大,字面意思是装饰,因为通过它你可以设置边框阴影渐变圆角等常用属性。BoxDecoration继承自Decoration类,因此我们通常会生成一个BoxDecoration实例来设置这些属性。

1) 边框

可以用Border.all构造函数直接生成4条边框,也可以用Border构造函数单独设置不同方向上的边框。不过令人惊讶的是官方提供的边框竟然不支持虚线issue在这里)。

// 同时设置4条边框:1px粗细的黑色实线边框
BoxDecoration(
  border: Border.all(color: Colors.black, width: 1, style: BorderStyle.solid)
)

// 设置单边框:上边框为1px粗细的黑色实线边框,右边框为1px粗细的红色实线边框
BoxDecoration(
  border: Border(
    top: BorderSide(color: Colors.black, width: 1, style: BorderStyle.solid),
    right: BorderSide(color: Colors.red, width: 1, style: BorderStyle.solid),
  ),
)

2) 阴影

阴影属性和web中的boxShadow几乎没有区别,可以指定xyblurspreadcolor等属性。

BoxDecoration(
  boxShadow: [
    BoxShadow(
      offset: Offset(0, 0),
      blurRadius: 6,
      spreadRadius: 10,
      color: Color.fromARGB(20, 0, 0, 0),
    ),
  ],
)

3) 渐变

如果你不想容器的背景颜色是单调的,可以尝试用gradient属性。Flutter同时支持线性渐变径向渐变

// 从左到右,红色到蓝色的线性渐变
BoxDecoration(
  gradient: LinearGradient(
    begin: Alignment.centerLeft,
    end: Alignment.centerRight,
    colors: [Colors.red, Colors.blue],
  ),
)

// 从中心向四周扩散,红色到蓝色的径向渐变
BoxDecoration(
  gradient: RadialGradient(
    center: Alignment.center,
    colors: [Colors.red, Colors.blue],
  ),
)

4) 圆角

通常情况下,你可能会用到BorderRadius.circular构造函数来同时设置4个角的圆角,或是BorderRadius.only构造函数来单独设置某几个角的圆角:

// 同时设置4个角的圆角为5
BoxDecoration(
  borderRadius: BorderRadius.circular(5),
)

// 设置单圆角:左上角的圆角为5,右上角的圆角为10
BoxDecoration(
  borderRadius: BorderRadius.only(
    topLeft: Radius.circular(5),
    topRight: Radius.circular(10),
  ),
)

2.1.6 transform

transform属性和我们在web/rn中经常用到的基本也没有差别,主要包括:平移缩放旋转倾斜。在Flutter中,封装了矩阵变换类Matrix4帮助我们进行变换:

  • translationValues(x, y, z): 平移x, y, z;
  • rotationX(radians): x轴旋转radians弧度;
  • rotationY(radians): y轴旋转radians弧度;
  • rotationZ(radians): z轴旋转radians弧度;
  • skew(alpha, beta): x轴倾斜alpha度,y轴倾斜beta度;
  • skewX(alpha): x轴倾斜alpha度;
  • skewY(beta): y轴倾斜beta度;

2.1.7 小结

Container组件的属性很丰富,虽然有些用法上和web/rn有些许差异,但基本上大同小异,所以过渡起来也不会有什么障碍。另外,由于Container组件是单子节点组件,也就是只允许子节点有一个。所以在布局上,很多时候我们会用RowColumn组件进行/布局。

2.2 Row/Column(行/列组件)

行/列组件

RowColumn组件其实和web/rn中的Flex布局(弹性盒子)特别相似,或者我们可以就这么理解。使用Flex布局的同学对主轴次轴的概念肯定都已经十分熟悉,Row组件的主轴就是横向,Column组件的主轴就是纵向。且它们的构造函数十分相似(已省略不常用属性):

Row({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  List<Widget> children = const <Widget>[],
})

Column({
  Key key,
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  List<Widget> children = const <Widget>[],
})

2.2.1 mainAxisAlignment

该属性的含义是主轴排列方式,根据上述构造函数可以知道RowColumn组件在主轴方向上默认都是从start开始,也就是说Row组件默认从左到右开始排列子组件,Column组件默认从上到下开始排列子组件。

当然,你还可以使用其他的可选值:

  • MainAxisAlignment.start
  • MainAxisAlignment.end
  • MainAxisAlignment.center
  • MainAxisAlignment.spaceBetween
  • MainAxisAlignment.spaceAround
  • MainAxisAlignment.spaceEvenly

2.2.2 crossAxisAlignment

该属性的含义是次轴排列方式,根据上述构造函数可以知道RowColumn组件在次轴方向上默认都是居中。

这里有一点需要特别注意:由于Column组件次轴方向上(即水平)默认是居中对齐,所以水平方向上不会撑满其父容器,此时需要指定CrossAxisAlignment.stretch才可以。

另外,crossAxisAlignment其他的可选值有:

  • crossAxisAlignment.start
  • crossAxisAlignment.end
  • crossAxisAlignment.center
  • crossAxisAlignment.stretch
  • crossAxisAlignment.baseline

2.2.3 mainAxisSize

字面意思上来说,该属性指的是在主轴上的尺寸。其实就是指在主轴方向上,是包裹其内容,还是撑满其父容器。它的可选值有MainAxisSize.minMainAxisSize.max。由于其默认值都是MainAxisSize.max,所以主轴方向上默认大小都是尽可能撑满父容器的。

2.2.4 小结

由于Row/Column组件和我们熟悉的Flex布局非常相似,所以上手起来非常容易,几乎零学习成本。

2.3 Stack/Positoned(绝对定位布局组件)

绝对定位布局组件

绝对定位布局在web/rn开发中也是使用频率较高的一种布局方式,Flutter也提供了相应的组件实现,需要将StackPositioned组件搭配在一起使用。比如下方的这个例子就是创建了一个黄色的盒子,并且在其四个角落放置了4个红色的小正方形。Stack组件就是绝对定位的容器,Positioned组件通过lefttop rightbottom四个方向上的属性值来决定其在父容器中的位置。

Container(
  height: 100,
  color: Colors.yellow,
  child: Stack(
    children: <Widget>[
      Positioned(
        left: 10,
        top: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
      Positioned(
        right: 10,
        top: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
      Positioned(
        left: 10,
        bottom: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
      Positioned(
        right: 10,
        bottom: 10,
        child: Container(width: 10, height: 10, color: Colors.red),
      ),
    ],
  ),
)

2.4 Text(文本组件)

(文本组件)

Text组件也是日常开发中最常用的基础组件之一,我们通常用它来展示文本信息。来看下其构造函数(已省略不常用属性):

const Text(
  this.data, {
  Key key,
  this.style,
  this.textAlign,
  this.softWrap,
  this.overflow,
  this.maxLines,
})
  • data: 显示的文本信息;
  • style: 文本样式,Flutter提供了一个TextStyle类,最常用的fontSizefontWeightcolorbackgroundColorshadows等属性都是通过它设置的;
  • textAlign: 文字对齐方式,常用可选值有TextAlignleftrightcenterjustify
  • softWrap: 文字是否换行;
  • overflow: 当文字溢出的时候,以何种方式处理(默认直接截断)。可选值有TextOverflowclipfadeellipsisvisible
  • maxLines: 当文字超过最大行数还没显示完的时候,就会根据overflow属性决定如何截断处理。

FlutterText组件足够灵活,提供了各种属性让我们定制,不过一般情况下,我们更多地只需下方几行代码就足够了:

Text(
  '这是测试文本',
  style: TextStyle(
    fontSize: 13,
    fontWeight: FontWeight.bold,
    color: Color(0xFF999999),
  ),
)

除了上述的应用场景外,有时我们还会遇到富文本的需求(即一段文本中,可能需要不同的字体样式)。比如在一些UI设计中经常会遇到表示价格的时候,符号比金额的字号小点。对于此类需求,我们可以用Flutter提供的Text.rich构造函数来创建相应的文本组件:

Text.rich(TextSpan(
  children: [
    TextSpan(
      '¥',
      style: TextStyle(
        fontSize: 12,
        color: Color(0xFFFF7528),
      ),
    ),
    TextSpan(
      '258',
      style: TextStyle(
        fontSize: 15,
        color: Color(0xFFFF7528),
      ),
    ),
  ]
))

2.5 Image(图片组件)

图片组件

Image图片组件作为丰富内容的基础组件之一,日常开发中的使用频率也非常高。看下其构造函数(已省略不常用属性):

Image({
  Key key,
  @required this.image,
  this.width,
  this.height,
  this.color,
  this.fit,
  this.repeat = ImageRepeat.noRepeat,
})
  • image: 图片源,最常用到主要有两种(AssetImageNetworkImage)。使用AssetImage之前,需要在pubspec.yaml文件中声明好图片资源,然后才能使用;而NextworkImage指定图片的网络地址即可,主要是在加载一些网络图片时会用到;
  • width: 图片宽度;
  • height: 图片高度;
  • color: 图片的背景颜色,当网络图片未加载完毕之前,会显示该背景颜色;
  • fit: 当我们希望图片根据容器大小进行适配而不是指定固定的宽高值时,可以通过该属性来实现。其可选值有BoxFitfillcontaincoverfitWidthfitHeightnonescaleDown
  • repeat: 决定当图片实际大小不足指定大小时是否使用重复效果。

另外,Flutter还提供了Image.networkImage.asset构造函数,其实是语法糖。比如下方的两段代码结果是完全一样的:

Image(
  image: NetworkImage('https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg'),
  width: 100,
  height: 100,
)

Image.network(
  'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg',
  width: 100,
  height: 100,
)

2.6 Icon(图标组件)

图标组件

Icon图标组件相比于图片有着放大不会失真的优势,在日常开发中也是经常会被用到。Flutter更是直接内置了一套Material风格的图标(你可以在这里预览所有的图标类型)。看下构造函数:

const Icon(
  this.icon, {
  Key key,
  this.size,
  this.color,
})
  • icon: 图标类型;
  • size: 图标大小;
  • color: 图标颜色。

3. 布局实战

通过上一节的介绍,我们对ContainerRowColumnStackPositionedTextImageIcon组件有了初步的认识。接下来,就让我们通过一个实际的例子来加深理解和记忆。

布局实战

3.1 准备工作 - 数据类型

根据上述卡片中的内容,我们可以定义一些字段。为了规范开发流程,我们先给卡片定义一个数据类型的类,这样在后续的开发过程中也能更好地对数据进行Mock和管理:

class PetCardViewModel {
  /// 封面地址
  final String coverUrl;

  /// 用户头像地址
  final String userImgUrl;

  /// 用户名
  final String userName;

  /// 用户描述
  final String description;

  /// 话题
  final String topic;

  /// 发布时间
  final String publishTime;

  /// 发布内容
  final String publishContent;

  /// 回复数量
  final int replies;

  /// 喜欢数量
  final int likes;

  /// 分享数量
  final int shares;

  const PetCardViewModel({
    this.coverUrl,
    this.userImgUrl,
    this.userName,
    this.description,
    this.topic,
    this.publishTime,
    this.publishContent,
    this.replies,
    this.likes,
    this.shares,
  });
}

3.2 搭建骨架,布局拆分

布局拆分

根据给的视觉图,我们可以将整体进行拆分,一共划分成4个部分:CoverUserInfoPublishContentInteractionArea。为此,我们可以搭起代码的基本骨架:

class PetCard extends StatelessWidget {
  final PetCardViewModel data;

  const PetCard({
    Key key,
    this.data,
  }) : super(key: key);

  Widget renderCover() {
    
  }

  Widget renderUserInfo() {
    
  }

  Widget renderPublishContent() {
  
  }

  Widget renderInteractionArea() {
   
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(8),
        boxShadow: [
          BoxShadow(
            blurRadius: 6,
            spreadRadius: 4,
            color: Color.fromARGB(20, 0, 0, 0),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          this.renderCover(),
          this.renderUserInfo(),
          this.renderPublishContent(),
          this.renderInteractionArea(),
        ],
      ),
    );
  }
}

3.3 封面区域

为了更好的凸现图片的效果,这里加了一个蒙层,所以此处刚好可以用得上Stack/Positioned布局和LinearGradient渐变,Dom结构如下:

封面区域

Widget renderCover() {
  return Stack(
    fit: StackFit.passthrough,
    children: <Widget>[
      ClipRRect(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(8),
          topRight: Radius.circular(8),
        ),
        child: Image.network(
          data.coverUrl,
          height: 200,
          fit: BoxFit.fitWidth,
        ),
      ),
      Positioned(
        left: 0,
        top: 100,
        right: 0,
        bottom: 0,
        child: Container(
          decoration: BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color.fromARGB(0, 0, 0, 0),
                Color.fromARGB(80, 0, 0, 0),
              ],
            ),
          ),
        ),
      ),
    ],
  );
}

3.4 用户信息区域

用户信息区域就非常适合使用RowColumn组件来进行布局,Dom结构如下:

用户信息区域

Widget renderUserInfo() {
  return Container(
    margin: EdgeInsets.only(top: 16),
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Row(
          children: <Widget>[
            CircleAvatar(
              radius: 20,
              backgroundColor: Color(0xFFCCCCCC),
              backgroundImage: NetworkImage(data.userImgUrl),
            ),
            Padding(padding: EdgeInsets.only(left: 8)),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text(
                  data.userName,
                  style: TextStyle(
                    fontSize: 15,
                    fontWeight: FontWeight.bold,
                    color: Color(0xFF333333),
                  ),
                ),
                Padding(padding: EdgeInsets.only(top: 2)),
                Text(
                  data.description,
                  style: TextStyle(
                    fontSize: 12,
                    color: Color(0xFF999999),
                  ),
                ),
              ],
            ),
          ],
        ),
        Text(
          data.publishTime,
          style: TextStyle(
            fontSize: 13,
            color: Color(0xFF999999),
          ),
        ),
      ],
    ),
  );
}

3.5 发布内容区域

通过这块区域的UI练习,我们可以实践Container组件设置不同的borderRadius,以及Text组件文本内容超出时的截断处理,Dom结构如下:

发布内容区域

Widget renderPublishContent() {
  return Container(
    margin: EdgeInsets.only(top: 16),
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        Container(
          margin: EdgeInsets.only(bottom: 14),
          padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
          decoration: BoxDecoration(
            color: Color(0xFFFFC600),
            borderRadius: BorderRadius.only(
              topRight: Radius.circular(8),
              bottomLeft: Radius.circular(8),
              bottomRight: Radius.circular(8),
            ),
          ),
          child: Text(
            '# ${data.topic}',
            style: TextStyle(
              fontSize: 12,
              color: Colors.white,
            ),
          ),
        ),
        Text(
          data.publishContent,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
          style: TextStyle(
            fontSize: 15,
            fontWeight: FontWeight.bold,
            color: Color(0xFF333333),
          ),
        ),
      ],
    ),
  );
}

3.6 互动区域

在这个模块,我们会用到Icon图标组件,可以控制其大小和颜色等属性,Dom结构如下:

互动区域

Widget renderInteractionArea() {
  return Container(
    margin: EdgeInsets.symmetric(vertical: 16),
    padding: EdgeInsets.symmetric(horizontal: 16),
    child: Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: <Widget>[
        Row(
          children: <Widget>[
            Icon(
              Icons.message,
              size: 16,
              color: Color(0xFF999999),
            ),
            Padding(padding: EdgeInsets.only(left: 6)),
            Text(
              data.replies.toString(),
              style: TextStyle(
                fontSize: 15,
                color: Color(0xFF999999),
              ),
            ),
          ],
        ),
        Row(
          children: <Widget>[
            Icon(
              Icons.favorite,
              size: 16,
              color: Color(0xFFFFC600),
            ),
            Padding(padding: EdgeInsets.only(left: 6)),
            Text(
              data.likes.toString(),
              style: TextStyle(
                fontSize: 15,
                color: Color(0xFF999999),
              ),
            ),
          ],
        ),
        Row(
          children: <Widget>[
            Icon(
              Icons.share,
              size: 16,
              color: Color(0xFF999999),
            ),
            Padding(padding: EdgeInsets.only(left: 6)),
            Text(
              data.shares.toString(),
              style: TextStyle(
                fontSize: 15,
                color: Color(0xFF999999),
              ),
            ),
          ],
        ),
      ],
    ),
  );
}

3.7 小结

通过上面的一个例子,我们成功地把一个看起来复杂的UI界面一步步拆解,将之前提到的组件都用了个遍,并且最终得到了不错的效果。其实,日常开发中90%以上的需求都离不开上述提到的基础组件。因此,只要稍加练习,熟悉了Flutter中的基础组件用法,就已经算是迈出了一大步哦~

这里还有银行卡朋友圈的UI练习例子,由于篇幅原因就不贴代码了,可以去github仓库看。

4. 总结

本文首先介绍了Flutter中构建UI界面最常用的基础组件(容器绝对定位布局文本图片图标)用法。接着,介绍了一个较复杂的UI实战例子。通过对Dom结构的层层拆解,前文提到过的组件得到一个综合运用,也算是巩固了前面所学的概念知识。

不过最后不得不吐槽一句:Flutter的嵌套真的很难受。。。如果不对UI布局进行模块拆分,那绝对是噩梦般的体验。而且不像web/rn开发样式可以单独抽离,Flutter这种将样式当做属性的处理方式,一眼看去真的很难理清dom结构,对于新接手代码的开发人员而言,需要费点时间理解。。。

本文所有代码托管在这儿,也可以关注我的Blog

查看原文

认证与成就

  • 获得 111 次点赞
  • 获得 27 枚徽章 获得 1 枚金徽章, 获得 6 枚银徽章, 获得 20 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-06-02
个人主页被 2.5k 人浏览